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, useRef } from "react";
4import { apiClient } from "@/lib/client/apiClient";
5
6interface Sale {
7 Sid: number;
8 CompletedDt: string;
9 LocationName: string;
10 Total: number;
11 TellerName: string;
12 f2117?: string; // Customer name
13 Cid?: number;
14 Physkey?: string;
15 _RowKey?: number;
16}
17
18interface SearchInfo {
19 Text: string;
20 IsError?: boolean;
21}
22
23export default function PastSalesFinderPage() {
24 const [data, setData] = useState<Sale[]>([]);
25 const [loading, setLoading] = useState(false);
26 const [searchText, setSearchText] = useState("sold today");
27 const [searchInfo, setSearchInfo] = useState<SearchInfo[]>([]);
28 const [showHelp, setShowHelp] = useState(false);
29 const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
30 const sequenceRef = useRef(1);
31
32 const [sortColumn, setSortColumn] = useState<keyof Sale>("CompletedDt");
33 const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
34
35 // Trigger initial search
36 useEffect(() => {
37 handleSearch();
38 }, []);
39
40 const handleSearchChange = (value: string) => {
41 setSearchText(value);
42
43 // Debounce search
44 if (searchTimeoutRef.current) {
45 clearTimeout(searchTimeoutRef.current);
46 }
47
48 searchTimeoutRef.current = setTimeout(() => {
49 handleSearch(value);
50 }, 300);
51 };
52
53 const handleSearch = async (query?: string) => {
54 const searchQuery = query !== undefined ? query : searchText;
55
56 if (!searchQuery.trim()) {
57 setData([]);
58 setSearchInfo([]);
59 return;
60 }
61
62 setLoading(true);
63 sequenceRef.current++;
64 const currentSequence = sequenceRef.current;
65
66 try {
67 const response = await apiClient.searchSales({
68 query: searchQuery,
69 limit: 30,
70 ref: currentSequence,
71 inputMethod: 'keyboard'
72 });
73
74 if (!response.success) {
75 console.error("Error searching sales:", response.error);
76 setData([]);
77 setSearchInfo([]);
78 setLoading(false);
79 return;
80 }
81
82 // Check if this is still the latest search
83 const returnedSequence = response.data?.YourRef || 0;
84 if (returnedSequence !== currentSequence) {
85 return; // Ignore outdated responses
86 }
87
88 if (response.data?.Sale_Read && Array.isArray(response.data.Sale_Read)) {
89 setData(response.data.Sale_Read);
90 } else {
91 setData([]);
92 }
93
94 // Handle search interpretation info
95 if (response.data?.ASTX && Array.isArray(response.data.ASTX)) {
96 setSearchInfo(response.data.ASTX);
97 } else {
98 setSearchInfo([]);
99 }
100 } catch (error) {
101 console.error("Error searching sales:", error);
102 setData([]);
103 setSearchInfo([]);
104 } finally {
105 setLoading(false);
106 }
107 };
108
109 const handleSort = (column: keyof Sale) => {
110 if (sortColumn === column) {
111 setSortDirection(sortDirection === "asc" ? "desc" : "asc");
112 } else {
113 setSortColumn(column);
114 setSortDirection("asc");
115 }
116 };
117
118 const sortedData = [...data].sort((a, b) => {
119 const aVal = a[sortColumn];
120 const bVal = b[sortColumn];
121
122 if (aVal === undefined || bVal === undefined) return 0;
123
124 if (typeof aVal === "number" && typeof bVal === "number") {
125 return sortDirection === "asc" ? aVal - bVal : bVal - aVal;
126 }
127
128 const aStr = String(aVal);
129 const bStr = String(bVal);
130 return sortDirection === "asc"
131 ? aStr.localeCompare(bStr)
132 : bStr.localeCompare(aStr);
133 });
134
135 const SortIndicator = ({ column }: { column: keyof Sale }) => {
136 if (sortColumn !== column) return null;
137 return <span className="ml-1">{sortDirection === "asc" ? "▲" : "▼"}</span>;
138 };
139
140 const formatCurrency = (value: number | undefined) => {
141 if (value === undefined || value === null) return "";
142 return new Intl.NumberFormat("en-US", {
143 style: "currency",
144 currency: "USD",
145 minimumFractionDigits: 2,
146 }).format(value);
147 };
148
149 const formatDateTime = (dateStr: string | undefined) => {
150 if (!dateStr) return "";
151 try {
152 const date = new Date(dateStr);
153 return date.toLocaleString();
154 } catch {
155 return dateStr;
156 }
157 };
158
159 return (
160 <div className="min-h-screen bg-bg p-6">
161 <div className="max-w-[1600px] mx-auto">
162 {/* Header */}
163 <div className="mb-6">
164 <h1 className="text-3xl font-bold text-text mb-2">Past Sales Finder</h1>
165 <p className="text-muted">
166 To help you locate individual sales. You should not treat this page as a general
167 purpose report, it is focused solely on finding sales.
168 </p>
169 </div>
170
171 {/* Search Box */}
172 <div className="bg-surface rounded-lg shadow p-6 mb-6">
173 <div className="mb-4">
174 <label className="block text-sm font-medium text-text mb-2">
175 Search:
176 </label>
177 <textarea
178 rows={3}
179 value={searchText}
180 onChange={(e) => handleSearchChange(e.target.value)}
181 placeholder='Enter product, customer or sale # here. Try "sold today", "plu 1234", "customer bob", "phase void"'
182 className="w-full px-4 py-3 border border-border rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
183 />
184 </div>
185
186 {/* Quick Tips */}
187 <div className="text-sm text-muted bg-info/10 border border-info/30 rounded p-3">
188 <strong>Quick Tips:</strong> Enter what you are searching for in simple english.
189 Such as "PLU 1234" or "sold yesterday". You can combine multiple terms: "plu 1234
190 sold yesterday".{" "}
191 <button
192 onClick={() => setShowHelp(true)}
193 className="text-brand hover:underline ml-2"
194 >
195 Quick help
196 </button>
197 </div>
198 </div>
199
200 {/* Search Interpretation Info */}
201 {searchInfo.length > 0 && (
202 <div className="bg-surface rounded-lg shadow p-4 mb-6 border-l-4 border-brand">
203 <p className="font-semibold text-text mb-2">Showing only:</p>
204 {searchInfo.map((info, idx) => (
205 <p
206 key={idx}
207 className={`ml-4 ${
208 info.IsError ? "text-red-600 font-bold" : "text-text"
209 }`}
210 >
211 {idx > 0 && !info.IsError && <span className="italic">and </span>}
212 {info.Text}
213 </p>
214 ))}
215 </div>
216 )}
217
218 {/* Loading State */}
219 {loading && (
220 <div className="bg-surface rounded-lg shadow p-8 text-center">
221 <div className="inline-flex items-center gap-2">
222 <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
223 <span className="text-text">Searching...</span>
224 </div>
225 </div>
226 )}
227
228 {/* Results Table */}
229 {!loading && data.length > 0 && (
230 <div className="bg-surface rounded-lg shadow overflow-hidden">
231 <div className="overflow-x-auto">
232 <table className="min-w-full divide-y divide-border">
233 <thead className="bg-surface-2">
234 <tr>
235 <th
236 onClick={() => handleSort("CompletedDt")}
237 className="px-4 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider cursor-pointer hover:bg-surface-2/80"
238 >
239 Date
240 <SortIndicator column="CompletedDt" />
241 </th>
242 <th
243 onClick={() => handleSort("LocationName")}
244 className="px-4 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider cursor-pointer hover:bg-surface-2/80"
245 >
246 Store
247 <SortIndicator column="LocationName" />
248 </th>
249 <th
250 onClick={() => handleSort("Sid")}
251 className="px-4 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider cursor-pointer hover:bg-surface-2/80"
252 >
253 Sale#
254 <SortIndicator column="Sid" />
255 </th>
256 <th
257 onClick={() => handleSort("Total")}
258 className="px-4 py-3 text-right text-xs font-medium text-muted uppercase tracking-wider cursor-pointer hover:bg-surface-2/80"
259 >
260 Total Sale
261 <SortIndicator column="Total" />
262 </th>
263 <th
264 onClick={() => handleSort("TellerName")}
265 className="px-4 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider cursor-pointer hover:bg-surface-2/80"
266 >
267 Teller
268 <SortIndicator column="TellerName" />
269 </th>
270 <th
271 onClick={() => handleSort("f2117")}
272 className="px-4 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider cursor-pointer hover:bg-surface-2/80"
273 >
274 Customer
275 <SortIndicator column="f2117" />
276 </th>
277 </tr>
278 </thead>
279 <tbody className="bg-surface divide-y divide-border">
280 {sortedData.map((row, idx) => (
281 <tr
282 key={`${row.Sid}-${idx}`}
283 className="hover:bg-surface-2"
284 >
285 <td className="px-4 py-3 text-sm text-text whitespace-nowrap">
286 {formatDateTime(row.CompletedDt)}
287 </td>
288 <td className="px-4 py-3 text-sm text-text">{row.LocationName}</td>
289 <td className="px-4 py-3 text-sm text-brand">
290 <a
291 href={`/report/pos/sales/fieldpine/SingleSale.htm?sid=${row.Sid}`}
292 className="hover:underline"
293 target="_blank"
294 rel="noopener noreferrer"
295 >
296 {row.Sid}
297 </a>
298 </td>
299 <td className="px-4 py-3 text-sm text-text text-right">
300 {formatCurrency(row.Total)}
301 </td>
302 <td className="px-4 py-3 text-sm text-text">{row.TellerName}</td>
303 <td className="px-4 py-3 text-sm text-brand">
304 {row.Cid ? (
305 <a
306 href={`/report/pos/Customer/Fieldpine/Customer_Single_Overview.htm?cid=${row.Cid}`}
307 className="hover:underline"
308 target="_blank"
309 rel="noopener noreferrer"
310 >
311 {row.f2117}
312 </a>
313 ) : (
314 row.f2117
315 )}
316 </td>
317 </tr>
318 ))}
319 </tbody>
320 </table>
321 </div>
322
323 {/* Footer */}
324 <div className="px-6 py-4 bg-surface-2 border-t border-border">
325 <p className="text-sm text-muted">
326 Showing {data.length} sale{data.length !== 1 ? "s" : ""}
327 </p>
328 </div>
329 </div>
330 )}
331
332 {/* No Results */}
333 {!loading && data.length === 0 && searchText.trim() && (
334 <div className="bg-surface rounded-lg shadow p-8 text-center text-muted">
335 No sales found matching "{searchText}". Try a different search term.
336 </div>
337 )}
338
339 {/* Help Modal */}
340 {showHelp && (
341 <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
342 <div className="bg-surface rounded-lg shadow-xl max-w-3xl max-h-[80vh] overflow-y-auto">
343 <div className="p-6">
344 <div className="flex justify-between items-start mb-4">
345 <h2 className="text-2xl font-bold text-text">Advanced Search Help</h2>
346 <button
347 onClick={() => setShowHelp(false)}
348 className="text-muted hover:text-text text-2xl"
349 >
350 ×
351 </button>
352 </div>
353
354 <div className="prose max-w-none">
355 <p className="text-text mb-4">
356 The search option on this screen works somewhat like a search engine and
357 attempts to understand what you are searching for. Rather than selecting
358 various selection controls you can simply type what you want.
359 </p>
360
361 <h3 className="text-lg font-semibold text-text mt-6 mb-3">Examples</h3>
362 <dl className="space-y-3">
363 <div>
364 <dt className="font-semibold text-text">sale 12345</dt>
365 <dd className="text-muted ml-4">
366 Only sales with the sale# 12345. Multiple sales can have the same
367 number.
368 </dd>
369 </div>
370 <div>
371 <dt className="font-semibold text-text">phase void</dt>
372 <dd className="text-muted ml-4">
373 Only show sales that were cancelled or voided
374 </dd>
375 </div>
376 <div>
377 <dt className="font-semibold text-text">voucher 104531</dt>
378 <dd className="text-muted ml-4">
379 Only show sales that referenced voucher# 104531
380 </dd>
381 </div>
382 <div>
383 <dt className="font-semibold text-text">plu 1234</dt>
384 <dd className="text-muted ml-4">
385 Only show sales that have the product(s) with PLU 1234
386 </dd>
387 </div>
388 <div>
389 <dt className="font-semibold text-text">price about 34.50</dt>
390 <dd className="text-muted ml-4">
391 Search for sales that are around $34.50 total price
392 </dd>
393 </div>
394 </dl>
395
396 <h3 className="text-lg font-semibold text-text mt-6 mb-3">Dates</h3>
397 <p className="text-text mb-2">
398 Dates can be entered in the following formats:
399 </p>
400 <ul className="list-disc list-inside text-muted space-y-1 ml-4">
401 <li>today</li>
402 <li>yesterday</li>
403 <li>last week</li>
404 <li>last month</li>
405 <li>this year</li>
406 </ul>
407
408 <h3 className="text-lg font-semibold text-text mt-6 mb-3">
409 Combining Searches
410 </h3>
411 <p className="text-text mb-2">
412 Simply type multiple criteria together:
413 </p>
414 <ul className="list-disc list-inside text-muted space-y-1 ml-4">
415 <li>sold this month plu 4567</li>
416 <li>phase void this month</li>
417 <li>phase void this year product 456</li>
418 </ul>
419
420 <p className="text-xs text-muted mt-6 pt-4 border-t border-border">
421 <strong>Doesn't work?</strong> Understanding english is quite hard for a
422 computer. If the text you enter isn't understood and you think it should
423 be, feel free to send the search text to support@fieldpine.com.
424 </p>
425 </div>
426 </div>
427 </div>
428 </div>
429 )}
430 </div>
431 </div>
432 );
433}