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 { Icon } from '@/contexts/IconContext';
5
6interface LabelPurpose {
7 id: number;
8 name: string;
9}
10
11interface LabelReason {
12 id: number;
13 name: string;
14}
15
16interface PrinterDevice {
17 id: number;
18 name: string;
19 deviceId: number;
20}
21
22const labelPurposes: LabelPurpose[] = [
23 { id: 142, name: 'Delivery Address' },
24 { id: 230, name: 'Product Item Label' },
25 { id: 231, name: 'Warehouse Label' },
26 { id: 232, name: 'Shelf Label' },
27 { id: 235, name: 'Product QR label' },
28 { id: 151, name: 'Markdown Pricing' },
29 { id: 260, name: 'Transit Delivery Address' },
30];
31
32const labelReasons: LabelReason[] = [
33 { id: 0, name: 'Default' },
34 { id: 1, name: 'Expires Soon' },
35 { id: 6, name: 'Store Promotion (-order)' },
36 { id: 3, name: 'Damaged' },
37 { id: 4, name: 'End of Line' },
38 { id: 2, name: 'Store Promotion (+order)' },
39 { id: 7, name: 'A B Testing' },
40 { id: 8, name: 'None. Just print a label' },
41];
42
43interface LabelDimensions {
44 width: number;
45 height: number;
46 name: string;
47}
48
49const commonLabelSizes: LabelDimensions[] = [
50 { width: 200, height: 100, name: '2 x 1 inch (Small)' },
51 { width: 300, height: 150, name: '3 x 1.5 inch (Medium)' },
52 { width: 300, height: 200, name: '3 x 2 inch (Standard)' },
53 { width: 400, height: 300, name: '4 x 3 inch (Large)' },
54 { width: 400, height: 600, name: '4 x 6 inch (Shipping)' },
55 { width: 600, height: 400, name: '6 x 4 inch (Wide)' },
56];
57
58interface LabelTemplate {
59 name: string;
60 description: string;
61 commands: string;
62 purposeId: number;
63}
64
65interface LabelElement {
66 type: 'text' | 'barcode' | 'qr';
67 x: number;
68 y: number;
69 text: string;
70 font?: number;
71 height?: number;
72 field?: string;
73}
74
75const templates: LabelTemplate[] = [
76 {
77 name: 'Basic Price Label',
78 description: 'Simple price label with product name and price',
79 purposeId: 151,
80 commands: `#type=1 #sequence=10
81P1 x(10) y(20) text(XX) replace(XX,f216) font(3)
82
83#type=1 #sequence=20
84P1 x(10) y(50) text($XX) replace(XX,f215) font(5)`
85 },
86 {
87 name: 'Barcode + Price Label',
88 description: 'Product barcode with description and price',
89 purposeId: 230,
90 commands: `#type=1 #sequence=10
91B1 x(50) y(30) text(XX) replace(XX,f239) height(80)
92
93#type=1 #sequence=20
94P1 x(10) y(120) text(XX) replace(XX,f216) font(3)
95
96#type=1 #sequence=30
97P1 x(10) y(140) text($XX) replace(XX,f215) font(4)`
98 },
99 {
100 name: 'QR Code Product Label',
101 description: 'QR code with product information',
102 purposeId: 235,
103 commands: `#type=1 #sequence=10
104QR x(20) y(20) text(https://example.com/p/XX) replace(XX,f213) height(100)
105
106#type=1 #sequence=20
107P1 x(130) y(40) text(XX) replace(XX,f216) font(3)
108
109#type=1 #sequence=30
110P1 x(130) y(70) text($XX) replace(XX,f215) font(4)`
111 },
112 {
113 name: 'Shelf Label Standard',
114 description: 'Complete shelf label with brand, description, and price',
115 purposeId: 232,
116 commands: `#type=1 #sequence=10
117P1 x(10) y(10) text(XX) replace(XX,f242) font(2)
118
119#type=1 #sequence=20
120P1 x(10) y(30) text(XX) replace(XX,f216) font(3)
121
122#type=1 #sequence=30
123P1 x(10) y(60) text($XX) replace(XX,f215) font(5)
124
125#type=1 #sequence=40
126B1 x(10) y(100) text(XX) replace(XX,f239) height(60)`
127 },
128 {
129 name: 'Delivery Address Label',
130 description: 'Shipping address with barcode',
131 purposeId: 142,
132 commands: `#type=1 #sequence=10
133P1 x(10) y(20) text(XX) replace(XX,f110) font(3)
134
135#type=1 #sequence=20
136P1 x(10) y(40) text(XX) replace(XX,f301) font(3)
137
138#type=1 #sequence=30
139P1 x(10) y(60) text(XX) replace(XX,f302) font(3)
140
141#type=1 #sequence=40
142B1 x(10) y(100) text(XX) replace(XX,f210) height(60)`
143 }
144];
145
146const fieldDefinitions = [
147 { code: 'f110', name: 'Ship to info', category: 'Shipping' },
148 { code: 'f210', name: 'Sale Physkey', category: 'Sale' },
149 { code: 'f211', name: 'Sale Sid', category: 'Sale' },
150 { code: 'f213', name: 'Product Id', category: 'Product' },
151 { code: 'f214', name: 'Price in cents', category: 'Product' },
152 { code: 'f215', name: 'Price as String', category: 'Product' },
153 { code: 'f216', name: 'Product Description', category: 'Product' },
154 { code: 'f239', name: 'Barcode', category: 'Product' },
155 { code: 'f242', name: 'Brand Name', category: 'Product' },
156 { code: 'f261', name: 'Free text 1', category: 'Custom' },
157 { code: 'f262', name: 'Free text 2', category: 'Custom' },
158 { code: 'f263', name: 'Free text 3', category: 'Custom' },
159 { code: 'f300', name: 'Store Name', category: 'Store' },
160 { code: 'f301', name: 'Address line 1', category: 'Store' },
161 { code: 'f302', name: 'Address line 2', category: 'Store' },
162];
163
164export default function LabelFormatsPage() {
165 const [purpose, setPurpose] = useState<number>(0);
166 const [reason, setReason] = useState<number>(0);
167 const [formatName, setFormatName] = useState<string>('');
168 const [formatNames, setFormatNames] = useState<string[]>([]);
169 const [commands, setCommands] = useState<string>('');
170 const [testPrinterId, setTestPrinterId] = useState<number>(-1);
171 const [testPid, setTestPid] = useState<string>('');
172 const [printers, setPrinters] = useState<PrinterDevice[]>([]);
173 const [previewUrl, setPreviewUrl] = useState<string>('');
174 const [isLoading, setIsLoading] = useState(false);
175 const [isSimpleMode, setIsSimpleMode] = useState(true);
176 const [showHelp, setShowHelp] = useState(false);
177 const [showTemplates, setShowTemplates] = useState(true);
178 const [elements, setElements] = useState<LabelElement[]>([]);
179 const [labelWidth, setLabelWidth] = useState(300);
180 const [labelHeight, setLabelHeight] = useState(200);
181 const [customDimensions, setCustomDimensions] = useState(false);
182
183 useEffect(() => {
184 loadPrinters();
185 }, []);
186
187 useEffect(() => {
188 if (purpose > 0) {
189 loadFormatData();
190 }
191 }, [purpose, reason]);
192
193 useEffect(() => {
194 updatePreview();
195 }, [commands, testPid, elements, isSimpleMode]);
196
197 const loadPrinters = async () => {
198 try {
199 // API call: /buck?3=localsystem.device.list&100=all
200 // This queries the Fieldpine POS server for locally connected printers
201 console.log('Loading printers from Windows...');
202
203 // TODO: Implement actual API call
204 // const response = await fetch('/buck?3=localsystem.device.list&100=all');
205 // const data = await response.json();
206 // Parse DEVI array and filter for label printers (f105 == 1)
207
208 // Mock data - would be replaced with actual Windows printer list
209 setPrinters([
210 { id: 0, name: 'Label Printer 1 (USB)', deviceId: 101 },
211 { id: 1, name: 'Label Printer 2 (Network)', deviceId: 102 },
212 ]);
213 } catch (error) {
214 console.error('Failed to load printers:', error);
215 }
216 };
217
218 const loadFormatData = async () => {
219 setIsLoading(true);
220 // API call: /buck?3=retailmax.elink.config.state&101=labelformats&200={reason}&201={purpose}
221 console.log(`Loading format data for purpose ${purpose}, reason ${reason}...`);
222
223 // Mock data - would parse response and populate commands textarea
224 setIsLoading(false);
225 };
226
227 // Get default elements for a purpose
228 const getDefaultElementsForPurpose = (purposeId: number): LabelElement[] => {
229 switch (purposeId) {
230 case 1: // Shelf Labels
231 return [
232 { type: 'text', x: 10, y: 10, text: 'f213', font: 4 }, // Product Name
233 { type: 'text', x: 10, y: 45, text: 'f242', font: 5 }, // Price
234 { type: 'barcode', x: 10, y: 70, text: 'f213', height: 60 }, // Barcode
235 ];
236 case 2: // Product Labels
237 return [
238 { type: 'text', x: 10, y: 10, text: 'f213', font: 3 }, // Product Name
239 { type: 'text', x: 10, y: 35, text: 'f214', font: 2 }, // SKU
240 { type: 'barcode', x: 10, y: 55, text: 'f213', height: 60 },
241 ];
242 case 3: // Barcode Labels
243 return [
244 { type: 'barcode', x: 10, y: 10, text: 'f213', height: 80 }, // Main barcode
245 { type: 'text', x: 10, y: 100, text: 'f213', font: 2 }, // Code below
246 ];
247 case 4: // Shipping Labels
248 return [
249 { type: 'text', x: 10, y: 10, text: 'f110', font: 3 }, // Customer Name
250 { type: 'text', x: 10, y: 35, text: 'f263', font: 2 }, // Address 1
251 { type: 'text', x: 10, y: 55, text: 'f264', font: 2 }, // City
252 { type: 'text', x: 10, y: 75, text: 'f265', font: 2 }, // Postal Code
253 { type: 'barcode', x: 10, y: 100, text: 'f210', height: 60 }, // Sale ID
254 ];
255 case 5: // Delivery Labels
256 return [
257 { type: 'text', x: 10, y: 10, text: 'DELIVERY TO:', font: 3 },
258 { type: 'text', x: 10, y: 35, text: 'f110', font: 4 }, // Customer Name
259 { type: 'text', x: 10, y: 65, text: 'f263', font: 2 }, // Address
260 { type: 'qr', x: 150, y: 10, text: 'f110', height: 80 }, // QR with customer info
261 ];
262 case 6: // Returns Labels
263 return [
264 { type: 'text', x: 10, y: 10, text: 'RETURN AUTHORIZATION', font: 4 },
265 { type: 'text', x: 10, y: 40, text: 'f210', font: 3 }, // Sale ID
266 { type: 'text', x: 10, y: 65, text: 'f300', font: 2 }, // Store Name
267 { type: 'barcode', x: 10, y: 90, text: 'f210', height: 60 },
268 ];
269 default: // Other
270 return [
271 { type: 'text', x: 10, y: 20, text: 'Sample Text', font: 3 },
272 ];
273 }
274 };
275
276 const handlePurposeChange = (newPurpose: number) => {
277 setPurpose(newPurpose);
278 if (newPurpose !== 151) {
279 setReason(0);
280 }
281 // Auto-populate elements in Simple Mode when purpose changes
282 if (isSimpleMode && elements.length === 0) {
283 const defaultElements = getDefaultElementsForPurpose(newPurpose);
284 setElements(defaultElements);
285 updateCommandsFromElements(defaultElements);
286 }
287 };
288
289 const handleFormatNameChange = (name: string) => {
290 if (name === 'Create New') {
291 const newName = prompt('Name of new format?\n(short and terse is best)');
292 if (newName && newName.length > 0) {
293 setFormatNames([...formatNames, newName]);
294 setFormatName(newName);
295 }
296 } else {
297 setFormatName(name);
298 // Load format by name
299 console.log(`Loading format: ${name}`);
300 }
301 };
302
303 const saveFormat = () => {
304 if (purpose === 0) {
305 alert('Please select a purpose first');
306 return;
307 }
308
309 if (!formatName || formatName.length === 0) {
310 alert('Please provide a format name');
311 return;
312 }
313
314 const lines = commands.split('\n').filter(line => line.trim().length > 0);
315
316 // Validate format
317 let errors: string[] = [];
318 for (let i = 0; i < lines.length; i += 2) {
319 const headerLine = lines[i];
320 if (!headerLine.startsWith('#')) {
321 errors.push(`Line ${i + 1} did not start with '#'`);
322 break;
323 }
324
325 const hasType = headerLine.includes('#type=');
326 const hasSequence = headerLine.includes('#sequence=');
327 if (!hasType || !hasSequence) {
328 errors.push(`Line ${i + 1} did not give type and sequence information`);
329 break;
330 }
331
332 if (i + 1 < lines.length) {
333 const cmdLine = lines[i + 1];
334 if (cmdLine.length === 0) {
335 errors.push(`Line ${i + 2} does not have a command`);
336 break;
337 }
338 }
339 }
340
341 if (errors.length > 0) {
342 alert(errors.join('\n'));
343 return;
344 }
345
346 // API call to save
347 console.log('Saving format:', {
348 purpose,
349 reason,
350 formatName,
351 commands,
352 });
353
354 // <DATI><f8_s>retailmax.elink.config.printingformats.edit</f8_s>
355 // <f11_B>E</f11_B> (or I for new)
356 // <f101_E>{purpose}</f101_E>
357 // <f105_E>{reason}</f105_E>
358 // <f106_s>{formatName}</f106_s>
359 // Parse commands and add DATS entries
360 // </DATI>
361
362 alert('Format saved successfully');
363 };
364
365 const updatePreview = () => {
366 if (isSimpleMode && elements.length > 0) {
367 // Simple mode uses element-based preview
368 return;
369 }
370 // In advanced mode, could parse commands for preview
371 console.log('Updating preview...');
372 };
373
374 const renderSimplePreview = () => {
375 if (elements.length === 0) {
376 return (
377 <div className="text-center text-muted text-sm">
378 Add elements to see preview
379 </div>
380 );
381 }
382
383 const aspectRatio = ((labelHeight / labelWidth) * 100);
384
385 return (
386 <div className="relative w-full" style={{ paddingBottom: `${aspectRatio}%` }}>
387 <div className="absolute inset-0 border-2 border-dashed border-border bg-surface rounded overflow-hidden">
388 {/* Dimension labels */}
389 <div className="absolute -top-6 left-0 right-0 text-center text-xs text-muted font-semibold">
390 {labelWidth} units
391 </div>
392 <div className="absolute top-0 bottom-0 -left-16 flex items-center justify-center text-xs text-muted font-semibold" style={{ writingMode: 'vertical-rl', transform: 'rotate(180deg)' }}>
393 {labelHeight} units
394 </div>
395
396 {elements.map((element, idx) => {
397 const xPercent = ((element.x / labelWidth) * 100);
398 const yPercent = ((element.y / labelHeight) * 100);
399
400 return (
401 <div
402 key={idx}
403 className="absolute"
404 style={{
405 left: `${xPercent}%`,
406 top: `${yPercent}%`,
407 }}
408 >
409 {element.type === 'text' && (
410 <div
411 className="whitespace-nowrap font-mono"
412 style={{
413 fontSize: element.font === 1 ? '8px' :
414 element.font === 2 ? '10px' :
415 element.font === 3 ? '12px' :
416 element.font === 4 ? '16px' : '20px',
417 }}
418 >
419 {element.field ? `{${fieldDefinitions.find(f => f.code === element.field)?.name || element.text}}` : element.text}
420 </div>
421 )}
422 {element.type === 'barcode' && (
423 <div className="flex flex-col items-center">
424 <div className="flex gap-px">
425 {[...Array(20)].map((_, i) => (
426 <div key={i} className="w-px bg-black" style={{ height: `${((element.height || 80) / 8)}px` }} />
427 ))}
428 </div>
429 <div className="text-xs mt-1 font-mono">
430 {element.field ? `{${fieldDefinitions.find(f => f.code === element.field)?.name}}` : element.text}
431 </div>
432 </div>
433 )}
434 {element.type === 'qr' && (
435 <div className="flex flex-col items-center">
436 <div
437 className="border-2 border-black bg-surface grid grid-cols-8 gap-px p-1"
438 style={{ width: `${((element.height || 80) / 4)}px`, height: `${((element.height || 80) / 4)}px` }}
439 >
440 {[...Array(64)].map((_, i) => (
441 <div key={i} className={`${Math.random() > 0.5 ? 'bg-black' : 'bg-surface'}`} />
442 ))}
443 </div>
444 <div className="text-xs mt-1 font-mono">QR</div>
445 </div>
446 )}
447 </div>
448 );
449 })}
450
451 {/* Grid overlay */}
452 <div className="absolute inset-0 pointer-events-none opacity-10">
453 <svg width="100%" height="100%">
454 <defs>
455 <pattern id="grid" width="10%" height="10%" patternUnits="userSpaceOnUse">
456 <path d="M 0 0 L 100 0 100 100" fill="none" stroke="gray" strokeWidth="0.5" />
457 </pattern>
458 </defs>
459 <rect width="100%" height="100%" fill="url(#grid)" />
460 </svg>
461 </div>
462 </div>
463 </div>
464 );
465 };
466
467 const loadTemplate = (template: LabelTemplate) => {
468 setCommands(template.commands);
469 setPurpose(template.purposeId);
470 setShowTemplates(false);
471 };
472
473 const loadPresetSize = (size: LabelDimensions) => {
474 setLabelWidth(size.width);
475 setLabelHeight(size.height);
476 setCustomDimensions(false);
477 };
478
479 const addElement = (type: 'text' | 'barcode' | 'qr') => {
480 const newElement: LabelElement = {
481 type,
482 x: 10,
483 y: 20 + (elements.length * 40),
484 text: type === 'text' ? 'Sample Text' : 'XX',
485 font: type === 'text' ? 3 : undefined,
486 height: type !== 'text' ? 80 : undefined,
487 };
488 setElements([...elements, newElement]);
489 updateCommandsFromElements([...elements, newElement]);
490 };
491
492 const updateElement = (index: number, updates: Partial<LabelElement>) => {
493 const newElements = [...elements];
494 newElements[index] = { ...newElements[index], ...updates };
495 setElements(newElements);
496 updateCommandsFromElements(newElements);
497 };
498
499 const removeElement = (index: number) => {
500 const newElements = elements.filter((_, i) => i !== index);
501 setElements(newElements);
502 updateCommandsFromElements(newElements);
503 };
504
505 const updateCommandsFromElements = (els: LabelElement[]) => {
506 let cmdText = '';
507 els.forEach((el, idx) => {
508 const seq = (idx + 1) * 10;
509 cmdText += `#type=1 #sequence=${seq}\n`;
510
511 if (el.type === 'text') {
512 let cmd = `P1 x(${el.x}) y(${el.y}) text(${el.text})`;
513 if (el.font) cmd += ` font(${el.font})`;
514 if (el.field) cmd += ` replace(${el.text},${el.field})`;
515 cmdText += cmd + '\n\n';
516 } else if (el.type === 'barcode') {
517 let cmd = `B1 x(${el.x}) y(${el.y}) text(${el.text})`;
518 if (el.height) cmd += ` height(${el.height})`;
519 if (el.field) cmd += ` replace(${el.text},${el.field})`;
520 cmdText += cmd + '\n\n';
521 } else if (el.type === 'qr') {
522 let cmd = `QR x(${el.x}) y(${el.y}) text(${el.text})`;
523 if (el.height) cmd += ` height(${el.height})`;
524 if (el.field) cmd += ` replace(${el.text},${el.field})`;
525 cmdText += cmd + '\n\n';
526 }
527 });
528 setCommands(cmdText.trim());
529 };
530
531 const testPrint = (isPreview: boolean) => {
532 if (testPrinterId < 0) {
533 if (!isPreview) alert('Please select a printer');
534 return;
535 }
536
537 console.log('Sending print job to Windows printer...', {
538 printerId: testPrinterId,
539 testPid,
540 commands,
541 isPreview,
542 });
543
544 // API call sends DATI XML to Fieldpine server:
545 // <DATI><f8_s>localsystem.print.format</f8_s>
546 // <f11_B>I</f11_B>
547 // <f100_E>151</f100_E> (purpose)
548 // <f101>{deviceId}</f101> (printer device ID)
549 // <f103_s>iprint</f103_s> (for preview mode)
550 // <f213_E>{testPid}</f213_E> (product ID)
551 // <FIFT><CMD1><f110_s>{command}</f110_s></CMD1>...</FIFT>
552 // </DATI>
553
554 // The Fieldpine server then:
555 // 1. Receives the XML packet
556 // 2. Parses the commands
557 // 3. Sends raw printer commands (ZPL, EPL, etc.) to Windows printer
558 // 4. Returns preview image if requested (via labelary.com API)
559
560 // TODO: Implement actual API call
561 // const xmlPacket = buildPrintXML(commands, testPid, printers[testPrinterId].deviceId);
562 // PostXML_RetOjectForGNAP("/DATI", xmlPacket, callback);
563
564 if (isPreview) {
565 // Mock preview - would get actual ZPL preview from server
566 setPreviewUrl('https://via.placeholder.com/300x300?text=Label+Preview');
567 } else {
568 alert('Print job sent to Windows printer (placeholder - API not implemented)');
569 }
570 };
571
572 return (
573 <div className="p-6 min-h-screen bg-bg">
574 {/* Header */}
575 <div className="mb-6">
576 <h1 className="text-3xl font-bold text-text mb-2 flex items-center gap-3">
577 <Icon name="label" size={32} className="text-brand" />
578 Label Formats
579 </h1>
580 <p className="text-muted">
581 Design and manage label formats for printing
582 </p>
583 </div>
584
585 {/* Main Content */}
586 <div className="max-w-[1600px] mx-auto">
587 <p className="text-muted">Label use requires specialised label printers</p>
588
589 {/* System Info */}
590 <div className="mt-3 bg-warn/10 border border-warn/30 rounded-lg p-3">
591 <p className="text-sm text-text flex items-center gap-2">
592 <Icon name="print" size={20} className="text-warn" />
593 <strong>Printer Connection:</strong> Label printers must be installed on your Windows POS server.
594 This web interface communicates with your local EverydayPOS server, which handles the actual printer connection.
595 </p>
596 </div>
597 </div>
598
599 {/* Mode Toggle & Help */}
600 <div className="flex gap-4 mb-6">
601 <div className="bg-surface rounded-lg shadow-md p-4 flex items-center gap-4">
602 <span className="text-sm font-semibold text-text">Mode:</span>
603 <button
604 onClick={() => setIsSimpleMode(true)}
605 className={`px-4 py-2 rounded-md transition-colors ${
606 isSimpleMode
607 ? 'bg-brand text-surface'
608 : 'bg-surface-2 text-text hover:bg-surface-2/80'
609 }`}
610 >
611 Simple (Recommended)
612 </button>
613 <button
614 onClick={() => setIsSimpleMode(false)}
615 className={`px-4 py-2 rounded-md transition-colors ${
616 !isSimpleMode
617 ? 'bg-brand text-surface'
618 : 'bg-surface-2 text-text hover:bg-surface-2/80'
619 }`}
620 >
621 Advanced (Code)
622 </button>
623 </div>
624
625 <button
626 onClick={() => setShowHelp(!showHelp)}
627 className="bg-info hover:bg-info/80 text-surface px-4 py-2 rounded-md shadow-md transition-colors flex items-center gap-2"
628 >
629 <Icon name="help" size={20} />
630 <span>{showHelp ? 'Hide' : 'Show'} Help</span>
631 </button>
632
633 <button
634 onClick={() => setShowTemplates(!showTemplates)}
635 className="bg-brand2 hover:bg-brand2/80 text-surface px-4 py-2 rounded-md shadow-md transition-colors flex items-center gap-2"
636 >
637 <Icon name="content_paste" size={20} />
638 <span>{showTemplates ? 'Hide' : 'Show'} Templates</span>
639 </button>
640 </div>
641
642 {/* Help Section */}
643 {showHelp && (
644 <div className="bg-info/10 border-l-4 border-info rounded-lg p-6 mb-6">
645 <h2 className="text-xl font-bold text-text mb-4 flex items-center gap-2">
646 <Icon name="school" size={24} className="text-info" />
647 Getting Started with Label Formats
648 </h2>
649
650 <div className="space-y-4">
651 <div>
652 <h3 className="font-semibold text-text mb-2 flex items-center gap-2">
653 <Icon name="location_on" size={18} className="text-brand" />
654 Step 1: Choose Your Mode
655 </h3>
656 <ul className="text-sm text-text space-y-1 ml-4">
657 <li><strong>Simple Mode:</strong> Visual editor - Add elements with buttons, position them easily</li>
658 <li><strong>Advanced Mode:</strong> Direct command editing for power users who know the syntax</li>
659 </ul>
660 </div>
661
662 <div>
663 <h3 className="font-semibold text-text mb-2">📋 Step 2: Use a Template (Recommended)</h3>
664 <p className="text-sm text-text">Click "Show Templates" and choose a pre-made label format. You can customize it after loading!</p>
665 <p className="text-sm text-muted mt-2">💡 <strong>Or select a Purpose:</strong> Each purpose automatically provides default elements appropriate for that label type (shelf labels get price + barcode, shipping labels get address fields, etc.)</p>
666 </div>
667
668 <div>
669 <h3 className="font-semibold text-text mb-2">✏️ Step 3: Customize Your Label</h3>
670 <ul className="text-sm text-text space-y-1 ml-4">
671 <li><strong>In Simple Mode:</strong> Use "Add Text", "Add Barcode", "Add QR Code" buttons</li>
672 <li>Adjust X and Y positions (start small, label is typically 300x200 units)</li>
673 <li>Select fields from dropdown to show dynamic data (Price, Product Name, etc.)</li>
674 <li>Choose font size (1=small, 3=medium, 5=large)</li>
675 </ul>
676 </div>
677
678 <div>
679 <h3 className="font-semibold text-text mb-2">🎯 Understanding Coordinates</h3>
680 <div className="bg-surface rounded p-3 text-sm text-text">
681 <p className="mb-2">Labels use X,Y coordinates like a graph:</p>
682 <ul className="space-y-1 ml-4">
683 <li>• <strong>X</strong> = horizontal position (left to right, 0-300)</li>
684 <li>• <strong>Y</strong> = vertical position (top to bottom, 0-200)</li>
685 <li>• Start at (10, 20) for first element</li>
686 <li>• Spacing: Add 30-50 to Y for each new line</li>
687 </ul>
688 </div>
689 </div>
690
691 <div>
692 <h3 className="font-semibold text-text mb-2">🔧 Step 4: Test Your Label</h3>
693 <p className="text-sm text-text">Select a printer, optionally enter a test product ID, then click "Preview" or "Test Print"</p>
694 </div>
695
696 <div className="bg-warn/10 border border-warn/30 rounded p-3">
697 <p className="text-sm text-text"><strong>💡 Pro Tip:</strong> Start with Simple Mode and a template. Once comfortable, switch to Advanced Mode to see the actual commands!</p>
698 </div>
699 </div>
700 </div>
701 )}
702
703 {/* Templates Section */}
704 {showTemplates && (
705 <div className="bg-surface rounded-lg shadow-md p-6 mb-6">
706 <h2 className="text-xl font-bold text-text mb-4 flex items-center gap-2">
707 <span>📋</span> Quick Start Templates
708 </h2>
709 <p className="text-sm text-muted mb-4">Click any template to load it and customize</p>
710 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
711 {templates.map((template, idx) => (
712 <div
713 key={idx}
714 onClick={() => loadTemplate(template)}
715 className="border-2 border-border rounded-lg p-4 hover:border-brand hover:bg-surface-2 cursor-pointer transition-all"
716 >
717 <h3 className="font-semibold text-text mb-1">{template.name}</h3>
718 <p className="text-sm text-muted">{template.description}</p>
719 </div>
720 ))}
721 </div>
722 </div>
723 )}
724
725 {/* Label Dimensions Section */}
726 <div className="bg-surface rounded-lg shadow-md p-6 mb-6">
727 <h2 className="text-lg font-bold text-text mb-4 flex items-center gap-2">
728 <span>📏</span> Label Dimensions
729 </h2>
730
731 <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3 mb-4">
732 {commonLabelSizes.map((size, idx) => (
733 <button
734 key={idx}
735 onClick={() => loadPresetSize(size)}
736 className={`border-2 rounded-lg p-3 transition-all text-sm ${
737 labelWidth === size.width && labelHeight === size.height && !customDimensions
738 ? 'border-brand bg-success/10 font-semibold'
739 : 'border-border hover:border-brand hover:bg-surface-2'
740 }`}
741 >
742 <div className="font-semibold text-text">{size.name.split(' ')[0]}</div>
743 <div className="text-xs text-muted">{size.name.split(' ').slice(1).join(' ')}</div>
744 </button>
745 ))}
746 </div>
747
748 <div className="flex items-center gap-4">
749 <label className="flex items-center gap-2 cursor-pointer">
750 <input
751 type="checkbox"
752 checked={customDimensions}
753 onChange={(e) => setCustomDimensions(e.target.checked)}
754 className="rounded"
755 />
756 <span className="text-sm font-semibold text-text">Custom Dimensions</span>
757 </label>
758
759 {customDimensions && (
760 <div className="flex items-center gap-3">
761 <div className="flex items-center gap-2">
762 <label className="text-sm text-text">Width:</label>
763 <input
764 type="number"
765 value={labelWidth}
766 onChange={(e) => setLabelWidth(Number(e.target.value))}
767 className="w-20 border border-border rounded px-2 py-1 text-sm"
768 min="50"
769 max="1000"
770 />
771 </div>
772 <div className="flex items-center gap-2">
773 <label className="text-sm text-text">Height:</label>
774 <input
775 type="number"
776 value={labelHeight}
777 onChange={(e) => setLabelHeight(Number(e.target.value))}
778 className="w-20 border border-border rounded px-2 py-1 text-sm"
779 min="50"
780 max="1000"
781 />
782 </div>
783 <span className="text-xs text-muted">units</span>
784 </div>
785 )}
786 </div>
787 </div>
788
789 {/* Configuration Section */}
790 <div className="bg-surface rounded-lg shadow-md p-6 mb-6">
791 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
792 {/* Purpose */}
793 <div>
794 <div className="flex items-center justify-between mb-2">
795 <label className="block text-sm font-semibold text-text">
796 Purpose
797 </label>
798 {isSimpleMode && elements.length > 0 && (
799 <button
800 type="button"
801 onClick={() => {
802 if (confirm('Reset to default elements for this purpose? Current elements will be replaced.')) {
803 const defaultElements = getDefaultElementsForPurpose(purpose);
804 setElements(defaultElements);
805 updateCommandsFromElements(defaultElements);
806 }
807 }}
808 className="text-sm text-brand hover:text-brand2"
809 >
810 Reset to Default
811 </button>
812 )}
813 </div>
814 <select
815 value={purpose}
816 onChange={(e) => handlePurposeChange(Number(e.target.value))}
817 className="w-full border border-border rounded-md px-3 py-2 focus:ring-2 focus:ring-brand focus:border-transparent"
818 >
819 <option value={0}>..Select</option>
820 {labelPurposes.map((p) => (
821 <option key={p.id} value={p.id}>
822 {p.name}
823 </option>
824 ))}
825 </select>
826 </div>
827
828 {/* Label Reason */}
829 <div>
830 <label className="block text-sm font-semibold text-text mb-2">
831 Label Reason
832 </label>
833 <select
834 value={reason}
835 onChange={(e) => setReason(Number(e.target.value))}
836 disabled={purpose !== 151}
837 className="w-full border border-border rounded-md px-3 py-2 focus:ring-2 focus:ring-brand focus:border-transparent disabled:bg-surface-2 disabled:cursor-not-allowed"
838 >
839 {labelReasons.map((r) => (
840 <option key={r.id} value={r.id}>
841 {r.name}
842 </option>
843 ))}
844 </select>
845 </div>
846
847 {/* Format Name */}
848 <div>
849 <label className="block text-sm font-semibold text-text mb-2">
850 Your Name
851 </label>
852 <select
853 value={formatName}
854 onChange={(e) => handleFormatNameChange(e.target.value)}
855 className="w-full border border-border rounded-md px-3 py-2 focus:ring-2 focus:ring-brand focus:border-transparent"
856 >
857 <option value="">..Select</option>
858 {formatNames.map((name) => (
859 <option key={name} value={name}>
860 {name}
861 </option>
862 ))}
863 {purpose === 235 && <option value="Create New">Create New</option>}
864 </select>
865 </div>
866
867 {/* Save Button */}
868 <div className="flex items-end">
869 <button
870 onClick={saveFormat}
871 className="w-full bg-brand hover:bg-brand2 text-surface font-semibold py-2 px-4 rounded-md transition-colors flex items-center justify-center gap-2"
872 >
873 <Icon name="save" size={20} />
874 Save
875 </button>
876 {isLoading && (
877 <span className="ml-2 text-sm text-muted">Loading...</span>
878 )}
879 </div>
880 </div>
881
882 {/* Commands Editor and Preview */}
883 <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
884 {/* Simple Mode Visual Editor */}
885 {isSimpleMode ? (
886 <div className="lg:col-span-2">
887 <div className="flex justify-between items-center mb-4">
888 <label className="block text-sm font-semibold text-text">
889 Label Elements
890 </label>
891 <div className="flex gap-2">
892 <button
893 onClick={() => addElement('text')}
894 className="bg-brand hover:bg-brand2 text-surface px-3 py-1 rounded text-sm transition-colors flex items-center gap-1"
895 >
896 <Icon name="add" size={16} />
897 Add Text
898 </button>
899 <button
900 onClick={() => addElement('barcode')}
901 className="bg-info hover:bg-info/80 text-surface px-3 py-1 rounded text-sm transition-colors flex items-center gap-1"
902 >
903 <Icon name="add" size={16} />
904 Add Barcode
905 </button>
906 <button
907 onClick={() => addElement('qr')}
908 className="bg-brand2 hover:bg-brand2/80 text-surface px-3 py-1 rounded text-sm transition-colors flex items-center gap-1"
909 >
910 <Icon name="add" size={16} />
911 Add QR Code
912 </button>
913 </div>
914 </div>
915
916 <div className="space-y-4 max-h-[600px] overflow-y-auto">
917 {elements.length === 0 && (
918 <div className="border-2 border-dashed border-border rounded-lg p-8 text-center">
919 <p className="text-muted mb-4">No elements yet. Click a button above to add your first element!</p>
920 <p className="text-sm text-muted/70">Or load a template to get started quickly</p>
921 </div>
922 )}
923
924 {elements.map((element, idx) => (
925 <div key={idx} className="border border-border rounded-lg p-4 bg-surface-2">
926 <div className="flex justify-between items-start mb-3">
927 <div className="flex items-center gap-2">
928 <span className={`px-2 py-1 rounded text-xs font-semibold text-surface ${
929 element.type === 'text' ? 'bg-success' :
930 element.type === 'barcode' ? 'bg-info' : 'bg-brand2'
931 }`}>
932 {element.type.toUpperCase()}
933 </span>
934 <span className="text-sm text-muted">Element {idx + 1}</span>
935 </div>
936 <button
937 onClick={() => removeElement(idx)}
938 className="text-danger hover:text-danger/80 text-sm font-semibold"
939 >
940 Remove
941 </button>
942 </div>
943
944 <div className="grid grid-cols-2 gap-3">
945 <div>
946 <label className="block text-xs font-semibold text-text mb-1">
947 X Position (left-right)
948 </label>
949 <input
950 type="number"
951 value={element.x}
952 onChange={(e) => updateElement(idx, { x: Number(e.target.value) })}
953 className="w-full border border-border rounded px-2 py-1 text-sm"
954 min="0"
955 max="300"
956 />
957 </div>
958 <div>
959 <label className="block text-xs font-semibold text-text mb-1">
960 Y Position (top-bottom)
961 </label>
962 <input
963 type="number"
964 value={element.y}
965 onChange={(e) => updateElement(idx, { y: Number(e.target.value) })}
966 className="w-full border border-border rounded px-2 py-1 text-sm"
967 min="0"
968 max="200"
969 />
970 </div>
971
972 <div className="col-span-2">
973 <label className="block text-xs font-semibold text-text mb-1">
974 {element.type === 'text' ? 'Text Content' : 'Placeholder'}
975 </label>
976 <input
977 type="text"
978 value={element.text}
979 onChange={(e) => updateElement(idx, { text: e.target.value })}
980 className="w-full border border-border rounded px-2 py-1 text-sm"
981 placeholder={element.type === 'text' ? 'Enter text...' : 'XX'}
982 />
983 </div>
984
985 <div>
986 <label className="block text-xs font-semibold text-text mb-1">
987 Data Field (optional)
988 </label>
989 <select
990 value={element.field || ''}
991 onChange={(e) => updateElement(idx, { field: e.target.value || undefined })}
992 className="w-full border border-border rounded px-2 py-1 text-sm"
993 >
994 <option value="">Static (no field)</option>
995 {fieldDefinitions.map((field) => (
996 <option key={field.code} value={field.code}>
997 {field.name} ({field.code})
998 </option>
999 ))}
1000 </select>
1001 </div>
1002
1003 {element.type === 'text' && (
1004 <div>
1005 <label className="block text-xs font-semibold text-text mb-1">
1006 Font Size
1007 </label>
1008 <select
1009 value={element.font || 3}
1010 onChange={(e) => updateElement(idx, { font: Number(e.target.value) })}
1011 className="w-full border border-border rounded px-2 py-1 text-sm"
1012 >
1013 <option value="1">Small (1)</option>
1014 <option value="2">Medium-Small (2)</option>
1015 <option value="3">Medium (3)</option>
1016 <option value="4">Medium-Large (4)</option>
1017 <option value="5">Large (5)</option>
1018 </select>
1019 </div>
1020 )}
1021
1022 {element.type !== 'text' && (
1023 <div>
1024 <label className="block text-xs font-semibold text-text mb-1">
1025 Height
1026 </label>
1027 <input
1028 type="number"
1029 value={element.height || 80}
1030 onChange={(e) => updateElement(idx, { height: Number(e.target.value) })}
1031 className="w-full border border-border rounded px-2 py-1 text-sm"
1032 min="20"
1033 max="150"
1034 />
1035 </div>
1036 )}
1037 </div>
1038 </div>
1039 ))}
1040 </div>
1041
1042 {/* Show generated commands in simple mode */}
1043 {elements.length > 0 && (
1044 <div className="mt-4">
1045 <details className="border border-border rounded-lg">
1046 <summary className="bg-surface-2 px-4 py-2 cursor-pointer font-semibold text-sm text-text hover:bg-surface-2/80">
1047 View Generated Commands (Advanced)
1048 </summary>
1049 <div className="p-4">
1050 <pre className="text-xs font-mono bg-surface-2 p-3 rounded overflow-x-auto">{commands}</pre>
1051 </div>
1052 </details>
1053 </div>
1054 )}
1055 </div>
1056 ) : (
1057 /* Advanced Mode - Raw Commands */
1058 <div className="lg:col-span-2">
1059 <label className="block text-sm font-semibold text-text mb-2">
1060 Commands (Advanced)
1061 </label>
1062 <textarea
1063 value={commands}
1064 onChange={(e) => setCommands(e.target.value)}
1065 rows={20}
1066 className="w-full border border-border rounded-md px-3 py-2 font-mono text-sm focus:ring-2 focus:ring-brand focus:border-transparent"
1067 placeholder={`#type=1 #sequence=20
1068P1 x(20) y(20) text(TEST TEST) font(5)
1069
1070#type=1 #sequence=30
1071B1 x(100) y(150) text(XX) replace(XX,f239) height(100)`}
1072 />
1073
1074 {/* Command Reference in Advanced Mode */}
1075 <div className="mt-4 bg-info/10 border border-info/30 rounded-lg p-4">
1076 <h3 className="text-sm font-semibold text-text mb-2">Command Syntax Reference</h3>
1077 <ul className="text-xs text-text space-y-1 font-mono">
1078 <li><strong>Text:</strong> P1 x(20) y(20) text(Hello) font(3)</li>
1079 <li><strong>Barcode:</strong> B1 x(100) y(50) text(XX) replace(XX,f239) height(80)</li>
1080 <li><strong>QR Code:</strong> QR x(200) y(100) text(URL) height(100)</li>
1081 <li><strong>Dynamic Data:</strong> Use replace(XX,f215) to insert field values</li>
1082 </ul>
1083 </div>
1084 </div>
1085 )}
1086
1087 {/* Example Output */}
1088 <div>
1089 <h3 className="text-sm font-semibold text-text mb-2">
1090 Live Preview
1091 <span className="ml-2 text-xs text-muted">(Simulated)</span>
1092 </h3>
1093 <div className="border border-border rounded-md p-4 bg-surface-2">
1094 {isSimpleMode ? (
1095 renderSimplePreview()
1096 ) : previewUrl ? (
1097 <img src={previewUrl} alt="Label Preview" className="max-w-full mx-auto" />
1098 ) : (
1099 <div className="text-center text-muted text-sm min-h-[300px] flex items-center justify-center">
1100 Click "Preview" to see your label
1101 </div>
1102 )}
1103 </div>
1104
1105 <div className="mt-3 bg-info/10 border border-info/30 rounded p-3">
1106 <p className="text-xs text-text">
1107 <strong>📏 Current Label Size:</strong> {labelWidth} x {labelHeight} units
1108 <br />
1109 <span className="text-muted">Preview is scaled to fit. Adjust dimensions above to match your physical label size.</span>
1110 </p>
1111 </div>
1112 </div>
1113 </div>
1114 </div>
1115
1116 {/* Test Printing Section */}
1117 <div className="bg-surface rounded-lg shadow-md p-6 mb-6">
1118 <h2 className="text-xl font-bold text-text mb-4">Test Printing</h2>
1119 <div className="grid grid-cols-1 md:grid-cols-3 gap-4 items-end">
1120 <div>
1121 <label className="block text-sm font-semibold text-text mb-2">
1122 Test Printer
1123 </label>
1124 <select
1125 value={testPrinterId}
1126 onChange={(e) => setTestPrinterId(Number(e.target.value))}
1127 className="w-full border border-border rounded-md px-3 py-2 focus:ring-2 focus:ring-brand focus:border-transparent"
1128 >
1129 <option value={-1}>Select Printer</option>
1130 {printers.map((printer) => (
1131 <option key={printer.id} value={printer.id}>
1132 {printer.name}
1133 </option>
1134 ))}
1135 </select>
1136 </div>
1137
1138 <div>
1139 <label className="block text-sm font-semibold text-text mb-2">
1140 Test Product Id
1141 </label>
1142 <input
1143 type="text"
1144 value={testPid}
1145 onChange={(e) => setTestPid(e.target.value)}
1146 className="w-full border border-border rounded-md px-3 py-2 focus:ring-2 focus:ring-brand focus:border-transparent"
1147 placeholder="Enter product ID"
1148 />
1149 </div>
1150
1151 <div className="flex gap-2">
1152 <button
1153 onClick={() => testPrint(false)}
1154 className="flex-1 bg-brand hover:bg-brand2 text-surface font-semibold py-2 px-4 rounded-md transition-colors flex items-center justify-center gap-2"
1155 >
1156 <Icon name="print" size={20} />
1157 Test Print
1158 </button>
1159 <button
1160 onClick={() => testPrint(true)}
1161 className="flex-1 bg-surface-2 hover:bg-surface-2/80 text-text font-semibold py-2 px-4 rounded-md transition-colors"
1162 >
1163 Preview
1164 </button>
1165 </div>
1166 </div>
1167 </div>
1168
1169 {/* Field Definitions Reference */}
1170 <div className="bg-surface rounded-lg shadow-md p-6">
1171 <h2 className="text-xl font-bold text-text mb-4">Field Definitions Reference</h2>
1172 <p className="text-sm text-muted mb-4">
1173 These fields automatically pull data from your products and sales. Use them in the "Data Field" dropdown in Simple Mode,
1174 or with the replace() function in Advanced Mode.
1175 </p>
1176
1177 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
1178 {['Product', 'Sale', 'Store', 'Shipping', 'Custom'].map((category) => {
1179 const categoryFields = fieldDefinitions.filter(f => f.category === category);
1180 if (categoryFields.length === 0) return null;
1181
1182 return (
1183 <div key={category}>
1184 <h3 className="font-semibold text-[#00946b] mb-3">{category} Fields</h3>
1185 <div className="space-y-2">
1186 {categoryFields.map((field) => (
1187 <div key={field.code} className="flex gap-3 items-start">
1188 <code className="text-xs font-mono bg-surface-2 px-2 py-1 rounded text-[#00946b] font-semibold min-w-[60px]">
1189 {field.code}
1190 </code>
1191 <span className="text-sm text-text">{field.name}</span>
1192 </div>
1193 ))}
1194 </div>
1195 </div>
1196 );
1197 })}
1198 </div>
1199
1200 {/* Additional fields collapsed */}
1201 <details className="mt-6">
1202 <summary className="cursor-pointer text-sm font-semibold text-text hover:text-[#00946b]">
1203 Show more fields (f263-f309)
1204 </summary>
1205 <div className="mt-3 grid grid-cols-2 md:grid-cols-4 gap-3 text-xs">
1206 <div><code className="text-[#00946b]">f263-f269</code> Free text 3-9</div>
1207 <div><code className="text-[#00946b]">f303-f309</code> Address lines 3-9</div>
1208 </div>
1209 </details>
1210 </div>
1211
1212 {/* Quick Tips */}
1213 <div className="bg-gradient-to-r from-green-50 to-blue-50 border-l-4 border-[#00946b] rounded-lg p-6 mt-6">
1214 <h3 className="text-lg font-bold text-text mb-3">💡 Quick Tips for Success</h3>
1215 <ul className="space-y-2 text-sm text-text">
1216 <li className="flex gap-2">
1217 <span className="text-[#00946b]">✓</span>
1218 <span><strong>Start with templates:</strong> They're tested and work well for common needs</span>
1219 </li>
1220 <li className="flex gap-2">
1221 <span className="text-[#00946b]">✓</span>
1222 <span><strong>Use Simple Mode first:</strong> It's easier and prevents syntax errors</span>
1223 </li>
1224 <li className="flex gap-2">
1225 <span className="text-[#00946b]">✓</span>
1226 <span><strong>Keep it simple:</strong> Labels with 3-5 elements work best</span>
1227 </li>
1228 <li className="flex gap-2">
1229 <span className="text-[#00946b]">✓</span>
1230 <span><strong>Test before saving:</strong> Use Preview to check layout before printing</span>
1231 </li>
1232 <li className="flex gap-2">
1233 <span className="text-[#00946b]">✓</span>
1234 <span><strong>Label sizes vary:</strong> Typical labels are about 300 units wide by 200 tall</span>
1235 </li>
1236 </ul>
1237 </div>
1238 </div>
1239 );
1240}