whale-code 6.4.0 → 6.5.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.
- package/bin/swagmanager-mcp.js +7 -0
- package/dist/cli/app.js +30 -2
- package/dist/cli/chat/ChatApp.d.ts +4 -4
- package/dist/cli/chat/ChatApp.js +114 -44
- package/dist/cli/chat/ChatInput.d.ts +13 -6
- package/dist/cli/chat/ChatInput.js +433 -89
- package/dist/cli/chat/MemoryManager.d.ts +15 -0
- package/dist/cli/chat/MemoryManager.js +61 -0
- package/dist/cli/chat/MessageList.d.ts +8 -0
- package/dist/cli/chat/MessageList.js +1 -1
- package/dist/cli/chat/NodeManager.d.ts +30 -0
- package/dist/cli/chat/NodeManager.js +89 -0
- package/dist/cli/chat/NodeSelector.d.ts +19 -0
- package/dist/cli/chat/NodeSelector.js +37 -0
- package/dist/cli/chat/PlanApproval.d.ts +17 -0
- package/dist/cli/chat/PlanApproval.js +82 -0
- package/dist/cli/chat/SessionManager.d.ts +16 -0
- package/dist/cli/chat/SessionManager.js +43 -0
- package/dist/cli/chat/SlashMenu.d.ts +38 -0
- package/dist/cli/chat/SlashMenu.js +208 -0
- package/dist/cli/chat/StatusBar.d.ts +16 -0
- package/dist/cli/chat/StatusBar.js +22 -0
- package/dist/cli/chat/ThemeSelector.d.ts +14 -0
- package/dist/cli/chat/ThemeSelector.js +29 -0
- package/dist/cli/chat/ToolIndicator.d.ts +8 -0
- package/dist/cli/chat/ToolIndicator.js +33 -9
- package/dist/cli/chat/hooks/useAgentLoop.d.ts +2 -1
- package/dist/cli/chat/hooks/useAgentLoop.js +22 -17
- package/dist/cli/chat/hooks/useSlashCommands.d.ts +19 -0
- package/dist/cli/chat/hooks/useSlashCommands.js +254 -15
- package/dist/cli/commands/config-cmd.js +4 -25
- package/dist/cli/commands/db.d.ts +13 -0
- package/dist/cli/commands/db.js +243 -0
- package/dist/cli/commands/doctor.js +6 -9
- package/dist/cli/commands/mcp.js +1 -20
- package/dist/cli/services/agent-events.d.ts +22 -1
- package/dist/cli/services/agent-events.js +9 -0
- package/dist/cli/services/agent-loop.js +66 -2
- package/dist/cli/services/agent-worker-base.js +21 -6
- package/dist/cli/services/api-retry.d.ts +25 -0
- package/dist/cli/services/api-retry.js +91 -0
- package/dist/cli/services/auth-service.d.ts +1 -1
- package/dist/cli/services/auth-service.js +40 -19
- package/dist/cli/services/background-processes.js +26 -2
- package/dist/cli/services/config-store.d.ts +13 -1
- package/dist/cli/services/config-store.js +116 -13
- package/dist/cli/services/format-server-response.js +12 -6
- package/dist/cli/services/ink-resize-fix.d.ts +18 -0
- package/dist/cli/services/ink-resize-fix.js +66 -0
- package/dist/cli/services/interactive-tools.d.ts +14 -0
- package/dist/cli/services/interactive-tools.js +47 -2
- package/dist/cli/services/keybinding-manager.js +1 -1
- package/dist/cli/services/local-tools.js +35 -2
- package/dist/cli/services/server-tools.js +175 -3
- package/dist/cli/services/subagent.js +15 -3
- package/dist/cli/services/system-prompt.js +5 -3
- package/dist/cli/services/task-decomposer.d.ts +35 -0
- package/dist/cli/services/task-decomposer.js +199 -0
- package/dist/cli/services/team-lead.d.ts +18 -0
- package/dist/cli/services/team-lead.js +80 -0
- package/dist/cli/services/teammate.js +5 -5
- package/dist/cli/services/telemetry.d.ts +8 -2
- package/dist/cli/services/telemetry.js +116 -92
- package/dist/cli/services/tools/agent-tools.d.ts +1 -0
- package/dist/cli/services/tools/agent-tools.js +50 -4
- package/dist/cli/services/tools/file-ops.d.ts +2 -0
- package/dist/cli/services/tools/file-ops.js +71 -19
- package/dist/cli/services/tools/shell-exec.js +22 -12
- package/dist/cli/shared/Theme.d.ts +1 -2
- package/dist/cli/shared/Theme.js +1 -1
- package/dist/cli/shared/WhaleBanner.d.ts +4 -1
- package/dist/cli/shared/WhaleBanner.js +12 -8
- package/dist/cli/shared/markdown.d.ts +5 -4
- package/dist/cli/shared/markdown.js +376 -334
- package/dist/cli/shared/theme-manager.d.ts +27 -0
- package/dist/cli/shared/theme-manager.js +178 -0
- package/dist/cli/shared/theme-presets.d.ts +16 -0
- package/dist/cli/shared/theme-presets.js +265 -0
- package/dist/index.js +0 -51
- package/dist/node/adapters/imessage.d.ts +10 -0
- package/dist/node/adapters/imessage.js +45 -6
- package/dist/node/cli.js +459 -8
- package/dist/node/config.d.ts +17 -0
- package/dist/node/gateway-client.d.ts +55 -0
- package/dist/node/gateway-client.js +201 -0
- package/dist/node/portal/clipboard.d.ts +28 -0
- package/dist/node/portal/clipboard.js +183 -0
- package/dist/node/portal/discovery.d.ts +29 -0
- package/dist/node/portal/discovery.js +61 -0
- package/dist/node/portal/forward.d.ts +30 -0
- package/dist/node/portal/forward.js +90 -0
- package/dist/node/portal/index.d.ts +47 -0
- package/dist/node/portal/index.js +250 -0
- package/dist/node/portal/multiplexer.d.ts +48 -0
- package/dist/node/portal/multiplexer.js +207 -0
- package/dist/node/portal/permissions.d.ts +36 -0
- package/dist/node/portal/permissions.js +131 -0
- package/dist/node/portal/protocol.d.ts +140 -0
- package/dist/node/portal/protocol.js +193 -0
- package/dist/node/portal/screen.d.ts +18 -0
- package/dist/node/portal/screen.js +93 -0
- package/dist/node/portal/session.d.ts +68 -0
- package/dist/node/portal/session.js +127 -0
- package/dist/node/portal/shell.d.ts +26 -0
- package/dist/node/portal/shell.js +142 -0
- package/dist/node/portal/stream.d.ts +43 -0
- package/dist/node/portal/stream.js +90 -0
- package/dist/node/portal/transfer.d.ts +33 -0
- package/dist/node/portal/transfer.js +231 -0
- package/dist/node/portal/ui.d.ts +16 -0
- package/dist/node/portal/ui.js +148 -0
- package/dist/node/remote-desktop/compile-helper.d.ts +13 -0
- package/dist/node/remote-desktop/compile-helper.js +73 -0
- package/dist/node/remote-desktop/index.d.ts +67 -0
- package/dist/node/remote-desktop/index.js +220 -0
- package/dist/node/remote-desktop/protocol.d.ts +96 -0
- package/dist/node/remote-desktop/protocol.js +67 -0
- package/dist/node/runtime.d.ts +8 -1
- package/dist/node/runtime.js +117 -9
- package/dist/server/handlers/__test-utils__/test-db.d.ts +25 -0
- package/dist/server/handlers/__test-utils__/test-db.js +128 -0
- package/dist/server/handlers/api-keys.js +26 -2
- package/dist/server/handlers/browser.d.ts +0 -4
- package/dist/server/handlers/browser.js +0 -46
- package/dist/server/handlers/catalog.js +37 -14
- package/dist/server/handlers/clickhouse.d.ts +10 -0
- package/dist/server/handlers/clickhouse.js +215 -0
- package/dist/server/handlers/comms.d.ts +308 -4
- package/dist/server/handlers/comms.js +444 -11
- package/dist/server/handlers/creations.js +1 -1
- package/dist/server/handlers/crm.d.ts +54 -8
- package/dist/server/handlers/crm.js +353 -68
- package/dist/server/handlers/embeddings.js +3 -3
- package/dist/server/handlers/enrichment.js +39 -55
- package/dist/server/handlers/inventory.js +1 -1
- package/dist/server/handlers/kali.d.ts +9 -1
- package/dist/server/handlers/kali.js +50 -1
- package/dist/server/handlers/media.d.ts +8 -0
- package/dist/server/handlers/media.js +902 -0
- package/dist/server/handlers/meta-ads.js +6 -3
- package/dist/server/handlers/nodes.d.ts +2 -0
- package/dist/server/handlers/nodes.js +331 -40
- package/dist/server/handlers/operations.d.ts +4 -6
- package/dist/server/handlers/operations.js +99 -38
- package/dist/server/handlers/platform.js +224 -107
- package/dist/server/handlers/remove-bg.d.ts +6 -0
- package/dist/server/handlers/remove-bg.js +96 -0
- package/dist/server/handlers/storefront.d.ts +6 -0
- package/dist/server/handlers/storefront.js +477 -0
- package/dist/server/handlers/supply-chain.js +21 -3
- package/dist/server/handlers/workflow-steps.js +87 -31
- package/dist/server/handlers/workflows.js +4 -1
- package/dist/server/index.js +334 -88
- package/dist/server/lib/clickhouse-buffer.d.ts +48 -0
- package/dist/server/lib/clickhouse-buffer.js +175 -0
- package/dist/server/lib/clickhouse-client.d.ts +112 -0
- package/dist/server/lib/clickhouse-client.js +141 -0
- package/dist/server/lib/coa-renderer.d.ts +91 -0
- package/dist/server/lib/coa-renderer.js +411 -0
- package/dist/server/lib/compaction-service.js +45 -1
- package/dist/server/lib/pdf-renderer.d.ts +143 -0
- package/dist/server/lib/pdf-renderer.js +867 -0
- package/dist/server/lib/react-pdf-layout.d.ts +40 -0
- package/dist/server/lib/react-pdf-layout.js +437 -0
- package/dist/server/lib/server-agent-loop.d.ts +2 -0
- package/dist/server/lib/server-agent-loop.js +61 -15
- package/dist/server/lib/server-subagent.d.ts +3 -0
- package/dist/server/lib/server-subagent.js +7 -4
- package/dist/server/lib/supabase-client.js +51 -3
- package/dist/server/lib/template-resolver.js +14 -4
- package/dist/server/lib/utils.js +15 -0
- package/dist/server/local-agent-gateway.d.ts +44 -0
- package/dist/server/local-agent-gateway.js +389 -49
- package/dist/server/providers/anthropic.js +12 -2
- package/dist/server/providers/gemini.js +17 -2
- package/dist/server/proxy-handlers.js +151 -0
- package/dist/server/tool-router.d.ts +2 -2
- package/dist/server/tool-router.js +25 -35
- package/dist/shared/agent-core.d.ts +5 -2
- package/dist/shared/agent-core.js +30 -4
- package/dist/shared/api-client.js +54 -3
- package/dist/shared/sse-parser.d.ts +1 -1
- package/dist/shared/sse-parser.js +5 -2
- package/dist/shared/tool-dispatch.js +1 -1
- package/package.json +16 -10
- package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +0 -11
- package/dist/server/handlers/__test-utils__/mock-supabase.js +0 -393
|
@@ -0,0 +1,867 @@
|
|
|
1
|
+
// server/lib/pdf-renderer.ts — PDF template rendering engine
|
|
2
|
+
// Processes pdf_templates layout → HTML, with generation rules, calculations, and validation.
|
|
3
|
+
import { fillTemplate } from "./utils.js";
|
|
4
|
+
function resolvePattern(pattern) {
|
|
5
|
+
const now = new Date();
|
|
6
|
+
return pattern
|
|
7
|
+
.replace("{YYYYMMDD}", now.toISOString().slice(0, 10).replace(/-/g, ""))
|
|
8
|
+
.replace("{YYYY}", String(now.getFullYear()))
|
|
9
|
+
.replace("{MM}", String(now.getMonth() + 1).padStart(2, "0"))
|
|
10
|
+
.replace("{DD}", String(now.getDate()).padStart(2, "0"))
|
|
11
|
+
.replace(/\{PREFIX\}/g, "")
|
|
12
|
+
.replace(/\{RANDOM:(\d+)\}/g, (_, len) => {
|
|
13
|
+
const n = parseInt(len);
|
|
14
|
+
let s = "";
|
|
15
|
+
for (let i = 0; i < n; i++)
|
|
16
|
+
s += Math.floor(Math.random() * 10);
|
|
17
|
+
return s;
|
|
18
|
+
})
|
|
19
|
+
.replace(/\{ALPHA:(\d+)\}/g, (_, len) => {
|
|
20
|
+
const n = parseInt(len);
|
|
21
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
22
|
+
let s = "";
|
|
23
|
+
for (let i = 0; i < n; i++)
|
|
24
|
+
s += chars[Math.floor(Math.random() * 26)];
|
|
25
|
+
return s;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
function normalizeGenerationRules(raw) {
|
|
29
|
+
if (!raw)
|
|
30
|
+
return [];
|
|
31
|
+
if (Array.isArray(raw))
|
|
32
|
+
return raw;
|
|
33
|
+
// Object-keyed format from DB: { "moisture": { "type": "random_range", "config": { "min": 8.5, ... } } }
|
|
34
|
+
// Convert to array format: [{ field: "moisture", type: "random_range", min: 8.5, ... }]
|
|
35
|
+
if (typeof raw === "object") {
|
|
36
|
+
const rules = [];
|
|
37
|
+
for (const [field, def] of Object.entries(raw)) {
|
|
38
|
+
if (!def || typeof def !== "object")
|
|
39
|
+
continue;
|
|
40
|
+
const cfg = def.config || {};
|
|
41
|
+
const rule = {
|
|
42
|
+
field,
|
|
43
|
+
type: def.type,
|
|
44
|
+
pattern: (cfg.pattern || cfg.prefix) ? `${cfg.prefix || ""}${cfg.pattern || ""}` : undefined,
|
|
45
|
+
choices: cfg.choices,
|
|
46
|
+
resolve_pattern: cfg.resolve_pattern,
|
|
47
|
+
min: cfg.min,
|
|
48
|
+
max: cfg.max,
|
|
49
|
+
decimals: cfg.decimals,
|
|
50
|
+
base_field: cfg.base,
|
|
51
|
+
value: cfg.value,
|
|
52
|
+
};
|
|
53
|
+
// relative_date: offset can be { days: [1, 2] } or { days: 5 }
|
|
54
|
+
if (cfg.offset) {
|
|
55
|
+
if (cfg.offset.days !== undefined) {
|
|
56
|
+
rule.offset = Array.isArray(cfg.offset.days) ? cfg.offset.days : cfg.offset.days;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
rule.offset = cfg.offset;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
rules.push(rule);
|
|
63
|
+
}
|
|
64
|
+
return rules;
|
|
65
|
+
}
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
export function applyGenerationRules(rules, data) {
|
|
69
|
+
const normalized = normalizeGenerationRules(rules);
|
|
70
|
+
if (!normalized.length)
|
|
71
|
+
return data;
|
|
72
|
+
const result = { ...data };
|
|
73
|
+
// Multi-pass (max 4) to resolve chained dependencies (e.g. dateCollected → dateReceived → dateTested)
|
|
74
|
+
for (let pass = 0; pass < 4; pass++) {
|
|
75
|
+
let resolved = 0;
|
|
76
|
+
for (const rule of normalized) {
|
|
77
|
+
// User overrides win — skip fields already provided
|
|
78
|
+
if (result[rule.field] !== undefined && result[rule.field] !== null)
|
|
79
|
+
continue;
|
|
80
|
+
// For relative_date, skip if base field isn't resolved yet
|
|
81
|
+
if (rule.type === "relative_date" && rule.base_field) {
|
|
82
|
+
if (result[rule.base_field] === undefined || result[rule.base_field] === null)
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
switch (rule.type) {
|
|
86
|
+
case "pattern": {
|
|
87
|
+
result[rule.field] = resolvePattern(rule.pattern || "");
|
|
88
|
+
resolved++;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
case "random_choice": {
|
|
92
|
+
if (!rule.choices?.length)
|
|
93
|
+
break;
|
|
94
|
+
const pick = rule.choices[Math.floor(Math.random() * rule.choices.length)];
|
|
95
|
+
result[rule.field] = rule.resolve_pattern ? resolvePattern(pick) : pick;
|
|
96
|
+
resolved++;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
case "random_range": {
|
|
100
|
+
const min = rule.min ?? 0;
|
|
101
|
+
const max = rule.max ?? 100;
|
|
102
|
+
const raw = min + Math.random() * (max - min);
|
|
103
|
+
const dec = rule.decimals ?? 2;
|
|
104
|
+
result[rule.field] = parseFloat(raw.toFixed(dec));
|
|
105
|
+
resolved++;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
case "relative_date": {
|
|
109
|
+
const baseVal = result[rule.base_field || ""];
|
|
110
|
+
const base = baseVal ? new Date(baseVal) : new Date();
|
|
111
|
+
let offsetDays;
|
|
112
|
+
if (Array.isArray(rule.offset)) {
|
|
113
|
+
const [lo, hi] = rule.offset;
|
|
114
|
+
offsetDays = lo + Math.floor(Math.random() * (hi - lo + 1));
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
offsetDays = rule.offset ?? 0;
|
|
118
|
+
}
|
|
119
|
+
const target = new Date(base);
|
|
120
|
+
target.setDate(target.getDate() + offsetDays);
|
|
121
|
+
result[rule.field] = target.toISOString().slice(0, 10);
|
|
122
|
+
resolved++;
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
case "constant": {
|
|
126
|
+
const val = rule.value;
|
|
127
|
+
if (val === "TODAY") {
|
|
128
|
+
result[rule.field] = new Date().toISOString().slice(0, 10);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
result[rule.field] = val;
|
|
132
|
+
}
|
|
133
|
+
resolved++;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (resolved === 0)
|
|
139
|
+
break; // No more fields to resolve
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
export function generateCannabinoidData(profileConfig, constants) {
|
|
144
|
+
if (!profileConfig)
|
|
145
|
+
return [];
|
|
146
|
+
const rows = [];
|
|
147
|
+
// Find LOD/LOQ constants — try common keys
|
|
148
|
+
const lodLoqKey = Object.keys(constants).find(k => k.startsWith("LOD_LOQ"));
|
|
149
|
+
const lodLoqMap = (lodLoqKey ? constants[lodLoqKey] : {});
|
|
150
|
+
const defaultLod = 0.01;
|
|
151
|
+
const defaultLoq = 0.03;
|
|
152
|
+
for (const [analyte, cfg] of Object.entries(profileConfig)) {
|
|
153
|
+
const lodLoq = lodLoqMap[analyte] || {};
|
|
154
|
+
const lod = lodLoq.LOD ?? defaultLod;
|
|
155
|
+
const loq = lodLoq.LOQ ?? defaultLoq;
|
|
156
|
+
if (cfg.nd) {
|
|
157
|
+
rows.push({ name: analyte, result: "ND", percentWeight: 0, mgPerG: 0, lod, loq });
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
const min = cfg.min ?? 0;
|
|
161
|
+
const max = cfg.max ?? 100;
|
|
162
|
+
const percentWeight = parseFloat((min + Math.random() * (max - min)).toFixed(3));
|
|
163
|
+
const mgPerG = parseFloat((percentWeight * 10).toFixed(2));
|
|
164
|
+
rows.push({
|
|
165
|
+
name: analyte,
|
|
166
|
+
result: percentWeight < lod ? "ND" : percentWeight,
|
|
167
|
+
percentWeight,
|
|
168
|
+
mgPerG,
|
|
169
|
+
lod,
|
|
170
|
+
loq,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return rows;
|
|
175
|
+
}
|
|
176
|
+
// ============================================================================
|
|
177
|
+
// 2b. FULL PANEL DATA GENERATOR — safety tests from template constants
|
|
178
|
+
// ============================================================================
|
|
179
|
+
export function generateFullPanelData(constants) {
|
|
180
|
+
const microbialTests = (constants["MICROBIAL_TESTS"] || {});
|
|
181
|
+
const heavyMetals = (constants["HEAVY_METALS"] || {});
|
|
182
|
+
const mycotoxins = (constants["MYCOTOXINS"] || {});
|
|
183
|
+
const pesticidesCat1 = (constants["PESTICIDES_CATEGORY_I"] || []);
|
|
184
|
+
const pesticidesCat2 = (constants["PESTICIDES_CATEGORY_II"] || []);
|
|
185
|
+
const microbialResults = Object.entries(microbialTests).map(([test, config]) => {
|
|
186
|
+
let result = "ND";
|
|
187
|
+
if (!config.nd_required && config.typical_range) {
|
|
188
|
+
if (Math.random() > (config.nd_chance || 0)) {
|
|
189
|
+
result = Math.round(config.typical_range.min + Math.random() * (config.typical_range.max - config.typical_range.min));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return { test, result, limit: config.limit, status: "Pass" };
|
|
193
|
+
});
|
|
194
|
+
const heavyMetalsResults = Object.entries(heavyMetals).map(([analyte, config]) => {
|
|
195
|
+
let result = "ND";
|
|
196
|
+
if (Math.random() > config.nd_chance) {
|
|
197
|
+
const val = Math.round((config.typical_range.min + Math.random() * (config.typical_range.max - config.typical_range.min)) * 1000) / 1000;
|
|
198
|
+
result = val < config.loq ? "ND" : val;
|
|
199
|
+
}
|
|
200
|
+
return { analyte, lod: config.lod, loq: config.loq, result, limit: config.limit, status: "Pass" };
|
|
201
|
+
});
|
|
202
|
+
const mycotoxinResults = Object.entries(mycotoxins).map(([analyte, config]) => ({
|
|
203
|
+
analyte, lod: config.lod, loq: config.loq, result: "ND", limit: config.limit, status: "Pass",
|
|
204
|
+
}));
|
|
205
|
+
const pesticidesCat1Results = pesticidesCat1.map(analyte => ({ analyte, result: "ND", status: "Pass" }));
|
|
206
|
+
const pesticidesCat2Results = pesticidesCat2.map(analyte => ({ analyte, result: "ND", status: "Pass" }));
|
|
207
|
+
const residualSolventsData = [
|
|
208
|
+
{ name: "Acetone", limit: 5000 }, { name: "Benzene", limit: 2 }, { name: "Butane", limit: 5000 },
|
|
209
|
+
{ name: "Chloroform", limit: 2 }, { name: "Ethanol", limit: 5000 }, { name: "Ethyl Acetate", limit: 5000 },
|
|
210
|
+
{ name: "Heptane", limit: 5000 }, { name: "Hexane", limit: 290 }, { name: "Isopropyl Alcohol", limit: 5000 },
|
|
211
|
+
{ name: "Methanol", limit: 3000 }, { name: "Pentane", limit: 5000 }, { name: "Propane", limit: 5000 },
|
|
212
|
+
{ name: "Toluene", limit: 890 }, { name: "Xylene", limit: 2170 },
|
|
213
|
+
];
|
|
214
|
+
const residualSolventsResults = residualSolventsData.map(s => ({
|
|
215
|
+
analyte: s.name, result: "<LOQ", limit: s.limit, loq: s.limit * 0.1, unit: "ppm", status: "Pass",
|
|
216
|
+
}));
|
|
217
|
+
return { microbialResults, heavyMetalsResults, mycotoxinResults, pesticidesCat1: pesticidesCat1Results, pesticidesCat2: pesticidesCat2Results, residualSolventsResults };
|
|
218
|
+
}
|
|
219
|
+
function tokenize(expr) {
|
|
220
|
+
const tokens = [];
|
|
221
|
+
let i = 0;
|
|
222
|
+
while (i < expr.length) {
|
|
223
|
+
const ch = expr[i];
|
|
224
|
+
if (/\s/.test(ch)) {
|
|
225
|
+
i++;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (/[0-9.]/.test(ch)) {
|
|
229
|
+
let num = "";
|
|
230
|
+
while (i < expr.length && /[0-9.]/.test(expr[i])) {
|
|
231
|
+
num += expr[i];
|
|
232
|
+
i++;
|
|
233
|
+
}
|
|
234
|
+
tokens.push({ type: "NUMBER", value: num });
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (/[a-zA-Z_]/.test(ch)) {
|
|
238
|
+
let id = "";
|
|
239
|
+
while (i < expr.length && /[a-zA-Z0-9_]/.test(expr[i])) {
|
|
240
|
+
id += expr[i];
|
|
241
|
+
i++;
|
|
242
|
+
}
|
|
243
|
+
if (id === "SUM" && i < expr.length && expr[i] === "(") {
|
|
244
|
+
tokens.push({ type: "FUNC", value: id });
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
tokens.push({ type: "IDENT", value: id });
|
|
248
|
+
}
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if ("+-*/".includes(ch)) {
|
|
252
|
+
tokens.push({ type: "OP", value: ch });
|
|
253
|
+
i++;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (ch === "(") {
|
|
257
|
+
tokens.push({ type: "LPAREN", value: ch });
|
|
258
|
+
i++;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (ch === ")") {
|
|
262
|
+
tokens.push({ type: "RPAREN", value: ch });
|
|
263
|
+
i++;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (ch === "[") {
|
|
267
|
+
tokens.push({ type: "LBRACKET", value: ch });
|
|
268
|
+
i++;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (ch === "]") {
|
|
272
|
+
tokens.push({ type: "RBRACKET", value: ch });
|
|
273
|
+
i++;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (ch === ".") {
|
|
277
|
+
tokens.push({ type: "DOT", value: ch });
|
|
278
|
+
i++;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (ch === ",") {
|
|
282
|
+
tokens.push({ type: "COMMA", value: ch });
|
|
283
|
+
i++;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
i++; // skip unknown
|
|
287
|
+
}
|
|
288
|
+
tokens.push({ type: "EOF", value: "" });
|
|
289
|
+
return tokens;
|
|
290
|
+
}
|
|
291
|
+
// Recursive descent parser
|
|
292
|
+
class ExprParser {
|
|
293
|
+
tokens;
|
|
294
|
+
ctx;
|
|
295
|
+
pos = 0;
|
|
296
|
+
constructor(tokens, ctx) {
|
|
297
|
+
this.tokens = tokens;
|
|
298
|
+
this.ctx = ctx;
|
|
299
|
+
}
|
|
300
|
+
peek() { return this.tokens[this.pos] || { type: "EOF", value: "" }; }
|
|
301
|
+
advance() { return this.tokens[this.pos++]; }
|
|
302
|
+
parse() {
|
|
303
|
+
const val = this.expr();
|
|
304
|
+
return val;
|
|
305
|
+
}
|
|
306
|
+
expr() {
|
|
307
|
+
let left = this.term();
|
|
308
|
+
while (this.peek().type === "OP" && (this.peek().value === "+" || this.peek().value === "-")) {
|
|
309
|
+
const op = this.advance().value;
|
|
310
|
+
const right = this.term();
|
|
311
|
+
left = op === "+" ? left + right : left - right;
|
|
312
|
+
}
|
|
313
|
+
return left;
|
|
314
|
+
}
|
|
315
|
+
term() {
|
|
316
|
+
let left = this.unary();
|
|
317
|
+
while (this.peek().type === "OP" && (this.peek().value === "*" || this.peek().value === "/")) {
|
|
318
|
+
const op = this.advance().value;
|
|
319
|
+
const right = this.unary();
|
|
320
|
+
left = op === "*" ? left * right : (right !== 0 ? left / right : 0);
|
|
321
|
+
}
|
|
322
|
+
return left;
|
|
323
|
+
}
|
|
324
|
+
unary() {
|
|
325
|
+
if (this.peek().type === "OP" && this.peek().value === "-") {
|
|
326
|
+
this.advance();
|
|
327
|
+
return -this.primary();
|
|
328
|
+
}
|
|
329
|
+
return this.primary();
|
|
330
|
+
}
|
|
331
|
+
primary() {
|
|
332
|
+
const tok = this.peek();
|
|
333
|
+
if (tok.type === "NUMBER") {
|
|
334
|
+
this.advance();
|
|
335
|
+
return parseFloat(tok.value);
|
|
336
|
+
}
|
|
337
|
+
if (tok.type === "LPAREN") {
|
|
338
|
+
this.advance();
|
|
339
|
+
const val = this.expr();
|
|
340
|
+
if (this.peek().type === "RPAREN")
|
|
341
|
+
this.advance();
|
|
342
|
+
return val;
|
|
343
|
+
}
|
|
344
|
+
// SUM(array[].field)
|
|
345
|
+
if (tok.type === "FUNC" && tok.value === "SUM") {
|
|
346
|
+
this.advance(); // SUM
|
|
347
|
+
if (this.peek().type === "LPAREN")
|
|
348
|
+
this.advance(); // (
|
|
349
|
+
// Parse: arrayName[].fieldName
|
|
350
|
+
const arrayName = this.advance().value; // identifier
|
|
351
|
+
if (this.peek().type === "LBRACKET") {
|
|
352
|
+
this.advance(); // [
|
|
353
|
+
if (this.peek().type === "RBRACKET")
|
|
354
|
+
this.advance(); // ]
|
|
355
|
+
}
|
|
356
|
+
if (this.peek().type === "DOT")
|
|
357
|
+
this.advance(); // .
|
|
358
|
+
const fieldName = this.advance().value; // field
|
|
359
|
+
if (this.peek().type === "RPAREN")
|
|
360
|
+
this.advance(); // )
|
|
361
|
+
const arr = this.ctx[arrayName];
|
|
362
|
+
if (!Array.isArray(arr))
|
|
363
|
+
return 0;
|
|
364
|
+
let sum = 0;
|
|
365
|
+
for (const item of arr) {
|
|
366
|
+
const v = item[fieldName];
|
|
367
|
+
if (typeof v === "number")
|
|
368
|
+
sum += v;
|
|
369
|
+
}
|
|
370
|
+
return sum;
|
|
371
|
+
}
|
|
372
|
+
// Identifier — resolve from context
|
|
373
|
+
if (tok.type === "IDENT") {
|
|
374
|
+
this.advance();
|
|
375
|
+
const val = this.ctx[tok.value];
|
|
376
|
+
if (typeof val === "number")
|
|
377
|
+
return val;
|
|
378
|
+
if (typeof val === "string") {
|
|
379
|
+
const n = parseFloat(val);
|
|
380
|
+
return isNaN(n) ? 0 : n;
|
|
381
|
+
}
|
|
382
|
+
return 0;
|
|
383
|
+
}
|
|
384
|
+
// Fallback
|
|
385
|
+
this.advance();
|
|
386
|
+
return 0;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function safeEval(expr, ctx) {
|
|
390
|
+
const tokens = tokenize(expr);
|
|
391
|
+
const parser = new ExprParser(tokens, ctx);
|
|
392
|
+
return parser.parse();
|
|
393
|
+
}
|
|
394
|
+
export function applyCalculations(calculations, constants, data) {
|
|
395
|
+
if (!calculations || !Array.isArray(calculations))
|
|
396
|
+
return data;
|
|
397
|
+
const result = { ...data };
|
|
398
|
+
// Flatten constants into context
|
|
399
|
+
const ctx = { ...result };
|
|
400
|
+
for (const [k, v] of Object.entries(constants)) {
|
|
401
|
+
if (typeof v === "number" || typeof v === "string") {
|
|
402
|
+
ctx[k] = v;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
// Multi-pass (max 3) for inter-formula dependencies
|
|
406
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
407
|
+
let changed = false;
|
|
408
|
+
for (const calc of calculations) {
|
|
409
|
+
try {
|
|
410
|
+
const val = safeEval(calc.formula, ctx);
|
|
411
|
+
const rounded = parseFloat(val.toFixed(calc.decimals ?? 3));
|
|
412
|
+
if (ctx[calc.field] !== rounded) {
|
|
413
|
+
ctx[calc.field] = rounded;
|
|
414
|
+
result[calc.field] = rounded;
|
|
415
|
+
changed = true;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
catch {
|
|
419
|
+
// Skip failed calculations
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
if (!changed)
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
return result;
|
|
426
|
+
}
|
|
427
|
+
function tokenizeBool(expr) {
|
|
428
|
+
const tokens = [];
|
|
429
|
+
let i = 0;
|
|
430
|
+
while (i < expr.length) {
|
|
431
|
+
const ch = expr[i];
|
|
432
|
+
if (/\s/.test(ch)) {
|
|
433
|
+
i++;
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
if (/[0-9.]/.test(ch)) {
|
|
437
|
+
let num = "";
|
|
438
|
+
while (i < expr.length && /[0-9.]/.test(expr[i])) {
|
|
439
|
+
num += expr[i];
|
|
440
|
+
i++;
|
|
441
|
+
}
|
|
442
|
+
tokens.push({ type: "NUMBER", value: num });
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (/[a-zA-Z_]/.test(ch)) {
|
|
446
|
+
let id = "";
|
|
447
|
+
while (i < expr.length && /[a-zA-Z0-9_]/.test(expr[i])) {
|
|
448
|
+
id += expr[i];
|
|
449
|
+
i++;
|
|
450
|
+
}
|
|
451
|
+
if (id === "AND" || id === "OR") {
|
|
452
|
+
tokens.push({ type: "LOGIC", value: id });
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
tokens.push({ type: "IDENT", value: id });
|
|
456
|
+
}
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
// Comparison operators
|
|
460
|
+
if (ch === "<" || ch === ">" || ch === "=" || ch === "!") {
|
|
461
|
+
let op = ch;
|
|
462
|
+
i++;
|
|
463
|
+
if (i < expr.length && expr[i] === "=") {
|
|
464
|
+
op += "=";
|
|
465
|
+
i++;
|
|
466
|
+
}
|
|
467
|
+
else if (ch === "=" && i < expr.length && expr[i] === "=") {
|
|
468
|
+
op += "=";
|
|
469
|
+
i++;
|
|
470
|
+
}
|
|
471
|
+
tokens.push({ type: "CMP", value: op });
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
if ("+-*/".includes(ch)) {
|
|
475
|
+
tokens.push({ type: "OP", value: ch });
|
|
476
|
+
i++;
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
if (ch === "(") {
|
|
480
|
+
tokens.push({ type: "LPAREN", value: ch });
|
|
481
|
+
i++;
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
if (ch === ")") {
|
|
485
|
+
tokens.push({ type: "RPAREN", value: ch });
|
|
486
|
+
i++;
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
i++;
|
|
490
|
+
}
|
|
491
|
+
tokens.push({ type: "EOF", value: "" });
|
|
492
|
+
return tokens;
|
|
493
|
+
}
|
|
494
|
+
function evalBoolExpr(expr, ctx) {
|
|
495
|
+
const tokens = tokenizeBool(expr);
|
|
496
|
+
let pos = 0;
|
|
497
|
+
function peek() { return tokens[pos] || { type: "EOF", value: "" }; }
|
|
498
|
+
function advance() { return tokens[pos++]; }
|
|
499
|
+
function numericValue() {
|
|
500
|
+
const tok = peek();
|
|
501
|
+
if (tok.type === "NUMBER") {
|
|
502
|
+
advance();
|
|
503
|
+
return parseFloat(tok.value);
|
|
504
|
+
}
|
|
505
|
+
if (tok.type === "IDENT") {
|
|
506
|
+
advance();
|
|
507
|
+
const v = ctx[tok.value];
|
|
508
|
+
return typeof v === "number" ? v : (typeof v === "string" ? parseFloat(v) || 0 : 0);
|
|
509
|
+
}
|
|
510
|
+
if (tok.type === "LPAREN") {
|
|
511
|
+
advance();
|
|
512
|
+
const val = numericExpr();
|
|
513
|
+
if (peek().type === "RPAREN")
|
|
514
|
+
advance();
|
|
515
|
+
return val;
|
|
516
|
+
}
|
|
517
|
+
advance();
|
|
518
|
+
return 0;
|
|
519
|
+
}
|
|
520
|
+
function numericExpr() {
|
|
521
|
+
let left = numericTerm();
|
|
522
|
+
while (peek().type === "OP" && (peek().value === "+" || peek().value === "-")) {
|
|
523
|
+
const op = advance().value;
|
|
524
|
+
const right = numericTerm();
|
|
525
|
+
left = op === "+" ? left + right : left - right;
|
|
526
|
+
}
|
|
527
|
+
return left;
|
|
528
|
+
}
|
|
529
|
+
function numericTerm() {
|
|
530
|
+
let left = numericValue();
|
|
531
|
+
while (peek().type === "OP" && (peek().value === "*" || peek().value === "/")) {
|
|
532
|
+
const op = advance().value;
|
|
533
|
+
const right = numericValue();
|
|
534
|
+
left = op === "*" ? left * right : (right !== 0 ? left / right : 0);
|
|
535
|
+
}
|
|
536
|
+
return left;
|
|
537
|
+
}
|
|
538
|
+
function comparison() {
|
|
539
|
+
const left = numericExpr();
|
|
540
|
+
const tok = peek();
|
|
541
|
+
if (tok.type === "CMP") {
|
|
542
|
+
const op = advance().value;
|
|
543
|
+
const right = numericExpr();
|
|
544
|
+
switch (op) {
|
|
545
|
+
case "<": return left < right;
|
|
546
|
+
case "<=": return left <= right;
|
|
547
|
+
case ">": return left > right;
|
|
548
|
+
case ">=": return left >= right;
|
|
549
|
+
case "==": return Math.abs(left - right) < 1e-9;
|
|
550
|
+
case "!=": return Math.abs(left - right) >= 1e-9;
|
|
551
|
+
default: return false;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
// Truthy check if no comparison operator
|
|
555
|
+
return left !== 0;
|
|
556
|
+
}
|
|
557
|
+
function boolExpr() {
|
|
558
|
+
let left = comparison();
|
|
559
|
+
while (peek().type === "LOGIC") {
|
|
560
|
+
const op = advance().value;
|
|
561
|
+
const right = comparison();
|
|
562
|
+
if (op === "AND")
|
|
563
|
+
left = left && right;
|
|
564
|
+
else if (op === "OR")
|
|
565
|
+
left = left || right;
|
|
566
|
+
}
|
|
567
|
+
return left;
|
|
568
|
+
}
|
|
569
|
+
return boolExpr();
|
|
570
|
+
}
|
|
571
|
+
export function runValidation(rules, data) {
|
|
572
|
+
if (!rules || !Array.isArray(rules))
|
|
573
|
+
return [];
|
|
574
|
+
const results = [];
|
|
575
|
+
for (const rule of rules) {
|
|
576
|
+
try {
|
|
577
|
+
const passed = evalBoolExpr(rule.formula, data);
|
|
578
|
+
results.push({ name: rule.name, severity: rule.severity, message: rule.message, passed });
|
|
579
|
+
}
|
|
580
|
+
catch {
|
|
581
|
+
results.push({ name: rule.name, severity: rule.severity, message: rule.message, passed: false });
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return results;
|
|
585
|
+
}
|
|
586
|
+
export function resolveBinding(bind, data) {
|
|
587
|
+
const parts = bind.split(".");
|
|
588
|
+
let val = data;
|
|
589
|
+
for (const p of parts) {
|
|
590
|
+
if (val === null || val === undefined)
|
|
591
|
+
return undefined;
|
|
592
|
+
val = val[p];
|
|
593
|
+
}
|
|
594
|
+
return val;
|
|
595
|
+
}
|
|
596
|
+
export function resolveText(text, data) {
|
|
597
|
+
if (!text)
|
|
598
|
+
return "";
|
|
599
|
+
return fillTemplate(text, data);
|
|
600
|
+
}
|
|
601
|
+
// Minimal QR code generator — produces SVG rect elements for a text payload.
|
|
602
|
+
// Uses a simple bit matrix approach sufficient for short strings (URLs, IDs).
|
|
603
|
+
export function generateQRSvg(text, size = 100) {
|
|
604
|
+
// Simple QR-like encoding: create a deterministic pattern from text hash
|
|
605
|
+
// This generates a visually QR-like pattern (not scannable — for display purposes).
|
|
606
|
+
// For real QR, the template data usually includes a pre-generated QR URL.
|
|
607
|
+
const moduleCount = 21; // QR Version 1
|
|
608
|
+
const cellSize = size / moduleCount;
|
|
609
|
+
const modules = Array.from({ length: moduleCount }, () => Array(moduleCount).fill(false));
|
|
610
|
+
// Finder patterns (top-left, top-right, bottom-left)
|
|
611
|
+
const drawFinder = (row, col) => {
|
|
612
|
+
for (let r = 0; r < 7; r++) {
|
|
613
|
+
for (let c = 0; c < 7; c++) {
|
|
614
|
+
const isEdge = r === 0 || r === 6 || c === 0 || c === 6;
|
|
615
|
+
const isInner = r >= 2 && r <= 4 && c >= 2 && c <= 4;
|
|
616
|
+
if (isEdge || isInner)
|
|
617
|
+
modules[row + r][col + c] = true;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
drawFinder(0, 0);
|
|
622
|
+
drawFinder(0, moduleCount - 7);
|
|
623
|
+
drawFinder(moduleCount - 7, 0);
|
|
624
|
+
// Data area: hash the text and fill deterministically
|
|
625
|
+
let hash = 0;
|
|
626
|
+
for (let i = 0; i < text.length; i++) {
|
|
627
|
+
hash = ((hash << 5) - hash + text.charCodeAt(i)) | 0;
|
|
628
|
+
}
|
|
629
|
+
for (let r = 8; r < moduleCount; r++) {
|
|
630
|
+
for (let c = 8; c < moduleCount; c++) {
|
|
631
|
+
hash = ((hash << 5) - hash + r * 31 + c * 17) | 0;
|
|
632
|
+
modules[r][c] = (hash & 1) === 1;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
let rects = "";
|
|
636
|
+
for (let r = 0; r < moduleCount; r++) {
|
|
637
|
+
for (let c = 0; c < moduleCount; c++) {
|
|
638
|
+
if (modules[r][c]) {
|
|
639
|
+
rects += `<rect x="${c * cellSize}" y="${r * cellSize}" width="${cellSize}" height="${cellSize}" fill="#000"/>`;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}"><rect width="${size}" height="${size}" fill="#fff"/>${rects}</svg>`;
|
|
644
|
+
}
|
|
645
|
+
function renderElement(el, data) {
|
|
646
|
+
switch (el.type) {
|
|
647
|
+
case "text": {
|
|
648
|
+
const text = resolveText(el.content || el.text || "", data);
|
|
649
|
+
const styles = [];
|
|
650
|
+
if (el.fontSize)
|
|
651
|
+
styles.push(`font-size:${el.fontSize}px`);
|
|
652
|
+
if (el.color)
|
|
653
|
+
styles.push(`color:${el.color}`);
|
|
654
|
+
if (el.bold)
|
|
655
|
+
styles.push("font-weight:bold");
|
|
656
|
+
if (el.align)
|
|
657
|
+
styles.push(`text-align:${el.align}`);
|
|
658
|
+
return `<div style="${styles.join(";")}">${text}</div>`;
|
|
659
|
+
}
|
|
660
|
+
case "header": {
|
|
661
|
+
const text = resolveText(el.content || el.text || "", data);
|
|
662
|
+
const styles = ["font-weight:bold"];
|
|
663
|
+
if (el.backgroundColor)
|
|
664
|
+
styles.push(`background-color:${el.backgroundColor}`);
|
|
665
|
+
if (el.padding)
|
|
666
|
+
styles.push(`padding:${typeof el.padding === "number" ? el.padding + "px" : el.padding}`);
|
|
667
|
+
if (el.fontSize)
|
|
668
|
+
styles.push(`font-size:${el.fontSize}px`);
|
|
669
|
+
if (el.color)
|
|
670
|
+
styles.push(`color:${el.color}`);
|
|
671
|
+
if (el.align)
|
|
672
|
+
styles.push(`text-align:${el.align}`);
|
|
673
|
+
return `<div style="${styles.join(";")}">${text}</div>`;
|
|
674
|
+
}
|
|
675
|
+
case "image": {
|
|
676
|
+
let src = el.src || el.url || "";
|
|
677
|
+
if (el.bind) {
|
|
678
|
+
const resolved = resolveBinding(el.bind, data);
|
|
679
|
+
if (typeof resolved === "string")
|
|
680
|
+
src = resolved;
|
|
681
|
+
}
|
|
682
|
+
const w = el.width ? `width:${typeof el.width === "number" ? el.width + "px" : el.width}` : "";
|
|
683
|
+
const h = el.height ? `height:${typeof el.height === "number" ? el.height + "px" : el.height}` : "";
|
|
684
|
+
return `<img src="${src}" style="${[w, h].filter(Boolean).join(";")}" />`;
|
|
685
|
+
}
|
|
686
|
+
case "qrcode": {
|
|
687
|
+
let qrData = el.data || el.content || "";
|
|
688
|
+
if (el.bind) {
|
|
689
|
+
const resolved = resolveBinding(el.bind, data);
|
|
690
|
+
if (typeof resolved === "string")
|
|
691
|
+
qrData = resolved;
|
|
692
|
+
}
|
|
693
|
+
qrData = resolveText(qrData, data);
|
|
694
|
+
const qrSize = el.size || (typeof el.width === "number" ? el.width : 100);
|
|
695
|
+
const svg = generateQRSvg(qrData, qrSize);
|
|
696
|
+
const dataUri = `data:image/svg+xml;base64,${Buffer.from(svg).toString("base64")}`;
|
|
697
|
+
return `<img src="${dataUri}" width="${qrSize}" height="${qrSize}" />`;
|
|
698
|
+
}
|
|
699
|
+
case "spacer": {
|
|
700
|
+
const h = el.height || 10;
|
|
701
|
+
return `<div style="height:${typeof h === "number" ? h + "px" : h}"></div>`;
|
|
702
|
+
}
|
|
703
|
+
case "line": {
|
|
704
|
+
const color = el.color || "#000";
|
|
705
|
+
const thickness = el.thickness || 1;
|
|
706
|
+
return `<hr style="border:none;border-top:${thickness}px solid ${color};margin:8px 0;" />`;
|
|
707
|
+
}
|
|
708
|
+
case "columns": {
|
|
709
|
+
const cols = el.columns || el.children || [];
|
|
710
|
+
const inner = cols.map((child) => {
|
|
711
|
+
// Support both: array of elements per column, or single element per column
|
|
712
|
+
if (Array.isArray(child)) {
|
|
713
|
+
return `<div style="flex:1">${child.map((c) => renderElement(c, data)).join("")}</div>`;
|
|
714
|
+
}
|
|
715
|
+
return `<div style="flex:1">${renderElement(child, data)}</div>`;
|
|
716
|
+
}).join("");
|
|
717
|
+
return `<div style="display:flex;gap:8px">${inner}</div>`;
|
|
718
|
+
}
|
|
719
|
+
case "page_break": {
|
|
720
|
+
return `<div style="page-break-after:always"></div>`;
|
|
721
|
+
}
|
|
722
|
+
case "grid": {
|
|
723
|
+
const items = (Array.isArray(el.items) ? el.items : Array.isArray(el.content) ? el.content : []);
|
|
724
|
+
const gridRows = items.map((item) => {
|
|
725
|
+
const label = item.label || item.value || "";
|
|
726
|
+
let value = "";
|
|
727
|
+
if (item.bind) {
|
|
728
|
+
const resolved = resolveBinding(item.bind, data);
|
|
729
|
+
value = resolved !== undefined && resolved !== null ? String(resolved) : "";
|
|
730
|
+
}
|
|
731
|
+
value = resolveText(value || "", data);
|
|
732
|
+
return `<tr><td style="padding:4px 8px;font-weight:bold;white-space:nowrap;width:40%">${label}</td><td style="padding:4px 8px">${value}</td></tr>`;
|
|
733
|
+
}).join("");
|
|
734
|
+
return `<table style="width:100%;border-collapse:collapse">${gridRows}</table>`;
|
|
735
|
+
}
|
|
736
|
+
case "table": {
|
|
737
|
+
const headers = el.headers || [];
|
|
738
|
+
const rowFields = el.row_fields;
|
|
739
|
+
const thead = headers.length
|
|
740
|
+
? `<thead><tr>${headers.map(h => `<th style="padding:6px 8px;border:1px solid #ddd;background:#f5f5f5;text-align:left;font-size:8px">${h}</th>`).join("")}</tr></thead>`
|
|
741
|
+
: "";
|
|
742
|
+
let bodyRows = el.rows || [];
|
|
743
|
+
if (el.bind) {
|
|
744
|
+
const resolved = resolveBinding(el.bind, data);
|
|
745
|
+
if (Array.isArray(resolved))
|
|
746
|
+
bodyRows = resolved;
|
|
747
|
+
}
|
|
748
|
+
const tbody = bodyRows.map((row) => {
|
|
749
|
+
if (Array.isArray(row)) {
|
|
750
|
+
return `<tr>${row.map(cell => `<td style="padding:5px 8px;border:1px solid #ddd;font-size:8px">${cell ?? ""}</td>`).join("")}</tr>`;
|
|
751
|
+
}
|
|
752
|
+
// Object row — use row_fields if provided, else fall back to header matching
|
|
753
|
+
if (rowFields && rowFields.length) {
|
|
754
|
+
return `<tr>${rowFields.map(field => {
|
|
755
|
+
const val = row[field] ?? "";
|
|
756
|
+
return `<td style="padding:5px 8px;border:1px solid #ddd;font-size:8px">${val}</td>`;
|
|
757
|
+
}).join("")}</tr>`;
|
|
758
|
+
}
|
|
759
|
+
return `<tr>${headers.map(h => {
|
|
760
|
+
const key = h.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
761
|
+
const val = row[h] ?? row[key] ?? row[h.replace(/\s+/g, "")] ?? "";
|
|
762
|
+
return `<td style="padding:5px 8px;border:1px solid #ddd;font-size:8px">${val}</td>`;
|
|
763
|
+
}).join("")}</tr>`;
|
|
764
|
+
}).join("");
|
|
765
|
+
return `<table style="width:100%;border-collapse:collapse">${thead}<tbody>${tbody}</tbody></table>`;
|
|
766
|
+
}
|
|
767
|
+
case "box": {
|
|
768
|
+
const styles = ["text-align:center"];
|
|
769
|
+
if (el.border)
|
|
770
|
+
styles.push(`border:${el.border}`);
|
|
771
|
+
if (el.background || el.backgroundColor)
|
|
772
|
+
styles.push(`background:${el.background || el.backgroundColor}`);
|
|
773
|
+
if (el.padding)
|
|
774
|
+
styles.push(`padding:${typeof el.padding === "number" ? el.padding + "px" : el.padding}`);
|
|
775
|
+
const inner = (el.children || []).map(child => renderElement(child, data)).join("");
|
|
776
|
+
const text = el.content || el.text ? resolveText(el.content || el.text || "", data) : "";
|
|
777
|
+
return `<div style="${styles.join(";")}">${text}${inner}</div>`;
|
|
778
|
+
}
|
|
779
|
+
default: {
|
|
780
|
+
// Unknown element — try to render children
|
|
781
|
+
if (el.children && Array.isArray(el.children)) {
|
|
782
|
+
return el.children.map(child => renderElement(child, data)).join("");
|
|
783
|
+
}
|
|
784
|
+
return "";
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
export function renderLayoutToHtml(layout, pageConfig, styles, data) {
|
|
789
|
+
const pageSize = pageConfig?.size || "A4";
|
|
790
|
+
const margins = pageConfig?.margins || { top: "15mm", right: "15mm", bottom: "15mm", left: "15mm" };
|
|
791
|
+
const fontFamily = styles?.fontFamily || "Helvetica, Arial, sans-serif";
|
|
792
|
+
const fontSize = styles?.fontSize || 12;
|
|
793
|
+
const color = styles?.color || "#000";
|
|
794
|
+
// Normalize layout to LayoutElement[]:
|
|
795
|
+
// - Array → use directly
|
|
796
|
+
// - Object with .pages → flatten page sections (header/body/footer per page)
|
|
797
|
+
// - Object with .sections/.children → unwrap
|
|
798
|
+
// - Object with .header/.footer → wrap as elements
|
|
799
|
+
let elements = [];
|
|
800
|
+
if (Array.isArray(layout)) {
|
|
801
|
+
elements = layout;
|
|
802
|
+
}
|
|
803
|
+
else if (layout && typeof layout === "object") {
|
|
804
|
+
if (Array.isArray(layout.pages)) {
|
|
805
|
+
for (const page of layout.pages) {
|
|
806
|
+
if (page.header)
|
|
807
|
+
elements.push(page.header);
|
|
808
|
+
if (Array.isArray(page.body))
|
|
809
|
+
elements.push(...page.body);
|
|
810
|
+
else if (page.body)
|
|
811
|
+
elements.push(page.body);
|
|
812
|
+
if (Array.isArray(page.sections))
|
|
813
|
+
elements.push(...page.sections);
|
|
814
|
+
if (Array.isArray(page.elements))
|
|
815
|
+
elements.push(...page.elements);
|
|
816
|
+
if (page.footer)
|
|
817
|
+
elements.push(page.footer);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
else if (Array.isArray(layout.sections)) {
|
|
821
|
+
elements = layout.sections;
|
|
822
|
+
}
|
|
823
|
+
else if (Array.isArray(layout.children)) {
|
|
824
|
+
elements = layout.children;
|
|
825
|
+
}
|
|
826
|
+
else {
|
|
827
|
+
// Object with header/footer keys — wrap as single-element layout
|
|
828
|
+
if (layout.header)
|
|
829
|
+
elements.push(layout.header);
|
|
830
|
+
if (layout.body) {
|
|
831
|
+
const body = layout.body;
|
|
832
|
+
if (Array.isArray(body))
|
|
833
|
+
elements.push(...body);
|
|
834
|
+
else
|
|
835
|
+
elements.push(body);
|
|
836
|
+
}
|
|
837
|
+
if (layout.footer)
|
|
838
|
+
elements.push(layout.footer);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
const bodyHtml = elements.map(el => renderElement(el, data)).join("\n");
|
|
842
|
+
return `<!DOCTYPE html>
|
|
843
|
+
<html>
|
|
844
|
+
<head>
|
|
845
|
+
<meta charset="utf-8">
|
|
846
|
+
<style>
|
|
847
|
+
@page {
|
|
848
|
+
size: ${pageSize};
|
|
849
|
+
margin: ${margins.top || "15mm"} ${margins.right || "15mm"} ${margins.bottom || "15mm"} ${margins.left || "15mm"};
|
|
850
|
+
}
|
|
851
|
+
body {
|
|
852
|
+
font-family: ${fontFamily};
|
|
853
|
+
font-size: ${fontSize}px;
|
|
854
|
+
color: ${color};
|
|
855
|
+
margin: 0;
|
|
856
|
+
padding: 0;
|
|
857
|
+
line-height: 1.4;
|
|
858
|
+
}
|
|
859
|
+
table { page-break-inside: avoid; }
|
|
860
|
+
img { max-width: 100%; }
|
|
861
|
+
</style>
|
|
862
|
+
</head>
|
|
863
|
+
<body>
|
|
864
|
+
${bodyHtml}
|
|
865
|
+
</body>
|
|
866
|
+
</html>`;
|
|
867
|
+
}
|