3import { useState, useEffect } from "react";
4import { apiClient } from "@/lib/client/apiClient";
7 f101: number; // Sale ID
9 f106: string; // Store name
10 f107: number; // Total sale
11 f104: string; // Teller name
12 f200: number; // Product ID
13 f201: string; // Product name
15 f203: number; // Line price
16 f206?: number; // Price cause code
17 f301?: string; // Paid by
18 f208?: number; // Line cost
19 f209?: number; // Gross profit
21 f121?: number; // Original sale ID
22 f190?: string; // Cash item name
23 PROD?: any[]; // Product packet
24 CUST?: any[]; // Customer packet
37interface PaymentType {
42export default function SaleItemsReviewPage() {
43 const [data, setData] = useState<SaleRow[]>([]);
44 const [loading, setLoading] = useState(false);
45 const [sortColumn, setSortColumn] = useState<keyof SaleRow>("f102");
46 const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
49 const [fromDate, setFromDate] = useState(() => {
51 d.setDate(d.getDate() - 1);
52 return d.toISOString().split("T")[0];
54 const [toDate, setToDate] = useState(() => {
56 d.setDate(d.getDate() + 1);
57 return d.toISOString().split("T")[0];
59 const [selectedStore, setSelectedStore] = useState("0");
60 const [selectedTax, setSelectedTax] = useState("-1");
61 const [selectedSupplier, setSelectedSupplier] = useState("");
62 const [selectedPaymentType, setSelectedPaymentType] = useState("0");
63 const [selectedDepartment, setSelectedDepartment] = useState("0");
64 const [selectedPhase, setSelectedPhase] = useState("1");
65 const [lineQtyOp, setLineQtyOp] = useState("0");
66 const [lineQty, setLineQty] = useState("");
67 const [basketQtyOp, setBasketQtyOp] = useState("0");
68 const [basketQty, setBasketQty] = useState("");
69 const [saleLinesOp, setSaleLinesOp] = useState("0");
70 const [saleLines, setSaleLines] = useState("");
71 const [totalPrice, setTotalPrice] = useState("0");
72 const [tellerFilter, setTellerFilter] = useState("");
73 const [customerFilter, setCustomerFilter] = useState("");
74 const [productFilter, setProductFilter] = useState("");
75 const [maxRows, setMaxRows] = useState("200");
78 const [locations, setLocations] = useState<Location[]>([]);
79 const [departments, setDepartments] = useState<Department[]>([]);
80 const [paymentTypes, setPaymentTypes] = useState<PaymentType[]>([]);
82 // Load dropdowns on mount
85 apiClient.getLocations({ want: 'name,id' }),
86 apiClient.getDepartments(),
87 apiClient.getPaymentTypes(),
89 .then(([locData, deptData, pmtData]) => {
90 if (locData.success && (locData.data as any)?.data?.Location) {
91 setLocations((locData.data as any).data.Location);
93 if (deptData.success && (deptData.data as any)?.data?.Department) {
94 setDepartments((deptData.data as any).data.Department);
96 if (pmtData.success && (pmtData.data as any)?.data?.PaymentType) {
97 setPaymentTypes((pmtData.data as any).data.PaymentType);
100 .catch((err) => console.error("Error loading dropdowns:", err));
103 const formatDateForAPI = (dateStr: string) => {
104 const [year, month, day] = dateStr.split("-");
105 return `${day}-${month}-${year}`;
108 const getPriceCause = (code: number | undefined): string => {
109 if (!code || code === 0) return "";
110 const causes: Record<number, string> = {
121 11: "DiscountVoucher",
128 18: "ModifierPriced",
138 if (code >= 257 && code <= 511) return "";
139 return causes[code] || String(code);
142 const handleSort = (column: keyof SaleRow) => {
143 if (sortColumn === column) {
144 setSortDirection(sortDirection === "asc" ? "desc" : "asc");
146 setSortColumn(column);
147 setSortDirection("asc");
151 const sortedData = [...data].sort((a, b) => {
152 const aVal = a[sortColumn];
153 const bVal = b[sortColumn];
155 if (aVal === undefined || bVal === undefined) return 0;
157 if (typeof aVal === "number" && typeof bVal === "number") {
158 return sortDirection === "asc" ? aVal - bVal : bVal - aVal;
161 const aStr = String(aVal);
162 const bStr = String(bVal);
163 return sortDirection === "asc"
164 ? aStr.localeCompare(bStr)
165 : bStr.localeCompare(aStr);
168 const SortIndicator = ({ column }: { column: keyof SaleRow }) => {
169 if (sortColumn !== column) return null;
170 return <span className="ml-1">{sortDirection === "asc" ? "▲" : "▼"}</span>;
173 const loadReport = async () => {
177 const apiKey = sessionStorage.getItem("fieldpine_apikey") || "";
178 const fromDateAPI = formatDateForAPI(fromDate);
179 const toDateAPI = formatDateForAPI(toDate);
184 if (selectedStore !== "0" && selectedStore) {
185 exPreds += `&9=f105,0,${selectedStore}`;
188 // Payment type filter
189 if (selectedPaymentType !== "0" && selectedPaymentType) {
190 exPreds += `&9=f300,0,${selectedPaymentType}`;
194 if (selectedDepartment !== "0" && selectedDepartment) {
195 if (selectedDepartment === "without") {
196 // Without department - need special handling
197 exPreds += "&9=f801,0,0";
199 exPreds += `&9=f801,0,${selectedDepartment}`;
204 if (Number(selectedPhase) > 1) {
205 exPreds += `&9=f109,0,${selectedPhase}`;
209 const taxVal = Number(selectedTax);
211 exPreds += "&9=f204,0,z";
212 } else if (taxVal === 2) {
213 exPreds += "&9=f204,0,0";
217 exPreds += `&9=f102,4,${fromDateAPI}`;
218 exPreds += `&9=f102,1,${toDateAPI}`;
220 // Line quantity filter
221 const lqty = Number(lineQty);
222 const lqtyOp = Number(lineQtyOp);
223 if (lqty !== 0 || lqtyOp !== 0) {
224 exPreds += `&9=f202,${lqtyOp},${lqty}`;
227 // Basket quantity filter
228 const bqty = Number(basketQty);
229 const bqtyOp = Number(basketQtyOp);
230 if (bqty !== 0 || bqtyOp !== 0) {
231 exPreds += `&9=f111,${bqtyOp},${bqty}`;
234 // Sale lines count filter
235 const slc = Number(saleLines);
236 const slcOp = Number(saleLinesOp);
237 if (slc !== 0 || slcOp !== 0) {
238 exPreds += `&9=f113,${slcOp},${slc}`;
241 // Total price filter
242 const tpOp = Number(totalPrice);
244 exPreds += "&9=f210,1,0";
245 } else if (tpOp === 2) {
246 exPreds += "&9=f210,3,0";
249 const filters = exPreds.split('&9=').filter(f => f).map(f => f.replace(/^9=/, ''));
250 filters.push("103=1"); // Full product packet
251 filters.push("104=1"); // Customer packet
254 const response = await apiClient.getSalesList({
255 limit: Number(maxRows),
259 if (response.success && response.data?.APPT && Array.isArray(response.data.APPT)) {
260 setData(response.data.APPT);
265 console.error("Error loading report:", error);
272 const formatCurrency = (value: number | undefined) => {
273 if (value === undefined || value === null) return "";
274 return new Intl.NumberFormat("en-US", {
277 minimumFractionDigits: 2,
281 const formatDate = (dateStr: string | undefined) => {
282 if (!dateStr) return "";
284 const date = new Date(dateStr);
285 return date.toLocaleDateString();
292 const totalQty = data.reduce((sum, row) => sum + (row.f202 || 0), 0);
293 const totalLinePrice = data.reduce((sum, row) => sum + (row.f203 || 0), 0);
296 <div className="min-h-screen bg-surface-2 p-6">
297 <div className="max-w-[1800px] mx-auto">
299 <div className="mb-6">
300 <h1 className="text-3xl font-bold text-text mb-2">Sale Items Review</h1>
301 <p className="text-muted">
302 Shows details of individual sale items sold. Filter by date, store, department,
303 payment type, and more.
308 <div className="bg-surface rounded-lg shadow p-6 mb-6">
310 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
312 <label className="block text-sm font-medium text-text mb-1">
318 onChange={(e) => setFromDate(e.target.value)}
319 className="w-full px-3 py-2 border border-border rounded-md"
324 <label className="block text-sm font-medium text-text mb-1">
330 onChange={(e) => setToDate(e.target.value)}
331 className="w-full px-3 py-2 border border-border rounded-md"
336 <label className="block text-sm font-medium text-text mb-1">Store</label>
338 value={selectedStore}
339 onChange={(e) => setSelectedStore(e.target.value)}
340 className="w-full px-3 py-2 border border-border rounded-md"
342 <option value="0">All Stores</option>
343 {locations.map((loc) => (
344 <option key={loc.Locid} value={loc.Locid}>
352 <label className="block text-sm font-medium text-text mb-1">Tax</label>
355 onChange={(e) => setSelectedTax(e.target.value)}
356 className="w-full px-3 py-2 border border-border rounded-md"
358 <option value="-1">Any/All</option>
359 <option value="1">TaxFree Products</option>
360 <option value="2">TaxFree Sales</option>
366 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
368 <label className="block text-sm font-medium text-text mb-1">
373 value={selectedSupplier}
374 onChange={(e) => setSelectedSupplier(e.target.value)}
375 placeholder="Autocomplete disabled"
376 className="w-full px-3 py-2 border border-border rounded-md"
382 <label className="block text-sm font-medium text-text mb-1">
386 value={selectedPaymentType}
387 onChange={(e) => setSelectedPaymentType(e.target.value)}
388 className="w-full px-3 py-2 border border-border rounded-md"
390 <option value="0">--Select--</option>
391 {paymentTypes.map((pmt) => (
392 <option key={pmt.Code} value={pmt.Code}>
400 <label className="block text-sm font-medium text-text mb-1">
404 value={selectedDepartment}
405 onChange={(e) => setSelectedDepartment(e.target.value)}
406 className="w-full px-3 py-2 border border-border rounded-md"
408 <option value="0" className="bg-surface-2">
411 <option value="without" className="bg-surface-2">
414 {departments.map((dept) => (
415 <option key={dept.Depid} value={dept.Depid}>
423 <label className="block text-sm font-medium text-text mb-1">State</label>
425 value={selectedPhase}
426 onChange={(e) => setSelectedPhase(e.target.value)}
427 className="w-full px-3 py-2 border border-border rounded-md"
429 <option value="1">Normal</option>
430 <option value="3">Current Layby</option>
431 <option value="10000">Void (a)</option>
432 <option value="10002">Void (b)</option>
437 {/* Row 3 - Quantity filters */}
438 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
440 <label className="block text-sm font-medium text-text mb-1">
443 <div className="flex gap-2">
446 onChange={(e) => setLineQtyOp(e.target.value)}
447 className="w-24 px-2 py-2 border border-border rounded-md"
449 <option value="0">=</option>
450 <option value="3">></option>
451 <option value="1"><</option>
452 <option value="4">>=</option>
453 <option value="2"><=</option>
454 <option value="5"><></option>
459 onChange={(e) => setLineQty(e.target.value)}
460 className="flex-1 px-3 py-2 border border-border rounded-md"
466 <label className="block text-sm font-medium text-text mb-1">
469 <div className="flex gap-2">
472 onChange={(e) => setBasketQtyOp(e.target.value)}
473 className="w-24 px-2 py-2 border border-border rounded-md"
475 <option value="0">=</option>
476 <option value="3">></option>
477 <option value="1"><</option>
478 <option value="4">>=</option>
479 <option value="2"><=</option>
480 <option value="5"><></option>
485 onChange={(e) => setBasketQty(e.target.value)}
486 className="flex-1 px-3 py-2 border border-border rounded-md"
492 <label className="block text-sm font-medium text-text mb-1">
495 <div className="flex gap-2">
498 onChange={(e) => setSaleLinesOp(e.target.value)}
499 className="w-24 px-2 py-2 border border-border rounded-md"
501 <option value="0">=</option>
502 <option value="3">></option>
503 <option value="1"><</option>
504 <option value="4">>=</option>
505 <option value="2"><=</option>
506 <option value="5"><></option>
511 onChange={(e) => setSaleLines(e.target.value)}
512 className="flex-1 px-3 py-2 border border-border rounded-md"
518 <label className="block text-sm font-medium text-text mb-1">
523 onChange={(e) => setTotalPrice(e.target.value)}
524 className="w-full px-3 py-2 border border-border rounded-md"
526 <option value="0">Any</option>
527 <option value="1">Below expected</option>
528 <option value="2">Above expected</option>
533 {/* Row 4 - Search fields */}
534 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
536 <label className="block text-sm font-medium text-text mb-1">
542 onChange={(e) => setTellerFilter(e.target.value)}
543 placeholder="Autocomplete disabled"
544 className="w-full px-3 py-2 border border-border rounded-md"
550 <label className="block text-sm font-medium text-text mb-1">
555 value={customerFilter}
556 onChange={(e) => setCustomerFilter(e.target.value)}
557 placeholder="Autocomplete disabled"
558 className="w-full px-3 py-2 border border-border rounded-md"
564 <label className="block text-sm font-medium text-text mb-1">
569 value={productFilter}
570 onChange={(e) => setProductFilter(e.target.value)}
571 placeholder="Autocomplete disabled"
572 className="w-full px-3 py-2 border border-border rounded-md"
578 <label className="block text-sm font-medium text-text mb-1">
581 <div className="flex gap-2">
585 onChange={(e) => setMaxRows(e.target.value)}
586 className="flex-1 px-3 py-2 border border-border rounded-md"
591 className="px-6 py-2 bg-brand text-white rounded-md hover:bg-brand/90 disabled:bg-muted/50 whitespace-nowrap"
593 {loading ? "Loading..." : "Display"}
600 {/* Loading State */}
602 <div className="bg-surface rounded-lg shadow p-8 text-center">
603 <div className="inline-flex items-center gap-2">
604 <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-info"></div>
605 <span className="text-text">Loading...</span>
610 {/* Results Table */}
611 {!loading && data.length > 0 && (
612 <div className="bg-surface rounded-lg shadow overflow-hidden">
613 <div className="overflow-x-auto">
614 <table className="min-w-full divide-y divide-border">
615 <thead className="bg-surface-2">
618 onClick={() => handleSort("f102")}
619 className="px-4 py-3 text-left text-xs font-medium text-text uppercase tracking-wider cursor-pointer hover:bg-surface-2"
622 <SortIndicator column="f102" />
625 onClick={() => handleSort("f106")}
626 className="px-4 py-3 text-left text-xs font-medium text-text uppercase tracking-wider cursor-pointer hover:bg-surface-2"
629 <SortIndicator column="f106" />
632 onClick={() => handleSort("f101")}
633 className="px-4 py-3 text-left text-xs font-medium text-text uppercase tracking-wider cursor-pointer hover:bg-surface-2"
636 <SortIndicator column="f101" />
639 onClick={() => handleSort("f107")}
640 className="px-4 py-3 text-right text-xs font-medium text-text uppercase tracking-wider cursor-pointer hover:bg-surface-2"
643 <SortIndicator column="f107" />
646 onClick={() => handleSort("f104")}
647 className="px-4 py-3 text-left text-xs font-medium text-text uppercase tracking-wider cursor-pointer hover:bg-surface-2"
650 <SortIndicator column="f104" />
653 onClick={() => handleSort("f201")}
654 className="px-4 py-3 text-left text-xs font-medium text-text uppercase tracking-wider cursor-pointer hover:bg-surface-2"
657 <SortIndicator column="f201" />
660 onClick={() => handleSort("f202")}
661 className="px-4 py-3 text-right text-xs font-medium text-text uppercase tracking-wider cursor-pointer hover:bg-surface-2"
664 <SortIndicator column="f202" />
667 onClick={() => handleSort("f203")}
668 className="px-4 py-3 text-right text-xs font-medium text-text uppercase tracking-wider cursor-pointer hover:bg-surface-2"
671 <SortIndicator column="f203" />
674 onClick={() => handleSort("f206")}
675 className="px-4 py-3 text-left text-xs font-medium text-text uppercase tracking-wider cursor-pointer hover:bg-surface-2"
678 <SortIndicator column="f206" />
681 onClick={() => handleSort("f301")}
682 className="px-4 py-3 text-left text-xs font-medium text-text uppercase tracking-wider cursor-pointer hover:bg-surface-2"
685 <SortIndicator column="f301" />
689 <tbody className="bg-surface divide-y divide-border">
690 {sortedData.map((row, idx) => (
692 key={`${row.f101}-${row.f200}-${idx}`}
693 className={idx % 2 === 0 ? "bg-surface" : "bg-surface-2"}
695 <td className="px-4 py-3 text-sm text-text whitespace-nowrap">
696 {formatDate(row.f102)}
698 <td className="px-4 py-3 text-sm text-text">{row.f106}</td>
699 <td className="px-4 py-3 text-sm text-brand">
701 href={`/report/pos/sales/fieldpine/SingleSale.htm?sid=${row.f101}`}
702 className="hover:underline"
704 rel="noopener noreferrer"
709 <td className="px-4 py-3 text-sm text-text text-right">
710 {formatCurrency(row.f107)}
712 <td className="px-4 py-3 text-sm text-text">{row.f104}</td>
713 <td className="px-4 py-3 text-sm text-brand">
715 href={`/report/pos/stock/fieldpine/singlepid.htm?pid=${row.f200}`}
716 className="hover:underline"
718 rel="noopener noreferrer"
723 <td className="px-4 py-3 text-sm text-text text-right">
726 <td className="px-4 py-3 text-sm text-text text-right">
727 {formatCurrency(row.f203)}
729 <td className="px-4 py-3 text-sm text-muted">
730 {getPriceCause(row.f206)}
732 <td className="px-4 py-3 text-sm text-text">{row.f301 || ""}</td>
736 {/* Totals Footer */}
737 <tfoot className="bg-text text-white">
739 <td className="px-4 py-3 text-sm font-bold">Totals</td>
740 <td className="px-4 py-3"></td>
741 <td className="px-4 py-3"></td>
742 <td className="px-4 py-3"></td>
743 <td className="px-4 py-3"></td>
744 <td className="px-4 py-3"></td>
745 <td className="px-4 py-3 text-sm font-bold text-right">{totalQty}</td>
746 <td className="px-4 py-3 text-sm font-bold text-right">
747 {formatCurrency(totalLinePrice)}
749 <td className="px-4 py-3"></td>
750 <td className="px-4 py-3"></td>
757 <div className="px-6 py-4 bg-surface-2 border-t border-border">
758 <p className="text-sm text-muted">
759 Showing {data.length} sale line{data.length !== 1 ? "s" : ""}
766 {!loading && data.length === 0 && (
767 <div className="bg-surface rounded-lg shadow p-8 text-center text-muted">
768 No sale items found for the selected criteria. Click Display to load results.