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, Suspense } from 'react';
4import { useSearchParams, useRouter } from 'next/navigation';
5
6interface POLineItem {
7 f1100: string; // Line ID
8 f1101: string; // Product ID
9 f1102: string; // Description
10 f1103: number; // Quantity Ordered
11 f1104: number; // Quantity Received
12 f1105: number; // Unit Cost
13 f1106: number; // Line Total
14 f1107: string; // Product Code
15 f1108: number; // Quantity Outstanding
16}
17
18interface PurchaseOrder {
19 f100: string; // PO ID
20 f101: string; // Supplier ID
21 f102: string; // Supplier Name
22 f103: string; // PO Date
23 f104: number; // Total Amount
24 f114: number; // Status
25 f132: string; // Notes
26 physkey: string;
27 lines: POLineItem[];
28}
29
30function ReceivePurchaseOrderContent() {
31 const searchParams = useSearchParams();
32 const router = useRouter();
33 const poId = searchParams.get('poid');
34 const physKey = searchParams.get('physkey');
35
36 const [purchaseOrder, setPurchaseOrder] = useState<PurchaseOrder | null>(null);
37 const [lineItems, setLineItems] = useState<POLineItem[]>([]);
38 const [receivingQuantities, setReceivingQuantities] = useState<Record<string, number>>({});
39 const [receiveDate, setReceiveDate] = useState(new Date().toISOString().split('T')[0]);
40 const [notes, setNotes] = useState('');
41 const [showScanMode, setShowScanMode] = useState(false);
42 const [scanInput, setScanInput] = useState('');
43 const [isLoading, setIsLoading] = useState(true);
44
45 useEffect(() => {
46 if (poId && physKey) {
47 loadPurchaseOrder();
48 }
49 }, [poId, physKey]);
50
51 const loadPurchaseOrder = async () => {
52 setIsLoading(true);
53 try {
54 // Mock data - replace with actual API call
55 const mockPO: PurchaseOrder = {
56 f100: poId || '',
57 f101: '1001',
58 f102: 'ABC Suppliers Ltd',
59 f103: '2025-12-15',
60 f104: 15240.50,
61 f114: 2, // Approved
62 f132: 'Urgent delivery required',
63 physkey: physKey || '',
64 lines: [
65 {
66 f1100: '1',
67 f1101: 'PROD001',
68 f1102: 'Premium Coffee Beans 1kg',
69 f1103: 50,
70 f1104: 0,
71 f1105: 25.50,
72 f1106: 1275.00,
73 f1107: 'COF-001',
74 f1108: 50
75 },
76 {
77 f1100: '2',
78 f1101: 'PROD002',
79 f1102: 'Organic Tea Selection Box',
80 f1103: 30,
81 f1104: 15,
82 f1105: 18.75,
83 f1106: 562.50,
84 f1107: 'TEA-002',
85 f1108: 15
86 },
87 {
88 f1100: '3',
89 f1101: 'PROD003',
90 f1102: 'Stainless Steel Teapot',
91 f1103: 20,
92 f1104: 0,
93 f1105: 42.00,
94 f1106: 840.00,
95 f1107: 'POT-003',
96 f1108: 20
97 },
98 {
99 f1100: '4',
100 f1101: 'PROD004',
101 f1102: 'Coffee Grinder Professional',
102 f1103: 10,
103 f1104: 0,
104 f1105: 156.30,
105 f1106: 1563.00,
106 f1107: 'GRN-004',
107 f1108: 10
108 },
109 {
110 f1100: '5',
111 f1101: 'PROD005',
112 f1102: 'Paper Cups 100pk',
113 f1103: 100,
114 f1104: 50,
115 f1105: 8.90,
116 f1106: 890.00,
117 f1107: 'CUP-005',
118 f1108: 50
119 }
120 ]
121 };
122
123 setPurchaseOrder(mockPO);
124 setLineItems(mockPO.lines);
125
126 // Initialize receiving quantities with outstanding amounts
127 const initialQuantities: Record<string, number> = {};
128 mockPO.lines.forEach(line => {
129 initialQuantities[line.f1100] = line.f1108; // Set to outstanding quantity
130 });
131 setReceivingQuantities(initialQuantities);
132
133 } catch (error) {
134 console.error('Error loading purchase order:', error);
135 alert('Failed to load purchase order');
136 } finally {
137 setIsLoading(false);
138 }
139 };
140
141 const handleQuantityChange = (lineId: string, value: string) => {
142 const numValue = parseInt(value) || 0;
143 setReceivingQuantities(prev => ({
144 ...prev,
145 [lineId]: numValue
146 }));
147 };
148
149 const handleReceiveAll = () => {
150 const allQuantities: Record<string, number> = {};
151 lineItems.forEach(line => {
152 allQuantities[line.f1100] = line.f1108; // Set all to outstanding
153 });
154 setReceivingQuantities(allQuantities);
155 };
156
157 const handleClearAll = () => {
158 const cleared: Record<string, number> = {};
159 lineItems.forEach(line => {
160 cleared[line.f1100] = 0;
161 });
162 setReceivingQuantities(cleared);
163 };
164
165 const handleScanSubmit = (e: React.FormEvent) => {
166 e.preventDefault();
167 if (!scanInput.trim()) return;
168
169 // Find matching product by code
170 const matchingLine = lineItems.find(line =>
171 line.f1107.toLowerCase() === scanInput.toLowerCase() ||
172 line.f1101.toLowerCase() === scanInput.toLowerCase()
173 );
174
175 if (matchingLine) {
176 setReceivingQuantities(prev => ({
177 ...prev,
178 [matchingLine.f1100]: (prev[matchingLine.f1100] || 0) + 1
179 }));
180 setScanInput('');
181 } else {
182 alert(`Product not found: ${scanInput}`);
183 }
184 };
185
186 const handleSubmitReceipt = async () => {
187 const receivingLines = Object.entries(receivingQuantities)
188 .filter(([_, qty]) => qty > 0)
189 .map(([lineId, qty]) => {
190 const line = lineItems.find(l => l.f1100 === lineId);
191 return {
192 lineId,
193 productId: line?.f1101,
194 receivedQty: qty
195 };
196 });
197
198 if (receivingLines.length === 0) {
199 alert('Please enter quantities to receive');
200 return;
201 }
202
203 console.log('Submitting receipt with Fieldpine DATI packet:');
204 const datiPacket = {
205 request: 'save_data',
206 client: 'everydaypos-web',
207 data: {
208 table: 'PoReceipts',
209 operation: 'insert',
210 fields: {
211 f100_E: purchaseOrder?.f100, // PO ID
212 f200_E: receiveDate, // Receipt Date
213 f201_s: notes, // Notes
214 lines: receivingLines.map(line => ({
215 f1100_E: line.lineId, // Line ID
216 f1101_E: line.productId, // Product ID
217 f1200_N: line.receivedQty // Received Quantity
218 }))
219 }
220 }
221 };
222 console.log(JSON.stringify(datiPacket, null, 2));
223
224 // Simulate API call
225 if (confirm(`Receive ${receivingLines.length} line item(s) for PO ${purchaseOrder?.f100}?`)) {
226 alert('Receipt processed successfully! (This would call Fieldpine API in production)');
227 router.push('/purchase-orders');
228 }
229 };
230
231 if (isLoading) {
232 return (
233 <div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 overflow-auto">
234 <div className="container mx-auto px-4 py-8">
235 <div className="text-center">Loading purchase order...</div>
236 </div>
237 </div>
238 );
239 }
240
241 if (!purchaseOrder) {
242 return (
243 <div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 overflow-auto">
244 <div className="container mx-auto px-4 py-8">
245 <div className="text-center text-red-600">Purchase order not found</div>
246 </div>
247 </div>
248 );
249 }
250
251 const totalReceiving = Object.values(receivingQuantities).reduce((sum, qty) => sum + qty, 0);
252 const totalOutstanding = lineItems.reduce((sum, line) => sum + line.f1108, 0);
253
254 return (
255 <div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 overflow-auto">
256 <div className="container mx-auto px-4 py-8">
257 {/* Header */}
258 <div className="mb-6">
259 <div className="flex items-center justify-between mb-2">
260 <h1 className="text-3xl font-bold text-slate-800">Receive Purchase Order</h1>
261 <button
262 onClick={() => router.push('/purchase-orders')}
263 className="px-4 py-2 text-slate-600 hover:text-slate-800"
264 >
265 ← Back to Orders
266 </button>
267 </div>
268 <p className="text-slate-600">PO #{purchaseOrder.f100} - {purchaseOrder.f102}</p>
269 </div>
270
271 {/* PO Summary Card */}
272 <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6 mb-6">
273 <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
274 <div>
275 <div className="text-sm text-slate-500">PO Date</div>
276 <div className="font-semibold text-slate-800">{purchaseOrder.f103}</div>
277 </div>
278 <div>
279 <div className="text-sm text-slate-500">Total Amount</div>
280 <div className="font-semibold text-slate-800">${purchaseOrder.f104.toFixed(2)}</div>
281 </div>
282 <div>
283 <div className="text-sm text-slate-500">Total Outstanding</div>
284 <div className="font-semibold text-amber-600">{totalOutstanding} units</div>
285 </div>
286 <div>
287 <div className="text-sm text-slate-500">Now Receiving</div>
288 <div className="font-semibold text-[#00946b]">{totalReceiving} units</div>
289 </div>
290 </div>
291 {purchaseOrder.f132 && (
292 <div className="mt-4 pt-4 border-t border-slate-200">
293 <div className="text-sm text-slate-500">Notes</div>
294 <div className="text-slate-700">{purchaseOrder.f132}</div>
295 </div>
296 )}
297 </div>
298
299 {/* Action Bar */}
300 <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-4 mb-6">
301 <div className="flex flex-wrap items-center gap-4">
302 <div className="flex items-center gap-2">
303 <label className="text-sm font-medium text-slate-700">Receive Date:</label>
304 <input
305 type="date"
306 value={receiveDate}
307 onChange={(e) => setReceiveDate(e.target.value)}
308 className="px-3 py-2 border border-slate-300 rounded-lg text-sm"
309 />
310 </div>
311
312 <div className="flex items-center gap-2 ml-auto">
313 <button
314 onClick={() => setShowScanMode(!showScanMode)}
315 className="px-4 py-2 bg-slate-600 text-white rounded-lg hover:bg-slate-700 text-sm"
316 >
317 {showScanMode ? '⌨️ Manual' : '📷 Scan Mode'}
318 </button>
319 <button
320 onClick={handleReceiveAll}
321 className="px-4 py-2 bg-[#00946b] text-white rounded-lg hover:opacity-80 text-sm"
322 >
323 Receive All
324 </button>
325 <button
326 onClick={handleClearAll}
327 className="px-4 py-2 bg-slate-400 text-white rounded-lg hover:bg-slate-500 text-sm"
328 >
329 Clear All
330 </button>
331 </div>
332 </div>
333
334 {showScanMode && (
335 <form onSubmit={handleScanSubmit} className="mt-4 pt-4 border-t border-slate-200">
336 <div className="flex items-center gap-2">
337 <label className="text-sm font-medium text-slate-700">Scan Product:</label>
338 <input
339 type="text"
340 value={scanInput}
341 onChange={(e) => setScanInput(e.target.value)}
342 placeholder="Scan barcode or enter product code"
343 className="flex-1 px-3 py-2 border border-slate-300 rounded-lg text-sm"
344 autoFocus
345 />
346 <button
347 type="submit"
348 className="px-4 py-2 bg-[#00946b] text-white rounded-lg hover:opacity-80 text-sm"
349 >
350 Add
351 </button>
352 </div>
353 </form>
354 )}
355 </div>
356
357 {/* Line Items Table */}
358 <div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden mb-6">
359 <div className="overflow-x-auto">
360 <table className="w-full">
361 <thead className="bg-slate-50 border-b border-slate-200">
362 <tr>
363 <th className="px-4 py-3 text-left text-sm font-semibold text-slate-700">Code</th>
364 <th className="px-4 py-3 text-left text-sm font-semibold text-slate-700">Description</th>
365 <th className="px-4 py-3 text-right text-sm font-semibold text-slate-700">Ordered</th>
366 <th className="px-4 py-3 text-right text-sm font-semibold text-slate-700">Already Received</th>
367 <th className="px-4 py-3 text-right text-sm font-semibold text-slate-700">Outstanding</th>
368 <th className="px-4 py-3 text-right text-sm font-semibold text-slate-700">Receiving Now</th>
369 <th className="px-4 py-3 text-right text-sm font-semibold text-slate-700">Unit Cost</th>
370 <th className="px-4 py-3 text-right text-sm font-semibold text-slate-700">Line Total</th>
371 </tr>
372 </thead>
373 <tbody className="divide-y divide-slate-200">
374 {lineItems.map((line) => {
375 const receivingQty = receivingQuantities[line.f1100] || 0;
376 const lineTotal = receivingQty * line.f1105;
377
378 return (
379 <tr key={line.f1100} className="hover:bg-slate-50">
380 <td className="px-4 py-3 text-sm text-slate-800 font-medium">{line.f1107}</td>
381 <td className="px-4 py-3 text-sm text-slate-700">{line.f1102}</td>
382 <td className="px-4 py-3 text-sm text-slate-700 text-right">{line.f1103}</td>
383 <td className="px-4 py-3 text-sm text-slate-700 text-right">{line.f1104}</td>
384 <td className="px-4 py-3 text-sm text-amber-600 font-semibold text-right">{line.f1108}</td>
385 <td className="px-4 py-3 text-right">
386 <input
387 type="number"
388 min="0"
389 max={line.f1108}
390 value={receivingQty}
391 onChange={(e) => handleQuantityChange(line.f1100, e.target.value)}
392 className="w-20 px-2 py-1 border border-slate-300 rounded text-sm text-right"
393 />
394 </td>
395 <td className="px-4 py-3 text-sm text-slate-700 text-right">${line.f1105.toFixed(2)}</td>
396 <td className="px-4 py-3 text-sm text-slate-800 font-semibold text-right">
397 ${lineTotal.toFixed(2)}
398 </td>
399 </tr>
400 );
401 })}
402 </tbody>
403 </table>
404 </div>
405 </div>
406
407 {/* Receipt Notes */}
408 <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6 mb-6">
409 <label className="block text-sm font-medium text-slate-700 mb-2">Receipt Notes</label>
410 <textarea
411 value={notes}
412 onChange={(e) => setNotes(e.target.value)}
413 placeholder="Add any notes about this receipt (optional)"
414 rows={3}
415 className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
416 />
417 </div>
418
419 {/* Submit Section */}
420 <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
421 <div className="flex items-center justify-between">
422 <div>
423 <div className="text-sm text-slate-500">Total Receiving</div>
424 <div className="text-2xl font-bold text-[#00946b]">{totalReceiving} units</div>
425 </div>
426 <div className="flex gap-3">
427 <button
428 onClick={() => router.push('/purchase-orders')}
429 className="px-6 py-3 bg-slate-300 text-slate-700 rounded-lg hover:bg-slate-400 font-semibold"
430 >
431 Cancel
432 </button>
433 <button
434 onClick={handleSubmitReceipt}
435 disabled={totalReceiving === 0}
436 className="px-6 py-3 bg-[#00946b] text-white rounded-lg hover:opacity-80 font-semibold disabled:opacity-50 disabled:cursor-not-allowed"
437 >
438 Process Receipt
439 </button>
440 </div>
441 </div>
442 </div>
443 </div>
444 </div>
445 );
446}
447
448export default function ReceivePurchaseOrder() {
449 return (
450 <Suspense fallback={<div className="flex items-center justify-center min-h-screen">Loading...</div>}>
451 <ReceivePurchaseOrderContent />
452 </Suspense>
453 );
454}