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";
2import { useEffect, useState } from "react";
3import { useRouter } from "next/navigation";
4import { Icon } from '@/contexts/IconContext';
5import { apiClient } from "@/lib/client/apiClient";
6import { SkeletonCard } from "@/components/LoadingSpinner";
7
8interface Product {
9 Pid: number; // Product ID
10 Name: string; // Product Name
11 Barcode?: string; // Barcode
12 SellPrice: number; // Selling Price
13 StockLevel?: number; // Current Stock Level
14 StockMin?: number; // Minimum Stock Level
15 PLU?: string; // PLU Code
16 Key?: string; // Unique Key
17 SupplierId?: string | number;
18 SupplierName?: string;
19}
20
21export default function ProductsPage() {
22 const router = useRouter();
23 const [products, setProducts] = useState<Product[]>([]);
24 const [searchTerm, setSearchTerm] = useState("");
25 const [sohFilter, setSohFilter] = useState<"all" | "in" | "out" | "belowMin">("all");
26 const [searchIn, setSearchIn] = useState<"all" | "name" | "barcode" | "plu" | "pid">("all");
27 const [minPrice, setMinPrice] = useState<string>("");
28 const [maxPrice, setMaxPrice] = useState<string>("");
29 const [lowStockFirst, setLowStockFirst] = useState<boolean>(false);
30 // Department filter
31 const [departmentId, setDepartmentId] = useState<string>("");
32 const [departments, setDepartments] = useState<Array<{ id: string; name: string }>>([]);
33 // Supplier filter
34 const [supplierSearch, setSupplierSearch] = useState("");
35 const [supplierId, setSupplierId] = useState<string>("");
36 const [supplierName, setSupplierName] = useState("");
37 const [showSupplierDropdown, setShowSupplierDropdown] = useState(false);
38 const [suppliers, setSuppliers] = useState<Array<{ id: string; name: string }>>([]);
39 const [loading, setLoading] = useState(false);
40 const [error, setError] = useState<string | null>(null);
41 const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
42 const [showStockModal, setShowStockModal] = useState(false);
43 const [stockAdjustment, setStockAdjustment] = useState<string>("");
44 const [stockReason, setStockReason] = useState<string>("");
45 const [saving, setSaving] = useState(false);
46 const [saveMessage, setSaveMessage] = useState<string>("");
47
48 // Pagination state
49 const [currentPage, setCurrentPage] = useState(0);
50 const [pageSize] = useState(50);
51 const [totalCount, setTotalCount] = useState(0);
52 const [hasMore, setHasMore] = useState(true);
53
54 useEffect(() => {
55 setCurrentPage(0); // Reset to first page on mount
56 loadProducts(0);
57 loadDepartments(); // Load departments for filter
58 }, []);
59
60 const loadDepartments = async () => {
61 try {
62 const result = await apiClient.get('/v1/elink/departments');
63 if (result.success && result.data) {
64 const depts = result.data.DATS || [];
65 const mapped = depts.map((d: any) => ({
66 id: String(d.f100 || ''),
67 name: d.f101 || d.f500 || `Dept ${d.f100}`
68 })).filter((d: any) => d.id && d.name);
69 setDepartments(mapped);
70 }
71 } catch (error) {
72 console.error('Failed to load departments:', error);
73 }
74 };
75
76 useEffect(() => {
77 // Debounce search - reset to page 0 when search changes
78 const timer = setTimeout(() => {
79 setCurrentPage(0);
80 loadProducts(0);
81 }, 400); // 400ms debounce for better UX
82 return () => clearTimeout(timer);
83 }, [searchTerm, supplierId, minPrice, maxPrice, sohFilter, departmentId]);
84
85 const clearAllFilters = () => {
86 setSearchTerm("");
87 setSohFilter("all");
88 setSearchIn("all");
89 setMinPrice("");
90 setMaxPrice("");
91 setLowStockFirst(false);
92 setDepartmentId("");
93 setSupplierSearch("");
94 setSupplierId("");
95 setSupplierName("");
96 setShowSupplierDropdown(false);
97 setCurrentPage(0);
98 // Refresh list
99 setTimeout(() => loadProducts(0), 0);
100 };
101
102 const loadProducts = async (page: number = currentPage) => {
103 try {
104 setLoading(true);
105 setError(null);
106
107 console.log('🔍 Loading products - Page:', page, 'PageSize:', pageSize, 'Skip:', page * pageSize);
108
109 // Build parameters for OpenAPI with server-side filtering
110 const params: any = {
111 limit: pageSize,
112 skip: page * pageSize,
113 source: 'openapi'
114 };
115
116 // Search filter - send raw search term, server will handle BUCK format
117 if (searchTerm.trim()) {
118 const term = searchTerm.trim();
119 switch (searchIn) {
120 case 'name':
121 params.search = term; // Server searches f101 (name/description)
122 break;
123 case 'barcode':
124 params.barcode = term; // Server searches f105 (barcode)
125 break;
126 case 'plu':
127 params.plu = term; // Server searches f102 (PLU)
128 break;
129 case 'pid':
130 // For PID search, use search with numeric value
131 params.search = term;
132 break;
133 default:
134 // General search - server will search name/description
135 params.search = term;
136 }
137 }
138
139 // Stock filter - server-side (f130)
140 if (sohFilter === 'in') {
141 params.stockFilter = 'in'; // Server filters: f130 > 0
142 } else if (sohFilter === 'out') {
143 params.stockFilter = 'out'; // Server filters: f130 = 0
144 }
145 // belowMin requires client-side filtering (f130 <= f131 comparison)
146
147 // Price range filters - server-side (f103)
148 if (minPrice) {
149 const min = Number(minPrice);
150 if (!isNaN(min)) {
151 params.minPrice = min; // Server filters: f103 >= min
152 }
153 }
154 if (maxPrice) {
155 const max = Number(maxPrice);
156 if (!isNaN(max)) {
157 params.maxPrice = max; // Server filters: f103 <= max
158 }
159 }
160
161 // Supplier filter - server-side (f104)
162 if (supplierId && supplierId !== 'all' && supplierId.trim()) {
163 params.supplierId = supplierId; // Server filters: f104 = supplierId
164 }
165
166 // Department filter - server-side (f106)
167 if (departmentId && departmentId !== 'all' && departmentId.trim()) {
168 params.departmentId = departmentId; // Server filters: f106 = departmentId
169 }
170
171 console.log('🌐 API Request params:', params);
172
173 // Use apiClient - server will handle ELINK/BUCK API with server-side filtering
174 const result = await apiClient.getProducts(params);
175
176 // Handle OpenAPI response
177 if (result.success) {
178 const apiData = result.data as any;
179
180 // ELINK API returns: { data: { value: [...] } }
181 let productData = apiData?.data?.Product || apiData?.value || [];
182
183 // Apply remaining client-side filters that can't be done server-side
184 if (Array.isArray(productData)) {
185 // belowMin filter (requires f130 <= f131 comparison)
186 if (sohFilter === 'belowMin') {
187 productData = productData.filter((p: any) =>
188 (p.StockLevel || p.f130 || 0) <= (p.StockMin || p.f131 || 0)
189 );
190 }
191 }
192
193 // Pagination handled on server now, but keep total count
194 const totalProducts = Array.isArray(productData) ? productData.length : 0;
195
196 console.log('📊 Server response:', {
197 total: totalProducts,
198 page: page + 1,
199 pageSize,
200 itemsOnPage: productData.length,
201 firstProductSample: productData[0]
202 });
203
204 if (Array.isArray(productData)) {
205 // Map OpenAPI format fields to component interface
206 const mappedProducts = productData.map((p: any) => ({
207 Pid: p.Pid,
208 Name: p.Name || p.Description || '',
209 Barcode: p.Barcode || p.Plu || '',
210 SellPrice: p.SellPrice || p.Price || 0,
211 StockLevel: p.StockLevel,
212 StockMin: p.StockMin,
213 PLU: p.PLU || p.Plu,
214 Key: p.Key,
215 SupplierId: p.Supplier?.Id || p.SupplierId || undefined,
216 SupplierName: p.Supplier?.Name || p.SupplierName || undefined,
217 }));
218
219 setProducts(mappedProducts);
220 setTotalCount(totalProducts);
221 setHasMore(mappedProducts.length >= pageSize); // More pages if we got full page
222
223 if (mappedProducts.length === 0 && page === 0) {
224 setError("No products found matching your filters");
225 }
226 } else {
227 setError("No products found");
228 setProducts([]);
229 setTotalCount(0);
230 }
231 } else {
232 setError("Failed to load products");
233 setProducts([]);
234 }
235 } catch (e) {
236 console.error('Failed to load products:', e);
237 setError("Network error while fetching products");
238 setProducts([]);
239 } finally {
240 setLoading(false);
241 }
242 };
243
244 // Server-side filtering is now handled in loadProducts()
245 // Only need to compute low stock products for stats
246 const isLowStock = (p: Product) => {
247 if (p.StockLevel === 0) return true;
248 if (p.StockLevel !== undefined && p.StockMin !== undefined) return p.StockLevel <= p.StockMin;
249 return false;
250 };
251
252 const handleStockUpdate = async () => {
253 if (!selectedProduct) return;
254
255 const adjustment = parseFloat(stockAdjustment);
256 if (isNaN(adjustment) || adjustment === 0) {
257 setSaveMessage("Please enter a valid adjustment amount");
258 return;
259 }
260
261 if (!stockReason) {
262 setSaveMessage("Please select a reason");
263 return;
264 }
265
266 try {
267 setSaving(true);
268 setSaveMessage("");
269
270 const response = await fetch('/api/v1/products/stock-adjustment', {
271 method: 'POST',
272 headers: {
273 'Content-Type': 'application/json',
274 },
275 body: JSON.stringify({
276 productId: selectedProduct.Pid,
277 adjustment,
278 reason: stockReason,
279 locationId: 1 // Default location
280 }),
281 });
282
283 const result = await response.json();
284
285 if (result.success) {
286 setSaveMessage(`✓ ${result.message || 'Stock updated successfully'}`);
287 // Refresh products list
288 setTimeout(() => {
289 loadProducts(currentPage);
290 // Close modal after 1.5 seconds
291 setTimeout(() => {
292 setShowStockModal(false);
293 setSelectedProduct(null);
294 setStockAdjustment("");
295 setStockReason("");
296 setSaveMessage("");
297 }, 1500);
298 }, 500);
299 } else {
300 setSaveMessage(`✗ ${result.error || 'Failed to update stock'}`);
301 }
302 } catch (err) {
303 console.error('Stock update error:', err);
304 setSaveMessage('✗ Network error. Please try again.');
305 } finally {
306 setSaving(false);
307 }
308 };
309
310 const lowStockProducts = products.filter(p =>
311 p.StockLevel !== undefined && p.StockMin !== undefined && p.StockLevel <= p.StockMin
312 );
313
314 return (
315 <div className="p-8 min-h-screen bg-bg">
316 <div className="flex justify-between items-center mb-6">
317 <h1 className="text-3xl font-bold text-text flex items-center gap-2">
318 Products <Icon name="inventory_2" size={32} />
319 </h1>
320 <div className="flex gap-3">
321 <button
322 onClick={() => {
323 setSelectedProduct(null);
324 setShowStockModal(true);
325 }}
326 className="px-4 py-2 bg-brand text-white rounded-lg hover:opacity-90 font-medium"
327 >
328 + Create Product
329 </button>
330 <button
331 onClick={() => {
332 window.location.href = '/pages/products/stocktake';
333 }}
334 className="px-4 py-2 bg-brand text-white rounded-lg hover:opacity-90 font-medium flex items-center gap-2"
335 >
336 <Icon name="inventory_2" size={20} /> Manage Stock
337 </button>
338 </div>
339 </div>
340
341 {/* Stats */}
342 <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
343 <div className="bg-brand-2 p-4 rounded-lg border border-border">
344 <div className="text-2xl font-bold text-brand">{totalCount || products.length}</div>
345 <div className="text-sm text-text">Total Products</div>
346 </div>
347 <div className="bg-surface-2 p-4 rounded-lg border border-warn">
348 <div className="text-2xl font-bold text-warn">{lowStockProducts.length}</div>
349 <div className="text-sm text-text">Low Stock (Current Page)</div>
350 </div>
351 <div className="bg-surface p-4 rounded-lg border border-border">
352 <div className="text-2xl font-bold text-text">{products.length}</div>
353 <div className="text-sm text-muted">Products on Page {currentPage + 1}</div>
354 </div>
355 </div>
356
357 <div className="bg-surface p-6 rounded-lg shadow border border-border">
358 <div className="grid grid-cols-1 md:grid-cols-4 gap-3 mb-4">
359 <div>
360 <label className="block text-sm font-medium text-text mb-1">Search</label>
361 <input
362 type="text"
363 placeholder="Name, barcode or ID..."
364 className="w-full p-3 border border-border rounded-lg bg-surface text-text focus:ring-2 focus:ring-brand"
365 value={searchTerm}
366 onChange={(e) => setSearchTerm(e.target.value)}
367 />
368 </div>
369 <div>
370 <label className="block text-sm font-medium text-text mb-1">Search In</label>
371 <select
372 className="w-full p-3 border rounded-lg"
373 value={searchIn}
374 onChange={(e) => setSearchIn(e.target.value as any)}
375 >
376 <option value="all">All fields</option>
377 <option value="name">Name only</option>
378 <option value="barcode">Barcode only</option>
379 <option value="plu">PLU only</option>
380 <option value="pid">Product ID only</option>
381 </select>
382 </div>
383 <div className="relative">
384 <label className="block text-sm font-medium text-text mb-1">Supplier</label>
385 <div className="flex gap-2">
386 <input
387 type="text"
388 placeholder="Search supplier name or ID..."
389 className="w-full p-3 border border-border rounded-lg bg-surface text-text focus:ring-2 focus:ring-brand"
390 value={supplierSearch}
391 onChange={async (e) => {
392 const v = e.target.value;
393 setSupplierSearch(v);
394 setShowSupplierDropdown(true);
395 // Live search suppliers
396 const resp = await apiClient.getSuppliers({ search: v, limit: 20 });
397 const arr: any[] = (resp.data as any[]) || [];
398 const mapped = arr.map((s: any) => ({ id: String(s.f100 || s.Id || s.id || ''), name: s.f101 || s.Name || s.name || '' }));
399 setSuppliers(mapped.filter(s => s.id && s.name));
400 }}
401 onFocus={async () => {
402 setShowSupplierDropdown(true);
403 if (suppliers.length === 0) {
404 const resp = await apiClient.getSuppliers({ limit: 20 });
405 const arr: any[] = (resp.data as any[]) || [];
406 const mapped = arr.map((s: any) => ({ id: String(s.f100 || s.Id || s.id || ''), name: s.f101 || s.Name || s.name || '' }));
407 setSuppliers(mapped.filter(s => s.id && s.name));
408 }
409 }}
410 />
411 {supplierId && (
412 <button
413 className="px-3 rounded bg-surface-2 border border-border text-sm text-text"
414 onClick={() => { setSupplierId(''); setSupplierName(''); setSupplierSearch(''); }}
415 title="Clear supplier"
416 >
417 Clear
418 </button>
419 )}
420 </div>
421 {showSupplierDropdown && suppliers.length > 0 && (
422 <div className="absolute z-20 bg-surface border border-border rounded shadow w-full max-h-56 overflow-auto mt-1">
423 {suppliers.map(s => (
424 <div
425 key={s.id}
426 className="px-3 py-2 hover:bg-surface-2 cursor-pointer"
427 onClick={() => {
428 setSupplierId(s.id);
429 setSupplierName(s.name);
430 setSupplierSearch(s.name);
431 setShowSupplierDropdown(false);
432 }}
433 >
434 <div className="text-sm text-text">{s.name}</div>
435 <div className="text-xs text-muted">ID: {s.id}</div>
436 </div>
437 ))}
438 </div>
439 )}
440 {supplierName && (
441 <div className="mt-1 text-xs text-brand">Selected: {supplierName}</div>
442 )}
443 </div>
444 <div>
445 <label className="block text-sm font-medium text-text mb-1">Department</label>
446 <select
447 className="w-full p-3 border border-border rounded-lg bg-surface text-text focus:ring-2 focus:ring-brand"
448 value={departmentId}
449 onChange={(e) => setDepartmentId(e.target.value)}
450 >
451 <option value="">All Departments</option>
452 {departments.map(dept => (
453 <option key={dept.id} value={dept.id}>
454 {dept.name}
455 </option>
456 ))}
457 </select>
458 </div>
459 <div>
460 <label className="block text-sm font-medium text-text mb-1">Stock On Hand</label>
461 <select
462 className="w-full p-3 border border-border rounded-lg bg-surface text-text focus:ring-2 focus:ring-brand"
463 value={sohFilter}
464 onChange={(e) => setSohFilter(e.target.value as any)}
465 >
466 <option value="all">All</option>
467 <option value="in">In Stock (&gt; 0)</option>
468 <option value="out">Out of Stock (0)</option>
469 <option value="belowMin">Below Min</option>
470 </select>
471 </div>
472 </div>
473
474 <div className="grid grid-cols-1 md:grid-cols-4 gap-3 mb-4">
475 <div className="md:col-span-2">
476 <label className="block text-sm font-medium text-text mb-1">Price Range</label>
477 <div className="flex gap-2">
478 <input
479 type="number"
480 min={0}
481 step={0.01}
482 placeholder="Min"
483 className="w-full p-3 border border-border rounded-lg bg-surface text-text focus:ring-2 focus:ring-brand"
484 value={minPrice}
485 onChange={(e) => setMinPrice(e.target.value)}
486 />
487 <input
488 type="number"
489 min={0}
490 step={0.01}
491 placeholder="Max"
492 className="w-full p-3 border border-border rounded-lg bg-surface text-text focus:ring-2 focus:ring-brand"
493 value={maxPrice}
494 onChange={(e) => setMaxPrice(e.target.value)}
495 />
496 </div>
497 </div>
498 <div className="flex items-end">
499 <label className="flex items-center gap-2 text-sm text-text">
500 <input
501 type="checkbox"
502 className="rounded"
503 checked={lowStockFirst}
504 onChange={(e) => setLowStockFirst(e.target.checked)}
505 />
506 Low stock first
507 </label>
508 </div>
509 </div>
510
511 <div className="flex justify-end mb-2">
512 <button
513 className="text-sm px-3 py-2 border border-border rounded hover:bg-surface-2 bg-surface text-text"
514 onClick={clearAllFilters}
515 title="Reset all filters"
516 >
517 Clear filters
518 </button>
519 </div>
520
521 {/* Error State */}
522 {error && (
523 <div className="bg-warn/10 border border-warn rounded-lg p-4 mb-4">
524 <div className="flex items-center">
525 <span className="text-warn">⚠️</span>
526 <span className="ml-2 text-warn">{error}</span>
527 <button
528 onClick={() => loadProducts(currentPage)}
529 className="ml-auto text-warn hover:opacity-80 underline"
530 >
531 Retry
532 </button>
533 </div>
534 </div>
535 )}
536
537 <div className="max-h-[540px] overflow-y-auto border border-border rounded-lg relative">
538 {loading ? (
539 <div className="p-4 space-y-3 bg-surface">
540 {[...Array(8)].map((_, i) => (
541 <div key={i} className="p-3 border-b border-border animate-pulse">
542 <div className="h-5 bg-surface-2 rounded w-3/4 mb-2"></div>
543 <div className="h-3 bg-surface-2 rounded w-1/2 mb-2"></div>
544 <div className="h-3 bg-surface-2 rounded w-1/4"></div>
545 </div>
546 ))}
547 </div>
548 ) : products.length === 0 ? (
549 <div className="p-6 text-center text-muted bg-surface">
550 {searchTerm ? `No products match "${searchTerm}"` : "No products found"}
551 </div>
552 ) : (
553 products.map(p => (
554 <div
555 key={p.Pid}
556 onClick={() => router.push(`/pages/products/${p.Pid}`)}
557 className="p-3 border-b border-border flex justify-between items-center hover:bg-surface-2 cursor-pointer bg-surface"
558 >
559 <div className="flex-1">
560 <div className="font-medium text-text">{p.Name || "Unnamed Product"}</div>
561 <div className="text-xs text-muted">
562 ID: {p.Pid}
563 {p.Barcode && ` • ${p.Barcode}`}
564 {p.PLU && ` • PLU: ${p.PLU}`}
565 {p.SupplierName && ` • Supplier: ${p.SupplierName}`}
566 </div>
567 {p.StockLevel !== undefined && (
568 <div className="text-xs">
569 <span className={`font-medium ${
570 p.StockMin !== undefined && p.StockLevel <= p.StockMin
571 ? 'text-danger'
572 : 'text-brand'
573 }`}>
574 Stock: {p.StockLevel}
575 {p.StockMin !== undefined && ` (Min: ${p.StockMin})`}
576 </span>
577 </div>
578 )}
579 </div>
580 <div className="flex items-center gap-3">
581 <div className="text-right">
582 <div className="text-brand font-bold">${p.SellPrice ? p.SellPrice.toFixed(2) : '0.00'}</div>
583 {p.StockLevel !== undefined && p.StockMin !== undefined && p.StockLevel <= p.StockMin && (
584 <div className="text-xs text-danger font-medium">LOW STOCK</div>
585 )}
586 </div>
587 <button
588 onClick={(e) => {
589 e.stopPropagation();
590 setSelectedProduct(p);
591 setShowStockModal(true);
592 }}
593 className="px-3 py-1 text-sm bg-brand text-white rounded hover:opacity-90"
594 >
595 Edit Stock
596 </button>
597 </div>
598 </div>
599 ))
600 )}
601 </div>
602
603 {/* Pagination Controls */}
604 {!loading && products.length > 0 && (
605 <div className="mt-4 flex justify-between items-center">
606 <div className="text-sm text-muted">
607 {totalCount ? (
608 <>
609 Showing {currentPage * pageSize + 1}-{Math.min((currentPage + 1) * pageSize, totalCount)} of {totalCount} products
610 </>
611 ) : (
612 <>
613 Page {currentPage + 1}
614 </>
615 )}
616 </div>
617 <div className="flex gap-2">
618 <button
619 onClick={() => {
620 const newPage = currentPage - 1;
621 setCurrentPage(newPage);
622 loadProducts(newPage);
623 }}
624 disabled={currentPage === 0}
625 className="px-4 py-2 border border-border rounded-lg bg-surface text-text disabled:opacity-50 disabled:cursor-not-allowed hover:bg-surface-2"
626 >
627 Previous
628 </button>
629 <button
630 onClick={() => {
631 const newPage = currentPage + 1;
632 setCurrentPage(newPage);
633 loadProducts(newPage);
634 }}
635 disabled={!hasMore || (totalCount > 0 && (currentPage + 1) * pageSize >= totalCount)}
636 className="px-4 py-2 border border-border rounded-lg bg-surface text-text disabled:opacity-50 disabled:cursor-not-allowed hover:bg-surface-2"
637 >
638 Next
639 </button>
640 </div>
641 </div>
642 )}
643 </div>
644
645 {/* Stock Edit Modal */}
646 {showStockModal && (
647 <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
648 <div className="bg-surface rounded-lg shadow-xl border border-border max-w-md w-full mx-4 p-6">
649 <div className="flex justify-between items-center mb-4">
650 <h2 className="text-xl font-bold text-text">
651 {selectedProduct ? `Edit Stock: ${selectedProduct.Name}` : 'Create New Product'}
652 </h2>
653 <button
654 onClick={() => {
655 setShowStockModal(false);
656 setSelectedProduct(null);
657 }}
658 className="text-muted hover:text-text"
659 >
660
661 </button>
662 </div>
663
664 {selectedProduct ? (
665 <div className="space-y-4">
666 <div>
667 <label className="block text-sm font-medium text-text mb-1">Product</label>
668 <div className="p-3 bg-surface-2 rounded border border-border">
669 <div className="font-medium text-text">{selectedProduct.Name}</div>
670 <div className="text-xs text-muted">ID: {selectedProduct.Pid}</div>
671 </div>
672 </div>
673
674 <div>
675 <label className="block text-sm font-medium text-text mb-1">Current Stock</label>
676 <div className="text-2xl font-bold text-text">
677 {selectedProduct.StockLevel ?? 'N/A'}
678 </div>
679 </div>
680
681 <div>
682 <label className="block text-sm font-medium text-text mb-1">Adjust Stock</label>
683 <input
684 type="number"
685 className="w-full p-3 border border-border rounded-lg bg-surface text-text focus:ring-2 focus:ring-brand"
686 placeholder="Enter adjustment (+/-)"
687 value={stockAdjustment}
688 onChange={(e) => setStockAdjustment(e.target.value)}
689 disabled={saving}
690 />
691 <div className="text-xs text-muted mt-1">
692 Enter positive number to add stock, negative to reduce
693 </div>
694 </div>
695
696 <div>
697 <label className="block text-sm font-medium text-text mb-1">Reason</label>
698 <select
699 className="w-full p-3 border border-border rounded-lg bg-surface text-text focus:ring-2 focus:ring-brand"
700 value={stockReason}
701 onChange={(e) => setStockReason(e.target.value)}
702 disabled={saving}
703 >
704 <option value="">Select reason...</option>
705 <option value="received">Stock Received</option>
706 <option value="sale">Sale</option>
707 <option value="damage">Damage/Loss</option>
708 <option value="return">Return</option>
709 <option value="adjustment">Manual Adjustment</option>
710 </select>
711 </div>
712
713 {saveMessage && (
714 <div className={`p-3 rounded-lg text-sm ${
715 saveMessage.startsWith('✓')
716 ? 'bg-brand/10 text-brand border border-brand'
717 : 'bg-danger/10 text-danger border border-danger'
718 }`}>
719 {saveMessage}
720 </div>
721 )}
722
723 <div className="flex gap-3 pt-4">
724 <button
725 onClick={handleStockUpdate}
726 disabled={saving}
727 className="flex-1 px-4 py-2 bg-brand text-white rounded-lg hover:opacity-90 font-medium disabled:opacity-50 disabled:cursor-not-allowed"
728 >
729 {saving ? 'Updating...' : 'Update Stock'}
730 </button>
731 <button
732 onClick={() => {
733 setShowStockModal(false);
734 setSelectedProduct(null);
735 setStockAdjustment("");
736 setStockReason("");
737 setSaveMessage("");
738 }}
739 disabled={saving}
740 className="px-4 py-2 border border-border rounded-lg bg-surface text-text hover:bg-surface-2 disabled:opacity-50"
741 >
742 Cancel
743 </button>
744 </div>
745 </div>
746 ) : (
747 <div className="space-y-4">
748 <div>
749 <label className="block text-sm font-medium text-text mb-1">Product Name</label>
750 <input
751 type="text"
752 className="w-full p-3 border border-border rounded-lg bg-surface text-text focus:ring-2 focus:ring-brand"
753 placeholder="Enter product name"
754 />
755 </div>
756
757 <div>
758 <label className="block text-sm font-medium text-text mb-1">Barcode/PLU</label>
759 <input
760 type="text"
761 className="w-full p-3 border border-border rounded-lg bg-surface text-text focus:ring-2 focus:ring-brand"
762 placeholder="Enter barcode"
763 />
764 </div>
765
766 <div>
767 <label className="block text-sm font-medium text-text mb-1">Sell Price</label>
768 <input
769 type="number"
770 step="0.01"
771 className="w-full p-3 border border-border rounded-lg bg-surface text-text focus:ring-2 focus:ring-brand"
772 placeholder="0.00"
773 />
774 </div>
775
776 <div>
777 <label className="block text-sm font-medium text-text mb-1">Initial Stock</label>
778 <input
779 type="number"
780 className="w-full p-3 border border-border rounded-lg bg-surface text-text focus:ring-2 focus:ring-brand"
781 placeholder="0"
782 />
783 </div>
784
785 <div className="flex gap-3 pt-4">
786 <button
787 onClick={() => {
788 alert('Product creation functionality will be implemented with API integration');
789 }}
790 className="flex-1 px-4 py-2 bg-brand text-white rounded-lg hover:opacity-90 font-medium"
791 >
792 Create Product
793 </button>
794 <button
795 onClick={() => {
796 setShowStockModal(false);
797 setSelectedProduct(null);
798 }}
799 className="px-4 py-2 border border-border rounded-lg bg-surface text-text hover:bg-surface-2"
800 >
801 Cancel
802 </button>
803 </div>
804 </div>
805 )}
806 </div>
807 </div>
808 )}
809 </div>
810 );
811}