@apptimate/ui 5.9.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,609 @@
1
+
2
+ import { QRCodeSVG } from "qrcode.react";
3
+ import React from "react";
4
+ import Barcode from "react-barcode";
5
+
6
+ export function replaceTokens(text: string, entityData: any): string {
7
+ if (!text || typeof text !== "string") return text;
8
+ const data = entityData || {};
9
+ let result = text.replace(/\{\{\s*([\w.]+)\s*\}\}(\s*g(?!\w))?/g, (_, path, trailingG: string | undefined) => {
10
+ const keys = path.trim().split(".");
11
+ let val = data;
12
+ for (const k of keys) {
13
+ if (val === undefined || val === null) break;
14
+ val = val[k];
15
+ }
16
+
17
+ // If undefined, let's try row context fallback
18
+ if ((val === undefined || val === null) && keys.length > 1) {
19
+ // If user provided a nested key but it's directly on data
20
+ if (data[path.trim()] !== undefined) {
21
+ val = data[path.trim()];
22
+ }
23
+ }
24
+
25
+ const fmtCurrency = (v: any) => (v && !isNaN(Number(v)) && Number(v) > 0) ? `${data.currency_code || ""} ${Number(v).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` : "—";
26
+
27
+ // Helper to compute payment info (shared between payment_breakdown and credit_display)
28
+ const computePaymentInfo = () => {
29
+ const payments = data.payments || data.invoice?.payments || [];
30
+ const grandTotal = Number(data.grand_total || data.total_estimated_cost || 0);
31
+ let amountPaid = 0;
32
+ let paidLines: string[] = [];
33
+
34
+ if (payments.length === 0) {
35
+ amountPaid = Number(data.amount_paid || data.total_advance || 0);
36
+ if (data.payment_method && String(data.payment_method).toLowerCase() !== "credit" && amountPaid > 0) {
37
+ const methodStr = String(data.payment_method).toUpperCase();
38
+ paidLines.push(`Paid (${methodStr}) - ${fmtCurrency(amountPaid)}`);
39
+ }
40
+ } else {
41
+ payments.forEach((p: any) => {
42
+ const methodStr = String(p.mode_code || p.payment_mode?.name || p.payment_method || "PAYMENT").toUpperCase();
43
+ if (methodStr === "CREDIT") return; // skip credit from paid lines
44
+ const amt = Number(p.amount ?? p.payment_amount ?? 0);
45
+ if (amt > 0) {
46
+ amountPaid += amt;
47
+ paidLines.push(`Paid (${methodStr}) - ${fmtCurrency(amt)}`);
48
+ }
49
+ });
50
+ }
51
+
52
+ // Calculate the remaining balance strictly based on what is being displayed
53
+ // as paid to prevent duplicate credit/cash balance display
54
+ const balance = Math.max(0, grandTotal - amountPaid);
55
+
56
+ const hasCredit = balance > 0.01 || (paidLines.length === 0 && grandTotal > 0 && amountPaid === 0);
57
+ const creditAmount = balance > 0.01 ? balance : (paidLines.length === 0 && grandTotal > 0 && amountPaid === 0 ? grandTotal : 0);
58
+
59
+ return { paidLines, hasCredit, creditAmount, grandTotal };
60
+ };
61
+
62
+ // Fallback logic for payment_breakdown (only paid methods, no credit)
63
+ if ((val === undefined || val === null) && path.trim() === "payment_breakdown") {
64
+ const { paidLines, hasCredit, creditAmount, grandTotal } = computePaymentInfo();
65
+
66
+ if (paidLines.length > 0) {
67
+ val = paidLines.join("\n");
68
+ } else if (hasCredit) {
69
+ val = "Credit - " + fmtCurrency(creditAmount);
70
+ } else {
71
+ val = "Credit - " + fmtCurrency(grandTotal);
72
+ }
73
+ }
74
+
75
+ // Fallback logic for refund_breakdown (shows refund methods and credit)
76
+ if ((val === undefined || val === null) && path.trim() === "refund_breakdown") {
77
+ const payments = data.payments || data.invoice?.payments || [];
78
+ const totalRefund = Number(data.refund_amount || data.grand_total || 0);
79
+ let amountRefunded = 0;
80
+ let refundLines: string[] = [];
81
+
82
+ if (payments.length === 0) {
83
+ amountRefunded = totalRefund;
84
+ if (data.refund_method && String(data.refund_method).toLowerCase() !== "credit" && amountRefunded > 0) {
85
+ const methodStr = String(data.refund_method).charAt(0).toUpperCase() + String(data.refund_method).slice(1);
86
+ refundLines.push(`Refund (${methodStr}) ${fmtCurrency(amountRefunded)}`);
87
+ }
88
+ } else {
89
+ payments.forEach((p: any) => {
90
+ const methodStr = String(p.mode_code || p.payment_mode?.name || p.payment_method || "PAYMENT");
91
+ const formattedMethod = methodStr.charAt(0).toUpperCase() + methodStr.slice(1).toLowerCase();
92
+ if (formattedMethod.toUpperCase() === "CREDIT") return; // skip credit from paid lines
93
+ const amt = Number(p.amount ?? p.payment_amount ?? 0);
94
+ if (amt > 0) {
95
+ amountRefunded += amt;
96
+ refundLines.push(`Refund (${formattedMethod}) ${fmtCurrency(amt)}`);
97
+ }
98
+ });
99
+ }
100
+
101
+ const balance = Math.max(0, totalRefund - amountRefunded);
102
+ const hasCredit = balance > 0.01 || (refundLines.length === 0 && totalRefund > 0 && amountRefunded === 0);
103
+ const creditAmount = balance > 0.01 ? balance : (refundLines.length === 0 && totalRefund > 0 && amountRefunded === 0 ? totalRefund : 0);
104
+
105
+ if (refundLines.length > 0) {
106
+ val = refundLines.join("\n");
107
+ if (hasCredit) val += `\nRefund (Credit) ${fmtCurrency(creditAmount)}`;
108
+ } else if (hasCredit) {
109
+ val = "Refund (Credit) " + fmtCurrency(creditAmount);
110
+ } else {
111
+ val = "Refund (Credit) " + fmtCurrency(totalRefund);
112
+ }
113
+ }
114
+
115
+ // Fallback logic for payment_breakdown_labels and _amounts
116
+ if ((val === undefined || val === null) && path.trim() === "payment_breakdown_labels") {
117
+ const { paidLines, hasCredit } = computePaymentInfo();
118
+ if (paidLines.length > 0) {
119
+ val = paidLines.map(l => l.split(' - ')[0]).join("\n");
120
+ } else if (hasCredit) {
121
+ val = "Credit";
122
+ } else {
123
+ val = "Credit";
124
+ }
125
+ }
126
+ if ((val === undefined || val === null) && path.trim() === "payment_breakdown_amounts") {
127
+ const { paidLines, hasCredit, creditAmount, grandTotal } = computePaymentInfo();
128
+ if (paidLines.length > 0) {
129
+ val = paidLines.map(l => l.split(' - ')[1]).join("\n");
130
+ } else if (hasCredit) {
131
+ val = fmtCurrency(creditAmount);
132
+ } else {
133
+ val = fmtCurrency(grandTotal);
134
+ }
135
+ }
136
+
137
+ // Fallback logic for credit_display (shows only if there is credit/balance due)
138
+ if ((val === undefined || val === null) && path.trim() === "credit_display") {
139
+ const { paidLines, hasCredit, creditAmount } = computePaymentInfo();
140
+ if (hasCredit && paidLines.length > 0) {
141
+ val = `Credit - ${fmtCurrency(creditAmount)}`;
142
+ } else {
143
+ val = ""; // no credit to show, or fully on credit (already shown in payment_breakdown)
144
+ }
145
+ }
146
+
147
+ // Fallback logic for credit_display_labels and _amounts
148
+ if ((val === undefined || val === null) && path.trim() === "credit_display_labels") {
149
+ const { paidLines, hasCredit } = computePaymentInfo();
150
+ if (hasCredit && paidLines.length > 0) {
151
+ val = "Credit";
152
+ } else {
153
+ val = "";
154
+ }
155
+ }
156
+ if ((val === undefined || val === null) && path.trim() === "credit_display_amounts") {
157
+ const { paidLines, hasCredit, creditAmount } = computePaymentInfo();
158
+ if (hasCredit && paidLines.length > 0) {
159
+ val = fmtCurrency(creditAmount);
160
+ } else {
161
+ val = "";
162
+ }
163
+ }
164
+
165
+ // Fallback logic for deduction_display (shows only if there is a deduction)
166
+ if ((val === undefined || val === null) && path.trim() === "deduction_display") {
167
+ const deduction = Number(data.deduction_amount || 0);
168
+ if (deduction > 0) {
169
+ val = `Deduction - ${fmtCurrency(deduction)}`;
170
+ } else {
171
+ val = "";
172
+ }
173
+ }
174
+
175
+ if ((val === undefined || val === null) && path.trim() === "total") val = data.grand_total;
176
+ if ((val === undefined || val === null) && path.trim() === "so_number") val = data.sale_number;
177
+ if ((val === undefined || val === null) && path.trim() === "so_date") val = new Date(data.created_at || data.sale_date).toLocaleDateString();
178
+ if ((val === undefined || val === null) && path.trim() === "expected_date") val = data.expected_date ? new Date(data.expected_date).toLocaleDateString() : (data.created_at ? new Date(data.created_at).toLocaleDateString() : "—");
179
+ if ((val === undefined || val === null) && path.trim() === "delivery_address") val = data.customer?.address || "N/A";
180
+ if ((val === undefined || val === null) && path.trim() === "po_number") val = data.po_number || "—";
181
+ if ((val === undefined || val === null) && path.trim() === "order_number") val = data.order_number || "—";
182
+ if ((val === undefined || val === null) && path.trim() === "sale_number") val = data.sale_number || "—";
183
+ if ((val === undefined || val === null) && path.trim() === "sale_date") val = data.sale_date ? new Date(data.sale_date).toLocaleDateString() : (data.created_at ? new Date(data.created_at).toLocaleDateString() : "—");
184
+ if ((val === undefined || val === null) && path.trim() === "sale_time") val = data.sale_time || (data.created_at ? new Date(data.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : "—");
185
+ if ((val === undefined || val === null) && path.trim() === "purchase_number") val = data.purchase_number || "—";
186
+ if ((val === undefined || val === null) && path.trim() === "total_estimated_cost") val = fmtCurrency(data.total_estimated_cost);
187
+ if ((val === undefined || val === null) && path.trim() === "total_advance") val = fmtCurrency(data.total_advance);
188
+ if ((val === undefined || val === null) && path.trim() === "balance_due") val = fmtCurrency(data.balance_due);
189
+ if ((val === undefined || val === null) && path.trim() === "discount_amount") val = fmtCurrency(data.discount_total || data.discount_amount || 0);
190
+ if ((val === undefined || val === null) && path.trim() === "tax_amount") val = fmtCurrency(data.tax_total || data.tax_amount || 0);
191
+ if ((val === undefined || val === null) && path.trim() === "subtotal") val = fmtCurrency(data.subtotal || data.total_estimated_cost || 0);
192
+ if ((val === undefined || val === null) && path.trim() === "order_date") val = data.order_date ? new Date(data.order_date).toLocaleDateString() : (data.created_at ? new Date(data.created_at).toLocaleDateString() : "—");
193
+ if ((val === undefined || val === null) && path.trim() === "customer_expected_date") val = data.customer_expected_date ? new Date(data.customer_expected_date).toLocaleDateString() : "—";
194
+ if ((val === undefined || val === null) && path.trim() === "occasion") val = data.occasion || "—";
195
+ if ((val === undefined || val === null) && path.trim() === "occasion_date") val = data.occasion_date ? new Date(data.occasion_date).toLocaleDateString() : "—";
196
+ if ((val === undefined || val === null) && path.trim() === "design_notes") val = data.design_notes || "None";
197
+ if ((val === undefined || val === null) && path.trim() === "created_at") val = data.created_at ? new Date(data.created_at).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) : "—";
198
+ if ((val === undefined || val === null) && path.trim() === "updated_at") val = data.updated_at ? new Date(data.updated_at).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) : "—";
199
+
200
+ // Fallback logic for barcode tokens
201
+ if ((val === undefined || val === null) && path.trim() === "item_name") val = data.name || data.item_name;
202
+ if ((val === undefined || val === null) && path.trim() === "item_sku") val = data.sku || data.code || data.item_sku;
203
+ if ((val === undefined || val === null) && path.trim() === "barcode") val = data.barcode || data.code || data.sku;
204
+ if ((val === undefined || val === null) && path.trim() === "qr_code") val = data.qr_code || data.barcode || data.code || data.sku;
205
+ if ((val === undefined || val === null) && path.trim() === "category") val = data.category?.name || data.category;
206
+ if ((val === undefined || val === null) && path.trim() === "weight") val = data.weight || data.gross_weight || data.net_weight;
207
+ if ((val === undefined || val === null) && path.trim() === "karat") val = data.purity?.karat || data.karat;
208
+
209
+ // Fallbacks for inventory-specific tokens
210
+ if ((val === undefined || val === null) && path.trim() === "inventory_items.name") val = data.name || data.item_name;
211
+ if ((val === undefined || val === null) && (path.trim() === "inventory_items.sku" || path.trim() === "sku")) val = data.sku || data.code || data.item_sku;
212
+ if ((val === undefined || val === null) && path.trim() === "inventory_item_variants.variant_name") val = data.variants?.[0]?.variant_name || data.name;
213
+ if ((val === undefined || val === null) && path.trim() === "inventory_item_variants.sku") val = data.variants?.[0]?.sku || data.sku || data.code;
214
+ if ((val === undefined || val === null) && path.trim() === "inventory_item_variants.barcode") val = data.variants?.[0]?.barcode || data.barcode || data.sku || data.code;
215
+ if ((val === undefined || val === null) && (path.trim() === "inventory_item_variants.sale_price" || path.trim() === "sale_price" || path.trim() === "price")) val = data.sale_price || data.price || data.variants?.[0]?.sale_price || 0;
216
+ if ((val === undefined || val === null) && (path.trim() === "inventory_item_variants.cost_price" || path.trim() === "cost_price")) val = data.cost_price || data.variants?.[0]?.cost_price || 0;
217
+ if ((val === undefined || val === null) && path.trim() === "inventory_batches.batch_number") val = data.batch_number || data.batch?.batch_number || "";
218
+ if ((val === undefined || val === null) && path.trim() === "inventory_batches.selling_price") val = data.price || data.selling_price || data.batch?.selling_price || data.sale_price || 0;
219
+
220
+ // Table Row Fallbacks for tokens that start with "items." (e.g. items.product.name)
221
+ if ((val === undefined || val === null) && path.trim().startsWith("items.") && !Array.isArray(data.items)) {
222
+ const p = path.trim().toLowerCase();
223
+ // General fallback: remove "items." and resolve the rest on data
224
+ const subPath = path.trim().substring(6);
225
+ const subKeys = subPath.split(".");
226
+ let subVal = data;
227
+ for (const sk of subKeys) {
228
+ if (subVal === undefined || subVal === null) break;
229
+ subVal = subVal[sk];
230
+ }
231
+ if (subVal !== undefined && subVal !== null) {
232
+ val = subVal;
233
+ } else {
234
+ if (p.includes("name") || p.includes("product") || p.includes("description")) val = data.description || data.item?.name || data.item_name;
235
+ if (p.includes("quantity") || p.includes("qty")) val = data.qty || data.quantity;
236
+ if (p.includes("total") || p.includes("amount") || p.includes("price") || p.includes("cost")) val = data.amount || data.subtotal || data.line_total;
237
+ }
238
+ }
239
+ let orgData: any = {};
240
+ try {
241
+ if (typeof window !== 'undefined') {
242
+ const stored = localStorage.getItem('selected_organization');
243
+ if (stored) orgData = JSON.parse(stored) || {};
244
+ }
245
+ } catch { }
246
+
247
+ if ((val === undefined || val === null) && path.trim() === "company_name") val = data.organization?.name || orgData.name || "";
248
+ if ((val === undefined || val === null) && path.trim() === "organization.address") val = data.organization?.location || orgData.location || "";
249
+ if ((val === undefined || val === null) && path.trim() === "organization.phone") val = data.organization?.primary_contact_number || orgData.primary_contact_number || "";
250
+ if ((val === undefined || val === null) && path.trim() === "organization.email") val = data.organization?.email || orgData.email || "";
251
+ if ((val === undefined || val === null) && (path.trim() === "organization.logo" || path.trim() === "organization.logo_url")) val = data.organization?.logo_url || orgData.logo_url || "";
252
+ if ((val === undefined || val === null) && path.trim() === "warehouse.address") val = data.warehouse?.location || "";
253
+ if ((val === undefined || val === null) && path.trim() === "warehouse.phone") val = data.warehouse?.primary_contact_number || "";
254
+ if ((val === undefined || val === null) && path.trim().startsWith("organization.")) {
255
+ const field = path.trim().split(".")[1];
256
+ if (field) {
257
+ val = data.organization?.[field] ?? orgData[field] ?? "";
258
+ }
259
+ }
260
+
261
+ // Format dates nicely
262
+ if (val !== undefined && val !== null && (path.includes("date") || path.includes("_at"))) {
263
+ if (typeof val === "string" && !isNaN(Date.parse(val)) && !path.includes("updated_at") && !path.includes("created_at")) {
264
+ val = new Date(val).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
265
+ } else if (typeof val === "string" && !isNaN(Date.parse(val))) {
266
+ val = new Date(val).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
267
+ }
268
+ }
269
+
270
+ // Format specific currency properties gracefully if they are numbers
271
+ if (val !== undefined && val !== null && (path.includes("amount") || path.includes("total") || path.includes("cost") || path.includes("price"))) {
272
+ if (!isNaN(Number(val)) && Number(val) > 0) {
273
+ val = `${data.currency_code ? data.currency_code + ' ' : ''}${Number(val).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
274
+ }
275
+ }
276
+
277
+ // Hide zero weights
278
+ if (path.includes("weight") && val !== undefined && val !== null) {
279
+ const strVal = String(val).trim();
280
+ if (strVal === "0" || strVal === "0.00" || strVal === "0.000" || strVal === "") {
281
+ val = "";
282
+ }
283
+ }
284
+
285
+ const finalVal = val !== undefined && val !== null && String(val).trim() !== "" ? String(val) : "";
286
+ return finalVal ? finalVal + (trailingG || "") : "";
287
+ });
288
+
289
+ // Cleanup dangling or repeated separators caused by empty token replacements
290
+ result = result.replace(/(?:\s*-\s*){2,}/g, ' - ').replace(/^(?:\s*-\s*)+/, '').replace(/(?:\s*-\s*)+$/, '');
291
+ return result;
292
+ }
293
+
294
+ export function TemplateRenderer({ template, lines, entityData, noPadding }: { template: any; lines: any[]; entityData: any; noPadding?: boolean }) {
295
+ const defaultFont = template.default_font_family || 'Courier New';
296
+ const defaultColor = template.default_font_color || "#000000";
297
+ const defaultFontSize = template.content_config?.default_font_size || 12;
298
+ const isA4 = (template.width_mm || template.page_width_mm || 210) > 100;
299
+
300
+ return (
301
+ <div style={{ padding: noPadding ? 0 : (template.content_config?.margins ? `${template.content_config.margins.top ?? template.margin_mm ?? 1}mm ${template.content_config.margins.right ?? template.margin_mm ?? 1}mm ${template.content_config.margins.bottom ?? template.margin_mm ?? 1}mm ${template.content_config.margins.left ?? template.margin_mm ?? 1}mm` : `${template.margin_mm !== undefined && template.margin_mm !== null ? template.margin_mm : 1}mm`), backgroundColor: "#fff", color: defaultColor, fontFamily: defaultFont, fontSize: `${defaultFontSize}px` }}>
302
+ {lines.map((line, idx) => (
303
+ <LineRenderer key={idx} line={line} defaultFont={defaultFont} defaultColor={defaultColor} defaultFontSize={defaultFontSize} entityData={entityData} isA4={isA4} />
304
+ ))}
305
+ </div>
306
+ );
307
+ }
308
+
309
+ function LineRenderer({ line, defaultFont, defaultColor, defaultFontSize, entityData, isA4 }: { line: any; defaultFont: string; defaultColor: string; defaultFontSize: number; entityData: any; isA4?: boolean }) {
310
+ let showIfKey = null;
311
+ if (line.extra_config) {
312
+ try {
313
+ const ec = typeof line.extra_config === 'string' ? JSON.parse(line.extra_config) : line.extra_config;
314
+ if (ec.show_if) showIfKey = ec.show_if;
315
+ } catch (e) { }
316
+ }
317
+
318
+ if (showIfKey) {
319
+ const key = showIfKey.replace(/[{}]/g, '').trim();
320
+ let val = entityData;
321
+ for (const k of key.split('.')) {
322
+ if (val == null) break;
323
+ val = val[k];
324
+ }
325
+ const num = Number(val);
326
+ if (!val || val === "0" || val === 0 || val === "0.00" || val === "—" || (!isNaN(num) && num <= 0)) {
327
+ return null;
328
+ }
329
+ }
330
+
331
+ const baseStyle: React.CSSProperties = {
332
+ fontFamily: `${line.font_family || defaultFont}, sans-serif`,
333
+ fontSize: `${Number(line.font_size) || defaultFontSize}px`,
334
+ lineHeight: 1.15,
335
+ fontWeight: line.font_weight === "bold" ? 700 : line.font_weight === "light" ? 300 : (isA4 === false ? 600 : 400),
336
+ color: line.font_color || defaultColor,
337
+ textAlign: (line.text_align || "left") as any,
338
+ textTransform: (line.text_transform || "none") as any,
339
+ fontStyle: line.is_italic ? "italic" : "normal",
340
+ textDecoration: line.is_underline ? "underline" : "none",
341
+ backgroundColor: line.background_color || "transparent",
342
+ padding: line.padding || "0",
343
+ borderRadius: line.border_radius || "0",
344
+ marginTop: `${line.spacing_before || 0}px`,
345
+ marginBottom: `${line.spacing_after || 0}px`,
346
+ maxWidth: `${line.max_width_percent || 100}%`,
347
+ wordBreak: "break-word" as const,
348
+ };
349
+
350
+ if (line.border_style && line.border_style !== "none") {
351
+ const bw = `${line.border_width || 1}px`;
352
+ const bc = line.border_color || "#000000";
353
+ const bs = line.border_style;
354
+ const sides = line.border_sides || "all";
355
+ if (sides === "all") baseStyle.border = `${bw} ${bs} ${bc}`;
356
+ else if (sides === "top-bottom") { baseStyle.borderTop = `${bw} ${bs} ${bc}`; baseStyle.borderBottom = `${bw} ${bs} ${bc}`; }
357
+ else if (sides === "left-right") { baseStyle.borderLeft = `${bw} ${bs} ${bc}`; baseStyle.borderRight = `${bw} ${bs} ${bc}`; }
358
+ else (baseStyle as any)[`border${sides.charAt(0).toUpperCase() + sides.slice(1)}`] = `${bw} ${bs} ${bc}`;
359
+ }
360
+
361
+ // Text
362
+ if (line.line_type === "text") {
363
+ return <div style={baseStyle}>{replaceTokens(line.static_text || "", entityData).trim().split("\n").map((t, i) => <div key={i}>{t || <br />}</div>)}</div>;
364
+ }
365
+
366
+ // Token (legacy)
367
+ if (line.line_type === "token") {
368
+ return <div style={baseStyle}>{replaceTokens(`{{${line.token_key}}}`, entityData)}</div>;
369
+ }
370
+
371
+ // Separator
372
+ if (line.line_type === "separator") {
373
+ const isVertical = line.separator_direction === "vertical";
374
+ const style = line.separator_style || "solid";
375
+ const thickness = line.separator_thickness || 1;
376
+ const color = line.separator_color || line.font_color || defaultColor;
377
+
378
+ if (isVertical) return <div style={{ display: "flex", justifyContent: "center", marginTop: `${line.spacing_before || 0}px`, marginBottom: `${line.spacing_after || 0}px` }}><div style={{ width: 0, height: "24px", borderLeft: `${thickness}px ${style} ${color}` }} /></div>;
379
+ return <div style={{ marginTop: `${line.spacing_before || 0}px`, marginBottom: `${line.spacing_after || 0}px` }}><hr style={{ border: "none", borderTop: style === "double" ? `${thickness}px double ${color}` : `${thickness}px ${style} ${color}`, margin: 0 }} /></div>;
380
+ }
381
+
382
+ // Spacer
383
+ if (line.line_type === "spacer") {
384
+ return <div style={{ height: `${(line.spacing_before || 4) + (line.spacing_after || 4)}px` }} />;
385
+ }
386
+
387
+ // Image
388
+ if (line.line_type === "image") {
389
+ const rawUrl = (line.extra_config || {}).url || "";
390
+ const url = rawUrl.includes("{{") ? replaceTokens(rawUrl, entityData) : rawUrl;
391
+ const width = (line.extra_config || {}).width_px || 100;
392
+ const align = line.text_align || "center";
393
+
394
+ if (!url) return null; // Don't render a grey box in actual prints if there's no image
395
+
396
+ return (
397
+ <div style={{ marginTop: `${line.spacing_before || 0}px`, marginBottom: `${line.spacing_after || 0}px`, display: "flex", justifyContent: align === "left" ? "flex-start" : align === "right" ? "flex-end" : "center" }}>
398
+ <img src={url} alt="Logo" style={{ width: `${width}px`, height: "auto", objectFit: "contain" }} />
399
+ </div>
400
+ );
401
+ }
402
+
403
+ // Columns
404
+ if (line.line_type === "columns") {
405
+ const childCols = line.extra_config?.columns || line.columns || [];
406
+ return (
407
+ <div style={{ display: "flex", marginTop: `${line.spacing_before || 0}px`, marginBottom: `${line.spacing_after || 0}px`, backgroundColor: line.background_color || "transparent", padding: line.padding || "0", borderRadius: line.border_radius || "0" }}>
408
+ {childCols.map((col: any, i: number) => (
409
+ <div key={i} style={{ width: `${col.width_percent || 50}%`, fontFamily: `${col.font_family || line.font_family || defaultFont}, sans-serif`, fontSize: `${col.font_size ? Number(col.font_size) : (line.font_size ? Number(line.font_size) : defaultFontSize)}px`, lineHeight: 1.15, fontWeight: col.font_weight === "bold" ? 700 : col.font_weight === "light" ? 300 : (line.font_weight === "bold" ? 700 : line.font_weight === "light" ? 300 : (isA4 === false ? 600 : 400)), color: col.font_color || line.font_color || defaultColor, textAlign: (col.text_align || col.align || "left") as any, fontStyle: col.is_italic ? "italic" : "normal", textDecoration: col.is_underline ? "underline" : "none", textTransform: (col.text_transform || "none") as any, backgroundColor: col.background_color || "transparent", padding: col.padding || "0", whiteSpace: "pre-wrap" }}>
410
+ {replaceTokens(col.content || col.static_text || "", entityData).split("\n").map((t, ii) => <div key={ii}>{t || <br />}</div>)}
411
+ </div>
412
+ ))}
413
+ </div>
414
+ );
415
+ }
416
+
417
+ // Table
418
+ if (line.line_type === "table") {
419
+ const ec = line.extra_config || {};
420
+ const cols = ec.columns || [];
421
+ const showHeader = ec.show_header !== false;
422
+ const headerBg = ec.header_background || "#003366";
423
+ const headerFc = ec.header_font_color || "#FFFFFF";
424
+ const rowSep = ec.row_separator || "dotted";
425
+ const stripeBg = ec.row_stripe_color || "#F8FAFC";
426
+ const headerFs = ec.header_font_size ? Number(ec.header_font_size) : (line.font_size ? Number(line.font_size) : defaultFontSize);
427
+ const rowFs = ec.row_font_size ? Number(ec.row_font_size) : (line.font_size ? Number(line.font_size) - 1 : (defaultFontSize - 1 > 0 ? defaultFontSize - 1 : 11));
428
+ const borderColor = ec.border_color || "#E5E7EB";
429
+ const outerBorder = ec.outer_border !== false;
430
+
431
+ // Resolve data source
432
+ const dataSourceRaw = ec.data_source || "{{ line_items }}";
433
+ const dsKey = dataSourceRaw.replace(/[{}]/g, '').trim();
434
+ let rowsData = [];
435
+ if (dsKey === "line_items" || dsKey === "items") {
436
+ rowsData = entityData.items || entityData.lines || [];
437
+ } else {
438
+ let val = entityData;
439
+ for (const k of dsKey.split('.')) {
440
+ if (val == null) break;
441
+ val = val[k];
442
+ }
443
+ rowsData = val || [];
444
+ }
445
+ if (!Array.isArray(rowsData)) rowsData = [];
446
+
447
+ return (
448
+ <div style={{ marginTop: `${line.spacing_before || 0}px`, marginBottom: `${line.spacing_after || 0}px`, border: outerBorder ? `1px solid ${borderColor}` : "none", borderRadius: line.border_radius || "0", overflow: "hidden", fontFamily: line.font_family || defaultFont }}>
449
+ {showHeader && (
450
+ <div style={{ display: "flex", flexWrap: "wrap", backgroundColor: headerBg, padding: ec.header_padding || "6px 8px" }}>
451
+ {cols.map((col: any, i: number) => (
452
+ <div key={i} style={{ width: `${col.header_width_percent || col.width_percent}%`, color: headerFc, fontSize: `${headerFs}px`, fontWeight: ec.header_bold !== false ? 700 : (isA4 === false ? 600 : 400), textAlign: col.align || "left", whiteSpace: "pre-wrap" }}>
453
+ {typeof col.label === 'string' ? col.label.split('\n').map((t: string, ti: number) => <div key={ti}>{t || <br />}</div>) : col.label}
454
+ </div>
455
+ ))}
456
+ </div>
457
+ )}
458
+ {rowsData.map((row: any, ri: number) => {
459
+ const curCode = entityData.currency_code ? entityData.currency_code + ' ' : "";
460
+ const fmtCurrency = (val: any) => (val && Number(val) > 0) ? `${curCode}${Number(val).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` : "—";
461
+ const fmtNum = (val: any, suffix = "") => (val && Number(val) > 0) ? `${parseFloat(Number(val).toFixed(4))}${suffix}` : "—";
462
+ const fmtQty = (val: any) => (val && Number(val) > 0) ? parseFloat(Number(val).toFixed(4)).toString() : "—";
463
+
464
+ const rowDataCtx = {
465
+ ...row,
466
+ index: ri + 1,
467
+ description: row.item?.name || row.item_name || "Item",
468
+ specification: row.item?.code || "",
469
+
470
+ // Rich item details for multi-line support
471
+ item_details: `${row.item_description || row.item_name || row.item?.name || "Item"}\n${row.item?.code ? row.item.code + " " : ""}${String(row.line_type || row.item?.category?.name || "JEWELRY").replace(/_/g, ' ').toUpperCase()}`,
472
+ type: row.receipt_type ? (row.receipt_type === 'bulk' ? 'BULK / LOT' : String(row.receipt_type).replace(/_/g, ' ').toUpperCase()) : (row.item?.item_type ? row.item.item_type.replace('_', ' ').toUpperCase() : (row.item_class || row.item?.item_class ? String(row.item_class || row.item?.item_class).replace('_', ' ').toUpperCase() : (row.item_type || "BULK / LOT").toUpperCase())),
473
+ // New richer fields for GRN matching UI
474
+ material_purity_rich: `${(row.material_type || row.item?.material_type)?.name || "Gold"}\n${row.purity?.purity_percentage || row.purity_percentage || "99.8"}% (${row.purity?.karat || row.purity?.purity_code || "24K"})\nRate: ${fmtCurrency(row.rate_per_gram || row.unit_price)} / g`,
475
+ qty: fmtQty(row.quantity || row.received_quantity || 1),
476
+ gross_wt: fmtNum(row.gross_weight || row.weight_grams || row.metal_weight || row.sale_weight || row.weight, ""),
477
+ weight: fmtNum(row.sale_weight || row.weight || row.gross_weight || row.weight_grams || row.metal_weight, ""),
478
+ material_wt: fmtNum(row.sale_weight || row.metal_weight || row.weight || row.gross_weight || row.weight_grams, ""),
479
+ weight_display: (row.weight_grams || row.net_weight || row.estimated_weight || row.material_weight || row.sale_weight || row.weight) && Number(row.weight_grams || row.net_weight || row.estimated_weight || row.material_weight || row.sale_weight || row.weight) > 0 ? `${parseFloat(Number(row.weight_grams || row.net_weight || row.estimated_weight || row.material_weight || row.sale_weight || row.weight).toFixed(4))}g` : "",
480
+ stone_wt: fmtNum(row.sale_stone_weight || row.stone_weight, ""),
481
+ stone_rich: row.stone_weight && Number(row.stone_weight) > 0 ? `${fmtNum(row.stone_weight, "g")}\n@ ${fmtCurrency(row.stone_cost || row.stone_amount)}` : "—",
482
+ item_value: fmtCurrency(row.item_value || row.subtotal || row.unit_price || row.amount),
483
+ charges_rich: [
484
+ (row.making_charge || row.making_charge_value) && Number(row.making_charge || row.making_charge_value) > 0 ? `MC: ${row.making_charge || row.making_charge_value} (${row.making_charge_type || 'fixed'})` : null,
485
+ (row.wastage || row.wastage_value) && Number(row.wastage || row.wastage_value) > 0 ? `W: ${row.wastage || row.wastage_value} (${row.wastage_type || 'fixed'})` : null,
486
+ (row.discount || row.discount_amount) && Number(row.discount || row.discount_amount) > 0 ? `Disc: -${fmtCurrency(row.discount || row.discount_amount)}` : null,
487
+ (row.tax_rate || row.tax_percentage) && Number(row.tax_rate || row.tax_percentage) > 0 ? `Tax: ${row.tax_rate || row.tax_percentage}%` : null
488
+ ].filter(Boolean).join("\n") || "—",
489
+
490
+ amount: fmtCurrency(row.estimated_total_cost || row.subtotal || row.line_total || row.total_cost || row.amount || row.grand_total),
491
+ amount_num: (row.estimated_total_cost || row.subtotal || row.line_total || row.total_cost || row.amount || row.grand_total)
492
+ ? Number(row.estimated_total_cost || row.subtotal || row.line_total || row.total_cost || row.amount || row.grand_total).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
493
+ : "—"
494
+ };
495
+
496
+ return (
497
+ <div key={ri} style={{ display: "flex", flexWrap: "wrap", padding: ec.row_padding || "4px 8px", backgroundColor: ri % 2 === 1 ? stripeBg : "transparent", borderTop: rowSep !== "none" ? `1px ${rowSep} ${borderColor}` : "none", pageBreakInside: "avoid" }}>
498
+ {cols.map((col: any, ci: number) => {
499
+ let cellVal: any = "";
500
+ if (col.key) {
501
+ let val = rowDataCtx[col.key];
502
+ if (val === undefined || val === null) {
503
+ // Fallback to replaceTokens which now handles dot notation and item.* prefixes
504
+ val = replaceTokens(`{{${col.key}}}`, rowDataCtx);
505
+ }
506
+ cellVal = val ?? "";
507
+ } else if (col.content) {
508
+ cellVal = replaceTokens(col.content, rowDataCtx);
509
+ }
510
+
511
+ // Format numbers if it looks like one and needs fixed decimals
512
+ if (typeof cellVal === 'number' || (typeof cellVal === 'string' && !isNaN(Number(cellVal)) && cellVal.includes('.'))) {
513
+ // Optional: Number(cellVal).toFixed(2);
514
+ }
515
+
516
+ return (
517
+ <div key={ci} style={{ width: `${col.width_percent}%`, fontSize: `${rowFs}px`, color: line.font_color || defaultColor, fontWeight: line.font_weight === "bold" ? 700 : line.font_weight === "light" ? 300 : (isA4 === false ? 600 : 400), textAlign: col.align || "left", whiteSpace: "pre-wrap", fontStyle: line.is_italic ? "italic" : "normal", textDecoration: line.is_underline ? "underline" : "none" }}>
518
+ {typeof cellVal === 'string' ? cellVal.split('\n').map((t: string, ti: number) => <div key={ti}>{t || <br />}</div>) : cellVal}
519
+ </div>
520
+ );
521
+ })}
522
+ </div>
523
+ );
524
+ })}
525
+ </div>
526
+ );
527
+ }
528
+
529
+ // Barcode
530
+ if (line.line_type === "barcode") {
531
+ const rawVal = (line.extra_config || {}).value || "123456789";
532
+ const val = replaceTokens(rawVal, entityData);
533
+ const align = line.text_align || "center";
534
+ return (
535
+ <div className="print-barcode-container" style={{ marginTop: `${line.spacing_before || 0}px`, marginBottom: `${line.spacing_after || 0}px`, display: "flex", justifyContent: align === "left" ? "flex-start" : align === "right" ? "flex-end" : "center", maxWidth: line.max_width_percent ? `${line.max_width_percent}%` : "100%", overflow: "hidden", lineHeight: 0 }}>
536
+ <style>{`.print-barcode-container svg, .print-barcode-container img { max-width: 100%; height: auto; object-fit: contain; }`}</style>
537
+ <Barcode value={val || " "} renderer="svg" height={(line.extra_config || {}).height_px || 40} width={(line.extra_config || {}).bar_width || 1} fontSize={12} displayValue={(line.extra_config || {}).display_value !== false} margin={0} />
538
+ </div>
539
+ );
540
+ }
541
+
542
+ // QR Code
543
+ if (line.line_type === "qrcode") {
544
+ const rawVal = (line.extra_config || {}).value || "https://example.com";
545
+ const val = replaceTokens(rawVal, entityData);
546
+ const align = line.text_align || "center";
547
+ return (
548
+ <div style={{ marginTop: `${line.spacing_before || 0}px`, marginBottom: `${line.spacing_after || 0}px`, display: "flex", justifyContent: align === "left" ? "flex-start" : align === "right" ? "flex-end" : "center" }}>
549
+ <QRCodeSVG value={val || " "} size={(line.extra_config || {}).size_px || 80} level="M" />
550
+ </div>
551
+ );
552
+ }
553
+
554
+ // Points (Cost Code Cipher)
555
+ if (line.line_type === "points") {
556
+ const ec = line.extra_config || {};
557
+ const rawVal = ec.value || "";
558
+ const path = rawVal.replace(/[{}]/g, '').trim();
559
+
560
+ let num = 0;
561
+ if (path === 'inventory_item_variants.cost_price' || path === 'cost_price') {
562
+ num = Number(entityData.cost_price || entityData.variants?.[0]?.cost_price || 0);
563
+ } else {
564
+ let val = entityData;
565
+ for (const k of path.split(".")) {
566
+ if (val == null) break;
567
+ val = val[k];
568
+ }
569
+ num = Number(val);
570
+ }
571
+
572
+ if (!isNaN(num) && num > 0) {
573
+ const map: Record<string, string> = {
574
+ '0': ec.map_0 || '0', '1': ec.map_1 || '1', '2': ec.map_2 || '2',
575
+ '3': ec.map_3 || '3', '4': ec.map_4 || '4', '5': ec.map_5 || '5',
576
+ '6': ec.map_6 || '6', '7': ec.map_7 || '7', '8': ec.map_8 || '8',
577
+ '9': ec.map_9 || '9',
578
+ };
579
+
580
+ const repeatChar = ec.repeat_char || '';
581
+ const intStr = Math.round(num).toString();
582
+
583
+ let cipherText = "";
584
+ let lastChar = "";
585
+
586
+ for (let i = 0; i < intStr.length; i++) {
587
+ const char = intStr[i];
588
+ let mapped = map[char] || char;
589
+ if (repeatChar && mapped === lastChar) {
590
+ mapped = repeatChar;
591
+ }
592
+ cipherText += mapped;
593
+ lastChar = mapped;
594
+ }
595
+
596
+ const prefix = ec.prefix || "";
597
+ const suffix = ec.suffix || "";
598
+ return (
599
+ <div style={baseStyle}>
600
+ {prefix}{cipherText}{suffix}
601
+ </div>
602
+ );
603
+ }
604
+
605
+ return null;
606
+ }
607
+
608
+ return null;
609
+ }