krishna_sales/src/screens/DailySalesReportPage.tsx
2026-09-07 13:09:32 +05:30

391 lines
15 KiB
TypeScript

import { useState, useEffect, type FormEvent } from 'react';
import { Card, Input, Select } from '../components/reusable';
import { Button } from '../components/buttons/Button';
import {
SALES_REPORT_ROUTES,
ROUTE_WISE_DISTRIBUTORS,
BASE_URL,
PIPELINE,
MAX_SKUS_PER_CHUNK
} from '../api/config';
import { Download, Printer } from 'lucide-react';
import jsPDF from 'jspdf';
import autoTable from 'jspdf-autotable';
export function DailySalesReportPage() {
const [date, setDate] = useState('');
const [route, setRoute] = useState('');
const [distributor, setDistributor] = useState('');
const [soEmail, setSoEmail] = useState('');
const [soOptions, setSoOptions] = useState<{ value: string, label: string }[]>([]);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const [reportData, setReportData] = useState<any>(null);
useEffect(() => {
async function fetchSOs() {
try {
const res = await fetch(`${BASE_URL}${PIPELINE.endpoints.salesOfficers}`);
const data = await res.json();
if (data?.response?.options) {
setSoOptions(data.response.options);
}
} catch (err) {
console.error('Failed to fetch SOs', err);
}
}
fetchSOs();
}, []);
const generatePdfDoc = () => {
const totalSkus = reportData?.response?.headers?.reduce((acc: number, h: any) => acc + (h.skus?.length || 0), 0) || 0;
const maxSkusOnPage = Math.min(totalSkus, MAX_SKUS_PER_CHUNK);
const requiredWidth = Math.max(297, (maxSkusOnPage + 3) * 12 + 40);
const doc = new jsPDF({ orientation: 'landscape', format: [requiredWidth, 210] });
const pageWidth = doc.internal.pageSize.getWidth();
const centerX = pageWidth / 2;
const rightMargin = pageWidth - 14;
const drawPageHeader = () => {
// Headers
doc.setFontSize(16);
doc.setFont('helvetica', 'bold');
doc.text('Krishna Flour Mills (Bangalore) Pvt. Limited', centerX, 15, { align: 'center' });
doc.setFontSize(11);
doc.setFont('helvetica', 'normal');
doc.text('19, Platform Road, Bengaluru - 560 020', centerX, 22, { align: 'center' });
doc.setFontSize(12);
doc.setFont('helvetica', 'bold');
doc.text('DAILY ORDER REPORT', centerX, 30, { align: 'center' });
// Simple underline for DAILY ORDER REPORT
const textWidth = doc.getTextWidth('DAILY ORDER REPORT');
doc.setLineWidth(0.5);
doc.line(centerX - textWidth / 2, 31, centerX + textWidth / 2, 31);
// Meta Info
doc.setFontSize(10);
let leftY = 40;
if (distributor) {
doc.text(`Distributor Name : ${distributor}`, 14, leftY);
leftY += 5;
}
let rightY = 40;
if (date) {
doc.text(`Date : ${date}`, rightMargin, rightY, { align: 'right' });
rightY += 5;
}
if (soEmail) {
const selectedSo = soOptions.find(o => o.value === soEmail);
doc.text(`SO Name : ${selectedSo ? selectedSo.label : soEmail}`, rightMargin, rightY, { align: 'right' });
rightY += 5;
}
};
// Split headers into chunks of exactly MAX_SKUS_PER_CHUNK, even if it means splitting a brand.
const headerChunks: any[][] = [];
let currentChunk: any[] = [];
let currentSkuCount = 0;
reportData?.response?.headers?.forEach((h: any) => {
const allSkus = h.skus || [];
if (allSkus.length === 0) return;
let remainingSkus = [...allSkus];
while (remainingSkus.length > 0) {
const availableSpace = MAX_SKUS_PER_CHUNK - currentSkuCount;
// Take as many SKUs as we can fit in the current chunk
const skusToTake = remainingSkus.splice(0, availableSpace);
currentChunk.push({
...h,
skus: skusToTake
});
currentSkuCount += skusToTake.length;
if (currentSkuCount >= MAX_SKUS_PER_CHUNK) {
headerChunks.push(currentChunk);
currentChunk = [];
currentSkuCount = 0;
}
}
});
if (currentChunk.length > 0) {
headerChunks.push(currentChunk);
}
if (headerChunks.length === 0) {
drawPageHeader(); // Fallback if no data
}
headerChunks.forEach((chunkHeaders, index) => {
if (index === 0) {
drawPageHeader();
} else {
doc.addPage();
drawPageHeader();
}
const headRow1: any[] = [
{ content: 'Sl No', rowSpan: 2, styles: { halign: 'center', valign: 'middle' } },
{ content: 'Store', rowSpan: 2, styles: { halign: 'center', valign: 'middle' } }
];
const headRow2: any[] = [];
chunkHeaders.forEach((h: any) => {
headRow1.push({ content: h.br_code, colSpan: h.skus?.length || 1, styles: { halign: 'center' } });
h.skus?.forEach((sku: string) => {
headRow2.push({ content: sku, styles: { halign: 'center' } });
});
});
const isLastChunk = index === headerChunks.length - 1;
if (isLastChunk) {
headRow1.push({ content: 'Total', rowSpan: 2, styles: { halign: 'center', valign: 'middle' } });
}
const body = reportData?.response?.rows?.map((row: any) => {
const rowData = [row.sl_no, row.store];
chunkHeaders.flatMap((h: any) => h.skus || []).forEach((sku: string) => {
const val = row.products?.[sku];
rowData.push(val === '0' || val === 0 ? '' : val || '');
});
if (isLastChunk) {
const val = row.total;
rowData.push(val === '0' || val === 0 ? '' : val || '');
}
return rowData;
}) || [];
if (reportData?.response?.total_row) {
const totalRowData = [reportData.response.total_row.sl_no, reportData.response.total_row.store];
chunkHeaders.flatMap((h: any) => h.skus || []).forEach((sku: string) => {
const val = reportData.response.total_row.products?.[sku];
totalRowData.push(val === '0' || val === 0 ? '' : val || '');
});
if (isLastChunk) {
const val = reportData.response.total_row.total;
totalRowData.push(val === '0' || val === 0 ? '' : val || '');
}
body.push(totalRowData);
}
autoTable(doc, {
head: [headRow1, headRow2],
body: body,
startY: 50,
theme: 'grid',
horizontalPageBreak: false,
styles: { fontSize: 6, textColor: [0, 0, 0], lineColor: [0, 0, 0], lineWidth: 0.2, cellPadding: 1, minCellWidth: 10, halign: 'center', valign: 'middle' },
headStyles: { fillColor: [243, 244, 246], fontStyle: 'bold', halign: 'center' },
didParseCell: (data) => {
if (data.section === 'body' && reportData?.response?.total_row && data.row.index === body.length - 1) {
data.cell.styles.fontStyle = 'bold';
data.cell.styles.fillColor = [230, 230, 230];
}
}
});
});
return doc;
};
const downloadPDF = () => {
const doc = generatePdfDoc();
doc.save(`Daily_Sales_Report_${date || 'Draft'}.pdf`);
};
const printReport = async () => {
const doc = generatePdfDoc();
const blob = doc.output('blob');
// Fallback for PC / unsupported browsers
const blobUrl = URL.createObjectURL(blob);
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = blobUrl;
document.body.appendChild(iframe);
iframe.onload = () => {
setTimeout(() => {
iframe.contentWindow?.focus();
iframe.contentWindow?.print();
}, 500);
};
};
async function submit(e: FormEvent) {
e.preventDefault();
setBusy(true);
setError(null);
setReportData(null);
try {
const res = await fetch(`${BASE_URL}${PIPELINE.endpoints.dailySalesReport}`, {
method: 'POST',
headers: {
'Accept': 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
body: JSON.stringify({
date,
route,
distributor,
so_name: soEmail,
})
});
if (!res.ok) {
throw new Error(`Error: ${res.status} ${res.statusText}`);
}
const data = await res.json();
setReportData(data);
} catch (err) {
setError((err as Error).message ?? 'Failed to fetch report');
} finally {
setBusy(false);
}
}
return (
<div className="flex flex-col gap-5 p-6 pb-20">
<h1 className="m-0 text-xl font-extrabold text-strong tracking-[-0.01em] print:hidden">Daily Sales Report</h1>
<Card className="print:hidden">
<form onSubmit={submit} className="flex flex-col gap-4">
<Input
label="Date"
type="date"
value={date}
onChange={(e) => setDate(e.target.value)}
/>
<Select
label="Route"
value={route}
onChange={(e) => {
setRoute(e.target.value);
setDistributor('');
}}
options={[{ value: '', label: 'Select Route' }, ...SALES_REPORT_ROUTES.map(r => ({ value: r, label: r }))]}
/>
<Select
label="Distributor"
value={distributor}
onChange={(e) => setDistributor(e.target.value)}
options={[{ value: '', label: 'Select Distributor' }, ...(route ? (ROUTE_WISE_DISTRIBUTORS[route] || []) : []).map(d => ({ value: d, label: d }))]}
/>
<Select
label="SO Name"
value={soEmail}
onChange={(e) => setSoEmail(e.target.value)}
options={[{ value: '', label: 'Select SO Name' }, ...soOptions]}
/>
{error && <div className="text-xs text-ruby-600 font-medium">{error}</div>}
<Button type="submit" full disabled={busy}>
{busy ? 'Fetching...' : 'Get Report'}
</Button>
</form>
</Card>
{reportData?.response && (
<Card className="overflow-x-auto p-0 pb-2 border-0 shadow-none print:m-0 print:p-0">
<div className="p-4 pb-2 print:hidden">
<h2 className="text-lg font-bold text-strong m-0">Report Details</h2>
</div>
<div className="w-full overflow-x-auto p-4 pt-2">
<div className="min-w-max">
<div className="flex flex-col items-center text-center gap-1">
<h1 className="m-0 text-lg font-bold text-strong">Krishna Flour Mills (Bangalore) Pvt. Limited</h1>
<h2 className="m-0 text-sm font-normal text-strong">19, Platform Road, Bengaluru - 560 020</h2>
<h3 className="m-0 text-base font-bold text-strong underline mt-1 mb-4">DAILY ORDER REPORT</h3>
</div>
<div className="flex justify-between items-start text-sm font-bold text-strong mb-2">
<div className="flex flex-col gap-1.5">
{distributor && <div>Distributor Name : {distributor}</div>}
</div>
<div className="text-right flex flex-col gap-1.5">
{date && <div>Date : {date}</div>}
{soEmail && <div>SO Name : {soOptions.find(o => o.value === soEmail)?.label || soEmail}</div>}
</div>
</div>
<table id="report-table" className="w-full text-sm text-left border border-black border-collapse mt-2">
<thead>
<tr className="border-b border-black bg-black/5">
<th className="py-2 px-3 font-bold text-black align-bottom border-r border-black whitespace-nowrap" rowSpan={2}>Sl No</th>
<th className="py-2 px-3 font-bold text-black align-bottom" rowSpan={2}>Store</th>
{reportData.response.headers?.map((h: any) => (
<th key={h.br_code} colSpan={h.skus?.length || 1} className="py-1 px-3 font-bold text-black text-center border-l border-black">
{h.br_code}
</th>
))}
<th className="py-2 px-3 font-bold text-black align-bottom border-l border-black" rowSpan={2}>Total</th>
</tr>
<tr className="border-b border-black bg-black/5">
{reportData.response.headers?.flatMap((h: any) => h.skus || []).map((sku: string, idx: number) => (
<th key={`${sku}-${idx}`} className="py-1 px-3 font-bold text-black text-center border-l border-black whitespace-nowrap">
{sku}
</th>
))}
</tr>
</thead>
<tbody>
{reportData.response.rows?.map((row: any) => (
<tr key={row.sl_no} className="border-b border-black hover:bg-black/5 transition-colors">
<td className="py-2 px-3 text-strong border-r border-black whitespace-nowrap">{row.sl_no}</td>
<td className="py-2 px-3 text-strong whitespace-nowrap">{row.store}</td>
{reportData.response.headers?.flatMap((h: any) => h.skus || []).map((sku: string, idx: number) => (
<td key={`${sku}-${idx}`} className="py-2 px-3 text-strong text-center border-l border-black">
{row.products?.[sku] === '0' || row.products?.[sku] === 0 ? '' : row.products?.[sku] || ''}
</td>
))}
<td className="py-2 px-3 text-strong font-bold text-center border-l border-black bg-black/5">{row.total === '0' || row.total === 0 ? '' : row.total || ''}</td>
</tr>
))}
{reportData.response.total_row && (
<tr className="border-b border-black bg-black/10 font-bold">
<td className="py-2 px-3 text-strong border-r border-black whitespace-nowrap">{reportData.response.total_row.sl_no}</td>
<td className="py-2 px-3 text-strong whitespace-nowrap text-right">{reportData.response.total_row.store}</td>
{reportData.response.headers?.flatMap((h: any) => h.skus || []).map((sku: string, idx: number) => (
<td key={`total-${sku}-${idx}`} className="py-2 px-3 text-strong text-center border-l border-black">
{reportData.response.total_row.products?.[sku] === '0' || reportData.response.total_row.products?.[sku] === 0 ? '' : reportData.response.total_row.products?.[sku] || ''}
</td>
))}
<td className="py-2 px-3 text-strong text-center border-l border-black">{reportData.response.total_row.total === '0' || reportData.response.total_row.total === 0 ? '' : reportData.response.total_row.total || ''}</td>
</tr>
)}
</tbody>
</table>
{(!reportData.response.rows || reportData.response.rows.length === 0) && (
<div className="py-8 text-center text-faint">No records found.</div>
)}
</div>
</div>
<div className="p-4 pt-4 pb-6 flex gap-4 print:hidden">
<Button type="button" onClick={downloadPDF} variant="secondary" iconLeft={<Download size={18} />} full>
Download PDF
</Button>
<Button type="button" onClick={printReport} iconLeft={<Printer size={18} />} full>
Print PDF
</Button>
</div>
</Card>
)}
</div>
);
}