3import { useState, useEffect } from 'react';
4import { apiClient } from '@/lib/client/apiClient';
5import { fieldpineApi } from "@/lib/client/fieldpineApi";
18 f101: string; // Sale ID
20 f103?: string; // Teller ID
21 f104?: string; // Teller Name
22 f105?: string; // Location ID
23 f106?: string; // Store Name
24 f107?: number; // Total Sale
25 f108?: string; // Customer ID
26 f200?: number; // Product ID
27 f201?: string; // Product Name
28 f202?: number; // Quantity
29 f203?: number; // Line Price
30 f206?: number; // Price Cause Code
31 f208?: number; // Line Cost
32 f209?: number; // Gross Profit
33 f301?: string; // Payment Method
34 PROD?: any[]; // Product details
35 CUST?: any[]; // Customer details
38export default function LostSalesPage() {
39 const [locations, setLocations] = useState<Location[]>([]);
40 const [departments, setDepartments] = useState<Department[]>([]);
41 const [fromDate, setFromDate] = useState(() => {
43 d.setDate(1); // First of month
44 return d.toISOString().split('T')[0];
46 const [toDate, setToDate] = useState(() => {
48 d.setDate(d.getDate() + 1); // Tomorrow
49 return d.toISOString().split('T')[0];
51 const [selectedLocation, setSelectedLocation] = useState('0');
52 const [selectedDepartment, setSelectedDepartment] = useState('0');
53 const [maxRows, setMaxRows] = useState('200');
54 const [sales, setSales] = useState<LostSale[]>([]);
55 const [loading, setLoading] = useState(false);
56 const [sortColumn, setSortColumn] = useState<keyof LostSale>('f102');
57 const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
65 const loadLocations = async () => {
67 const apiKey = sessionStorage.getItem("fieldpine_apikey");
70 const response = await fieldpineApi({
71 endpoint: '/buck?3=retailmax.elink.locations&100=1',
76 setLocations(response.DATS);
79 console.error('Error loading locations:', error);
83 const loadDepartments = async () => {
85 const apiKey = sessionStorage.getItem("fieldpine_apikey");
88 const response = await fieldpineApi({
89 endpoint: '/buck?3=retailmax.elink.departments',
94 setDepartments(response.DATS);
97 console.error('Error loading departments:', error);
101 const loadLostSales = async () => {
104 const apiKey = sessionStorage.getItem("fieldpine_apikey");
106 console.error("No API key found");
114 const fromFormatted = formatDateForAPI(fromDate);
115 const toFormatted = formatDateForAPI(toDate);
116 predicates += `&9=f102,4,${fromFormatted}`;
117 predicates += `&9=f102,1,${toFormatted}`;
119 // Filter for lost sales (f214 in specific status codes)
120 predicates += '&9=f214,in,10000,10001,10002,128';
122 // Add location filter
123 if (selectedLocation !== '0' && selectedLocation !== '') {
124 predicates += `&9=f105,0,${selectedLocation}`;
127 // Add department filter
128 if (selectedDepartment !== '0' && selectedDepartment !== '') {
129 predicates += `&9=f801,0,${selectedDepartment}`;
132 // Full product and customer packets
133 predicates += '&103=1&104=1';
135 // Add max rows limit
136 predicates += `&8=${maxRows}`;
138 const response = await fieldpineApi({
139 endpoint: `/buck?3=retailmax.elink.saleflat.list${predicates}`,
144 setSales(response.APPT);
149 console.error('Error loading lost sales:', error);
156 const formatDateForAPI = (dateStr: string): string => {
157 const date = new Date(dateStr);
158 const day = date.getDate();
159 const month = date.getMonth() + 1;
160 const year = date.getFullYear();
161 return `${day}-${month}-${year}`;
164 const handleSearch = () => {
168 const getPriceCause = (code?: number): string => {
169 if (!code || code === 0) return '';
171 case 1: return 'SpecOffer';
172 case 2: return 'Reward';
173 case 3: return 'Combo';
174 case 4: return 'QtyDisc';
175 case 5: return 'StoreDisc';
176 case 6: return 'DiscManual';
177 case 7: return 'PriceMap';
178 case 8: return 'CustDisc';
179 case 9: return 'TellerAction';
180 case 11: return 'DiscVoucher';
181 default: return String(code);
185 const formatDate = (dateStr?: string): string => {
186 if (!dateStr) return '';
187 const date = new Date(dateStr);
188 return date.toLocaleDateString('en-NZ', {
195 const formatCurrency = (value?: number): string => {
196 if (!value) return '$0.00';
197 return new Intl.NumberFormat('en-NZ', {
203 const handleSort = (column: keyof LostSale) => {
204 if (sortColumn === column) {
205 setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
207 setSortColumn(column);
208 setSortDirection('asc');
212 const sortedSales = [...sales].sort((a, b) => {
213 const aVal = a[sortColumn];
214 const bVal = b[sortColumn];
216 if (aVal === undefined || bVal === undefined) return 0;
218 if (typeof aVal === 'number' && typeof bVal === 'number') {
219 return sortDirection === 'asc' ? aVal - bVal : bVal - aVal;
222 const aStr = String(aVal);
223 const bStr = String(bVal);
224 return sortDirection === 'asc'
225 ? aStr.localeCompare(bStr)
226 : bStr.localeCompare(aStr);
229 const SortIcon = ({ column }: { column: keyof LostSale }) => {
230 if (sortColumn !== column) return <span className="text-muted/50">⇅</span>;
231 return <span>{sortDirection === 'asc' ? '↑' : '↓'}</span>;
234 const getCustomerName = (sale: LostSale): string => {
235 if (sale.CUST && sale.CUST[0] && sale.CUST[0].f101) {
236 return sale.CUST[0].f101;
242 <div className="p-6">
244 <div className="mb-6">
245 <h1 className="text-3xl font-bold mb-2">Lost Sales 📉</h1>
246 <p className="text-sm text-muted mb-2">
247 Details of items that were ordered but unfulfilled when the order was shipped. Applies only to stores that use sales picking.
249 <div className="text-sm text-muted">
250 <a href="/pages/reports/sales-reports" className="text-brand hover:underline">Sales</a>
252 <span>Lost Sales</span>
257 <div className="bg-surface rounded-lg shadow p-6 mb-6">
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-2">
261 From Date (including):
266 onChange={(e) => setFromDate(e.target.value)}
267 className="w-full border rounded px-3 py-2"
272 <label className="block text-sm font-medium text-text mb-2">
278 onChange={(e) => setToDate(e.target.value)}
279 className="w-full border rounded px-3 py-2"
284 <label className="block text-sm font-medium text-text mb-2">
288 value={selectedLocation}
289 onChange={(e) => setSelectedLocation(e.target.value)}
290 className="w-full border rounded px-3 py-2"
292 <option value="0">All Stores</option>
293 {locations.map(loc => (
294 <option key={loc.f100} value={loc.f100}>
302 <label className="block text-sm font-medium text-text mb-2">
306 value={selectedDepartment}
307 onChange={(e) => setSelectedDepartment(e.target.value)}
308 className="w-full border rounded px-3 py-2"
310 <option value="0">--Select--</option>
311 {departments.map(dept => (
312 <option key={dept.f100} value={dept.f100}>
320 <label className="block text-sm font-medium text-text mb-2">
326 onChange={(e) => setMaxRows(e.target.value)}
327 className="w-full border rounded px-3 py-2"
331 <div className="flex items-end">
333 onClick={handleSearch}
335 className="w-full bg-brand text-white px-6 py-2 rounded hover:bg-brand2 disabled:bg-muted/50"
337 {loading ? 'Loading...' : 'Display'}
343 {/* Results Table */}
344 <div className="bg-surface rounded-lg shadow overflow-hidden">
346 <div className="p-8 text-center">
347 <div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-info"></div>
348 <p className="mt-2 text-muted">Loading lost sales...</p>
352 <div className="overflow-x-auto max-h-[600px] overflow-y-auto">
353 <table className="w-full text-sm">
354 <thead className="bg-surface-2 sticky top-0">
357 className="px-4 py-3 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
358 onClick={() => handleSort('f102')}
360 Date <SortIcon column="f102" />
363 className="px-4 py-3 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
364 onClick={() => handleSort('f106')}
366 Store <SortIcon column="f106" />
369 className="px-4 py-3 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
370 onClick={() => handleSort('f101')}
372 Sale# <SortIcon column="f101" />
375 className="px-4 py-3 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
376 onClick={() => handleSort('f107')}
378 Total Sale <SortIcon column="f107" />
381 className="px-4 py-3 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
382 onClick={() => handleSort('f104')}
384 Teller <SortIcon column="f104" />
386 <th className="px-4 py-3 text-left font-semibold">
390 className="px-4 py-3 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
391 onClick={() => handleSort('f201')}
393 Product <SortIcon column="f201" />
396 className="px-4 py-3 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
397 onClick={() => handleSort('f202')}
399 Qty <SortIcon column="f202" />
402 className="px-4 py-3 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
403 onClick={() => handleSort('f203')}
405 Line Price <SortIcon column="f203" />
408 className="px-4 py-3 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
409 onClick={() => handleSort('f209')}
411 GP <SortIcon column="f209" />
413 <th className="px-4 py-3 text-center font-semibold">
418 <tbody className="divide-y divide-border">
419 {sortedSales.length === 0 ? (
421 <td colSpan={11} className="px-4 py-8 text-center text-muted">
426 sortedSales.map((sale, idx) => (
427 <tr key={idx} className="hover:bg-surface-2">
428 <td className="px-4 py-3 whitespace-nowrap">
429 {formatDate(sale.f102)}
431 <td className="px-4 py-3">
434 <td className="px-4 py-3">
436 href={`/report/pos/sales/fieldpine/SingleSale.htm?sid=${sale.f101}`}
437 className="text-brand hover:underline"
439 rel="noopener noreferrer"
444 <td className="px-4 py-3 text-right font-semibold">
445 {formatCurrency(sale.f107)}
447 <td className="px-4 py-3">
450 <td className="px-4 py-3">
451 {getCustomerName(sale) || '-'}
453 <td className="px-4 py-3">
456 href={`/report/pos/stock/fieldpine/singlepid.htm?pid=${sale.f200}`}
457 className="text-brand hover:underline"
459 rel="noopener noreferrer"
461 {sale.f201 || sale.f200}
467 <td className="px-4 py-3 text-right">
470 <td className="px-4 py-3 text-right">
471 {formatCurrency(sale.f203)}
473 <td className="px-4 py-3 text-right">
474 {formatCurrency(sale.f209)}
476 <td className="px-4 py-3 text-center text-xs">
477 {getPriceCause(sale.f206)}
486 {sales.length > 0 && (
487 <div className="px-4 py-3 bg-surface-2 border-t text-sm text-muted">
488 Showing {sales.length} lost sale{sales.length !== 1 ? 's' : ''}