3import { useState, useEffect } from "react";
4import { apiClient } from "@/lib/client/apiClient";
7 f100: number; // Sale ID
9 f102: number; // Total sale
10 f2115?: string; // Teller name
11 f2117?: string; // Customer name
12 f2125?: string; // Store name
13 f117?: number; // Customer ID
14 f900?: string; // Email
15 f130?: string; // External ID
16 f301?: string; // Internal comments
17 f133?: string; // Comments
18 f148?: string; // Delivery country
19 f185?: string; // Order no
20 f139?: string; // Internal physkey
21 f121?: number; // Original sale ID
22 f7102?: number; // Picking assigned store
23 f7120?: string; // Picking quick comments
24 f7123?: string; // Picking customer comments
25 f7140?: string; // Picking error reason
26 f7141?: string; // Picking error detail
27 f7142?: string; // Picking error message
28 CUST?: any[]; // Customer packet
36export default function SaleHeaderReviewPage() {
37 const [data, setData] = useState<SaleHeader[]>([]);
38 const [loading, setLoading] = useState(false);
39 const [sortColumn, setSortColumn] = useState<keyof SaleHeader>("f101");
40 const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
43 const [fromDate, setFromDate] = useState(() => {
45 d.setDate(d.getDate() - 1);
46 return d.toISOString().split("T")[0];
48 const [toDate, setToDate] = useState(() => {
50 d.setDate(d.getDate() + 1);
51 return d.toISOString().split("T")[0];
53 const [selectedStore, setSelectedStore] = useState("0");
54 const [tellerFilter, setTellerFilter] = useState("");
55 const [customerFilter, setCustomerFilter] = useState("");
56 const [customerFilterType, setCustomerFilterType] = useState("0"); // 0=Show Only, 5=Exclude
57 const [excludeUnknown, setExcludeUnknown] = useState(false);
58 const [onlyLayby, setOnlyLayby] = useState(false);
59 const [advancedSearch, setAdvancedSearch] = useState("");
60 const [maxRows, setMaxRows] = useState("2000");
61 const [advancedSearchInfo, setAdvancedSearchInfo] = useState<string[]>([]);
64 const [locations, setLocations] = useState<Location[]>([]);
66 // Load locations on mount
68 apiClient.getLocations({ want: 'name,id' })
70 if (locData.success && (locData.data as any)?.data?.Location) {
71 setLocations((locData.data as any).data.Location);
74 .catch((err) => console.error("Error loading locations:", err));
77 const formatDateForAPI = (dateStr: string) => {
78 const [year, month, day] = dateStr.split("-");
79 return `${day}-${month}-${year}`;
82 const handleSort = (column: keyof SaleHeader) => {
83 if (sortColumn === column) {
84 setSortDirection(sortDirection === "asc" ? "desc" : "asc");
86 setSortColumn(column);
87 setSortDirection("asc");
91 const sortedData = [...data].sort((a, b) => {
92 const aVal = a[sortColumn];
93 const bVal = b[sortColumn];
95 if (aVal === undefined || bVal === undefined) return 0;
97 if (typeof aVal === "number" && typeof bVal === "number") {
98 return sortDirection === "asc" ? aVal - bVal : bVal - aVal;
101 const aStr = String(aVal);
102 const bStr = String(bVal);
103 return sortDirection === "asc"
104 ? aStr.localeCompare(bStr)
105 : bStr.localeCompare(aStr);
108 const SortIndicator = ({ column }: { column: keyof SaleHeader }) => {
109 if (sortColumn !== column) return null;
110 return <span className="ml-1">{sortDirection === "asc" ? "▲" : "▼"}</span>;
113 const loadReport = async () => {
116 setAdvancedSearchInfo([]);
118 const apiKey = sessionStorage.getItem("fieldpine_apikey") || "";
119 const fromDateAPI = formatDateForAPI(fromDate);
120 const toDateAPI = formatDateForAPI(toDate);
122 const filters: string[] = [
123 "f108,0,1" // Some base filter
127 if (selectedStore !== "0" && selectedStore) {
128 filters.push(`f125,0,${selectedStore}`);
133 filters.push("f110,0,layby");
136 // Exclude unknown customer
137 if (excludeUnknown) {
138 filters.push("f117,3,0");
142 filters.push(`f101,4,${fromDateAPI}`);
143 filters.push(`f101,1,${toDateAPI}`);
146 if (advancedSearch.trim()) {
147 filters.push(`f504,0,${encodeURIComponent(advancedSearch)}`);
151 const response = await apiClient.getSalesList({
152 limit: Number(maxRows),
153 fields: "2115,2117,2125",
157 if (response.success && response.data?.DATS && Array.isArray(response.data.DATS)) {
158 setData(response.data.DATS);
163 // Handle advanced search display info
164 if (response.success && response.data?.F504 && Array.isArray(response.data.F504)) {
165 const searchInfo = response.data.F504.map((rec: any) => rec.f110 || "").filter(
168 setAdvancedSearchInfo(searchInfo);
171 console.error("Error loading report:", error);
178 const formatCurrency = (value: number | undefined) => {
179 if (value === undefined || value === null) return "";
180 return new Intl.NumberFormat("en-US", {
183 minimumFractionDigits: 2,
187 const formatDate = (dateStr: string | undefined) => {
188 if (!dateStr) return "";
190 const date = new Date(dateStr);
191 return date.toLocaleDateString();
198 const totalSales = data.reduce((sum, row) => sum + (row.f102 || 0), 0);
201 <div className="min-h-screen bg-bg p-6">
202 <div className="max-w-[1800px] mx-auto">
204 <div className="mb-6">
205 <h1 className="text-3xl font-bold text-text mb-2">Sale Header Review</h1>
206 <p className="text-muted">
207 Shows summary of sales transactions - one line per sale. Filter by date, store,
213 <div className="bg-surface rounded-lg shadow p-6 mb-6">
215 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-4">
217 <label className="block text-sm font-medium text-text mb-1">
223 onChange={(e) => setFromDate(e.target.value)}
224 className="w-full px-3 py-2 border border-border rounded-md"
229 <label className="block text-sm font-medium text-text mb-1">
235 onChange={(e) => setToDate(e.target.value)}
236 className="w-full px-3 py-2 border border-border rounded-md"
241 <label className="block text-sm font-medium text-text mb-1">Store</label>
243 value={selectedStore}
244 onChange={(e) => setSelectedStore(e.target.value)}
245 className="w-full px-3 py-2 border border-border rounded-md"
247 <option value="0">All Stores</option>
248 {locations.map((loc) => (
249 <option key={loc.Locid} value={loc.Locid}>
258 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-4">
260 <label className="block text-sm font-medium text-text mb-1">
266 onChange={(e) => setTellerFilter(e.target.value)}
267 placeholder="Autocomplete disabled"
268 className="w-full px-3 py-2 border border-border rounded-md"
274 <label className="block text-sm font-medium text-text mb-1">
277 <div className="flex gap-2">
279 value={customerFilterType}
280 onChange={(e) => setCustomerFilterType(e.target.value)}
281 className="w-32 px-2 py-2 border border-border rounded-md"
283 <option value="0">Show Only</option>
284 <option value="5">Exclude</option>
288 value={customerFilter}
289 onChange={(e) => setCustomerFilter(e.target.value)}
290 placeholder="Autocomplete disabled"
291 className="flex-1 px-3 py-2 border border-border rounded-md"
295 <div className="mt-2">
296 <label className="flex items-center text-sm text-text">
299 checked={excludeUnknown}
300 onChange={(e) => setExcludeUnknown(e.target.checked)}
303 Exclude Unknown Customer
309 <label className="block text-sm font-medium text-text mb-1">
312 <div className="flex gap-2">
316 onChange={(e) => setMaxRows(e.target.value)}
317 className="flex-1 px-3 py-2 border border-border rounded-md"
322 className="px-6 py-2 bg-brand text-white rounded-md hover:bg-brand/90 disabled:bg-muted/50 whitespace-nowrap"
324 {loading ? "Loading..." : "Display"}
330 {/* Row 3 - Additional options */}
331 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
333 <label className="flex items-center text-sm text-text">
337 onChange={(e) => setOnlyLayby(e.target.checked)}
344 <div className="border border-orange-300 rounded p-3">
345 <label className="block text-sm font-medium text-text mb-1">
348 className="ml-2 text-brand cursor-pointer text-xs"
349 title="Advanced search allows natural language queries like 'referred by john', 'rep sue', 'void', 'sold in stratford'"
356 value={advancedSearch}
357 onChange={(e) => setAdvancedSearch(e.target.value)}
358 placeholder="e.g., 'void', 'referred by john', 'rep sue'"
359 className="w-full px-3 py-2 border border-border rounded-md"
365 {/* Advanced Search Info */}
366 {advancedSearchInfo.length > 0 && (
367 <div className="bg-info/10 border border-info/30 rounded-lg p-4 mb-6">
368 <p className="font-semibold text-info mb-2">Showing only:</p>
369 {advancedSearchInfo.map((info, idx) => (
370 <p key={idx} className="text-info ml-4">
371 {idx > 0 && <span className="italic">and </span>}
378 {/* Loading State */}
380 <div className="bg-surface rounded-lg shadow p-8 text-center">
381 <div className="inline-flex items-center gap-2">
382 <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-info"></div>
383 <span className="text-text">Loading...</span>
388 {/* Results Table */}
389 {!loading && data.length > 0 && (
390 <div className="bg-surface rounded-lg shadow overflow-hidden">
391 <div className="overflow-x-auto">
392 <table className="min-w-full divide-y divide-border">
393 <thead className="bg-surface-2">
396 onClick={() => handleSort("f101")}
397 className="px-4 py-3 text-left text-xs font-medium text-text uppercase tracking-wider cursor-pointer hover:bg-surface-2"
400 <SortIndicator column="f101" />
403 onClick={() => handleSort("f2125")}
404 className="px-4 py-3 text-left text-xs font-medium text-text uppercase tracking-wider cursor-pointer hover:bg-surface-2"
407 <SortIndicator column="f2125" />
410 onClick={() => handleSort("f100")}
411 className="px-4 py-3 text-left text-xs font-medium text-text uppercase tracking-wider cursor-pointer hover:bg-surface-2"
414 <SortIndicator column="f100" />
417 onClick={() => handleSort("f102")}
418 className="px-4 py-3 text-right text-xs font-medium text-text uppercase tracking-wider cursor-pointer hover:bg-surface-2"
421 <SortIndicator column="f102" />
424 onClick={() => handleSort("f2115")}
425 className="px-4 py-3 text-left text-xs font-medium text-text uppercase tracking-wider cursor-pointer hover:bg-surface-2"
428 <SortIndicator column="f2115" />
431 onClick={() => handleSort("f2117")}
432 className="px-4 py-3 text-left text-xs font-medium text-text uppercase tracking-wider cursor-pointer hover:bg-surface-2"
435 <SortIndicator column="f2117" />
439 <tbody className="bg-surface divide-y divide-border">
440 {sortedData.map((row, idx) => (
442 key={`${row.f100}-${idx}`}
443 className={idx % 2 === 0 ? "bg-surface" : "bg-surface-2"}
445 <td className="px-4 py-3 text-sm text-text whitespace-nowrap">
446 {formatDate(row.f101)}
448 <td className="px-4 py-3 text-sm text-text">{row.f2125}</td>
449 <td className="px-4 py-3 text-sm text-brand">
451 href={`/report/pos/sales/fieldpine/SingleSale.htm?sid=${row.f100}`}
452 className="hover:underline"
454 rel="noopener noreferrer"
459 <td className="px-4 py-3 text-sm text-text text-right">
460 {formatCurrency(row.f102)}
462 <td className="px-4 py-3 text-sm text-text">{row.f2115}</td>
463 <td className="px-4 py-3 text-sm text-brand">
466 href={`/report/pos/Customer/Fieldpine/Customer_Single_Overview.htm?cid=${row.f117}`}
467 className="hover:underline"
469 rel="noopener noreferrer"
480 {/* Totals Footer */}
481 <tfoot className="bg-text text-white">
483 <td className="px-4 py-3 text-sm font-bold">Totals</td>
484 <td className="px-4 py-3"></td>
485 <td className="px-4 py-3"></td>
486 <td className="px-4 py-3 text-sm font-bold text-right">
487 {formatCurrency(totalSales)}
489 <td className="px-4 py-3"></td>
490 <td className="px-4 py-3"></td>
497 <div className="px-6 py-4 bg-surface-2 border-t border-border">
498 <p className="text-sm text-muted">
499 Showing {data.length} sale{data.length !== 1 ? "s" : ""}
506 {!loading && data.length === 0 && (
507 <div className="bg-surface rounded-lg shadow p-8 text-center text-muted">
508 No sales found for the selected criteria. Click Display to load results.