updates on the orderdetails page and implemented the download option
This commit is contained in:
197
src/lib/ordersPdfGenerator.js
Normal file
197
src/lib/ordersPdfGenerator.js
Normal file
@@ -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`);
|
||||
}
|
||||
276
src/pages/doormile/reports/OperationalReportCards.jsx
Normal file
276
src/pages/doormile/reports/OperationalReportCards.jsx
Normal file
@@ -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 (
|
||||
<div className="space-y-3 pt-1">
|
||||
{/* Section Header matching Reference Image: OPERATIONAL REPORTS */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-[11px] font-bold uppercase tracking-wider text-slate-400 font-heading">
|
||||
OPERATIONAL REPORTS
|
||||
</h2>
|
||||
<span className="text-xs text-slate-400">
|
||||
Downloadable executive summaries
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 3 Report Cards in a Responsive Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{REPORTS.map((report) => {
|
||||
const Icon = report.icon;
|
||||
const isBusy = downloadingCardId === report.id;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={report.id}
|
||||
className="bg-white rounded-2xl border border-slate-200/90 shadow-2xs hover:shadow-md hover:border-slate-300 transition-all p-5 flex flex-col justify-between group"
|
||||
>
|
||||
<div>
|
||||
{/* Top Row: Icon on left, Operational Badge on right */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div
|
||||
className={cn(
|
||||
'w-9 h-9 rounded-xl flex items-center justify-center shrink-0 shadow-2xs transition-transform group-hover:scale-105',
|
||||
report.iconColor
|
||||
)}
|
||||
>
|
||||
<Icon className="w-4.5 h-4.5" />
|
||||
</div>
|
||||
|
||||
<span className="px-2.5 py-0.5 rounded-full text-[10.5px] font-semibold bg-slate-100 text-slate-600 border border-slate-200/80">
|
||||
Operational
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Card Title */}
|
||||
<h3 className="text-base font-bold text-slate-900 font-heading mt-3.5 tracking-tight group-hover:text-brand transition-colors">
|
||||
{report.title}
|
||||
</h3>
|
||||
|
||||
{/* Card Description */}
|
||||
<p className="text-xs text-slate-500 mt-1.5 leading-relaxed line-clamp-2">
|
||||
{report.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Card Footer: Metadata on left, Export action on right */}
|
||||
<div className="flex items-center justify-between pt-4 mt-4 border-t border-slate-100 text-xs">
|
||||
<span className="text-slate-400 font-medium text-[11px]">
|
||||
{report.footerMeta}
|
||||
</span>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
className="inline-flex items-center gap-1.5 font-semibold text-slate-700 hover:text-brand active:scale-95 transition-all cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
{isBusy ? (
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin text-brand" />
|
||||
) : (
|
||||
<ArrowDownToLine className="w-3.5 h-3.5" />
|
||||
)}
|
||||
<span>Export</span>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-52 p-1.5">
|
||||
<DropdownMenuLabel className="text-[10px] uppercase font-bold text-slate-400 tracking-wider px-2 py-1">
|
||||
Choose Export Format
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDownloadPdf(report)}
|
||||
className="cursor-pointer flex items-center gap-2 text-xs font-semibold py-2 px-2.5 rounded-lg text-slate-800 hover:text-brand hover:bg-brand-tint/60 transition-colors"
|
||||
>
|
||||
<FileText className="w-4 h-4 text-brand" />
|
||||
<div className="flex-1">
|
||||
<div>Download PDF Report</div>
|
||||
<div className="text-[10px] text-slate-400 font-normal">
|
||||
Branded summary with metrics
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDownloadExcel(report)}
|
||||
className="cursor-pointer flex items-center gap-2 text-xs font-semibold py-2 px-2.5 rounded-lg text-slate-800 hover:text-emerald-700 hover:bg-emerald-50 transition-colors"
|
||||
>
|
||||
<FileSpreadsheet className="w-4 h-4 text-emerald-600" />
|
||||
<div className="flex-1">
|
||||
<div>Download Excel (.xlsx)</div>
|
||||
<div className="text-[10px] text-slate-400 font-normal">
|
||||
Full tabular spreadsheet
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator className="my-1" />
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleApplyToTable(report)}
|
||||
className="cursor-pointer flex items-center gap-2 text-xs font-medium py-1.5 px-2.5 rounded-lg text-slate-600 hover:text-slate-900 hover:bg-slate-100 transition-colors"
|
||||
>
|
||||
<Calendar className="w-3.5 h-3.5 text-slate-400" />
|
||||
<span>View in Table Below</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { ListToolbar } from '@/components/doormile/ListToolbar';
|
||||
import { useDeliveries, useRiderOptions, useTenants } from '@/lib/doormileHooks';
|
||||
import { formatDoormileTimestamp } from '@/lib/doormileTimestamp';
|
||||
import { currency, exportRows, km as formatKm, matchesQuery, orDash, useDebouncedValue } from '@/lib/doormileFormat';
|
||||
import OperationalReportCards from './OperationalReportCards';
|
||||
|
||||
/**
|
||||
* Orders details — the row-level export behind the summary.
|
||||
@@ -160,7 +161,10 @@ export default function OrdersDetails() {
|
||||
];
|
||||
|
||||
return (
|
||||
<Stack space="lg">
|
||||
<Stack space="xl">
|
||||
{/* Downloadable Operational Report Cards matching reference */}
|
||||
<OperationalReportCards onSelectRange={setRange} />
|
||||
|
||||
<Stack space="sm">
|
||||
<ListToolbar
|
||||
search={search}
|
||||
|
||||
Reference in New Issue
Block a user