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 { Icon } from '@/contexts/IconContext';
6
7interface Product {
8 Pid: number;
9 Description: string;
10 Plu?: string;
11 Price?: number;
12 department?: number;
13 Barcodes?: string[];
14}
15
16interface PrinterCartridge {
17 printerId: number;
18 printerName: string;
19 cartridges: number[];
20}
21
22export default function PrintersCartridgesPage() {
23 const [products, setProducts] = useState<Product[]>([]);
24 const [printers, setPrinters] = useState<Product[]>([]);
25 const [cartridges, setCartridges] = useState<Product[]>([]);
26 const [relationships, setRelationships] = useState<PrinterCartridge[]>([]);
27 const [loading, setLoading] = useState(true);
28 const [saving, setSaving] = useState(false);
29 const [searchTerm, setSearchTerm] = useState('');
30 const [selectedPrinter, setSelectedPrinter] = useState<Product | null>(null);
31 const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
32 const [expandedPrinters, setExpandedPrinters] = useState<Set<number>>(new Set());
33
34 useEffect(() => {
35 loadData();
36 }, []);
37
38 const loadData = async () => {
39 try {
40 setLoading(true);
41
42 console.log('[Printers-Cartridges] Loading products...');
43
44 // Load all products using eLink/BUCK API for comprehensive field data
45 const result = await apiClient.getProducts({
46 limit: 10000,
47 source: 'elink' // Use eLink/BUCK API to get field codes (f100, f101, etc.)
48 });
49
50 console.log('[Printers-Cartridges] API result:', result);
51
52 if (result.success && result.data) {
53 // eLink returns data in result.data.data.Product format
54 const allProducts = (result.data as any)?.data?.Product || (result.data as any)?.Product || (result.data as any)?.DATS || [];
55 console.log(`[Printers-Cartridges] Loaded ${allProducts.length} products`);
56
57 // Debug: Check first few products
58 if (allProducts.length > 0) {
59 console.log('[Printers-Cartridges] Sample product:', allProducts[0]);
60 }
61
62 // Filter printers (department 6 or contains "printer" in description)
63 const printerList = allProducts.filter((p: any) => {
64 const dept = p.f106 || p.Depid || p.department;
65 const desc = (p.f101 || p.Description || '').toLowerCase();
66 const isPrinterDept = dept === 6;
67 const isPrinterName = /\b(printer|printers|multifunction|mfc|all.in.one)\b/i.test(desc);
68 const match = isPrinterDept || isPrinterName;
69
70 if (match) {
71 console.log('[Printers-Cartridges] Found printer:', desc, '(dept:', dept, ')');
72 }
73
74 return match;
75 }).map((p: any) => ({
76 Pid: p.f100 || p.Pid,
77 Description: p.f101 || p.Description,
78 Plu: p.f102 || p.Plu,
79 Price: p.f103 || p.Price,
80 department: p.f106 || p.Depid || p.department,
81 Barcodes: p.f513 || p.Barcodes
82 }));
83
84 console.log(`[Printers-Cartridges] Found ${printerList.length} printers`);
85
86 // Filter cartridges (contains cartridge/toner/ink/drum keywords)
87 const cartridgeList = allProducts.filter((p: any) => {
88 const desc = (p.f101 || p.Description || '').toLowerCase();
89 return /\b(cartridge|toner|ink|drum)\b/i.test(desc);
90 }).map((p: any) => ({
91 Pid: p.f100 || p.Pid,
92 Description: p.f101 || p.Description,
93 Plu: p.f102 || p.Plu,
94 Price: p.f103 || p.Price,
95 department: p.f106 || p.Depid || p.department,
96 Barcodes: p.f513 || p.Barcodes
97 }));
98
99 console.log(`[Printers-Cartridges] Found ${cartridgeList.length} cartridges`);
100
101 setProducts(allProducts);
102 setPrinters(printerList.sort((a: any, b: any) => a.Description.localeCompare(b.Description)));
103 setCartridges(cartridgeList.sort((a: any, b: any) => a.Description.localeCompare(b.Description)));
104
105 // Load saved relationships
106 await loadRelationships();
107 } else {
108 console.error('[Printers-Cartridges] API call failed:', result);
109 showMessage('error', 'Failed to load products from Fieldpine');
110 }
111 } catch (error) {
112 console.error('Error loading data:', error);
113 showMessage('error', 'Failed to load printer and cartridge data');
114 } finally {
115 setLoading(false);
116 }
117 };
118
119 const loadRelationships = async () => {
120 try {
121 // Load existing relationships directly from eLink with pagination
122 console.log('[Printers-Cartridges] Loading relationships from eLink...');
123
124 const result = await apiClient.getBuckData({
125 table: 'retailmax.elink.printercartridge',
126 want: 'all',
127 limit: 5000 // Load in batches to avoid timeout
128 });
129
130 if (!result.success || !result.data?.APPD) {
131 console.log('[Printers-Cartridges] No relationships found');
132 setRelationships([]);
133 return;
134 }
135
136 const rows = result.data.APPD;
137 console.log(`[Printers-Cartridges] Loaded ${rows.length} relationship rows`);
138
139 // Group by printer ID
140 const relationshipMap = new Map<number, PrinterCartridge>();
141
142 for (const row of rows) {
143 const printerId = Number(row.f100) || 0; // printer pid
144 const cartridgeId = Number(row.f101) || 0; // cartridge pid
145
146 if (!printerId || !cartridgeId) continue;
147
148 if (!relationshipMap.has(printerId)) {
149 const printer = printers.find(p => p.Pid === printerId);
150 relationshipMap.set(printerId, {
151 printerId,
152 printerName: printer?.Description || `Printer ${printerId}`,
153 cartridges: []
154 });
155 }
156
157 relationshipMap.get(printerId)!.cartridges.push(cartridgeId);
158 }
159
160 setRelationships(Array.from(relationshipMap.values()));
161 } catch (error) {
162 console.error('[Printers-Cartridges] Error loading relationships:', error);
163 setRelationships([]);
164 }
165 };
166
167 const showMessage = (type: 'success' | 'error', text: string) => {
168 setMessage({ type, text });
169 setTimeout(() => setMessage(null), 3000);
170 };
171
172 const handleSave = async () => {
173 try {
174 setSaving(true);
175
176 const response = await fetch('/api/v1/printers-cartridges', {
177 method: 'POST',
178 headers: { 'Content-Type': 'application/json' },
179 body: JSON.stringify({ relationships })
180 });
181
182 const result = await response.json();
183
184 if (result.success) {
185 showMessage('success', 'Printer-cartridge relationships saved successfully!');
186 } else {
187 showMessage('error', result.error || 'Failed to save relationships');
188 }
189 } catch (error) {
190 console.error('Error saving relationships:', error);
191 showMessage('error', 'Failed to save printer-cartridge relationships');
192 } finally {
193 setSaving(false);
194 }
195 };
196
197 const addCartridgeToPrinter = (printerId: number, cartridgeId: number) => {
198 setRelationships(prev => {
199 const existing = prev.find(r => r.printerId === printerId);
200
201 if (existing) {
202 // Add cartridge if not already present
203 if (!existing.cartridges.includes(cartridgeId)) {
204 return prev.map(r =>
205 r.printerId === printerId
206 ? { ...r, cartridges: [...r.cartridges, cartridgeId] }
207 : r
208 );
209 }
210 return prev;
211 } else {
212 // Create new relationship
213 const printer = printers.find(p => p.Pid === printerId);
214 return [...prev, {
215 printerId,
216 printerName: printer?.Description || '',
217 cartridges: [cartridgeId]
218 }];
219 }
220 });
221 };
222
223 const removeCartridgeFromPrinter = (printerId: number, cartridgeId: number) => {
224 setRelationships(prev =>
225 prev.map(r =>
226 r.printerId === printerId
227 ? { ...r, cartridges: r.cartridges.filter(id => id !== cartridgeId) }
228 : r
229 ).filter(r => r.cartridges.length > 0) // Remove empty relationships
230 );
231 };
232
233 const toggleExpanded = (printerId: number) => {
234 setExpandedPrinters(prev => {
235 const newSet = new Set(prev);
236 if (newSet.has(printerId)) {
237 newSet.delete(printerId);
238 } else {
239 newSet.add(printerId);
240 }
241 return newSet;
242 });
243 };
244
245 const getCompatibleCartridges = (printerId: number): Product[] => {
246 const relationship = relationships.find(r => r.printerId === printerId);
247 if (!relationship) return [];
248
249 return relationship.cartridges
250 .map(cartId => cartridges.find(c => c.Pid === cartId))
251 .filter((c): c is Product => c !== undefined);
252 };
253
254 const filteredPrinters = printers.filter(p =>
255 searchTerm === '' ||
256 p.Description.toLowerCase().includes(searchTerm.toLowerCase()) ||
257 p.Plu?.toLowerCase().includes(searchTerm.toLowerCase())
258 );
259
260 const filteredCartridges = cartridges.filter(c =>
261 searchTerm === '' ||
262 c.Description.toLowerCase().includes(searchTerm.toLowerCase()) ||
263 c.Plu?.toLowerCase().includes(searchTerm.toLowerCase())
264 );
265
266 if (loading) {
267 return (
268 <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
269 <div className="text-center py-12">
270 <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-brand mx-auto"></div>
271 <p className="mt-4 text-muted">Loading printer and cartridge data...</p>
272 </div>
273 </div>
274 );
275 }
276
277 return (
278 <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
279 <div className="mb-8">
280 <div className="flex items-center gap-3 mb-2">
281 <Icon name="print" size={32} className="text-brand" />
282 <h1 className="text-3xl font-bold text-text">Printers &amp; Cartridges</h1>
283 </div>
284 <p className="mt-2 text-muted">
285 Maintain links between printer models and their compatible cartridges. This data powers the printer search on the sales screen.
286 </p>
287 </div>
288
289 {/* Message Banner */}
290 {message && (
291 <div className={`mb-6 p-4 rounded-lg flex items-center gap-2 ${
292 message.type === 'success' ? 'bg-success/10 text-success' : 'bg-danger/10 text-danger'
293 }`}>
294 <Icon name={message.type === 'success' ? 'check_circle' : 'error'} size={20} />
295 {message.text}
296 </div>
297 )}
298
299 {/* Getting Started Info */}
300 {!loading && relationships.length === 0 && (
301 <div className="mb-6 p-4 rounded-lg bg-info/10 border border-info/30">
302 <div className="flex items-start gap-2">
303 <Icon name="info" size={20} className="text-info mt-0.5" />
304 <div>
305 <h3 className="font-semibold text-info mb-2">Manage Printer-Cartridge Relationships</h3>
306 <p className="text-info/90 text-sm">
307 Click on a printer in the left panel, then click compatible cartridges in the right panel to create or modify links.
308 </p>
309 </div>
310 </div>
311 </div>
312 )}
313
314 {/* Stats */}
315 <div className="grid grid-cols-3 gap-4 mb-6">
316 <div className="bg-brand/10 border border-brand/30 rounded-lg p-4 text-center">
317 <Icon name="print" size={28} className="text-brand mx-auto mb-2" />
318 <div className="text-2xl font-bold text-text">{printers.length}</div>
319 <div className="text-sm text-muted">Printers</div>
320 </div>
321 <div className="bg-brand2/10 border border-brand2/30 rounded-lg p-4 text-center">
322 <Icon name="inventory_2" size={28} className="text-brand2 mx-auto mb-2" />
323 <div className="text-2xl font-bold text-text">{cartridges.length}</div>
324 <div className="text-sm text-muted">Cartridges</div>
325 </div>
326 <div className="bg-success/10 border border-success/30 rounded-lg p-4 text-center">
327 <Icon name="link" size={28} className="text-success mx-auto mb-2" />
328 <div className="text-2xl font-bold text-text">{relationships.length}</div>
329 <div className="text-sm text-muted">Relationships</div>
330 </div>
331 </div>
332
333 {/* Action Buttons */}
334 <div className="mb-6 flex justify-between items-center">
335 <div className="relative w-96">
336 <Icon name="search" size={20} className="absolute left-3 top-1/2 -translate-y-1/2 text-muted" />
337 <input
338 type="text"
339 placeholder="Search printers or cartridges..."
340 value={searchTerm}
341 onChange={(e) => setSearchTerm(e.target.value)}
342 className="pl-10 pr-4 py-2 border border-border rounded-lg w-full bg-surface text-text focus:ring-2 focus:ring-brand focus:border-brand"
343 />
344 </div>
345 <button
346 onClick={handleSave}
347 disabled={saving}
348 className="px-6 py-3 bg-brand text-white rounded-lg hover:bg-brand/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed font-semibold flex items-center gap-2"
349 >
350 <Icon name={saving ? 'sync' : 'save'} size={20} className={saving ? 'animate-spin' : ''} />
351 {saving ? 'Saving...' : 'Save Relationships'}
352 </button>
353 </div>
354
355 <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
356 {/* Printers with Cartridges */}
357 <div className="bg-surface rounded-lg shadow-md p-6 border border-border">
358 <h2 className="text-xl font-bold text-text mb-4 flex items-center gap-2">
359 <Icon name="print" size={24} className="text-brand" />
360 Printers &amp; Compatible Cartridges
361 </h2>
362 <p className="text-sm text-muted mb-4">
363 Click on a printer to manage its compatible cartridges.
364 </p>
365
366 <div className="space-y-2 max-h-[600px] overflow-y-auto">
367 {filteredPrinters.map(printer => {
368 const compatible = getCompatibleCartridges(printer.Pid);
369 const isExpanded = expandedPrinters.has(printer.Pid);
370
371 return (
372 <div key={printer.Pid} className="border-2 border-border rounded-lg bg-surface-2">
373 <div
374 className="p-3 flex items-center justify-between cursor-pointer hover:bg-surface"
375 onClick={() => toggleExpanded(printer.Pid)}
376 >
377 <div className="flex items-center flex-1">
378 <Icon
379 name={isExpanded ? 'expand_more' : 'chevron_right'}
380 size={20}
381 className="mr-2 text-muted"
382 />
383 <div>
384 <div className="font-semibold text-text">{printer.Description}</div>
385 <div className="text-xs text-muted">
386 ID: {printer.Pid} {printer.Plu && `| PLU: ${printer.Plu}`}
387 </div>
388 </div>
389 </div>
390 <div className="flex items-center gap-2">
391 {compatible.length > 0 && (
392 <span className="text-xs bg-brand text-white px-2 py-1 rounded">
393 {compatible.length} cartridge{compatible.length !== 1 ? 's' : ''}
394 </span>
395 )}
396 <button
397 onClick={(e) => {
398 e.stopPropagation();
399 setSelectedPrinter(printer);
400 }}
401 className="text-brand hover:text-brand/80 text-sm px-3 py-1 bg-brand/10 rounded hover:bg-brand/20 flex items-center gap-1"
402 >
403 <Icon name="add" size={16} />
404 Add Cartridge
405 </button>
406 </div>
407 </div>
408
409 {isExpanded && compatible.length > 0 && (
410 <div className="px-3 pb-3 space-y-2">
411 <div className="ml-8 pl-4 border-l-2 border-brand/30 space-y-2">
412 {compatible.map(cartridge => (
413 <div
414 key={cartridge.Pid}
415 className="flex items-center justify-between p-2 bg-surface rounded border border-border"
416 >
417 <div className="flex-1">
418 <div className="text-sm text-text">{cartridge.Description}</div>
419 <div className="text-xs text-muted">
420 ID: {cartridge.Pid} {cartridge.Plu && `| PLU: ${cartridge.Plu}`}
421 {cartridge.Price && ` | $${cartridge.Price.toFixed(2)}`}
422 </div>
423 </div>
424 <button
425 onClick={() => removeCartridgeFromPrinter(printer.Pid, cartridge.Pid)}
426 className="text-danger hover:text-danger/80 text-xs px-2 py-1 flex items-center gap-1"
427 >
428 <Icon name="delete" size={16} />
429 Remove
430 </button>
431 </div>
432 ))}
433 </div>
434 </div>
435 )}
436
437 {isExpanded && compatible.length === 0 && (
438 <div className="px-3 pb-3">
439 <div className="ml-8 text-sm text-muted italic">
440 No compatible cartridges defined yet
441 </div>
442 </div>
443 )}
444 </div>
445 );
446 })}
447 </div>
448 </div>
449
450 {/* Cartridge Selection */}
451 <div className="bg-surface rounded-lg shadow-md p-6 border border-border">
452 <h2 className="text-xl font-bold text-text mb-4 flex items-center gap-2">
453 <Icon name="inventory_2" size={24} className="text-brand2" />
454 {selectedPrinter ? `Add Cartridge to: ${selectedPrinter.Description}` : 'Available Cartridges'}
455 </h2>
456
457 {selectedPrinter && (
458 <button
459 onClick={() => setSelectedPrinter(null)}
460 className="mb-4 text-sm text-brand hover:text-brand/80 flex items-center gap-1"
461 >
462 <Icon name="arrow_back" size={16} />
463 Back to all cartridges
464 </button>
465 )}
466
467 <p className="text-sm text-muted mb-4">
468 {selectedPrinter
469 ? 'Click on a cartridge to add it as compatible with the selected printer.'
470 : 'Select a printer from the left to add compatible cartridges.'}
471 </p>
472
473 <div className="space-y-2 max-h-[600px] overflow-y-auto">
474 {filteredCartridges.map(cartridge => {
475 const isAlreadyAdded = selectedPrinter &&
476 getCompatibleCartridges(selectedPrinter.Pid).some(c => c.Pid === cartridge.Pid);
477
478 return (
479 <div
480 key={cartridge.Pid}
481 onClick={() => {
482 if (selectedPrinter && !isAlreadyAdded) {
483 addCartridgeToPrinter(selectedPrinter.Pid, cartridge.Pid);
484 showMessage('success', `Added ${cartridge.Description} to ${selectedPrinter.Description}`);
485 }
486 }}
487 className={`p-3 border-2 rounded-lg transition-all ${
488 isAlreadyAdded
489 ? 'border-border bg-surface-2 opacity-50 cursor-not-allowed'
490 : selectedPrinter
491 ? 'border-border hover:border-brand hover:bg-brand/5 cursor-pointer'
492 : 'border-border bg-surface-2 cursor-default'
493 }`}
494 >
495 <div className="flex items-center justify-between">
496 <div className="flex-1">
497 <div className="font-medium text-text">{cartridge.Description}</div>
498 <div className="text-xs text-muted">
499 ID: {cartridge.Pid} {cartridge.Plu && `| PLU: ${cartridge.Plu}`}
500 {cartridge.Price && ` | $${cartridge.Price.toFixed(2)}`}
501 </div>
502 </div>
503 {isAlreadyAdded && (
504 <span className="text-xs bg-muted/20 text-muted px-2 py-1 rounded">
505 Already Added
506 </span>
507 )}
508 {selectedPrinter && !isAlreadyAdded && (
509 <span className="text-xs text-brand flex items-center gap-1">
510 Click to add
511 <Icon name="arrow_forward" size={14} />
512 </span>
513 )}
514 </div>
515 </div>
516 );
517 })}
518 </div>
519 </div>
520 </div>
521
522 {/* Help Section */}
523 <div className="mt-8 bg-info/10 border border-info/30 rounded-lg p-6">
524 <div className="flex items-start gap-3">
525 <Icon name="help_outline" size={24} className="text-info mt-0.5" />
526 <div>
527 <h3 className="font-semibold text-info mb-2">How to use this page:</h3>
528 <ul className="space-y-2 text-info/90 text-sm">
529 <li className="flex items-start gap-2">
530 <Icon name="visibility" size={16} className="mt-0.5 flex-shrink-0" />
531 <span><strong>View relationships:</strong> Click the arrow next to a printer to see its compatible cartridges</span>
532 </li>
533 <li className="flex items-start gap-2">
534 <Icon name="add_circle" size={16} className="mt-0.5 flex-shrink-0" />
535 <span><strong>Add cartridges:</strong> Click "Add Cartridge" on a printer, then click cartridges from the right panel</span>
536 </li>
537 <li className="flex items-start gap-2">
538 <Icon name="delete" size={16} className="mt-0.5 flex-shrink-0" />
539 <span><strong>Remove cartridges:</strong> Expand a printer and click "Remove" next to any cartridge</span>
540 </li>
541 <li className="flex items-start gap-2">
542 <Icon name="search" size={16} className="mt-0.5 flex-shrink-0" />
543 <span><strong>Search:</strong> Use the search box to filter printers and cartridges by name or PLU</span>
544 </li>
545 <li className="flex items-start gap-2">
546 <Icon name="save" size={16} className="mt-0.5 flex-shrink-0" />
547 <span><strong>Save changes:</strong> Click "Save Relationships" to update Fieldpine with your changes</span>
548 </li>
549 <li className="flex items-start gap-2">
550 <Icon name="point_of_sale" size={16} className="mt-0.5 flex-shrink-0" />
551 <span><strong>Sales screen integration:</strong> These relationships are used on the sales screen when searching for printer cartridges</span>
552 </li>
553 </ul>
554 </div>
555 </div>
556 </div>
557 </div>
558 );
559}