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 } from 'react';
4import { useRouter } from 'next/navigation';
5
6interface OrderLineItem {
7 id: string;
8 productCode: string;
9 productName: string;
10 quantity: number;
11 unitCost: number;
12 lineTotal: number;
13 extendedCost: number;
14}
15
16interface Supplier {
17 id: string;
18 name: string;
19 email?: string;
20 phone?: string;
21}
22
23interface Product {
24 code: string;
25 name: string;
26 description?: string;
27 lastCost?: number;
28 supplier?: string;
29}
30
31export default function CreatePurchaseOrder() {
32 const router = useRouter();
33
34 const [supplierId, setSupplierId] = useState('');
35 const [supplierName, setSupplierName] = useState('');
36 const [supplierSearch, setSupplierSearch] = useState('');
37 const [showSupplierDropdown, setShowSupplierDropdown] = useState(false);
38 const [suppliers, setSuppliers] = useState<Supplier[]>([]);
39 const [orderDate, setOrderDate] = useState(new Date().toISOString().split('T')[0]);
40 const [expectedDeliveryDate, setExpectedDeliveryDate] = useState('');
41 const [poNumber, setPoNumber] = useState('');
42 const [notes, setNotes] = useState('');
43 const [freightCost, setFreightCost] = useState('');
44 const [lineItems, setLineItems] = useState<OrderLineItem[]>([]);
45
46 // New line form
47 const [newProductCode, setNewProductCode] = useState('');
48 const [newProductName, setNewProductName] = useState('');
49 const [productCodeSearch, setProductCodeSearch] = useState('');
50 const [productNameSearch, setProductNameSearch] = useState('');
51 const [showProductCodeDropdown, setShowProductCodeDropdown] = useState(false);
52 const [showProductNameDropdown, setShowProductNameDropdown] = useState(false);
53 const [filteredProducts, setFilteredProducts] = useState<Product[]>([]);
54 const [newQuantity, setNewQuantity] = useState('');
55 const [newUnitCost, setNewUnitCost] = useState('');
56
57 // Email state
58 const [showEmailModal, setShowEmailModal] = useState(false);
59 const [sendViaEmail, setSendViaEmail] = useState(false);
60 const [otherEmail, setOtherEmail] = useState('');
61 const [additionalNotes, setAdditionalNotes] = useState('');
62 const [showEmailOptions, setShowEmailOptions] = useState(false);
63
64 // Mock suppliers - replace with actual API call
65 const mockSuppliers: Supplier[] = [
66 { id: '1001', name: 'ABC Suppliers Ltd', email: 'orders@abcsuppliers.com', phone: '555-0100' },
67 { id: '1002', name: 'Best Wholesale Co', email: 'sales@bestwholesale.com', phone: '555-0200' },
68 { id: '1003', name: 'Central Distributors', email: 'info@centraldist.com', phone: '555-0300' },
69 { id: '1004', name: 'Delta Trading Inc', email: 'orders@deltatrading.com', phone: '555-0400' },
70 { id: '1005', name: 'Elite Products Ltd', email: 'sales@eliteproducts.com', phone: '555-0500' },
71 { id: '1006', name: 'Fresh Foods Suppliers', email: 'orders@freshfoods.com', phone: '555-0600' },
72 { id: '1007', name: 'Global Imports Co', email: 'info@globalimports.com', phone: '555-0700' },
73 { id: '1008', name: 'Home Goods Wholesale', email: 'sales@homegoods.com', phone: '555-0800' },
74 ];
75
76 // Mock inventory catalog - replace with actual API call
77 const mockInventory: Product[] = [
78 { code: 'COF-001', name: 'Premium Coffee Beans 1kg', description: 'Arabica blend', lastCost: 25.50 },
79 { code: 'TEA-002', name: 'Organic Tea Selection Box', description: '12 varieties', lastCost: 18.75 },
80 { code: 'POT-003', name: 'Stainless Steel Teapot', description: '1.5L capacity', lastCost: 42.00 },
81 { code: 'GRN-004', name: 'Coffee Grinder Professional', description: 'Commercial grade', lastCost: 156.30 },
82 { code: 'CUP-005', name: 'Paper Cups 100pk', description: '12oz disposable', lastCost: 8.90 },
83 { code: 'MUG-006', name: 'Ceramic Mug Set', description: '6 piece set', lastCost: 34.50 },
84 { code: 'SYR-007', name: 'Flavored Syrup Vanilla', description: '750ml bottle', lastCost: 12.25 },
85 { code: 'SYR-008', name: 'Flavored Syrup Caramel', description: '750ml bottle', lastCost: 12.25 },
86 { code: 'SYR-009', name: 'Flavored Syrup Hazelnut', description: '750ml bottle', lastCost: 12.25 },
87 { code: 'MIL-010', name: 'Milk Frother Electric', description: 'Battery operated', lastCost: 28.90 },
88 { code: 'SPO-011', name: 'Stainless Steel Spoons', description: '12 pack', lastCost: 15.75 },
89 { code: 'NAP-012', name: 'Paper Napkins 500pk', description: 'White premium', lastCost: 6.50 },
90 ];
91
92 const handleProductCodeSearch = (value: string) => {
93 setProductCodeSearch(value);
94 if (value.trim()) {
95 const filtered = mockInventory.filter(p =>
96 p.code.toLowerCase().includes(value.toLowerCase())
97 );
98 setFilteredProducts(filtered);
99 setShowProductCodeDropdown(true);
100 setShowProductNameDropdown(false);
101 } else {
102 setFilteredProducts(mockInventory);
103 setShowProductCodeDropdown(true);
104 setShowProductNameDropdown(false);
105 }
106 };
107
108 const handleProductNameSearch = (value: string) => {
109 setProductNameSearch(value);
110 if (value.trim()) {
111 const filtered = mockInventory.filter(p =>
112 p.name.toLowerCase().includes(value.toLowerCase()) ||
113 (p.description && p.description.toLowerCase().includes(value.toLowerCase()))
114 );
115 setFilteredProducts(filtered);
116 setShowProductNameDropdown(true);
117 setShowProductCodeDropdown(false);
118 } else {
119 setFilteredProducts(mockInventory);
120 setShowProductNameDropdown(true);
121 setShowProductCodeDropdown(false);
122 }
123 };
124
125 const handleSelectProduct = (product: Product) => {
126 setNewProductCode(product.code);
127 setNewProductName(product.name);
128 setProductCodeSearch(product.code);
129 setProductNameSearch(product.name);
130 if (product.lastCost) {
131 setNewUnitCost(product.lastCost.toString());
132 }
133 setShowProductCodeDropdown(false);
134 setShowProductNameDropdown(false);
135 };
136
137 const handleShowAllProducts = (type: 'code' | 'name') => {
138 setFilteredProducts(mockInventory);
139 if (type === 'code') {
140 setShowProductCodeDropdown(true);
141 setShowProductNameDropdown(false);
142 } else {
143 setShowProductNameDropdown(true);
144 setShowProductCodeDropdown(false);
145 }
146 };
147
148 const handleClearProduct = () => {
149 setNewProductCode('');
150 setNewProductName('');
151 setProductCodeSearch('');
152 setProductNameSearch('');
153 setNewUnitCost('');
154 setFilteredProducts([]);
155 };
156
157 const handleSupplierSearch = (value: string) => {
158 setSupplierSearch(value);
159 if (value.trim()) {
160 const filtered = mockSuppliers.filter(s =>
161 s.name.toLowerCase().includes(value.toLowerCase()) ||
162 s.id.includes(value)
163 );
164 setSuppliers(filtered);
165 setShowSupplierDropdown(true);
166 } else {
167 setSuppliers(mockSuppliers);
168 setShowSupplierDropdown(true);
169 }
170 };
171
172 const handleShowAllSuppliers = () => {
173 setSuppliers(mockSuppliers);
174 setShowSupplierDropdown(true);
175 };
176
177 const handleSelectSupplier = (supplier: Supplier) => {
178 setSupplierId(supplier.id);
179 setSupplierName(supplier.name);
180 setSupplierSearch(supplier.name);
181 setShowSupplierDropdown(false);
182 };
183
184 const handleClearSupplier = () => {
185 setSupplierId('');
186 setSupplierName('');
187 setSupplierSearch('');
188 setSuppliers([]);
189 };
190
191 const calculateExtendedCost = () => {
192 const quantity = parseFloat(newQuantity) || 0;
193 const unitCost = parseFloat(newUnitCost) || 0;
194 return quantity * unitCost;
195 };
196
197 const handleAddLine = () => {
198 if (!newProductCode || !newProductName || !newQuantity || !newUnitCost) {
199 alert('Please fill in all line item fields');
200 return;
201 }
202
203 const quantity = parseFloat(newQuantity);
204 const unitCost = parseFloat(newUnitCost);
205 const extendedCost = calculateExtendedCost();
206 const lineTotal = extendedCost;
207
208 const newLine: OrderLineItem = {
209 id: Date.now().toString(),
210 productCode: newProductCode,
211 productName: newProductName,
212 quantity,
213 unitCost,
214 lineTotal,
215 extendedCost
216 };
217
218 setLineItems([...lineItems, newLine]);
219
220 // Clear form
221 setNewProductCode('');
222 setNewProductName('');
223 setProductCodeSearch('');
224 setProductNameSearch('');
225 setNewQuantity('');
226 setNewUnitCost('');
227 };
228
229 const handleLineFormKeyDown = (e: React.KeyboardEvent) => {
230 if (e.key === 'Enter') {
231 e.preventDefault();
232 handleAddLine();
233 }
234 };
235
236 const handleRemoveLine = (id: string) => {
237 setLineItems(lineItems.filter(line => line.id !== id));
238 };
239
240 const handleUpdateLineQuantity = (id: string, value: string) => {
241 const quantity = parseFloat(value) || 0;
242 setLineItems(lineItems.map(line => {
243 if (line.id === id) {
244 return {
245 ...line,
246 quantity,
247 lineTotal: quantity * line.unitCost + line.extendedCost
248 };
249 }
250 return line;
251 }));
252 };
253
254 const handleUpdateLineUnitCost = (id: string, value: string) => {
255 const unitCost = parseFloat(value) || 0;
256 setLineItems(lineItems.map(line => {
257 if (line.id === id) {
258 return {
259 ...line,
260 unitCost,
261 lineTotal: line.quantity * unitCost + line.extendedCost
262 };
263 }
264 return line;
265 }));
266 };
267
268 const handleUpdateLineExtendedCost = (id: string) => {
269 setLineItems(lineItems.map(line => {
270 if (line.id === id) {
271 const extendedCost = line.quantity * line.unitCost;
272 return {
273 ...line,
274 extendedCost,
275 lineTotal: extendedCost
276 };
277 }
278 return line;
279 }));
280 };
281
282 const calculateTotal = () => {
283 const lineTotal = lineItems.reduce((sum, line) => sum + line.lineTotal, 0);
284 const freight = parseFloat(freightCost) || 0;
285 return lineTotal + freight;
286 };
287
288 const handleSubmit = async () => {
289 if (!supplierId || !supplierName) {
290 alert('Please enter supplier information');
291 return;
292 }
293
294 if (lineItems.length === 0) {
295 alert('Please add at least one line item');
296 return;
297 }
298
299 const orderTotal = calculateTotal();
300
301 if (confirm(`Create purchase order for ${supplierName} with ${lineItems.length} line item(s)?\n\nSubtotal: $${lineItems.reduce((sum, line) => sum + line.lineTotal, 0).toFixed(2)}\nFreight: $${(parseFloat(freightCost) || 0).toFixed(2)}\nTotal: $${orderTotal.toFixed(2)}`)) {
302
303 // Generate PO number (in production, this would come from Fieldpine)
304 const generatedPO = poNumber || `PO-${Date.now()}`;
305
306 console.log('Creating purchase order with Fieldpine DATI packet:');
307 const datiPacket = {
308 request: 'save_data',
309 client: 'everydaypos-web',
310 data: {
311 table: 'PurchaseOrders',
312 operation: 'insert',
313 fields: {
314 f100_s: generatedPO, // PO Number
315 f101_E: supplierId, // Supplier ID
316 f102_s: supplierName, // Supplier Name
317 f103_E: orderDate, // Order Date
318 f104_N: orderTotal, // Total Amount (including freight)
319 f105_E: expectedDeliveryDate || null, // Expected Delivery Date
320 f106_N: parseFloat(freightCost) || 0, // Freight Cost
321 f114_N: 1, // Status: 1 = Draft
322 f132_s: notes, // Notes
323 lines: lineItems.map((line, index) => ({
324 f1100_E: (index + 1).toString(), // Line Number
325 f1101_E: line.productCode, // Product Code
326 f1102_s: line.productName, // Product Description
327 f1103_N: line.quantity, // Quantity Ordered
328 f1105_N: line.unitCost, // Unit Cost
329 f1106_N: line.lineTotal, // Line Total
330 f1107_s: line.productCode, // Product Code (alt field)
331 f1108_N: line.quantity // Quantity Outstanding (initially same as ordered)
332 }))
333 }
334 }
335 };
336 console.log(JSON.stringify(datiPacket, null, 2));
337
338 // Simulate API call success
339 await new Promise(resolve => setTimeout(resolve, 500));
340
341 // Show success message with PO number
342 alert(`✓ Purchase Order Created Successfully!\n\nPO Number: ${generatedPO}\nSupplier: ${supplierName}\nTotal: $${orderTotal.toFixed(2)}\n\nIn production, this would be saved to Fieldpine.`);
343
344 // Navigate back to purchase orders page
345 router.push('/purchase-orders');
346 }
347 };
348
349 const handleSaveDraft = () => {
350 alert('Draft saved! (This would call Fieldpine API with status=0 in production)');
351 };
352
353 const handleSubmitWithEmail = async () => {
354 if (!sendViaEmail) {
355 alert('Please enable email sending');
356 return;
357 }
358
359 if (!supplierId || !supplierName) {
360 alert('Please enter supplier information');
361 return;
362 }
363
364 if (lineItems.length === 0) {
365 alert('Please add at least one line item');
366 return;
367 }
368
369 const orderTotal = calculateTotal();
370 const generatedPO = poNumber || `PO-${Date.now()}`;
371
372 if (confirm(`Create purchase order and email to supplier?\n\nPO: ${generatedPO}\nSupplier: ${supplierName}\nTotal: $${orderTotal.toFixed(2)}`)) {
373
374 // 1. Log DATI packet for order creation
375 console.log('Creating purchase order with Fieldpine DATI packet:');
376 const datiPacket = {
377 request: 'save_data',
378 client: 'everydaypos-web',
379 data: {
380 table: 'PurchaseOrders',
381 operation: 'insert',
382 fields: {
383 f100_s: generatedPO,
384 f101_E: supplierId,
385 f102_s: supplierName,
386 f103_E: orderDate,
387 f104_N: orderTotal,
388 f105_E: expectedDeliveryDate || null,
389 f106_N: parseFloat(freightCost) || 0,
390 f114_N: 1,
391 f132_s: notes,
392 lines: lineItems.map((line, index) => ({
393 f1100_E: (index + 1).toString(),
394 f1101_E: line.productCode,
395 f1102_s: line.productName,
396 f1103_N: line.quantity,
397 f1105_N: line.unitCost,
398 f1106_N: line.lineTotal,
399 f1107_s: line.productCode,
400 f1108_N: line.quantity
401 }))
402 }
403 }
404 };
405 console.log(JSON.stringify(datiPacket, null, 2));
406
407 // 2. Simulate order creation
408 await new Promise(resolve => setTimeout(resolve, 300));
409
410 // 3. Prepare and send email
411 const targetEmail = otherEmail.trim() || 'supplier@example.com';
412
413 let emailBody = `Dear ${supplierName},\n\n`;
414 emailBody += `Please find our purchase order details below:\n\n`;
415 emailBody += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
416 emailBody += `PURCHASE ORDER #${generatedPO}\n`;
417 emailBody += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`;
418
419 emailBody += `Order Date: ${orderDate}\n`;
420 emailBody += `Expected Delivery: ${expectedDeliveryDate || 'To be confirmed'}\n\n`;
421
422 if (additionalNotes) {
423 emailBody += `ADDITIONAL NOTES:\n${additionalNotes}\n\n`;
424 }
425
426 emailBody += `ORDER SUMMARY:\n`;
427 emailBody += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
428 emailBody += `Total Lines: ${lineItems.length}\n`;
429 emailBody += `Subtotal: $${lineItems.reduce((sum, line) => sum + line.lineTotal, 0).toFixed(2)}\n`;
430 if (parseFloat(freightCost)) {
431 emailBody += `Freight: $${parseFloat(freightCost).toFixed(2)}\n`;
432 }
433 emailBody += `Total: $${orderTotal.toFixed(2)}\n\n`;
434
435 emailBody += `LINE ITEMS:\n`;
436 emailBody += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
437 lineItems.forEach((line, index) => {
438 emailBody += `${index + 1}. ${line.productCode} - ${line.productName}\n`;
439 emailBody += ` Quantity: ${line.quantity} @ $${line.unitCost.toFixed(2)} each\n`;
440 emailBody += ` Line Total: $${line.lineTotal.toFixed(2)}\n\n`;
441 });
442
443 if (notes) {
444 emailBody += `\nNOTES:\n${notes}\n\n`;
445 }
446
447 emailBody += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
448 emailBody += `\nThank you for your service.\n\n`;
449 emailBody += `Best regards\n`;
450 emailBody += `\n---\nGenerated by EverydayPOS`;
451
452 // Log email DATI packet
453 const emailDatiPacket = {
454 request: 'save_data',
455 client: 'everydaypos-web',
456 data: {
457 table: 'PurchaseOrders',
458 operation: 'email',
459 fields: {
460 f100_s: generatedPO,
461 email: targetEmail,
462 subject: `Purchase Order #${generatedPO} from ${supplierName}`,
463 body: emailBody
464 }
465 }
466 };
467 console.log('Email DATI packet:', emailDatiPacket);
468
469 // Create mailto link
470 const subject = encodeURIComponent(`Purchase Order #${generatedPO} from ${supplierName}`);
471 const mailtoLink = `mailto:${targetEmail}?subject=${subject}&body=${encodeURIComponent(emailBody)}`;
472 window.location.href = mailtoLink;
473
474 alert(`✓ Purchase Order Created & Email Ready!\n\nPO Number: ${generatedPO}\nSupplier: ${supplierName}\nTotal: $${orderTotal.toFixed(2)}\n\nEmail to: ${targetEmail}`);
475
476 // Close modal
477 setShowEmailModal(false);
478 setSendViaEmail(false);
479 setOtherEmail('');
480 setAdditionalNotes('');
481 setShowEmailOptions(false);
482
483 // Navigate after a delay to allow email client to open
484 setTimeout(() => {
485 router.push('/purchase-orders');
486 }, 1000);
487 }
488 };
489
490 return (
491 <div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 overflow-auto">
492 <div className="container mx-auto px-4 py-8">
493 {/* Header */}
494 <div className="mb-6">
495 <div className="flex items-center justify-between mb-2">
496 <h1 className="text-3xl font-bold text-slate-800">Create Purchase Order</h1>
497 <button
498 onClick={() => router.push('/purchase-orders')}
499 className="px-4 py-2 text-slate-600 hover:text-slate-800"
500 >
501 ← Back to Orders
502 </button>
503 </div>
504 <p className="text-slate-600">Create a new purchase order for your supplier</p>
505 </div>
506
507 {/* Supplier Information */}
508 <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6 mb-6">
509 <h2 className="text-lg font-semibold text-slate-800 mb-4">Purchase Order Details</h2>
510 <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
511 <div>
512 <label className="block text-sm font-medium text-slate-700 mb-2">PO Number</label>
513 <input
514 type="text"
515 value={poNumber}
516 onChange={(e) => setPoNumber(e.target.value)}
517 placeholder="Auto-generated if left blank"
518 className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
519 />
520 </div>
521 </div>
522 <h3 className="text-lg font-semibold text-slate-800 mb-4">Supplier Information</h3>
523 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
524 <div className="md:col-span-2 relative">
525 <label className="block text-sm font-medium text-slate-700 mb-2">
526 Search Supplier <span className="text-red-500">*</span>
527 </label>
528 <div className="relative">
529 <input
530 type="text"
531 value={supplierSearch}
532 onChange={(e) => handleSupplierSearch(e.target.value)}
533 onFocus={() => handleShowAllSuppliers()}
534 placeholder="Start typing supplier name or ID..."
535 className="w-full px-3 py-2 pr-20 border border-slate-300 rounded-lg text-sm"
536 />
537 <div className="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-2">
538 {supplierId && (
539 <button
540 onClick={handleClearSupplier}
541 className="text-slate-400 hover:text-slate-600"
542 >
543
544 </button>
545 )}
546 <button
547 onClick={handleShowAllSuppliers}
548 className="text-slate-400 hover:text-slate-600"
549 title="Show all suppliers"
550 >
551
552 </button>
553 </div>
554 </div>
555 {showSupplierDropdown && suppliers.length > 0 && (
556 <div className="absolute z-10 w-full mt-1 bg-white border border-slate-300 rounded-lg shadow-lg max-h-60 overflow-y-auto">
557 {suppliers.map(supplier => (
558 <div
559 key={supplier.id}
560 onClick={() => handleSelectSupplier(supplier)}
561 className="px-4 py-3 hover:bg-slate-50 cursor-pointer border-b border-slate-100 last:border-b-0"
562 >
563 <div className="font-medium text-slate-800">{supplier.name}</div>
564 <div className="text-sm text-slate-500">ID: {supplier.id}</div>
565 {supplier.email && (
566 <div className="text-xs text-slate-400 mt-1">{supplier.email}</div>
567 )}
568 </div>
569 ))}
570 </div>
571 )}
572 </div>
573 {supplierId && (
574 <>
575 <div>
576 <label className="block text-sm font-medium text-slate-700 mb-2">Supplier ID</label>
577 <input
578 type="text"
579 value={supplierId}
580 readOnly
581 className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm bg-slate-50"
582 />
583 </div>
584 <div>
585 <label className="block text-sm font-medium text-slate-700 mb-2">Supplier Name</label>
586 <input
587 type="text"
588 value={supplierName}
589 readOnly
590 className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm bg-slate-50"
591 />
592 </div>
593 </>
594 )}
595 <div>
596 <label className="block text-sm font-medium text-slate-700 mb-2">
597 Order Date <span className="text-red-500">*</span>
598 </label>
599 <input
600 type="date"
601 value={orderDate}
602 onChange={(e) => setOrderDate(e.target.value)}
603 className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
604 />
605 </div>
606 <div>
607 <label className="block text-sm font-medium text-slate-700 mb-2">
608 Expected Delivery Date
609 </label>
610 <input
611 type="date"
612 value={expectedDeliveryDate}
613 onChange={(e) => setExpectedDeliveryDate(e.target.value)}
614 className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
615 />
616 </div>
617 </div>
618 </div>
619
620 {/* Freight and Notes */}
621 <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6 mb-6">
622 <label className="block text-sm font-medium text-slate-700 mb-2">Notes</label>
623 <textarea
624 value={notes}
625 onChange={(e) => setNotes(e.target.value)}
626 placeholder="Add any notes or special instructions"
627 rows={3}
628 className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
629 />
630 </div>
631
632 {/* Add Line Item Form */}
633 <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6 mb-6">
634 <h2 className="text-lg font-semibold text-slate-800 mb-4">Add Line Item</h2>
635 <div className="grid grid-cols-1 md:grid-cols-6 gap-4">
636 <div className="relative">
637 <label className="block text-sm font-medium text-slate-700 mb-2">Product Code</label>
638 <input
639 type="text"
640 value={productCodeSearch}
641 onChange={(e) => handleProductCodeSearch(e.target.value)}
642 onFocus={() => handleShowAllProducts('code')}
643 placeholder="Search code..."
644 className="w-full px-3 py-2 pr-8 border border-slate-300 rounded-lg text-sm"
645 />
646 <button
647 onClick={() => handleShowAllProducts('code')}
648 className="absolute right-2 top-[38px] text-slate-400 hover:text-slate-600"
649 title="Show all products"
650 >
651
652 </button>
653 {showProductCodeDropdown && filteredProducts.length > 0 && (
654 <div className="absolute z-20 w-full mt-1 bg-white border border-slate-300 rounded-lg shadow-lg max-h-60 overflow-y-auto">
655 {filteredProducts.map(product => (
656 <div
657 key={product.code}
658 onClick={() => handleSelectProduct(product)}
659 className="px-3 py-2 hover:bg-slate-50 cursor-pointer border-b border-slate-100 last:border-b-0"
660 >
661 <div className="font-medium text-slate-800 text-sm">{product.code}</div>
662 <div className="text-xs text-slate-500">{product.name}</div>
663 </div>
664 ))}
665 </div>
666 )}
667 </div>
668 <div className="md:col-span-2 relative">
669 <label className="block text-sm font-medium text-slate-700 mb-2">Product Name</label>
670 <input
671 type="text"
672 value={productNameSearch}
673 onChange={(e) => handleProductNameSearch(e.target.value)}
674 onFocus={() => handleShowAllProducts('name')}
675 placeholder="Search product..."
676 className="w-full px-3 py-2 pr-8 border border-slate-300 rounded-lg text-sm"
677 />
678 <button
679 onClick={() => handleShowAllProducts('name')}
680 className="absolute right-2 top-[38px] text-slate-400 hover:text-slate-600"
681 title="Show all products"
682 >
683
684 </button>
685 {showProductNameDropdown && filteredProducts.length > 0 && (
686 <div className="absolute z-20 w-full mt-1 bg-white border border-slate-300 rounded-lg shadow-lg max-h-60 overflow-y-auto">
687 {filteredProducts.map(product => (
688 <div
689 key={product.code}
690 onClick={() => handleSelectProduct(product)}
691 className="px-3 py-2 hover:bg-slate-50 cursor-pointer border-b border-slate-100 last:border-b-0"
692 >
693 <div className="font-medium text-slate-800 text-sm">{product.name}</div>
694 <div className="text-xs text-slate-500">{product.code} • {product.description}</div>
695 </div>
696 ))}
697 </div>
698 )}
699 </div>
700 <div>
701 <label className="block text-sm font-medium text-slate-700 mb-2">Quantity</label>
702 <input
703 type="number"
704 value={newQuantity}
705 onChange={(e) => setNewQuantity(e.target.value)}
706 onKeyDown={handleLineFormKeyDown}
707 placeholder="0"
708 min="0"
709 step="1"
710 className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
711 />
712 </div>
713 <div>
714 <label className="block text-sm font-medium text-slate-700 mb-2">Unit Cost ex. GST</label>
715 <input
716 type="number"
717 value={newUnitCost}
718 onChange={(e) => setNewUnitCost(e.target.value)}
719 onKeyDown={handleLineFormKeyDown}
720 placeholder="0.00"
721 min="0"
722 step="0.01"
723 className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
724 />
725 </div>
726 <div>
727 <label className="block text-sm font-medium text-slate-700 mb-2">Extended Cost ex. GST</label>
728 <input
729 type="number"
730 value={calculateExtendedCost().toFixed(2)}
731 readOnly
732 className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm bg-slate-50 text-slate-700 font-semibold"
733 />
734 </div>
735 </div>
736 <div className="mt-4 flex justify-between items-center">
737 <button
738 onClick={handleAddLine}
739 className="px-6 py-2 bg-[#00946b] text-white rounded-lg hover:opacity-80 font-semibold"
740 >
741 + Add Line Item
742 </button>
743 {(newProductCode || newProductName) && (
744 <button
745 onClick={handleClearProduct}
746 className="px-4 py-2 text-slate-600 hover:text-slate-800 text-sm"
747 >
748 Clear Product
749 </button>
750 )}
751 </div>
752 </div>
753
754 {/* Line Items Table */}
755 {lineItems.length > 0 && (
756 <div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden mb-6">
757 <div className="px-6 py-4 border-b border-slate-200">
758 <h2 className="text-lg font-semibold text-slate-800">Order Lines ({lineItems.length})</h2>
759 </div>
760 <div className="overflow-x-auto">
761 <table className="w-full">
762 <thead className="bg-slate-50 border-b border-slate-200">
763 <tr>
764 <th className="px-4 py-3 text-left text-sm font-semibold text-slate-700">Code</th>
765 <th className="px-4 py-3 text-left text-sm font-semibold text-slate-700">Description</th>
766 <th className="px-4 py-3 text-right text-sm font-semibold text-slate-700">Quantity</th>
767 <th className="px-4 py-3 text-right text-sm font-semibold text-slate-700">Unit Cost</th>
768 <th className="px-4 py-3 text-right text-sm font-semibold text-slate-700">Extended Cost</th>
769 <th className="px-4 py-3 text-right text-sm font-semibold text-slate-700">Line Total</th>
770 <th className="px-4 py-3 text-center text-sm font-semibold text-slate-700">Actions</th>
771 </tr>
772 </thead>
773 <tbody className="divide-y divide-slate-200">
774 {lineItems.map((line) => (
775 <tr key={line.id} className="hover:bg-slate-50">
776 <td className="px-4 py-3 text-sm text-slate-800 font-medium">{line.productCode}</td>
777 <td className="px-4 py-3 text-sm text-slate-700">{line.productName}</td>
778 <td className="px-4 py-3 text-right">
779 <input
780 type="number"
781 value={line.quantity}
782 onChange={(e) => handleUpdateLineQuantity(line.id, e.target.value)}
783 min="0"
784 step="1"
785 className="w-20 px-2 py-1 border border-slate-300 rounded text-sm text-right"
786 />
787 </td>
788 <td className="px-4 py-3 text-right">
789 <input
790 type="number"
791 value={line.unitCost}
792 onChange={(e) => handleUpdateLineUnitCost(line.id, e.target.value)}
793 min="0"
794 step="0.01"
795 className="w-24 px-2 py-1 border border-slate-300 rounded text-sm text-right"
796 />
797 </td>
798 <td className="px-4 py-3 text-right text-sm text-slate-800 font-semibold">
799 ${line.extendedCost.toFixed(2)}
800 </td>
801 <td className="px-4 py-3 text-sm text-slate-800 font-semibold text-right">
802 ${line.lineTotal.toFixed(2)}
803 </td>
804 <td className="px-4 py-3 text-center">
805 <button
806 onClick={() => handleRemoveLine(line.id)}
807 className="text-red-600 hover:text-red-800 text-sm font-medium"
808 >
809 Remove
810 </button>
811 </td>
812 </tr>
813 ))}
814 </tbody>
815 <tfoot className="bg-slate-50 border-t-2 border-slate-300">
816 <tr>
817 <td colSpan={4} className="px-4 py-3 text-right text-sm font-bold text-slate-800">
818 Total:
819 </td>
820 <td className="px-4 py-3 text-right text-lg font-bold text-[#00946b]">
821 ${calculateTotal().toFixed(2)}
822 </td>
823 <td></td>
824 </tr>
825 </tfoot>
826 </table>
827 </div>
828 </div>
829 )}
830
831 {/* Action Buttons */}
832 <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
833 <div className="flex items-center justify-between">
834 <div className="flex-1">
835 <div className="flex items-end gap-6 mb-4">
836 <div>
837 <label className="block text-sm font-medium text-slate-700 mb-2">Freight ex. GST</label>
838 <input
839 type="number"
840 value={freightCost}
841 onChange={(e) => setFreightCost(e.target.value)}
842 placeholder="0.00"
843 min="0"
844 step="0.01"
845 className="w-32 px-3 py-2 border border-slate-300 rounded-lg text-lg font-semibold text-slate-700"
846 />
847 </div>
848 </div>
849 <div className="text-sm text-slate-500">Subtotal</div>
850 <div className="text-lg font-semibold text-slate-700">${lineItems.reduce((sum, line) => sum + line.lineTotal, 0).toFixed(2)}</div>
851 <div className="text-sm text-slate-500 mt-3 border-t border-slate-200 pt-2">Order Total ex. GST</div>
852 <div className="text-3xl font-bold text-[#00946b]">${calculateTotal().toFixed(2)}</div>
853 <div className="text-sm text-slate-600 mt-1">{lineItems.length} line item(s)</div>
854 </div>
855 <div className="flex gap-3">
856 <button
857 onClick={() => router.push('/purchase-orders')}
858 className="px-6 py-3 bg-slate-300 text-slate-700 rounded-lg hover:bg-slate-400 font-semibold"
859 >
860 Cancel
861 </button>
862 <button
863 onClick={handleSaveDraft}
864 className="px-6 py-3 bg-slate-600 text-white rounded-lg hover:bg-slate-700 font-semibold"
865 >
866 Save as Draft
867 </button>
868 <button
869 onClick={handleSubmit}
870 disabled={lineItems.length === 0 || !supplierId || !supplierName}
871 className="px-6 py-3 bg-[#00946b] text-white rounded-lg hover:opacity-80 font-semibold disabled:opacity-50 disabled:cursor-not-allowed"
872 >
873 Create Order
874 </button>
875 <button
876 onClick={() => setShowEmailModal(true)}
877 disabled={lineItems.length === 0 || !supplierId || !supplierName}
878 className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-semibold disabled:opacity-50 disabled:cursor-not-allowed"
879 >
880 Create Order & Email
881 </button>
882 </div>
883 </div>
884 </div>
885 {/* Email Modal */}
886 {showEmailModal && (
887 <div className="fixed inset-0 bg-black/30 z-50 flex items-center justify-center p-4">
888 <div className="bg-white rounded-lg shadow-xl max-w-md w-full">
889 <div className="border-b border-slate-200 p-4 flex items-center justify-between">
890 <h2 className="text-lg font-semibold text-slate-800">Email Options</h2>
891 <button
892 onClick={() => {
893 setShowEmailModal(false);
894 setSendViaEmail(false);
895 setOtherEmail('');
896 setAdditionalNotes('');
897 setShowEmailOptions(false);
898 }}
899 className="text-slate-400 hover:text-slate-600"
900 >
901
902 </button>
903 </div>
904
905 <div className="p-6 space-y-4">
906 <div className="border border-slate-200 rounded-lg p-4 bg-slate-50">
907 <label className="flex items-center gap-2 cursor-pointer mb-3">
908 <input
909 type="checkbox"
910 checked={sendViaEmail}
911 onChange={(e) => setSendViaEmail(e.target.checked)}
912 className="w-4 h-4 text-blue-600 rounded"
913 />
914 <span className="font-medium text-slate-700">Email to Supplier</span>
915 </label>
916
917 {sendViaEmail && (
918 <>
919 <div className="text-sm text-slate-600 mb-3">
920 Email: {supplierName}
921 </div>
922
923 <button
924 onClick={() => setShowEmailOptions(!showEmailOptions)}
925 className="text-sm text-blue-600 hover:text-blue-700 mb-3"
926 >
927 {showEmailOptions ? '▲' : '▼'} Options
928 </button>
929
930 {showEmailOptions && (
931 <div className="space-y-3 mb-3 border-t border-slate-200 pt-3">
932 <div>
933 <label className="block text-sm font-medium text-slate-700 mb-1">
934 Override Email
935 </label>
936 <input
937 type="email"
938 value={otherEmail}
939 onChange={(e) => setOtherEmail(e.target.value)}
940 placeholder="Enter alternative email"
941 className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
942 />
943 </div>
944
945 <div>
946 <label className="block text-sm font-medium text-slate-700 mb-1">
947 Additional Notes
948 </label>
949 <textarea
950 value={additionalNotes}
951 onChange={(e) => setAdditionalNotes(e.target.value)}
952 rows={3}
953 placeholder="Add any extra message"
954 className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
955 />
956 </div>
957 </div>
958 )}
959 </>
960 )}
961 </div>
962
963 <div className="flex gap-3">
964 <button
965 onClick={() => {
966 setShowEmailModal(false);
967 setSendViaEmail(false);
968 setOtherEmail('');
969 setAdditionalNotes('');
970 setShowEmailOptions(false);
971 }}
972 className="flex-1 px-4 py-2 bg-slate-300 text-slate-700 rounded-lg hover:bg-slate-400 font-semibold"
973 >
974 Cancel
975 </button>
976 <button
977 onClick={handleSubmitWithEmail}
978 disabled={!sendViaEmail}
979 className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-semibold disabled:opacity-50 disabled:cursor-not-allowed"
980 >
981 Create & Send
982 </button>
983 </div>
984 </div>
985 </div>
986 </div>
987 )}
988 </div>
989 </div>
990 );
991}