3import { useState, useEffect, useRef } from "react";
4import { apiClient } from "@/lib/client/apiClient";
12 f2117?: string; // Customer name
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);
32 const [sortColumn, setSortColumn] = useState<keyof Sale>("CompletedDt");
33 const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
35 // Trigger initial search
40 const handleSearchChange = (value: string) => {
44 if (searchTimeoutRef.current) {
45 clearTimeout(searchTimeoutRef.current);
48 searchTimeoutRef.current = setTimeout(() => {
53 const handleSearch = async (query?: string) => {
54 const searchQuery = query !== undefined ? query : searchText;
56 if (!searchQuery.trim()) {
63 sequenceRef.current++;
64 const currentSequence = sequenceRef.current;
67 const response = await apiClient.searchSales({
71 inputMethod: 'keyboard'
74 if (!response.success) {
75 console.error("Error searching sales:", response.error);
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
88 if (response.data?.Sale_Read && Array.isArray(response.data.Sale_Read)) {
89 setData(response.data.Sale_Read);
94 // Handle search interpretation info
95 if (response.data?.ASTX && Array.isArray(response.data.ASTX)) {
96 setSearchInfo(response.data.ASTX);
101 console.error("Error searching sales:", error);
109 const handleSort = (column: keyof Sale) => {
110 if (sortColumn === column) {
111 setSortDirection(sortDirection === "asc" ? "desc" : "asc");
113 setSortColumn(column);
114 setSortDirection("asc");
118 const sortedData = [...data].sort((a, b) => {
119 const aVal = a[sortColumn];
120 const bVal = b[sortColumn];
122 if (aVal === undefined || bVal === undefined) return 0;
124 if (typeof aVal === "number" && typeof bVal === "number") {
125 return sortDirection === "asc" ? aVal - bVal : bVal - aVal;
128 const aStr = String(aVal);
129 const bStr = String(bVal);
130 return sortDirection === "asc"
131 ? aStr.localeCompare(bStr)
132 : bStr.localeCompare(aStr);
135 const SortIndicator = ({ column }: { column: keyof Sale }) => {
136 if (sortColumn !== column) return null;
137 return <span className="ml-1">{sortDirection === "asc" ? "▲" : "▼"}</span>;
140 const formatCurrency = (value: number | undefined) => {
141 if (value === undefined || value === null) return "";
142 return new Intl.NumberFormat("en-US", {
145 minimumFractionDigits: 2,
149 const formatDateTime = (dateStr: string | undefined) => {
150 if (!dateStr) return "";
152 const date = new Date(dateStr);
153 return date.toLocaleString();
160 <div className="min-h-screen bg-bg p-6">
161 <div className="max-w-[1600px] mx-auto">
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.
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">
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"
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".{" "}
192 onClick={() => setShowHelp(true)}
193 className="text-brand hover:underline ml-2"
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) => (
208 info.IsError ? "text-red-600 font-bold" : "text-text"
211 {idx > 0 && !info.IsError && <span className="italic">and </span>}
218 {/* Loading State */}
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>
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">
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"
240 <SortIndicator column="CompletedDt" />
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"
247 <SortIndicator column="LocationName" />
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"
254 <SortIndicator column="Sid" />
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"
261 <SortIndicator column="Total" />
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"
268 <SortIndicator column="TellerName" />
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"
275 <SortIndicator column="f2117" />
279 <tbody className="bg-surface divide-y divide-border">
280 {sortedData.map((row, idx) => (
282 key={`${row.Sid}-${idx}`}
283 className="hover:bg-surface-2"
285 <td className="px-4 py-3 text-sm text-text whitespace-nowrap">
286 {formatDateTime(row.CompletedDt)}
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">
291 href={`/report/pos/sales/fieldpine/SingleSale.htm?sid=${row.Sid}`}
292 className="hover:underline"
294 rel="noopener noreferrer"
299 <td className="px-4 py-3 text-sm text-text text-right">
300 {formatCurrency(row.Total)}
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">
306 href={`/report/pos/Customer/Fieldpine/Customer_Single_Overview.htm?cid=${row.Cid}`}
307 className="hover:underline"
309 rel="noopener noreferrer"
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" : ""}
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.
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>
347 onClick={() => setShowHelp(false)}
348 className="text-muted hover:text-text text-2xl"
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.
361 <h3 className="text-lg font-semibold text-text mt-6 mb-3">Examples</h3>
362 <dl className="space-y-3">
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
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
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
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
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
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:
400 <ul className="list-disc list-inside text-muted space-y-1 ml-4">
408 <h3 className="text-lg font-semibold text-text mt-6 mb-3">
411 <p className="text-text mb-2">
412 Simply type multiple criteria together:
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>
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.