Rakshitjan commited on
Commit
f2df4bb
·
verified ·
1 Parent(s): f9b13f5

Create main.py

Browse files
Files changed (1) hide show
  1. main.py +542 -0
main.py ADDED
@@ -0,0 +1,542 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import base64
3
+ import math
4
+ from datetime import datetime, timezone
5
+ from zoneinfo import ZoneInfo
6
+ from io import BytesIO
7
+
8
+ import requests
9
+ from fastapi import FastAPI, HTTPException
10
+ from fastapi.responses import Response
11
+ from reportlab.pdfgen import canvas
12
+ from reportlab.lib.pagesizes import A4
13
+ from reportlab.lib.units import cm
14
+ from reportlab.lib.utils import ImageReader
15
+ from reportlab.lib import colors
16
+ from PIL import Image, ImageOps
17
+ from Crypto.Cipher import AES
18
+ from Crypto.Util.Padding import pad, unpad
19
+ from pydantic import BaseModel
20
+
21
+ # Configuration from environment variables
22
+ BASE_URL = os.getenv("BASE_URL", "https://rakshitjan-cps-b2c.hf.space")
23
+ ADMIN_EMAIL = os.getenv("ADMIN_EMAIL", "admin123@example.com")
24
+ ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "SecurePass123")
25
+ AES_KEY_STR = os.getenv("AES_KEY_STR", "7E892875A52C59A3B588306B13C31FBD")
26
+ AES_IV_STR = os.getenv("AES_IV_STR", "XYE45DKJ0967GFAZ")
27
+ GEOAPIFY_API_KEY = os.getenv("GEOAPIFY_API_KEY", "ceaea8a8a8f14f46b73221384eb4b5f0")
28
+ LOGO_PATH = os.getenv("LOGO_PATH", "logo.png")
29
+
30
+ app = FastAPI(title="Case Report Generator", version="1.0.0")
31
+
32
+ # AES Encryption/Decryption functions
33
+ def aes_encrypt(plaintext: str, key_str: str, iv_str: str) -> str:
34
+ key = key_str.encode("utf-8")
35
+ iv = iv_str.encode("utf-8")
36
+ cipher = AES.new(key, AES.MODE_CBC, iv)
37
+ ct = cipher.encrypt(pad(plaintext.encode("utf-8"), AES.block_size))
38
+ return base64.b64encode(ct).decode("utf-8")
39
+
40
+ def aes_decrypt(enc_b64: str, key_str: str, iv_str: str) -> str:
41
+ try:
42
+ key = key_str.encode("utf-8")
43
+ iv = iv_str.encode("utf-8")
44
+ cipher = AES.new(key, AES.MODE_CBC, iv)
45
+ pt = unpad(cipher.decrypt(base64.b64decode(enc_b64)), AES.block_size)
46
+ return pt.decode("utf-8")
47
+ except Exception:
48
+ return enc_b64
49
+
50
+ # API Client functions
51
+ session = requests.Session()
52
+ session.headers.update({"Content-Type": "application/json"})
53
+
54
+ def admin_login():
55
+ payload = {"email": aes_encrypt(ADMIN_EMAIL, AES_KEY_STR, AES_IV_STR),
56
+ "password": ADMIN_PASSWORD}
57
+ r = session.post(f"{BASE_URL}/api/admin/login", json=payload, timeout=30)
58
+ data = r.json()
59
+ if r.status_code != 200 or "token" not in data:
60
+ raise RuntimeError(f"Login failed: {data}")
61
+ session.headers.update({"Authorization": f"Bearer {data['token']}"})
62
+ print("✅ Admin Logged in")
63
+ return data["token"]
64
+
65
+ def get_case(case_id: str):
66
+ r = session.get(f"{BASE_URL}/api/cases/{case_id}", timeout=30)
67
+ r.raise_for_status()
68
+ return r.json()
69
+
70
+ def get_files(case_id: str):
71
+ r = session.get(f"{BASE_URL}/api/cases/{case_id}/files", timeout=60)
72
+ r.raise_for_status()
73
+ return r.json().get("files", [])
74
+
75
+ def download_file(file_id: str):
76
+ r = session.get(f"{BASE_URL}/api/files/{file_id}", timeout=60)
77
+ if r.status_code == 200:
78
+ return r.content
79
+ raise RuntimeError(f"File download failed for {file_id}: {r.status_code}")
80
+
81
+ # Geoapify functions
82
+ def forward_geocode(address: str):
83
+ if not address: return (None, None)
84
+ url = f"https://api.geoapify.com/v1/geocode/search?text={requests.utils.quote(address)}&apiKey={GEOAPIFY_API_KEY}"
85
+ res = requests.get(url, timeout=20).json()
86
+ if res.get("features"):
87
+ p = res["features"][0]["properties"]
88
+ return p.get("lat"), p.get("lon")
89
+ return (None, None)
90
+
91
+ def reverse_geocode(lat: float, lng: float):
92
+ if lat is None or lng is None: return "Unknown Address"
93
+ url = f"https://api.geoapify.com/v1/geocode/reverse?lat={lat}&lon={lng}&apiKey={GEOAPIFY_API_KEY}"
94
+ res = requests.get(url, timeout=20).json()
95
+ return res.get("features", [{}])[0].get("properties", {}).get("formatted", "Unknown Address")
96
+
97
+ def static_map_with_path(capture_lat, capture_lng, case_lat=None, case_lng=None, zoom: int = 17, size="1800x600"):
98
+ """Map shows capture pin (red), case pin (blue), path between them."""
99
+ if capture_lat is None or capture_lng is None:
100
+ return None
101
+ parts = [
102
+ f"center=lonlat:{capture_lng},{capture_lat}",
103
+ f"zoom={zoom}",
104
+ f"size={size}",
105
+ f"scaleFactor=2",
106
+ f"marker=lonlat:{capture_lng},{capture_lat};color:%23ff0000;size:large"
107
+ ]
108
+ if case_lat is not None and case_lng is not None:
109
+ parts.append(f"marker=lonlat:{case_lng},{case_lat};color:%23007bff;size:large")
110
+ parts.append(f"path=color:%23ff0000;weight:4|lonlat:{case_lng},{case_lat}|lonlat:{capture_lng},{capture_lat}")
111
+
112
+ url = "https://maps.geoapify.com/v1/staticmap?" + "&".join(parts) + f"&apiKey={GEOAPIFY_API_KEY}"
113
+ r = requests.get(url, timeout=30)
114
+ if r.status_code == 200:
115
+ return Image.open(BytesIO(r.content)).convert("RGB")
116
+ return None
117
+
118
+ def haversine_km(lat1, lon1, lat2, lon2):
119
+ if None in (lat1, lon1, lat2, lon2): return None
120
+ R = 6371.0 # km
121
+ phi1, phi2 = math.radians(lat1), math.radians(lat2)
122
+ dphi = math.radians(lat2 - lat1)
123
+ dlambda = math.radians(lon2 - lon1)
124
+ a = math.sin(dphi/2)**2 + math.cos(phi1)*math.cos(phi2)*math.sin(dlambda/2)**2
125
+ return 2 * R * math.atan2(math.sqrt(a), math.sqrt(1-a))
126
+
127
+ # Utility functions
128
+ IST = ZoneInfo("Asia/Kolkata")
129
+
130
+ def fmt_ist(dt_str: str) -> str:
131
+ """ISO string -> 'DD/MM/YYYY hh:mm:ss AM/PM IST'"""
132
+ if not dt_str:
133
+ return "N/A"
134
+ try:
135
+ if dt_str.endswith("Z"):
136
+ dt = datetime.fromisoformat(dt_str.replace("Z", "+00:00"))
137
+ else:
138
+ dt = datetime.fromisoformat(dt_str)
139
+ dt_ist = dt.astimezone(IST)
140
+ return dt_ist.strftime("%d/%m/%Y %I:%M:%S %p IST")
141
+ except Exception:
142
+ return dt_str
143
+
144
+ def exported_on_str() -> str:
145
+ now_ist = datetime.now(IST)
146
+ return now_ist.strftime("%B %d, %Y")
147
+
148
+ def draw_wrapped_label_value(c, x, y, label, text, max_width_cm=16.0):
149
+ """Draw 'Label: value...' with word wrapping; following lines indented."""
150
+ max_w = max_width_cm * cm
151
+ c.setFont("Helvetica", 10)
152
+ label_w = c.stringWidth(f"{label}: ", "Helvetica", 10)
153
+ words = (text or "N/A")
154
+ words = str(words).split()
155
+ first = True
156
+ line = ""
157
+ width = 0
158
+ lines = []
159
+
160
+ for w in words:
161
+ w_w = c.stringWidth(w + " ", "Helvetica", 10)
162
+ if first:
163
+ if label_w + width + w_w < max_w:
164
+ line += w + " "; width += w_w
165
+ else:
166
+ lines.append(f"{label}: {line.strip()}")
167
+ first = False
168
+ line, width = w + " ", w_w
169
+ else:
170
+ if width + w_w < max_w:
171
+ line += w + " "; width += w_w
172
+ else:
173
+ lines.append(" " * int(label_w/6) + line.strip())
174
+ line, width = w + " ", w_w
175
+
176
+ if first:
177
+ lines.append(f"{label}: {line.strip()}")
178
+ else:
179
+ if line:
180
+ lines.append(" " * int(label_w/6) + line.strip())
181
+
182
+ for ln in lines:
183
+ c.drawString(x, y, ln); y -= 0.45*cm
184
+ return y
185
+
186
+ DISPLAY_NAME = {
187
+ "aadhaar_photo": "Photo of Aadhar",
188
+ "customer_photo": "Customer's Photo",
189
+ "residence_photo": "Residence Photo",
190
+ "business_photo": "Business Address Photo"
191
+ }
192
+
193
+ def display_type(file_type: str) -> str:
194
+ return DISPLAY_NAME.get(file_type, file_type or "Upload")
195
+
196
+ def make_thumb(img_bytes: bytes, max_w=860, max_h=640):
197
+ img = Image.open(BytesIO(img_bytes)).convert("RGB")
198
+ img.thumbnail((max_w, max_h))
199
+ return ImageOps.expand(img, border=1, fill=(170,170,170))
200
+
201
+ # PDF Generation functions
202
+ W, H = A4
203
+ MARGIN = 1.2*cm
204
+
205
+ def draw_header(c, title):
206
+ c.setFillColor(colors.HexColor("#0F61A8"))
207
+ c.rect(0, H-2.4*cm, W, 2.4*cm, fill=1, stroke=0)
208
+
209
+ title_x = MARGIN
210
+ if os.path.exists(LOGO_PATH):
211
+ try:
212
+ c.drawImage(LOGO_PATH, MARGIN, H-2.2*cm,
213
+ width=2.4*cm, height=2.0*cm, preserveAspectRatio=True, mask='auto')
214
+ title_x = MARGIN + 2.6*cm
215
+ except:
216
+ pass
217
+
218
+ c.setFillColor(colors.white)
219
+ c.setFont("Helvetica-Bold", 15)
220
+ c.drawString(title_x, H-1.2*cm, title)
221
+
222
+ c.setFont("Helvetica", 10)
223
+ exp = f"Exported on {exported_on_str()}"
224
+ c.drawRightString(W - MARGIN, H-1.0*cm, exp)
225
+ c.setFillColor(colors.black)
226
+
227
+ def draw_footer(c, page):
228
+ c.setFont("Helvetica", 9)
229
+ c.drawRightString(W - MARGIN, 1.0*cm, f"Page {page}")
230
+
231
+ def section_title(c, text, x, y):
232
+ c.setFont("Helvetica-Bold", 14)
233
+ c.drawString(x, y, text)
234
+ return y - 0.9*cm
235
+
236
+ def label_value(c, x, y, label, value):
237
+ c.setFont("Helvetica-Bold", 10)
238
+ c.drawString(x, y, f"{label}:")
239
+ c.setFont("Helvetica", 10)
240
+ c.drawString(x + 4.8*cm, y, str("N/A" if value in (None, "", []) else value))
241
+
242
+ def build_pdf(case: dict, files: list, out_path: str):
243
+ c = canvas.Canvas(out_path, pagesize=A4)
244
+ page = 1
245
+
246
+ ad = case.get("demographic_details", {}).get("address_details", {})
247
+ case_addr = f"{ad.get('residential_address','')}, {ad.get('city','')}, {ad.get('state','')} {ad.get('pincode','')}"
248
+ case_lat, case_lng = forward_geocode(case_addr)
249
+
250
+ title = f"Name : {case.get('case_applicant_name','')}"
251
+ draw_header(c, title)
252
+ y = H - 3.4*cm
253
+
254
+ y = section_title(c, "Customer Details", MARGIN, y)
255
+ label_value(c, MARGIN, y, "Customer Name", case.get("case_applicant_name")); y -= 0.6*cm
256
+ label_value(c, MARGIN, y, "Customer ID", case.get("case_id")); y -= 0.6*cm
257
+ label_value(c, MARGIN, y, "Phone Number", case.get("case_applicant_contact")); y -= 0.6*cm
258
+ email_id = case.get("demographic_details",{}).get("contact_information",{}).get("email_id")
259
+ label_value(c, MARGIN, y, "Email", email_id); y -= 0.6*cm
260
+ y = draw_wrapped_label_value(c, MARGIN, y, "Customer Address", case_addr, max_width_cm=18.0); y -= 0.4*cm
261
+
262
+ y = section_title(c, "Inspection Summary", MARGIN, y)
263
+ c.setFont("Helvetica", 10)
264
+ c.drawString(MARGIN, y, f"Request Started: {fmt_ist(case.get('created_at'))}"); y -= 0.5*cm
265
+ c.drawString(MARGIN, y, f"Total Uploads: {len(files)} uploads"); y -= 0.5*cm
266
+ c.drawString(MARGIN, y, f"Status: {case.get('status')}"); y -= 0.5*cm
267
+ c.drawString(MARGIN, y, f"Priority: {case.get('priority')}"); y -= 0.7*cm
268
+
269
+ if case_lat:
270
+ smap = static_map_with_path(case_lat, case_lng, case_lat, case_lng, zoom=12)
271
+ if smap:
272
+ map_h = H/2 + 2*cm
273
+ map_w = W - 2*MARGIN
274
+ c.drawImage(
275
+ ImageReader(smap),
276
+ MARGIN,
277
+ y - map_h,
278
+ width=map_w,
279
+ height=map_h,
280
+ preserveAspectRatio=True,
281
+ mask='auto'
282
+ )
283
+ y -= map_h + 0.8*cm
284
+
285
+ draw_footer(c, page)
286
+ c.showPage(); page += 1
287
+
288
+ draw_header(c, title)
289
+ y = H - 3.4*cm
290
+ c.setFont("Helvetica-Bold", 14)
291
+ c.drawString(MARGIN, y, "Image Thumbnails")
292
+ y -= 0.8*cm
293
+
294
+ x0, y0 = MARGIN, y
295
+ col_w = (W - 2*MARGIN) / 3
296
+ row_h = 6.1*cm
297
+
298
+ for i, f in enumerate(files[:6]):
299
+ img = make_thumb(f["_bin"])
300
+ col = i % 3
301
+ row = i // 3
302
+ xi = x0 + col * col_w + 0.2*cm
303
+ yi = y0 - row * row_h - 4.6*cm
304
+ c.drawImage(ImageReader(img), xi, yi,
305
+ width=col_w-0.6*cm, height=4.0*cm,
306
+ preserveAspectRatio=True, mask='auto')
307
+ c.setFont("Helvetica", 9)
308
+ c.drawString(xi, yi - 0.3*cm,
309
+ f"Upload {i+1} — {display_type(f.get('file_type'))}")
310
+
311
+ draw_footer(c, page)
312
+ c.showPage(); page += 1
313
+
314
+ draw_header(c, title)
315
+ y = H - 3.4*cm
316
+
317
+ y = section_title(c, "Demographic Details", MARGIN, y)
318
+ demo = case.get("demographic_details", {}) or {}
319
+ ci = demo.get("contact_information", {}) or {}
320
+ pd = demo.get("personal_details", {}) or {}
321
+ label_value(c, MARGIN, y, "Aadhaar Photo Match", demo.get("aadhaar_photo_match")); y -= 0.5*cm
322
+ label_value(c, MARGIN, y, "Email ID", ci.get("email_id")); y -= 0.5*cm
323
+ label_value(c, MARGIN, y, "Mobile Number", ci.get("mobile_number")); y -= 0.5*cm
324
+ label_value(c, MARGIN, y, "Gender", pd.get("gender")); y -= 0.5*cm
325
+ label_value(c, MARGIN, y, "Education", pd.get("education")); y -= 0.5*cm
326
+ label_value(c, MARGIN, y, "Family Members", pd.get("number_of_family_members")); y -= 1.0*cm
327
+
328
+ y = section_title(c, "Business Details", MARGIN, y)
329
+ bd = case.get("business_details", {}) or {}
330
+ ei = bd.get("enterprise_information", {}) or {}
331
+ bld = bd.get("business_location_details", {}) or {}
332
+ ba = bd.get("business_address", {}) or {}
333
+ bact= bd.get("business_activity", {}) or {}
334
+ binfo=bd.get("business_info", {}) or {}
335
+ label_value(c, MARGIN, y, "Enterprise Name", ei.get("enterprise_name")); y -= 0.5*cm
336
+ label_value(c, MARGIN, y, "Organization Type", ei.get("type_of_organization")); y -= 0.5*cm
337
+ label_value(c, MARGIN, y, "Business Location Type", bld.get("business_location")); y -= 0.5*cm
338
+ label_value(c, MARGIN, y, "Address", ba.get("address")); y -= 0.5*cm
339
+ label_value(c, MARGIN, y,
340
+ "City/District/State/Pincode",
341
+ f"{ba.get('city','')}, {ba.get('district','')}, {ba.get('state','')} - {ba.get('pincode','')}")
342
+ y -= 1.0*cm
343
+
344
+ y = section_title(c, "Financial Details", MARGIN, y)
345
+ fin = case.get("financial_details", {}) or {}
346
+ bfi = fin.get("business_financial_information", {}) or {}
347
+ ffi = fin.get("family_financial_information", {}) or {}
348
+ def fmt_inr(v):
349
+ try:
350
+ return f"{int(v):,} INR"
351
+ except:
352
+ return "N/A"
353
+
354
+ label_value(c, MARGIN, y, "Monthly Income", fmt_inr(bfi.get("monthly_income_from_business"))); y -= 0.5*cm
355
+ label_value(c, MARGIN, y, "Monthly Expense", fmt_inr(bfi.get("monthly_expense_of_business"))); y -= 0.5*cm
356
+ label_value(c, MARGIN, y, "Family Income", fmt_inr(ffi.get("monthly_family_income"))); y -= 0.5*cm
357
+
358
+ draw_footer(c, page)
359
+ c.showPage(); page += 1
360
+
361
+ for idx, f in enumerate(files, start=1):
362
+ draw_header(c, title)
363
+ y = H - 3.4*cm
364
+
365
+ c.setFont("Helvetica-Bold", 14)
366
+ c.drawString(MARGIN, y, f"Upload {idx} — {display_type(f.get('file_type'))}")
367
+ y -= 0.8*cm
368
+
369
+ gl = f.get("geo_location", {}) or {}
370
+ lat = gl.get("lat")
371
+ lng = gl.get("lng")
372
+
373
+ img = make_thumb(f["_bin"])
374
+ c.drawImage(ImageReader(img), MARGIN, y-9.8*cm,
375
+ width=8.6*cm, height=9.0*cm, preserveAspectRatio=True, mask='auto')
376
+
377
+ mimg = static_map_with_path(lat, lng, case_lat, case_lng, zoom=17)
378
+ if mimg:
379
+ c.drawImage(ImageReader(mimg), 11.0*cm, y-9.8*cm,
380
+ width=8.0*cm, height=9.0*cm,
381
+ preserveAspectRatio=True, mask='auto')
382
+
383
+ yb = y - 10.4*cm
384
+ rev = reverse_geocode(lat, lng)
385
+ yb = draw_wrapped_label_value(c, MARGIN, yb, "Location Address", rev, max_width_cm=18.0)
386
+
387
+ dist_km = haversine_km(case_lat, case_lng, lat, lng) if (lat is not None) else None
388
+ dist_m = round(dist_km * 1000) if dist_km else None
389
+ c.setFont("Helvetica", 10)
390
+ c.drawString(MARGIN, yb,
391
+ f"Distance from case location: {dist_m} meters" if dist_m else "Distance from case location: N/A")
392
+ yb -= 0.7*cm
393
+
394
+ up_at = fmt_ist(f.get("uploaded_at"))
395
+ c.setFont("Helvetica", 10)
396
+ c.drawString(MARGIN, yb, f"Uploaded At: {up_at}")
397
+ yb -= 1.0*cm
398
+
399
+ c.setFont("Helvetica-Bold", 12)
400
+ c.drawString(MARGIN, yb, "Metadata & Sensor Information")
401
+ yb -= 0.6*cm
402
+ c.setFont("Helvetica", 10)
403
+ c.drawString(MARGIN, yb, f"Server Time: {up_at}")
404
+ yb -= 0.6*cm
405
+ c.drawString(MARGIN, yb, f"Device Time: {up_at}")
406
+ yb -= 0.6*cm
407
+ c.drawString(MARGIN, yb,
408
+ f"Accuracy: {dist_m} meters" if dist_m else "Distance from case location: N/A")
409
+ yb -= 0.6*cm
410
+ if lat:
411
+ c.drawString(MARGIN, yb, f"Geo: Lat {lat:.6f}, Lng {lng:.6f}")
412
+ else:
413
+ c.drawString(MARGIN, yb, "Geo: N/A")
414
+ yb -= 0.6*cm
415
+
416
+ draw_footer(c, page)
417
+ c.showPage(); page += 1
418
+
419
+ draw_header(c, title)
420
+ y = H - 3.4*cm
421
+ c.setFont("Helvetica-Bold", 14)
422
+ c.drawString(MARGIN, y, "Inspection Timeline")
423
+ y -= 0.8*cm
424
+
425
+ c.setFillColorRGB(0.88, 0.97, 0.90)
426
+ c.roundRect(MARGIN, y-0.8*cm, W-2*MARGIN, 1.1*cm, 0.3*cm, fill=1, stroke=0)
427
+ c.setFillColorRGB(0.12, 0.45, 0.25)
428
+ c.setFont("Helvetica-Bold", 12)
429
+ c.drawString(MARGIN + 0.6*cm, y-0.4*cm, "● Ready for Review")
430
+ c.setFillColor(colors.black)
431
+ y -= 2.0*cm
432
+
433
+ events = []
434
+ def evt(text, ts, sub=None):
435
+ if ts:
436
+ events.append((text, ts, sub))
437
+
438
+ evt("Case Created", case.get("created_at"))
439
+ evt("Assigned to Agent", case.get("assigned_at"))
440
+ evt("Accepted by Agent", case.get("accepted_at"))
441
+
442
+ for f in files:
443
+ sub = reverse_geocode(f.get("geo_location", {}).get("lat"), f.get("geo_location", {}).get("lng"))
444
+ evt(f"Upload — {display_type(f.get('file_type'))}",
445
+ f.get("uploaded_at"),
446
+ sub)
447
+
448
+ evt("Case Updated", case.get("updated_at"))
449
+ evt("Case Completed", case.get("completed_at"))
450
+
451
+ def parse(s):
452
+ try:
453
+ return datetime.fromisoformat(s.replace("Z","+00:00"))
454
+ except:
455
+ return datetime.min
456
+ events.sort(key=lambda x: parse(x[1]))
457
+
458
+ for text, ts, sub in events:
459
+ c.setFont("Helvetica", 10)
460
+ c.drawString(MARGIN, y, f"• {fmt_ist(ts)}")
461
+ y -= 0.45*cm
462
+
463
+ c.setFont("Helvetica-Bold", 11)
464
+ c.drawString(MARGIN + 0.6*cm, y, text)
465
+ y -= 0.45*cm
466
+
467
+ if sub:
468
+ c.setFont("Helvetica", 9)
469
+ c.setFillColor(colors.grey)
470
+ y = draw_wrapped_label_value(c, MARGIN + 0.6*cm, y, "Uploaded from", sub, max_width_cm=17.0)
471
+ c.setFillColor(colors.black)
472
+ y += 0.3*cm
473
+
474
+ y -= 0.3*cm
475
+
476
+ if y < 3*cm:
477
+ draw_footer(c, page)
478
+ c.showPage(); page += 1
479
+ draw_header(c, title)
480
+ y = H - 3.4*cm
481
+ c.setFont("Helvetica-Bold", 14)
482
+ c.drawString(MARGIN, y, "Inspection Timeline (Cont.)")
483
+ y -= 1.0*cm
484
+
485
+ draw_footer(c, page)
486
+ c.showPage()
487
+ c.save()
488
+
489
+ # FastAPI endpoints
490
+ class CaseRequest(BaseModel):
491
+ case_id: str
492
+
493
+ @app.get("/health")
494
+ async def health_check():
495
+ return {"status": "healthy"}
496
+
497
+ @app.post("/generate-report")
498
+ async def generate_report(case_request: CaseRequest):
499
+ try:
500
+ case_id = case_request.case_id
501
+ print(f"🔐 Logging in for case {case_id}...")
502
+ admin_login()
503
+
504
+ print("📂 Fetching case…")
505
+ case = get_case(case_id)
506
+
507
+ print("📁 Fetching files…")
508
+ files = get_files(case_id)
509
+
510
+ print("📸 Downloading binaries…")
511
+ for f in files:
512
+ f["_bin"] = download_file(f["file_id"])
513
+
514
+ print("🧭 Generating report…")
515
+ output_filename = f"Report_{case_id}.pdf"
516
+ output_path = f"/app/output/{output_filename}"
517
+
518
+ build_pdf(case, files, output_path)
519
+
520
+ with open(output_path, "rb") as f:
521
+ pdf_content = f.read()
522
+
523
+ # Clean up
524
+ if os.path.exists(output_path):
525
+ os.remove(output_path)
526
+
527
+ return Response(
528
+ content=pdf_content,
529
+ media_type="application/pdf",
530
+ headers={"Content-Disposition": f"attachment; filename={output_filename}"}
531
+ )
532
+
533
+ except Exception as e:
534
+ raise HTTPException(status_code=500, detail=f"Error generating PDF: {str(e)}")
535
+
536
+ @app.get("/")
537
+ async def root():
538
+ return {"message": "Case Report Generator API", "status": "running"}
539
+
540
+ if __name__ == "__main__":
541
+ import uvicorn
542
+ uvicorn.run(app, host="0.0.0.0", port=7860)