Ali2206 commited on
Commit
d7e0738
·
1 Parent(s): 546abe0

Fix duplicate save issue and improve image handling - Updated backend to prevent duplicate entries - Fixed MongoDB immutable _id field error - Added comprehensive logging for debugging - Improved image capture and storage functionality

Browse files
Files changed (5) hide show
  1. .env +3 -0
  2. __pycache__/main.cpython-311.pyc +0 -0
  3. main.py +833 -0
  4. mongodb.env +8 -0
  5. requirements.txt +4 -1
.env ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ MONGODB_URI=mongodb+srv://yakdhanali97:Syrou2206@cluster0.szf60ea.mongodb.net/audit_checklist?retryWrites=true&w=majority&appName=Cluster0
2
+ PORT=5000
3
+ CORS_ORIGIN=http://localhost:3000
__pycache__/main.cpython-311.pyc ADDED
Binary file (39.6 kB). View file
 
main.py ADDED
@@ -0,0 +1,833 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FastAPI Server for Audit Checklist Application
3
+ =============================================
4
+
5
+ This server provides a REST API for managing audit checklists with MongoDB integration.
6
+ It supports CRUD operations for checklist data and user management.
7
+
8
+ Features:
9
+ - MongoDB Atlas integration with async Motor driver
10
+ - CORS support for React frontend
11
+ - Comprehensive error handling
12
+ - Health check endpoint
13
+ - Environment-based configuration
14
+
15
+ Author: Audit Checklist Team
16
+ Version: 1.0.0
17
+ """
18
+
19
+ import os
20
+ import sys
21
+ import base64
22
+ from datetime import datetime
23
+ from typing import Optional, List, Dict, Any, Union
24
+ import logging
25
+
26
+ # FastAPI imports
27
+ from fastapi import FastAPI, HTTPException, status, UploadFile, File
28
+ from fastapi.middleware.cors import CORSMiddleware
29
+ from fastapi.responses import JSONResponse
30
+
31
+ # Pydantic for data validation
32
+ from pydantic import BaseModel, Field
33
+
34
+ # MongoDB async driver
35
+ import motor.motor_asyncio
36
+ from bson import ObjectId
37
+
38
+ # Environment variables
39
+ from dotenv import load_dotenv
40
+
41
+ # Load environment variables from .env file
42
+ load_dotenv('mongodb.env')
43
+
44
+ # Configure logging
45
+ logging.basicConfig(
46
+ level=logging.INFO,
47
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
48
+ )
49
+ logger = logging.getLogger(__name__)
50
+
51
+ # =============================================================================
52
+ # CONFIGURATION AND INITIALIZATION
53
+ # =============================================================================
54
+
55
+ # Environment variables with fallback defaults
56
+ MONGODB_URI = os.getenv('MONGODB_URI', 'mongodb://localhost:27017/audit_checklist')
57
+ PORT = int(os.getenv('PORT', 8000)) # Hugging Face Spaces uses port 8000
58
+ CORS_ORIGIN = os.getenv('CORS_ORIGIN', '*') # Allow all origins by default for mobile apps
59
+
60
+ # Initialize FastAPI application
61
+ app = FastAPI(
62
+ title="Audit Checklist API",
63
+ description="REST API for managing audit checklists with MongoDB integration",
64
+ version="1.0.0",
65
+ docs_url="/docs", # Swagger UI documentation
66
+ redoc_url="/redoc" # ReDoc documentation
67
+ )
68
+
69
+ # Configure CORS middleware
70
+ # Allow all origins for Hugging Face Spaces deployment
71
+ cors_origins = [CORS_ORIGIN] if CORS_ORIGIN != "*" else ["*"]
72
+
73
+ app.add_middleware(
74
+ CORSMiddleware,
75
+ allow_origins=cors_origins,
76
+ allow_credentials=True,
77
+ allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
78
+ allow_headers=["*"],
79
+ )
80
+
81
+ # MongoDB connection setup
82
+ client = None
83
+ db = None
84
+
85
+ async def connect_to_mongodb():
86
+ """
87
+ Establish connection to MongoDB Atlas
88
+
89
+ This function creates an async connection to MongoDB using the Motor driver.
90
+ It handles connection errors gracefully and logs the status.
91
+ """
92
+ global client, db
93
+ try:
94
+ client = motor.motor_asyncio.AsyncIOMotorClient(MONGODB_URI)
95
+ db = client.audit_checklist
96
+
97
+ # Test the connection
98
+ await client.admin.command('ping')
99
+ logger.info("Successfully connected to MongoDB Atlas")
100
+
101
+ # Create indexes for better performance
102
+ await create_database_indexes()
103
+
104
+ except Exception as e:
105
+ logger.error(f"Failed to connect to MongoDB: {e}")
106
+ raise e
107
+
108
+ async def create_database_indexes():
109
+ """
110
+ Create database indexes for optimal query performance
111
+
112
+ This function creates indexes on frequently queried fields to improve
113
+ database performance and query speed.
114
+ """
115
+ try:
116
+ # Index on userId for faster user-specific queries
117
+ await db.checklists.create_index("userId", unique=False)
118
+
119
+ # Index on createdAt for time-based queries
120
+ await db.checklists.create_index("createdAt", unique=False)
121
+
122
+ logger.info("Database indexes created successfully")
123
+
124
+ except Exception as e:
125
+ logger.warning(f"Could not create indexes: {e}")
126
+
127
+ async def close_mongodb_connection():
128
+ """Close MongoDB connection gracefully"""
129
+ global client
130
+ if client:
131
+ client.close()
132
+ logger.info("MongoDB connection closed")
133
+
134
+ # =============================================================================
135
+ # PYDANTIC MODELS FOR DATA VALIDATION
136
+ # =============================================================================
137
+
138
+ class ChecklistItem(BaseModel):
139
+ """
140
+ Model for individual checklist items
141
+
142
+ Attributes:
143
+ id: Unique identifier for the item
144
+ requirement: Description of the requirement to be checked
145
+ compliance: Compliance status (N/A, Compliant, Non-Compliant)
146
+ deviation: Description of any deviation found
147
+ action: Action taken to address the issue
148
+ checkedAt: Timestamp when the item was checked
149
+ checkedBy: Name of the person who checked the item
150
+ """
151
+ id: str = Field(..., description="Unique identifier for the checklist item")
152
+ requirement: str = Field(..., description="Description of the requirement")
153
+ compliance: str = Field(default="N/A", description="Compliance status: N/A, Compliant, or Non-Compliant")
154
+ deviation: str = Field(default="", description="Description of any deviation found")
155
+ action: str = Field(default="", description="Action taken to address the issue")
156
+ checkedAt: Optional[datetime] = Field(default=None, description="Timestamp when item was checked")
157
+ checkedBy: str = Field(default="", description="Name of the person who checked the item")
158
+
159
+ class ChecklistSection(BaseModel):
160
+ """
161
+ Model for checklist sections containing multiple items
162
+
163
+ Attributes:
164
+ id: Unique identifier for the section
165
+ title: Display title of the section
166
+ icon: Icon identifier for UI display
167
+ items: List of checklist items in this section
168
+ """
169
+ id: str = Field(..., description="Unique identifier for the section")
170
+ title: str = Field(..., description="Display title of the section")
171
+ icon: str = Field(..., description="Icon identifier for UI display")
172
+ items: List[ChecklistItem] = Field(default=[], description="List of checklist items")
173
+
174
+ class Metadata(BaseModel):
175
+ """
176
+ Metadata model for checklist information
177
+ """
178
+ userName: Optional[str] = Field(default=None, description="Name of the user who saved the checklist")
179
+ savedAt: Optional[str] = Field(default=None, description="ISO timestamp when saved")
180
+ savedAtFormatted: Optional[str] = Field(default=None, description="Formatted timestamp when saved")
181
+ userId: Optional[str] = Field(default=None, description="User ID")
182
+ version: Optional[str] = Field(default="1.0", description="Version of the checklist")
183
+
184
+ class ChecklistData(BaseModel):
185
+ """
186
+ Complete checklist data model
187
+
188
+ Attributes:
189
+ userId: Unique identifier for the user
190
+ title: Title of the checklist
191
+ sections: List of sections in the checklist
192
+ totalItems: Total number of items across all sections
193
+ completedItems: Number of completed items
194
+ nonCompliantItems: Number of non-compliant items
195
+ complianceScore: Calculated compliance percentage
196
+ verificationDate: Date when the checklist was verified
197
+ createdAt: Timestamp when the checklist was created
198
+ updatedAt: Timestamp when the checklist was last updated
199
+ imageData: Base64 encoded image data (stored when compliance < 100%)
200
+ imageType: MIME type of the image (e.g., image/jpeg, image/png)
201
+ """
202
+ userId: str = Field(..., description="Unique identifier for the user")
203
+ title: str = Field(..., description="Title of the checklist")
204
+ sections: List[ChecklistSection] = Field(default=[], description="List of checklist sections")
205
+ totalItems: Optional[int] = Field(default=0, description="Total number of items")
206
+ completedItems: Optional[int] = Field(default=0, description="Number of completed items")
207
+ nonCompliantItems: Optional[int] = Field(default=0, description="Number of non-compliant items")
208
+ complianceScore: Optional[float] = Field(default=0.0, description="Compliance percentage score")
209
+ verificationDate: Optional[datetime] = Field(default=None, description="Date of verification")
210
+ createdAt: Optional[datetime] = Field(default=None, description="Creation timestamp")
211
+ updatedAt: Optional[datetime] = Field(default=None, description="Last update timestamp")
212
+ metadata: Optional[Metadata] = Field(default=None, description="Additional metadata including user information")
213
+ imageData: Optional[str] = Field(default=None, description="Base64 encoded image data for non-compliant checklists")
214
+ imageType: Optional[str] = Field(default=None, description="MIME type of the image")
215
+
216
+ class Config:
217
+ # Allow extra fields that might be sent from frontend
218
+ extra = "allow"
219
+
220
+ class ChecklistResponse(BaseModel):
221
+ """
222
+ Standard API response model for checklist operations
223
+
224
+ Attributes:
225
+ success: Boolean indicating if the operation was successful
226
+ data: The checklist data (if successful)
227
+ message: Optional message describing the result
228
+ error: Error message (if unsuccessful)
229
+ """
230
+ success: bool = Field(..., description="Whether the operation was successful")
231
+ data: Optional[Union[ChecklistData, Dict[str, Any]]] = Field(default=None, description="Checklist data")
232
+ message: Optional[str] = Field(default=None, description="Success message")
233
+ error: Optional[str] = Field(default=None, description="Error message")
234
+
235
+ class HealthResponse(BaseModel):
236
+ """Health check response model"""
237
+ status: str = Field(..., description="Health status")
238
+ timestamp: datetime = Field(..., description="Current timestamp")
239
+ database: str = Field(..., description="Database connection status")
240
+ version: str = Field(default="1.0.0", description="API version")
241
+
242
+ # =============================================================================
243
+ # UTILITY FUNCTIONS
244
+ # =============================================================================
245
+
246
+ def calculate_checklist_metrics(checklist_data: Dict[str, Any]) -> Dict[str, Any]:
247
+ """
248
+ Calculate compliance metrics for a checklist
249
+
250
+ Args:
251
+ checklist_data: Dictionary containing checklist data
252
+
253
+ Returns:
254
+ Dictionary with calculated metrics (totalItems, completedItems, nonCompliantItems, complianceScore)
255
+ """
256
+ total_items = 0
257
+ completed_items = 0
258
+ non_compliant_items = 0
259
+
260
+ # Iterate through all sections and items
261
+ for section in checklist_data.get('sections', []):
262
+ for item in section.get('items', []):
263
+ total_items += 1
264
+
265
+ # Count completed items (those that are not N/A)
266
+ if item.get('compliance') != 'N/A':
267
+ completed_items += 1
268
+
269
+ # Count non-compliant items
270
+ if item.get('compliance') == 'Non-Compliant':
271
+ non_compliant_items += 1
272
+
273
+ # Calculate compliance score
274
+ compliance_score = (completed_items / total_items * 100) if total_items > 0 else 0
275
+
276
+ return {
277
+ 'totalItems': total_items,
278
+ 'completedItems': completed_items,
279
+ 'nonCompliantItems': non_compliant_items,
280
+ 'complianceScore': round(compliance_score, 2)
281
+ }
282
+
283
+ def serialize_checklist(checklist_doc: Dict[str, Any]) -> Dict[str, Any]:
284
+ """
285
+ Serialize MongoDB document to JSON-serializable format
286
+
287
+ Args:
288
+ checklist_doc: MongoDB document
289
+
290
+ Returns:
291
+ Dictionary with ObjectId converted to string
292
+ """
293
+ if checklist_doc and '_id' in checklist_doc:
294
+ checklist_doc['_id'] = str(checklist_doc['_id'])
295
+
296
+ # Convert datetime objects to ISO strings
297
+ for field in ['createdAt', 'updatedAt', 'verificationDate']:
298
+ if field in checklist_doc and checklist_doc[field]:
299
+ # Only convert if it's a datetime object, not a string
300
+ if hasattr(checklist_doc[field], 'isoformat'):
301
+ checklist_doc[field] = checklist_doc[field].isoformat()
302
+
303
+ # Convert datetime objects in items
304
+ for section in checklist_doc.get('sections', []):
305
+ for item in section.get('items', []):
306
+ if 'checkedAt' in item and item['checkedAt']:
307
+ # Only convert if it's a datetime object, not a string
308
+ if hasattr(item['checkedAt'], 'isoformat'):
309
+ item['checkedAt'] = item['checkedAt'].isoformat()
310
+
311
+ return checklist_doc
312
+
313
+ # =============================================================================
314
+ # API ENDPOINTS
315
+ # =============================================================================
316
+
317
+ @app.on_event("startup")
318
+ async def startup_event():
319
+ """
320
+ Application startup event handler
321
+
322
+ This function is called when the FastAPI application starts up.
323
+ It initializes the MongoDB connection and sets up the database.
324
+ """
325
+ logger.info("Starting Audit Checklist API server...")
326
+ await connect_to_mongodb()
327
+ logger.info(f"Server ready on port {PORT}")
328
+
329
+ @app.on_event("shutdown")
330
+ async def shutdown_event():
331
+ """
332
+ Application shutdown event handler
333
+
334
+ This function is called when the FastAPI application shuts down.
335
+ It gracefully closes the MongoDB connection.
336
+ """
337
+ logger.info("Shutting down Audit Checklist API server...")
338
+ await close_mongodb_connection()
339
+
340
+ @app.get("/", response_model=Dict[str, str])
341
+ async def root():
342
+ """
343
+ Root endpoint providing basic API information
344
+
345
+ Returns:
346
+ Dictionary with API name and version
347
+ """
348
+ return {
349
+ "message": "Audit Checklist API",
350
+ "version": "1.0.0",
351
+ "docs": "/docs",
352
+ "health": "/health"
353
+ }
354
+
355
+ @app.get("/health", response_model=HealthResponse)
356
+ async def health_check():
357
+ """
358
+ Health check endpoint for monitoring and load balancers
359
+
360
+ This endpoint checks the status of the API server and database connection.
361
+ It's useful for health monitoring, load balancers, and DevOps automation.
362
+
363
+ Returns:
364
+ HealthResponse with current status information
365
+ """
366
+ try:
367
+ # Test database connection
368
+ await client.admin.command('ping')
369
+ db_status = "connected"
370
+ except Exception as e:
371
+ logger.error(f"Database health check failed: {e}")
372
+ db_status = "disconnected"
373
+
374
+ return HealthResponse(
375
+ status="healthy" if db_status == "connected" else "unhealthy",
376
+ timestamp=datetime.utcnow(),
377
+ database=db_status,
378
+ version="1.0.0"
379
+ )
380
+
381
+ @app.get("/api/checklist/{user_id}", response_model=ChecklistResponse)
382
+ async def get_checklist(user_id: str):
383
+ """
384
+ Retrieve checklist data for a specific user
385
+
386
+ This endpoint fetches the checklist data for a given user ID. If no checklist
387
+ exists for the user, it creates a new one with the default structure.
388
+
389
+ Args:
390
+ user_id: Unique identifier for the user
391
+
392
+ Returns:
393
+ ChecklistResponse containing the checklist data
394
+
395
+ Raises:
396
+ HTTPException: If database operation fails
397
+ """
398
+ try:
399
+ logger.info(f"Fetching checklist for user: {user_id}")
400
+
401
+ # Query the database for the user's checklist
402
+ # Find the most recent checklist for the user
403
+ checklist_doc = await db.checklists.find_one(
404
+ {"userId": user_id},
405
+ sort=[("createdAt", -1)] # Get the most recent one
406
+ )
407
+
408
+ if not checklist_doc:
409
+ # Create new checklist if none exists
410
+ logger.info(f"No checklist found for user {user_id}, creating new one")
411
+
412
+ # Default checklist structure - Complete 38-item audit checklist
413
+ default_checklist = {
414
+ "userId": user_id,
415
+ "title": "Checklist di Audit Operativo (38 Controlli)",
416
+ "sections": [
417
+ {
418
+ "id": "S1",
419
+ "title": "1. PERSONALE E IGIENE (6 Controlli)",
420
+ "icon": "Users",
421
+ "items": [
422
+ {"id": "I1.1", "requirement": "Indumenti da lavoro puliti (divisa, grembiule).", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
423
+ {"id": "I1.2", "requirement": "Scarpe antinfortunistiche / Calzature pulite.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
424
+ {"id": "I1.3", "requirement": "Cuffie e/o Retine per capelli indossate correttamente.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
425
+ {"id": "I1.4", "requirement": "Assenza di gioielli, piercing visibili, unghie lunghe.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
426
+ {"id": "I1.5", "requirement": "Lavaggio mani documentato all'ingresso/reingresso.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
427
+ {"id": "I1.6", "requirement": "Assenza di cibo/bevande non autorizzate nelle aree produttive.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}
428
+ ]
429
+ },
430
+ {
431
+ "id": "S2",
432
+ "title": "2. STRUTTURE E IMPIANTI (6 Controlli)",
433
+ "icon": "Building",
434
+ "items": [
435
+ {"id": "I2.1", "requirement": "Illuminazione adeguata e funzionante in tutte le aree.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
436
+ {"id": "I2.2", "requirement": "Porte esterne/interne in buone condizioni e chiuse correttamente (sigillatura).", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
437
+ {"id": "I2.3", "requirement": "Integrità di pavimenti, pareti e soffitti (assenza di crepe/danni).", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
438
+ {"id": "I2.4", "requirement": "Controllo vetri / lampade protette o anti-frantumazione.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
439
+ {"id": "I2.5", "requirement": "Condizioni igieniche dei servizi igienici e spogliatoi (pulizia e dotazioni).", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
440
+ {"id": "I2.6", "requirement": "Ventilazione e aspirazione funzionanti, pulite e non ostruite.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}
441
+ ]
442
+ },
443
+ {
444
+ "id": "S3",
445
+ "title": "3. GESTIONE E IGIENE AMBIENTALE (4 Controlli)",
446
+ "icon": "Package",
447
+ "items": [
448
+ {"id": "I3.1", "requirement": "Contenitori dei rifiuti puliti, chiusi e identificati.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
449
+ {"id": "I3.2", "requirement": "Separazione corretta dei rifiuti (es. umido, secco, plastica).", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
450
+ {"id": "I3.3", "requirement": "Area di stoccaggio rifiuti (interna ed esterna) ordinata e sanificata.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
451
+ {"id": "I3.4", "requirement": "Frequenza di rimozione dei rifiuti adeguata a prevenire accumuli.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}
452
+ ]
453
+ },
454
+ {
455
+ "id": "S4",
456
+ "title": "4. CONTROLLO PROCESSO E QUALITÀ (6 Controlli)",
457
+ "icon": "Settings",
458
+ "items": [
459
+ {"id": "I4.1", "requirement": "Monitoraggio e registrazione dei Punti Critici di Controllo (CCP).", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
460
+ {"id": "I4.2", "requirement": "Procedure di Buona Fabbricazione (GMP) rispettate (es. pulizia prima dell'avvio).", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
461
+ {"id": "I4.3", "requirement": "Verifica e calibrazione del Metal Detector eseguita e registrata.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
462
+ {"id": "I4.4", "requirement": "Corretta identificazione dei lotti di materie prime e semilavorati.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
463
+ {"id": "I4.5", "requirement": "Rispettate le temperature di stoccaggio dei prodotti finiti e intermedi.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
464
+ {"id": "I4.6", "requirement": "Corretta gestione, etichettatura e isolamento dei prodotti non conformi.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}
465
+ ]
466
+ },
467
+ {
468
+ "id": "S5",
469
+ "title": "5. CONTROLLO INFESTANTI (5 Controlli)",
470
+ "icon": "Shield",
471
+ "items": [
472
+ {"id": "I5.1", "requirement": "Assenza di tracce visibili di roditori, insetti o uccelli.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
473
+ {"id": "I5.2", "requirement": "Stazioni di monitoraggio/trappole numerate, integre e registrate.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
474
+ {"id": "I5.3", "requirement": "Mappe e registri delle trappole aggiornati e disponibili.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
475
+ {"id": "I5.4", "requirement": "Barriere fisiche (es. reti, spazzole) anti-intrusione in buone condizioni.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
476
+ {"id": "I5.5", "requirement": "Prodotti chimici di controllo infestanti stoccati in modo sicuro e separato.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}
477
+ ]
478
+ },
479
+ {
480
+ "id": "S6",
481
+ "title": "6. MANUTENZIONE E VETRI (6 Controlli)",
482
+ "icon": "Settings",
483
+ "items": [
484
+ {"id": "I6.1", "requirement": "Piano di manutenzione preventiva e correttiva rispettato e registrato.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
485
+ {"id": "I6.2", "requirement": "Lubrificanti e oli utilizzati autorizzati per uso alimentare (se richiesto).", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
486
+ {"id": "I6.3", "requirement": "Registri di calibrazione/verifica degli strumenti di misura critici aggiornati.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
487
+ {"id": "I6.4", "requirement": "Gestione dei vetri rotti (registri rotture) aggiornata e conforme alla procedura.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
488
+ {"id": "I6.5", "requirement": "Assenza di perdite, gocciolamenti o cavi scoperti da impianti/macchinari.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
489
+ {"id": "I6.6", "requirement": "Area di deposito utensili e pezzi di ricambio ordinata e pulita.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}
490
+ ]
491
+ },
492
+ {
493
+ "id": "S7",
494
+ "title": "7. DOCUMENTAZIONE E FORMAZIONE (5 Controlli)",
495
+ "icon": "FileText",
496
+ "items": [
497
+ {"id": "I7.1", "requirement": "Documentazione di processo (procedure, istruzioni) aggiornata e disponibile.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
498
+ {"id": "I7.2", "requirement": "Registri di formazione del personale aggiornati e verificabili.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
499
+ {"id": "I7.3", "requirement": "Certificazioni e attestati del personale validi e aggiornati.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
500
+ {"id": "I7.4", "requirement": "Controllo documenti (versioni, distribuzione, obsolescenza) conforme.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
501
+ {"id": "I7.5", "requirement": "Archiviazione e conservazione documenti secondo i requisiti normativi.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}
502
+ ]
503
+ }
504
+ ],
505
+ "totalItems": 38,
506
+ "completedItems": 0,
507
+ "nonCompliantItems": 0,
508
+ "complianceScore": 0.0,
509
+ "verificationDate": None,
510
+ "createdAt": datetime.utcnow(),
511
+ "updatedAt": datetime.utcnow()
512
+ }
513
+
514
+ # Insert the new checklist
515
+ result = await db.checklists.insert_one(default_checklist)
516
+ checklist_doc = await db.checklists.find_one({"_id": result.inserted_id})
517
+ logger.info(f"Created new checklist for user {user_id}")
518
+
519
+ # Serialize the document
520
+ serialized_checklist = serialize_checklist(checklist_doc)
521
+
522
+ return ChecklistResponse(
523
+ success=True,
524
+ data=ChecklistData(**serialized_checklist),
525
+ message="Checklist retrieved successfully"
526
+ )
527
+
528
+ except Exception as e:
529
+ logger.error(f"Error retrieving checklist for user {user_id}: {e}")
530
+ raise HTTPException(
531
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
532
+ detail=f"Failed to retrieve checklist: {str(e)}"
533
+ )
534
+
535
+ @app.put("/api/checklist/{user_id}", response_model=ChecklistResponse)
536
+ async def save_checklist(user_id: str, checklist_data: dict):
537
+ """
538
+ Save or update checklist data for a specific user
539
+
540
+ This endpoint saves the checklist data to the database. It calculates
541
+ compliance metrics and updates the timestamp.
542
+
543
+ Args:
544
+ user_id: Unique identifier for the user
545
+ checklist_data: The checklist data to save
546
+
547
+ Returns:
548
+ ChecklistResponse confirming the save operation
549
+
550
+ Raises:
551
+ HTTPException: If database operation fails
552
+ """
553
+ try:
554
+ logger.info(f"Saving checklist for user: {user_id}")
555
+
556
+ # Use the dictionary directly (already converted from JSON)
557
+ checklist_dict = checklist_data.copy()
558
+
559
+ # Calculate compliance metrics
560
+ metrics = calculate_checklist_metrics(checklist_dict)
561
+ checklist_dict.update(metrics)
562
+
563
+ # Handle image data for non-compliant checklists
564
+ compliance_score = metrics.get('complianceScore', 0.0)
565
+ if compliance_score < 100.0 and 'collectedImages' in checklist_dict and checklist_dict['collectedImages']:
566
+ # Process collected images from individual items
567
+ collected_images = checklist_dict['collectedImages']
568
+ logger.info(f"Storing {len(collected_images)} images for non-compliant checklist (score: {compliance_score}%)")
569
+
570
+ # Store the collected images in the checklist data
571
+ checklist_dict['imageData'] = collected_images
572
+ checklist_dict['imageType'] = 'multiple_items'
573
+ elif compliance_score < 100.0:
574
+ logger.info(f"No image data provided for non-compliant checklist (score: {compliance_score}%)")
575
+
576
+ # Update timestamps
577
+ checklist_dict['updatedAt'] = datetime.utcnow()
578
+
579
+ # Check if this is an update to existing checklist or new save
580
+ existing_checklist = await db.checklists.find_one({"userId": user_id})
581
+
582
+ if existing_checklist:
583
+ # Update existing checklist
584
+ checklist_dict['createdAt'] = existing_checklist.get('createdAt', datetime.utcnow())
585
+
586
+ # Remove _id if it exists to prevent immutable field error
587
+ if '_id' in checklist_dict:
588
+ del checklist_dict['_id']
589
+
590
+ result = await db.checklists.update_one(
591
+ {"userId": user_id},
592
+ {"$set": checklist_dict}
593
+ )
594
+
595
+ if result.modified_count > 0:
596
+ logger.info(f"Updated existing checklist for user {user_id}")
597
+ message = "Checklist updated successfully"
598
+ else:
599
+ logger.error(f"Failed to update checklist for user {user_id}")
600
+ raise HTTPException(
601
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
602
+ detail="Failed to update checklist"
603
+ )
604
+ else:
605
+ # Create new checklist
606
+ checklist_dict['createdAt'] = datetime.utcnow()
607
+
608
+ # Remove _id if it exists to let MongoDB generate a new one
609
+ if '_id' in checklist_dict:
610
+ del checklist_dict['_id']
611
+
612
+ result = await db.checklists.insert_one(checklist_dict)
613
+
614
+ if result.inserted_id:
615
+ logger.info(f"Created new checklist for user {user_id}")
616
+ message = "Checklist created successfully"
617
+ else:
618
+ logger.error(f"Failed to create checklist for user {user_id}")
619
+ raise HTTPException(
620
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
621
+ detail="Failed to save checklist"
622
+ )
623
+
624
+ # Retrieve the updated/created checklist
625
+ if existing_checklist:
626
+ # For updates, get the updated document
627
+ updated_checklist = await db.checklists.find_one({"userId": user_id})
628
+ serialized_checklist = serialize_checklist(updated_checklist)
629
+ else:
630
+ # For new checklists, get the newly created document
631
+ created_checklist = await db.checklists.find_one({"_id": result.inserted_id})
632
+ serialized_checklist = serialize_checklist(created_checklist)
633
+
634
+ return ChecklistResponse(
635
+ success=True,
636
+ data=serialized_checklist, # Return as dict instead of Pydantic model
637
+ message=message
638
+ )
639
+
640
+ except Exception as e:
641
+ logger.error(f"Error saving checklist for user {user_id}: {e}")
642
+ raise HTTPException(
643
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
644
+ detail=f"Failed to save checklist: {str(e)}"
645
+ )
646
+
647
+ @app.get("/api/checklists/by-user/{user_name}", response_model=Dict[str, Any])
648
+ async def get_checklists_by_user_name(user_name: str):
649
+ """
650
+ Retrieve all checklists for a specific user name
651
+
652
+ Args:
653
+ user_name: The name of the user to retrieve checklists for
654
+
655
+ Returns:
656
+ Dictionary containing list of checklists for the user
657
+
658
+ Raises:
659
+ HTTPException: If database operation fails
660
+ """
661
+ try:
662
+ logger.info(f"Retrieving checklists for user: {user_name}")
663
+
664
+ # Find all checklists where metadata.userName matches
665
+ cursor = db.checklists.find({"metadata.userName": user_name})
666
+ checklists = []
667
+
668
+ async for checklist_doc in cursor:
669
+ serialized_checklist = serialize_checklist(checklist_doc)
670
+ checklists.append(serialized_checklist)
671
+
672
+ return {
673
+ "success": True,
674
+ "data": checklists,
675
+ "count": len(checklists),
676
+ "user_name": user_name
677
+ }
678
+
679
+ except Exception as e:
680
+ logger.error(f"Error retrieving checklists for user {user_name}: {e}")
681
+ raise HTTPException(
682
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
683
+ detail=f"Failed to retrieve checklists for user: {str(e)}"
684
+ )
685
+
686
+ @app.post("/api/checklist/{user_id}/image")
687
+ async def upload_checklist_image(user_id: str, image: UploadFile = File(...)):
688
+ """
689
+ Upload an image for a checklist (for non-compliant cases)
690
+
691
+ Args:
692
+ user_id: Unique identifier for the user
693
+ image: The image file to upload
694
+
695
+ Returns:
696
+ Dictionary containing the base64 encoded image data and metadata
697
+ """
698
+ try:
699
+ logger.info(f"Uploading image for user: {user_id}")
700
+
701
+ # Validate image file type
702
+ allowed_types = ["image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp"]
703
+ if image.content_type not in allowed_types:
704
+ raise HTTPException(
705
+ status_code=status.HTTP_400_BAD_REQUEST,
706
+ detail=f"Invalid image type. Allowed types: {', '.join(allowed_types)}"
707
+ )
708
+
709
+ # Read image data
710
+ image_data = await image.read()
711
+
712
+ # Encode to base64
713
+ base64_data = base64.b64encode(image_data).decode('utf-8')
714
+
715
+ # Create data URL
716
+ data_url = f"data:{image.content_type};base64,{base64_data}"
717
+
718
+ logger.info(f"Image uploaded successfully for user {user_id}, size: {len(image_data)} bytes")
719
+
720
+ return {
721
+ "success": True,
722
+ "data": {
723
+ "imageData": base64_data,
724
+ "imageType": image.content_type,
725
+ "dataUrl": data_url,
726
+ "size": len(image_data),
727
+ "filename": image.filename
728
+ },
729
+ "message": "Image uploaded successfully"
730
+ }
731
+
732
+ except HTTPException:
733
+ raise
734
+ except Exception as e:
735
+ logger.error(f"Error uploading image for user {user_id}: {e}")
736
+ raise HTTPException(
737
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
738
+ detail=f"Failed to upload image: {str(e)}"
739
+ )
740
+
741
+ @app.get("/api/checklists", response_model=Dict[str, Any])
742
+ async def get_all_checklists():
743
+ """
744
+ Retrieve all checklists (admin endpoint)
745
+
746
+ This endpoint returns all checklists in the database. It's intended for
747
+ administrative purposes and should be protected with proper authentication
748
+ in a production environment.
749
+
750
+ Returns:
751
+ Dictionary containing list of all checklists
752
+
753
+ Raises:
754
+ HTTPException: If database operation fails
755
+ """
756
+ try:
757
+ logger.info("Retrieving all checklists")
758
+
759
+ # Find all checklists
760
+ cursor = db.checklists.find({})
761
+ checklists = []
762
+
763
+ async for checklist_doc in cursor:
764
+ serialized_checklist = serialize_checklist(checklist_doc)
765
+ checklists.append(serialized_checklist)
766
+
767
+ return {
768
+ "success": True,
769
+ "data": checklists,
770
+ "count": len(checklists),
771
+ "message": f"Retrieved {len(checklists)} checklists"
772
+ }
773
+
774
+ except Exception as e:
775
+ logger.error(f"Error retrieving all checklists: {e}")
776
+ raise HTTPException(
777
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
778
+ detail=f"Failed to retrieve checklists: {str(e)}"
779
+ )
780
+
781
+ # =============================================================================
782
+ # ERROR HANDLERS
783
+ # =============================================================================
784
+
785
+ @app.exception_handler(HTTPException)
786
+ async def http_exception_handler(request, exc):
787
+ """Handle HTTP exceptions with consistent error format"""
788
+ logger.error(f"HTTP Exception: {exc.detail}")
789
+ return JSONResponse(
790
+ status_code=exc.status_code,
791
+ content={
792
+ "success": False,
793
+ "error": exc.detail,
794
+ "status_code": exc.status_code
795
+ }
796
+ )
797
+
798
+ @app.exception_handler(Exception)
799
+ async def general_exception_handler(request, exc):
800
+ """Handle general exceptions with logging"""
801
+ logger.error(f"Unhandled exception: {exc}", exc_info=True)
802
+ return JSONResponse(
803
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
804
+ content={
805
+ "success": False,
806
+ "error": "Internal server error",
807
+ "detail": str(exc) if os.getenv("DEBUG", "false").lower() == "true" else "An unexpected error occurred"
808
+ }
809
+ )
810
+
811
+ # =============================================================================
812
+ # MAIN EXECUTION
813
+ # =============================================================================
814
+
815
+ if __name__ == "__main__":
816
+ """
817
+ Main execution block for running the server
818
+
819
+ This block is executed when the script is run directly (not imported).
820
+ It starts the Uvicorn ASGI server with the configured settings.
821
+ """
822
+ import uvicorn
823
+
824
+ logger.info("Starting FastAPI server...")
825
+
826
+ # Run the server
827
+ uvicorn.run(
828
+ "main:app",
829
+ host="0.0.0.0",
830
+ port=PORT,
831
+ reload=True, # Enable auto-reload for development
832
+ log_level="info"
833
+ )
mongodb.env ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # MongoDB Configuration
2
+ MONGODB_URI=mongodb+srv://yakdhanali97:Syrou2206@cluster0.szf60ea.mongodb.net/audit_checklist?retryWrites=true&w=majority&appName=Cluster0
3
+
4
+ # Server Configuration
5
+ PORT=5000
6
+
7
+ # CORS Configuration
8
+ CORS_ORIGIN=*
requirements.txt CHANGED
@@ -10,11 +10,14 @@ pymongo==4.6.0 # MongoDB driver
10
  # Environment variables
11
  python-dotenv==1.0.0
12
 
13
- # CORS middleware
14
  python-multipart==0.0.6
15
 
16
  # Date/time handling
17
  python-dateutil==2.8.2
18
 
 
 
 
19
  # Additional utilities
20
  httpx==0.25.2 # For making HTTP requests if needed
 
10
  # Environment variables
11
  python-dotenv==1.0.0
12
 
13
+ # CORS middleware and file upload support
14
  python-multipart==0.0.6
15
 
16
  # Date/time handling
17
  python-dateutil==2.8.2
18
 
19
+ # Image processing (if needed for validation)
20
+ Pillow==10.0.1
21
+
22
  # Additional utilities
23
  httpx==0.25.2 # For making HTTP requests if needed