| """ |
| FastAPI Server for Audit Checklist Application |
| ============================================= |
| |
| This server provides a REST API for managing audit checklists with MongoDB integration. |
| It supports CRUD operations for checklist data and user management. |
| |
| Features: |
| - MongoDB Atlas integration with async Motor driver |
| - CORS support for React frontend |
| - Comprehensive error handling |
| - Health check endpoint |
| - Environment-based configuration |
| |
| Author: Audit Checklist Team |
| Version: 1.0.0 |
| """ |
|
|
| import os |
| import sys |
| import base64 |
| from datetime import datetime |
| from typing import Optional, List, Dict, Any, Union |
| import logging |
|
|
| |
| from fastapi import FastAPI, HTTPException, status, UploadFile, File |
| from fastapi.middleware.cors import CORSMiddleware |
| from fastapi.responses import JSONResponse |
|
|
| |
| from pydantic import BaseModel, Field |
|
|
| |
| import motor.motor_asyncio |
| from bson import ObjectId |
|
|
| |
| from dotenv import load_dotenv |
|
|
| |
| load_dotenv('mongodb.env') |
|
|
| |
| logging.basicConfig( |
| level=logging.INFO, |
| format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' |
| ) |
| logger = logging.getLogger(__name__) |
|
|
| |
| |
| |
|
|
| |
| MONGODB_URI = os.getenv('MONGODB_URI', 'mongodb://localhost:27017/audit_checklist') |
| PORT = int(os.getenv('PORT', 8000)) |
| CORS_ORIGIN = os.getenv('CORS_ORIGIN', '*') |
|
|
| |
| app = FastAPI( |
| title="Audit Checklist API", |
| description="REST API for managing audit checklists with MongoDB integration", |
| version="1.0.0", |
| docs_url="/docs", |
| redoc_url="/redoc" |
| ) |
|
|
| |
| |
| cors_origins = [CORS_ORIGIN] if CORS_ORIGIN != "*" else ["*"] |
|
|
| app.add_middleware( |
| CORSMiddleware, |
| allow_origins=cors_origins, |
| allow_credentials=True, |
| allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], |
| allow_headers=["*"], |
| ) |
|
|
| |
| client = None |
| db = None |
|
|
| async def connect_to_mongodb(): |
| """ |
| Establish connection to MongoDB Atlas |
| |
| This function creates an async connection to MongoDB using the Motor driver. |
| It handles connection errors gracefully and logs the status. |
| """ |
| global client, db |
| try: |
| client = motor.motor_asyncio.AsyncIOMotorClient(MONGODB_URI) |
| db = client.audit_checklist |
| |
| |
| await client.admin.command('ping') |
| logger.info("Successfully connected to MongoDB Atlas") |
| |
| |
| await create_database_indexes() |
| |
| except Exception as e: |
| logger.error(f"Failed to connect to MongoDB: {e}") |
| raise e |
|
|
| async def create_database_indexes(): |
| """ |
| Create database indexes for optimal query performance |
| |
| This function creates indexes on frequently queried fields to improve |
| database performance and query speed. |
| """ |
| try: |
| |
| await db.checklists.create_index("userId", unique=False) |
| |
| |
| await db.checklists.create_index("createdAt", unique=False) |
| |
| logger.info("Database indexes created successfully") |
| |
| except Exception as e: |
| logger.warning(f"Could not create indexes: {e}") |
|
|
| async def close_mongodb_connection(): |
| """Close MongoDB connection gracefully""" |
| global client |
| if client: |
| client.close() |
| logger.info("MongoDB connection closed") |
|
|
| |
| |
| |
|
|
| class ChecklistItem(BaseModel): |
| """ |
| Model for individual checklist items |
| |
| Attributes: |
| id: Unique identifier for the item |
| requirement: Description of the requirement to be checked |
| compliance: Compliance status (N/A, Compliant, Non-Compliant) |
| deviation: Description of any deviation found |
| action: Action taken to address the issue |
| checkedAt: Timestamp when the item was checked |
| checkedBy: Name of the person who checked the item |
| """ |
| id: str = Field(..., description="Unique identifier for the checklist item") |
| requirement: str = Field(..., description="Description of the requirement") |
| compliance: str = Field(default="N/A", description="Compliance status: N/A, Compliant, or Non-Compliant") |
| deviation: str = Field(default="", description="Description of any deviation found") |
| action: str = Field(default="", description="Action taken to address the issue") |
| checkedAt: Optional[datetime] = Field(default=None, description="Timestamp when item was checked") |
| checkedBy: str = Field(default="", description="Name of the person who checked the item") |
|
|
| class ChecklistSection(BaseModel): |
| """ |
| Model for checklist sections containing multiple items |
| |
| Attributes: |
| id: Unique identifier for the section |
| title: Display title of the section |
| icon: Icon identifier for UI display |
| items: List of checklist items in this section |
| """ |
| id: str = Field(..., description="Unique identifier for the section") |
| title: str = Field(..., description="Display title of the section") |
| icon: str = Field(..., description="Icon identifier for UI display") |
| items: List[ChecklistItem] = Field(default=[], description="List of checklist items") |
|
|
| class Metadata(BaseModel): |
| """ |
| Metadata model for checklist information |
| """ |
| userName: Optional[str] = Field(default=None, description="Name of the user who saved the checklist") |
| savedAt: Optional[str] = Field(default=None, description="ISO timestamp when saved") |
| savedAtFormatted: Optional[str] = Field(default=None, description="Formatted timestamp when saved") |
| userId: Optional[str] = Field(default=None, description="User ID") |
| version: Optional[str] = Field(default="1.0", description="Version of the checklist") |
|
|
| class ChecklistData(BaseModel): |
| """ |
| Complete checklist data model |
| |
| Attributes: |
| userId: Unique identifier for the user |
| title: Title of the checklist |
| sections: List of sections in the checklist |
| totalItems: Total number of items across all sections |
| completedItems: Number of completed items |
| nonCompliantItems: Number of non-compliant items |
| complianceScore: Calculated compliance percentage |
| verificationDate: Date when the checklist was verified |
| createdAt: Timestamp when the checklist was created |
| updatedAt: Timestamp when the checklist was last updated |
| imageData: Base64 encoded image data (stored when compliance < 100%) |
| imageType: MIME type of the image (e.g., image/jpeg, image/png) |
| """ |
| userId: str = Field(..., description="Unique identifier for the user") |
| title: str = Field(..., description="Title of the checklist") |
| sections: List[ChecklistSection] = Field(default=[], description="List of checklist sections") |
| totalItems: Optional[int] = Field(default=0, description="Total number of items") |
| completedItems: Optional[int] = Field(default=0, description="Number of completed items") |
| nonCompliantItems: Optional[int] = Field(default=0, description="Number of non-compliant items") |
| complianceScore: Optional[float] = Field(default=0.0, description="Compliance percentage score") |
| verificationDate: Optional[datetime] = Field(default=None, description="Date of verification") |
| createdAt: Optional[datetime] = Field(default=None, description="Creation timestamp") |
| updatedAt: Optional[datetime] = Field(default=None, description="Last update timestamp") |
| metadata: Optional[Metadata] = Field(default=None, description="Additional metadata including user information") |
| imageData: Optional[str] = Field(default=None, description="Base64 encoded image data for non-compliant checklists") |
| imageType: Optional[str] = Field(default=None, description="MIME type of the image") |
| |
| class Config: |
| |
| extra = "allow" |
|
|
| class ChecklistResponse(BaseModel): |
| """ |
| Standard API response model for checklist operations |
| |
| Attributes: |
| success: Boolean indicating if the operation was successful |
| data: The checklist data (if successful) |
| message: Optional message describing the result |
| error: Error message (if unsuccessful) |
| """ |
| success: bool = Field(..., description="Whether the operation was successful") |
| data: Optional[Union[ChecklistData, Dict[str, Any]]] = Field(default=None, description="Checklist data") |
| message: Optional[str] = Field(default=None, description="Success message") |
| error: Optional[str] = Field(default=None, description="Error message") |
|
|
| class HealthResponse(BaseModel): |
| """Health check response model""" |
| status: str = Field(..., description="Health status") |
| timestamp: datetime = Field(..., description="Current timestamp") |
| database: str = Field(..., description="Database connection status") |
| version: str = Field(default="1.0.0", description="API version") |
|
|
| |
| |
| |
|
|
| def calculate_checklist_metrics(checklist_data: Dict[str, Any]) -> Dict[str, Any]: |
| """ |
| Calculate compliance metrics for a checklist |
| |
| Args: |
| checklist_data: Dictionary containing checklist data |
| |
| Returns: |
| Dictionary with calculated metrics (totalItems, completedItems, nonCompliantItems, complianceScore) |
| """ |
| total_items = 0 |
| completed_items = 0 |
| non_compliant_items = 0 |
| |
| |
| for section in checklist_data.get('sections', []): |
| for item in section.get('items', []): |
| total_items += 1 |
| |
| |
| if item.get('compliance') != 'N/A': |
| completed_items += 1 |
| |
| |
| if item.get('compliance') == 'Non-Compliant': |
| non_compliant_items += 1 |
| |
| |
| compliance_score = (completed_items / total_items * 100) if total_items > 0 else 0 |
| |
| return { |
| 'totalItems': total_items, |
| 'completedItems': completed_items, |
| 'nonCompliantItems': non_compliant_items, |
| 'complianceScore': round(compliance_score, 2) |
| } |
|
|
| def serialize_checklist(checklist_doc: Dict[str, Any]) -> Dict[str, Any]: |
| """ |
| Serialize MongoDB document to JSON-serializable format |
| |
| Args: |
| checklist_doc: MongoDB document |
| |
| Returns: |
| Dictionary with ObjectId converted to string |
| """ |
| if checklist_doc and '_id' in checklist_doc: |
| checklist_doc['_id'] = str(checklist_doc['_id']) |
| |
| |
| for field in ['createdAt', 'updatedAt', 'verificationDate']: |
| if field in checklist_doc and checklist_doc[field]: |
| |
| if hasattr(checklist_doc[field], 'isoformat'): |
| checklist_doc[field] = checklist_doc[field].isoformat() |
| |
| |
| for section in checklist_doc.get('sections', []): |
| for item in section.get('items', []): |
| if 'checkedAt' in item and item['checkedAt']: |
| |
| if hasattr(item['checkedAt'], 'isoformat'): |
| item['checkedAt'] = item['checkedAt'].isoformat() |
| |
| return checklist_doc |
|
|
| |
| |
| |
|
|
| @app.on_event("startup") |
| async def startup_event(): |
| """ |
| Application startup event handler |
| |
| This function is called when the FastAPI application starts up. |
| It initializes the MongoDB connection and sets up the database. |
| """ |
| logger.info("Starting Audit Checklist API server...") |
| await connect_to_mongodb() |
| logger.info(f"Server ready on port {PORT}") |
|
|
| @app.on_event("shutdown") |
| async def shutdown_event(): |
| """ |
| Application shutdown event handler |
| |
| This function is called when the FastAPI application shuts down. |
| It gracefully closes the MongoDB connection. |
| """ |
| logger.info("Shutting down Audit Checklist API server...") |
| await close_mongodb_connection() |
|
|
| @app.get("/", response_model=Dict[str, str]) |
| async def root(): |
| """ |
| Root endpoint providing basic API information |
| |
| Returns: |
| Dictionary with API name and version |
| """ |
| return { |
| "message": "Audit Checklist API", |
| "version": "1.0.0", |
| "docs": "/docs", |
| "health": "/health" |
| } |
|
|
| @app.get("/health", response_model=HealthResponse) |
| async def health_check(): |
| """ |
| Health check endpoint for monitoring and load balancers |
| |
| This endpoint checks the status of the API server and database connection. |
| It's useful for health monitoring, load balancers, and DevOps automation. |
| |
| Returns: |
| HealthResponse with current status information |
| """ |
| try: |
| |
| await client.admin.command('ping') |
| db_status = "connected" |
| except Exception as e: |
| logger.error(f"Database health check failed: {e}") |
| db_status = "disconnected" |
| |
| return HealthResponse( |
| status="healthy" if db_status == "connected" else "unhealthy", |
| timestamp=datetime.utcnow(), |
| database=db_status, |
| version="1.0.0" |
| ) |
|
|
| @app.get("/api/checklist/{user_id}", response_model=ChecklistResponse) |
| async def get_checklist(user_id: str): |
| """ |
| Retrieve checklist data for a specific user |
| |
| This endpoint fetches the checklist data for a given user ID. If no checklist |
| exists for the user, it creates a new one with the default structure. |
| |
| Args: |
| user_id: Unique identifier for the user |
| |
| Returns: |
| ChecklistResponse containing the checklist data |
| |
| Raises: |
| HTTPException: If database operation fails |
| """ |
| try: |
| logger.info(f"Fetching checklist for user: {user_id}") |
| |
| |
| |
| checklist_doc = await db.checklists.find_one( |
| {"userId": user_id}, |
| sort=[("createdAt", -1)] |
| ) |
| |
| if not checklist_doc: |
| |
| logger.info(f"No checklist found for user {user_id}, creating new one") |
| |
| |
| default_checklist = { |
| "userId": user_id, |
| "title": "Checklist di Audit Operativo (38 Controlli)", |
| "sections": [ |
| { |
| "id": "S1", |
| "title": "1. PERSONALE E IGIENE (6 Controlli)", |
| "icon": "Users", |
| "items": [ |
| {"id": "I1.1", "requirement": "Indumenti da lavoro puliti (divisa, grembiule).", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}, |
| {"id": "I1.2", "requirement": "Scarpe antinfortunistiche / Calzature pulite.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}, |
| {"id": "I1.3", "requirement": "Cuffie e/o Retine per capelli indossate correttamente.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}, |
| {"id": "I1.4", "requirement": "Assenza di gioielli, piercing visibili, unghie lunghe.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}, |
| {"id": "I1.5", "requirement": "Lavaggio mani documentato all'ingresso/reingresso.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}, |
| {"id": "I1.6", "requirement": "Assenza di cibo/bevande non autorizzate nelle aree produttive.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""} |
| ] |
| }, |
| { |
| "id": "S2", |
| "title": "2. STRUTTURE E IMPIANTI (6 Controlli)", |
| "icon": "Building", |
| "items": [ |
| {"id": "I2.1", "requirement": "Illuminazione adeguata e funzionante in tutte le aree.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}, |
| {"id": "I2.2", "requirement": "Porte esterne/interne in buone condizioni e chiuse correttamente (sigillatura).", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}, |
| {"id": "I2.3", "requirement": "Integrità di pavimenti, pareti e soffitti (assenza di crepe/danni).", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}, |
| {"id": "I2.4", "requirement": "Controllo vetri / lampade protette o anti-frantumazione.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}, |
| {"id": "I2.5", "requirement": "Condizioni igieniche dei servizi igienici e spogliatoi (pulizia e dotazioni).", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}, |
| {"id": "I2.6", "requirement": "Ventilazione e aspirazione funzionanti, pulite e non ostruite.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""} |
| ] |
| }, |
| { |
| "id": "S3", |
| "title": "3. GESTIONE E IGIENE AMBIENTALE (4 Controlli)", |
| "icon": "Package", |
| "items": [ |
| {"id": "I3.1", "requirement": "Contenitori dei rifiuti puliti, chiusi e identificati.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}, |
| {"id": "I3.2", "requirement": "Separazione corretta dei rifiuti (es. umido, secco, plastica).", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}, |
| {"id": "I3.3", "requirement": "Area di stoccaggio rifiuti (interna ed esterna) ordinata e sanificata.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}, |
| {"id": "I3.4", "requirement": "Frequenza di rimozione dei rifiuti adeguata a prevenire accumuli.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""} |
| ] |
| }, |
| { |
| "id": "S4", |
| "title": "4. CONTROLLO PROCESSO E QUALITÀ (6 Controlli)", |
| "icon": "Settings", |
| "items": [ |
| {"id": "I4.1", "requirement": "Monitoraggio e registrazione dei Punti Critici di Controllo (CCP).", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}, |
| {"id": "I4.2", "requirement": "Procedure di Buona Fabbricazione (GMP) rispettate (es. pulizia prima dell'avvio).", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}, |
| {"id": "I4.3", "requirement": "Verifica e calibrazione del Metal Detector eseguita e registrata.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}, |
| {"id": "I4.4", "requirement": "Corretta identificazione dei lotti di materie prime e semilavorati.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}, |
| {"id": "I4.5", "requirement": "Rispettate le temperature di stoccaggio dei prodotti finiti e intermedi.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}, |
| {"id": "I4.6", "requirement": "Corretta gestione, etichettatura e isolamento dei prodotti non conformi.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""} |
| ] |
| }, |
| { |
| "id": "S5", |
| "title": "5. CONTROLLO INFESTANTI (5 Controlli)", |
| "icon": "Shield", |
| "items": [ |
| {"id": "I5.1", "requirement": "Assenza di tracce visibili di roditori, insetti o uccelli.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}, |
| {"id": "I5.2", "requirement": "Stazioni di monitoraggio/trappole numerate, integre e registrate.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}, |
| {"id": "I5.3", "requirement": "Mappe e registri delle trappole aggiornati e disponibili.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}, |
| {"id": "I5.4", "requirement": "Barriere fisiche (es. reti, spazzole) anti-intrusione in buone condizioni.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}, |
| {"id": "I5.5", "requirement": "Prodotti chimici di controllo infestanti stoccati in modo sicuro e separato.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""} |
| ] |
| }, |
| { |
| "id": "S6", |
| "title": "6. MANUTENZIONE E VETRI (6 Controlli)", |
| "icon": "Settings", |
| "items": [ |
| {"id": "I6.1", "requirement": "Piano di manutenzione preventiva e correttiva rispettato e registrato.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}, |
| {"id": "I6.2", "requirement": "Lubrificanti e oli utilizzati autorizzati per uso alimentare (se richiesto).", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}, |
| {"id": "I6.3", "requirement": "Registri di calibrazione/verifica degli strumenti di misura critici aggiornati.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}, |
| {"id": "I6.4", "requirement": "Gestione dei vetri rotti (registri rotture) aggiornata e conforme alla procedura.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}, |
| {"id": "I6.5", "requirement": "Assenza di perdite, gocciolamenti o cavi scoperti da impianti/macchinari.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}, |
| {"id": "I6.6", "requirement": "Area di deposito utensili e pezzi di ricambio ordinata e pulita.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""} |
| ] |
| }, |
| { |
| "id": "S7", |
| "title": "7. DOCUMENTAZIONE E FORMAZIONE (5 Controlli)", |
| "icon": "FileText", |
| "items": [ |
| {"id": "I7.1", "requirement": "Documentazione di processo (procedure, istruzioni) aggiornata e disponibile.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}, |
| {"id": "I7.2", "requirement": "Registri di formazione del personale aggiornati e verificabili.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}, |
| {"id": "I7.3", "requirement": "Certificazioni e attestati del personale validi e aggiornati.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}, |
| {"id": "I7.4", "requirement": "Controllo documenti (versioni, distribuzione, obsolescenza) conforme.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}, |
| {"id": "I7.5", "requirement": "Archiviazione e conservazione documenti secondo i requisiti normativi.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""} |
| ] |
| } |
| ], |
| "totalItems": 38, |
| "completedItems": 0, |
| "nonCompliantItems": 0, |
| "complianceScore": 0.0, |
| "verificationDate": None, |
| "createdAt": datetime.utcnow(), |
| "updatedAt": datetime.utcnow() |
| } |
| |
| |
| result = await db.checklists.insert_one(default_checklist) |
| checklist_doc = await db.checklists.find_one({"_id": result.inserted_id}) |
| logger.info(f"Created new checklist for user {user_id}") |
| |
| |
| serialized_checklist = serialize_checklist(checklist_doc) |
| |
| return ChecklistResponse( |
| success=True, |
| data=ChecklistData(**serialized_checklist), |
| message="Checklist retrieved successfully" |
| ) |
| |
| except Exception as e: |
| logger.error(f"Error retrieving checklist for user {user_id}: {e}") |
| raise HTTPException( |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
| detail=f"Failed to retrieve checklist: {str(e)}" |
| ) |
|
|
| @app.put("/api/checklist/{user_id}", response_model=ChecklistResponse) |
| async def save_checklist(user_id: str, checklist_data: dict): |
| """ |
| Save or update checklist data for a specific user |
| |
| This endpoint saves the checklist data to the database. It calculates |
| compliance metrics and updates the timestamp. |
| |
| Args: |
| user_id: Unique identifier for the user |
| checklist_data: The checklist data to save |
| |
| Returns: |
| ChecklistResponse confirming the save operation |
| |
| Raises: |
| HTTPException: If database operation fails |
| """ |
| try: |
| logger.info(f"Saving checklist for user: {user_id}") |
| |
| |
| checklist_dict = checklist_data.copy() |
| |
| |
| metrics = calculate_checklist_metrics(checklist_dict) |
| checklist_dict.update(metrics) |
| |
| |
| compliance_score = metrics.get('complianceScore', 0.0) |
| if compliance_score < 100.0 and 'collectedImages' in checklist_dict and checklist_dict['collectedImages']: |
| |
| collected_images = checklist_dict['collectedImages'] |
| logger.info(f"Storing {len(collected_images)} images for non-compliant checklist (score: {compliance_score}%)") |
| |
| |
| checklist_dict['imageData'] = collected_images |
| checklist_dict['imageType'] = 'multiple_items' |
| elif compliance_score < 100.0: |
| logger.info(f"No image data provided for non-compliant checklist (score: {compliance_score}%)") |
| |
| |
| checklist_dict['updatedAt'] = datetime.utcnow() |
| |
| |
| existing_checklist = await db.checklists.find_one({"userId": user_id}) |
| |
| if existing_checklist: |
| |
| checklist_dict['createdAt'] = existing_checklist.get('createdAt', datetime.utcnow()) |
| |
| |
| if '_id' in checklist_dict: |
| del checklist_dict['_id'] |
| |
| result = await db.checklists.update_one( |
| {"userId": user_id}, |
| {"$set": checklist_dict} |
| ) |
| |
| if result.modified_count > 0: |
| logger.info(f"Updated existing checklist for user {user_id}") |
| message = "Checklist updated successfully" |
| else: |
| logger.error(f"Failed to update checklist for user {user_id}") |
| raise HTTPException( |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
| detail="Failed to update checklist" |
| ) |
| else: |
| |
| checklist_dict['createdAt'] = datetime.utcnow() |
| |
| |
| if '_id' in checklist_dict: |
| del checklist_dict['_id'] |
| |
| result = await db.checklists.insert_one(checklist_dict) |
| |
| if result.inserted_id: |
| logger.info(f"Created new checklist for user {user_id}") |
| message = "Checklist created successfully" |
| else: |
| logger.error(f"Failed to create checklist for user {user_id}") |
| raise HTTPException( |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
| detail="Failed to save checklist" |
| ) |
| |
| |
| if existing_checklist: |
| |
| updated_checklist = await db.checklists.find_one({"userId": user_id}) |
| serialized_checklist = serialize_checklist(updated_checklist) |
| else: |
| |
| created_checklist = await db.checklists.find_one({"_id": result.inserted_id}) |
| serialized_checklist = serialize_checklist(created_checklist) |
| |
| return ChecklistResponse( |
| success=True, |
| data=serialized_checklist, |
| message=message |
| ) |
| |
| except Exception as e: |
| logger.error(f"Error saving checklist for user {user_id}: {e}") |
| raise HTTPException( |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
| detail=f"Failed to save checklist: {str(e)}" |
| ) |
|
|
| @app.get("/api/checklists/by-user/{user_name}", response_model=Dict[str, Any]) |
| async def get_checklists_by_user_name(user_name: str): |
| """ |
| Retrieve all checklists for a specific user name |
| |
| Args: |
| user_name: The name of the user to retrieve checklists for |
| |
| Returns: |
| Dictionary containing list of checklists for the user |
| |
| Raises: |
| HTTPException: If database operation fails |
| """ |
| try: |
| logger.info(f"Retrieving checklists for user: {user_name}") |
| |
| |
| cursor = db.checklists.find({"metadata.userName": user_name}) |
| checklists = [] |
| |
| async for checklist_doc in cursor: |
| serialized_checklist = serialize_checklist(checklist_doc) |
| checklists.append(serialized_checklist) |
| |
| return { |
| "success": True, |
| "data": checklists, |
| "count": len(checklists), |
| "user_name": user_name |
| } |
| |
| except Exception as e: |
| logger.error(f"Error retrieving checklists for user {user_name}: {e}") |
| raise HTTPException( |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
| detail=f"Failed to retrieve checklists for user: {str(e)}" |
| ) |
|
|
| @app.post("/api/checklist/{user_id}/image") |
| async def upload_checklist_image(user_id: str, image: UploadFile = File(...)): |
| """ |
| Upload an image for a checklist (for non-compliant cases) |
| |
| Args: |
| user_id: Unique identifier for the user |
| image: The image file to upload |
| |
| Returns: |
| Dictionary containing the base64 encoded image data and metadata |
| """ |
| try: |
| logger.info(f"Uploading image for user: {user_id}") |
| |
| |
| allowed_types = ["image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp"] |
| if image.content_type not in allowed_types: |
| raise HTTPException( |
| status_code=status.HTTP_400_BAD_REQUEST, |
| detail=f"Invalid image type. Allowed types: {', '.join(allowed_types)}" |
| ) |
| |
| |
| image_data = await image.read() |
| |
| |
| base64_data = base64.b64encode(image_data).decode('utf-8') |
| |
| |
| data_url = f"data:{image.content_type};base64,{base64_data}" |
| |
| logger.info(f"Image uploaded successfully for user {user_id}, size: {len(image_data)} bytes") |
| |
| return { |
| "success": True, |
| "data": { |
| "imageData": base64_data, |
| "imageType": image.content_type, |
| "dataUrl": data_url, |
| "size": len(image_data), |
| "filename": image.filename |
| }, |
| "message": "Image uploaded successfully" |
| } |
| |
| except HTTPException: |
| raise |
| except Exception as e: |
| logger.error(f"Error uploading image for user {user_id}: {e}") |
| raise HTTPException( |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
| detail=f"Failed to upload image: {str(e)}" |
| ) |
|
|
| async def ensure_default_template_exists(): |
| """ |
| Ensure the default template exists in the database. |
| Creates it if it doesn't exist. |
| """ |
| try: |
| logger.info("Checking if default template exists...") |
| |
| template = await db.checklist_templates.find_one({ |
| "$or": [ |
| {"templateId": "default"}, |
| {"templateId": "default-audit-checklist"} |
| ] |
| }) |
| |
| if not template: |
| logger.info("Default template not found, creating it now...") |
| |
| |
| default_template = { |
| "templateId": "default", |
| "title": "Checklist di Audit Operativo", |
| "sections": [ |
| { |
| "id": "S1", |
| "title": "1. PERSONALE E IGIENE", |
| "icon": "Users", |
| "items": [ |
| {"id": "I1.1", "requirement": "Indumenti da lavoro puliti (divisa, grembiule)."}, |
| {"id": "I1.2", "requirement": "Scarpe antinfortunistiche / Calzature pulite."}, |
| {"id": "I1.3", "requirement": "Cuffie e/o Retine per capelli indossate correttamente."}, |
| {"id": "I1.4", "requirement": "Assenza di gioielli, piercing visibili, unghie lunghe."}, |
| {"id": "I1.5", "requirement": "Lavaggio mani documentato all'ingresso/reingresso."}, |
| {"id": "I1.6", "requirement": "Assenza di cibo/bevande non autorizzate nelle aree produttive."}, |
| ], |
| }, |
| { |
| "id": "S2", |
| "title": "2. STRUTTURE E IMPIANTI", |
| "icon": "Building", |
| "items": [ |
| {"id": "I2.1", "requirement": "Illuminazione adeguata e funzionante in tutte le aree."}, |
| {"id": "I2.2", "requirement": "Porte esterne/interne in buone condizioni e chiuse correttamente (sigillatura)."}, |
| {"id": "I2.3", "requirement": "Integrità di pavimenti, pareti e soffitti (assenza di crepe/danni)."}, |
| {"id": "I2.4", "requirement": "Controllo vetri / lampade protette o anti-frantumazione."}, |
| {"id": "I2.5", "requirement": "Condizioni igieniche dei servizi igienici e spogliatoi (pulizia e dotazioni)."}, |
| {"id": "I2.6", "requirement": "Ventilazione e aspirazione funzionanti, pulite e non ostruite."}, |
| ], |
| }, |
| { |
| "id": "S3", |
| "title": "3. GESTIONE E IGIENE AMBIENTALE", |
| "icon": "Package", |
| "items": [ |
| {"id": "I3.1", "requirement": "Contenitori dei rifiuti puliti, chiusi e identificati."}, |
| {"id": "I3.2", "requirement": "Separazione corretta dei rifiuti (es. umido, secco, plastica)."}, |
| {"id": "I3.3", "requirement": "Area di stoccaggio rifiuti (interna ed esterna) ordinata e sanificata."}, |
| {"id": "I3.4", "requirement": "Frequenza di rimozione dei rifiuti adeguata a prevenire accumuli."}, |
| ], |
| }, |
| { |
| "id": "S4", |
| "title": "4. CONTROLLO PROCESSO E QUALITÀ", |
| "icon": "Settings", |
| "items": [ |
| {"id": "I4.1", "requirement": "Monitoraggio e registrazione dei Punti Critici di Controllo (CCP)."}, |
| {"id": "I4.2", "requirement": "Procedure di Buona Fabbricazione (GMP) rispettate (es. pulizia prima dell'avvio)."}, |
| {"id": "I4.3", "requirement": "Verifica e calibrazione del Metal Detector eseguita e registrata."}, |
| {"id": "I4.4", "requirement": "Corretta identificazione dei lotti di materie prime e semilavorati."}, |
| {"id": "I4.5", "requirement": "Rispettate le temperature di stoccaggio dei prodotti finiti e intermedi."}, |
| {"id": "I4.6", "requirement": "Corretta gestione, etichettatura e isolamento dei prodotti non conformi."}, |
| ], |
| }, |
| { |
| "id": "S5", |
| "title": "5. CONTROLLO INFESTANTI", |
| "icon": "Shield", |
| "items": [ |
| {"id": "I5.1", "requirement": "Assenza di tracce visibili di roditori, insetti o uccelli."}, |
| {"id": "I5.2", "requirement": "Stazioni di monitoraggio/trappole numerate, integre e registrate."}, |
| {"id": "I5.3", "requirement": "Mappe e registri delle trappole aggiornati e disponibili."}, |
| {"id": "I5.4", "requirement": "Barriere fisiche (es. reti, spazzole) anti-intrusione in buone condizioni."}, |
| {"id": "I5.5", "requirement": "Prodotti chimici di controllo infestanti stoccati in modo sicuro e separato."}, |
| ], |
| }, |
| { |
| "id": "S6", |
| "title": "6. MANUTENZIONE E VETRI", |
| "icon": "Settings", |
| "items": [ |
| {"id": "I6.1", "requirement": "Piano di manutenzione preventiva e correttiva rispettato e registrato."}, |
| {"id": "I6.2", "requirement": "Lubrificanti e oli utilizzati autorizzati per uso alimentare (se richiesto)."}, |
| {"id": "I6.3", "requirement": "Registri di calibrazione/verifica degli strumenti di misura critici aggiornati."}, |
| {"id": "I6.4", "requirement": "Gestione dei vetri rotti (registri rotture) aggiornata e conforme alla procedura."}, |
| {"id": "I6.5", "requirement": "Assenza di perdite, gocciolamenti o cavi scoperti da impianti/macchinari."}, |
| {"id": "I6.6", "requirement": "Area di deposito utensili e pezzi di ricambio ordinata e pulita."}, |
| ], |
| }, |
| { |
| "id": "S7", |
| "title": "7. DOCUMENTAZIONE E FORMAZIONE", |
| "icon": "FileText", |
| "items": [ |
| {"id": "I7.1", "requirement": "Documentazione di processo (procedure, istruzioni) aggiornata e disponibile."}, |
| {"id": "I7.2", "requirement": "Registri di formazione del personale aggiornati e verificabili."}, |
| {"id": "I7.3", "requirement": "Certificazioni e attestati del personale validi e aggiornati."}, |
| {"id": "I7.4", "requirement": "Controllo documenti (versioni, distribuzione, obsolescenza) conforme."}, |
| {"id": "I7.5", "requirement": "Archiviazione e conservazione documenti secondo i requisiti normativi."}, |
| ], |
| }, |
| ], |
| "metadata": { |
| "createdAt": datetime.utcnow().isoformat(), |
| "updatedAt": datetime.utcnow().isoformat(), |
| } |
| } |
| |
| result = await db.checklist_templates.insert_one(default_template) |
| logger.info(f"✅ Successfully created default template with ID: {result.inserted_id}") |
| |
| template = await db.checklist_templates.find_one({"templateId": "default"}) |
| return template |
| else: |
| logger.info("✅ Default template already exists in database") |
| return template |
| |
| except Exception as e: |
| logger.error(f"❌ Error ensuring default template exists: {e}", exc_info=True) |
| return None |
|
|
| @app.get("/api/checklist-template/default", response_model=Dict[str, Any]) |
| async def get_default_checklist_template(): |
| """ |
| Get the default checklist template |
| |
| Returns: |
| Dictionary containing the default checklist template structure |
| |
| Raises: |
| HTTPException: If template not found or database operation fails |
| """ |
| try: |
| logger.info("📋 GET /api/checklist-template/default - Retrieving default checklist template") |
| |
| |
| template_doc = await ensure_default_template_exists() |
| logger.info(f"Template check result: {'Found' if template_doc else 'Not Found'}") |
| |
| if not template_doc: |
| |
| logger.info("Checking for old template ID: default-audit-checklist") |
| template_doc = await db.checklist_templates.find_one({"templateId": "default-audit-checklist"}) |
| |
| if not template_doc: |
| |
| logger.info("Default template not found, creating from hardcoded structure") |
| |
| |
| default_template = { |
| "templateId": "default-audit-checklist", |
| "title": "Checklist di Audit Operativo (38 Controlli)", |
| "description": "Template per audit operativo in ambiente di produzione alimentare", |
| "version": "1.0", |
| "isTemplate": True, |
| "createdAt": datetime.utcnow(), |
| "updatedAt": datetime.utcnow(), |
| "sections": [ |
| { |
| "id": "S1", |
| "title": "1. PERSONALE E IGIENE", |
| "icon": "Users", |
| "items": [ |
| {"id": "I1.1", "requirement": "Indumenti da lavoro puliti (divisa, grembiule).", "compliance": "N/A", "deviation": "", "action": "", "imageData": None}, |
| {"id": "I1.2", "requirement": "Scarpe antinfortunistiche / Calzature pulite.", "compliance": "N/A", "deviation": "", "action": "", "imageData": None}, |
| {"id": "I1.3", "requirement": "Cuffie e/o Retine per capelli indossate correttamente.", "compliance": "N/A", "deviation": "", "action": "", "imageData": None}, |
| {"id": "I1.4", "requirement": "Assenza di gioielli, piercing visibili, unghie lunghe.", "compliance": "N/A", "deviation": "", "action": "", "imageData": None}, |
| {"id": "I1.5", "requirement": "Lavaggio mani documentato all'ingresso/reingresso.", "compliance": "N/A", "deviation": "", "action": "", "imageData": None}, |
| {"id": "I1.6", "requirement": "Assenza di cibo/bevande non autorizzate nelle aree produttive.", "compliance": "N/A", "deviation": "", "action": "", "imageData": None} |
| ] |
| }, |
| { |
| "id": "S2", |
| "title": "2. STRUTTURE E IMPIANTI", |
| "icon": "Building", |
| "items": [ |
| {"id": "I2.1", "requirement": "Illuminazione adeguata e funzionante in tutte le aree.", "compliance": "N/A", "deviation": "", "action": "", "imageData": None}, |
| {"id": "I2.2", "requirement": "Porte esterne/interne in buone condizioni e chiuse correttamente (sigillatura).", "compliance": "N/A", "deviation": "", "action": "", "imageData": None}, |
| {"id": "I2.3", "requirement": "Integrità di pavimenti, pareti e soffitti (assenza di crepe/danni).", "compliance": "N/A", "deviation": "", "action": "", "imageData": None}, |
| {"id": "I2.4", "requirement": "Controllo vetri / lampade protette o anti-frantumazione.", "compliance": "N/A", "deviation": "", "action": "", "imageData": None}, |
| {"id": "I2.5", "requirement": "Condizioni igieniche dei servizi igienici e spogliatoi (pulizia e dotazioni).", "compliance": "N/A", "deviation": "", "action": "", "imageData": None}, |
| {"id": "I2.6", "requirement": "Ventilazione e aspirazione funzionanti, pulite e non ostruite.", "compliance": "N/A", "deviation": "", "action": "", "imageData": None} |
| ] |
| }, |
| { |
| "id": "S3", |
| "title": "3. GESTIONE E IGIENE AMBIENTALE", |
| "icon": "Package", |
| "items": [ |
| {"id": "I3.1", "requirement": "Contenitori dei rifiuti puliti, chiusi e identificati.", "compliance": "N/A", "deviation": "", "action": "", "imageData": None}, |
| {"id": "I3.2", "requirement": "Separazione corretta dei rifiuti (es. umido, secco, plastica).", "compliance": "N/A", "deviation": "", "action": "", "imageData": None}, |
| {"id": "I3.3", "requirement": "Area di stoccaggio rifiuti (interna ed esterna) ordinata e sanificata.", "compliance": "N/A", "deviation": "", "action": "", "imageData": None}, |
| {"id": "I3.4", "requirement": "Frequenza di rimozione dei rifiuti adeguata a prevenire accumuli.", "compliance": "N/A", "deviation": "", "action": "", "imageData": None} |
| ] |
| }, |
| { |
| "id": "S4", |
| "title": "4. CONTROLLO PROCESSO E QUALITÀ", |
| "icon": "Settings", |
| "items": [ |
| {"id": "I4.1", "requirement": "Monitoraggio e registrazione dei Punti Critici di Controllo (CCP).", "compliance": "N/A", "deviation": "", "action": "", "imageData": None}, |
| {"id": "I4.2", "requirement": "Procedure di Buona Fabbricazione (GMP) rispettate (es. pulizia prima dell'avvio).", "compliance": "N/A", "deviation": "", "action": "", "imageData": None}, |
| {"id": "I4.3", "requirement": "Verifica e calibrazione del Metal Detector eseguita e registrata.", "compliance": "N/A", "deviation": "", "action": "", "imageData": None}, |
| {"id": "I4.4", "requirement": "Corretta identificazione dei lotti di materie prime e semilavorati.", "compliance": "N/A", "deviation": "", "action": "", "imageData": None}, |
| {"id": "I4.5", "requirement": "Rispettate le temperature di stoccaggio dei prodotti finiti e intermedi.", "compliance": "N/A", "deviation": "", "action": "", "imageData": None}, |
| {"id": "I4.6", "requirement": "Corretta gestione, etichettatura e isolamento dei prodotti non conformi.", "compliance": "N/A", "deviation": "", "action": "", "imageData": None} |
| ] |
| }, |
| { |
| "id": "S5", |
| "title": "5. CONTROLLO INFESTANTI", |
| "icon": "Shield", |
| "items": [ |
| {"id": "I5.1", "requirement": "Assenza di tracce visibili di roditori, insetti o uccelli.", "compliance": "N/A", "deviation": "", "action": "", "imageData": None}, |
| {"id": "I5.2", "requirement": "Stazioni di monitoraggio/trappole numerate, integre e registrate.", "compliance": "N/A", "deviation": "", "action": "", "imageData": None}, |
| {"id": "I5.3", "requirement": "Mappe e registri delle trappole aggiornati e disponibili.", "compliance": "N/A", "deviation": "", "action": "", "imageData": None}, |
| {"id": "I5.4", "requirement": "Barriere fisiche (es. reti, spazzole) anti-intrusione in buone condizioni.", "compliance": "N/A", "deviation": "", "action": "", "imageData": None}, |
| {"id": "I5.5", "requirement": "Prodotti chimici di controllo infestanti stoccati in modo sicuro e separato.", "compliance": "N/A", "deviation": "", "action": "", "imageData": None} |
| ] |
| }, |
| { |
| "id": "S6", |
| "title": "6. MANUTENZIONE E VETRI", |
| "icon": "Settings", |
| "items": [ |
| {"id": "I6.1", "requirement": "Piano di manutenzione preventiva e correttiva rispettato e registrato.", "compliance": "N/A", "deviation": "", "action": "", "imageData": None}, |
| {"id": "I6.2", "requirement": "Lubrificanti e oli utilizzati autorizzati per uso alimentare (se richiesto).", "compliance": "N/A", "deviation": "", "action": "", "imageData": None}, |
| {"id": "I6.3", "requirement": "Registri di calibrazione/verifica degli strumenti di misura critici aggiornati.", "compliance": "N/A", "deviation": "", "action": "", "imageData": None}, |
| {"id": "I6.4", "requirement": "Gestione dei vetri rotti (registri rotture) aggiornata e conforme alla procedura.", "compliance": "N/A", "deviation": "", "action": "", "imageData": None}, |
| {"id": "I6.5", "requirement": "Assenza di perdite, gocciolamenti o cavi scoperti da impianti/macchinari.", "compliance": "N/A", "deviation": "", "action": "", "imageData": None}, |
| {"id": "I6.6", "requirement": "Area di deposito utensili e pezzi di ricambio ordinata e pulita.", "compliance": "N/A", "deviation": "", "action": "", "imageData": None} |
| ] |
| }, |
| { |
| "id": "S7", |
| "title": "7. DOCUMENTAZIONE E FORMAZIONE", |
| "icon": "FileText", |
| "items": [ |
| {"id": "I7.1", "requirement": "Documentazione di processo (procedure, istruzioni) aggiornata e disponibile.", "compliance": "N/A", "deviation": "", "action": "", "imageData": None}, |
| {"id": "I7.2", "requirement": "Registri di formazione del personale aggiornati e verificabili.", "compliance": "N/A", "deviation": "", "action": "", "imageData": None}, |
| {"id": "I7.3", "requirement": "Certificazioni e attestati del personale validi e aggiornati.", "compliance": "N/A", "deviation": "", "action": "", "imageData": None}, |
| {"id": "I7.4", "requirement": "Controllo documenti (versioni, distribuzione, obsolescenza) conforme.", "compliance": "N/A", "deviation": "", "action": "", "imageData": None}, |
| {"id": "I7.5", "requirement": "Archiviazione e conservazione documenti secondo i requisiti normativi.", "compliance": "N/A", "deviation": "", "action": "", "imageData": None} |
| ] |
| } |
| ], |
| "totalItems": 38, |
| "completedItems": 0, |
| "nonCompliantItems": 0, |
| "complianceScore": 0.0 |
| } |
| |
| |
| await db.checklist_templates.insert_one(default_template) |
| template_doc = default_template |
| |
| |
| serialized_template = serialize_checklist(template_doc) |
| |
| return { |
| "success": True, |
| "data": serialized_template, |
| "message": "Default checklist template retrieved successfully" |
| } |
| |
| except Exception as e: |
| logger.error(f"Error retrieving default checklist template: {e}") |
| raise HTTPException( |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
| detail=f"Failed to retrieve checklist template: {str(e)}" |
| ) |
|
|
| @app.post("/api/checklist-template/item", response_model=Dict[str, Any]) |
| async def add_template_item(item_data: dict): |
| """ |
| Add a new item to the shared checklist template |
| |
| Args: |
| item_data: Dictionary containing item information (sectionId, requirement, etc.) |
| |
| Returns: |
| Dictionary confirming the item was added |
| |
| Raises: |
| HTTPException: If database operation fails |
| """ |
| try: |
| logger.info(f"Adding new item to shared template") |
| |
| |
| template = await ensure_default_template_exists() |
| |
| if not template: |
| raise HTTPException( |
| status_code=status.HTTP_404_NOT_FOUND, |
| detail="Template not found" |
| ) |
| |
| template_id = template.get("templateId", "default") |
| |
| |
| section_id = item_data.get('sectionId') |
| section_index = next((i for i, s in enumerate(template['sections']) if s['id'] == section_id), None) |
| |
| if section_index is None: |
| raise HTTPException( |
| status_code=status.HTTP_404_NOT_FOUND, |
| detail=f"Section {section_id} not found" |
| ) |
| |
| new_item = { |
| "id": item_data.get('id', f"I{section_id[-1]}.{len(template['sections'][section_index]['items'])+1}"), |
| "requirement": item_data.get('requirement', ''), |
| } |
| |
| |
| result = await db.checklist_templates.update_one( |
| {"templateId": template_id, "sections.id": section_id}, |
| {"$push": {"sections.$.items": new_item}} |
| ) |
| |
| if result.modified_count > 0: |
| logger.info(f"Successfully added item to template") |
| return { |
| "success": True, |
| "message": "Item added successfully", |
| "data": new_item |
| } |
| else: |
| raise HTTPException( |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
| detail="Failed to add item" |
| ) |
| |
| except HTTPException: |
| raise |
| except Exception as e: |
| logger.error(f"Error adding item to template: {e}") |
| raise HTTPException( |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
| detail=f"Failed to add item: {str(e)}" |
| ) |
|
|
| @app.put("/api/checklist-template/item/{item_id}", response_model=Dict[str, Any]) |
| async def update_template_item(item_id: str, item_data: dict): |
| """ |
| Update an existing item in the shared checklist template |
| |
| Args: |
| item_id: ID of the item to update |
| item_data: Dictionary containing updated item information |
| |
| Returns: |
| Dictionary confirming the item was updated |
| |
| Raises: |
| HTTPException: If database operation fails |
| """ |
| try: |
| logger.info(f"Updating item {item_id} in shared template") |
| |
| |
| template = await ensure_default_template_exists() |
| template_id = template.get("templateId") if template else "default" |
| |
| |
| update_fields = {} |
| if 'requirement' in item_data: |
| update_fields["sections.$[].items.$[item].requirement"] = item_data['requirement'] |
| |
| if not update_fields: |
| raise HTTPException( |
| status_code=status.HTTP_400_BAD_REQUEST, |
| detail="No valid fields to update" |
| ) |
| |
| |
| result = await db.checklist_templates.update_one( |
| {"templateId": template_id}, |
| {"$set": update_fields}, |
| array_filters=[{"item.id": item_id}] |
| ) |
| |
| if result.modified_count > 0: |
| logger.info(f"Successfully updated item {item_id} in template") |
| return { |
| "success": True, |
| "message": "Item updated successfully", |
| "data": item_data |
| } |
| else: |
| raise HTTPException( |
| status_code=status.HTTP_404_NOT_FOUND, |
| detail="Item not found or no changes made" |
| ) |
| |
| except HTTPException: |
| raise |
| except Exception as e: |
| logger.error(f"Error updating item {item_id} in template: {e}") |
| raise HTTPException( |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
| detail=f"Failed to update item: {str(e)}" |
| ) |
|
|
| @app.delete("/api/checklist-template/item/{item_id}", response_model=Dict[str, Any]) |
| async def delete_template_item(item_id: str): |
| """ |
| Delete an item from the shared checklist template |
| |
| Args: |
| item_id: ID of the item to delete |
| |
| Returns: |
| Dictionary confirming the item was deleted |
| |
| Raises: |
| HTTPException: If database operation fails |
| """ |
| try: |
| logger.info(f"Deleting item {item_id} from shared template") |
| |
| |
| template = await ensure_default_template_exists() |
| template_id = template.get("templateId") if template else "default" |
| |
| |
| result = await db.checklist_templates.update_one( |
| {"templateId": template_id}, |
| {"$pull": {"sections.$[].items": {"id": item_id}}} |
| ) |
| |
| if result.modified_count > 0: |
| logger.info(f"Successfully deleted item {item_id} from template") |
| return { |
| "success": True, |
| "message": "Item deleted successfully" |
| } |
| else: |
| raise HTTPException( |
| status_code=status.HTTP_404_NOT_FOUND, |
| detail="Item not found" |
| ) |
| |
| except HTTPException: |
| raise |
| except Exception as e: |
| logger.error(f"Error deleting item {item_id} from template: {e}") |
| raise HTTPException( |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
| detail=f"Failed to delete item: {str(e)}" |
| ) |
|
|
| @app.post("/api/checklist-template/section", response_model=Dict[str, Any]) |
| async def add_template_section(section_data: dict): |
| """ |
| Add a new section to the shared checklist template |
| |
| Args: |
| section_data: Dictionary containing section information |
| |
| Returns: |
| Dictionary confirming the section was added |
| |
| Raises: |
| HTTPException: If database operation fails |
| """ |
| try: |
| logger.info(f"Adding new section to shared template") |
| |
| |
| template = await ensure_default_template_exists() |
| |
| if not template: |
| raise HTTPException( |
| status_code=status.HTTP_404_NOT_FOUND, |
| detail="Template not found" |
| ) |
| |
| template_id = template.get("templateId", "default") |
| |
| |
| new_section = { |
| "id": section_data.get('id', f"S{len(template['sections'])+1}"), |
| "title": section_data.get('title', 'New Section'), |
| "icon": section_data.get('icon', 'Settings'), |
| "items": [] |
| } |
| |
| |
| result = await db.checklist_templates.update_one( |
| {"templateId": template_id}, |
| {"$push": {"sections": new_section}} |
| ) |
| |
| if result.modified_count > 0: |
| logger.info(f"Successfully added section to template") |
| return { |
| "success": True, |
| "message": "Section added successfully", |
| "data": new_section |
| } |
| else: |
| raise HTTPException( |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
| detail="Failed to add section" |
| ) |
| |
| except HTTPException: |
| raise |
| except Exception as e: |
| logger.error(f"Error adding section to template: {e}") |
| raise HTTPException( |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
| detail=f"Failed to add section: {str(e)}" |
| ) |
|
|
| @app.delete("/api/checklist-template/section/{section_id}", response_model=Dict[str, Any]) |
| async def delete_template_section(section_id: str): |
| """ |
| Delete a section from the shared checklist template |
| |
| Args: |
| section_id: ID of the section to delete |
| |
| Returns: |
| Dictionary confirming the section was deleted |
| |
| Raises: |
| HTTPException: If database operation fails |
| """ |
| try: |
| logger.info(f"Deleting section {section_id} from shared template") |
| |
| |
| template = await ensure_default_template_exists() |
| template_id = template.get("templateId") if template else "default" |
| |
| |
| result = await db.checklist_templates.update_one( |
| {"templateId": template_id}, |
| {"$pull": {"sections": {"id": section_id}}} |
| ) |
| |
| if result.modified_count > 0: |
| logger.info(f"Successfully deleted section {section_id} from template") |
| return { |
| "success": True, |
| "message": "Section deleted successfully" |
| } |
| else: |
| raise HTTPException( |
| status_code=status.HTTP_404_NOT_FOUND, |
| detail="Section not found" |
| ) |
| |
| except HTTPException: |
| raise |
| except Exception as e: |
| logger.error(f"Error deleting section {section_id} from template: {e}") |
| raise HTTPException( |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
| detail=f"Failed to delete section: {str(e)}" |
| ) |
|
|
| @app.post("/api/checklist/{user_id}/item", response_model=Dict[str, Any]) |
| async def add_checklist_item(user_id: str, item_data: dict): |
| """ |
| Add a new item to a checklist |
| |
| Args: |
| user_id: Unique identifier for the user |
| item_data: Dictionary containing item information (sectionId, requirement, etc.) |
| |
| Returns: |
| Dictionary confirming the item was added |
| |
| Raises: |
| HTTPException: If database operation fails |
| """ |
| try: |
| logger.info(f"Adding new item to checklist for user: {user_id}") |
| |
| |
| checklist_doc = await db.checklists.find_one({"userId": user_id}) |
| |
| if not checklist_doc: |
| raise HTTPException( |
| status_code=status.HTTP_404_NOT_FOUND, |
| detail="Checklist not found for user" |
| ) |
| |
| |
| section_id = item_data.get('sectionId') |
| new_item = { |
| "id": item_data.get('id', f"I{section_id}.{len(checklist_doc['sections'][int(section_id[-1])-1]['items'])+1}"), |
| "requirement": item_data.get('requirement', ''), |
| "compliance": "N/A", |
| "deviation": "", |
| "action": "", |
| "imageData": None, |
| "checkedAt": None, |
| "checkedBy": "" |
| } |
| |
| |
| result = await db.checklists.update_one( |
| {"userId": user_id, "sections.id": section_id}, |
| {"$push": {f"sections.$.items": new_item}} |
| ) |
| |
| if result.modified_count > 0: |
| logger.info(f"Successfully added item to checklist for user {user_id}") |
| return { |
| "success": True, |
| "message": "Item added successfully", |
| "data": new_item |
| } |
| else: |
| raise HTTPException( |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
| detail="Failed to add item" |
| ) |
| |
| except HTTPException: |
| raise |
| except Exception as e: |
| logger.error(f"Error adding item for user {user_id}: {e}") |
| raise HTTPException( |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
| detail=f"Failed to add item: {str(e)}" |
| ) |
|
|
| @app.put("/api/checklist/{user_id}/item/{item_id}", response_model=Dict[str, Any]) |
| async def update_checklist_item(user_id: str, item_id: str, item_data: dict): |
| """ |
| Update an existing checklist item |
| |
| Args: |
| user_id: Unique identifier for the user |
| item_id: ID of the item to update |
| item_data: Dictionary containing updated item information |
| |
| Returns: |
| Dictionary confirming the item was updated |
| |
| Raises: |
| HTTPException: If database operation fails |
| """ |
| try: |
| logger.info(f"Updating item {item_id} for user: {user_id}") |
| |
| |
| checklist = await db.checklists.find_one({"userId": user_id}) |
| if not checklist: |
| logger.warning(f"No checklist found for user {user_id}") |
| raise HTTPException( |
| status_code=status.HTTP_404_NOT_FOUND, |
| detail=f"Checklist not found for user {user_id}. Please create a checklist first." |
| ) |
| |
| |
| update_fields = {} |
| for field in ['requirement', 'compliance', 'deviation', 'action']: |
| if field in item_data: |
| update_fields[f"sections.$[].items.$[item].{field}"] = item_data[field] |
| |
| if not update_fields: |
| raise HTTPException( |
| status_code=status.HTTP_400_BAD_REQUEST, |
| detail="No valid fields to update" |
| ) |
| |
| |
| result = await db.checklists.update_one( |
| {"userId": user_id}, |
| {"$set": update_fields}, |
| array_filters=[{"item.id": item_id}] |
| ) |
| |
| if result.modified_count > 0: |
| logger.info(f"Successfully updated item {item_id} for user {user_id}") |
| return { |
| "success": True, |
| "message": "Item updated successfully", |
| "data": item_data |
| } |
| else: |
| raise HTTPException( |
| status_code=status.HTTP_404_NOT_FOUND, |
| detail="Item not found or no changes made" |
| ) |
| |
| except HTTPException: |
| raise |
| except Exception as e: |
| logger.error(f"Error updating item {item_id} for user {user_id}: {e}") |
| raise HTTPException( |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
| detail=f"Failed to update item: {str(e)}" |
| ) |
|
|
| @app.delete("/api/checklist/{user_id}/item/{item_id}", response_model=Dict[str, Any]) |
| async def delete_checklist_item(user_id: str, item_id: str): |
| """ |
| Delete a checklist item |
| |
| Args: |
| user_id: Unique identifier for the user |
| item_id: ID of the item to delete |
| |
| Returns: |
| Dictionary confirming the item was deleted |
| |
| Raises: |
| HTTPException: If database operation fails |
| """ |
| try: |
| logger.info(f"Deleting item {item_id} for user: {user_id}") |
| |
| |
| checklist = await db.checklists.find_one({"userId": user_id}) |
| if not checklist: |
| logger.warning(f"No checklist found for user {user_id}") |
| raise HTTPException( |
| status_code=status.HTTP_404_NOT_FOUND, |
| detail=f"Checklist not found for user {user_id}. Please create a checklist first." |
| ) |
| |
| |
| result = await db.checklists.update_one( |
| {"userId": user_id}, |
| {"$pull": {"sections.$[].items": {"id": item_id}}} |
| ) |
| |
| if result.modified_count > 0: |
| logger.info(f"Successfully deleted item {item_id} for user {user_id}") |
| return { |
| "success": True, |
| "message": "Item deleted successfully" |
| } |
| else: |
| raise HTTPException( |
| status_code=status.HTTP_404_NOT_FOUND, |
| detail="Item not found" |
| ) |
| |
| except HTTPException: |
| raise |
| except Exception as e: |
| logger.error(f"Error deleting item {item_id} for user {user_id}: {e}") |
| raise HTTPException( |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
| detail=f"Failed to delete item: {str(e)}" |
| ) |
|
|
| @app.post("/api/checklist/{user_id}/section", response_model=Dict[str, Any]) |
| async def add_checklist_section(user_id: str, section_data: dict): |
| """ |
| Add a new section to a checklist |
| |
| Args: |
| user_id: Unique identifier for the user |
| section_data: Dictionary containing section information |
| |
| Returns: |
| Dictionary confirming the section was added |
| |
| Raises: |
| HTTPException: If database operation fails |
| """ |
| try: |
| logger.info(f"Adding new section to checklist for user: {user_id}") |
| |
| |
| checklist_doc = await db.checklists.find_one({"userId": user_id}) |
| |
| if not checklist_doc: |
| raise HTTPException( |
| status_code=status.HTTP_404_NOT_FOUND, |
| detail="Checklist not found for user" |
| ) |
| |
| |
| new_section = { |
| "id": section_data.get('id', f"S{len(checklist_doc['sections'])+1}"), |
| "title": section_data.get('title', 'New Section'), |
| "icon": section_data.get('icon', 'Settings'), |
| "items": [] |
| } |
| |
| |
| result = await db.checklists.update_one( |
| {"userId": user_id}, |
| {"$push": {"sections": new_section}} |
| ) |
| |
| if result.modified_count > 0: |
| logger.info(f"Successfully added section to checklist for user {user_id}") |
| return { |
| "success": True, |
| "message": "Section added successfully", |
| "data": new_section |
| } |
| else: |
| raise HTTPException( |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
| detail="Failed to add section" |
| ) |
| |
| except HTTPException: |
| raise |
| except Exception as e: |
| logger.error(f"Error adding section for user {user_id}: {e}") |
| raise HTTPException( |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
| detail=f"Failed to add section: {str(e)}" |
| ) |
|
|
| @app.delete("/api/checklist/{user_id}/section/{section_id}", response_model=Dict[str, Any]) |
| async def delete_checklist_section(user_id: str, section_id: str): |
| """ |
| Delete a checklist section and all its items |
| |
| Args: |
| user_id: Unique identifier for the user |
| section_id: ID of the section to delete |
| |
| Returns: |
| Dictionary confirming the section was deleted |
| |
| Raises: |
| HTTPException: If database operation fails |
| """ |
| try: |
| logger.info(f"Deleting section {section_id} for user: {user_id}") |
| |
| |
| checklist = await db.checklists.find_one({"userId": user_id}) |
| if not checklist: |
| logger.warning(f"No checklist found for user {user_id}") |
| raise HTTPException( |
| status_code=status.HTTP_404_NOT_FOUND, |
| detail=f"Checklist not found for user {user_id}. Please create a checklist first." |
| ) |
| |
| |
| result = await db.checklists.update_one( |
| {"userId": user_id}, |
| {"$pull": {"sections": {"id": section_id}}} |
| ) |
| |
| if result.modified_count > 0: |
| logger.info(f"Successfully deleted section {section_id} for user {user_id}") |
| return { |
| "success": True, |
| "message": "Section deleted successfully" |
| } |
| else: |
| raise HTTPException( |
| status_code=status.HTTP_404_NOT_FOUND, |
| detail="Section not found" |
| ) |
| |
| except HTTPException: |
| raise |
| except Exception as e: |
| logger.error(f"Error deleting section {section_id} for user {user_id}: {e}") |
| raise HTTPException( |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
| detail=f"Failed to delete section: {str(e)}" |
| ) |
|
|
| @app.get("/api/checklists", response_model=Dict[str, Any]) |
| async def get_all_checklists(): |
| """ |
| Retrieve all checklists (admin endpoint) |
| |
| This endpoint returns all checklists in the database. It's intended for |
| administrative purposes and should be protected with proper authentication |
| in a production environment. |
| |
| Returns: |
| Dictionary containing list of all checklists |
| |
| Raises: |
| HTTPException: If database operation fails |
| """ |
| try: |
| logger.info("Retrieving all checklists") |
| |
| |
| cursor = db.checklists.find({}) |
| checklists = [] |
| |
| async for checklist_doc in cursor: |
| serialized_checklist = serialize_checklist(checklist_doc) |
| checklists.append(serialized_checklist) |
| |
| return { |
| "success": True, |
| "data": checklists, |
| "count": len(checklists), |
| "message": f"Retrieved {len(checklists)} checklists" |
| } |
| |
| except Exception as e: |
| logger.error(f"Error retrieving all checklists: {e}") |
| raise HTTPException( |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
| detail=f"Failed to retrieve checklists: {str(e)}" |
| ) |
|
|
| |
| |
| |
|
|
| @app.exception_handler(HTTPException) |
| async def http_exception_handler(request, exc): |
| """Handle HTTP exceptions with consistent error format""" |
| logger.error(f"HTTP Exception: {exc.detail}") |
| return JSONResponse( |
| status_code=exc.status_code, |
| content={ |
| "success": False, |
| "error": exc.detail, |
| "status_code": exc.status_code |
| } |
| ) |
|
|
| @app.exception_handler(Exception) |
| async def general_exception_handler(request, exc): |
| """Handle general exceptions with logging""" |
| logger.error(f"Unhandled exception: {exc}", exc_info=True) |
| return JSONResponse( |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
| content={ |
| "success": False, |
| "error": "Internal server error", |
| "detail": str(exc) if os.getenv("DEBUG", "false").lower() == "true" else "An unexpected error occurred" |
| } |
| ) |
|
|
| |
| |
| |
|
|
| if __name__ == "__main__": |
| """ |
| Main execution block for running the server |
| |
| This block is executed when the script is run directly (not imported). |
| It starts the Uvicorn ASGI server with the configured settings. |
| """ |
| import uvicorn |
| |
| logger.info("Starting FastAPI server...") |
| |
| |
| uvicorn.run( |
| "main:app", |
| host="0.0.0.0", |
| port=PORT, |
| reload=True, |
| log_level="info" |
| ) |
|
|