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';
5import { fieldpineApi } from "@/lib/client/fieldpineApi";
6
7interface Location {
8 f100: number;
9 f101: string;
10}
11
12interface Department {
13 f100: number;
14 f101: string;
15}
16
17interface Quotation {
18 f101: string; // Sale ID
19 f102: string; // Date
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 CUST?: any[]; // Customer details
31}
32
33export default function OpenQuotationsPage() {
34 const [locations, setLocations] = useState<Location[]>([]);
35 const [departments, setDepartments] = useState<Department[]>([]);
36 const [fromDate, setFromDate] = useState('');
37 const [toDate, setToDate] = useState(() => {
38 const d = new Date();
39 d.setDate(d.getDate() + 1);
40 return d.toISOString().split('T')[0];
41 });
42 const [selectedLocation, setSelectedLocation] = useState('0');
43 const [selectedDepartment, setSelectedDepartment] = useState('0');
44 const [maxRows, setMaxRows] = useState('200');
45 const [quotations, setQuotations] = useState<Quotation[]>([]);
46 const [loading, setLoading] = useState(false);
47 const [sortColumn, setSortColumn] = useState<keyof Quotation>('f102');
48 const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
49
50 useEffect(() => {
51 loadLocations();
52 loadDepartments();
53 }, []);
54
55 const loadLocations = async () => {
56 try {
57 const apiKey = sessionStorage.getItem("fieldpine_apikey");
58 if (!apiKey) return;
59
60 const response = await fieldpineApi({
61 endpoint: '/buck?3=retailmax.elink.locations&100=1',
62 apiKey
63 });
64
65 if (response.DATS) {
66 setLocations(response.DATS);
67 }
68 } catch (error) {
69 console.error('Error loading locations:', error);
70 }
71 };
72
73 const loadDepartments = async () => {
74 try {
75 const apiKey = sessionStorage.getItem("fieldpine_apikey");
76 if (!apiKey) return;
77
78 const response = await fieldpineApi({
79 endpoint: '/buck?3=retailmax.elink.departments',
80 apiKey
81 });
82
83 if (response.DATS) {
84 setDepartments(response.DATS);
85 }
86 } catch (error) {
87 console.error('Error loading departments:', error);
88 }
89 };
90
91 const loadQuotations = async () => {
92 setLoading(true);
93 try {
94 const apiKey = sessionStorage.getItem("fieldpine_apikey");
95 if (!apiKey) {
96 console.error("No API key found");
97 setLoading(false);
98 return;
99 }
100
101 let predicates = '';
102
103 // Date range (optional)
104 if (fromDate) {
105 const fromFormatted = formatDateForAPI(fromDate);
106 predicates += `&9=f102,4,${fromFormatted}`;
107 }
108 if (toDate) {
109 const toFormatted = formatDateForAPI(toDate);
110 predicates += `&9=f102,1,${toFormatted}`;
111 }
112
113 // Filter for quotations (f109 = 10001)
114 predicates += '&9=f109,0,10001';
115
116 // Add location filter
117 if (selectedLocation !== '0' && selectedLocation !== '') {
118 predicates += `&9=f105,0,${selectedLocation}`;
119 }
120
121 // Add department filter
122 if (selectedDepartment !== '0' && selectedDepartment !== '') {
123 predicates += `&9=f801,0,${selectedDepartment}`;
124 }
125
126 // Full product and customer packets
127 predicates += '&103=1&104=1';
128
129 // Add max rows limit
130 predicates += `&8=${maxRows}`;
131
132 const response = await fieldpineApi({
133 endpoint: `/buck?3=retailmax.elink.saleflat.list${predicates}`,
134 apiKey
135 });
136
137 if (response.APPT) {
138 setQuotations(response.APPT);
139 } else {
140 setQuotations([]);
141 }
142 } catch (error) {
143 console.error('Error loading quotations:', error);
144 setQuotations([]);
145 } finally {
146 setLoading(false);
147 }
148 };
149
150 const formatDateForAPI = (dateStr: string): string => {
151 const date = new Date(dateStr);
152 const day = date.getDate();
153 const month = date.getMonth() + 1;
154 const year = date.getFullYear();
155 return `${day}-${month}-${year}`;
156 };
157
158 const handleSearch = () => {
159 loadQuotations();
160 };
161
162 const formatDate = (dateStr?: string): string => {
163 if (!dateStr) return '';
164 const date = new Date(dateStr);
165 return date.toLocaleDateString('en-NZ', {
166 year: 'numeric',
167 month: 'short',
168 day: 'numeric'
169 });
170 };
171
172 const formatCurrency = (value?: number): string => {
173 if (!value) return '$0.00';
174 return new Intl.NumberFormat('en-NZ', {
175 style: 'currency',
176 currency: 'NZD'
177 }).format(value);
178 };
179
180 const handleSort = (column: keyof Quotation) => {
181 if (sortColumn === column) {
182 setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
183 } else {
184 setSortColumn(column);
185 setSortDirection('asc');
186 }
187 };
188
189 const sortedQuotations = [...quotations].sort((a, b) => {
190 const aVal = a[sortColumn];
191 const bVal = b[sortColumn];
192
193 if (aVal === undefined || bVal === undefined) return 0;
194
195 if (typeof aVal === 'number' && typeof bVal === 'number') {
196 return sortDirection === 'asc' ? aVal - bVal : bVal - aVal;
197 }
198
199 const aStr = String(aVal);
200 const bStr = String(bVal);
201 return sortDirection === 'asc'
202 ? aStr.localeCompare(bStr)
203 : bStr.localeCompare(aStr);
204 });
205
206 const SortIcon = ({ column }: { column: keyof Quotation }) => {
207 if (sortColumn !== column) return <span className="text-muted/50">⇅</span>;
208 return <span>{sortDirection === 'asc' ? '↑' : '↓'}</span>;
209 };
210
211 const getCustomerName = (quote: Quotation): string => {
212 if (quote.CUST && quote.CUST[0] && quote.CUST[0].f101) {
213 return quote.CUST[0].f101;
214 }
215 return '';
216 };
217
218 const handleViewQuote = (saleId: string) => {
219 // Open the quote PDF in a new window
220 window.open(`/OpenApi/DocumentRequest?template=121&output=pdf&saleid=${saleId}`, '_blank');
221 };
222
223 return (
224 <div className="p-6">
225 {/* Header */}
226 <div className="mb-6">
227 <div className="flex items-center justify-between">
228 <div>
229 <h1 className="text-3xl font-bold mb-2">Open Quotations 💬</h1>
230 <p className="text-sm text-muted mb-2">
231 View and manage open sales quotations
232 </p>
233 <div className="text-sm text-muted">
234 <a href="/pages/reports/sales-reports" className="text-brand hover:underline">Sales</a>
235 {' > '}
236 <span>Open Quotations</span>
237 </div>
238 </div>
239 <div>
240 <a
241 href="/report/pos/sales/fieldpine/EnterQuote.htm"
242 className="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700"
243 target="_blank"
244 rel="noopener noreferrer"
245 >
246 Enter New Quote
247 </a>
248 </div>
249 </div>
250 </div>
251
252 {/* Filters */}
253 <div className="bg-surface rounded-lg shadow p-6 mb-6">
254 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-4">
255 <div>
256 <label className="block text-sm font-medium text-text mb-2">
257 From Date (including):
258 </label>
259 <input
260 type="date"
261 value={fromDate}
262 onChange={(e) => setFromDate(e.target.value)}
263 className="w-full border rounded px-3 py-2"
264 />
265 </div>
266
267 <div>
268 <label className="block text-sm font-medium text-text mb-2">
269 To Date (excluding):
270 </label>
271 <input
272 type="date"
273 value={toDate}
274 onChange={(e) => setToDate(e.target.value)}
275 className="w-full border rounded px-3 py-2"
276 />
277 </div>
278
279 <div>
280 <label className="block text-sm font-medium text-text mb-2">
281 Store:
282 </label>
283 <select
284 value={selectedLocation}
285 onChange={(e) => setSelectedLocation(e.target.value)}
286 className="w-full border rounded px-3 py-2"
287 >
288 <option value="0">All Stores</option>
289 {locations.map(loc => (
290 <option key={loc.f100} value={loc.f100}>
291 {loc.f101}
292 </option>
293 ))}
294 </select>
295 </div>
296
297 <div>
298 <label className="block text-sm font-medium text-text mb-2">
299 Department:
300 </label>
301 <select
302 value={selectedDepartment}
303 onChange={(e) => setSelectedDepartment(e.target.value)}
304 className="w-full border rounded px-3 py-2"
305 >
306 <option value="0">--Select--</option>
307 {departments.map(dept => (
308 <option key={dept.f100} value={dept.f100}>
309 {dept.f101}
310 </option>
311 ))}
312 </select>
313 </div>
314
315 <div>
316 <label className="block text-sm font-medium text-text mb-2">
317 Max Rows:
318 </label>
319 <input
320 type="number"
321 value={maxRows}
322 onChange={(e) => setMaxRows(e.target.value)}
323 className="w-full border rounded px-3 py-2"
324 />
325 </div>
326
327 <div className="flex items-end">
328 <button
329 onClick={handleSearch}
330 disabled={loading}
331 className="w-full bg-brand text-white px-6 py-2 rounded hover:bg-brand2 disabled:bg-muted/50"
332 >
333 {loading ? 'Loading...' : 'Display'}
334 </button>
335 </div>
336 </div>
337 </div>
338
339 {/* Results Table */}
340 <div className="bg-surface rounded-lg shadow overflow-hidden">
341 {loading ? (
342 <div className="p-8 text-center">
343 <div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-info"></div>
344 <p className="mt-2 text-muted">Loading quotations...</p>
345 </div>
346 ) : (
347 <>
348 <div className="overflow-x-auto max-h-[600px] overflow-y-auto">
349 <table className="w-full text-sm">
350 <thead className="bg-surface-2 sticky top-0">
351 <tr>
352 <th
353 className="px-4 py-3 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
354 onClick={() => handleSort('f102')}
355 >
356 Date <SortIcon column="f102" />
357 </th>
358 <th
359 className="px-4 py-3 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
360 onClick={() => handleSort('f106')}
361 >
362 Store <SortIcon column="f106" />
363 </th>
364 <th
365 className="px-4 py-3 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
366 onClick={() => handleSort('f101')}
367 >
368 Sale# <SortIcon column="f101" />
369 </th>
370 <th
371 className="px-4 py-3 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
372 onClick={() => handleSort('f107')}
373 >
374 Total Sale <SortIcon column="f107" />
375 </th>
376 <th
377 className="px-4 py-3 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
378 onClick={() => handleSort('f104')}
379 >
380 Teller <SortIcon column="f104" />
381 </th>
382 <th
383 className="px-4 py-3 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
384 onClick={() => handleSort('f201')}
385 >
386 Product <SortIcon column="f201" />
387 </th>
388 <th
389 className="px-4 py-3 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
390 onClick={() => handleSort('f202')}
391 >
392 Qty <SortIcon column="f202" />
393 </th>
394 <th
395 className="px-4 py-3 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
396 onClick={() => handleSort('f203')}
397 >
398 Line Price <SortIcon column="f203" />
399 </th>
400 <th className="px-4 py-3 text-left font-semibold">
401 Customer Name
402 </th>
403 <th className="px-4 py-3 text-center font-semibold">
404 Options
405 </th>
406 </tr>
407 </thead>
408 <tbody className="divide-y divide-border">
409 {sortedQuotations.length === 0 ? (
410 <tr>
411 <td colSpan={10} className="px-4 py-8 text-center text-muted">
412 No open quotations found
413 </td>
414 </tr>
415 ) : (
416 sortedQuotations.map((quote, idx) => (
417 <tr key={idx} className="hover:bg-surface-2">
418 <td className="px-4 py-3 whitespace-nowrap">
419 {formatDate(quote.f102)}
420 </td>
421 <td className="px-4 py-3">
422 {quote.f106 || '-'}
423 </td>
424 <td className="px-4 py-3">
425 <a
426 href={`/report/pos/sales/fieldpine/SingleSale.htm?sid=${quote.f101}`}
427 className="text-brand hover:underline"
428 target="_blank"
429 rel="noopener noreferrer"
430 >
431 {quote.f101}
432 </a>
433 </td>
434 <td className="px-4 py-3 text-right font-semibold">
435 {formatCurrency(quote.f107)}
436 </td>
437 <td className="px-4 py-3">
438 {quote.f104 || '-'}
439 </td>
440 <td className="px-4 py-3">
441 {quote.f200 ? (
442 <a
443 href={`/report/pos/stock/fieldpine/singlepid.htm?pid=${quote.f200}`}
444 className="text-brand hover:underline"
445 target="_blank"
446 rel="noopener noreferrer"
447 >
448 {quote.f201 || quote.f200}
449 </a>
450 ) : (
451 quote.f201 || '-'
452 )}
453 </td>
454 <td className="px-4 py-3 text-right">
455 {quote.f202 || 0}
456 </td>
457 <td className="px-4 py-3 text-right">
458 {formatCurrency(quote.f203)}
459 </td>
460 <td className="px-4 py-3">
461 {getCustomerName(quote) || '-'}
462 </td>
463 <td className="px-4 py-3 text-center">
464 <button
465 onClick={() => handleViewQuote(quote.f101)}
466 className="bg-brand text-white px-3 py-1 rounded text-xs hover:bg-brand2"
467 >
468 View Quote
469 </button>
470 </td>
471 </tr>
472 ))
473 )}
474 </tbody>
475 </table>
476 </div>
477
478 {quotations.length > 0 && (
479 <div className="px-4 py-3 bg-surface-2 border-t text-sm text-muted">
480 Showing {quotations.length} open quotation{quotations.length !== 1 ? 's' : ''}
481 </div>
482 )}
483 </>
484 )}
485 </div>
486 </div>
487 );
488}