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 SaleHeader {
7 f100: number; // Sale ID
8 f101: string; // Date
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
29}
30
31interface Location {
32 Locid: string;
33 Name: string;
34}
35
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");
41
42 // Filter states
43 const [fromDate, setFromDate] = useState(() => {
44 const d = new Date();
45 d.setDate(d.getDate() - 1);
46 return d.toISOString().split("T")[0];
47 });
48 const [toDate, setToDate] = useState(() => {
49 const d = new Date();
50 d.setDate(d.getDate() + 1);
51 return d.toISOString().split("T")[0];
52 });
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[]>([]);
62
63 // Dropdown data
64 const [locations, setLocations] = useState<Location[]>([]);
65
66 // Load locations on mount
67 useEffect(() => {
68 apiClient.getLocations({ want: 'name,id' })
69 .then((locData) => {
70 if (locData.success && (locData.data as any)?.data?.Location) {
71 setLocations((locData.data as any).data.Location);
72 }
73 })
74 .catch((err) => console.error("Error loading locations:", err));
75 }, []);
76
77 const formatDateForAPI = (dateStr: string) => {
78 const [year, month, day] = dateStr.split("-");
79 return `${day}-${month}-${year}`;
80 };
81
82 const handleSort = (column: keyof SaleHeader) => {
83 if (sortColumn === column) {
84 setSortDirection(sortDirection === "asc" ? "desc" : "asc");
85 } else {
86 setSortColumn(column);
87 setSortDirection("asc");
88 }
89 };
90
91 const sortedData = [...data].sort((a, b) => {
92 const aVal = a[sortColumn];
93 const bVal = b[sortColumn];
94
95 if (aVal === undefined || bVal === undefined) return 0;
96
97 if (typeof aVal === "number" && typeof bVal === "number") {
98 return sortDirection === "asc" ? aVal - bVal : bVal - aVal;
99 }
100
101 const aStr = String(aVal);
102 const bStr = String(bVal);
103 return sortDirection === "asc"
104 ? aStr.localeCompare(bStr)
105 : bStr.localeCompare(aStr);
106 });
107
108 const SortIndicator = ({ column }: { column: keyof SaleHeader }) => {
109 if (sortColumn !== column) return null;
110 return <span className="ml-1">{sortDirection === "asc" ? "▲" : "▼"}</span>;
111 };
112
113 const loadReport = async () => {
114 setLoading(true);
115 setData([]);
116 setAdvancedSearchInfo([]);
117
118 const apiKey = sessionStorage.getItem("fieldpine_apikey") || "";
119 const fromDateAPI = formatDateForAPI(fromDate);
120 const toDateAPI = formatDateForAPI(toDate);
121
122 const filters: string[] = [
123 "f108,0,1" // Some base filter
124 ];
125
126 // Store filter
127 if (selectedStore !== "0" && selectedStore) {
128 filters.push(`f125,0,${selectedStore}`);
129 }
130
131 // Layby filter
132 if (onlyLayby) {
133 filters.push("f110,0,layby");
134 }
135
136 // Exclude unknown customer
137 if (excludeUnknown) {
138 filters.push("f117,3,0");
139 }
140
141 // Date filters
142 filters.push(`f101,4,${fromDateAPI}`);
143 filters.push(`f101,1,${toDateAPI}`);
144
145 // Advanced search
146 if (advancedSearch.trim()) {
147 filters.push(`f504,0,${encodeURIComponent(advancedSearch)}`);
148 }
149
150 try {
151 const response = await apiClient.getSalesList({
152 limit: Number(maxRows),
153 fields: "2115,2117,2125",
154 filters
155 });
156
157 if (response.success && response.data?.DATS && Array.isArray(response.data.DATS)) {
158 setData(response.data.DATS);
159 } else {
160 setData([]);
161 }
162
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(
166 (s: string) => s
167 );
168 setAdvancedSearchInfo(searchInfo);
169 }
170 } catch (error) {
171 console.error("Error loading report:", error);
172 setData([]);
173 } finally {
174 setLoading(false);
175 }
176 };
177
178 const formatCurrency = (value: number | undefined) => {
179 if (value === undefined || value === null) return "";
180 return new Intl.NumberFormat("en-US", {
181 style: "currency",
182 currency: "USD",
183 minimumFractionDigits: 2,
184 }).format(value);
185 };
186
187 const formatDate = (dateStr: string | undefined) => {
188 if (!dateStr) return "";
189 try {
190 const date = new Date(dateStr);
191 return date.toLocaleDateString();
192 } catch {
193 return dateStr;
194 }
195 };
196
197 // Calculate totals
198 const totalSales = data.reduce((sum, row) => sum + (row.f102 || 0), 0);
199
200 return (
201 <div className="min-h-screen bg-bg p-6">
202 <div className="max-w-[1800px] mx-auto">
203 {/* Header */}
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,
208 customer, and more.
209 </p>
210 </div>
211
212 {/* Filters */}
213 <div className="bg-surface rounded-lg shadow p-6 mb-6">
214 {/* Row 1 */}
215 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-4">
216 <div>
217 <label className="block text-sm font-medium text-text mb-1">
218 From (including)
219 </label>
220 <input
221 type="date"
222 value={fromDate}
223 onChange={(e) => setFromDate(e.target.value)}
224 className="w-full px-3 py-2 border border-border rounded-md"
225 />
226 </div>
227
228 <div>
229 <label className="block text-sm font-medium text-text mb-1">
230 To (excluding)
231 </label>
232 <input
233 type="date"
234 value={toDate}
235 onChange={(e) => setToDate(e.target.value)}
236 className="w-full px-3 py-2 border border-border rounded-md"
237 />
238 </div>
239
240 <div>
241 <label className="block text-sm font-medium text-text mb-1">Store</label>
242 <select
243 value={selectedStore}
244 onChange={(e) => setSelectedStore(e.target.value)}
245 className="w-full px-3 py-2 border border-border rounded-md"
246 >
247 <option value="0">All Stores</option>
248 {locations.map((loc) => (
249 <option key={loc.Locid} value={loc.Locid}>
250 {loc.Name}
251 </option>
252 ))}
253 </select>
254 </div>
255 </div>
256
257 {/* Row 2 */}
258 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-4">
259 <div>
260 <label className="block text-sm font-medium text-text mb-1">
261 Teller (optional)
262 </label>
263 <input
264 type="text"
265 value={tellerFilter}
266 onChange={(e) => setTellerFilter(e.target.value)}
267 placeholder="Autocomplete disabled"
268 className="w-full px-3 py-2 border border-border rounded-md"
269 disabled
270 />
271 </div>
272
273 <div>
274 <label className="block text-sm font-medium text-text mb-1">
275 Customer (optional)
276 </label>
277 <div className="flex gap-2">
278 <select
279 value={customerFilterType}
280 onChange={(e) => setCustomerFilterType(e.target.value)}
281 className="w-32 px-2 py-2 border border-border rounded-md"
282 >
283 <option value="0">Show Only</option>
284 <option value="5">Exclude</option>
285 </select>
286 <input
287 type="text"
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"
292 disabled
293 />
294 </div>
295 <div className="mt-2">
296 <label className="flex items-center text-sm text-text">
297 <input
298 type="checkbox"
299 checked={excludeUnknown}
300 onChange={(e) => setExcludeUnknown(e.target.checked)}
301 className="mr-2"
302 />
303 Exclude Unknown Customer
304 </label>
305 </div>
306 </div>
307
308 <div>
309 <label className="block text-sm font-medium text-text mb-1">
310 Max Rows
311 </label>
312 <div className="flex gap-2">
313 <input
314 type="number"
315 value={maxRows}
316 onChange={(e) => setMaxRows(e.target.value)}
317 className="flex-1 px-3 py-2 border border-border rounded-md"
318 />
319 <button
320 onClick={loadReport}
321 disabled={loading}
322 className="px-6 py-2 bg-brand text-white rounded-md hover:bg-brand/90 disabled:bg-muted/50 whitespace-nowrap"
323 >
324 {loading ? "Loading..." : "Display"}
325 </button>
326 </div>
327 </div>
328 </div>
329
330 {/* Row 3 - Additional options */}
331 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
332 <div>
333 <label className="flex items-center text-sm text-text">
334 <input
335 type="checkbox"
336 checked={onlyLayby}
337 onChange={(e) => setOnlyLayby(e.target.checked)}
338 className="mr-2"
339 />
340 Only Layby
341 </label>
342 </div>
343
344 <div className="border border-orange-300 rounded p-3">
345 <label className="block text-sm font-medium text-text mb-1">
346 Advanced Search
347 <span
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'"
350 >
351 ℹ️ Help
352 </span>
353 </label>
354 <input
355 type="text"
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"
360 />
361 </div>
362 </div>
363 </div>
364
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>}
372 {info}
373 </p>
374 ))}
375 </div>
376 )}
377
378 {/* Loading State */}
379 {loading && (
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>
384 </div>
385 </div>
386 )}
387
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">
394 <tr>
395 <th
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"
398 >
399 Date
400 <SortIndicator column="f101" />
401 </th>
402 <th
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"
405 >
406 Store
407 <SortIndicator column="f2125" />
408 </th>
409 <th
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"
412 >
413 Sale#
414 <SortIndicator column="f100" />
415 </th>
416 <th
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"
419 >
420 Total Sale
421 <SortIndicator column="f102" />
422 </th>
423 <th
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"
426 >
427 Teller
428 <SortIndicator column="f2115" />
429 </th>
430 <th
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"
433 >
434 Customer
435 <SortIndicator column="f2117" />
436 </th>
437 </tr>
438 </thead>
439 <tbody className="bg-surface divide-y divide-border">
440 {sortedData.map((row, idx) => (
441 <tr
442 key={`${row.f100}-${idx}`}
443 className={idx % 2 === 0 ? "bg-surface" : "bg-surface-2"}
444 >
445 <td className="px-4 py-3 text-sm text-text whitespace-nowrap">
446 {formatDate(row.f101)}
447 </td>
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">
450 <a
451 href={`/report/pos/sales/fieldpine/SingleSale.htm?sid=${row.f100}`}
452 className="hover:underline"
453 target="_blank"
454 rel="noopener noreferrer"
455 >
456 {row.f100}
457 </a>
458 </td>
459 <td className="px-4 py-3 text-sm text-text text-right">
460 {formatCurrency(row.f102)}
461 </td>
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">
464 {row.f117 ? (
465 <a
466 href={`/report/pos/Customer/Fieldpine/Customer_Single_Overview.htm?cid=${row.f117}`}
467 className="hover:underline"
468 target="_blank"
469 rel="noopener noreferrer"
470 >
471 {row.f2117}
472 </a>
473 ) : (
474 row.f2117
475 )}
476 </td>
477 </tr>
478 ))}
479 </tbody>
480 {/* Totals Footer */}
481 <tfoot className="bg-text text-white">
482 <tr>
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)}
488 </td>
489 <td className="px-4 py-3"></td>
490 <td className="px-4 py-3"></td>
491 </tr>
492 </tfoot>
493 </table>
494 </div>
495
496 {/* Footer */}
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" : ""}
500 </p>
501 </div>
502 </div>
503 )}
504
505 {/* No Results */}
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.
509 </div>
510 )}
511 </div>
512 </div>
513 );
514}