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 f208?: number; // Line cost
17 f209?: number; // Gross profit
18 f30012?: string; // Discount name
19 f30006?: number; // Discount amount
20 PROD?: any[]; // Product packet
21 CUST?: any[]; // Customer packet
36export default function SalesWithDiscountsPage() {
37 const [data, setData] = useState<SaleRow[]>([]);
38 const [loading, setLoading] = useState(false);
39 const [sortColumn, setSortColumn] = useState<keyof SaleRow>("f102");
40 const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
43 const [fromDate, setFromDate] = useState(() => {
45 d.setDate(d.getDate() - 1); // Yesterday
46 return d.toISOString().split("T")[0];
48 const [toDate, setToDate] = useState(() => {
50 d.setDate(d.getDate() + 1); // Tomorrow
51 return d.toISOString().split("T")[0];
53 const [selectedStore, setSelectedStore] = useState("0");
54 const [selectedDiscount, setSelectedDiscount] = useState("0");
55 const [tellerFilter, setTellerFilter] = useState("");
56 const [customerFilter, setCustomerFilter] = useState("");
57 const [productFilter, setProductFilter] = useState("");
58 const [maxRows, setMaxRows] = useState("200");
61 const [locations, setLocations] = useState<Location[]>([]);
62 const [discounts, setDiscounts] = useState<Discount[]>([]);
64 // Load locations and discounts on mount
67 apiClient.getLocations({ source: 'elink' }),
68 apiClient.getDiscounts(),
70 .then(([locData, discData]) => {
71 if (locData.success && locData.data) {
72 const locs = locData.data.map((d: any) => ({
73 Locid: d.Locid || d.f100 || "",
74 Name: d.Name || d.f101 || d.Locid || d.f100 || "",
79 if (discData.success && discData.data) {
80 const discs = discData.data.map((d: any) => ({
81 DiscId: d.DiscId || d.f100,
83 Name: d.Name || d.f101,
89 .catch((err) => console.error("Error loading dropdowns:", err));
92 const formatDateForAPI = (dateStr: string) => {
93 const [year, month, day] = dateStr.split("-");
94 return `${day}-${month}-${year}`;
97 const handleSort = (column: keyof SaleRow) => {
98 if (sortColumn === column) {
99 setSortDirection(sortDirection === "asc" ? "desc" : "asc");
101 setSortColumn(column);
102 setSortDirection("asc");
106 const sortedData = [...data].sort((a, b) => {
107 const aVal = a[sortColumn];
108 const bVal = b[sortColumn];
110 if (aVal === undefined || bVal === undefined) return 0;
112 if (typeof aVal === "number" && typeof bVal === "number") {
113 return sortDirection === "asc" ? aVal - bVal : bVal - aVal;
116 const aStr = String(aVal);
117 const bStr = String(bVal);
118 return sortDirection === "asc"
119 ? aStr.localeCompare(bStr)
120 : bStr.localeCompare(aStr);
123 const SortIndicator = ({ column }: { column: keyof SaleRow }) => {
124 if (sortColumn !== column) return null;
125 return <span className="ml-1">{sortDirection === "asc" ? "▲" : "▼"}</span>;
128 const loadReport = async () => {
132 const fromDateAPI = formatDateForAPI(fromDate);
133 const toDateAPI = formatDateForAPI(toDate);
135 const filters: string[] = [];
138 if (selectedStore !== "0" && selectedStore) {
139 filters.push(`f105,0,${selectedStore}`);
143 if (selectedDiscount !== "0" && selectedDiscount) {
144 filters.push(`f216,0,${selectedDiscount}`);
146 filters.push("f216,gt,0"); // Any discount
150 filters.push(`f102,4,${fromDateAPI}`);
151 filters.push(`f102,1,${toDateAPI}`);
154 const response = await apiClient.getSalesList({
155 limit: Number(maxRows),
156 fields: "206,30005,30006,30012,!300-301",
157 filters: [...filters, "103=1", "104=1"] // Include packets
160 if (response.success && response.data?.APPT && Array.isArray(response.data.APPT)) {
161 setData(response.data.APPT);
166 console.error("Error loading report:", error);
173 const formatCurrency = (value: number | undefined) => {
174 if (value === undefined || value === null) return "";
175 return new Intl.NumberFormat("en-US", {
178 minimumFractionDigits: 2,
182 const formatDate = (dateStr: string | undefined) => {
183 if (!dateStr) return "";
185 const date = new Date(dateStr);
186 return date.toLocaleDateString();
193 <div className="min-h-screen bg-bg p-6">
194 <div className="max-w-[1800px] mx-auto">
196 <div className="mb-6">
197 <h1 className="text-3xl font-bold text-text mb-2">
198 Sales With Discounts Review
200 <p className="text-muted">
201 Review sales transactions that include discounts. Filter by date range, store,
202 discount type, or specific products.
207 <div className="bg-surface rounded-lg shadow p-6 mb-6">
208 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
210 <label className="block text-sm font-medium text-text mb-1">
216 onChange={(e) => setFromDate(e.target.value)}
217 className="w-full px-3 py-2 border border-border rounded-md"
222 <label className="block text-sm font-medium text-text mb-1">
228 onChange={(e) => setToDate(e.target.value)}
229 className="w-full px-3 py-2 border border-border rounded-md"
234 <label className="block text-sm font-medium text-text mb-1">Store</label>
236 value={selectedStore}
237 onChange={(e) => setSelectedStore(e.target.value)}
238 className="w-full px-3 py-2 border border-border rounded-md"
240 <option value="0">All Stores</option>
241 {locations.map((loc) => (
242 <option key={loc.Locid} value={loc.Locid}>
250 <label className="block text-sm font-medium text-text mb-1">
254 value={selectedDiscount}
255 onChange={(e) => setSelectedDiscount(e.target.value)}
256 className="w-full px-3 py-2 border border-border rounded-md"
258 <option value="0">Any</option>
259 {discounts.map((disc, idx) => (
262 value={disc.DiscId || disc.f100 || idx}
264 {disc.Name || disc.f101 || disc.DiscId || disc.f100}
271 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
273 <label className="block text-sm font-medium text-text mb-1">
279 onChange={(e) => setTellerFilter(e.target.value)}
280 placeholder="Autocomplete disabled"
281 className="w-full px-3 py-2 border border-border rounded-md"
287 <label className="block text-sm font-medium text-text mb-1">
292 value={customerFilter}
293 onChange={(e) => setCustomerFilter(e.target.value)}
294 placeholder="Autocomplete disabled"
295 className="w-full px-3 py-2 border border-border rounded-md"
301 <label className="block text-sm font-medium text-text mb-1">
306 value={productFilter}
307 onChange={(e) => setProductFilter(e.target.value)}
308 placeholder="Autocomplete disabled"
309 className="w-full px-3 py-2 border border-border rounded-md"
315 <label className="block text-sm font-medium text-text mb-1">
318 <div className="flex gap-2">
322 onChange={(e) => setMaxRows(e.target.value)}
323 className="flex-1 px-3 py-2 border border-border rounded-md"
328 className="px-6 py-2 bg-brand text-white rounded-md hover:bg-brand2 disabled:bg-muted/50 whitespace-nowrap"
330 {loading ? "Loading..." : "Display"}
337 {/* Loading State */}
339 <div className="bg-surface rounded-lg shadow p-8 text-center">
340 <div className="inline-flex items-center gap-2">
341 <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
342 <span className="text-text">Loading...</span>
347 {/* Results Table */}
348 {!loading && data.length > 0 && (
349 <div className="bg-surface rounded-lg shadow overflow-hidden">
350 <div className="overflow-x-auto">
351 <table className="min-w-full divide-y divide-gray-200">
352 <thead className="bg-surface-2">
355 onClick={() => handleSort("f102")}
356 className="px-4 py-3 text-left text-xs font-medium text-text uppercase tracking-wider cursor-pointer hover:bg-surface-2"
359 <SortIndicator column="f102" />
362 onClick={() => handleSort("f106")}
363 className="px-4 py-3 text-left text-xs font-medium text-text uppercase tracking-wider cursor-pointer hover:bg-surface-2"
366 <SortIndicator column="f106" />
369 onClick={() => handleSort("f101")}
370 className="px-4 py-3 text-left text-xs font-medium text-text uppercase tracking-wider cursor-pointer hover:bg-surface-2"
373 <SortIndicator column="f101" />
376 onClick={() => handleSort("f107")}
377 className="px-4 py-3 text-right text-xs font-medium text-text uppercase tracking-wider cursor-pointer hover:bg-surface-2"
380 <SortIndicator column="f107" />
383 onClick={() => handleSort("f104")}
384 className="px-4 py-3 text-left text-xs font-medium text-text uppercase tracking-wider cursor-pointer hover:bg-surface-2"
387 <SortIndicator column="f104" />
390 onClick={() => handleSort("f201")}
391 className="px-4 py-3 text-left text-xs font-medium text-text uppercase tracking-wider cursor-pointer hover:bg-surface-2"
394 <SortIndicator column="f201" />
397 onClick={() => handleSort("f202")}
398 className="px-4 py-3 text-right text-xs font-medium text-text uppercase tracking-wider cursor-pointer hover:bg-surface-2"
401 <SortIndicator column="f202" />
404 onClick={() => handleSort("f203")}
405 className="px-4 py-3 text-right text-xs font-medium text-text uppercase tracking-wider cursor-pointer hover:bg-surface-2"
408 <SortIndicator column="f203" />
411 onClick={() => handleSort("f30012")}
412 className="px-4 py-3 text-left text-xs font-medium text-text uppercase tracking-wider cursor-pointer hover:bg-surface-2"
415 <SortIndicator column="f30012" />
418 onClick={() => handleSort("f30006")}
419 className="px-4 py-3 text-right text-xs font-medium text-text uppercase tracking-wider cursor-pointer hover:bg-surface-2"
422 <SortIndicator column="f30006" />
426 <tbody className="bg-surface divide-y divide-gray-200">
427 {sortedData.map((row, idx) => (
429 key={`${row.f101}-${row.f200}-${idx}`}
430 className={idx % 2 === 0 ? "bg-surface" : "bg-surface-2"}
432 <td className="px-4 py-3 text-sm text-text whitespace-nowrap">
433 {formatDate(row.f102)}
435 <td className="px-4 py-3 text-sm text-text">{row.f106}</td>
436 <td className="px-4 py-3 text-sm text-brand">
438 href={`/report/pos/sales/fieldpine/SingleSale.htm?sid=${row.f101}`}
439 className="hover:underline"
441 rel="noopener noreferrer"
446 <td className="px-4 py-3 text-sm text-text text-right">
447 {formatCurrency(row.f107)}
449 <td className="px-4 py-3 text-sm text-text">{row.f104}</td>
450 <td className="px-4 py-3 text-sm text-brand">
452 href={`/report/pos/stock/fieldpine/singlepid.htm?pid=${row.f200}`}
453 className="hover:underline"
455 rel="noopener noreferrer"
460 <td className="px-4 py-3 text-sm text-text text-right">
463 <td className="px-4 py-3 text-sm text-text text-right">
464 {formatCurrency(row.f203)}
466 <td className="px-4 py-3 text-sm text-text">
469 <td className="px-4 py-3 text-sm text-text text-right">
470 {formatCurrency(row.f30006)}
479 <div className="px-6 py-4 bg-surface-2 border-t border-border">
480 <p className="text-sm text-muted">
481 Showing {data.length} sale line{data.length !== 1 ? "s" : ""} with discounts
488 {!loading && data.length === 0 && (
489 <div className="bg-surface rounded-lg shadow p-8 text-center text-muted">
490 No sales with discounts found for the selected criteria. Click Display to load