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 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
22}
23
24interface Location {
25 Locid: string;
26 Name: string;
27}
28
29interface Discount {
30 DiscId?: number;
31 f100?: number;
32 Name?: string;
33 f101?: string;
34}
35
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");
41
42 // Filter states
43 const [fromDate, setFromDate] = useState(() => {
44 const d = new Date();
45 d.setDate(d.getDate() - 1); // Yesterday
46 return d.toISOString().split("T")[0];
47 });
48 const [toDate, setToDate] = useState(() => {
49 const d = new Date();
50 d.setDate(d.getDate() + 1); // Tomorrow
51 return d.toISOString().split("T")[0];
52 });
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");
59
60 // Dropdown data
61 const [locations, setLocations] = useState<Location[]>([]);
62 const [discounts, setDiscounts] = useState<Discount[]>([]);
63
64 // Load locations and discounts on mount
65 useEffect(() => {
66 Promise.all([
67 apiClient.getLocations({ source: 'elink' }),
68 apiClient.getDiscounts(),
69 ])
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 || "",
75 }));
76 setLocations(locs);
77 }
78
79 if (discData.success && discData.data) {
80 const discs = discData.data.map((d: any) => ({
81 DiscId: d.DiscId || d.f100,
82 f100: d.f100,
83 Name: d.Name || d.f101,
84 f101: d.f101,
85 }));
86 setDiscounts(discs);
87 }
88 })
89 .catch((err) => console.error("Error loading dropdowns:", err));
90 }, []);
91
92 const formatDateForAPI = (dateStr: string) => {
93 const [year, month, day] = dateStr.split("-");
94 return `${day}-${month}-${year}`;
95 };
96
97 const handleSort = (column: keyof SaleRow) => {
98 if (sortColumn === column) {
99 setSortDirection(sortDirection === "asc" ? "desc" : "asc");
100 } else {
101 setSortColumn(column);
102 setSortDirection("asc");
103 }
104 };
105
106 const sortedData = [...data].sort((a, b) => {
107 const aVal = a[sortColumn];
108 const bVal = b[sortColumn];
109
110 if (aVal === undefined || bVal === undefined) return 0;
111
112 if (typeof aVal === "number" && typeof bVal === "number") {
113 return sortDirection === "asc" ? aVal - bVal : bVal - aVal;
114 }
115
116 const aStr = String(aVal);
117 const bStr = String(bVal);
118 return sortDirection === "asc"
119 ? aStr.localeCompare(bStr)
120 : bStr.localeCompare(aStr);
121 });
122
123 const SortIndicator = ({ column }: { column: keyof SaleRow }) => {
124 if (sortColumn !== column) return null;
125 return <span className="ml-1">{sortDirection === "asc" ? "▲" : "▼"}</span>;
126 };
127
128 const loadReport = async () => {
129 setLoading(true);
130 setData([]);
131
132 const fromDateAPI = formatDateForAPI(fromDate);
133 const toDateAPI = formatDateForAPI(toDate);
134
135 const filters: string[] = [];
136
137 // Store filter
138 if (selectedStore !== "0" && selectedStore) {
139 filters.push(`f105,0,${selectedStore}`);
140 }
141
142 // Discount filter
143 if (selectedDiscount !== "0" && selectedDiscount) {
144 filters.push(`f216,0,${selectedDiscount}`);
145 } else {
146 filters.push("f216,gt,0"); // Any discount
147 }
148
149 // Date filters
150 filters.push(`f102,4,${fromDateAPI}`);
151 filters.push(`f102,1,${toDateAPI}`);
152
153 try {
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
158 });
159
160 if (response.success && response.data?.APPT && Array.isArray(response.data.APPT)) {
161 setData(response.data.APPT);
162 } else {
163 setData([]);
164 }
165 } catch (error) {
166 console.error("Error loading report:", error);
167 setData([]);
168 } finally {
169 setLoading(false);
170 }
171 };
172
173 const formatCurrency = (value: number | undefined) => {
174 if (value === undefined || value === null) return "";
175 return new Intl.NumberFormat("en-US", {
176 style: "currency",
177 currency: "USD",
178 minimumFractionDigits: 2,
179 }).format(value);
180 };
181
182 const formatDate = (dateStr: string | undefined) => {
183 if (!dateStr) return "";
184 try {
185 const date = new Date(dateStr);
186 return date.toLocaleDateString();
187 } catch {
188 return dateStr;
189 }
190 };
191
192 return (
193 <div className="min-h-screen bg-bg p-6">
194 <div className="max-w-[1800px] mx-auto">
195 {/* Header */}
196 <div className="mb-6">
197 <h1 className="text-3xl font-bold text-text mb-2">
198 Sales With Discounts Review
199 </h1>
200 <p className="text-muted">
201 Review sales transactions that include discounts. Filter by date range, store,
202 discount type, or specific products.
203 </p>
204 </div>
205
206 {/* Filters */}
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">
209 <div>
210 <label className="block text-sm font-medium text-text mb-1">
211 From (including)
212 </label>
213 <input
214 type="date"
215 value={fromDate}
216 onChange={(e) => setFromDate(e.target.value)}
217 className="w-full px-3 py-2 border border-border rounded-md"
218 />
219 </div>
220
221 <div>
222 <label className="block text-sm font-medium text-text mb-1">
223 To (excluding)
224 </label>
225 <input
226 type="date"
227 value={toDate}
228 onChange={(e) => setToDate(e.target.value)}
229 className="w-full px-3 py-2 border border-border rounded-md"
230 />
231 </div>
232
233 <div>
234 <label className="block text-sm font-medium text-text mb-1">Store</label>
235 <select
236 value={selectedStore}
237 onChange={(e) => setSelectedStore(e.target.value)}
238 className="w-full px-3 py-2 border border-border rounded-md"
239 >
240 <option value="0">All Stores</option>
241 {locations.map((loc) => (
242 <option key={loc.Locid} value={loc.Locid}>
243 {loc.Name}
244 </option>
245 ))}
246 </select>
247 </div>
248
249 <div>
250 <label className="block text-sm font-medium text-text mb-1">
251 Discount
252 </label>
253 <select
254 value={selectedDiscount}
255 onChange={(e) => setSelectedDiscount(e.target.value)}
256 className="w-full px-3 py-2 border border-border rounded-md"
257 >
258 <option value="0">Any</option>
259 {discounts.map((disc, idx) => (
260 <option
261 key={idx}
262 value={disc.DiscId || disc.f100 || idx}
263 >
264 {disc.Name || disc.f101 || disc.DiscId || disc.f100}
265 </option>
266 ))}
267 </select>
268 </div>
269 </div>
270
271 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
272 <div>
273 <label className="block text-sm font-medium text-text mb-1">
274 Teller (optional)
275 </label>
276 <input
277 type="text"
278 value={tellerFilter}
279 onChange={(e) => setTellerFilter(e.target.value)}
280 placeholder="Autocomplete disabled"
281 className="w-full px-3 py-2 border border-border rounded-md"
282 disabled
283 />
284 </div>
285
286 <div>
287 <label className="block text-sm font-medium text-text mb-1">
288 Customer (optional)
289 </label>
290 <input
291 type="text"
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"
296 disabled
297 />
298 </div>
299
300 <div>
301 <label className="block text-sm font-medium text-text mb-1">
302 Product (optional)
303 </label>
304 <input
305 type="text"
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"
310 disabled
311 />
312 </div>
313
314 <div>
315 <label className="block text-sm font-medium text-text mb-1">
316 Max Rows
317 </label>
318 <div className="flex gap-2">
319 <input
320 type="number"
321 value={maxRows}
322 onChange={(e) => setMaxRows(e.target.value)}
323 className="flex-1 px-3 py-2 border border-border rounded-md"
324 />
325 <button
326 onClick={loadReport}
327 disabled={loading}
328 className="px-6 py-2 bg-brand text-white rounded-md hover:bg-brand2 disabled:bg-muted/50 whitespace-nowrap"
329 >
330 {loading ? "Loading..." : "Display"}
331 </button>
332 </div>
333 </div>
334 </div>
335 </div>
336
337 {/* Loading State */}
338 {loading && (
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>
343 </div>
344 </div>
345 )}
346
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">
353 <tr>
354 <th
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"
357 >
358 Date
359 <SortIndicator column="f102" />
360 </th>
361 <th
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"
364 >
365 Store
366 <SortIndicator column="f106" />
367 </th>
368 <th
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"
371 >
372 Sale#
373 <SortIndicator column="f101" />
374 </th>
375 <th
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"
378 >
379 Total Sale
380 <SortIndicator column="f107" />
381 </th>
382 <th
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"
385 >
386 Teller
387 <SortIndicator column="f104" />
388 </th>
389 <th
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"
392 >
393 Product
394 <SortIndicator column="f201" />
395 </th>
396 <th
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"
399 >
400 Qty
401 <SortIndicator column="f202" />
402 </th>
403 <th
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"
406 >
407 Line Price
408 <SortIndicator column="f203" />
409 </th>
410 <th
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"
413 >
414 Disc Name
415 <SortIndicator column="f30012" />
416 </th>
417 <th
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"
420 >
421 Disc$
422 <SortIndicator column="f30006" />
423 </th>
424 </tr>
425 </thead>
426 <tbody className="bg-surface divide-y divide-gray-200">
427 {sortedData.map((row, idx) => (
428 <tr
429 key={`${row.f101}-${row.f200}-${idx}`}
430 className={idx % 2 === 0 ? "bg-surface" : "bg-surface-2"}
431 >
432 <td className="px-4 py-3 text-sm text-text whitespace-nowrap">
433 {formatDate(row.f102)}
434 </td>
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">
437 <a
438 href={`/report/pos/sales/fieldpine/SingleSale.htm?sid=${row.f101}`}
439 className="hover:underline"
440 target="_blank"
441 rel="noopener noreferrer"
442 >
443 {row.f101}
444 </a>
445 </td>
446 <td className="px-4 py-3 text-sm text-text text-right">
447 {formatCurrency(row.f107)}
448 </td>
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">
451 <a
452 href={`/report/pos/stock/fieldpine/singlepid.htm?pid=${row.f200}`}
453 className="hover:underline"
454 target="_blank"
455 rel="noopener noreferrer"
456 >
457 {row.f201}
458 </a>
459 </td>
460 <td className="px-4 py-3 text-sm text-text text-right">
461 {row.f202}
462 </td>
463 <td className="px-4 py-3 text-sm text-text text-right">
464 {formatCurrency(row.f203)}
465 </td>
466 <td className="px-4 py-3 text-sm text-text">
467 {row.f30012 || ""}
468 </td>
469 <td className="px-4 py-3 text-sm text-text text-right">
470 {formatCurrency(row.f30006)}
471 </td>
472 </tr>
473 ))}
474 </tbody>
475 </table>
476 </div>
477
478 {/* Footer */}
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
482 </p>
483 </div>
484 </div>
485 )}
486
487 {/* No Results */}
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
491 results.
492 </div>
493 )}
494 </div>
495 </div>
496 );
497}