diff --git a/package-lock.json b/package-lock.json index e6c9cce..d735a81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,8 @@ "clsx": "^2.1.1", "dayjs": "^1.11.23", "framer-motion": "^11.16.4", + "jspdf": "^4.2.1", + "jspdf-autotable": "^5.0.8", "leaflet": "^1.9.4", "lucide-react": "^0.475.0", "papaparse": "^5.6.0", @@ -1983,7 +1985,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -5027,6 +5028,19 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "license": "MIT" + }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/react": { "version": "19.2.18", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", @@ -5061,6 +5075,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", @@ -5661,6 +5682,16 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.11.12", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.12.tgz", @@ -5859,6 +5890,26 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/cfb": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", @@ -6076,6 +6127,21 @@ "dev": true, "license": "MIT" }, + "node_modules/core-js": { + "version": "3.50.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.50.0.tgz", + "integrity": "sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-js-compat": { "version": "3.50.0", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.50.0.tgz", @@ -6142,6 +6208,16 @@ "node": ">= 8" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/css-mediaquery": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz", @@ -6586,6 +6662,16 @@ "node": ">=12" } }, + "node_modules/dompurify": { + "version": "3.4.15", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.15.tgz", + "integrity": "sha512-EUBjM+B+lkDE41iE82DDSCfkoPGfXx8IxFxPMjNzm/Uk4xDet77rTN9wqlxlVg71kK7XGuUMv6wUxJUwwv+Xyw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -7278,6 +7364,17 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-png": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz", + "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==", + "license": "MIT", + "dependencies": { + "@types/pako": "^2.0.3", + "iobuffer": "^5.3.2", + "pako": "^2.1.0" + } + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -7297,6 +7394,12 @@ "bser": "2.1.1" } }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -7846,6 +7949,20 @@ "dev": true, "license": "MIT" }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "optional": true, + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -8037,6 +8154,12 @@ "loose-envify": "^1.0.0" } }, + "node_modules/iobuffer": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz", + "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==", + "license": "MIT" + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -9702,6 +9825,32 @@ "node": ">=6" } }, + "node_modules/jspdf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.1.tgz", + "integrity": "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "fast-png": "^6.2.0", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.11", + "core-js": "^3.6.0", + "dompurify": "^3.3.1", + "html2canvas": "^1.0.0-rc.5" + } + }, + "node_modules/jspdf-autotable": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-5.0.8.tgz", + "integrity": "sha512-Hy05N86yBO7CXBrnSLOge7i1ZYpKH2DjQ94iybaP7vBhSInjvRBgDc99ngKzSbSO8Jc98ZCally8I6n0tj2RJQ==", + "license": "MIT", + "peerDependencies": { + "jspdf": "^2 || ^3 || ^4" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -10331,6 +10480,22 @@ "node": ">=6" } }, + "node_modules/pako": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.2.0.tgz", + "integrity": "sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "(MIT AND Zlib)" + }, "node_modules/papaparse": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.6.0.tgz", @@ -10418,6 +10583,13 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", + "optional": true + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -10856,6 +11028,16 @@ ], "license": "MIT" }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, "node_modules/react": { "version": "19.2.8", "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", @@ -11186,6 +11368,13 @@ "node": ">=4" } }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true + }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -11345,6 +11534,16 @@ "node": ">=0.10.0" } }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, "node_modules/rollup": { "version": "4.62.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", @@ -11749,6 +11948,16 @@ "node": ">=8" } }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -12004,6 +12213,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -12103,6 +12322,16 @@ "node": ">=8" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -12565,6 +12794,16 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "optional": true, + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", diff --git a/package.json b/package.json index 8dde36c..28a2a60 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,8 @@ "clsx": "^2.1.1", "dayjs": "^1.11.23", "framer-motion": "^11.16.4", + "jspdf": "^4.2.1", + "jspdf-autotable": "^5.0.8", "leaflet": "^1.9.4", "lucide-react": "^0.475.0", "papaparse": "^5.6.0", diff --git a/src/lib/ordersPdfGenerator.js b/src/lib/ordersPdfGenerator.js new file mode 100644 index 0000000..63f8b5b --- /dev/null +++ b/src/lib/ordersPdfGenerator.js @@ -0,0 +1,197 @@ +import { jsPDF } from 'jspdf'; +import autoTable from 'jspdf-autotable'; +import dayjs from 'dayjs'; +import { formatDoormileTimestamp } from '@/lib/doormileTimestamp'; + +/** + * Generates an executive-grade operational PDF report for Doormile Orders. + */ +export async function generateOrdersPdfReport({ + title = 'Orders Report', + subtitle = 'Operational Delivery Breakdown', + dateRangeLabel = 'Last 7 Days', + fromDate, + toDate, + rows = [], +}) { + const doc = new jsPDF({ + orientation: 'landscape', + unit: 'mm', + format: 'a4', + }); + + const pageWidth = doc.internal.pageSize.getWidth(); + const pageHeight = doc.internal.pageSize.getHeight(); + + // 1. Top Brand Crimson Header Bar + doc.setFillColor(200, 16, 46); // Doormile Brand Crimson #C8102E + doc.rect(0, 0, pageWidth, 24, 'F'); + + // Company Name + doc.setTextColor(255, 255, 255); + doc.setFont('helvetica', 'bold'); + doc.setFontSize(15); + doc.text('DOORMILE EXPRESS', 14, 10.5); + + // Subtitle + doc.setFont('helvetica', 'normal'); + doc.setFontSize(8.5); + doc.text(`${title.toUpperCase()} · ${subtitle}`, 14, 17.5); + + // Date and Metadata on Right + doc.setFontSize(8); + doc.text( + `Period: ${dateRangeLabel} (${fromDate} to ${toDate})`, + pageWidth - 14, + 10.5, + { align: 'right' } + ); + doc.text( + `Generated: ${dayjs().format('DD MMM YYYY, hh:mm A')}`, + pageWidth - 14, + 17.5, + { align: 'right' } + ); + + // 2. Metric KPI Summary Ribbon + const totalOrders = rows.length; + const completedOrders = rows.filter((r) => + ['delivered', 'completed'].includes((r.orderstatus || '').toLowerCase()) + ).length; + const totalRevenue = rows.reduce( + (sum, r) => sum + (Number(r.deliverycharges) || 0), + 0 + ); + const totalKm = rows.reduce((sum, r) => sum + (Number(r.kms) || 0), 0); + + const kpis = [ + { label: 'TOTAL ORDERS', val: String(totalOrders) }, + { + label: 'DELIVERED SUCESSFULLY', + val: `${completedOrders} (${totalOrders ? Math.round((completedOrders / totalOrders) * 100) : 0}%)`, + }, + { + label: 'DELIVERY CHARGES', + val: `Rs. ${totalRevenue.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`, + }, + { label: 'TOTAL DISTANCE', val: `${totalKm.toFixed(1)} km` }, + ]; + + doc.setFillColor(248, 250, 252); + doc.rect(14, 28, pageWidth - 28, 13, 'F'); + doc.setDrawColor(226, 232, 240); + doc.rect(14, 28, pageWidth - 28, 13, 'S'); + + const colWidth = (pageWidth - 28) / kpis.length; + kpis.forEach((kpi, idx) => { + const x = 14 + idx * colWidth + 5; + doc.setTextColor(100, 116, 139); + doc.setFontSize(6.5); + doc.setFont('helvetica', 'bold'); + doc.text(kpi.label, x, 32.5); + + doc.setTextColor(16, 24, 32); + doc.setFontSize(9.5); + doc.setFont('helvetica', 'bold'); + doc.text(kpi.val, x, 38); + }); + + // 3. Tabular Orders List + const tableData = rows.map((r, i) => [ + i + 1, + r.orderid || '—', + formatDoormileTimestamp(r.orderdate, 'DD/MM/YY hh:mm A') || '—', + r.tenantname || '—', + (r.pickupaddress || '—').substring(0, 42), + `${r.deliverycustomer ? r.deliverycustomer + ' - ' : ''}${(r.deliveryaddress || '—').substring(0, 38)}`, + r.ridername || 'Unassigned', + r.kms ? `${Number(r.kms).toFixed(1)}` : '0.0', + r.deliverycharges != null ? `Rs. ${Number(r.deliverycharges).toFixed(2)}` : '—', + (r.orderstatus || 'pending').toUpperCase(), + ]); + + autoTable(doc, { + startY: 45, + head: [ + [ + '#', + 'Order ID', + 'Date & Time', + 'Client', + 'Pickup Location', + 'Drop Customer & Location', + 'Miler', + 'Km', + 'Amount', + 'Status', + ], + ], + body: + tableData.length > 0 + ? tableData + : [ + [ + '—', + 'No orders recorded in this date range', + '', + '', + '', + '', + '', + '', + '', + '', + ], + ], + theme: 'grid', + headStyles: { + fillColor: [200, 16, 46], + textColor: [255, 255, 255], + fontSize: 7.5, + fontStyle: 'bold', + halign: 'left', + cellPadding: 2, + }, + styles: { + fontSize: 7, + textColor: [30, 41, 59], + cellPadding: 1.8, + overflow: 'linebreak', + }, + alternateRowStyles: { + fillColor: [248, 250, 252], + }, + columnStyles: { + 0: { cellWidth: 8, halign: 'center' }, + 1: { cellWidth: 24, fontStyle: 'bold' }, + 2: { cellWidth: 26 }, + 3: { cellWidth: 26 }, + 4: { cellWidth: 50 }, + 5: { cellWidth: 54 }, + 6: { cellWidth: 26 }, + 7: { cellWidth: 14, halign: 'right' }, + 8: { cellWidth: 22, halign: 'right', fontStyle: 'bold' }, + 9: { cellWidth: 20, halign: 'center' }, + }, + margin: { left: 14, right: 14, top: 45, bottom: 12 }, + didDrawPage: (data) => { + // Footer + doc.setFontSize(7); + doc.setTextColor(148, 163, 184); + doc.text( + 'Doormile Express Operations Report · Confidential · System Generated', + 14, + pageHeight - 6 + ); + doc.text( + `Page ${doc.internal.getNumberOfPages()}`, + pageWidth - 14, + pageHeight - 6, + { align: 'right' } + ); + }, + }); + + const cleanTitle = title.replace(/\s+/g, '_'); + doc.save(`Doormile_${cleanTitle}_${fromDate}_to_${toDate}.pdf`); +} diff --git a/src/pages/doormile/reports/OperationalReportCards.jsx b/src/pages/doormile/reports/OperationalReportCards.jsx new file mode 100644 index 0000000..0cc4a41 --- /dev/null +++ b/src/pages/doormile/reports/OperationalReportCards.jsx @@ -0,0 +1,276 @@ +import React, { useState } from 'react'; +import dayjs from 'dayjs'; +import { + BarChart2, + TrendingUp, + Package, + ArrowDownToLine, + FileText, + FileSpreadsheet, + Calendar, + Loader2, + Check, +} from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { generateOrdersPdfReport } from '@/lib/ordersPdfGenerator'; +import { exportRows } from '@/lib/doormileFormat'; +import { fetchDeliveries } from '@/api/doormile/queries'; +import { OpenToast } from '@/components/third-party/OpenToast'; + +const EXPORT_COLUMNS = [ + { key: 'orderid', header: 'Order ID' }, + { key: 'orderdate', header: 'Date & Time' }, + { key: 'tenantname', header: 'Client' }, + { key: 'deliverycustomer', header: 'Customer' }, + { key: 'pickupaddress', header: 'Pickup' }, + { key: 'deliveryaddress', header: 'Drop' }, + { key: 'ridername', header: 'Miler' }, + { key: 'kms', header: 'Km' }, + { key: 'deliverycharges', header: 'Amount' }, + { key: 'orderstatus', header: 'Status' }, +]; + +export default function OperationalReportCards({ onSelectRange }) { + const [downloadingCardId, setDownloadingCardId] = useState(null); + + const today = dayjs().format('YYYY-MM-DD'); + + const REPORTS = [ + { + id: 'last-7-days', + days: 7, + title: 'Last 7 Days Orders Report', + description: + 'Complete order dispatch, miler fulfillment, and client billing breakdown for the trailing 7 days.', + fromDate: dayjs().subtract(6, 'day').format('YYYY-MM-DD'), + toDate: today, + footerMeta: 'Trailing 7d · PDF · XLSX', + icon: BarChart2, + iconColor: 'bg-brand-tint text-brand', + }, + { + id: 'last-15-days', + days: 15, + title: 'Last 15 Days Orders Report', + description: + 'Mid-month operational summary, delivery SLA tracking, and courier performance across hubs.', + fromDate: dayjs().subtract(14, 'day').format('YYYY-MM-DD'), + toDate: today, + footerMeta: 'Trailing 15d · PDF · XLSX', + icon: TrendingUp, + iconColor: 'bg-slate-100 text-slate-700', + }, + { + id: 'last-30-days', + days: 30, + title: 'Last 30 Days Orders Report', + description: + 'Monthly comprehensive operational audit of all orders, route kilometers, and delivery revenues.', + fromDate: dayjs().subtract(29, 'day').format('YYYY-MM-DD'), + toDate: today, + footerMeta: 'Trailing 30d · PDF · XLSX', + icon: Package, + iconColor: 'bg-emerald-50 text-emerald-700', + }, + ]; + + const fetchOrdersForReport = async (report) => { + try { + const res = await fetchDeliveries({ + pageParam: 1, + queryKey: [ + 'deliveries', + '', + '', + '', + report.fromDate, + report.toDate, + 500, + ], + }); + return res?.rows || []; + } catch (err) { + console.warn('Failed to fetch range-specific deliveries:', err); + return []; + } + }; + + const handleDownloadPdf = async (report) => { + setDownloadingCardId(report.id); + OpenToast(`Generating ${report.title} PDF...`, 'info', 2000); + + try { + const rows = await fetchOrdersForReport(report); + await generateOrdersPdfReport({ + title: report.title, + subtitle: `Trailing ${report.days} Days Breakdown`, + dateRangeLabel: `Last ${report.days} Days`, + fromDate: report.fromDate, + toDate: report.toDate, + rows, + }); + OpenToast(`${report.title} PDF downloaded successfully!`, 'success', 2500); + } catch (error) { + console.error('Error generating PDF:', error); + OpenToast('Failed to generate PDF report', 'error', 3000); + } finally { + setDownloadingCardId(null); + } + }; + + const handleDownloadExcel = async (report) => { + setDownloadingCardId(report.id); + OpenToast(`Preparing ${report.title} Excel export...`, 'info', 2000); + + try { + const rows = await fetchOrdersForReport(report); + await exportRows( + rows, + EXPORT_COLUMNS, + `Doormile_Orders_${report.days}Days_${report.fromDate}_to_${report.toDate}` + ); + OpenToast(`${report.title} Excel file downloaded!`, 'success', 2500); + } catch (error) { + console.error('Error generating Excel:', error); + OpenToast('Failed to export Excel report', 'error', 3000); + } finally { + setDownloadingCardId(null); + } + }; + + const handleApplyToTable = (report) => { + if (onSelectRange) { + onSelectRange({ from: report.fromDate, to: report.toDate }); + OpenToast(`Filtered table to ${report.title}`, 'info', 1800); + } + }; + + return ( +
+ {report.description} +
+