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
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { sanitizeFilterValue, escapeCSV, fillTemplate, groupBy } from "../lib/utils.js";
|
|
2
|
-
import { generatePdfFromHtml } from "./browser.js";
|
|
3
2
|
import { validateUrl } from "../lib/ssrf-guard.js";
|
|
3
|
+
import { applyGenerationRules, generateCannabinoidData, applyCalculations, runValidation, generateFullPanelData, } from "../lib/pdf-renderer.js";
|
|
4
|
+
import { renderLayoutToPdf, renderHtmlToPdf, renderLabelToPdf } from "../lib/react-pdf-layout.js";
|
|
5
|
+
import { renderCOAToPdf } from "../lib/coa-renderer.js";
|
|
6
|
+
import QRCode from "qrcode";
|
|
4
7
|
const MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024; // 10MB per attachment
|
|
5
8
|
const MAX_TOTAL_ATTACHMENT_BYTES = 25 * 1024 * 1024; // 25MB total
|
|
6
9
|
const MAX_ATTACHMENT_COUNT = 10;
|
|
@@ -301,12 +304,12 @@ export async function handleDocuments(sb, args, storeId) {
|
|
|
301
304
|
const storagePath = `${sid}/${fileName}`;
|
|
302
305
|
let uploadBuffer;
|
|
303
306
|
if (docType === "pdf") {
|
|
304
|
-
// PDF generation from HTML content via
|
|
307
|
+
// PDF generation from HTML content via React-PDF
|
|
305
308
|
const htmlContent = args.html || args.content;
|
|
306
309
|
if (!htmlContent)
|
|
307
310
|
return { success: false, error: "html or content (HTML string) is required for PDF documents" };
|
|
308
311
|
try {
|
|
309
|
-
const pdfBuffer = await
|
|
312
|
+
const pdfBuffer = await renderHtmlToPdf(htmlContent, {
|
|
310
313
|
format: args.format || "A4",
|
|
311
314
|
landscape: args.landscape || false,
|
|
312
315
|
});
|
|
@@ -530,10 +533,10 @@ export async function handleDocuments(sb, args, storeId) {
|
|
|
530
533
|
return { success: true, data: { count: data?.length || 0, stores: data } };
|
|
531
534
|
}
|
|
532
535
|
case "list_profiles": {
|
|
533
|
-
const { data, error } = await sb.from("
|
|
534
|
-
.select("id, name,
|
|
536
|
+
const { data, error } = await sb.from("document_profiles")
|
|
537
|
+
.select("id, name, category, sample_type, config, template_id, is_active, created_at")
|
|
535
538
|
.eq("is_active", true)
|
|
536
|
-
.
|
|
539
|
+
.eq("owner_store_id", sid)
|
|
537
540
|
.order("name");
|
|
538
541
|
if (error)
|
|
539
542
|
return { success: false, error: error.message };
|
|
@@ -541,10 +544,10 @@ export async function handleDocuments(sb, args, storeId) {
|
|
|
541
544
|
success: true,
|
|
542
545
|
data: {
|
|
543
546
|
count: data?.length || 0,
|
|
544
|
-
profiles: (data || []).map(
|
|
545
|
-
id:
|
|
546
|
-
|
|
547
|
-
|
|
547
|
+
profiles: (data || []).map(p => ({
|
|
548
|
+
id: p.id, name: p.name, category: p.category,
|
|
549
|
+
sample_type: p.sample_type, template_id: p.template_id,
|
|
550
|
+
has_config: Object.keys(p.config || {}).length > 0,
|
|
548
551
|
})),
|
|
549
552
|
},
|
|
550
553
|
};
|
|
@@ -582,7 +585,437 @@ export async function handleDocuments(sb, args, storeId) {
|
|
|
582
585
|
const succeeded = results.filter(r => !r.error).length;
|
|
583
586
|
return { success: true, data: { total: items.length, succeeded, failed: items.length - succeeded, results } };
|
|
584
587
|
}
|
|
588
|
+
// ================================================================
|
|
589
|
+
// PDF TEMPLATE SYSTEM — full layout rendering, calculations, profiles
|
|
590
|
+
// ================================================================
|
|
591
|
+
case "generate_pdf": {
|
|
592
|
+
const templateId = args.template_id;
|
|
593
|
+
const templateSlug = args.template_slug;
|
|
594
|
+
if (!templateId && !templateSlug)
|
|
595
|
+
return { success: false, error: "template_id or template_slug required" };
|
|
596
|
+
// 1. Load pdf_template
|
|
597
|
+
let tplQuery = sb.from("pdf_templates").select("*").eq("is_active", true);
|
|
598
|
+
if (templateId)
|
|
599
|
+
tplQuery = tplQuery.eq("id", templateId);
|
|
600
|
+
else
|
|
601
|
+
tplQuery = tplQuery.eq("slug", templateSlug);
|
|
602
|
+
tplQuery = tplQuery.or(`store_id.eq.${sid},store_id.is.null`);
|
|
603
|
+
const { data: tpl, error: tplErr } = await tplQuery.limit(1).single();
|
|
604
|
+
if (tplErr || !tpl)
|
|
605
|
+
return { success: false, error: `Template not found: ${templateId || templateSlug}` };
|
|
606
|
+
// 2. Optionally load document_profile + its client store
|
|
607
|
+
let profileConfig = {};
|
|
608
|
+
let profileConstants = {};
|
|
609
|
+
let profileClientData = {};
|
|
610
|
+
let resolvedClientStoreId = null;
|
|
611
|
+
if (args.profile_id) {
|
|
612
|
+
const { data: profile } = await sb.from("document_profiles")
|
|
613
|
+
.select("*").eq("id", args.profile_id).eq("is_active", true).single();
|
|
614
|
+
if (profile) {
|
|
615
|
+
const cfg = (profile.config || {});
|
|
616
|
+
profileConfig = cfg;
|
|
617
|
+
profileConstants = (cfg.constants || {});
|
|
618
|
+
resolvedClientStoreId = profile.client_store_id || null;
|
|
619
|
+
// Load client info from the profile's linked store
|
|
620
|
+
if (profile.client_store_id) {
|
|
621
|
+
const { data: clientStore } = await sb.from("stores")
|
|
622
|
+
.select("store_name, legal_name, address, city, state, zip, distributor_license_number, phone, email")
|
|
623
|
+
.eq("id", profile.client_store_id).single();
|
|
624
|
+
if (clientStore) {
|
|
625
|
+
profileClientData = {
|
|
626
|
+
clientName: clientStore.legal_name || clientStore.store_name,
|
|
627
|
+
clientAddress: [clientStore.address, clientStore.city, clientStore.state, clientStore.zip].filter(Boolean).join(", ") || null,
|
|
628
|
+
licenseNumber: clientStore.distributor_license_number || null,
|
|
629
|
+
clientPhone: clientStore.phone || null,
|
|
630
|
+
clientEmail: clientStore.email || null,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
// Use profile name as sampleName if not already set
|
|
635
|
+
if (profile.name && !profileConstants.sampleName) {
|
|
636
|
+
profileClientData.sampleName = profile.name;
|
|
637
|
+
}
|
|
638
|
+
if (profile.sample_type) {
|
|
639
|
+
profileClientData.sampleType = profile.sample_type;
|
|
640
|
+
}
|
|
641
|
+
if (profile.default_size) {
|
|
642
|
+
profileClientData.sampleSize = profile.default_size;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
// 3. Optionally load customer (via store_customer_profiles + platform_users) — overrides profile client
|
|
647
|
+
let clientData = {};
|
|
648
|
+
if (args.customer_id) {
|
|
649
|
+
const { data: rel } = await sb.from("user_creation_relationships")
|
|
650
|
+
.select("id, store_id, platform_users!inner(first_name, last_name, email, phone)")
|
|
651
|
+
.eq("id", args.customer_id).single();
|
|
652
|
+
if (rel) {
|
|
653
|
+
const pu = rel.platform_users;
|
|
654
|
+
const { data: prof } = await sb.from("store_customer_profiles")
|
|
655
|
+
.select("street_address, city, state, postal_code, drivers_license_number, medical_card_number")
|
|
656
|
+
.eq("relationship_id", rel.id).maybeSingle();
|
|
657
|
+
clientData = {
|
|
658
|
+
clientName: [pu.first_name, pu.last_name].filter(Boolean).join(" "),
|
|
659
|
+
clientEmail: pu.email,
|
|
660
|
+
clientPhone: pu.phone,
|
|
661
|
+
clientAddress: prof ? [prof.street_address, prof.city, prof.state, prof.postal_code].filter(Boolean).join(", ") : null,
|
|
662
|
+
clientLicense: prof?.drivers_license_number || null,
|
|
663
|
+
clientMedicalCard: prof?.medical_card_number || null,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
// 4. Merge data: constants ← profile config ← profile client ← explicit client ← user data
|
|
668
|
+
const tplConstants = (tpl.constants || {});
|
|
669
|
+
const mergedConstants = { ...tplConstants, ...profileConstants };
|
|
670
|
+
const cannabinoidCfg = (profileConfig.cannabinoids ?? undefined);
|
|
671
|
+
const { cannabinoids: _cfgRanges, ...profileDataOnly } = profileConfig;
|
|
672
|
+
let mergedData = {
|
|
673
|
+
...mergedConstants,
|
|
674
|
+
...profileDataOnly,
|
|
675
|
+
...profileClientData,
|
|
676
|
+
...clientData,
|
|
677
|
+
...(args.data || {}),
|
|
678
|
+
date: new Date().toISOString().slice(0, 10),
|
|
679
|
+
approvalDate: new Date().toISOString().slice(0, 10),
|
|
680
|
+
};
|
|
681
|
+
// 5. Apply generation rules — auto-fill sampleId, dates, etc.
|
|
682
|
+
mergedData = applyGenerationRules(tpl.generation_rules, mergedData);
|
|
683
|
+
// 6. Generate cannabinoid data if profile has cannabinoid config
|
|
684
|
+
if (cannabinoidCfg && !mergedData.cannabinoids) {
|
|
685
|
+
mergedData.cannabinoids = generateCannabinoidData(cannabinoidCfg, mergedConstants);
|
|
686
|
+
}
|
|
687
|
+
// 7. Apply calculations
|
|
688
|
+
mergedData = applyCalculations(tpl.calculations, mergedConstants, mergedData);
|
|
689
|
+
// Also run per-row calculations if cannabinoids exist
|
|
690
|
+
if (Array.isArray(mergedData.cannabinoids)) {
|
|
691
|
+
const rows = mergedData.cannabinoids;
|
|
692
|
+
for (const row of rows) {
|
|
693
|
+
const rowCtx = { ...mergedConstants, ...row };
|
|
694
|
+
const rowCalcs = (tpl.row_calculations || tpl.calculations);
|
|
695
|
+
if (rowCalcs) {
|
|
696
|
+
const computed = applyCalculations(rowCalcs, mergedConstants, rowCtx);
|
|
697
|
+
Object.assign(row, computed);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
// 7b. Generate full panel data if requested (safety tests, pesticides, solvents)
|
|
702
|
+
if (mergedData.fullPanel && !mergedData.microbialResults) {
|
|
703
|
+
const tplConstants = (tpl.constants || {});
|
|
704
|
+
const fullPanel = generateFullPanelData(tplConstants);
|
|
705
|
+
mergedData.microbialResults = fullPanel.microbialResults;
|
|
706
|
+
mergedData.heavyMetalsResults = fullPanel.heavyMetalsResults;
|
|
707
|
+
mergedData.mycotoxinResults = fullPanel.mycotoxinResults;
|
|
708
|
+
mergedData.pesticidesCat1 = fullPanel.pesticidesCat1;
|
|
709
|
+
mergedData.pesticidesCat2 = fullPanel.pesticidesCat2;
|
|
710
|
+
mergedData.testsResidualSolvents = fullPanel.residualSolventsResults;
|
|
711
|
+
mergedData.totalPages = 5;
|
|
712
|
+
mergedData.complianceResults = mergedData.complianceResults || {
|
|
713
|
+
heavyMetals: { status: "PASS" }, microbial: { status: "PASS" },
|
|
714
|
+
pesticides: { status: "PASS" }, mycotoxins: { status: "PASS" },
|
|
715
|
+
residualSolvents: { status: "PASS" },
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
// 7c. Generate QR code for COA templates — links to Quantix COA landing page
|
|
719
|
+
if (tpl.slug?.startsWith("cannabis-coa") && !mergedData.qrCodeDataUrl) {
|
|
720
|
+
const productSlug = (mergedData.sampleName || "certificate")
|
|
721
|
+
.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
722
|
+
const coaUrl = `https://quantixanalytics.com/coa/${sid}/${productSlug}`;
|
|
723
|
+
try {
|
|
724
|
+
mergedData.qrCodeDataUrl = await QRCode.toDataURL(coaUrl, { width: 120, margin: 1 });
|
|
725
|
+
}
|
|
726
|
+
catch { /* skip QR if generation fails */ }
|
|
727
|
+
}
|
|
728
|
+
// 8. Validate
|
|
729
|
+
const validationResults = runValidation(tpl.validation_rules, mergedData);
|
|
730
|
+
const errors = validationResults.filter(v => v.severity === "error" && !v.passed);
|
|
731
|
+
const warnings = validationResults.filter(v => v.severity === "warning" && !v.passed);
|
|
732
|
+
if (errors.length > 0 && !args.force) {
|
|
733
|
+
return {
|
|
734
|
+
success: false,
|
|
735
|
+
error: `Validation failed: ${errors.map(e => e.message).join("; ")}`,
|
|
736
|
+
data: { errors, warnings },
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
// 9-10. Render PDF — all templates use React-PDF
|
|
740
|
+
let pdfBuffer;
|
|
741
|
+
try {
|
|
742
|
+
if (tpl.slug?.startsWith("cannabis-coa")) {
|
|
743
|
+
// COA templates use dedicated renderer
|
|
744
|
+
pdfBuffer = await renderCOAToPdf(mergedData);
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
747
|
+
// Detect label layout vs generic layout
|
|
748
|
+
const layout = tpl.layout;
|
|
749
|
+
const isLabel = layout?.version === 1 && Array.isArray(layout?.elements) && layout.elements[0]?.field;
|
|
750
|
+
if (isLabel) {
|
|
751
|
+
const items = Array.isArray(mergedData.items)
|
|
752
|
+
? mergedData.items
|
|
753
|
+
: [mergedData];
|
|
754
|
+
pdfBuffer = await renderLabelToPdf(layout, tpl.page_config, items);
|
|
755
|
+
}
|
|
756
|
+
else {
|
|
757
|
+
pdfBuffer = await renderLayoutToPdf(tpl.layout, tpl.page_config, tpl.styles, mergedData);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
catch (err) {
|
|
762
|
+
return { success: false, error: `PDF generation failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
763
|
+
}
|
|
764
|
+
// 11. Upload to Supabase storage
|
|
765
|
+
const sampleName = mergedData.sampleName || mergedData.productName || "";
|
|
766
|
+
const docName = args.name || (sampleName ? `${sampleName} COA` : fillTemplate(tpl.name || "Document", mergedData));
|
|
767
|
+
const safeName = docName.replace(/[^a-zA-Z0-9_\-]/g, "_");
|
|
768
|
+
const fileName = `${safeName}_${Date.now()}.pdf`;
|
|
769
|
+
const storagePath = `${sid}/${fileName}`;
|
|
770
|
+
const bucketName = "store-documents";
|
|
771
|
+
const { error: uploadErr } = await sb.storage
|
|
772
|
+
.from(bucketName)
|
|
773
|
+
.upload(storagePath, new Uint8Array(pdfBuffer), { contentType: "application/pdf", upsert: true });
|
|
774
|
+
if (uploadErr)
|
|
775
|
+
return { success: false, error: `Upload failed: ${uploadErr.message}` };
|
|
776
|
+
const { data: urlData } = sb.storage.from(bucketName).getPublicUrl(storagePath);
|
|
777
|
+
// 12. Build COA metadata for the Quantix landing page
|
|
778
|
+
const coaMetadata = {
|
|
779
|
+
size_bytes: pdfBuffer.length,
|
|
780
|
+
from_pdf_template: true,
|
|
781
|
+
template_version: tpl.version,
|
|
782
|
+
};
|
|
783
|
+
if (tpl.slug?.startsWith("cannabis-coa")) {
|
|
784
|
+
coaMetadata.sample_name = sampleName;
|
|
785
|
+
coaMetadata.sample_id = mergedData.sampleId || null;
|
|
786
|
+
coaMetadata.sample_type = mergedData.sampleType || mergedData.strain || null;
|
|
787
|
+
coaMetadata.sample_size = mergedData.sampleSize || null;
|
|
788
|
+
coaMetadata.batch_number = mergedData.batchId || null;
|
|
789
|
+
coaMetadata.lab_name = mergedData.labName || null;
|
|
790
|
+
coaMetadata.lab_contact = mergedData.labContact || null;
|
|
791
|
+
coaMetadata.lab_website = mergedData.labWebsite || null;
|
|
792
|
+
coaMetadata.lab_director = mergedData.labDirector || null;
|
|
793
|
+
coaMetadata.director_title = mergedData.directorTitle || null;
|
|
794
|
+
coaMetadata.logo_url = mergedData.logoUrl || null;
|
|
795
|
+
coaMetadata.signature_url = mergedData.signatureUrl || null;
|
|
796
|
+
coaMetadata.client_name = mergedData.clientName || null;
|
|
797
|
+
coaMetadata.client_address = mergedData.clientAddress || null;
|
|
798
|
+
coaMetadata.date_collected = mergedData.dateCollected || null;
|
|
799
|
+
coaMetadata.date_received = mergedData.dateReceived || null;
|
|
800
|
+
coaMetadata.date_tested = mergedData.dateTested || null;
|
|
801
|
+
coaMetadata.date_reported = mergedData.dateReported || null;
|
|
802
|
+
coaMetadata.status = "Pass";
|
|
803
|
+
coaMetadata.thc_total = mergedData.totalTHC || null;
|
|
804
|
+
coaMetadata.cbd_total = mergedData.totalCBD || null;
|
|
805
|
+
coaMetadata.cannabinoids_total = mergedData.totalCannabinoids || null;
|
|
806
|
+
coaMetadata.moisture = mergedData.moisture || null;
|
|
807
|
+
// Detailed cannabinoid data for the interactive landing page
|
|
808
|
+
const cannabinoids = mergedData.cannabinoids;
|
|
809
|
+
if (Array.isArray(cannabinoids)) {
|
|
810
|
+
coaMetadata.cannabinoids = Object.fromEntries(cannabinoids.filter((c) => typeof c.percentWeight === "number" && c.percentWeight > 0)
|
|
811
|
+
.map((c) => [c.name, c.percentWeight]));
|
|
812
|
+
coaMetadata.cannabinoids_detailed = cannabinoids.map((c) => ({
|
|
813
|
+
name: c.name, percent: c.percentWeight, mg_per_g: c.mgPerG, lod: c.lod, loq: c.loq, result: String(c.result ?? c.percentWeight),
|
|
814
|
+
}));
|
|
815
|
+
}
|
|
816
|
+
// Safety test panels
|
|
817
|
+
if (mergedData.fullPanel) {
|
|
818
|
+
coaMetadata.test_panels = {
|
|
819
|
+
cannabinoids: true, microbial: true, heavy_metals: true,
|
|
820
|
+
mycotoxins: true, pesticides: true, residual_solvents: true,
|
|
821
|
+
};
|
|
822
|
+
coaMetadata.safety_tests = {
|
|
823
|
+
microbial: "Pass", heavy_metals: "Pass", mycotoxins: "Pass",
|
|
824
|
+
pesticides: "Pass", residual_solvents: "Pass",
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
// 13. Insert store_documents record
|
|
829
|
+
const { data: record, error: insertErr } = await sb.from("store_documents").insert({
|
|
830
|
+
store_id: sid,
|
|
831
|
+
document_type: tpl.document_type || "pdf",
|
|
832
|
+
file_name: fileName,
|
|
833
|
+
file_url: urlData.publicUrl,
|
|
834
|
+
file_size: pdfBuffer.length,
|
|
835
|
+
file_type: "application/pdf",
|
|
836
|
+
document_name: docName,
|
|
837
|
+
source_name: "PDF Template Engine",
|
|
838
|
+
document_date: new Date().toISOString().split("T")[0],
|
|
839
|
+
customer_id: args.customer_id || null,
|
|
840
|
+
client_store_id: resolvedClientStoreId,
|
|
841
|
+
data: { template_id: tpl.id, template_slug: tpl.slug, profile_id: args.profile_id || null },
|
|
842
|
+
metadata: coaMetadata,
|
|
843
|
+
}).select("id, document_name, file_url, created_at").single();
|
|
844
|
+
if (insertErr)
|
|
845
|
+
return { success: false, error: insertErr.message };
|
|
846
|
+
return {
|
|
847
|
+
success: true,
|
|
848
|
+
data: {
|
|
849
|
+
id: record.id,
|
|
850
|
+
name: record.document_name,
|
|
851
|
+
url: record.file_url,
|
|
852
|
+
template: tpl.name,
|
|
853
|
+
size: pdfBuffer.length,
|
|
854
|
+
warnings: warnings.map(w => w.message),
|
|
855
|
+
},
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
case "list_pdf_templates": {
|
|
859
|
+
let query = sb.from("pdf_templates")
|
|
860
|
+
.select("id, slug, name, description, document_type, version, created_at")
|
|
861
|
+
.eq("is_active", true)
|
|
862
|
+
.or(`store_id.eq.${sid},store_id.is.null`)
|
|
863
|
+
.order("name");
|
|
864
|
+
if (args.document_type)
|
|
865
|
+
query = query.eq("document_type", args.document_type);
|
|
866
|
+
if (args.limit)
|
|
867
|
+
query = query.limit(args.limit);
|
|
868
|
+
const { data, error } = await query;
|
|
869
|
+
if (error)
|
|
870
|
+
return { success: false, error: error.message };
|
|
871
|
+
return {
|
|
872
|
+
success: true,
|
|
873
|
+
data: {
|
|
874
|
+
count: data?.length || 0,
|
|
875
|
+
templates: (data || []).map(t => ({
|
|
876
|
+
id: t.id, slug: t.slug, name: t.name,
|
|
877
|
+
description: t.description, type: t.document_type, version: t.version,
|
|
878
|
+
})),
|
|
879
|
+
},
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
case "list_document_profiles": {
|
|
883
|
+
let query = sb.from("document_profiles")
|
|
884
|
+
.select("id, name, category, sample_type, default_size, client_store_id, template_id, created_at")
|
|
885
|
+
.eq("is_active", true)
|
|
886
|
+
.eq("owner_store_id", sid)
|
|
887
|
+
.order("name");
|
|
888
|
+
if (args.template_id)
|
|
889
|
+
query = query.eq("template_id", args.template_id);
|
|
890
|
+
if (args.category)
|
|
891
|
+
query = query.eq("category", args.category);
|
|
892
|
+
if (args.limit)
|
|
893
|
+
query = query.limit(args.limit);
|
|
894
|
+
const { data, error } = await query;
|
|
895
|
+
if (error)
|
|
896
|
+
return { success: false, error: error.message };
|
|
897
|
+
return {
|
|
898
|
+
success: true,
|
|
899
|
+
data: {
|
|
900
|
+
count: data?.length || 0,
|
|
901
|
+
profiles: (data || []).map(p => ({
|
|
902
|
+
id: p.id, name: p.name, category: p.category,
|
|
903
|
+
sample_type: p.sample_type, default_size: p.default_size,
|
|
904
|
+
client_store_id: p.client_store_id,
|
|
905
|
+
})),
|
|
906
|
+
},
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
case "list_clients":
|
|
910
|
+
case "list_customers": {
|
|
911
|
+
let query = sb.from("v_store_customers")
|
|
912
|
+
.select("id, first_name, last_name, email, phone, loyalty_tier, total_orders, is_active, created_at")
|
|
913
|
+
.eq("store_id", sid)
|
|
914
|
+
.order("created_at", { ascending: false });
|
|
915
|
+
if (args.query) {
|
|
916
|
+
const term = `%${sanitizeFilterValue(String(args.query).trim())}%`;
|
|
917
|
+
query = query.or(`first_name.ilike.${term},last_name.ilike.${term},email.ilike.${term}`);
|
|
918
|
+
}
|
|
919
|
+
if (args.limit)
|
|
920
|
+
query = query.limit(args.limit);
|
|
921
|
+
const { data, error } = await query;
|
|
922
|
+
if (error)
|
|
923
|
+
return { success: false, error: error.message };
|
|
924
|
+
return {
|
|
925
|
+
success: true,
|
|
926
|
+
data: {
|
|
927
|
+
count: data?.length || 0,
|
|
928
|
+
customers: (data || []).map(c => ({
|
|
929
|
+
id: c.id, name: [c.first_name, c.last_name].filter(Boolean).join(" "),
|
|
930
|
+
email: c.email, phone: c.phone, loyalty_tier: c.loyalty_tier,
|
|
931
|
+
total_orders: c.total_orders, is_active: c.is_active,
|
|
932
|
+
})),
|
|
933
|
+
},
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
case "deliver_documents": {
|
|
937
|
+
// Batch-assign documents to a customer and optionally send notification email
|
|
938
|
+
const docIds = args.document_ids || (args.document_id ? [args.document_id] : []);
|
|
939
|
+
const customerId = args.customer_id;
|
|
940
|
+
if (!docIds.length)
|
|
941
|
+
return { success: false, error: "document_ids (array) or document_id (string) is required" };
|
|
942
|
+
if (!customerId)
|
|
943
|
+
return { success: false, error: "customer_id is required" };
|
|
944
|
+
// Verify customer belongs to this store
|
|
945
|
+
const { data: rel, error: relErr } = await sb.from("user_creation_relationships")
|
|
946
|
+
.select("id, store_id, platform_users!inner(first_name, last_name, email)")
|
|
947
|
+
.eq("id", customerId).eq("store_id", sid).single();
|
|
948
|
+
if (relErr || !rel)
|
|
949
|
+
return { success: false, error: "Customer not found for this store" };
|
|
950
|
+
const pu = rel.platform_users;
|
|
951
|
+
const customerEmail = pu.email;
|
|
952
|
+
const customerName = [pu.first_name, pu.last_name].filter(Boolean).join(" ");
|
|
953
|
+
// Assign all documents to the customer
|
|
954
|
+
const { error: updateErr } = await sb.from("store_documents")
|
|
955
|
+
.update({ customer_id: customerId })
|
|
956
|
+
.in("id", docIds)
|
|
957
|
+
.eq("store_id", sid);
|
|
958
|
+
if (updateErr)
|
|
959
|
+
return { success: false, error: `Failed to assign documents: ${updateErr.message}` };
|
|
960
|
+
// Get document names for the email
|
|
961
|
+
const { data: docs } = await sb.from("store_documents")
|
|
962
|
+
.select("id, document_name, file_url")
|
|
963
|
+
.in("id", docIds);
|
|
964
|
+
let emailSent = false;
|
|
965
|
+
if (!args.skip_email && customerEmail) {
|
|
966
|
+
const portalUrl = args.portal_url || null;
|
|
967
|
+
const docList = (docs || []).map(d => `<li><a href="${d.file_url}">${d.document_name || "Document"}</a></li>`).join("");
|
|
968
|
+
// Get store name for template
|
|
969
|
+
const { data: store } = await sb.from("stores").select("store_name").eq("id", sid).single();
|
|
970
|
+
const emailResult = await handleEmail(sb, {
|
|
971
|
+
action: "send_template",
|
|
972
|
+
to: customerEmail,
|
|
973
|
+
template: "documents-delivered",
|
|
974
|
+
template_data: {
|
|
975
|
+
customer_name: customerName || "there",
|
|
976
|
+
store_name: store?.store_name || "",
|
|
977
|
+
document_count: String(docIds.length),
|
|
978
|
+
document_list: docList,
|
|
979
|
+
portal_url: portalUrl || "",
|
|
980
|
+
},
|
|
981
|
+
}, storeId);
|
|
982
|
+
emailSent = emailResult.success;
|
|
983
|
+
}
|
|
984
|
+
return {
|
|
985
|
+
success: true,
|
|
986
|
+
data: {
|
|
987
|
+
document_ids: docIds,
|
|
988
|
+
customer_id: customerId,
|
|
989
|
+
customer_email: customerEmail,
|
|
990
|
+
email_sent: emailSent,
|
|
991
|
+
count: docIds.length,
|
|
992
|
+
},
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
case "list_customer_documents": {
|
|
996
|
+
const customerId = args.customer_id;
|
|
997
|
+
if (!customerId)
|
|
998
|
+
return { success: false, error: "customer_id is required" };
|
|
999
|
+
let query = sb.from("store_documents")
|
|
1000
|
+
.select("id, document_name, file_url, file_type, created_at, thumbnail_url, metadata, document_type")
|
|
1001
|
+
.eq("store_id", sid)
|
|
1002
|
+
.eq("customer_id", customerId)
|
|
1003
|
+
.eq("is_active", true)
|
|
1004
|
+
.order("created_at", { ascending: false });
|
|
1005
|
+
if (args.limit)
|
|
1006
|
+
query = query.limit(args.limit);
|
|
1007
|
+
const { data, error } = await query;
|
|
1008
|
+
if (error)
|
|
1009
|
+
return { success: false, error: error.message };
|
|
1010
|
+
return {
|
|
1011
|
+
success: true,
|
|
1012
|
+
data: {
|
|
1013
|
+
count: data?.length || 0,
|
|
1014
|
+
documents: data || [],
|
|
1015
|
+
},
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
585
1018
|
default:
|
|
586
|
-
return { success: false, error: `Unknown documents action: ${action}. Valid: create, find, delete, create_template, list_templates, from_template, list_stores, list_profiles, generate` };
|
|
1019
|
+
return { success: false, error: `Unknown documents action: ${action}. Valid: create, find, delete, create_template, list_templates, from_template, list_stores, list_profiles, generate, bulk_generate, generate_pdf, list_pdf_templates, list_document_profiles, list_customers, deliver_documents, list_customer_documents` };
|
|
587
1020
|
}
|
|
588
1021
|
}
|
|
@@ -55,7 +55,7 @@ const { products, store, categories } = useStore();
|
|
|
55
55
|
Product shape:
|
|
56
56
|
{ id, name, sku, description, short_description, status, type,
|
|
57
57
|
cost_price, wholesale_price, stock_quantity, featured,
|
|
58
|
-
|
|
58
|
+
custom_fields: { thca_percentage, strain_type, terpenes, effects, cbd_total, ... },
|
|
59
59
|
pricing_data: { mode, tiers: [{ label, quantity, unit, price }] },
|
|
60
60
|
category: { id, name } }
|
|
61
61
|
|
|
@@ -4,14 +4,12 @@ export declare function handleCustomers(sb: SupabaseClient, args: Record<string,
|
|
|
4
4
|
error: string;
|
|
5
5
|
count?: undefined;
|
|
6
6
|
data?: undefined;
|
|
7
|
-
note?: undefined;
|
|
8
7
|
linked?: undefined;
|
|
9
8
|
} | {
|
|
10
9
|
success: boolean;
|
|
11
10
|
count: number;
|
|
12
11
|
data: {
|
|
13
12
|
id: any;
|
|
14
|
-
platform_user_id: any;
|
|
15
13
|
first_name: any;
|
|
16
14
|
last_name: any;
|
|
17
15
|
email: any;
|
|
@@ -22,22 +20,30 @@ export declare function handleCustomers(sb: SupabaseClient, args: Record<string,
|
|
|
22
20
|
total_orders: any;
|
|
23
21
|
lifetime_value: any;
|
|
24
22
|
is_active: any;
|
|
23
|
+
rfm_segment: any;
|
|
24
|
+
is_vip_customer: any;
|
|
25
|
+
is_at_risk: any;
|
|
26
|
+
is_churned: any;
|
|
27
|
+
reorder_due: any;
|
|
28
|
+
ai_churn_risk: any;
|
|
29
|
+
days_since_last_order: any;
|
|
30
|
+
engagement_score: any;
|
|
31
|
+
age_bracket: any;
|
|
25
32
|
created_at: any;
|
|
26
33
|
}[];
|
|
27
34
|
error?: undefined;
|
|
28
|
-
note?: undefined;
|
|
29
35
|
linked?: undefined;
|
|
30
36
|
} | {
|
|
37
|
+
warning?: string | undefined;
|
|
38
|
+
note?: string | undefined;
|
|
31
39
|
success: boolean;
|
|
32
|
-
data:
|
|
40
|
+
data: Record<string, unknown> | null;
|
|
33
41
|
error?: undefined;
|
|
34
42
|
count?: undefined;
|
|
35
|
-
note?: undefined;
|
|
36
43
|
linked?: undefined;
|
|
37
44
|
} | {
|
|
38
45
|
success: boolean;
|
|
39
46
|
data: any;
|
|
40
|
-
note: string;
|
|
41
47
|
error?: undefined;
|
|
42
48
|
count?: undefined;
|
|
43
49
|
linked?: undefined;
|
|
@@ -57,7 +63,6 @@ export declare function handleCustomers(sb: SupabaseClient, args: Record<string,
|
|
|
57
63
|
created_at: any;
|
|
58
64
|
}[];
|
|
59
65
|
error?: undefined;
|
|
60
|
-
note?: undefined;
|
|
61
66
|
linked?: undefined;
|
|
62
67
|
} | {
|
|
63
68
|
success: boolean;
|
|
@@ -69,7 +74,48 @@ export declare function handleCustomers(sb: SupabaseClient, args: Record<string,
|
|
|
69
74
|
error?: undefined;
|
|
70
75
|
count?: undefined;
|
|
71
76
|
data?: undefined;
|
|
72
|
-
|
|
77
|
+
} | {
|
|
78
|
+
success: boolean;
|
|
79
|
+
count: number;
|
|
80
|
+
data: {
|
|
81
|
+
id: any;
|
|
82
|
+
points: any;
|
|
83
|
+
transaction_type: any;
|
|
84
|
+
reference_type: any;
|
|
85
|
+
reference_id: any;
|
|
86
|
+
description: any;
|
|
87
|
+
balance_before: any;
|
|
88
|
+
balance_after: any;
|
|
89
|
+
expires_at: any;
|
|
90
|
+
created_at: any;
|
|
91
|
+
}[];
|
|
92
|
+
error?: undefined;
|
|
93
|
+
linked?: undefined;
|
|
94
|
+
} | {
|
|
95
|
+
success: boolean;
|
|
96
|
+
count: number;
|
|
97
|
+
data: {
|
|
98
|
+
id: any;
|
|
99
|
+
name: any;
|
|
100
|
+
ai_description: any;
|
|
101
|
+
type: any;
|
|
102
|
+
segment_rules: any;
|
|
103
|
+
filter_criteria: any;
|
|
104
|
+
customer_count: any;
|
|
105
|
+
is_active: any;
|
|
106
|
+
color: any;
|
|
107
|
+
icon: any;
|
|
108
|
+
targeting_tips: any;
|
|
109
|
+
created_at: any;
|
|
110
|
+
}[];
|
|
111
|
+
error?: undefined;
|
|
112
|
+
linked?: undefined;
|
|
113
|
+
} | {
|
|
114
|
+
success: boolean;
|
|
115
|
+
count: number;
|
|
116
|
+
data: Record<string, unknown>[];
|
|
117
|
+
error?: undefined;
|
|
118
|
+
linked?: undefined;
|
|
73
119
|
}>;
|
|
74
120
|
export declare function handleOrders(sb: SupabaseClient, args: Record<string, unknown>, storeId?: string): Promise<{
|
|
75
121
|
success: boolean;
|