Jul 13 - Update web API to support iOS app on scheduled inspection and issue's handler
This commit is contained in:
@@ -44,6 +44,7 @@ bp = Blueprint('api_issues', __name__)
|
||||
_ALLOWED_ROLES = {'admin', 'director', 'inspector', 'project_manager'}
|
||||
_VALID_SEVERITY = {'low', 'medium', 'high', 'critical'}
|
||||
_VALID_STATUSES = {'open', 'in_progress', 'resolved', 'pending_verification'}
|
||||
_VALID_HANDLERS = {'internal', 'facility', 'vendor'}
|
||||
_UUID_RE = re.compile(
|
||||
r'^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$',
|
||||
re.IGNORECASE,
|
||||
@@ -81,6 +82,18 @@ def _issue_payload(issue):
|
||||
'area_name': issue.area.name if issue.area else None,
|
||||
# Assigned-to display name — set when a director assigns the issue to a user.
|
||||
'assigned_to_name': issue.assigned_user.display_name if issue.assigned_user else None,
|
||||
# ── Handler ("Handled By", phase35) ───────────────────────────────
|
||||
# handler_type categorises WHO resolves the issue:
|
||||
# internal = our staff (assigned_to) facility = facility's own staff
|
||||
# vendor = external contractor
|
||||
'handler_type': issue.handler_type or 'internal',
|
||||
'handler_label': issue.handler_label,
|
||||
'facility_handler_name': issue.facility_handler_name or None,
|
||||
'facility_handler_contact': issue.facility_handler_contact or None,
|
||||
'facility_handler_notes': issue.facility_handler_notes or None,
|
||||
'vendor_name': issue.vendor_name or None,
|
||||
'vendor_contact': issue.vendor_contact or None,
|
||||
'vendor_notes': issue.vendor_notes or None,
|
||||
}
|
||||
|
||||
|
||||
@@ -373,6 +386,86 @@ def update_issue_status(issue_id):
|
||||
|
||||
return api_ok({'issue_id': issue.id, 'status': issue.status})
|
||||
|
||||
|
||||
# ── Update Issue Handler ("Handled By") ───────────────────────────────────────
|
||||
|
||||
@bp.route('/issues/<int:issue_id>/handler', methods=['PATCH'])
|
||||
@jwt_required
|
||||
def update_issue_handler(issue_id):
|
||||
"""
|
||||
Set who handles an issue ("Handled By") from the mobile app.
|
||||
|
||||
Unlike the web form (which limits handler edits to admin/director/PM),
|
||||
the iPad allows the assigned inspector to set the handler from the field,
|
||||
scoped to issues at their assigned facilities.
|
||||
|
||||
Request JSON
|
||||
------------
|
||||
{
|
||||
"handler_type": "internal" | "facility" | "vendor",
|
||||
"facility_handler_name": "...", // optional (facility handler)
|
||||
"facility_handler_contact": "...", // optional
|
||||
"facility_handler_notes": "...", // optional
|
||||
"vendor_name": "...", // optional (vendor handler)
|
||||
"vendor_contact": "...", // optional
|
||||
"vendor_notes": "..." // optional
|
||||
}
|
||||
|
||||
Only keys present in the body are updated; empty strings clear a field.
|
||||
handler_type is required.
|
||||
|
||||
Access:
|
||||
- admin / director / project_manager : any issue
|
||||
- inspector : only issues at their assigned facilities
|
||||
"""
|
||||
user = g.api_user
|
||||
if user.role not in _ALLOWED_ROLES:
|
||||
return api_error('Access denied', 403)
|
||||
|
||||
issue = db.session.get(Issue, issue_id)
|
||||
if issue is None:
|
||||
return api_error('Issue not found', 404)
|
||||
|
||||
if user.role == 'inspector':
|
||||
fids = get_inspector_scope(user)
|
||||
facility = issue.resolved_facility
|
||||
if not fids or not facility or facility.id not in fids:
|
||||
return api_error('Access denied', 403)
|
||||
|
||||
data = request.get_json(silent=True) or {}
|
||||
handler = (data.get('handler_type') or '').strip().lower()
|
||||
|
||||
if handler not in _VALID_HANDLERS:
|
||||
return api_error(
|
||||
f'handler_type must be one of: {", ".join(sorted(_VALID_HANDLERS))}', 400
|
||||
)
|
||||
|
||||
old_handler = issue.handler_type or 'internal'
|
||||
issue.handler_type = handler
|
||||
|
||||
# Update only the detail fields that were supplied. Empty string clears
|
||||
# the field (stored as NULL); a missing key leaves the field untouched.
|
||||
_text_fields = (
|
||||
'facility_handler_name', 'facility_handler_contact', 'facility_handler_notes',
|
||||
'vendor_name', 'vendor_contact', 'vendor_notes',
|
||||
)
|
||||
for field in _text_fields:
|
||||
if field in data:
|
||||
val = (data.get(field) or '').strip()
|
||||
setattr(issue, field, val or None)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
log_action(ACTION_UPDATE, 'Issue', issue.id,
|
||||
f'handler {old_handler} → {handler}',
|
||||
f'source=mobile; updated_by={user.username}')
|
||||
|
||||
logger.info('API ISSUES | handler_updated | issue_id=%d | %s→%s | user=%s',
|
||||
issue.id, old_handler, handler, user.username)
|
||||
|
||||
return api_ok({'issue_id': issue.id, 'handler_type': issue.handler_type})
|
||||
|
||||
|
||||
# ── Update Issue Photos (mobile) ──────────────────────────────────────────────
|
||||
|
||||
@bp.route('/issues/<int:issue_id>/photos', methods=['PATCH'])
|
||||
|
||||
Reference in New Issue
Block a user