Spaces:
Sleeping
Sleeping
| // server.js | |
| import fs from "fs"; | |
| import path from "path"; | |
| import crypto from "crypto"; | |
| import fetch from "node-fetch"; | |
| import express from "express"; | |
| import bodyParser from "body-parser"; | |
| import PDFDocument from "pdfkit"; | |
| import sharp from "sharp"; | |
| import moment from "moment-timezone"; | |
| import cors from "cors"; | |
| // Configuration (all from environment variables) | |
| const BASE_URL = process.env.BASE_URL || "https://rakshitjan-cps-b2c.hf.space"; | |
| const ADMIN_EMAIL = process.env.ADMIN_EMAIL || "admin123@example.com"; | |
| const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || "SecurePass123"; | |
| const AES_KEY_STR = process.env.AES_KEY_STR || "7E892875A52C59A3B588306B13C31FBD"; | |
| const AES_IV_STR = process.env.AES_IV_STR || "XYE45DKJ0967GFAZ"; | |
| const GOOGLE_MAPS_API_KEY = process.env.GOOGLE_MAPS_API_KEY || "AIzaSyBpna0F0J-c9QNBh8lSnfT2Z2uzD_vS9Ag"; // For geocoding | |
| const GEOAPIFY_API_KEY = process.env.GEOAPIFY_API_KEY || "ceaea8a8a8f14f46b73221384eb4b5f0" ; // For static maps | |
| const LOGO_PATH = process.env.LOGO_PATH || "logo.png"; | |
| const statelist = JSON.parse(fs.readFileSync("state.json", "utf8")); | |
| const citylist = JSON.parse(fs.readFileSync("city.json", "utf8")); | |
| const districtlist = JSON.parse(fs.readFileSync("district.json", "utf8")); | |
| const organizationlist = JSON.parse(fs.readFileSync("organization.json", "utf8")); | |
| const genderlist = JSON.parse(fs.readFileSync("gender.json", "utf8")); | |
| const qualificationlist = JSON.parse(fs.readFileSync("qualification.json", "utf8")); | |
| const ownershiplist = JSON.parse(fs.readFileSync("ownership.json", "utf8")); | |
| const app = express(); | |
| app.use(bodyParser.json({ limit: "50mb" })); | |
| // Allow ALL origins | |
| app.use(cors()); | |
| // Utility: convert cm to PDF points (1 pt = 1/72 in; 1 cm = 28.3464567 pt) | |
| const CM = 28.3464567; | |
| const A4_WIDTH = 595.2755905511812; // points (210mm) | |
| const A4_HEIGHT = 841.8897637795277; // points (297mm) | |
| const MARGIN = 1.2 * CM; | |
| // AES encryption like Python's Crypto.Cipher AES-CBC w/ PKCS7 padding | |
| function aesEncrypt(plaintext, keyStr, ivStr) { | |
| // Validate inputs | |
| if (!plaintext) throw new Error("Plaintext is required for encryption"); | |
| if (!keyStr) throw new Error("AES key is required"); | |
| if (!ivStr) throw new Error("AES IV is required"); | |
| const key = Buffer.from(keyStr, "utf8"); | |
| const iv = Buffer.from(ivStr, "utf8"); | |
| // Validate key and IV lengths | |
| if (key.length !== 16 && key.length !== 24 && key.length !== 32) { | |
| throw new Error(`Invalid AES key length: ${key.length} bytes. Must be 16, 24, or 32 bytes.`); | |
| } | |
| // Node's createCipheriv uses PKCS#7 padding by default (called PKCS#5 in OpenSSL) | |
| const cipher = crypto.createCipheriv(`aes-${key.length * 8}-cbc`, key, iv); | |
| const encrypted = Buffer.concat([ | |
| cipher.update(Buffer.from(plaintext, "utf8")), | |
| cipher.final() | |
| ]); | |
| return encrypted.toString("base64"); | |
| } | |
| // HTTP session mimic (we reuse fetch) | |
| let token = null; | |
| async function adminLogin() { | |
| try { | |
| // encrypt email same as python | |
| const encryptedEmail = aesEncrypt(ADMIN_EMAIL, AES_KEY_STR, AES_IV_STR); | |
| const payload = { | |
| email: encryptedEmail, | |
| password: ADMIN_PASSWORD | |
| }; | |
| console.log(`Logging in to ${BASE_URL} with admin email...`); | |
| const res = await fetch(`${BASE_URL}/api/admin/login`, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(payload), | |
| timeout: 30000 | |
| }); | |
| const data = await res.json(); | |
| if (res.status !== 200 || !data.token) { | |
| console.error(`Login failed. Status: ${res.status}, Response:`, data); | |
| throw new Error(`Login failed: ${JSON.stringify(data)}`); | |
| } | |
| token = data.token; | |
| console.log("Admin login successful"); | |
| return token; | |
| } catch (error) { | |
| console.error("Admin login error:", error.message); | |
| throw error; | |
| } | |
| } | |
| async function getCase(caseId) { | |
| if (!token) await adminLogin(); | |
| console.log(`Fetching case ${caseId}...`); | |
| const res = await fetch(`${BASE_URL}/api/cases/${caseId}`, { | |
| headers: { | |
| "Content-Type": "application/json", | |
| "Authorization": `Bearer ${token}` | |
| }, | |
| timeout: 30000 | |
| }); | |
| if (!res.ok) { | |
| console.error(`Failed to get case ${caseId}: ${res.status} ${res.statusText}`); | |
| throw new Error(`Failed get_case: ${res.status}`); | |
| } | |
| return await res.json(); | |
| } | |
| async function getFiles(caseId) { | |
| if (!token) await adminLogin(); | |
| console.log(`Fetching files for case ${caseId}...`); | |
| const res = await fetch(`${BASE_URL}/api/cases/${caseId}/files`, { | |
| headers: { | |
| "Content-Type": "application/json", | |
| "Authorization": `Bearer ${token}` | |
| }, | |
| timeout: 60000 | |
| }); | |
| if (!res.ok) { | |
| console.error(`Failed to get files for case ${caseId}: ${res.status} ${res.statusText}`); | |
| throw new Error(`Failed get_files: ${res.status}`); | |
| } | |
| const json = await res.json(); | |
| console.log(`Retrieved ${json.files?.length || 0} files for case ${caseId}`); | |
| return json.files || []; | |
| } | |
| async function downloadFile(fileId) { | |
| if (!token) await adminLogin(); | |
| console.log(`Downloading file ${fileId}...`); | |
| const res = await fetch(`${BASE_URL}/api/files/${fileId}`, { | |
| headers: { "Authorization": `Bearer ${token}` }, | |
| timeout: 60000 | |
| }); | |
| if (!res.ok) { | |
| console.error(`File download failed for ${fileId}: ${res.status} ${res.statusText}`); | |
| throw new Error(`File download failed for ${fileId}: ${res.status}`); | |
| } | |
| const buf = await res.arrayBuffer(); | |
| return Buffer.from(buf); | |
| } | |
| // Google Maps Geocoding API helpers (for forward/reverse geocoding) | |
| async function forwardGeocode(address) { | |
| if (!address) { | |
| console.log("No address provided for geocoding"); | |
| return [null, null]; | |
| } | |
| if (!GOOGLE_MAPS_API_KEY) { | |
| console.error("GOOGLE_MAPS_API_KEY is not set"); | |
| return [null, null]; | |
| } | |
| console.log(`Geocoding address: ${address.substring(0, 50)}...`); | |
| try { | |
| const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${GOOGLE_MAPS_API_KEY}`; | |
| const res = await fetch(url, { timeout: 20000 }); | |
| if (!res.ok) { | |
| console.error(`Geocoding API error: ${res.status} ${res.statusText}`); | |
| return [null, null]; | |
| } | |
| const json = await res.json(); | |
| if (json.status === "OK" && json.results && json.results[0] && json.results[0].geometry && json.results[0].geometry.location) { | |
| const loc = json.results[0].geometry.location; | |
| console.log(`Geocoding successful: ${loc.lat}, ${loc.lng}`); | |
| return [loc.lat, loc.lng]; | |
| } else { | |
| console.warn(`Geocoding failed: ${json.status} - ${json.error_message || 'No results'}`); | |
| return [null, null]; | |
| } | |
| } catch (error) { | |
| console.error("Geocoding error:", error.message); | |
| return [null, null]; | |
| } | |
| } | |
| async function reverseGeocode(lat, lon) { | |
| if (lat == null || lon == null) { | |
| console.log("No coordinates provided for reverse geocoding"); | |
| return "Unknown Address"; | |
| } | |
| if (!GOOGLE_MAPS_API_KEY) { | |
| console.error("GOOGLE_MAPS_API_KEY is not set"); | |
| return "Unknown Address"; | |
| } | |
| console.log(`Reverse geocoding coordinates: ${lat}, ${lon}`); | |
| try { | |
| const url = `https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${lon}&key=${GOOGLE_MAPS_API_KEY}`; | |
| const res = await fetch(url, { timeout: 20000 }); | |
| if (!res.ok) { | |
| console.error(`Reverse geocoding API error: ${res.status} ${res.statusText}`); | |
| return "Unknown Address"; | |
| } | |
| const json = await res.json(); | |
| if (json.status === "OK" && json.results && json.results[0] && json.results[0].formatted_address) { | |
| const address = json.results[0].formatted_address; | |
| console.log(`Reverse geocoding successful: ${address.substring(0, 50)}...`); | |
| return address; | |
| } else { | |
| console.warn(`Reverse geocoding failed: ${json.status} - ${json.error_message || 'No results'}`); | |
| return "Unknown Address"; | |
| } | |
| } catch (error) { | |
| console.error("Reverse geocoding error:", error.message); | |
| return "Unknown Address"; | |
| } | |
| } | |
| // Geoapify Static Maps API (for static maps with markers and path) | |
| async function staticMapWithPath(captureLat, captureLng, caseLat = null, caseLng = null, zoom = 17, size = "1800x600") { | |
| if (captureLat == null || captureLng == null) { | |
| console.log("No capture coordinates provided for static map"); | |
| return null; | |
| } | |
| if (!GEOAPIFY_API_KEY) { | |
| console.error("GEOAPIFY_API_KEY is not set"); | |
| return null; | |
| } | |
| console.log(`Generating static map for: ${captureLat}, ${captureLng}`); | |
| try { | |
| let parts = [ | |
| `center=lonlat:${captureLng},${captureLat}`, | |
| `zoom=${zoom}`, | |
| `size=${size}`, | |
| `scaleFactor=2`, | |
| `marker=lonlat:${captureLng},${captureLat};color:%23ff0000;size:large` | |
| ]; | |
| if (caseLat != null && caseLng != null) { | |
| parts.push(`marker=lonlat:${caseLng},${caseLat};color:%23007bff;size:large`); | |
| parts.push(`path=color:%23ff0000;weight:4|lonlat:${caseLng},${caseLat}|lonlat:${captureLng},${captureLat}`); | |
| } | |
| const url = `https://maps.geoapify.com/v1/staticmap?${parts.join("&")}&apiKey=${GEOAPIFY_API_KEY}`; | |
| const res = await fetch(url, { timeout: 30000 }); | |
| if (!res.ok) { | |
| console.error(`Static map API error: ${res.status} ${res.statusText}`); | |
| return null; | |
| } | |
| const buf = Buffer.from(await res.arrayBuffer()); | |
| console.log("Static map generated successfully"); | |
| return buf; | |
| } catch (error) { | |
| console.error("Static map generation error:", error.message); | |
| return null; | |
| } | |
| } | |
| function haversineKm(lat1, lon1, lat2, lon2) { | |
| if ([lat1, lon1, lat2, lon2].some(v => v == null)) return null; | |
| const R = 6371.0; | |
| const phi1 = lat1 * Math.PI / 180; | |
| const phi2 = lat2 * Math.PI / 180; | |
| const dphi = (lat2 - lat1) * Math.PI / 180; | |
| const dlambda = (lon2 - lon1) * Math.PI / 180; | |
| const a = Math.sin(dphi / 2) * Math.sin(dphi / 2) + Math.cos(phi1) * Math.cos(phi2) * Math.sin(dlambda / 2) * Math.sin(dlambda / 2); | |
| return 2 * R * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); | |
| } | |
| // date formatting to IST similar to fmt_ist in python | |
| function fmtIST(dtStr) { | |
| if (!dtStr) return "N/A"; | |
| try { | |
| // Accept Z or offset | |
| const m = moment(dtStr); | |
| const ist = m.tz("Asia/Kolkata").format("DD/MM/YYYY hh:mm:ss A") + " IST"; | |
| return ist; | |
| } catch { | |
| return dtStr; | |
| } | |
| } | |
| function exportedOnStr() { | |
| return moment().tz("Asia/Kolkata").format("MMMM DD, YYYY"); | |
| } | |
| // Mapping display names | |
| const DISPLAY_NAME = { | |
| "aadhaar_photo": "Photo of Aadhar", | |
| "customer_photo": "Customer's Photo", | |
| "residence_photo": "Residence Photo", | |
| "business_photo": "Business Address Photo" | |
| }; | |
| function displayType(fileType) { | |
| return DISPLAY_NAME[fileType] || fileType || "Upload"; | |
| } | |
| // create thumbnail similar to Python make_thumb | |
| async function makeThumb(imgBuf, maxW = 860, maxH = 640) { | |
| try { | |
| // produce an RGB JPEG/PNG buffer sized with thumbnail | |
| const img = sharp(imgBuf).rotate().resize({ width: maxW, height: maxH, fit: "inside" }).flatten({ background: { r: 255, g: 255, b: 255 } }); | |
| const png = await img.png().toBuffer(); | |
| // create bordered image by extending canvas | |
| const bordered = await sharp(png) | |
| .extend({ top: 1, bottom: 1, left: 1, right: 1, background: { r: 170, g: 170, b: 170 } }) | |
| .png() | |
| .toBuffer(); | |
| return bordered; | |
| } catch (error) { | |
| console.error("Thumbnail creation error:", error.message); | |
| // Return original buffer if thumbnail creation fails | |
| return imgBuf; | |
| } | |
| } | |
| // text wrapping helper that attempts to match Python's draw_wrapped_label_value. | |
| // We measure text widths using doc.widthOfString and output lines manually at exact coordinates. | |
| function drawWrappedLabelValue(doc, x, y, label, text, maxWidthCm = 16.0) { | |
| const maxW = maxWidthCm * CM; | |
| const fontSize = 10; | |
| doc.font("Helvetica").fontSize(fontSize); | |
| const labelStr = `${label}: `; | |
| const labelW = doc.widthOfString(labelStr); | |
| const words = (text || "N/A").toString().split(/\s+/); | |
| let first = true; | |
| let line = ""; | |
| let width = 0; | |
| let lines = []; | |
| for (let w of words) { | |
| const w_w = doc.widthOfString(w + " "); | |
| if (first) { | |
| if (labelW + width + w_w < maxW) { | |
| line += w + " "; | |
| width += w_w; | |
| } else { | |
| lines.push(labelStr + line.trim()); | |
| first = false; | |
| line = w + " "; | |
| width = w_w; | |
| } | |
| } else { | |
| if (width + w_w < maxW) { | |
| line += w + " "; | |
| width += w_w; | |
| } else { | |
| // indent roughly by label_w/6 characters -> compute pixels | |
| const indentText = " ".repeat(Math.max(0, Math.floor(labelW / (fontSize * 0.6)))); | |
| lines.push(indentText + line.trim()); | |
| line = w + " "; | |
| width = w_w; | |
| } | |
| } | |
| } | |
| if (first) { | |
| lines.push(labelStr + (line.trim())); | |
| } else { | |
| if (line) { | |
| const indentText = " ".repeat(Math.max(0, Math.floor(labelW / (fontSize * 0.6)))); | |
| lines.push(indentText + line.trim()); | |
| } | |
| } | |
| for (let ln of lines) { | |
| doc.text(ln, x, y, { lineBreak: false }); | |
| y += 0.45 * CM; | |
| } | |
| return y; | |
| } | |
| // Draw header/footer helpers (matching positions & fonts from python) | |
| function drawHeader(doc, title) { | |
| // header blue rectangle at top: height 2.4 cm | |
| doc.save(); | |
| doc.rect(0, 0, A4_WIDTH, 2.4 * CM).fill("#0F61A8"); | |
| // logo | |
| let titleX = MARGIN; | |
| if (fs.existsSync(LOGO_PATH)) { | |
| try { | |
| doc.image(LOGO_PATH, MARGIN, 0.2 * CM, { width: 2.4 * CM, height: 2.0 * CM }); | |
| titleX = MARGIN + 2.6 * CM; | |
| } catch (e) { | |
| console.warn(`Could not load logo from ${LOGO_PATH}:`, e.message); | |
| } | |
| } | |
| doc.fillColor("white").font("Helvetica-Bold").fontSize(15).text(title, titleX, 0.9 * CM, { continued: false }); | |
| doc.font("Helvetica").fontSize(10); | |
| const exp = `Exported on ${exportedOnStr()}`; | |
| // draw right-aligned | |
| const w = doc.widthOfString(exp); | |
| doc.fillColor("white").text(exp, A4_WIDTH - MARGIN - w, 1.0 * CM); | |
| doc.fillColor("black"); | |
| doc.restore(); | |
| } | |
| function drawFooter(doc, page) { | |
| doc.font("Helvetica").fontSize(9).text(`Page ${page}`, A4_WIDTH - MARGIN - 50, A4_HEIGHT - 1.0 * CM); | |
| } | |
| // Build PDF function (mirrors build_pdf in Python) | |
| async function buildPdf(caseObj, files, outPath) { | |
| return new Promise(async (resolve, reject) => { | |
| try { | |
| console.log(`Building PDF for case ${caseObj.case_id} with ${files.length} files...`); | |
| const doc = new PDFDocument({ size: "A4", margin: 0 }); | |
| const ws = fs.createWriteStream(outPath); | |
| doc.pipe(ws); | |
| let page = 1; | |
| // prepare address and geocode | |
| const ad = ((caseObj.demographic_details || {}).address_details) || {}; | |
| const caseAddr = `${ad.residential_address || ""}, ${ad.city || ""}, ${ad.state || ""} ${ad.pincode || ""}`.trim(); | |
| console.log(`Case address: ${caseAddr}`); | |
| const [caseLat, caseLng] = await forwardGeocode(caseAddr); | |
| if (caseLat && caseLng) { | |
| console.log(`Case coordinates: ${caseLat}, ${caseLng}`); | |
| } | |
| const title = `Name : ${caseObj.case_applicant_name || ""}`; | |
| drawHeader(doc, title); | |
| let y = 3.4 * CM; | |
| // Customer Details section | |
| const customerstate = statelist.find(state => state.id === caseObj.residenceDetail.stateId); | |
| const customerdistrict = districtlist.find(district => district.id === caseObj.residenceDetail.districtId); | |
| const customercity = citylist.find(city => city.id === caseObj.residenceDetail.cityId); | |
| const customeraddress=caseObj.residenceDetail.address1 +", "+ caseObj.residenceDetail.address2 +", "+ caseObj.residenceDetail.address3 +", "+customercity.city_name +", "+ customerdistrict.district_name +", "+ customerstate.state_name +", "+ caseObj.residenceDetail.pincode; | |
| y = sectionTitle(doc, "Customer Details", MARGIN, y); | |
| labelValue(doc, MARGIN, y, "Customer Name", caseObj.case_applicant_name); y += 0.6 * CM; | |
| labelValue(doc, MARGIN, y, "Customer ID", caseObj.case_id); y += 0.6 * CM; | |
| labelValue(doc, MARGIN, y, "Phone Number", caseObj.case_applicant_contact); y += 0.6 * CM; | |
| //const emailId = (caseObj.applicantDetail.email || {}).contact_information && (caseObj.demographic_details.contact_information.email_id); | |
| labelValue(doc, MARGIN, y, "Email", caseObj.applicantDetail.email); y += 0.6 * CM; | |
| y = drawWrappedLabelValue(doc, MARGIN, y, "Customer Address", customeraddress, 18.0); y += 0.6 * CM; | |
| // Inspection Summary | |
| y = sectionTitle(doc, "Inspection Summary", MARGIN, y); | |
| doc.font("Helvetica").fontSize(10); | |
| doc.text(`Request Started: ${fmtIST(caseObj.created_at)}`, MARGIN, y); y += 0.5 * CM; | |
| doc.text(`Total Uploads: ${files.length} uploads`, MARGIN, y); y += 0.5 * CM; | |
| doc.text(`Status: ${caseObj.status}`, MARGIN, y); y += 0.5 * CM; | |
| doc.text(`Priority: ${caseObj.priority}`, MARGIN, y); y += 0.7 * CM; | |
| // render static map if caseLat present (attempt) | |
| if (caseLat) { | |
| console.log("Generating overview map..."); | |
| const mapBuf = await staticMapWithPath(caseLat, caseLng, caseLat, caseLng, 12); | |
| if (mapBuf) { | |
| // approximate map size used in python: map_h = H/2 + 2*cm | |
| const mapH = A4_HEIGHT / 2 + 2 * CM; | |
| const mapW = A4_WIDTH - 2 * MARGIN; | |
| doc.image(mapBuf, MARGIN, y, { width: mapW, height: mapH }); | |
| y += mapH + 0.8 * CM; | |
| } | |
| } | |
| drawFooter(doc, page); | |
| doc.addPage(); page += 1; | |
| // Page 2: thumbnails | |
| drawHeader(doc, title); | |
| y = 3.4 * CM; | |
| doc.font("Helvetica-Bold").fontSize(14).text("Image Thumbnails", MARGIN, y); | |
| y += 0.8 * CM; | |
| const x0 = MARGIN; | |
| const y0 = y; | |
| const colW = (A4_WIDTH - 2 * MARGIN) / 3; | |
| const rowH = 6.1 * CM; | |
| for (let i = 0; i < Math.min(6, files.length); ++i) { | |
| const f = files[i]; | |
| console.log(`Creating thumbnail for upload ${i+1} (${f.file_type})...`); | |
| const imgBuf = await makeThumb(f._bin); | |
| const col = i % 3; | |
| const row = Math.floor(i / 3); | |
| const xi = x0 + col * colW + 0.2 * CM; | |
| const yi = y0 + row * rowH + 4.6 * CM; | |
| // draw image within width colW-0.6cm and height 4 cm | |
| try { | |
| doc.image(imgBuf, xi, yi, { width: colW - 0.6 * CM, height: 4.0 * CM, align: "center", valign: "center" }); | |
| } catch (e) { | |
| console.warn(`Could not draw thumbnail ${i+1}:`, e.message); | |
| } | |
| doc.font("Helvetica").fontSize(9).text(`Upload ${i + 1} — ${displayType(f.file_type)}`, xi, yi - 0.3 * CM); | |
| } | |
| drawFooter(doc, page); | |
| doc.addPage(); page += 1; | |
| // Page 3: Demographic, Business, Financial | |
| drawHeader(doc, title); | |
| y = 3.4 * CM; | |
| y = sectionTitle(doc, "Demographic Details", MARGIN, y); | |
| const demo = caseObj.applicantDetail || {}; | |
| const ci = demo.contact_information || {}; | |
| const pd = demo.personal_details || {}; | |
| const gender = genderlist.find(gender => gender.id === demo.genderId); | |
| const qualification = qualificationlist.find(qualification => qualification.id === demo.qualification); | |
| labelValue(doc, MARGIN, y, "Aadhaar Photo Match", demo.aadhaar_photo_match); y += 0.5 * CM; | |
| labelValue(doc, MARGIN, y, "Email ID", demo.email); y += 0.5 * CM; | |
| labelValue(doc, MARGIN, y, "Mobile Number", demo.mobile); y += 0.5 * CM; | |
| labelValue(doc, MARGIN, y, "Gender", gender.gender_name); y += 0.5 * CM; | |
| labelValue(doc, MARGIN, y, "Education", qualification.qualification_name); y += 0.5 * CM; | |
| labelValue(doc, MARGIN, y, "Family Members", demo.numOfDependent); y += 1.0 * CM; | |
| y = sectionTitle(doc, "Business Details", MARGIN, y); | |
| const bd = caseObj.businessDetail || {}; | |
| const ei = bd.enterprise_information || {}; | |
| const bld = bd.business_location_details || {}; | |
| const ba = bd.business_address || {}; | |
| const state = statelist.find(state => state.id === bd.stateId); | |
| const district = districtlist.find(district => district.id === bd.districtId); | |
| const city = citylist.find(city => city.id === bd.cityId); | |
| const organization = organizationlist.find(organization => organization.id === bd.organizationType); | |
| //const city = citylist.find(city => city.id === bd.cityId); | |
| labelValue(doc, MARGIN, y, "Enterprise Name", bd.businessName); y += 0.5 * CM; | |
| labelValue(doc, MARGIN, y, "Organization Type", organization.organization_name); y += 0.5 * CM; | |
| labelValue(doc, MARGIN, y, "Business Location Type", bd.businessLocation); y += 0.5 * CM; | |
| labelValue(doc, MARGIN, y, "Address", `${bd.address1} ${bd.address2} ${bd.landmark }`); y += 0.5 * CM; | |
| labelValue(doc, MARGIN, y, "City/District/State/Pincode", `${city.city_name || ""}/ ${district.district_name || ""}/ ${state.state_name || ""} - ${bd.pincode || ""}`); y += 1.0 * CM; | |
| y = sectionTitle(doc, "Financial Details", MARGIN, y); | |
| const fin = caseObj.financial_details || {}; | |
| const bfi = fin.business_financial_information || {}; | |
| const ffi = fin.family_financial_information || {}; | |
| function fmtInr(v) { | |
| try { | |
| if (v == null || v === "" || isNaN(v)) return "N/A"; | |
| const amount = parseInt(v); | |
| // Format with Indian numbering system and ₹ symbol | |
| return `₹ ${amount.toLocaleString('en-IN')}`; | |
| } catch { | |
| return "N/A"; | |
| } | |
| } | |
| labelValue(doc, MARGIN, y, "Monthly Income", fmtInr(caseObj.incomeExpenditure.monthlyIncome)); y += 0.5 * CM; | |
| labelValue(doc, MARGIN, y, "Monthly Expense", fmtInr(caseObj.familyExpenses.totalExp)); y += 0.5 * CM; | |
| labelValue(doc, MARGIN, y, "Family Income", fmtInr(caseObj.incomeExpenditure.totalIncome)); y += 0.5 * CM; | |
| drawFooter(doc, page); | |
| doc.addPage(); page += 1; | |
| // For each file, a detailed page | |
| for (let idx = 0; idx < files.length; ++idx) { | |
| const f = files[idx]; | |
| console.log(`Processing detailed page for upload ${idx+1}...`); | |
| drawHeader(doc, title); | |
| y = 3.4 * CM; | |
| doc.font("Helvetica-Bold").fontSize(14).text(`Upload ${idx + 1} — ${displayType(f.file_type)}`, MARGIN, y); | |
| y += 0.8 * CM; | |
| // integrity_dashboard image insertion (if present) | |
| try { | |
| const integrityPath = "integrity_dashboard.png"; | |
| if (fs.existsSync(integrityPath)) { | |
| const imageWidth = 16 * CM; | |
| const imageHeight = 2.5 * CM; | |
| const centerX = (A4_WIDTH - imageWidth) / 2; | |
| doc.image(integrityPath, centerX, y, { width: imageWidth, height: imageHeight }); | |
| y = y + imageHeight + 0.8 * CM; | |
| } else { | |
| y += 0.5 * CM; | |
| } | |
| } catch (e) { | |
| y += 0.5 * CM; | |
| } | |
| const gl = f.geo_location || {}; | |
| const lat = gl.lat; | |
| const lng = gl.lng; | |
| // draw the file image on left | |
| console.log(`Creating thumbnail for detailed view ${idx+1}...`); | |
| const imgBuf = await makeThumb(f._bin); | |
| try { | |
| doc.image(imgBuf, MARGIN, y, { width: 8.6 * CM, height: 9.0 * CM }); | |
| } catch (e) { | |
| console.warn(`Could not draw main image ${idx+1}:`, e.message); | |
| } | |
| // draw static map for the single upload using Geoapify | |
| if (lat != null && lng != null) { | |
| console.log(`Generating static map for upload ${idx+1}...`); | |
| const mimgBuf = await staticMapWithPath(lat, lng, caseLat, caseLng, 17); | |
| if (mimgBuf) { | |
| try { | |
| doc.image(mimgBuf, 11.0 * CM, y, { width: 8.0 * CM, height: 9.0 * CM }); | |
| } catch (e) { | |
| console.warn(`Could not draw static map ${idx+1}:`, e.message); | |
| } | |
| } | |
| } | |
| let yb = y + 10.4 * CM; | |
| // Use Google Maps for reverse geocoding | |
| const rev = await reverseGeocode(lat, lng); | |
| yb = drawWrappedLabelValue(doc, MARGIN, yb, "Location Address", rev, 18.0); | |
| const distKm = (caseLat != null && lat != null) ? haversineKm(caseLat, caseLng, lat, lng) : null; | |
| const distM = distKm ? Math.round(distKm * 1000) : null; | |
| doc.font("Helvetica").fontSize(10); | |
| doc.text(distM ? `Distance from case location: ${distM} meters` : "Distance from case location: N/A", MARGIN, yb); | |
| yb += 0.7 * CM; | |
| const upAt = fmtIST(f.uploaded_at); | |
| doc.text(`Uploaded At: ${upAt}`, MARGIN, yb); yb += 1.0 * CM; | |
| doc.font("Helvetica-Bold").fontSize(12).text("Metadata & Sensor Information", MARGIN, yb); yb += 0.6 * CM; | |
| doc.font("Helvetica").fontSize(10).text(`Server Time: ${upAt}`, MARGIN, yb); yb += 0.6 * CM; | |
| doc.text(`Device Time: ${upAt}`, MARGIN, yb); yb += 0.6 * CM; | |
| doc.text(distM ? `Accuracy: ${distM} meters` : "Distance from case location: N/A", MARGIN, yb); yb += 0.6 * CM; | |
| if (lat != null) { | |
| doc.text(`Geo: Lat ${lat.toFixed(6)}, Lng ${lng.toFixed(6)}`, MARGIN, yb); | |
| } else { | |
| doc.text("Geo: N/A", MARGIN, yb); | |
| } | |
| yb += 0.6 * CM; | |
| drawFooter(doc, page); | |
| doc.addPage(); page += 1; | |
| } | |
| // Timeline page (mirrors python) | |
| console.log("Creating timeline page..."); | |
| drawHeader(doc, title); | |
| y = 3.4 * CM; | |
| doc.font("Helvetica-Bold").fontSize(14).text("Inspection Timeline", MARGIN, y); y += 0.8 * CM; | |
| // draw a rounded rect with a pale bg similar to python's setFillColorRGB | |
| doc.save(); | |
| doc.roundedRect(MARGIN, y, A4_WIDTH - 2 * MARGIN, 1.1 * CM, 0.3 * CM).fillOpacity(1).fill("#E0F7E6"); | |
| doc.fillColor("#1F743F").font("Helvetica-Bold").fontSize(12).text("Ready for Review", MARGIN + 0.6 * CM, y + 0.4 * CM); | |
| doc.fillColor("black"); | |
| doc.restore(); | |
| y += 2.0 * CM; | |
| const events = []; | |
| function evt(text, ts, sub = null) { | |
| if (ts) events.push({ text, ts, sub }); | |
| } | |
| evt("Case Created", caseObj.created_at); | |
| evt("Assigned to Agent", caseObj.assigned_at); | |
| evt("Accepted by Agent", caseObj.accepted_at); | |
| for (let f of files) { | |
| const sub = await reverseGeocode((f.geo_location || {}).lat, (f.geo_location || {}).lng); | |
| evt(`Upload — ${displayType(f.file_type)}`, f.uploaded_at, sub); | |
| } | |
| evt("Case Updated", caseObj.updated_at); | |
| evt("Case Completed", caseObj.completed_at); | |
| function parseTs(s) { | |
| try { | |
| return new Date(s); | |
| } catch { | |
| return new Date(0); | |
| } | |
| } | |
| events.sort((a, b) => parseTs(a.ts) - parseTs(b.ts)); | |
| for (let ev of events) { | |
| doc.font("Helvetica").fontSize(10).text(`• ${fmtIST(ev.ts)}`, MARGIN, y); y += 0.45 * CM; | |
| doc.font("Helvetica-Bold").fontSize(11).text(ev.text, MARGIN + 0.6 * CM, y); y += 0.45 * CM; | |
| if (ev.sub) { | |
| doc.font("Helvetica").fontSize(9).fillColor("gray"); | |
| y = drawWrappedLabelValue(doc, MARGIN + 0.6 * CM, y, "Uploaded from", ev.sub, 17.0); | |
| doc.fillColor("black"); | |
| y += 0.3 * CM; | |
| } | |
| y += 0.3 * CM; | |
| if (y > A4_HEIGHT - 3 * CM) { | |
| drawFooter(doc, page); | |
| doc.addPage(); page += 1; | |
| drawHeader(doc, title); | |
| y = 3.4 * CM; | |
| doc.font("Helvetica-Bold").fontSize(14).text("Inspection Timeline (Cont.)", MARGIN, y); y += 1.0 * CM; | |
| } | |
| } | |
| drawFooter(doc, page); | |
| doc.addPage(); // finalize | |
| doc.end(); | |
| ws.on("finish", () => { | |
| console.log(`PDF created successfully at ${outPath}`); | |
| resolve(); | |
| }); | |
| ws.on("error", (e) => { | |
| console.error("PDF write stream error:", e); | |
| reject(e); | |
| }); | |
| } catch (e) { | |
| console.error("PDF build error:", e); | |
| reject(e); | |
| } | |
| }); | |
| } | |
| // small helpers to draw section title and label:value | |
| function sectionTitle(doc, text, x, y) { | |
| doc.font("Helvetica-Bold").fontSize(14).text(text, x, y); | |
| return y + 0.9 * CM; | |
| } | |
| function labelValue(doc, x, y, label, value) { | |
| doc.font("Helvetica-Bold").fontSize(10).text(`${label}:`, x, y); | |
| doc.font("Helvetica").fontSize(10).text((value == null || value === "" || (Array.isArray(value) && value.length === 0)) ? "N/A" : value.toString(), x + 4.8 * CM, y); | |
| } | |
| // FastAPI-equivalent endpoints | |
| app.get("/health", (req, res) => { | |
| console.log("Health check"); | |
| res.json({ | |
| status: "healthy", | |
| env_loaded: !!ADMIN_EMAIL && !!ADMIN_PASSWORD && !!AES_KEY_STR && !!AES_IV_STR | |
| }); | |
| }); | |
| app.post("/generate-report", async (req, res) => { | |
| console.log("Generate report request received"); | |
| try { | |
| const caseId = (req.body || {}).case_id; | |
| if (!caseId) { | |
| console.error("Missing case_id in request"); | |
| return res.status(400).json({ error: "case_id required" }); | |
| } | |
| console.log(`Generating report for case: ${caseId}`); | |
| await adminLogin(); | |
| const caseObj = await getCase(caseId); | |
| const files = await getFiles(caseId); | |
| console.log(caseObj) | |
| console.log(`Downloading ${files.length} files...`); | |
| // download binaries | |
| for (let i = 0; i < files.length; i++) { | |
| console.log(`Downloading file ${i+1}/${files.length}...`); | |
| files[i]._bin = await downloadFile(files[i].file_id); | |
| } | |
| // ensure output dir | |
| const outDir = "/app/output"; | |
| if (!fs.existsSync(outDir)) { | |
| console.log(`Creating output directory: ${outDir}`); | |
| fs.mkdirSync(outDir, { recursive: true }); | |
| } | |
| const outputFilename = `Report_${caseId}.pdf`; | |
| const outputPath = path.join(outDir, outputFilename); | |
| console.log(`Building PDF at: ${outputPath}`); | |
| await buildPdf(caseObj, files, outputPath); | |
| const pdfContent = fs.readFileSync(outputPath); | |
| // cleanup | |
| try { | |
| fs.unlinkSync(outputPath); | |
| console.log(`Cleaned up temporary file: ${outputPath}`); | |
| } catch { } | |
| console.log(`Sending PDF response: ${outputFilename} (${pdfContent.length} bytes)`); | |
| res.setHeader("Content-Type", "application/pdf"); | |
| res.setHeader("Content-Disposition", `attachment; filename="${outputFilename}"`); | |
| res.send(pdfContent); | |
| console.log("PDF sent successfully"); | |
| } catch (e) { | |
| console.error("Error generating PDF:", e); | |
| res.status(500).json({ | |
| error: `Error generating PDF: ${e.message || String(e)}`, | |
| stack: process.env.NODE_ENV === 'development' ? e.stack : undefined | |
| }); | |
| } | |
| }); | |
| app.get("/", (req, res) => { | |
| console.log("Root endpoint accessed"); | |
| res.json({ | |
| message: "Case Report Generator API", | |
| status: "running", | |
| endpoints: { | |
| health: "GET /health", | |
| generate: "POST /generate-report" | |
| } | |
| }); | |
| }); | |
| const PORT = process.env.PORT || 7860; | |
| app.listen(PORT, "0.0.0.0", () => { | |
| console.log(`Server listening on port ${PORT}`); | |
| console.log(`Environment check:`); | |
| }); | |
| // // server.js | |
| // import fs from "fs"; | |
| // import path from "path"; | |
| // import crypto from "crypto"; | |
| // import fetch from "node-fetch"; | |
| // import express from "express"; | |
| // import bodyParser from "body-parser"; | |
| // import PDFDocument from "pdfkit"; | |
| // import sharp from "sharp"; | |
| // import moment from "moment-timezone"; | |
| // import cors from "cors"; | |
| // // Configuration (all from environment variables) | |
| // const BASE_URL = process.env.BASE_URL || "https://rakshitjan-cps-b2c.hf.space"; | |
| // const ADMIN_EMAIL = process.env.ADMIN_EMAIL || "admin123@example.com"; | |
| // const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || "SecurePass123"; | |
| // const AES_KEY_STR = process.env.AES_KEY_STR || "7E892875A52C59A3B588306B13C31FBD"; | |
| // const AES_IV_STR = process.env.AES_IV_STR || "XYE45DKJ0967GFAZ"; | |
| // const GOOGLE_MAPS_API_KEY = process.env.GOOGLE_MAPS_API_KEY || "AIzaSyBpna0F0J-c9QNBh8lSnfT2Z2uzD_vS9Ag"; // For geocoding | |
| // const GEOAPIFY_API_KEY = process.env.GEOAPIFY_API_KEY || "ceaea8a8a8f14f46b73221384eb4b5f0" ; // For static maps | |
| // const LOGO_PATH = process.env.LOGO_PATH || "logo.png"; | |
| // // PDF compression settings | |
| // const PDF_COMPRESSION_ENABLED = process.env.PDF_COMPRESSION_ENABLED !== "false"; | |
| // const IMAGE_COMPRESSION_QUALITY = parseInt(process.env.IMAGE_COMPRESSION_QUALITY) || 65; | |
| // const MAX_IMAGE_DIMENSION = parseInt(process.env.MAX_IMAGE_DIMENSION) || 1200; | |
| // const THUMBNAIL_QUALITY = parseInt(process.env.THUMBNAIL_QUALITY) || 55; | |
| // const app = express(); | |
| // app.use(bodyParser.json({ limit: "50mb" })); | |
| // // Allow ALL origins | |
| // app.use(cors()); | |
| // // Utility: convert cm to PDF points (1 pt = 1/72 in; 1 cm = 28.3464567 pt) | |
| // const CM = 28.3464567; | |
| // const A4_WIDTH = 595.2755905511812; // points (210mm) | |
| // const A4_HEIGHT = 841.8897637795277; // points (297mm) | |
| // const MARGIN = 1.2 * CM; | |
| // // AES encryption like Python's Crypto.Cipher AES-CBC w/ PKCS7 padding | |
| // function aesEncrypt(plaintext, keyStr, ivStr) { | |
| // // Validate inputs | |
| // if (!plaintext) throw new Error("Plaintext is required for encryption"); | |
| // if (!keyStr) throw new Error("AES key is required"); | |
| // if (!ivStr) throw new Error("AES IV is required"); | |
| // const key = Buffer.from(keyStr, "utf8"); | |
| // const iv = Buffer.from(ivStr, "utf8"); | |
| // // Validate key and IV lengths | |
| // if (key.length !== 16 && key.length !== 24 && key.length !== 32) { | |
| // throw new Error(`Invalid AES key length: ${key.length} bytes. Must be 16, 24, or 32 bytes.`); | |
| // } | |
| // // Node's createCipheriv uses PKCS#7 padding by default (called PKCS#5 in OpenSSL) | |
| // const cipher = crypto.createCipheriv(`aes-${key.length * 8}-cbc`, key, iv); | |
| // const encrypted = Buffer.concat([ | |
| // cipher.update(Buffer.from(plaintext, "utf8")), | |
| // cipher.final() | |
| // ]); | |
| // return encrypted.toString("base64"); | |
| // } | |
| // // HTTP session mimic (we reuse fetch) | |
| // let token = null; | |
| // async function adminLogin() { | |
| // try { | |
| // // encrypt email same as python | |
| // const encryptedEmail = aesEncrypt(ADMIN_EMAIL, AES_KEY_STR, AES_IV_STR); | |
| // const payload = { | |
| // email: encryptedEmail, | |
| // password: ADMIN_PASSWORD | |
| // }; | |
| // console.log(`Logging in to ${BASE_URL} with admin email...`); | |
| // const res = await fetch(`${BASE_URL}/api/admin/login`, { | |
| // method: "POST", | |
| // headers: { "Content-Type": "application/json" }, | |
| // body: JSON.stringify(payload), | |
| // timeout: 30000 | |
| // }); | |
| // const data = await res.json(); | |
| // if (res.status !== 200 || !data.token) { | |
| // console.error(`Login failed. Status: ${res.status}, Response:`, data); | |
| // throw new Error(`Login failed: ${JSON.stringify(data)}`); | |
| // } | |
| // token = data.token; | |
| // console.log("Admin login successful"); | |
| // return token; | |
| // } catch (error) { | |
| // console.error("Admin login error:", error.message); | |
| // throw error; | |
| // } | |
| // } | |
| // async function getCase(caseId) { | |
| // if (!token) await adminLogin(); | |
| // console.log(`Fetching case ${caseId}...`); | |
| // const res = await fetch(`${BASE_URL}/api/cases/${caseId}`, { | |
| // headers: { | |
| // "Content-Type": "application/json", | |
| // "Authorization": `Bearer ${token}` | |
| // }, | |
| // timeout: 30000 | |
| // }); | |
| // if (!res.ok) { | |
| // console.error(`Failed to get case ${caseId}: ${res.status} ${res.statusText}`); | |
| // throw new Error(`Failed get_case: ${res.status}`); | |
| // } | |
| // return await res.json(); | |
| // } | |
| // async function getFiles(caseId) { | |
| // if (!token) await adminLogin(); | |
| // console.log(`Fetching files for case ${caseId}...`); | |
| // const res = await fetch(`${BASE_URL}/api/cases/${caseId}/files`, { | |
| // headers: { | |
| // "Content-Type": "application/json", | |
| // "Authorization": `Bearer ${token}` | |
| // }, | |
| // timeout: 60000 | |
| // }); | |
| // if (!res.ok) { | |
| // console.error(`Failed to get files for case ${caseId}: ${res.status} ${res.statusText}`); | |
| // throw new Error(`Failed get_files: ${res.status}`); | |
| // } | |
| // const json = await res.json(); | |
| // console.log(`Retrieved ${json.files?.length || 0} files for case ${caseId}`); | |
| // return json.files || []; | |
| // } | |
| // async function downloadFile(fileId) { | |
| // if (!token) await adminLogin(); | |
| // console.log(`Downloading file ${fileId}...`); | |
| // const res = await fetch(`${BASE_URL}/api/files/${fileId}`, { | |
| // headers: { "Authorization": `Bearer ${token}` }, | |
| // timeout: 60000 | |
| // }); | |
| // if (!res.ok) { | |
| // console.error(`File download failed for ${fileId}: ${res.status} ${res.statusText}`); | |
| // throw new Error(`File download failed for ${fileId}: ${res.status}`); | |
| // } | |
| // const buf = await res.arrayBuffer(); | |
| // return Buffer.from(buf); | |
| // } | |
| // // Google Maps Geocoding API helpers (for forward/reverse geocoding) | |
| // async function forwardGeocode(address) { | |
| // if (!address) { | |
| // console.log("No address provided for geocoding"); | |
| // return [null, null]; | |
| // } | |
| // if (!GOOGLE_MAPS_API_KEY) { | |
| // console.error("GOOGLE_MAPS_API_KEY is not set"); | |
| // return [null, null]; | |
| // } | |
| // console.log(`Geocoding address: ${address.substring(0, 50)}...`); | |
| // try { | |
| // const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${GOOGLE_MAPS_API_KEY}`; | |
| // const res = await fetch(url, { timeout: 20000 }); | |
| // if (!res.ok) { | |
| // console.error(`Geocoding API error: ${res.status} ${res.statusText}`); | |
| // return [null, null]; | |
| // } | |
| // const json = await res.json(); | |
| // if (json.status === "OK" && json.results && json.results[0] && json.results[0].geometry && json.results[0].geometry.location) { | |
| // const loc = json.results[0].geometry.location; | |
| // console.log(`Geocoding successful: ${loc.lat}, ${loc.lng}`); | |
| // return [loc.lat, loc.lng]; | |
| // } else { | |
| // console.warn(`Geocoding failed: ${json.status} - ${json.error_message || 'No results'}`); | |
| // return [null, null]; | |
| // } | |
| // } catch (error) { | |
| // console.error("Geocoding error:", error.message); | |
| // return [null, null]; | |
| // } | |
| // } | |
| // async function reverseGeocode(lat, lon) { | |
| // if (lat == null || lon == null) { | |
| // console.log("No coordinates provided for reverse geocoding"); | |
| // return "Unknown Address"; | |
| // } | |
| // if (!GOOGLE_MAPS_API_KEY) { | |
| // console.error("GOOGLE_MAPS_API_KEY is not set"); | |
| // return "Unknown Address"; | |
| // } | |
| // console.log(`Reverse geocoding coordinates: ${lat}, ${lon}`); | |
| // try { | |
| // const url = `https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${lon}&key=${GOOGLE_MAPS_API_KEY}`; | |
| // const res = await fetch(url, { timeout: 20000 }); | |
| // if (!res.ok) { | |
| // console.error(`Reverse geocoding API error: ${res.status} ${res.statusText}`); | |
| // return "Unknown Address"; | |
| // } | |
| // const json = await res.json(); | |
| // if (json.status === "OK" && json.results && json.results[0] && json.results[0].formatted_address) { | |
| // const address = json.results[0].formatted_address; | |
| // console.log(`Reverse geocoding successful: ${address.substring(0, 50)}...`); | |
| // return address; | |
| // } else { | |
| // console.warn(`Reverse geocoding failed: ${json.status} - ${json.error_message || 'No results'}`); | |
| // return "Unknown Address"; | |
| // } | |
| // } catch (error) { | |
| // console.error("Reverse geocoding error:", error.message); | |
| // return "Unknown Address"; | |
| // } | |
| // } | |
| // // Geoapify Static Maps API (for static maps with markers and path) | |
| // async function staticMapWithPath(captureLat, captureLng, caseLat = null, caseLng = null, zoom = 17, size = "1800x600") { | |
| // if (captureLat == null || captureLng == null) { | |
| // console.log("No capture coordinates provided for static map"); | |
| // return null; | |
| // } | |
| // if (!GEOAPIFY_API_KEY) { | |
| // console.error("GEOAPIFY_API_KEY is not set"); | |
| // return null; | |
| // } | |
| // console.log(`Generating static map for: ${captureLat}, ${captureLng}`); | |
| // try { | |
| // let parts = [ | |
| // `center=lonlat:${captureLng},${captureLat}`, | |
| // `zoom=${zoom}`, | |
| // `size=${size}`, | |
| // `scaleFactor=2`, | |
| // `marker=lonlat:${captureLng},${captureLat};color:%23ff0000;size:large` | |
| // ]; | |
| // if (caseLat != null && caseLng != null) { | |
| // parts.push(`marker=lonlat:${caseLng},${caseLat};color:%23007bff;size:large`); | |
| // parts.push(`path=color:%23ff0000;weight:4|lonlat:${caseLng},${caseLat}|lonlat:${captureLng},${captureLat}`); | |
| // } | |
| // const url = `https://maps.geoapify.com/v1/staticmap?${parts.join("&")}&apiKey=${GEOAPIFY_API_KEY}`; | |
| // const res = await fetch(url, { timeout: 30000 }); | |
| // if (!res.ok) { | |
| // console.error(`Static map API error: ${res.status} ${res.statusText}`); | |
| // return null; | |
| // } | |
| // const buf = Buffer.from(await res.arrayBuffer()); | |
| // console.log("Static map generated successfully"); | |
| // return buf; | |
| // } catch (error) { | |
| // console.error("Static map generation error:", error.message); | |
| // return null; | |
| // } | |
| // } | |
| // function haversineKm(lat1, lon1, lat2, lon2) { | |
| // if ([lat1, lon1, lat2, lon2].some(v => v == null)) return null; | |
| // const R = 6371.0; | |
| // const phi1 = lat1 * Math.PI / 180; | |
| // const phi2 = lat2 * Math.PI / 180; | |
| // const dphi = (lat2 - lat1) * Math.PI / 180; | |
| // const dlambda = (lon2 - lon1) * Math.PI / 180; | |
| // const a = Math.sin(dphi / 2) * Math.sin(dphi / 2) + Math.cos(phi1) * Math.cos(phi2) * Math.sin(dlambda / 2) * Math.sin(dlambda / 2); | |
| // return 2 * R * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); | |
| // } | |
| // // date formatting to IST similar to fmt_ist in python | |
| // function fmtIST(dtStr) { | |
| // if (!dtStr) return "N/A"; | |
| // try { | |
| // // Accept Z or offset | |
| // const m = moment(dtStr); | |
| // const ist = m.tz("Asia/Kolkata").format("DD/MM/YYYY hh:mm:ss A") + " IST"; | |
| // return ist; | |
| // } catch { | |
| // return dtStr; | |
| // } | |
| // } | |
| // function exportedOnStr() { | |
| // return moment().tz("Asia/Kolkata").format("MMMM DD, YYYY"); | |
| // } | |
| // // Mapping display names | |
| // const DISPLAY_NAME = { | |
| // "aadhaar_photo": "Photo of Aadhar", | |
| // "customer_photo": "Customer's Photo", | |
| // "residence_photo": "Residence Photo", | |
| // "business_photo": "Business Address Photo" | |
| // }; | |
| // function displayType(fileType) { | |
| // return DISPLAY_NAME[fileType] || fileType || "Upload"; | |
| // } | |
| // // Optimized image compression - resize and compress aggressively | |
| // async function compressImageBuffer(imageBuffer, quality = IMAGE_COMPRESSION_QUALITY, maxDimension = MAX_IMAGE_DIMENSION) { | |
| // try { | |
| // // Get image info | |
| // const image = sharp(imageBuffer); | |
| // const metadata = await image.metadata(); | |
| // // Check if image is already small | |
| // if (imageBuffer.length < 100 * 1024) { // Less than 100KB | |
| // console.log(`Image already small (${Math.round(imageBuffer.length / 1024)}KB), skipping compression`); | |
| // return imageBuffer; | |
| // } | |
| // console.log(`Compressing image: ${Math.round(imageBuffer.length / 1024)}KB, ${metadata.width}x${metadata.height}`); | |
| // // Calculate new dimensions if needed | |
| // let newWidth = metadata.width; | |
| // let newHeight = metadata.height; | |
| // if (metadata.width > maxDimension || metadata.height > maxDimension) { | |
| // if (metadata.width > metadata.height) { | |
| // newWidth = maxDimension; | |
| // newHeight = Math.round((metadata.height / metadata.width) * maxDimension); | |
| // } else { | |
| // newHeight = maxDimension; | |
| // newWidth = Math.round((metadata.width / metadata.height) * maxDimension); | |
| // } | |
| // console.log(`Resizing image from ${metadata.width}x${metadata.height} to ${newWidth}x${newHeight}`); | |
| // } | |
| // // Use WebP for best compression (much better than JPEG for same quality) | |
| // const compressedBuffer = await sharp(imageBuffer) | |
| // .resize(newWidth, newHeight, { | |
| // fit: 'inside', | |
| // withoutEnlargement: true | |
| // }) | |
| // .webp({ | |
| // quality: Math.max(quality, 50), // Minimum 50% quality | |
| // effort: 6, // Maximum compression effort (slowest but best) | |
| // lossless: false | |
| // }) | |
| // .toBuffer() | |
| // .catch(async (webpError) => { | |
| // console.log(`WebP failed, using JPEG: ${webpError.message}`); | |
| // // Fallback to JPEG with mozjpeg for better compression | |
| // return await sharp(imageBuffer) | |
| // .resize(newWidth, newHeight, { | |
| // fit: 'inside', | |
| // withoutEnlargement: true | |
| // }) | |
| // .flatten({ background: { r: 255, g: 255, b: 255 } }) // Remove transparency | |
| // .jpeg({ | |
| // quality: Math.max(quality - 10, 40), // JPEG needs lower quality for similar size | |
| // mozjpeg: true, // Better compression | |
| // chromaSubsampling: '4:4:4' // Better quality | |
| // }) | |
| // .toBuffer(); | |
| // }); | |
| // const originalSize = imageBuffer.length; | |
| // const compressedSize = compressedBuffer.length; | |
| // const compressionRatio = ((originalSize - compressedSize) / originalSize * 100).toFixed(1); | |
| // console.log(`Image compressed: ${Math.round(originalSize / 1024)}KB → ${Math.round(compressedSize / 1024)}KB (${compressionRatio}% reduction)`); | |
| // return compressedBuffer; | |
| // } catch (error) { | |
| // console.error("Image compression error:", error.message); | |
| // // Return original buffer if compression fails | |
| // return imageBuffer; | |
| // } | |
| // } | |
| // // Optimized thumbnail creation with aggressive compression | |
| // async function makeThumb(imgBuf, maxW = 860, maxH = 640) { | |
| // try { | |
| // // First compress and resize the image aggressively for thumbnails | |
| // const compressedImg = await compressImageBuffer(imgBuf, THUMBNAIL_QUALITY, Math.max(maxW, maxH)); | |
| // // Create thumbnail with specific dimensions | |
| // const img = sharp(compressedImg) | |
| // .rotate() | |
| // .resize({ | |
| // width: maxW, | |
| // height: maxH, | |
| // fit: "inside", | |
| // withoutEnlargement: true, | |
| // kernel: sharp.kernel.lanczos3 // Better downsampling | |
| // }) | |
| // .flatten({ background: { r: 255, g: 255, b: 255 } }); | |
| // // Use WebP for thumbnails | |
| // let outputBuffer; | |
| // try { | |
| // outputBuffer = await img | |
| // .webp({ | |
| // quality: THUMBNAIL_QUALITY, | |
| // effort: 6, | |
| // lossless: false | |
| // }) | |
| // .toBuffer(); | |
| // } catch { | |
| // // Fallback to JPEG | |
| // outputBuffer = await img | |
| // .jpeg({ | |
| // quality: THUMBNAIL_QUALITY - 10, | |
| // mozjpeg: true | |
| // }) | |
| // .toBuffer(); | |
| // } | |
| // // Add minimal border | |
| // const bordered = await sharp(outputBuffer) | |
| // .extend({ | |
| // top: 1, | |
| // bottom: 1, | |
| // left: 1, | |
| // right: 1, | |
| // background: { r: 170, g: 170, b: 170 } | |
| // }); | |
| // let finalBuffer; | |
| // try { | |
| // finalBuffer = await bordered | |
| // .webp({ | |
| // quality: THUMBNAIL_QUALITY, | |
| // effort: 6 | |
| // }) | |
| // .toBuffer(); | |
| // } catch { | |
| // finalBuffer = await bordered | |
| // .jpeg({ | |
| // quality: THUMBNAIL_QUALITY - 10, | |
| // mozjpeg: true | |
| // }) | |
| // .toBuffer(); | |
| // } | |
| // return finalBuffer; | |
| // } catch (error) { | |
| // console.error("Thumbnail creation error:", error.message); | |
| // // Return original if thumbnail creation fails | |
| // return await compressImageBuffer(imgBuf, THUMBNAIL_QUALITY); | |
| // } | |
| // } | |
| // // Compress static maps aggressively (maps don't need high quality) | |
| // async function compressStaticMap(mapBuffer) { | |
| // if (!mapBuffer) return mapBuffer; | |
| // try { | |
| // const originalSize = mapBuffer.length; | |
| // // If already small, return as is | |
| // if (originalSize < 50 * 1024) { | |
| // return mapBuffer; | |
| // } | |
| // console.log(`Compressing static map: ${Math.round(originalSize / 1024)}KB`); | |
| // // Static maps can be heavily compressed | |
| // const compressedBuffer = await sharp(mapBuffer) | |
| // .webp({ | |
| // quality: 50, // Low quality for maps | |
| // effort: 6, | |
| // lossless: false | |
| // }) | |
| // .toBuffer() | |
| // .catch(() => { | |
| // // Fallback to JPEG | |
| // return sharp(mapBuffer) | |
| // .jpeg({ | |
| // quality: 55, | |
| // mozjpeg: true | |
| // }) | |
| // .toBuffer(); | |
| // }); | |
| // const compressedSize = compressedBuffer.length; | |
| // const compressionRatio = ((originalSize - compressedSize) / originalSize * 100).toFixed(1); | |
| // console.log(`Static map compressed: ${Math.round(originalSize / 1024)}KB → ${Math.round(compressedSize / 1024)}KB (${compressionRatio}% reduction)`); | |
| // return compressedBuffer; | |
| // } catch (error) { | |
| // console.error("Static map compression error:", error.message); | |
| // return mapBuffer; | |
| // } | |
| // } | |
| // // Simple PDF optimization - we rely on image compression and PDFKit's built-in compression | |
| // async function optimizePDF(inputPath, outputPath) { | |
| // if (!PDF_COMPRESSION_ENABLED) { | |
| // fs.copyFileSync(inputPath, outputPath); | |
| // return outputPath; | |
| // } | |
| // try { | |
| // // For Hugging Face, we can't use Ghostscript, so we rely on the compression | |
| // // we've already done at the image level and PDFKit's built-in compression | |
| // console.log("Using image-level compression (optimized for Hugging Face)"); | |
| // // Just copy the file - compression was already done during creation | |
| // fs.copyFileSync(inputPath, outputPath); | |
| // return outputPath; | |
| // } catch (error) { | |
| // console.error("PDF optimization failed:", error.message); | |
| // // Fallback to original | |
| // fs.copyFileSync(inputPath, outputPath); | |
| // return outputPath; | |
| // } | |
| // } | |
| // // text wrapping helper that attempts to match Python's draw_wrapped_label_value. | |
| // function drawWrappedLabelValue(doc, x, y, label, text, maxWidthCm = 16.0) { | |
| // const maxW = maxWidthCm * CM; | |
| // const fontSize = 10; | |
| // doc.font("Helvetica").fontSize(fontSize); | |
| // const labelStr = `${label}: `; | |
| // const labelW = doc.widthOfString(labelStr); | |
| // const words = (text || "N/A").toString().split(/\s+/); | |
| // let first = true; | |
| // let line = ""; | |
| // let width = 0; | |
| // let lines = []; | |
| // for (let w of words) { | |
| // const w_w = doc.widthOfString(w + " "); | |
| // if (first) { | |
| // if (labelW + width + w_w < maxW) { | |
| // line += w + " "; | |
| // width += w_w; | |
| // } else { | |
| // lines.push(labelStr + line.trim()); | |
| // first = false; | |
| // line = w + " "; | |
| // width = w_w; | |
| // } | |
| // } else { | |
| // if (width + w_w < maxW) { | |
| // line += w + " "; | |
| // width += w_w; | |
| // } else { | |
| // const indentText = " ".repeat(Math.max(0, Math.floor(labelW / (fontSize * 0.6)))); | |
| // lines.push(indentText + line.trim()); | |
| // line = w + " "; | |
| // width = w_w; | |
| // } | |
| // } | |
| // } | |
| // if (first) { | |
| // lines.push(labelStr + (line.trim())); | |
| // } else { | |
| // if (line) { | |
| // const indentText = " ".repeat(Math.max(0, Math.floor(labelW / (fontSize * 0.6)))); | |
| // lines.push(indentText + line.trim()); | |
| // } | |
| // } | |
| // for (let ln of lines) { | |
| // doc.text(ln, x, y, { lineBreak: false }); | |
| // y += 0.45 * CM; | |
| // } | |
| // return y; | |
| // } | |
| // // Draw header/footer helpers | |
| // function drawHeader(doc, title) { | |
| // // header blue rectangle at top: height 2.4 cm | |
| // doc.save(); | |
| // doc.rect(0, 0, A4_WIDTH, 2.4 * CM).fill("#0F61A8"); | |
| // // logo | |
| // let titleX = MARGIN; | |
| // if (fs.existsSync(LOGO_PATH)) { | |
| // try { | |
| // const logoBuffer = fs.readFileSync(LOGO_PATH); | |
| // doc.image(logoBuffer, MARGIN, 0.2 * CM, { width: 2.4 * CM, height: 2.0 * CM }); | |
| // titleX = MARGIN + 2.6 * CM; | |
| // } catch (e) { | |
| // console.warn(`Could not load logo from ${LOGO_PATH}:`, e.message); | |
| // } | |
| // } | |
| // doc.fillColor("white").font("Helvetica-Bold").fontSize(15).text(title, titleX, 0.9 * CM, { continued: false }); | |
| // doc.font("Helvetica").fontSize(10); | |
| // const exp = `Exported on ${exportedOnStr()}`; | |
| // const w = doc.widthOfString(exp); | |
| // doc.fillColor("white").text(exp, A4_WIDTH - MARGIN - w, 1.0 * CM); | |
| // doc.fillColor("black"); | |
| // doc.restore(); | |
| // } | |
| // function drawFooter(doc, page) { | |
| // doc.font("Helvetica").fontSize(9).text(`Page ${page}`, A4_WIDTH - MARGIN - 50, A4_HEIGHT - 1.0 * CM); | |
| // } | |
| // // Optimized Build PDF function | |
| // async function buildPdf(caseObj, files, outPath) { | |
| // return new Promise(async (resolve, reject) => { | |
| // try { | |
| // console.log(`Building optimized PDF for case ${caseObj.case_id} with ${files.length} files...`); | |
| // const doc = new PDFDocument({ | |
| // size: "A4", | |
| // margin: 0, | |
| // compress: true, // Enable PDF compression | |
| // pdfVersion: '1.5', // Better compression | |
| // info: { | |
| // Title: `Report for ${caseObj.case_applicant_name || ''}`, | |
| // Author: 'Case Report System', | |
| // Subject: 'Case Inspection Report', | |
| // Keywords: 'case,report,inspection', | |
| // Creator: 'PDFKit with Compression', | |
| // Producer: 'Hugging Face Optimized', | |
| // CreationDate: new Date() | |
| // } | |
| // }); | |
| // const ws = fs.createWriteStream(outPath); | |
| // doc.pipe(ws); | |
| // let page = 1; | |
| // const ad = ((caseObj.demographic_details || {}).address_details) || {}; | |
| // const caseAddr = `${ad.residential_address || ""}, ${ad.city || ""}, ${ad.state || ""} ${ad.pincode || ""}`.trim(); | |
| // console.log(`Case address: ${caseAddr}`); | |
| // const [caseLat, caseLng] = await forwardGeocode(caseAddr); | |
| // if (caseLat && caseLng) { | |
| // console.log(`Case coordinates: ${caseLat}, ${caseLng}`); | |
| // } | |
| // const title = `Name : ${caseObj.case_applicant_name || ""}`; | |
| // drawHeader(doc, title); | |
| // let y = 3.4 * CM; | |
| // // Customer Details section | |
| // y = sectionTitle(doc, "Customer Details", MARGIN, y); | |
| // labelValue(doc, MARGIN, y, "Customer Name", caseObj.case_applicant_name); y += 0.6 * CM; | |
| // labelValue(doc, MARGIN, y, "Customer ID", caseObj.case_id); y += 0.6 * CM; | |
| // labelValue(doc, MARGIN, y, "Phone Number", caseObj.case_applicant_contact); y += 0.6 * CM; | |
| // const emailId = (caseObj.demographic_details || {}).contact_information && (caseObj.demographic_details.contact_information.email_id); | |
| // labelValue(doc, MARGIN, y, "Email", emailId); y += 0.6 * CM; | |
| // y = drawWrappedLabelValue(doc, MARGIN, y, "Customer Address", caseAddr, 18.0); y += 0.4 * CM; | |
| // // Inspection Summary | |
| // y = sectionTitle(doc, "Inspection Summary", MARGIN, y); | |
| // doc.font("Helvetica").fontSize(10); | |
| // doc.text(`Request Started: ${fmtIST(caseObj.created_at)}`, MARGIN, y); y += 0.5 * CM; | |
| // doc.text(`Total Uploads: ${files.length} uploads`, MARGIN, y); y += 0.5 * CM; | |
| // doc.text(`Status: ${caseObj.status}`, MARGIN, y); y += 0.5 * CM; | |
| // doc.text(`Priority: ${caseObj.priority}`, MARGIN, y); y += 0.7 * CM; | |
| // // render static map if caseLat present | |
| // if (caseLat) { | |
| // console.log("Generating and compressing overview map..."); | |
| // const mapBuf = await staticMapWithPath(caseLat, caseLng, caseLat, caseLng, 12, "1200x400"); // Smaller size | |
| // if (mapBuf) { | |
| // const compressedMapBuf = await compressStaticMap(mapBuf); | |
| // const mapH = 8 * CM; // Smaller height | |
| // const mapW = A4_WIDTH - 2 * MARGIN; | |
| // doc.image(compressedMapBuf, MARGIN, y, { width: mapW, height: mapH }); | |
| // y += mapH + 0.8 * CM; | |
| // } | |
| // } | |
| // drawFooter(doc, page); | |
| // doc.addPage(); page += 1; | |
| // // Page 2: thumbnails (limited to 6 for size) | |
| // drawHeader(doc, title); | |
| // y = 3.4 * CM; | |
| // doc.font("Helvetica-Bold").fontSize(14).text("Image Thumbnails", MARGIN, y); | |
| // y += 0.8 * CM; | |
| // const x0 = MARGIN; | |
| // const y0 = y; | |
| // const colW = (A4_WIDTH - 2 * MARGIN) / 3; | |
| // const rowH = 5.5 * CM; // Smaller row height | |
| // // Limit thumbnails to 6 max for file size | |
| // const maxThumbnails = Math.min(6, files.length); | |
| // for (let i = 0; i < maxThumbnails; ++i) { | |
| // const f = files[i]; | |
| // console.log(`Creating optimized thumbnail for upload ${i+1}...`); | |
| // const imgBuf = await makeThumb(f._bin); | |
| // const col = i % 3; | |
| // const row = Math.floor(i / 3); | |
| // const xi = x0 + col * colW + 0.2 * CM; | |
| // const yi = y0 + row * rowH + 4.0 * CM; // Adjusted position | |
| // try { | |
| // doc.image(imgBuf, xi, yi, { width: colW - 0.8 * CM, height: 3.5 * CM }); // Smaller thumbnails | |
| // } catch (e) { | |
| // console.warn(`Could not draw thumbnail ${i+1}:`, e.message); | |
| // } | |
| // doc.font("Helvetica").fontSize(8).text(`Upload ${i + 1} — ${displayType(f.file_type)}`, xi, yi - 0.3 * CM); | |
| // } | |
| // drawFooter(doc, page); | |
| // doc.addPage(); page += 1; | |
| // // Page 3: Demographic, Business, Financial | |
| // drawHeader(doc, title); | |
| // y = 3.4 * CM; | |
| // y = sectionTitle(doc, "Demographic Details", MARGIN, y); | |
| // const demo = caseObj.demographic_details || {}; | |
| // const ci = demo.contact_information || {}; | |
| // const pd = demo.personal_details || {}; | |
| // labelValue(doc, MARGIN, y, "Aadhaar Photo Match", demo.aadhaar_photo_match); y += 0.5 * CM; | |
| // labelValue(doc, MARGIN, y, "Email ID", ci.email_id); y += 0.5 * CM; | |
| // labelValue(doc, MARGIN, y, "Mobile Number", ci.mobile_number); y += 0.5 * CM; | |
| // labelValue(doc, MARGIN, y, "Gender", pd.gender); y += 0.5 * CM; | |
| // labelValue(doc, MARGIN, y, "Education", pd.education); y += 0.5 * CM; | |
| // labelValue(doc, MARGIN, y, "Family Members", pd.number_of_family_members); y += 1.0 * CM; | |
| // y = sectionTitle(doc, "Business Details", MARGIN, y); | |
| // const bd = caseObj.business_details || {}; | |
| // const ei = bd.enterprise_information || {}; | |
| // const bld = bd.business_location_details || {}; | |
| // const ba = bd.business_address || {}; | |
| // labelValue(doc, MARGIN, y, "Enterprise Name", ei.enterprise_name); y += 0.5 * CM; | |
| // labelValue(doc, MARGIN, y, "Organization Type", ei.type_of_organization); y += 0.5 * CM; | |
| // labelValue(doc, MARGIN, y, "Business Location Type", bld.business_location); y += 0.5 * CM; | |
| // labelValue(doc, MARGIN, y, "Address", ba.address); y += 0.5 * CM; | |
| // labelValue(doc, MARGIN, y, "City/District/State/Pincode", `${ba.city || ""}, ${ba.district || ""}, ${ba.state || ""} - ${ba.pincode || ""}`); y += 1.0 * CM; | |
| // y = sectionTitle(doc, "Financial Details", MARGIN, y); | |
| // const fin = caseObj.financial_details || {}; | |
| // const bfi = fin.business_financial_information || {}; | |
| // const ffi = fin.family_financial_information || {}; | |
| // function fmtInr(v) { | |
| // try { | |
| // if (v == null || v === "" || isNaN(v)) return "N/A"; | |
| // const amount = parseInt(v); | |
| // return `₹ ${amount.toLocaleString('en-IN')}`; | |
| // } catch { | |
| // return "N/A"; | |
| // } | |
| // } | |
| // labelValue(doc, MARGIN, y, "Monthly Income", fmtInr(bfi.monthly_income_from_business)); y += 0.5 * CM; | |
| // labelValue(doc, MARGIN, y, "Monthly Expense", fmtInr(bfi.monthly_expense_of_business)); y += 0.5 * CM; | |
| // labelValue(doc, MARGIN, y, "Family Income", fmtInr(ffi.monthly_family_income)); y += 0.5 * CM; | |
| // drawFooter(doc, page); | |
| // doc.addPage(); page += 1; | |
| // // For each file, a detailed page (with aggressive image compression) | |
| // for (let idx = 0; idx < files.length; ++idx) { | |
| // const f = files[idx]; | |
| // console.log(`Processing detailed page for upload ${idx+1}...`); | |
| // drawHeader(doc, title); | |
| // y = 3.4 * CM; | |
| // doc.font("Helvetica-Bold").fontSize(14).text(`Upload ${idx + 1} — ${displayType(f.file_type)}`, MARGIN, y); | |
| // y += 0.8 * CM; | |
| // // integrity_dashboard image (skip if not critical) | |
| // try { | |
| // const integrityPath = "integrity_dashboard.png"; | |
| // if (fs.existsSync(integrityPath) && fs.statSync(integrityPath).size < 100 * 1024) { | |
| // const imageWidth = 16 * CM; | |
| // const imageHeight = 2.5 * CM; | |
| // const centerX = (A4_WIDTH - imageWidth) / 2; | |
| // doc.image(integrityPath, centerX, y, { width: imageWidth, height: imageHeight }); | |
| // y = y + imageHeight + 0.8 * CM; | |
| // } else { | |
| // y += 0.5 * CM; | |
| // } | |
| // } catch (e) { | |
| // y += 0.5 * CM; | |
| // } | |
| // const gl = f.geo_location || {}; | |
| // const lat = gl.lat; | |
| // const lng = gl.lng; | |
| // // Main image with aggressive compression | |
| // console.log(`Creating compressed main image for upload ${idx+1}...`); | |
| // const imgBuf = await compressImageBuffer(f._bin, IMAGE_COMPRESSION_QUALITY - 10, 1000); | |
| // try { | |
| // doc.image(imgBuf, MARGIN, y, { width: 8.0 * CM, height: 8.0 * CM }); // Smaller size | |
| // } catch (e) { | |
| // console.warn(`Could not draw main image ${idx+1}:`, e.message); | |
| // } | |
| // // Static map (only if coordinates exist) | |
| // if (lat != null && lng != null) { | |
| // console.log(`Generating compressed static map for upload ${idx+1}...`); | |
| // const mimgBuf = await staticMapWithPath(lat, lng, caseLat, caseLng, 17, "800x600"); // Smaller map | |
| // if (mimgBuf) { | |
| // const compressedMapBuf = await compressStaticMap(mimgBuf); | |
| // try { | |
| // doc.image(compressedMapBuf, 10.5 * CM, y, { width: 7.5 * CM, height: 7.5 * CM }); // Smaller | |
| // } catch (e) { | |
| // console.warn(`Could not draw static map ${idx+1}:`, e.message); | |
| // } | |
| // } | |
| // } | |
| // let yb = y + 9.0 * CM; // Adjusted for smaller images | |
| // // Reverse geocode | |
| // const rev = await reverseGeocode(lat, lng); | |
| // yb = drawWrappedLabelValue(doc, MARGIN, yb, "Location Address", rev, 18.0); | |
| // const distKm = (caseLat != null && lat != null) ? haversineKm(caseLat, caseLng, lat, lng) : null; | |
| // const distM = distKm ? Math.round(distKm * 1000) : null; | |
| // doc.font("Helvetica").fontSize(10); | |
| // doc.text(distM ? `Distance from case location: ${distM} meters` : "Distance from case location: N/A", MARGIN, yb); | |
| // yb += 0.7 * CM; | |
| // const upAt = fmtIST(f.uploaded_at); | |
| // doc.text(`Uploaded At: ${upAt}`, MARGIN, yb); yb += 0.8 * CM; | |
| // // Condensed metadata section | |
| // doc.font("Helvetica-Bold").fontSize(11).text("Metadata", MARGIN, yb); yb += 0.5 * CM; | |
| // doc.font("Helvetica").fontSize(9); | |
| // doc.text(`Server Time: ${upAt}`, MARGIN, yb); yb += 0.4 * CM; | |
| // if (lat != null) { | |
| // doc.text(`Location: ${lat.toFixed(4)}, ${lng.toFixed(4)}`, MARGIN, yb); | |
| // } | |
| // yb += 0.4 * CM; | |
| // drawFooter(doc, page); | |
| // // Only add new page if not the last file | |
| // if (idx < files.length - 1) { | |
| // doc.addPage(); | |
| // page += 1; | |
| // } | |
| // } | |
| // // Timeline page (optional - can be skipped for very large reports) | |
| // if (files.length <= 10) { // Only add timeline for reasonable number of files | |
| // console.log("Creating timeline page..."); | |
| // doc.addPage(); page += 1; | |
| // drawHeader(doc, title); | |
| // y = 3.4 * CM; | |
| // doc.font("Helvetica-Bold").fontSize(14).text("Inspection Timeline", MARGIN, y); y += 0.8 * CM; | |
| // doc.save(); | |
| // doc.roundedRect(MARGIN, y, A4_WIDTH - 2 * MARGIN, 1.1 * CM, 0.3 * CM).fillOpacity(1).fill("#E0F7E6"); | |
| // doc.fillColor("#1F743F").font("Helvetica-Bold").fontSize(12).text("Ready for Review", MARGIN + 0.6 * CM, y + 0.4 * CM); | |
| // doc.fillColor("black"); | |
| // doc.restore(); | |
| // y += 2.0 * CM; | |
| // const events = []; | |
| // function evt(text, ts, sub = null) { | |
| // if (ts) events.push({ text, ts, sub }); | |
| // } | |
| // evt("Case Created", caseObj.created_at); | |
| // evt("Assigned to Agent", caseObj.assigned_at); | |
| // evt("Accepted by Agent", caseObj.accepted_at); | |
| // // Limit timeline events | |
| // for (let i = 0; i < Math.min(files.length, 5); i++) { | |
| // const f = files[i]; | |
| // const sub = await reverseGeocode((f.geo_location || {}).lat, (f.geo_location || {}).lng); | |
| // evt(`Upload — ${displayType(f.file_type)}`, f.uploaded_at, sub); | |
| // } | |
| // evt("Case Updated", caseObj.updated_at); | |
| // evt("Case Completed", caseObj.completed_at); | |
| // function parseTs(s) { | |
| // try { | |
| // return new Date(s); | |
| // } catch { | |
| // return new Date(0); | |
| // } | |
| // } | |
| // events.sort((a, b) => parseTs(a.ts) - parseTs(b.ts)); | |
| // for (let ev of events) { | |
| // doc.font("Helvetica").fontSize(9).text(`• ${fmtIST(ev.ts)}`, MARGIN, y); y += 0.4 * CM; | |
| // doc.font("Helvetica-Bold").fontSize(10).text(ev.text, MARGIN + 0.6 * CM, y); y += 0.4 * CM; | |
| // if (ev.sub) { | |
| // doc.font("Helvetica").fontSize(8).fillColor("gray"); | |
| // const subText = ev.sub.length > 50 ? ev.sub.substring(0, 47) + "..." : ev.sub; | |
| // doc.text(` ${subText}`, MARGIN + 0.6 * CM, y); | |
| // doc.fillColor("black"); | |
| // y += 0.4 * CM; | |
| // } | |
| // y += 0.2 * CM; | |
| // if (y > A4_HEIGHT - 3 * CM) { | |
| // drawFooter(doc, page); | |
| // doc.addPage(); page += 1; | |
| // drawHeader(doc, title); | |
| // y = 3.4 * CM; | |
| // doc.font("Helvetica-Bold").fontSize(14).text("Inspection Timeline (Cont.)", MARGIN, y); y += 1.0 * CM; | |
| // } | |
| // } | |
| // drawFooter(doc, page); | |
| // } | |
| // doc.end(); | |
| // ws.on("finish", () => { | |
| // console.log(`Optimized PDF created successfully at ${outPath}`); | |
| // resolve(); | |
| // }); | |
| // ws.on("error", (e) => { | |
| // console.error("PDF write stream error:", e); | |
| // reject(e); | |
| // }); | |
| // } catch (e) { | |
| // console.error("PDF build error:", e); | |
| // reject(e); | |
| // } | |
| // }); | |
| // } | |
| // // Helper functions | |
| // function sectionTitle(doc, text, x, y) { | |
| // doc.font("Helvetica-Bold").fontSize(14).text(text, x, y); | |
| // return y + 0.9 * CM; | |
| // } | |
| // function labelValue(doc, x, y, label, value) { | |
| // doc.font("Helvetica-Bold").fontSize(10).text(`${label}:`, x, y); | |
| // doc.font("Helvetica").fontSize(10).text((value == null || value === "" || (Array.isArray(value) && value.length === 0)) ? "N/A" : value.toString(), x + 4.8 * CM, y); | |
| // } | |
| // // FastAPI-equivalent endpoints | |
| // app.get("/health", (req, res) => { | |
| // console.log("Health check"); | |
| // res.json({ | |
| // status: "healthy", | |
| // env_loaded: !!ADMIN_EMAIL && !!ADMIN_PASSWORD && !!AES_KEY_STR && !!AES_IV_STR, | |
| // pdf_compression: { | |
| // enabled: PDF_COMPRESSION_ENABLED, | |
| // image_quality: IMAGE_COMPRESSION_QUALITY, | |
| // thumbnail_quality: THUMBNAIL_QUALITY, | |
| // max_image_dimension: MAX_IMAGE_DIMENSION | |
| // } | |
| // }); | |
| // }); | |
| // app.post("/generate-report", async (req, res) => { | |
| // console.log("Generate report request received"); | |
| // try { | |
| // const caseId = (req.body || {}).case_id; | |
| // if (!caseId) { | |
| // console.error("Missing case_id in request"); | |
| // return res.status(400).json({ error: "case_id required" }); | |
| // } | |
| // console.log(`Generating optimized report for case: ${caseId}`); | |
| // await adminLogin(); | |
| // const caseObj = await getCase(caseId); | |
| // const files = await getFiles(caseId); | |
| // console.log(`Downloading and compressing ${files.length} files...`); | |
| // // Download and immediately compress files | |
| // for (let i = 0; i < files.length; i++) { | |
| // console.log(`Processing file ${i+1}/${files.length}...`); | |
| // const originalBuffer = await downloadFile(files[i].file_id); | |
| // // Store compressed version | |
| // files[i]._bin = await compressImageBuffer(originalBuffer, IMAGE_COMPRESSION_QUALITY); | |
| // } | |
| // // Create output directory | |
| // const outDir = "/tmp/output"; | |
| // if (!fs.existsSync(outDir)) { | |
| // console.log(`Creating output directory: ${outDir}`); | |
| // fs.mkdirSync(outDir, { recursive: true }); | |
| // } | |
| // // Create temporary files | |
| // const timestamp = Date.now(); | |
| // const originalFilename = `Report_${caseId}_${timestamp}.pdf`; | |
| // const optimizedFilename = `Report_${caseId}_${timestamp}_optimized.pdf`; | |
| // const originalPath = path.join(outDir, originalFilename); | |
| // const optimizedPath = path.join(outDir, optimizedFilename); | |
| // console.log(`Building optimized PDF at: ${originalPath}`); | |
| // await buildPdf(caseObj, files, originalPath); | |
| // // Optimize the PDF | |
| // console.log(`Optimizing PDF...`); | |
| // await optimizePDF(originalPath, optimizedPath); | |
| // // Read the optimized PDF | |
| // const pdfContent = fs.readFileSync(optimizedPath); | |
| // // Get file sizes for logging | |
| // const originalStats = fs.existsSync(originalPath) ? fs.statSync(originalPath) : { size: 0 }; | |
| // const optimizedStats = fs.statSync(optimizedPath); | |
| // const compressionRatio = originalStats.size > 0 ? | |
| // ((originalStats.size - optimizedStats.size) / originalStats.size * 100).toFixed(1) : "N/A"; | |
| // console.log(`PDF optimization results:`); | |
| // console.log(` Final size: ${Math.round(optimizedStats.size / 1024)}KB`); | |
| // if (originalStats.size > 0) { | |
| // console.log(` Reduction: ${compressionRatio}% (from ${Math.round(originalStats.size / 1024)}KB)`); | |
| // } | |
| // // Cleanup | |
| // [originalPath, optimizedPath].forEach(filePath => { | |
| // try { | |
| // if (fs.existsSync(filePath)) { | |
| // fs.unlinkSync(filePath); | |
| // console.log(`Cleaned up: ${path.basename(filePath)}`); | |
| // } | |
| // } catch (e) { | |
| // console.warn(`Could not delete ${filePath}:`, e.message); | |
| // } | |
| // }); | |
| // const outputFilename = `Report_${caseId}.pdf`; | |
| // console.log(`Sending optimized PDF: ${outputFilename} (${Math.round(optimizedStats.size / 1024)}KB)`); | |
| // res.setHeader("Content-Type", "application/pdf"); | |
| // res.setHeader("Content-Disposition", `attachment; filename="${outputFilename}"`); | |
| // res.setHeader("X-PDF-Size", `${Math.round(optimizedStats.size / 1024)}KB`); | |
| // res.setHeader("X-PDF-Compression", "Hugging Face Optimized"); | |
| // res.send(pdfContent); | |
| // console.log("Optimized PDF sent successfully"); | |
| // } catch (e) { | |
| // console.error("Error generating PDF:", e); | |
| // res.status(500).json({ | |
| // error: `Error generating PDF: ${e.message || String(e)}`, | |
| // stack: process.env.NODE_ENV === 'development' ? e.stack : undefined | |
| // }); | |
| // } | |
| // }); | |
| // app.get("/", (req, res) => { | |
| // console.log("Root endpoint accessed"); | |
| // res.json({ | |
| // message: "Case Report Generator API (Hugging Face Optimized)", | |
| // status: "running", | |
| // pdf_compression: { | |
| // enabled: PDF_COMPRESSION_ENABLED, | |
| // image_quality: IMAGE_COMPRESSION_QUALITY, | |
| // thumbnail_quality: THUMBNAIL_QUALITY, | |
| // max_image_dimension: MAX_IMAGE_DIMENSION, | |
| // note: "Optimized for Hugging Face Spaces - uses aggressive WebP compression" | |
| // }, | |
| // endpoints: { | |
| // health: "GET /health", | |
| // generate: "POST /generate-report" | |
| // } | |
| // }); | |
| // }); | |
| // const PORT = process.env.PORT || 7860; | |
| // app.listen(PORT, "0.0.0.0", () => { | |
| // console.log(`Server listening on port ${PORT}`); | |
| // console.log(`PDF Compression: ${PDF_COMPRESSION_ENABLED ? 'ENABLED' : 'DISABLED'}`); | |
| // console.log(`Image Quality: ${IMAGE_COMPRESSION_QUALITY}%`); | |
| // console.log(`Thumbnail Quality: ${THUMBNAIL_QUALITY}%`); | |
| // console.log(`Max Image Dimension: ${MAX_IMAGE_DIMENSION}px`); | |
| // console.log(`Optimized for Hugging Face Spaces`); | |
| // }); |