import React, { useCallback, useEffect, useRef, useState } from "react"; import { Search, Download, Printer, Calendar as CalendarIcon, BarChart3, LineChart, ArrowUpRight, RefreshCw, FileText } from "lucide-react"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"; import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "../ui/card"; import { Badge } from "../ui/badge"; import { Calendar } from "../ui/calendar"; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Line, LineChart as RechartsLineChart } from "recharts"; import { toast } from "sonner"; import { cn } from "../ui/utils"; import { skipCountForPage } from "../../lib/paginationQuery"; import { getLocations } from "../../services/locationService"; import { getPartners } from "../../services/partnerService"; import { getGroups } from "../../services/groupService"; import { exportLabelReportPdf, exportPrintLogPdf, getLabelReport, getReportsPrintLogList, reprintPrintLog, } from "../../services/reportsService"; import type { LocationDto } from "../../types/location"; import type { PartnerListItem } from "../../types/partner"; import type { GroupListItem } from "../../types/group"; import type { LabelReportData, ReportsPrintLogListItem } from "../../types/reports"; import { ApiError } from "../../lib/apiClient"; import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, } from "../ui/pagination"; const ALL = "all" as const; function defaultDateRange(): { start: string; end: string } { const end = new Date(); const start = new Date(end); start.setDate(start.getDate() - 29); return { start: start.toISOString().slice(0, 10), end: end.toISOString().slice(0, 10), }; } function parseIsoDate(value: string): Date | undefined { const s = (value ?? "").trim(); if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) return undefined; const d = new Date(`${s}T00:00:00`); if (Number.isNaN(d.getTime())) return undefined; return d; } function formatIsoDate(value: Date): string { const y = value.getFullYear(); const m = String(value.getMonth() + 1).padStart(2, "0"); const d = String(value.getDate()).padStart(2, "0"); return `${y}-${m}-${d}`; } function formatPrintedAt(raw: string): string { const s = (raw ?? "").trim(); if (!s) return "None"; const d = new Date(s); if (Number.isNaN(d.getTime())) return s; return d.toLocaleString("en-US", { year: "numeric", month: "2-digit", day: "2-digit", hour: "numeric", minute: "2-digit", second: "2-digit", hour12: true, }); } function toDisplay(v: string | null | undefined): string { const s = (v ?? "").trim(); return s ? s : "None"; } function formatPct(n: number): string { if (!Number.isFinite(n)) return "0%"; const sign = n > 0 ? "+" : ""; return `${sign}${n.toFixed(1)}%`; } function formatTrendLabel(iso: string): string { const s = (iso ?? "").trim(); if (!s) return ""; const d = new Date(s); if (Number.isNaN(d.getTime())) return s.length > 10 ? s.slice(0, 10) : s; return d.toLocaleDateString("en-US", { month: "numeric", day: "numeric" }); } function templateCell(template: string) { const t = template ?? ""; if (!t.trim()) return None; const hasAlert = t.endsWith(" !!!"); const main = hasAlert ? t.slice(0, -4) : t; const lastSpace = main.lastIndexOf(" "); const prefix = lastSpace < 0 ? "" : main.slice(0, lastSpace + 1); const boldPart = lastSpace < 0 ? main : main.slice(lastSpace + 1); return ( <> {prefix} {boldPart} {hasAlert && !!!} > ); } const EMPTY_LABEL_REPORT: LabelReportData = { summary: { totalLabelsPrinted: 0, totalLabelsPrintedPrevPeriod: 0, totalLabelsPrintedChangeRate: 0, hottestCategoryName: "None", hottestCategoryCount: 0, topProductName: "None", topProductCount: 0, avgDailyPrints: 0, avgDailyPrintsChangeRate: 0, }, labelsByCategory: [], printVolumeTrend: [], mostUsedProducts: [], }; export type ReportsViewProps = { layoutReportsOpenKey?: number; layoutReportsTargetTab?: "print-log" | "label-report"; }; export function ReportsView({ layoutReportsOpenKey = 0, layoutReportsTargetTab = "label-report", }: ReportsViewProps = {}) { const [activeTab, setActiveTab] = useState<"print-log" | "label-report">("print-log"); const lastAppliedLayoutKeyRef = useRef(0); const [partners, setPartners] = useState([]); const [groups, setGroups] = useState([]); const [locations, setLocations] = useState([]); const [partnerId, setPartnerId] = useState(ALL); const [groupId, setGroupId] = useState(ALL); const [locationId, setLocationId] = useState(ALL); const { start: defaultStart, end: defaultEnd } = defaultDateRange(); const [startDate, setStartDate] = useState(defaultStart); const [endDate, setEndDate] = useState(defaultEnd); const [keyword, setKeyword] = useState(""); const [debouncedKeyword, setDebouncedKeyword] = useState(""); const keywordTimerRef = useRef(null); const [printRows, setPrintRows] = useState([]); const [printTotal, setPrintTotal] = useState(0); const [printLoading, setPrintLoading] = useState(false); const [pageIndex, setPageIndex] = useState(1); const [pageSize] = useState(10); const [labelData, setLabelData] = useState(null); const [labelLoading, setLabelLoading] = useState(false); const [exporting, setExporting] = useState(false); const [reprintBusyId, setReprintBusyId] = useState(null); const [filterMetaLoading, setFilterMetaLoading] = useState(true); const printAbortRef = useRef(null); const labelAbortRef = useRef(null); useEffect(() => { if (keywordTimerRef.current) window.clearTimeout(keywordTimerRef.current); keywordTimerRef.current = window.setTimeout(() => setDebouncedKeyword(keyword.trim()), 300); return () => { if (keywordTimerRef.current) window.clearTimeout(keywordTimerRef.current); }; }, [keyword]); useEffect(() => { if (layoutReportsOpenKey > 0 && layoutReportsOpenKey > lastAppliedLayoutKeyRef.current) { lastAppliedLayoutKeyRef.current = layoutReportsOpenKey; setActiveTab(layoutReportsTargetTab); } }, [layoutReportsOpenKey, layoutReportsTargetTab]); const totalPages = Math.max(1, Math.ceil(printTotal / pageSize)); const partnerNameForQuery = useCallback((): string | undefined => { if (partnerId === ALL) return undefined; const p = partners.find((x) => x.id === partnerId); return p?.partnerName?.trim() || undefined; }, [partnerId, partners]); const groupNameForQuery = useCallback((): string | undefined => { if (groupId === ALL) return undefined; const g = groups.find((x) => x.id === groupId); return g?.groupName?.trim() || undefined; }, [groupId, groups]); const buildReportFilters = useCallback( () => ({ partnerId: partnerId === ALL ? undefined : partnerId, groupId: groupId === ALL ? undefined : groupId, locationId: locationId === ALL ? undefined : locationId, startDate: startDate.trim() || undefined, endDate: endDate.trim() || undefined, keyword: debouncedKeyword || undefined, }), [debouncedKeyword, endDate, groupId, locationId, partnerId, startDate] ); useEffect(() => { setPageIndex(1); }, [debouncedKeyword, partnerId, groupId, locationId, startDate, endDate, activeTab, pageSize]); useEffect(() => { let cancel = false; const run = async () => { setFilterMetaLoading(true); try { const [pRes, gRes] = await Promise.all([ getPartners({ skipCount: 1, maxResultCount: 500, state: true }), getGroups({ skipCount: 1, maxResultCount: 500, state: true }), ]); if (cancel) return; setPartners(pRes.items ?? []); setGroups(gRes.items ?? []); } catch (e) { if (!cancel) { setPartners([]); setGroups([]); toast.error("Failed to load companies / regions.", { description: e instanceof Error ? e.message : "Please try again.", }); } } finally { if (!cancel) setFilterMetaLoading(false); } }; run(); return () => { cancel = true; }; }, []); useEffect(() => { let cancel = false; const run = async () => { try { const res = await getLocations({ skipCount: 1, maxResultCount: 2000, partner: partnerNameForQuery(), groupName: groupNameForQuery(), state: true, }); if (cancel) return; setLocations(res.items ?? []); } catch (e) { if (!cancel) { setLocations([]); toast.error("Failed to load locations.", { description: e instanceof Error ? e.message : "Please try again.", }); } } }; run(); return () => { cancel = true; }; }, [groupNameForQuery, partnerNameForQuery, partnerId, groupId]); useEffect(() => { if (activeTab !== "print-log") return; const run = async () => { printAbortRef.current?.abort(); const ac = new AbortController(); printAbortRef.current = ac; setPrintLoading(true); const f = buildReportFilters(); try { const res = await getReportsPrintLogList( { skipCount: skipCountForPage(pageIndex), maxResultCount: pageSize, sorting: "PrintedAt desc", ...f, }, ac.signal ); setPrintRows(res.items ?? []); setPrintTotal(res.totalCount ?? 0); } catch (e) { if (e instanceof Error && e.name === "AbortError") return; toast.error("Failed to load print log.", { description: e instanceof Error ? e.message : "Please try again.", }); setPrintRows([]); setPrintTotal(0); } finally { if (!ac.signal.aborted) setPrintLoading(false); } }; void run(); return () => printAbortRef.current?.abort(); }, [activeTab, buildReportFilters, pageIndex, pageSize]); useEffect(() => { if (activeTab !== "label-report") return; const run = async () => { labelAbortRef.current?.abort(); const ac = new AbortController(); labelAbortRef.current = ac; setLabelLoading(true); const f = buildReportFilters(); try { const data = await getLabelReport(f, ac.signal); setLabelData(data); } catch (e) { if (e instanceof Error && e.name === "AbortError") return; toast.error("Failed to load label report.", { description: e instanceof Error ? e.message : "Please try again.", }); setLabelData(EMPTY_LABEL_REPORT); } finally { if (!ac.signal.aborted) setLabelLoading(false); } }; void run(); return () => labelAbortRef.current?.abort(); }, [activeTab, buildReportFilters]); const onPartnerChange = (v: string) => { setPartnerId(v); setGroupId(ALL); setLocationId(ALL); }; const onGroupChange = (v: string) => { setGroupId(v); setLocationId(ALL); }; const handleReprint = async (row: ReportsPrintLogListItem) => { const loc = (row.locationId ?? "").trim(); const task = (row.taskId ?? "").trim(); if (!loc || !task) { toast.error("Cannot reprint", { description: "Missing location or task id." }); return; } const key = task; setReprintBusyId(key); try { await reprintPrintLog({ locationId: loc, taskId: task, printQuantity: 1 }); toast.success("Reprint request sent", { description: `Task ${row.labelCode || task}` }); } catch (e) { const msg = e instanceof ApiError ? e.message : e instanceof Error ? e.message : "Please try again."; toast.error("Reprint failed", { description: msg }); } finally { setReprintBusyId(null); } }; const handleExport = async () => { const f = buildReportFilters(); setExporting(true); try { if (activeTab === "print-log") { await exportPrintLogPdf({ ...f, skipCount: 1, maxResultCount: 10, sorting: "PrintedAt desc", }); } else { await exportLabelReportPdf(f); } toast.success("Export ready", { description: "The PDF download should start shortly." }); } catch (e) { const msg = e instanceof ApiError ? e.message : e instanceof Error ? e.message : "Please try again."; toast.error("Export failed", { description: msg }); } finally { setExporting(false); } }; const report = labelData ?? EMPTY_LABEL_REPORT; const trendData = (report.printVolumeTrend ?? []).map((p) => ({ date: formatTrendLabel(p.date), count: p.count, })); const categoryData = (report.labelsByCategory ?? []).map((c) => ({ name: c.name, count: c.count })); return ( All companies {partners.map((p) => ( {toDisplay(p.partnerName)} ))} All regions {groups .filter((g) => (partnerId === ALL ? true : g.partnerId === partnerId)) .map((g) => ( {toDisplay(g.groupName)} ))} All locations {locations.map((loc) => ( {toDisplay(loc.locationName) || toDisplay(loc.locationCode)} ))} Period Search: {startDate || "YYYY-MM-DD"} d && setStartDate(formatIsoDate(d))} initialFocus /> - {endDate || "YYYY-MM-DD"} d && setEndDate(formatIsoDate(d))} initialFocus /> setKeyword(e.target.value)} placeholder="Search Product or Category..." className="flex-1 min-w-0 border-0 bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 py-2 px-2 h-full placeholder:text-gray-500" /> void handleExport()} > {exporting ? "Exporting…" : "Export Report"} setActiveTab("print-log")} style={ activeTab === "print-log" ? { borderBottomWidth: 2, borderBottomStyle: "solid", borderBottomColor: "#2563eb" } : undefined } className={cn( "px-4 py-2.5 text-sm font-medium whitespace-nowrap cursor-pointer transition-colors -mb-px border-b-2", activeTab === "print-log" ? "text-blue-600" : "border-b-transparent text-gray-600 hover:text-gray-800" )} > Print Log setActiveTab("label-report")} style={ activeTab === "label-report" ? { borderBottomWidth: 2, borderBottomStyle: "solid", borderBottomColor: "#2563eb" } : undefined } className={cn( "px-4 py-2.5 text-sm font-medium whitespace-nowrap cursor-pointer transition-colors -mb-px border-b-2", activeTab === "label-report" ? "text-blue-600" : "border-b-transparent text-gray-600 hover:text-gray-800" )} > Label Report {activeTab === "print-log" && ( {printLoading && ( Loading print log… )} {!printLoading && ( Label ID Product Name Category Template Printed At Printed By Location Expiry Date Action {printRows.length === 0 && ( No print records )} {printRows.map((log) => ( {toDisplay(log.labelCode)} {toDisplay(log.productName)} {toDisplay(log.categoryName)} {templateCell(log.templateText)} {formatPrintedAt(log.printedAt)} {toDisplay(log.printedByName)} {toDisplay(log.locationText)} {toDisplay(log.expiryDateText)} void handleReprint(log)} > {reprintBusyId === (log.taskId || "") ? "…" : "Reprint"} ))} )} {printTotal === 0 ? "Showing 0 of 0" : `Showing ${(pageIndex - 1) * pageSize + 1}–${Math.min(pageIndex * pageSize, printTotal)} of ${printTotal}`} pageIndex > 1 && setPageIndex((p) => Math.max(1, p - 1))} aria-disabled={pageIndex <= 1} /> e.preventDefault()}> Page {pageIndex} / {totalPages} = totalPages ? "pointer-events-none opacity-50" : "cursor-pointer"} onClick={() => pageIndex < totalPages && setPageIndex((p) => Math.min(totalPages, p + 1))} aria-disabled={pageIndex >= totalPages} /> )} {activeTab === "label-report" && ( {labelLoading && Loading label report…} {!labelLoading && ( <> Total Labels Printed {report.summary.totalLabelsPrinted.toLocaleString("en-US")} {formatPct(report.summary.totalLabelsPrintedChangeRate)} vs previous period Most Printed Category {toDisplay(report.summary.hottestCategoryName)} {report.summary.hottestCategoryCount.toLocaleString("en-US")} label(s) in range Top Product {toDisplay(report.summary.topProductName)} {report.summary.topProductCount.toLocaleString("en-US")} label(s) in range Avg. Daily Prints {Number.isFinite(report.summary.avgDailyPrints) ? report.summary.avgDailyPrints.toLocaleString("en-US", { maximumFractionDigits: 1 }) : "0"} {formatPct(report.summary.avgDailyPrintsChangeRate)} vs previous period Labels by Category Distribution of printed labels by label category in the selected range. {categoryData.length === 0 ? ( No data ) : ( `${v}`} /> )} Print Volume Trends Daily print volume in the current filter window (up to 7 days). {trendData.length === 0 ? ( No data ) : ( )} Most Used Products Product Name Category Total Printed Usage % {report.mostUsedProducts.length === 0 && ( No data )} {report.mostUsedProducts.map((row, i) => ( {toDisplay(row.productName)} {toDisplay(row.categoryName)} {row.totalPrinted.toLocaleString("en-US")} {Number.isFinite(row.usagePercent) ? `${row.usagePercent.toFixed(1)}%` : "—"} ))} > )} )} ); }
{formatPct(report.summary.totalLabelsPrintedChangeRate)} vs previous period
{report.summary.hottestCategoryCount.toLocaleString("en-US")} label(s) in range
{report.summary.topProductCount.toLocaleString("en-US")} label(s) in range
{formatPct(report.summary.avgDailyPrintsChangeRate)} vs previous period