3import { useState, useEffect } from 'react';
4import { Icon } from '@/contexts/IconContext';
6interface LabelPurpose {
11interface LabelReason {
16interface PrinterDevice {
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' },
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' },
43interface LabelDimensions {
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)' },
58interface LabelTemplate {
65interface LabelElement {
66 type: 'text' | 'barcode' | 'qr';
75const templates: LabelTemplate[] = [
77 name: 'Basic Price Label',
78 description: 'Simple price label with product name and price',
80 commands: `#type=1 #sequence=10
81P1 x(10) y(20) text(XX) replace(XX,f216) font(3)
84P1 x(10) y(50) text($XX) replace(XX,f215) font(5)`
87 name: 'Barcode + Price Label',
88 description: 'Product barcode with description and price',
90 commands: `#type=1 #sequence=10
91B1 x(50) y(30) text(XX) replace(XX,f239) height(80)
94P1 x(10) y(120) text(XX) replace(XX,f216) font(3)
97P1 x(10) y(140) text($XX) replace(XX,f215) font(4)`
100 name: 'QR Code Product Label',
101 description: 'QR code with product information',
103 commands: `#type=1 #sequence=10
104QR x(20) y(20) text(https://example.com/p/XX) replace(XX,f213) height(100)
107P1 x(130) y(40) text(XX) replace(XX,f216) font(3)
110P1 x(130) y(70) text($XX) replace(XX,f215) font(4)`
113 name: 'Shelf Label Standard',
114 description: 'Complete shelf label with brand, description, and price',
116 commands: `#type=1 #sequence=10
117P1 x(10) y(10) text(XX) replace(XX,f242) font(2)
120P1 x(10) y(30) text(XX) replace(XX,f216) font(3)
123P1 x(10) y(60) text($XX) replace(XX,f215) font(5)
126B1 x(10) y(100) text(XX) replace(XX,f239) height(60)`
129 name: 'Delivery Address Label',
130 description: 'Shipping address with barcode',
132 commands: `#type=1 #sequence=10
133P1 x(10) y(20) text(XX) replace(XX,f110) font(3)
136P1 x(10) y(40) text(XX) replace(XX,f301) font(3)
139P1 x(10) y(60) text(XX) replace(XX,f302) font(3)
142B1 x(10) y(100) text(XX) replace(XX,f210) height(60)`
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' },
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);
191 }, [purpose, reason]);
195 }, [commands, testPid, elements, isSimpleMode]);
197 const loadPrinters = async () => {
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...');
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)
208 // Mock data - would be replaced with actual Windows printer list
210 { id: 0, name: 'Label Printer 1 (USB)', deviceId: 101 },
211 { id: 1, name: 'Label Printer 2 (Network)', deviceId: 102 },
214 console.error('Failed to load printers:', error);
218 const loadFormatData = async () => {
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}...`);
223 // Mock data - would parse response and populate commands textarea
227 // Get default elements for a purpose
228 const getDefaultElementsForPurpose = (purposeId: number): LabelElement[] => {
230 case 1: // Shelf Labels
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
236 case 2: // Product Labels
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 },
242 case 3: // Barcode Labels
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
247 case 4: // Shipping Labels
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
255 case 5: // Delivery Labels
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
262 case 6: // Returns Labels
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 },
271 { type: 'text', x: 10, y: 20, text: 'Sample Text', font: 3 },
276 const handlePurposeChange = (newPurpose: number) => {
277 setPurpose(newPurpose);
278 if (newPurpose !== 151) {
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);
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);
298 // Load format by name
299 console.log(`Loading format: ${name}`);
303 const saveFormat = () => {
305 alert('Please select a purpose first');
309 if (!formatName || formatName.length === 0) {
310 alert('Please provide a format name');
314 const lines = commands.split('\n').filter(line => line.trim().length > 0);
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 '#'`);
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`);
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`);
341 if (errors.length > 0) {
342 alert(errors.join('\n'));
347 console.log('Saving format:', {
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
362 alert('Format saved successfully');
365 const updatePreview = () => {
366 if (isSimpleMode && elements.length > 0) {
367 // Simple mode uses element-based preview
370 // In advanced mode, could parse commands for preview
371 console.log('Updating preview...');
374 const renderSimplePreview = () => {
375 if (elements.length === 0) {
377 <div className="text-center text-muted text-sm">
378 Add elements to see preview
383 const aspectRatio = ((labelHeight / labelWidth) * 100);
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">
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)' }}>
396 {elements.map((element, idx) => {
397 const xPercent = ((element.x / labelWidth) * 100);
398 const yPercent = ((element.y / labelHeight) * 100);
405 left: `${xPercent}%`,
409 {element.type === 'text' && (
411 className="whitespace-nowrap font-mono"
413 fontSize: element.font === 1 ? '8px' :
414 element.font === 2 ? '10px' :
415 element.font === 3 ? '12px' :
416 element.font === 4 ? '16px' : '20px',
419 {element.field ? `{${fieldDefinitions.find(f => f.code === element.field)?.name || element.text}}` : element.text}
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` }} />
429 <div className="text-xs mt-1 font-mono">
430 {element.field ? `{${fieldDefinitions.find(f => f.code === element.field)?.name}}` : element.text}
434 {element.type === 'qr' && (
435 <div className="flex flex-col items-center">
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` }}
440 {[...Array(64)].map((_, i) => (
441 <div key={i} className={`${Math.random() > 0.5 ? 'bg-black' : 'bg-surface'}`} />
444 <div className="text-xs mt-1 font-mono">QR</div>
452 <div className="absolute inset-0 pointer-events-none opacity-10">
453 <svg width="100%" height="100%">
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" />
459 <rect width="100%" height="100%" fill="url(#grid)" />
467 const loadTemplate = (template: LabelTemplate) => {
468 setCommands(template.commands);
469 setPurpose(template.purposeId);
470 setShowTemplates(false);
473 const loadPresetSize = (size: LabelDimensions) => {
474 setLabelWidth(size.width);
475 setLabelHeight(size.height);
476 setCustomDimensions(false);
479 const addElement = (type: 'text' | 'barcode' | 'qr') => {
480 const newElement: LabelElement = {
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,
488 setElements([...elements, newElement]);
489 updateCommandsFromElements([...elements, newElement]);
492 const updateElement = (index: number, updates: Partial<LabelElement>) => {
493 const newElements = [...elements];
494 newElements[index] = { ...newElements[index], ...updates };
495 setElements(newElements);
496 updateCommandsFromElements(newElements);
499 const removeElement = (index: number) => {
500 const newElements = elements.filter((_, i) => i !== index);
501 setElements(newElements);
502 updateCommandsFromElements(newElements);
505 const updateCommandsFromElements = (els: LabelElement[]) => {
507 els.forEach((el, idx) => {
508 const seq = (idx + 1) * 10;
509 cmdText += `#type=1 #sequence=${seq}\n`;
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';
528 setCommands(cmdText.trim());
531 const testPrint = (isPreview: boolean) => {
532 if (testPrinterId < 0) {
533 if (!isPreview) alert('Please select a printer');
537 console.log('Sending print job to Windows printer...', {
538 printerId: testPrinterId,
544 // API call sends DATI XML to Fieldpine server:
545 // <DATI><f8_s>localsystem.print.format</f8_s>
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>
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)
560 // TODO: Implement actual API call
561 // const xmlPacket = buildPrintXML(commands, testPid, printers[testPrinterId].deviceId);
562 // PostXML_RetOjectForGNAP("/DATI", xmlPacket, callback);
565 // Mock preview - would get actual ZPL preview from server
566 setPreviewUrl('https://via.placeholder.com/300x300?text=Label+Preview');
568 alert('Print job sent to Windows printer (placeholder - API not implemented)');
573 <div className="p-6 min-h-screen bg-bg">
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" />
580 <p className="text-muted">
581 Design and manage label formats for printing
586 <div className="max-w-[1600px] mx-auto">
587 <p className="text-muted">Label use requires specialised label printers</p>
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.
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>
604 onClick={() => setIsSimpleMode(true)}
605 className={`px-4 py-2 rounded-md transition-colors ${
607 ? 'bg-brand text-surface'
608 : 'bg-surface-2 text-text hover:bg-surface-2/80'
614 onClick={() => setIsSimpleMode(false)}
615 className={`px-4 py-2 rounded-md transition-colors ${
617 ? 'bg-brand text-surface'
618 : 'bg-surface-2 text-text hover:bg-surface-2/80'
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"
629 <Icon name="help" size={20} />
630 <span>{showHelp ? 'Hide' : 'Show'} Help</span>
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"
637 <Icon name="content_paste" size={20} />
638 <span>{showTemplates ? 'Hide' : 'Show'} Templates</span>
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
650 <div className="space-y-4">
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
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>
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>
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>
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>
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>
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>
703 {/* Templates Section */}
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
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) => (
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"
717 <h3 className="font-semibold text-text mb-1">{template.name}</h3>
718 <p className="text-sm text-muted">{template.description}</p>
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
731 <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3 mb-4">
732 {commonLabelSizes.map((size, 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'
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>
748 <div className="flex items-center gap-4">
749 <label className="flex items-center gap-2 cursor-pointer">
752 checked={customDimensions}
753 onChange={(e) => setCustomDimensions(e.target.checked)}
756 <span className="text-sm font-semibold text-text">Custom Dimensions</span>
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>
766 onChange={(e) => setLabelWidth(Number(e.target.value))}
767 className="w-20 border border-border rounded px-2 py-1 text-sm"
772 <div className="flex items-center gap-2">
773 <label className="text-sm text-text">Height:</label>
777 onChange={(e) => setLabelHeight(Number(e.target.value))}
778 className="w-20 border border-border rounded px-2 py-1 text-sm"
783 <span className="text-xs text-muted">units</span>
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">
794 <div className="flex items-center justify-between mb-2">
795 <label className="block text-sm font-semibold text-text">
798 {isSimpleMode && elements.length > 0 && (
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);
808 className="text-sm text-brand hover:text-brand2"
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"
819 <option value={0}>..Select</option>
820 {labelPurposes.map((p) => (
821 <option key={p.id} value={p.id}>
830 <label className="block text-sm font-semibold text-text mb-2">
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"
839 {labelReasons.map((r) => (
840 <option key={r.id} value={r.id}>
849 <label className="block text-sm font-semibold text-text mb-2">
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"
857 <option value="">..Select</option>
858 {formatNames.map((name) => (
859 <option key={name} value={name}>
863 {purpose === 235 && <option value="Create New">Create New</option>}
868 <div className="flex items-end">
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"
873 <Icon name="save" size={20} />
877 <span className="ml-2 text-sm text-muted">Loading...</span>
882 {/* Commands Editor and Preview */}
883 <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
884 {/* Simple Mode Visual Editor */}
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">
891 <div className="flex gap-2">
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"
896 <Icon name="add" size={16} />
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"
903 <Icon name="add" size={16} />
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"
910 <Icon name="add" size={16} />
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>
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'
932 {element.type.toUpperCase()}
934 <span className="text-sm text-muted">Element {idx + 1}</span>
937 onClick={() => removeElement(idx)}
938 className="text-danger hover:text-danger/80 text-sm font-semibold"
944 <div className="grid grid-cols-2 gap-3">
946 <label className="block text-xs font-semibold text-text mb-1">
947 X Position (left-right)
952 onChange={(e) => updateElement(idx, { x: Number(e.target.value) })}
953 className="w-full border border-border rounded px-2 py-1 text-sm"
959 <label className="block text-xs font-semibold text-text mb-1">
960 Y Position (top-bottom)
965 onChange={(e) => updateElement(idx, { y: Number(e.target.value) })}
966 className="w-full border border-border rounded px-2 py-1 text-sm"
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'}
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'}
986 <label className="block text-xs font-semibold text-text mb-1">
987 Data Field (optional)
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"
994 <option value="">Static (no field)</option>
995 {fieldDefinitions.map((field) => (
996 <option key={field.code} value={field.code}>
997 {field.name} ({field.code})
1003 {element.type === 'text' && (
1005 <label className="block text-xs font-semibold text-text mb-1">
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"
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>
1022 {element.type !== 'text' && (
1024 <label className="block text-xs font-semibold text-text mb-1">
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"
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)
1049 <div className="p-4">
1050 <pre className="text-xs font-mono bg-surface-2 p-3 rounded overflow-x-auto">{commands}</pre>
1057 /* Advanced Mode - Raw Commands */
1058 <div className="lg:col-span-2">
1059 <label className="block text-sm font-semibold text-text mb-2">
1064 onChange={(e) => setCommands(e.target.value)}
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)
1071B1 x(100) y(150) text(XX) replace(XX,f239) height(100)`}
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>
1087 {/* Example Output */}
1089 <h3 className="text-sm font-semibold text-text mb-2">
1091 <span className="ml-2 text-xs text-muted">(Simulated)</span>
1093 <div className="border border-border rounded-md p-4 bg-surface-2">
1095 renderSimplePreview()
1097 <img src={previewUrl} alt="Label Preview" className="max-w-full mx-auto" />
1099 <div className="text-center text-muted text-sm min-h-[300px] flex items-center justify-center">
1100 Click "Preview" to see your label
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
1109 <span className="text-muted">Preview is scaled to fit. Adjust dimensions above to match your physical label size.</span>
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">
1121 <label className="block text-sm font-semibold text-text mb-2">
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"
1129 <option value={-1}>Select Printer</option>
1130 {printers.map((printer) => (
1131 <option key={printer.id} value={printer.id}>
1139 <label className="block text-sm font-semibold text-text mb-2">
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"
1151 <div className="flex gap-2">
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"
1156 <Icon name="print" size={20} />
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"
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.
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;
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]">
1191 <span className="text-sm text-text">{field.name}</span>
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)
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>
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>
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>
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>
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>
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>