EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
page.tsx
Go to the documentation of this file.
1"use client";
2
3import { useState, useEffect } from "react";
4import { apiClient } from "@/lib/client/apiClient";
5
6interface SaleRow {
7 f101: number; // Sale ID
8 f102: string; // Date
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
14 f202: number; // Qty
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
20 f219?: number; // GP%
21 f121?: number; // Original sale ID
22 f190?: string; // Cash item name
23 PROD?: any[]; // Product packet
24 CUST?: any[]; // Customer packet
25}
26
27interface Location {
28 Locid: string;
29 Name: string;
30}
31
32interface Department {
33 Depid: string;
34 Description: string;
35}
36
37interface PaymentType {
38 Code: number;
39 CustSpell: string;
40}
41
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");
47
48 // Filter states
49 const [fromDate, setFromDate] = useState(() => {
50 const d = new Date();
51 d.setDate(d.getDate() - 1);
52 return d.toISOString().split("T")[0];
53 });
54 const [toDate, setToDate] = useState(() => {
55 const d = new Date();
56 d.setDate(d.getDate() + 1);
57 return d.toISOString().split("T")[0];
58 });
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");
76
77 // Dropdown data
78 const [locations, setLocations] = useState<Location[]>([]);
79 const [departments, setDepartments] = useState<Department[]>([]);
80 const [paymentTypes, setPaymentTypes] = useState<PaymentType[]>([]);
81
82 // Load dropdowns on mount
83 useEffect(() => {
84 Promise.all([
85 apiClient.getLocations({ want: 'name,id' }),
86 apiClient.getDepartments(),
87 apiClient.getPaymentTypes(),
88 ])
89 .then(([locData, deptData, pmtData]) => {
90 if (locData.success && (locData.data as any)?.data?.Location) {
91 setLocations((locData.data as any).data.Location);
92 }
93 if (deptData.success && (deptData.data as any)?.data?.Department) {
94 setDepartments((deptData.data as any).data.Department);
95 }
96 if (pmtData.success && (pmtData.data as any)?.data?.PaymentType) {
97 setPaymentTypes((pmtData.data as any).data.PaymentType);
98 }
99 })
100 .catch((err) => console.error("Error loading dropdowns:", err));
101 }, []);
102
103 const formatDateForAPI = (dateStr: string) => {
104 const [year, month, day] = dateStr.split("-");
105 return `${day}-${month}-${year}`;
106 };
107
108 const getPriceCause = (code: number | undefined): string => {
109 if (!code || code === 0) return "";
110 const causes: Record<number, string> = {
111 1: "SpecOffer",
112 2: "Reward",
113 3: "Combo",
114 4: "QtyDisc",
115 5: "StoreDisc",
116 6: "DiscManual",
117 7: "PriceMap",
118 8: "CustDisc",
119 9: "TellerAction",
120 10: "Discount",
121 11: "DiscountVoucher",
122 12: "ManuallySet",
123 13: "TicketPrice",
124 14: "PriceMap",
125 15: "PriceOptions",
126 16: "Markdown",
127 17: "VariantPrice",
128 18: "ModifierPriced",
129 19: "ProgramSet",
130 20: "Scales",
131 256: "02Barcode",
132 257: "ModifierItem",
133 267: "PriceBand",
134 268: "PriceBand",
135 269: "PriceBand",
136 270: "PriceBand",
137 };
138 if (code >= 257 && code <= 511) return "";
139 return causes[code] || String(code);
140 };
141
142 const handleSort = (column: keyof SaleRow) => {
143 if (sortColumn === column) {
144 setSortDirection(sortDirection === "asc" ? "desc" : "asc");
145 } else {
146 setSortColumn(column);
147 setSortDirection("asc");
148 }
149 };
150
151 const sortedData = [...data].sort((a, b) => {
152 const aVal = a[sortColumn];
153 const bVal = b[sortColumn];
154
155 if (aVal === undefined || bVal === undefined) return 0;
156
157 if (typeof aVal === "number" && typeof bVal === "number") {
158 return sortDirection === "asc" ? aVal - bVal : bVal - aVal;
159 }
160
161 const aStr = String(aVal);
162 const bStr = String(bVal);
163 return sortDirection === "asc"
164 ? aStr.localeCompare(bStr)
165 : bStr.localeCompare(aStr);
166 });
167
168 const SortIndicator = ({ column }: { column: keyof SaleRow }) => {
169 if (sortColumn !== column) return null;
170 return <span className="ml-1">{sortDirection === "asc" ? "▲" : "▼"}</span>;
171 };
172
173 const loadReport = async () => {
174 setLoading(true);
175 setData([]);
176
177 const apiKey = sessionStorage.getItem("fieldpine_apikey") || "";
178 const fromDateAPI = formatDateForAPI(fromDate);
179 const toDateAPI = formatDateForAPI(toDate);
180
181 let exPreds = "";
182
183 // Store filter
184 if (selectedStore !== "0" && selectedStore) {
185 exPreds += `&9=f105,0,${selectedStore}`;
186 }
187
188 // Payment type filter
189 if (selectedPaymentType !== "0" && selectedPaymentType) {
190 exPreds += `&9=f300,0,${selectedPaymentType}`;
191 }
192
193 // Department filter
194 if (selectedDepartment !== "0" && selectedDepartment) {
195 if (selectedDepartment === "without") {
196 // Without department - need special handling
197 exPreds += "&9=f801,0,0";
198 } else {
199 exPreds += `&9=f801,0,${selectedDepartment}`;
200 }
201 }
202
203 // Sale phase filter
204 if (Number(selectedPhase) > 1) {
205 exPreds += `&9=f109,0,${selectedPhase}`;
206 }
207
208 // Tax filter
209 const taxVal = Number(selectedTax);
210 if (taxVal === 1) {
211 exPreds += "&9=f204,0,z";
212 } else if (taxVal === 2) {
213 exPreds += "&9=f204,0,0";
214 }
215
216 // Date filters
217 exPreds += `&9=f102,4,${fromDateAPI}`;
218 exPreds += `&9=f102,1,${toDateAPI}`;
219
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}`;
225 }
226
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}`;
232 }
233
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}`;
239 }
240
241 // Total price filter
242 const tpOp = Number(totalPrice);
243 if (tpOp === 1) {
244 exPreds += "&9=f210,1,0";
245 } else if (tpOp === 2) {
246 exPreds += "&9=f210,3,0";
247 }
248
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
252
253 try {
254 const response = await apiClient.getSalesList({
255 limit: Number(maxRows),
256 filters
257 });
258
259 if (response.success && response.data?.APPT && Array.isArray(response.data.APPT)) {
260 setData(response.data.APPT);
261 } else {
262 setData([]);
263 }
264 } catch (error) {
265 console.error("Error loading report:", error);
266 setData([]);
267 } finally {
268 setLoading(false);
269 }
270 };
271
272 const formatCurrency = (value: number | undefined) => {
273 if (value === undefined || value === null) return "";
274 return new Intl.NumberFormat("en-US", {
275 style: "currency",
276 currency: "USD",
277 minimumFractionDigits: 2,
278 }).format(value);
279 };
280
281 const formatDate = (dateStr: string | undefined) => {
282 if (!dateStr) return "";
283 try {
284 const date = new Date(dateStr);
285 return date.toLocaleDateString();
286 } catch {
287 return dateStr;
288 }
289 };
290
291 // Calculate totals
292 const totalQty = data.reduce((sum, row) => sum + (row.f202 || 0), 0);
293 const totalLinePrice = data.reduce((sum, row) => sum + (row.f203 || 0), 0);
294
295 return (
296 <div className="min-h-screen bg-surface-2 p-6">
297 <div className="max-w-[1800px] mx-auto">
298 {/* Header */}
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.
304 </p>
305 </div>
306
307 {/* Filters */}
308 <div className="bg-surface rounded-lg shadow p-6 mb-6">
309 {/* Row 1 */}
310 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
311 <div>
312 <label className="block text-sm font-medium text-text mb-1">
313 From (including)
314 </label>
315 <input
316 type="date"
317 value={fromDate}
318 onChange={(e) => setFromDate(e.target.value)}
319 className="w-full px-3 py-2 border border-border rounded-md"
320 />
321 </div>
322
323 <div>
324 <label className="block text-sm font-medium text-text mb-1">
325 To (excluding)
326 </label>
327 <input
328 type="date"
329 value={toDate}
330 onChange={(e) => setToDate(e.target.value)}
331 className="w-full px-3 py-2 border border-border rounded-md"
332 />
333 </div>
334
335 <div>
336 <label className="block text-sm font-medium text-text mb-1">Store</label>
337 <select
338 value={selectedStore}
339 onChange={(e) => setSelectedStore(e.target.value)}
340 className="w-full px-3 py-2 border border-border rounded-md"
341 >
342 <option value="0">All Stores</option>
343 {locations.map((loc) => (
344 <option key={loc.Locid} value={loc.Locid}>
345 {loc.Name}
346 </option>
347 ))}
348 </select>
349 </div>
350
351 <div>
352 <label className="block text-sm font-medium text-text mb-1">Tax</label>
353 <select
354 value={selectedTax}
355 onChange={(e) => setSelectedTax(e.target.value)}
356 className="w-full px-3 py-2 border border-border rounded-md"
357 >
358 <option value="-1">Any/All</option>
359 <option value="1">TaxFree Products</option>
360 <option value="2">TaxFree Sales</option>
361 </select>
362 </div>
363 </div>
364
365 {/* Row 2 */}
366 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
367 <div>
368 <label className="block text-sm font-medium text-text mb-1">
369 Supplier (optional)
370 </label>
371 <input
372 type="text"
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"
377 disabled
378 />
379 </div>
380
381 <div>
382 <label className="block text-sm font-medium text-text mb-1">
383 Payment Type
384 </label>
385 <select
386 value={selectedPaymentType}
387 onChange={(e) => setSelectedPaymentType(e.target.value)}
388 className="w-full px-3 py-2 border border-border rounded-md"
389 >
390 <option value="0">--Select--</option>
391 {paymentTypes.map((pmt) => (
392 <option key={pmt.Code} value={pmt.Code}>
393 {pmt.CustSpell}
394 </option>
395 ))}
396 </select>
397 </div>
398
399 <div>
400 <label className="block text-sm font-medium text-text mb-1">
401 Department
402 </label>
403 <select
404 value={selectedDepartment}
405 onChange={(e) => setSelectedDepartment(e.target.value)}
406 className="w-full px-3 py-2 border border-border rounded-md"
407 >
408 <option value="0" className="bg-surface-2">
409 Any/All
410 </option>
411 <option value="without" className="bg-surface-2">
412 Without Department
413 </option>
414 {departments.map((dept) => (
415 <option key={dept.Depid} value={dept.Depid}>
416 {dept.Description}
417 </option>
418 ))}
419 </select>
420 </div>
421
422 <div>
423 <label className="block text-sm font-medium text-text mb-1">State</label>
424 <select
425 value={selectedPhase}
426 onChange={(e) => setSelectedPhase(e.target.value)}
427 className="w-full px-3 py-2 border border-border rounded-md"
428 >
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>
433 </select>
434 </div>
435 </div>
436
437 {/* Row 3 - Quantity filters */}
438 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
439 <div>
440 <label className="block text-sm font-medium text-text mb-1">
441 Line Quantity
442 </label>
443 <div className="flex gap-2">
444 <select
445 value={lineQtyOp}
446 onChange={(e) => setLineQtyOp(e.target.value)}
447 className="w-24 px-2 py-2 border border-border rounded-md"
448 >
449 <option value="0">=</option>
450 <option value="3">&gt;</option>
451 <option value="1">&lt;</option>
452 <option value="4">&gt;=</option>
453 <option value="2">&lt;=</option>
454 <option value="5">&lt;&gt;</option>
455 </select>
456 <input
457 type="number"
458 value={lineQty}
459 onChange={(e) => setLineQty(e.target.value)}
460 className="flex-1 px-3 py-2 border border-border rounded-md"
461 />
462 </div>
463 </div>
464
465 <div>
466 <label className="block text-sm font-medium text-text mb-1">
467 Total Basket Qty
468 </label>
469 <div className="flex gap-2">
470 <select
471 value={basketQtyOp}
472 onChange={(e) => setBasketQtyOp(e.target.value)}
473 className="w-24 px-2 py-2 border border-border rounded-md"
474 >
475 <option value="0">=</option>
476 <option value="3">&gt;</option>
477 <option value="1">&lt;</option>
478 <option value="4">&gt;=</option>
479 <option value="2">&lt;=</option>
480 <option value="5">&lt;&gt;</option>
481 </select>
482 <input
483 type="number"
484 value={basketQty}
485 onChange={(e) => setBasketQty(e.target.value)}
486 className="flex-1 px-3 py-2 border border-border rounded-md"
487 />
488 </div>
489 </div>
490
491 <div>
492 <label className="block text-sm font-medium text-text mb-1">
493 Number of Salelines
494 </label>
495 <div className="flex gap-2">
496 <select
497 value={saleLinesOp}
498 onChange={(e) => setSaleLinesOp(e.target.value)}
499 className="w-24 px-2 py-2 border border-border rounded-md"
500 >
501 <option value="0">=</option>
502 <option value="3">&gt;</option>
503 <option value="1">&lt;</option>
504 <option value="4">&gt;=</option>
505 <option value="2">&lt;=</option>
506 <option value="5">&lt;&gt;</option>
507 </select>
508 <input
509 type="number"
510 value={saleLines}
511 onChange={(e) => setSaleLines(e.target.value)}
512 className="flex-1 px-3 py-2 border border-border rounded-md"
513 />
514 </div>
515 </div>
516
517 <div>
518 <label className="block text-sm font-medium text-text mb-1">
519 Total Price
520 </label>
521 <select
522 value={totalPrice}
523 onChange={(e) => setTotalPrice(e.target.value)}
524 className="w-full px-3 py-2 border border-border rounded-md"
525 >
526 <option value="0">Any</option>
527 <option value="1">Below expected</option>
528 <option value="2">Above expected</option>
529 </select>
530 </div>
531 </div>
532
533 {/* Row 4 - Search fields */}
534 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
535 <div>
536 <label className="block text-sm font-medium text-text mb-1">
537 Teller (optional)
538 </label>
539 <input
540 type="text"
541 value={tellerFilter}
542 onChange={(e) => setTellerFilter(e.target.value)}
543 placeholder="Autocomplete disabled"
544 className="w-full px-3 py-2 border border-border rounded-md"
545 disabled
546 />
547 </div>
548
549 <div>
550 <label className="block text-sm font-medium text-text mb-1">
551 Customer (optional)
552 </label>
553 <input
554 type="text"
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"
559 disabled
560 />
561 </div>
562
563 <div>
564 <label className="block text-sm font-medium text-text mb-1">
565 Product (optional)
566 </label>
567 <input
568 type="text"
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"
573 disabled
574 />
575 </div>
576
577 <div>
578 <label className="block text-sm font-medium text-text mb-1">
579 Max Rows
580 </label>
581 <div className="flex gap-2">
582 <input
583 type="number"
584 value={maxRows}
585 onChange={(e) => setMaxRows(e.target.value)}
586 className="flex-1 px-3 py-2 border border-border rounded-md"
587 />
588 <button
589 onClick={loadReport}
590 disabled={loading}
591 className="px-6 py-2 bg-brand text-white rounded-md hover:bg-brand/90 disabled:bg-muted/50 whitespace-nowrap"
592 >
593 {loading ? "Loading..." : "Display"}
594 </button>
595 </div>
596 </div>
597 </div>
598 </div>
599
600 {/* Loading State */}
601 {loading && (
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>
606 </div>
607 </div>
608 )}
609
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">
616 <tr>
617 <th
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"
620 >
621 Date
622 <SortIndicator column="f102" />
623 </th>
624 <th
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"
627 >
628 Store
629 <SortIndicator column="f106" />
630 </th>
631 <th
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"
634 >
635 Sale#
636 <SortIndicator column="f101" />
637 </th>
638 <th
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"
641 >
642 Total Sale
643 <SortIndicator column="f107" />
644 </th>
645 <th
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"
648 >
649 Teller
650 <SortIndicator column="f104" />
651 </th>
652 <th
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"
655 >
656 Product
657 <SortIndicator column="f201" />
658 </th>
659 <th
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"
662 >
663 Qty
664 <SortIndicator column="f202" />
665 </th>
666 <th
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"
669 >
670 Line Price
671 <SortIndicator column="f203" />
672 </th>
673 <th
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"
676 >
677 C
678 <SortIndicator column="f206" />
679 </th>
680 <th
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"
683 >
684 Paid By
685 <SortIndicator column="f301" />
686 </th>
687 </tr>
688 </thead>
689 <tbody className="bg-surface divide-y divide-border">
690 {sortedData.map((row, idx) => (
691 <tr
692 key={`${row.f101}-${row.f200}-${idx}`}
693 className={idx % 2 === 0 ? "bg-surface" : "bg-surface-2"}
694 >
695 <td className="px-4 py-3 text-sm text-text whitespace-nowrap">
696 {formatDate(row.f102)}
697 </td>
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">
700 <a
701 href={`/report/pos/sales/fieldpine/SingleSale.htm?sid=${row.f101}`}
702 className="hover:underline"
703 target="_blank"
704 rel="noopener noreferrer"
705 >
706 {row.f101}
707 </a>
708 </td>
709 <td className="px-4 py-3 text-sm text-text text-right">
710 {formatCurrency(row.f107)}
711 </td>
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">
714 <a
715 href={`/report/pos/stock/fieldpine/singlepid.htm?pid=${row.f200}`}
716 className="hover:underline"
717 target="_blank"
718 rel="noopener noreferrer"
719 >
720 {row.f201}
721 </a>
722 </td>
723 <td className="px-4 py-3 text-sm text-text text-right">
724 {row.f202}
725 </td>
726 <td className="px-4 py-3 text-sm text-text text-right">
727 {formatCurrency(row.f203)}
728 </td>
729 <td className="px-4 py-3 text-sm text-muted">
730 {getPriceCause(row.f206)}
731 </td>
732 <td className="px-4 py-3 text-sm text-text">{row.f301 || ""}</td>
733 </tr>
734 ))}
735 </tbody>
736 {/* Totals Footer */}
737 <tfoot className="bg-text text-white">
738 <tr>
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)}
748 </td>
749 <td className="px-4 py-3"></td>
750 <td className="px-4 py-3"></td>
751 </tr>
752 </tfoot>
753 </table>
754 </div>
755
756 {/* Footer */}
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" : ""}
760 </p>
761 </div>
762 </div>
763 )}
764
765 {/* No Results */}
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.
769 </div>
770 )}
771 </div>
772 </div>
773 );
774}