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(null); const [reportData, setReportData] = useState(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 (

Daily Sales Report

setDate(e.target.value)} /> setDistributor(e.target.value)} options={[{ value: '', label: 'Select Distributor' }, ...(route ? (ROUTE_WISE_DISTRIBUTORS[route] || []) : []).map(d => ({ value: d, label: d }))]} />