2import { useEffect, useMemo, useState } from "react";
3import { fieldpine } from "@/lib/fieldpine";
4import { Icon } from '@/contexts/IconContext';
6interface ProductOption {
13interface FormatOption {
16 type: "A4 Sign" | "Shelf Label";
20const fallbackProducts: ProductOption[] = [
21 { id: 101, name: "Organic Pears 1kg", description: "Crisp and sweet", price: 2.95 },
22 { id: 205, name: "Honeycrisp Apples", description: "Seasonal promo", price: 3.49 },
23 { id: 330, name: "Bananas", description: "Everyday price", price: 1.99 },
24 { id: 441, name: "Almond Milk 1L", description: "Dairy free", price: 4.5 },
27const starterExamples: Record<number, string> = {
28 1: "SPECIAL\n\nPEARS\n$2.95\n\nToday Only",
29 2: "SPECIAL\n\nPEARS\n>color red\n$2.95\n>color black\n\n\nToday Only",
30 3: ">fontsize 30mm\nSPECIAL\n\nPEARS\n>background red white\n$2.95\n>background white\n>color black\n\nToday Only\n>left\n>fontsize 8mm\nLimit 3 per customer, while stocks last, no rainchecks",
33const initialFormats: FormatOption[] = [
34 { id: "a4-summer", name: "Summer A4", type: "A4 Sign", content: starterExamples[1] },
35 { id: "shelf-std", name: "Standard Shelf", type: "Shelf Label", content: "PRICE\n>fontsize 14mm\n>left\nProduct Name" },
38function mmToPx(input: string) {
39 const numeric = parseFloat(input);
40 if (Number.isNaN(numeric)) return undefined;
41 return numeric * 3.78; // rough mm to px conversion
44export default function SignsAndLabelsPage() {
45 const [signText, setSignText] = useState(starterExamples[1]);
46 const [products, setProducts] = useState<ProductOption[]>(fallbackProducts);
47 const [selectedProduct, setSelectedProduct] = useState<ProductOption>(fallbackProducts[0]);
48 const [productQuery, setProductQuery] = useState("");
49 const [selectedQueue, setSelectedQueue] = useState("Main Label Printer");
50 const [queues] = useState(["Main Label Printer", "Office Laser", "Back Office Zebra"]);
51 const [formats, setFormats] = useState<FormatOption[]>(initialFormats);
52 const [selectedFormatId, setSelectedFormatId] = useState(initialFormats[0].id);
53 const [saveMessage, setSaveMessage] = useState("");
54 const [newFormatName, setNewFormatName] = useState("");
55 const [newFormatType, setNewFormatType] = useState<FormatOption["type"]>("A4 Sign");
56 const [printMessage, setPrintMessage] = useState("");
57 const [isPrinting, setIsPrinting] = useState(false);
58 const [productsLoading, setProductsLoading] = useState(false);
59 const [productError, setProductError] = useState<string | null>(null);
60 const [includeName, setIncludeName] = useState(true);
61 const [includeDescription, setIncludeDescription] = useState(false);
62 const [includePrice, setIncludePrice] = useState(false);
63 const [pdfMessage, setPdfMessage] = useState<string | null>(null);
66 const savedQueue = typeof window !== "undefined" ? localStorage.getItem("signs.queue") : null;
67 if (savedQueue) setSelectedQueue(savedQueue);
71 if (typeof window !== "undefined") {
72 localStorage.setItem("signs.queue", selectedQueue);
78 const fetchProducts = async () => {
79 setProductsLoading(true);
80 setProductError(null);
82 const result = await fieldpine.getProducts({ limit: 50, search: productQuery || undefined });
83 const mapped: ProductOption[] = (result.products || []).map((p: any) => ({
84 id: p.pid ?? p.id ?? p.productId ?? 0,
85 name: p.description ?? p.name ?? `Product ${p.pid ?? p.id ?? ""}`,
86 description: p.longDescription ?? p.description ?? p.name,
87 price: Number(p.sellprice ?? p.sellPrice ?? p.price ?? 0),
88 })).filter((p: ProductOption) => p.id !== 0);
92 setSelectedProduct((prev) => {
93 const stillExists = mapped.find((m) => m.id === prev?.id);
94 return stillExists ?? mapped[0];
97 setProducts(fallbackProducts);
98 setSelectedProduct(fallbackProducts[0]);
100 } catch (error: any) {
102 setProductError(error?.message || "Unable to load products");
103 setProducts(fallbackProducts);
104 setSelectedProduct(fallbackProducts[0]);
106 if (active) setProductsLoading(false);
110 const timer = setTimeout(fetchProducts, 250);
117 const filteredProducts = useMemo(() => {
118 if (!productQuery.trim()) return products;
119 const q = productQuery.toLowerCase();
120 return products.filter((p) =>
121 p.name.toLowerCase().includes(q) ||
122 p.description?.toLowerCase().includes(q) ||
123 p.id.toString() === q
125 }, [productQuery, products]);
127 const parsedLines = useMemo(() => {
128 const autoLines: string[] = [];
129 if (includeName && selectedProduct?.name) autoLines.push(selectedProduct.name);
130 if (includeDescription && selectedProduct?.description) autoLines.push(selectedProduct.description);
131 if (includePrice && Number.isFinite(selectedProduct?.price)) autoLines.push(`$${selectedProduct.price.toFixed(2)}`);
133 const lines = [...autoLines, ...signText.split(/\r?\n/)] as string[];
134 const out: { id: number; text: string; style: React.CSSProperties }[] = [];
135 let currentColor = "#0f172a";
136 let currentBackground = "white";
137 let currentAlign: React.CSSProperties["textAlign"] = "center";
138 let currentFontSize = 28;
140 lines.forEach((raw, idx) => {
141 if (raw.startsWith(">")) {
142 const parts = raw.slice(1).trim().split(/\s+/);
143 const [command, ...rest] = parts;
144 if (command === "color" && rest[0]) currentColor = rest[0];
145 if (command === "background" && rest[0]) currentBackground = rest[0];
146 if (command === "fontsize" && rest[0]) currentFontSize = mmToPx(rest[0]) ?? currentFontSize;
147 if (command === "left") currentAlign = "left";
148 if (command === "center") currentAlign = "center";
149 if (command === "right") currentAlign = "right";
155 text: raw.length ? raw : "\u00a0",
158 background: currentBackground,
159 textAlign: currentAlign,
160 fontSize: currentFontSize,
170 const handleExample = (n: number) => {
171 setSignText(starterExamples[n]);
175 const handleLoadFormat = () => {
176 const fmt = formats.find((f) => f.id === selectedFormatId);
178 setSignText(fmt.content);
179 setSaveMessage(`Loaded ${fmt.name}`);
180 setTimeout(() => setSaveMessage(""), 2000);
183 const handleSaveFormat = () => {
184 setFormats((prev) => prev.map((f) => (f.id === selectedFormatId ? { ...f, content: signText } : f)));
185 const fmt = formats.find((f) => f.id === selectedFormatId);
186 setSaveMessage(fmt ? `${fmt.name} saved` : "Saved changes");
187 setTimeout(() => setSaveMessage(""), 2200);
190 const handleSaveNew = () => {
191 if (!newFormatName.trim()) {
192 setSaveMessage("Enter a name to save a new format");
195 const id = `custom-${Date.now()}`;
196 const next: FormatOption = { id, name: newFormatName.trim(), type: newFormatType, content: signText };
197 setFormats((prev) => [...prev, next]);
198 setSelectedFormatId(id);
199 setNewFormatName("");
200 setSaveMessage(`Saved new format '${next.name}'`);
201 setTimeout(() => setSaveMessage(""), 2200);
204 const handlePrint = () => {
206 setPrintMessage("Sending to printer queue...");
208 setIsPrinting(false);
209 setPrintMessage(`Queued to ${selectedQueue} for ${selectedProduct.name}`);
213 const handlePrintToPdf = () => {
214 if (typeof window === "undefined") return;
216 const html = `<!doctype html>
219 <meta charset="utf-8" />
220 <title>Sign Preview</title>
222 body { margin: 0; padding: 24px; font-family: 'Inter', system-ui, sans-serif; background: #f8fafc; }
223 .page { width: 210mm; height: 297mm; padding: 16mm; box-sizing: border-box; background: white; border: 1px solid #cbd5e1; box-shadow: 0 10px 30px rgba(15, 23, 42, 0.12); }
224 .header { display: flex; justify-content: space-between; font-size: 12px; color: #475569; margin-bottom: 12px; }
225 .lines { display: flex; flex-direction: column; gap: 8px; align-items: stretch; }
231 ${parsedLines.map((line) => `<div style="${[
232 `color:${line.style.color ?? "#0f172a"}`,
233 `background:${line.style.background ?? "white"}`,
234 `text-align:${line.style.textAlign ?? "center"}`,
235 `font-size:${typeof line.style.fontSize === "number" ? `${line.style.fontSize}px` : line.style.fontSize ?? "28px"}`,
236 `line-height:${line.style.lineHeight ?? 1.05}`,
237 `font-weight:${line.style.fontWeight ?? "bold"}`,
238 `padding:${line.style.padding ?? "4px 6px"}`
239 ].join(";")}">${line.text || " "}</div>`).join("")}
242 <script>window.onload = () => { window.focus(); window.print(); };</script>
247 const blob = new Blob([html], { type: "text/html" });
248 const url = URL.createObjectURL(blob);
249 const iframe = document.createElement("iframe");
250 iframe.style.position = "fixed";
251 iframe.style.right = "0";
252 iframe.style.bottom = "0";
253 iframe.style.width = "0";
254 iframe.style.height = "0";
255 iframe.style.border = "0";
257 iframe.onload = () => {
259 iframe.contentWindow?.focus();
260 iframe.contentWindow?.print();
261 setPdfMessage("Opened browser print dialog — choose Save as PDF.");
263 setPdfMessage("Unable to open print dialog. Allow popups or try again.");
266 document.body.appendChild(iframe);
268 document.body.removeChild(iframe);
269 URL.revokeObjectURL(url);
272 setPdfMessage("Unable to generate PDF preview.");
276 const handleSelectProduct = (id: number) => {
277 const match = products.find((p) => p.id === id);
278 if (match) setSelectedProduct(match);
282 <div className="p-8 space-y-6">
283 <div className="flex items-center justify-between gap-4">
285 <p className="text-sm text-muted mb-1">Products / Signs & Labels</p>
286 <h1 className="text-3xl font-bold text-text">Signs and Labels</h1>
287 <p className="text-sm text-muted">Draft, preview, and send signage to your label printers.</p>
289 <div className="hidden md:flex items-center gap-3 bg-surface border border-border rounded-lg px-4 py-2 shadow-sm">
290 <div className="text-xs text-muted">Queue</div>
292 className="border border-border rounded px-3 py-1 text-sm"
293 value={selectedQueue}
294 onChange={(e) => setSelectedQueue(e.target.value)}
297 <option key={q} value={q}>{q}</option>
303 <div className="grid gap-6 lg:grid-cols-3">
304 <div className="lg:col-span-2 space-y-6">
305 <div className="bg-surface border border-border rounded-xl shadow-sm p-4 sm:p-6">
306 <div className="flex flex-col gap-3">
307 <div className="flex items-center justify-between gap-3">
309 <h2 className="text-xl font-semibold text-text">Sign details</h2>
310 <p className="text-sm text-muted">Write your sign text. Use > commands for quick styling.</p>
312 <div className="flex gap-2">
313 {[1, 2, 3].map((n) => (
316 onClick={() => handleExample(n)}
317 className="px-3 py-2 text-sm bg-surface-2 hover:bg-surface rounded border border-border"
325 <div className="flex flex-wrap gap-3 text-sm text-muted">
326 <label className="inline-flex items-center gap-2">
327 <input type="checkbox" checked={includeName} onChange={(e) => setIncludeName(e.target.checked)} />
330 <label className="inline-flex items-center gap-2">
331 <input type="checkbox" checked={includeDescription} onChange={(e) => setIncludeDescription(e.target.checked)} />
334 <label className="inline-flex items-center gap-2">
335 <input type="checkbox" checked={includePrice} onChange={(e) => setIncludePrice(e.target.checked)} />
342 onChange={(e) => setSignText(e.target.value)}
344 className="w-full border border-border rounded-lg p-3 font-mono text-sm outline-none focus:ring-2 focus:ring-brand/50"
345 aria-label="Sign text"
347 <div className="mt-2 text-xs text-muted">
348 Commands: <code>{">color red"}</code>, <code>{">fontsize 30mm"}</code>, <code>{">left|center|right"}</code>, <code>{">background yellow"}</code>
352 <div className="bg-surface border border-border rounded-xl shadow-sm p-4 sm:p-6 flex flex-col gap-4">
353 <div className="flex items-center justify-between">
355 <h3 className="text-lg font-semibold text-text">Sample product</h3>
356 <p className="text-sm text-muted">Used for preview and when sending to the printer queue.</p>
358 <div className="text-sm text-muted">${selectedProduct.price.toFixed(2)}</div>
360 <div className="flex flex-col gap-3">
362 className="border border-border rounded px-3 py-2 text-sm"
363 placeholder="Search product by name or ID"
365 onChange={(e) => setProductQuery(e.target.value)}
367 {productError && <div className="text-xs text-red-600">{productError}</div>}
368 <div className="flex flex-wrap gap-2">
370 <div className="text-sm text-muted">Loading products…</div>
371 ) : filteredProducts.length === 0 ? (
372 <div className="text-sm text-muted">No products found</div>
374 filteredProducts.map((p) => (
377 onClick={() => handleSelectProduct(p.id)}
378 className={`px-3 py-2 rounded border text-sm ${selectedProduct.id === p.id ? "bg-brand text-white border-brand" : "bg-surface border-border hover:border-brand"}`}
380 <div className="font-medium">{p.name}</div>
381 {p.description && <div className="text-xs text-muted">{p.description}</div>}
390 <div className="space-y-6">
391 <div className="bg-surface border border-border rounded-xl shadow-sm p-4 sm:p-6">
392 <div className="flex items-center justify-between mb-3">
394 <h3 className="text-lg font-semibold text-text">Quick preview</h3>
395 <p className="text-xs text-muted">Preview is indicative. Print to see final layout.</p>
397 <span className="text-xs px-2 py-1 rounded bg-surface-2 border border-border">A4</span>
399 <div className="border border-border rounded-lg bg-surface overflow-hidden">
400 <div className="bg-surface-2 px-3 py-2 text-xs text-muted flex justify-between">
401 <span>{selectedProduct.name}</span>
402 <span>Queue: {selectedQueue}</span>
404 <div className="p-4 min-h-[420px] flex items-center justify-center">
405 <div className="w-full max-w-[360px] border border-border shadow-inner rounded-lg bg-surface aspect-[1/1.414] flex flex-col items-stretch justify-center p-4 gap-2">
406 {parsedLines.map((line) => (
407 <div key={line.id} style={line.style}>
415 onClick={handlePrint}
416 disabled={isPrinting}
417 className="w-full mt-4 px-4 py-3 rounded-lg bg-brand text-white font-semibold hover:bg-brand/90 disabled:opacity-60"
419 {isPrinting ? "Sending..." : "Print Large Sign"}
421 {printMessage && <div className="mt-2 text-sm text-brand">{printMessage}</div>}
423 onClick={handlePrintToPdf}
424 className="w-full mt-2 px-4 py-3 rounded-lg border border-border bg-surface text-text font-semibold hover:bg-surface-2"
426 Save/Print as PDF (browser)
428 {pdfMessage && <div className="mt-1 text-xs text-muted">{pdfMessage}</div>}
431 <div className="bg-surface border border-border rounded-xl shadow-sm p-4 sm:p-6 space-y-4">
432 <div className="flex items-center justify-between">
433 <h3 className="text-lg font-semibold text-text">Design & Save</h3>
435 className="border border-border rounded px-2 py-1 text-sm"
436 value={selectedQueue}
437 onChange={(e) => setSelectedQueue(e.target.value)}
440 <option key={q} value={q}>{q}</option>
445 <div className="space-y-3">
446 <label className="text-sm font-medium text-text">Load an existing format</label>
447 <div className="flex gap-2">
449 className="flex-1 border border-border rounded px-3 py-2 text-sm"
450 value={selectedFormatId}
451 onChange={(e) => setSelectedFormatId(e.target.value)}
453 {formats.map((fmt) => (
454 <option key={fmt.id} value={fmt.id}>{fmt.name} ({fmt.type})</option>
458 onClick={handleLoadFormat}
459 className="px-3 py-2 rounded bg-surface-2 border border-border text-sm hover:bg-surface"
464 onClick={handleSaveFormat}
465 className="px-3 py-2 rounded bg-brand text-white text-sm hover:bg-brand/90"
472 <div className="border-t border-border pt-3 space-y-3">
473 <div className="text-sm font-medium text-text">Save as new</div>
474 <div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
476 className="border border-border rounded px-3 py-2 text-sm"
477 placeholder="Format name"
478 value={newFormatName}
479 onChange={(e) => setNewFormatName(e.target.value)}
482 className="border border-border rounded px-3 py-2 text-sm"
483 value={newFormatType}
484 onChange={(e) => setNewFormatType(e.target.value as FormatOption["type"])}
486 <option value="A4 Sign">A4 Sign</option>
487 <option value="Shelf Label">Shelf Label</option>
491 onClick={handleSaveNew}
492 className="w-full px-3 py-2 rounded bg-surface-2 border border-border text-sm hover:bg-surface"
498 {saveMessage && <div className="text-sm text-brand">{saveMessage}</div>}