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 { Icon } from '@/contexts/IconContext';
5
6interface PickingSale {
7 f100: number; // Sale ID
8 f102: number; // Total
9 f109: string; // Date
10 f2001: number; // Age in days
11 f905: number; // Line count
12 f108: number; // Status
13 f7102?: number; // Assigned store
14 f7120?: string; // Picking comments
15 f7121?: number; // Row color
16 f7123?: string; // Customer comments
17 BIG1?: Array<{ f103: string }>; // Biggest item
18 DELI?: Array<{
19 f302?: string; // Notes
20 f303?: string; // Phone
21 f304?: string; // Name
22 f305?: string; // Email
23 f306?: string; // Click+Collect store
24 }>;
25 CUST?: Array<{
26 f101?: string; // Name
27 f112?: string; // Phone
28 f115?: string; // Email
29 Name?: string;
30 }>;
31}
32
33interface Location {
34 f100: number;
35 f101: string;
36}
37
38export default function SalesPickingPage() {
39 const [sales, setSales] = useState<PickingSale[]>([]);
40 const [locations, setLocations] = useState<Location[]>([]);
41 const [loading, setLoading] = useState(true);
42 const [searchTerm, setSearchTerm] = useState('');
43 const [colorFilter, setColorFilter] = useState('');
44 const [locationFilter, setLocationFilter] = useState('');
45 const [rowLimit, setRowLimit] = useState(400);
46
47 useEffect(() => {
48 loadLocations();
49 loadSales();
50 }, []);
51
52 const loadLocations = async () => {
53 try {
54 const response = await fetch('/api/v1/locations');
55 if (response.ok) {
56 const data = await response.json();
57 setLocations(data.data || []);
58 }
59 } catch (error) {
60 console.error('Failed to load locations:', error);
61 }
62 };
63
64 const loadSales = async () => {
65 setLoading(true);
66 try {
67 const response = await fetch(`/api/v1/sales/picking?limit=${rowLimit}`);
68 if (response.ok) {
69 const data = await response.json();
70 setSales(data.data || []);
71 }
72 } catch (error) {
73 console.error('Failed to load sales:', error);
74 } finally {
75 setLoading(false);
76 }
77 };
78
79 const updateSale = async (saleId: number, updates: Record<string, any>) => {
80 try {
81 await fetch(`/api/v1/sales/${saleId}`, {
82 method: 'PATCH',
83 headers: { 'Content-Type': 'application/json' },
84 body: JSON.stringify(updates)
85 });
86
87 // Update local state
88 setSales(prev => prev.map(s =>
89 s.f100 === saleId ? { ...s, ...updates } : s
90 ));
91 } catch (error) {
92 console.error('Failed to update sale:', error);
93 }
94 };
95
96 const getCustomerName = (sale: PickingSale) => {
97 return sale.DELI?.[0]?.f304 ||
98 sale.CUST?.[0]?.Name ||
99 sale.CUST?.[0]?.f101 ||
100 '';
101 };
102
103 const getPhone = (sale: PickingSale) => {
104 return sale.DELI?.[0]?.f303 || sale.CUST?.[0]?.f112 || '';
105 };
106
107 const getEmail = (sale: PickingSale) => {
108 return sale.DELI?.[0]?.f305 || sale.CUST?.[0]?.f115 || '';
109 };
110
111 const getNotes = (sale: PickingSale) => {
112 return sale.DELI?.[0]?.f302 || '';
113 };
114
115 const getRowColor = (colorCode?: number) => {
116 switch (colorCode) {
117 case 2: return '#00FF00'; // Lime
118 case 7: return '#FFFF00'; // Yellow
119 case 25: return '#8A2BE2'; // Blue Violet
120 case 64: return '#FF69B4'; // Hot Pink
121 case 131: return '#4682B4'; // Steel Blue
122 case 132: return '#D2B48C'; // Tan
123 default: return '';
124 }
125 };
126
127 const formatDate = (dateStr: string | undefined) => {
128 if (!dateStr) return '';
129 try {
130 // Fieldpine returns dates like "2025|12|24|16|5|3||"
131 if (dateStr.includes('|')) {
132 const parts = dateStr.split('|');
133 const year = parseInt(parts[0]);
134 const month = parseInt(parts[1]);
135 const day = parseInt(parts[2]);
136 const hour = parseInt(parts[3] || '0');
137 const minute = parseInt(parts[4] || '0');
138
139 const date = new Date(year, month - 1, day, hour, minute);
140 const monthName = date.toLocaleDateString('en-US', { month: 'short' });
141 const timeStr = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
142
143 return `${day}-${monthName}-${year} ${timeStr}`;
144 }
145
146 // Fallback for standard date formats
147 const date = new Date(dateStr);
148 if (isNaN(date.getTime())) return dateStr;
149
150 const day = date.getDate();
151 const month = date.toLocaleDateString('en-US', { month: 'short' });
152 const year = date.getFullYear();
153 const time = date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
154
155 return `${day}-${month}-${year} ${time}`;
156 } catch {
157 return dateStr || '';
158 }
159 };
160
161 const filteredSales = sales.filter(sale => {
162 // Search filter
163 if (searchTerm) {
164 const term = searchTerm.toLowerCase();
165 const matches =
166 String(sale.f100).includes(term) ||
167 getCustomerName(sale).toLowerCase().includes(term) ||
168 getNotes(sale).toLowerCase().includes(term);
169 if (!matches) return false;
170 }
171
172 // Color filter
173 if (colorFilter) {
174 const cfn = Number(colorFilter);
175 const hcolor = sale.f7121 || 0;
176 if (cfn === -1 && hcolor > 0) return false; // No set color
177 if (cfn === -2 && hcolor <= 0) return false; // Any set color
178 if (cfn > 0 && hcolor !== cfn) return false;
179 }
180
181 // Location filter
182 if (locationFilter) {
183 const locId = Number(locationFilter);
184 const cLoc = sale.f7102 || 0;
185 if (locId === -1 && cLoc > 0) return false; // Not assigned
186 if (locId > 0 && cLoc !== locId) return false;
187 }
188
189 return true;
190 });
191
192 if (loading) {
193 return (
194 <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
195 <div className="text-center py-12">
196 <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-brand mx-auto"></div>
197 <p className="mt-4 text-muted">Loading sales awaiting picking...</p>
198 </div>
199 </div>
200 );
201 }
202
203 return (
204 <div className="max-w-full mx-auto px-4 sm:px-6 lg:px-8 py-8">
205 <div className="mb-8">
206 <h1 className="text-3xl font-bold text-text">Sales Picking</h1>
207 <p className="mt-2 text-muted">
208 Sales that have been received and are awaiting stock picking and shipping.
209 </p>
210 </div>
211
212 {/* Filters */}
213 <div className="mb-6 bg-surface rounded-lg shadow p-4">
214 <div className="grid grid-cols-1 md:grid-cols-5 gap-4">
215 <div>
216 <label className="block text-sm font-medium text-text mb-1">Rows</label>
217 <div className="flex gap-2">
218 <input
219 type="number"
220 value={rowLimit}
221 onChange={(e) => setRowLimit(Number(e.target.value))}
222 className="block w-20 rounded-md border-border shadow-sm focus:border-brand focus:ring-brand sm:text-sm"
223 />
224 <button
225 onClick={loadSales}
226 className="px-4 py-2 bg-brand text-white rounded-md hover:opacity-90 transition-all"
227 >
228 Load
229 </button>
230 </div>
231 </div>
232
233 <div>
234 <label className="block text-sm font-medium text-text mb-1">Search</label>
235 <input
236 type="text"
237 placeholder="Sale #, customer, notes..."
238 value={searchTerm}
239 onChange={(e) => setSearchTerm(e.target.value)}
240 className="block w-full rounded-md border-border shadow-sm focus:border-brand focus:ring-brand sm:text-sm"
241 />
242 </div>
243
244 <div>
245 <label className="block text-sm font-medium text-text mb-1">Color</label>
246 <select
247 value={colorFilter}
248 onChange={(e) => setColorFilter(e.target.value)}
249 className="block w-full rounded-md border-border shadow-sm focus:border-brand focus:ring-brand sm:text-sm"
250 >
251 <option value="">Any/all</option>
252 <option value="-1">No set color</option>
253 <option value="-2">Any set color</option>
254 <option value="64" style={{ backgroundColor: '#FF69B4' }}>Hot Pink</option>
255 <option value="7" style={{ backgroundColor: '#FFFF00' }}>Yellow</option>
256 <option value="2" style={{ backgroundColor: '#00FF00' }}>Lime</option>
257 <option value="25" style={{ backgroundColor: '#8A2BE2' }}>Blue Violet</option>
258 <option value="131" style={{ backgroundColor: '#4682B4' }}>Steel Blue</option>
259 <option value="132" style={{ backgroundColor: '#D2B48C' }}>Tan</option>
260 </select>
261 </div>
262
263 <div>
264 <label className="block text-sm font-medium text-text mb-1">Store</label>
265 <select
266 value={locationFilter}
267 onChange={(e) => setLocationFilter(e.target.value)}
268 className="block w-full rounded-md border-border shadow-sm focus:border-brand focus:ring-brand sm:text-sm"
269 >
270 <option value="">Any/All</option>
271 <option value="-1">Not Assigned</option>
272 {locations.map(loc => (
273 <option key={loc.f100} value={loc.f100}>{loc.f101}</option>
274 ))}
275 </select>
276 </div>
277
278 <div className="flex items-end">
279 <div className="text-sm text-muted">
280 Displaying {filteredSales.length} of {sales.length} sales
281 </div>
282 </div>
283 </div>
284 </div>
285
286 {/* Sales Table */}
287 <div className="bg-surface rounded-lg shadow overflow-x-auto">
288 <table className="min-w-full divide-y divide-border">
289 <thead className="bg-[var(--brand)] text-surface">
290 <tr>
291 <th className="px-3 py-3 text-left text-xs font-medium uppercase tracking-wider">Date</th>
292 <th className="px-3 py-3 text-left text-xs font-medium uppercase tracking-wider">Age</th>
293 <th className="px-3 py-3 text-left text-xs font-medium uppercase tracking-wider">Sale#</th>
294 <th className="px-3 py-3 text-left text-xs font-medium uppercase tracking-wider">Total</th>
295 <th className="px-3 py-3 text-left text-xs font-medium uppercase tracking-wider">Lines</th>
296 <th className="px-3 py-3 text-left text-xs font-medium uppercase tracking-wider">Store</th>
297 <th className="px-3 py-3 text-left text-xs font-medium uppercase tracking-wider">Comments</th>
298 <th className="px-3 py-3 text-left text-xs font-medium uppercase tracking-wider">Customer</th>
299 <th className="px-3 py-3 text-left text-xs font-medium uppercase tracking-wider">Phone</th>
300 <th className="px-3 py-3 text-left text-xs font-medium uppercase tracking-wider">Email</th>
301 <th className="px-3 py-3 text-left text-xs font-medium uppercase tracking-wider">Notes</th>
302 </tr>
303 </thead>
304 <tbody className="bg-surface divide-y divide-border">
305 {filteredSales.map((sale, idx) => (
306 <tr
307 key={sale.f100}
308 style={{ backgroundColor: getRowColor(sale.f7121) || (idx % 2 === 0 ? 'var(--surface)' : 'var(--surface-2)') }}
309 >
310 <td className="px-3 py-2 whitespace-nowrap text-sm text-text">
311 {formatDate(sale.f109)}
312 </td>
313 <td className="px-3 py-2 whitespace-nowrap text-sm text-text">
314 {sale.f2001?.toFixed(1) || '0.0'}
315 </td>
316 <td className="px-3 py-2 whitespace-nowrap text-sm">
317 <a
318 href={`/pages/sales/${sale.f100}`}
319 className="text-brand hover:underline"
320 >
321 {sale.f100}
322 </a>
323 </td>
324 <td className="px-3 py-2 whitespace-nowrap text-sm text-text">
325 ${sale.f102?.toFixed(2)}
326 </td>
327 <td className="px-3 py-2 whitespace-nowrap text-sm text-text">
328 {sale.f905}
329 </td>
330 <td className="px-3 py-2 text-sm">
331 <select
332 value={sale.f7102 || 0}
333 onChange={(e) => updateSale(sale.f100, { f7102: Number(e.target.value) })}
334 className="block w-full rounded border-border text-sm focus:border-brand focus:ring-brand"
335 >
336 <option value="0">Not Assigned</option>
337 {locations.map(loc => (
338 <option key={loc.f100} value={loc.f100}>{loc.f101}</option>
339 ))}
340 </select>
341 </td>
342 <td className="px-3 py-2 text-sm">
343 <input
344 type="text"
345 value={sale.f7120 || ''}
346 onChange={(e) => updateSale(sale.f100, { f7120: e.target.value })}
347 className="block w-full rounded border-border text-sm focus:border-brand focus:ring-brand"
348 placeholder="Pick comments..."
349 />
350 </td>
351 <td className="px-3 py-2 text-sm text-text">
352 {getCustomerName(sale)}
353 </td>
354 <td className="px-3 py-2 text-sm text-text">
355 {getPhone(sale) && (
356 <a href={`tel:${getPhone(sale)}`} className="text-brand hover:underline">
357 {getPhone(sale)}
358 </a>
359 )}
360 </td>
361 <td className="px-3 py-2 text-sm text-text">
362 {getEmail(sale) && (
363 <a href={`mailto:${getEmail(sale)}`} className="text-brand hover:underline">
364 {getEmail(sale)}
365 </a>
366 )}
367 </td>
368 <td className="px-3 py-2 text-sm text-text max-w-xs truncate" title={getNotes(sale)}>
369 {getNotes(sale)}
370 </td>
371 </tr>
372 ))}
373 </tbody>
374 </table>
375
376 {filteredSales.length === 0 && (
377 <div className="text-center py-12 text-muted">
378 No sales found matching your filters.
379 </div>
380 )}
381 </div>
382 </div>
383 );
384}