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 CUST?: any[]; // Customer details
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(() => {
39 d.setDate(d.getDate() + 1);
40 return d.toISOString().split('T')[0];
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');
55 const loadLocations = async () => {
57 const apiKey = sessionStorage.getItem("fieldpine_apikey");
60 const response = await fieldpineApi({
61 endpoint: '/buck?3=retailmax.elink.locations&100=1',
66 setLocations(response.DATS);
69 console.error('Error loading locations:', error);
73 const loadDepartments = async () => {
75 const apiKey = sessionStorage.getItem("fieldpine_apikey");
78 const response = await fieldpineApi({
79 endpoint: '/buck?3=retailmax.elink.departments',
84 setDepartments(response.DATS);
87 console.error('Error loading departments:', error);
91 const loadQuotations = async () => {
94 const apiKey = sessionStorage.getItem("fieldpine_apikey");
96 console.error("No API key found");
103 // Date range (optional)
105 const fromFormatted = formatDateForAPI(fromDate);
106 predicates += `&9=f102,4,${fromFormatted}`;
109 const toFormatted = formatDateForAPI(toDate);
110 predicates += `&9=f102,1,${toFormatted}`;
113 // Filter for quotations (f109 = 10001)
114 predicates += '&9=f109,0,10001';
116 // Add location filter
117 if (selectedLocation !== '0' && selectedLocation !== '') {
118 predicates += `&9=f105,0,${selectedLocation}`;
121 // Add department filter
122 if (selectedDepartment !== '0' && selectedDepartment !== '') {
123 predicates += `&9=f801,0,${selectedDepartment}`;
126 // Full product and customer packets
127 predicates += '&103=1&104=1';
129 // Add max rows limit
130 predicates += `&8=${maxRows}`;
132 const response = await fieldpineApi({
133 endpoint: `/buck?3=retailmax.elink.saleflat.list${predicates}`,
138 setQuotations(response.APPT);
143 console.error('Error loading quotations:', error);
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}`;
158 const handleSearch = () => {
162 const formatDate = (dateStr?: string): string => {
163 if (!dateStr) return '';
164 const date = new Date(dateStr);
165 return date.toLocaleDateString('en-NZ', {
172 const formatCurrency = (value?: number): string => {
173 if (!value) return '$0.00';
174 return new Intl.NumberFormat('en-NZ', {
180 const handleSort = (column: keyof Quotation) => {
181 if (sortColumn === column) {
182 setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
184 setSortColumn(column);
185 setSortDirection('asc');
189 const sortedQuotations = [...quotations].sort((a, b) => {
190 const aVal = a[sortColumn];
191 const bVal = b[sortColumn];
193 if (aVal === undefined || bVal === undefined) return 0;
195 if (typeof aVal === 'number' && typeof bVal === 'number') {
196 return sortDirection === 'asc' ? aVal - bVal : bVal - aVal;
199 const aStr = String(aVal);
200 const bStr = String(bVal);
201 return sortDirection === 'asc'
202 ? aStr.localeCompare(bStr)
203 : bStr.localeCompare(aStr);
206 const SortIcon = ({ column }: { column: keyof Quotation }) => {
207 if (sortColumn !== column) return <span className="text-muted/50">⇅</span>;
208 return <span>{sortDirection === 'asc' ? '↑' : '↓'}</span>;
211 const getCustomerName = (quote: Quotation): string => {
212 if (quote.CUST && quote.CUST[0] && quote.CUST[0].f101) {
213 return quote.CUST[0].f101;
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');
224 <div className="p-6">
226 <div className="mb-6">
227 <div className="flex items-center justify-between">
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
233 <div className="text-sm text-muted">
234 <a href="/pages/reports/sales-reports" className="text-brand hover:underline">Sales</a>
236 <span>Open Quotations</span>
241 href="/report/pos/sales/fieldpine/EnterQuote.htm"
242 className="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700"
244 rel="noopener noreferrer"
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">
256 <label className="block text-sm font-medium text-text mb-2">
257 From Date (including):
262 onChange={(e) => setFromDate(e.target.value)}
263 className="w-full border rounded px-3 py-2"
268 <label className="block text-sm font-medium text-text mb-2">
274 onChange={(e) => setToDate(e.target.value)}
275 className="w-full border rounded px-3 py-2"
280 <label className="block text-sm font-medium text-text mb-2">
284 value={selectedLocation}
285 onChange={(e) => setSelectedLocation(e.target.value)}
286 className="w-full border rounded px-3 py-2"
288 <option value="0">All Stores</option>
289 {locations.map(loc => (
290 <option key={loc.f100} value={loc.f100}>
298 <label className="block text-sm font-medium text-text mb-2">
302 value={selectedDepartment}
303 onChange={(e) => setSelectedDepartment(e.target.value)}
304 className="w-full border rounded px-3 py-2"
306 <option value="0">--Select--</option>
307 {departments.map(dept => (
308 <option key={dept.f100} value={dept.f100}>
316 <label className="block text-sm font-medium text-text mb-2">
322 onChange={(e) => setMaxRows(e.target.value)}
323 className="w-full border rounded px-3 py-2"
327 <div className="flex items-end">
329 onClick={handleSearch}
331 className="w-full bg-brand text-white px-6 py-2 rounded hover:bg-brand2 disabled:bg-muted/50"
333 {loading ? 'Loading...' : 'Display'}
339 {/* Results Table */}
340 <div className="bg-surface rounded-lg shadow overflow-hidden">
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>
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">
353 className="px-4 py-3 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
354 onClick={() => handleSort('f102')}
356 Date <SortIcon column="f102" />
359 className="px-4 py-3 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
360 onClick={() => handleSort('f106')}
362 Store <SortIcon column="f106" />
365 className="px-4 py-3 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
366 onClick={() => handleSort('f101')}
368 Sale# <SortIcon column="f101" />
371 className="px-4 py-3 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
372 onClick={() => handleSort('f107')}
374 Total Sale <SortIcon column="f107" />
377 className="px-4 py-3 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
378 onClick={() => handleSort('f104')}
380 Teller <SortIcon column="f104" />
383 className="px-4 py-3 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
384 onClick={() => handleSort('f201')}
386 Product <SortIcon column="f201" />
389 className="px-4 py-3 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
390 onClick={() => handleSort('f202')}
392 Qty <SortIcon column="f202" />
395 className="px-4 py-3 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
396 onClick={() => handleSort('f203')}
398 Line Price <SortIcon column="f203" />
400 <th className="px-4 py-3 text-left font-semibold">
403 <th className="px-4 py-3 text-center font-semibold">
408 <tbody className="divide-y divide-border">
409 {sortedQuotations.length === 0 ? (
411 <td colSpan={10} className="px-4 py-8 text-center text-muted">
412 No open quotations found
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)}
421 <td className="px-4 py-3">
424 <td className="px-4 py-3">
426 href={`/report/pos/sales/fieldpine/SingleSale.htm?sid=${quote.f101}`}
427 className="text-brand hover:underline"
429 rel="noopener noreferrer"
434 <td className="px-4 py-3 text-right font-semibold">
435 {formatCurrency(quote.f107)}
437 <td className="px-4 py-3">
440 <td className="px-4 py-3">
443 href={`/report/pos/stock/fieldpine/singlepid.htm?pid=${quote.f200}`}
444 className="text-brand hover:underline"
446 rel="noopener noreferrer"
448 {quote.f201 || quote.f200}
454 <td className="px-4 py-3 text-right">
457 <td className="px-4 py-3 text-right">
458 {formatCurrency(quote.f203)}
460 <td className="px-4 py-3">
461 {getCustomerName(quote) || '-'}
463 <td className="px-4 py-3 text-center">
465 onClick={() => handleViewQuote(quote.f101)}
466 className="bg-brand text-white px-3 py-1 rounded text-xs hover:bg-brand2"
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' : ''}