diff --git a/app/api/issues.py b/app/api/issues.py index 43ca522..d67204a 100644 --- a/app/api/issues.py +++ b/app/api/issues.py @@ -3,10 +3,19 @@ app/api/issues.py ----------------- Mobile API endpoint for submitting issues from the iPad app. -POST /api/v1/issues +POST /api/v1/issues Creates a new issue record. Accepts a mobile_local_id for idempotency. Triggers notify_by_matrix() and log_action() identically to the web route. + +GET /api/v1/issues/ + Returns current status, severity, description, and assigned_to for an issue. + Inspectors may only fetch issues assigned to them. + +PATCH /api/v1/issues//status + Updates the status of an issue. + Inspectors may only update issues assigned to them. + Admins/directors may update any issue. """ import logging @@ -18,7 +27,7 @@ from app.models.facility import Facility, Area from app.models.inspection import Inspection from app.api.errors import api_ok, api_error from app.api.decorators import jwt_required -from app.utils.audit import log_action, ACTION_CREATE +from app.utils.audit import log_action, ACTION_CREATE, ACTION_UPDATE from app.utils.notifications import notify_by_matrix from app.utils.time_utils import now_eastern @@ -28,6 +37,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'} # ── Create Issue ────────────────────────────────────────────────────────────── @@ -144,3 +154,99 @@ def create_issue(): issue.id, facility.name, severity, user.username) return api_ok({'issue_id': issue.id, 'duplicate': False}) + + +# ── Get Issue Detail ────────────────────────────────────────────────────────── + +@bp.route('/issues/', methods=['GET']) +@jwt_required +def get_issue(issue_id): + """ + Return current status, severity, description, assigned_to, and facility + for a single issue. + + Access: + - admin / director / project_manager : any issue + - inspector : only issues where assigned_to == current user + """ + 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' and issue.assigned_to != user.id: + return api_error('Access denied', 403) + + facility = issue.resolved_facility + return api_ok({ + 'id': issue.id, + 'status': issue.status, + 'severity': issue.severity, + 'description': issue.description, + 'assigned_to': issue.assigned_to, + 'facility_id': facility.id if facility else None, + 'facility_name': facility.name if facility else None, + 'reported_at': issue.reported_at.isoformat() if issue.reported_at else None, + 'resolved_at': issue.resolved_at.isoformat() if issue.resolved_at else None, + }) + + +# ── Update Issue Status ─────────────────────────────────────────────────────── + +@bp.route('/issues//status', methods=['PATCH']) +@jwt_required +def update_issue_status(issue_id): + """ + Update the status of an issue. + + Request JSON + ------------ + { "status": "in_progress" } // one of: open | in_progress | resolved | pending_verification + + Access: + - admin / director : any issue + - inspector : only issues where assigned_to == current user + """ + 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' and issue.assigned_to != user.id: + return api_error('Access denied — you can only update issues assigned to you', 403) + + data = request.get_json(silent=True) or {} + new_status = (data.get('status') or '').strip().lower() + + if new_status not in _VALID_STATUSES: + return api_error( + f'status must be one of: {", ".join(sorted(_VALID_STATUSES))}', 400 + ) + + old_status = issue.status + issue.status = new_status + + if new_status == 'resolved' and not issue.resolved_at: + issue.resolved_at = now_eastern() + issue.sla_notified = None + elif new_status != 'resolved': + issue.resolved_at = None + if old_status == 'resolved': + issue.sla_notified = None + + db.session.commit() + + log_action(ACTION_UPDATE, 'Issue', issue.id, + f'status {old_status} → {new_status}', + f'source=mobile; updated_by={user.username}') + + logger.info('API ISSUES | status_updated | issue_id=%d | %s→%s | user=%s', + issue.id, old_status, new_status, user.username) + + return api_ok({'issue_id': issue.id, 'status': issue.status})