Files
LT_Janitorial_Quality_Control/app/api/facilities.py
T

181 lines
5.7 KiB
Python

"""
app/api/facilities.py
---------------------
Mobile API endpoints for facilities and areas.
GET /api/v1/facilities
Returns all active facilities accessible to the current user.
Respects customer scoping via get_customer_scope().
Staff roles (admin, director, inspector, project_manager) receive all
active facilities.
GET /api/v1/facilities/<facility_id>/areas
Returns all areas for a specific facility.
Used by the iPad app to populate the area picker when starting an
inspection.
"""
import logging
from flask import Blueprint, g
from app import db
from app.models.facility import Facility, Area
from app.models.project import Project
from app.api.errors import api_ok, api_error
from app.api.decorators import jwt_required
from app.utils.scope import get_customer_scope
logger = logging.getLogger(__name__)
bp = Blueprint('api_facilities', __name__)
def _facility_payload(facility: Facility) -> dict:
"""Serialize a Facility to the dict returned in API responses."""
project_name = facility.project.name if facility.project else None
return {
'id': facility.id,
'name': facility.name,
'address': facility.address or '',
'contact_person': facility.contact_person or '',
'contact_phone': facility.contact_phone or '',
'project_id': facility.project_id,
'project_name': project_name,
'is_active': facility.active,
}
def _area_payload(area: Area) -> dict:
"""Serialize an Area to the dict returned in API responses."""
return {
'id': area.id,
'facility_id': area.facility_id,
'name': area.name,
'area_type': area.area_type or '',
}
# ── Facilities List ───────────────────────────────────────────────────────────
@bp.route('/facilities', methods=['GET'])
@jwt_required
def list_facilities():
"""
Return all active facilities the current user has access to.
Staff roles (admin, director, inspector, project_manager) receive all
active facilities across all contracts.
Customer role receives only their scoped facilities (via
CustomerAssignment records).
Response 200
------------
{
"ok": true,
"data": {
"facilities": [
{
"id": 7,
"name": "Main Office Building",
"address": "123 Corporate Dr",
"contact_person": "Jane Smith",
"contact_phone": "555-1234",
"project_id": 2,
"project_name": "Corporate Cleaning Contract",
"is_active": true
}
],
"count": 1
}
}
"""
user = g.api_user
# Customer role: honour facility-level scoping
facility_ids = get_customer_scope(user)
if facility_ids is not None:
# Customer — scope to assigned facilities only
if not facility_ids:
logger.info('API FACILITIES | user=%s | role=customer | no_assignments',
user.username)
return api_ok({'facilities': [], 'count': 0})
facilities = (
Facility.query
.filter(
Facility.id.in_(facility_ids),
Facility.active == True, # noqa: E712
)
.order_by(Facility.name)
.all()
)
else:
# Internal staff — all active facilities
facilities = (
Facility.query
.filter(Facility.active == True) # noqa: E712
.order_by(Facility.name)
.all()
)
payload = [_facility_payload(f) for f in facilities]
logger.info('API FACILITIES | list | user=%s | role=%s | count=%d',
user.username, user.role, len(payload))
return api_ok({'facilities': payload, 'count': len(payload)})
# ── Areas for a Facility ──────────────────────────────────────────────────────
@bp.route('/facilities/<int:facility_id>/areas', methods=['GET'])
@jwt_required
def list_areas(facility_id):
"""
Return all areas for the given facility.
Used by the iPad app to populate the area picker when starting an
inspection. Customer users are validated against their scope before
the areas are returned.
Response 200
------------
{
"ok": true,
"data": {
"facility_id": 7,
"areas": [
{ "id": 12, "facility_id": 7, "name": "Main Lobby", "area_type": "lobby" }
],
"count": 1
}
}
"""
user = g.api_user
facility = db.session.get(Facility, facility_id)
if facility is None or not facility.active:
return api_error('Facility not found', 404)
# Customer scope validation — ensure the customer is assigned to this facility
facility_ids = get_customer_scope(user)
if facility_ids is not None and facility_id not in facility_ids:
logger.warning('API FACILITIES/AREAS | access denied | user=%s | facility_id=%d',
user.username, facility_id)
return api_error('Access denied', 403)
areas = (
Area.query
.filter_by(facility_id=facility_id)
.order_by(Area.name)
.all()
)
payload = [_area_payload(a) for a in areas]
logger.info('API FACILITIES/AREAS | user=%s | facility_id=%d | count=%d',
user.username, facility_id, len(payload))
return api_ok({'facility_id': facility_id, 'areas': payload, 'count': len(payload)})