125 lines
4.2 KiB
Python
125 lines
4.2 KiB
Python
"""
|
|
app/api/scheduled.py
|
|
--------------------
|
|
Mobile API endpoint for planned inspection assignments (MT-5, phase43).
|
|
|
|
GET /api/v1/scheduled-inspections
|
|
Returns ACTIVE, PLAN-MODE schedules the caller is responsible for.
|
|
- inspector : only schedules where inspector_id == the caller
|
|
- admin / director / project_manager / auditor : all active plan schedules
|
|
Powers the "Scheduled" section on the iPad Dashboard and My Inspections
|
|
lists. The iPad taps "Start", which opens the normal new-inspection flow
|
|
with the facility + template preselected (client-side); the schedule
|
|
lifecycle (fulfil / roll-forward) continues to be driven by the web app.
|
|
|
|
Why plan-mode only
|
|
------------------
|
|
`mode='auto'` schedules materialise themselves into a real Inspection at
|
|
next_run_at, which the iPad already fetches via /api/v1/inspections. Returning
|
|
them here too would show the same work twice, and "Start" is meaningless for a
|
|
schedule that starts itself. This mirrors the web dashboard panel (MT-4).
|
|
|
|
A plan-mode schedule is a PLAN, not an inspection — see
|
|
app/models/inspection_schedule.py for the full lifecycle.
|
|
"""
|
|
|
|
import logging
|
|
|
|
from flask import Blueprint, request, g
|
|
from app.models.inspection_schedule import InspectionSchedule
|
|
from app.api.errors import api_ok, api_error
|
|
from app.api.decorators import jwt_required
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
bp = Blueprint('api_scheduled', __name__)
|
|
|
|
_ALLOWED_ROLES = {'admin', 'director', 'inspector', 'project_manager', 'auditor'}
|
|
|
|
|
|
def _scheduled_payload(s):
|
|
"""Serialise an InspectionSchedule to the dict returned in list responses.
|
|
|
|
`next_due_date` is the date part of next_run_at — MT reuses next_run_at as
|
|
the due datetime for both modes (see phase43).
|
|
"""
|
|
return {
|
|
'id': s.id,
|
|
'name': s.name,
|
|
'facility_id': s.facility_id,
|
|
'facility_name': s.facility.name if s.facility else None,
|
|
'area_id': s.area_id,
|
|
'area_name': s.area.name if s.area else None,
|
|
'template_id': s.template_id,
|
|
'template_name': s.template.name if s.template else None,
|
|
'inspector_id': s.inspector_id,
|
|
'frequency': s.frequency,
|
|
'frequency_label': s.frequency_label,
|
|
'mode': s.mode,
|
|
'next_due_date': s.next_run_at.date().isoformat() if s.next_run_at else None,
|
|
'is_overdue': s.is_overdue(),
|
|
'notes': s.notes or None,
|
|
}
|
|
|
|
|
|
# ── List Scheduled Inspections ────────────────────────────────────────────────
|
|
|
|
@bp.route('/scheduled-inspections', methods=['GET'])
|
|
@jwt_required
|
|
def list_scheduled():
|
|
"""
|
|
Return active plan-mode scheduled inspections for the authenticated user.
|
|
|
|
Query parameters
|
|
----------------
|
|
limit int Default 100, max 200.
|
|
offset int Default 0.
|
|
|
|
Response 200
|
|
------------
|
|
{
|
|
"ok": true,
|
|
"data": {
|
|
"scheduled": [...],
|
|
"total": 3,
|
|
"limit": 100,
|
|
"offset": 0
|
|
}
|
|
}
|
|
"""
|
|
user = g.api_user
|
|
if user.role not in _ALLOWED_ROLES:
|
|
return api_error('Access denied', 403)
|
|
|
|
try:
|
|
limit = min(int(request.args.get('limit', 100)), 200)
|
|
offset = max(int(request.args.get('offset', 0)), 0)
|
|
except (TypeError, ValueError):
|
|
return api_error('limit and offset must be integers', 400)
|
|
|
|
query = InspectionSchedule.query.filter(
|
|
InspectionSchedule.active.is_(True),
|
|
InspectionSchedule.mode == 'plan',
|
|
)
|
|
|
|
if user.role == 'inspector':
|
|
# Inspectors only see schedules assigned directly to them.
|
|
query = query.filter(InspectionSchedule.inspector_id == user.id)
|
|
|
|
total = query.count()
|
|
rows = (
|
|
query
|
|
.order_by(InspectionSchedule.next_run_at.asc())
|
|
.offset(offset)
|
|
.limit(limit)
|
|
.all()
|
|
)
|
|
|
|
payload = [_scheduled_payload(s) for s in rows]
|
|
|
|
logger.info('API SCHEDULED | list | user=%s | count=%d | total=%d',
|
|
user.username, len(payload), total)
|
|
|
|
return api_ok({'scheduled': payload, 'total': total,
|
|
'limit': limit, 'offset': offset})
|