@flowselections/mailbox-orders 1.0.9 → 1.0.10

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.
Files changed (57) hide show
  1. package/dist-lib/components/MailboxAuthBanner.d.ts +16 -0
  2. package/dist-lib/components/MailboxAuthBanner.d.ts.map +1 -0
  3. package/dist-lib/components/MailboxAuthBanner.js +14 -0
  4. package/dist-lib/components/MailboxProcessedPage.d.ts.map +1 -1
  5. package/dist-lib/components/MailboxProcessedPage.js +10 -2
  6. package/dist-lib/components/MailboxProposalsPage.d.ts.map +1 -1
  7. package/dist-lib/components/MailboxProposalsPage.js +425 -104
  8. package/dist-lib/components/SearchableSelect.d.ts +1 -0
  9. package/dist-lib/components/SearchableSelect.d.ts.map +1 -1
  10. package/dist-lib/components/SearchableSelect.js +2 -1
  11. package/dist-lib/components/settings/MailboxAiPromptCard.d.ts +2 -0
  12. package/dist-lib/components/settings/MailboxAiPromptCard.d.ts.map +1 -0
  13. package/dist-lib/components/settings/MailboxAiPromptCard.js +39 -0
  14. package/dist-lib/index.d.ts.map +1 -1
  15. package/dist-lib/index.js +2 -0
  16. package/dist-lib/integrations/supabase/auth-attacher.d.ts.map +1 -1
  17. package/dist-lib/integrations/supabase/auth-attacher.js +5 -1
  18. package/dist-lib/integrations/supabase/auth-middleware.d.ts +2 -11202
  19. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  20. package/dist-lib/integrations/supabase/auth-middleware.js +8 -7
  21. package/dist-lib/integrations/supabase/client.d.ts +16514 -7163
  22. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  23. package/dist-lib/integrations/supabase/client.server.d.ts +16516 -7164
  24. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  25. package/dist-lib/integrations/supabase/types.d.ts +16768 -7202
  26. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  27. package/dist-lib/integrations/supabase/types.js +59 -8
  28. package/dist-lib/lib/ai-config.server.d.ts +46 -0
  29. package/dist-lib/lib/ai-config.server.d.ts.map +1 -0
  30. package/dist-lib/lib/ai-config.server.js +99 -0
  31. package/dist-lib/lib/delivery-date.d.ts +37 -0
  32. package/dist-lib/lib/delivery-date.d.ts.map +1 -0
  33. package/dist-lib/lib/delivery-date.js +96 -0
  34. package/dist-lib/lib/load-carriers.d.ts +26 -0
  35. package/dist-lib/lib/load-carriers.d.ts.map +1 -0
  36. package/dist-lib/lib/load-carriers.js +73 -0
  37. package/dist-lib/lib/mailbox-ai-prompt.d.ts +9 -0
  38. package/dist-lib/lib/mailbox-ai-prompt.d.ts.map +1 -0
  39. package/dist-lib/lib/mailbox-ai-prompt.js +30 -0
  40. package/dist-lib/lib/mailbox-ai-settings.functions.d.ts +23 -0
  41. package/dist-lib/lib/mailbox-ai-settings.functions.d.ts.map +1 -0
  42. package/dist-lib/lib/mailbox-ai-settings.functions.js +58 -0
  43. package/dist-lib/lib/mailbox-ai.server.d.ts +49 -4
  44. package/dist-lib/lib/mailbox-ai.server.d.ts.map +1 -1
  45. package/dist-lib/lib/mailbox-ai.server.js +284 -6
  46. package/dist-lib/lib/mailbox-body.d.ts +7 -0
  47. package/dist-lib/lib/mailbox-body.d.ts.map +1 -0
  48. package/dist-lib/lib/mailbox-body.js +38 -0
  49. package/dist-lib/lib/mailbox-ingest.server.d.ts +76 -0
  50. package/dist-lib/lib/mailbox-ingest.server.d.ts.map +1 -0
  51. package/dist-lib/lib/mailbox-ingest.server.js +863 -0
  52. package/dist-lib/lib/mailbox-template.functions.d.ts +7 -67213
  53. package/dist-lib/lib/mailbox-template.functions.d.ts.map +1 -1
  54. package/dist-lib/lib/mailbox.functions.d.ts +107 -190506
  55. package/dist-lib/lib/mailbox.functions.d.ts.map +1 -1
  56. package/dist-lib/lib/mailbox.functions.js +544 -270
  57. package/package.json +4 -3
@@ -1,17 +1,68 @@
1
1
  export const Constants = {
2
2
  public: {
3
3
  Enums: {
4
- app_role: [
5
- "admin",
6
- "manager",
7
- "warehouse",
8
- "sales",
9
- "developer",
10
- "receptie",
11
- "monteur",
4
+ ai_import_status: [
5
+ "nieuw",
6
+ "verwerken",
7
+ "bevestigen",
8
+ "klaar",
9
+ "mislukt",
12
10
  ],
11
+ app_role: ["admin", "planner", "monteur", "administratie", "ai_admin"],
12
+ doc_layout_type: [
13
+ "pakbon",
14
+ "factuur",
15
+ "offerte",
16
+ "orderbevestiging",
17
+ "etiket",
18
+ "verzendlabel",
19
+ "productlabel",
20
+ "barcode_label",
21
+ "magazijnlijst",
22
+ "controlelijst",
23
+ "transportdocument",
24
+ "vrij",
25
+ "sticker",
26
+ "productiebon",
27
+ ],
28
+ doc_orientation: ["portrait", "landscape"],
13
29
  exact_env: ["staging", "live"],
14
30
  exact_push_status: ["pending", "sent", "error"],
31
+ itk_customer_segment: ["Business", "Private"],
32
+ itk_dynamic_field_type: ["text", "select", "date", "checkbox"],
33
+ planning_item_status: [
34
+ "gepland",
35
+ "bezig",
36
+ "afgerond",
37
+ "geannuleerd",
38
+ "concept",
39
+ "vrijgegeven",
40
+ ],
41
+ print_job_status: [
42
+ "wachtend",
43
+ "bezig",
44
+ "voltooid",
45
+ "mislukt",
46
+ "geannuleerd",
47
+ ],
48
+ printer_connection: [
49
+ "ipp",
50
+ "raw9100",
51
+ "printnode",
52
+ "cups",
53
+ "usb",
54
+ "windows",
55
+ "airprint",
56
+ "cloud",
57
+ ],
58
+ printer_kind: [
59
+ "labelprinter",
60
+ "laserprinter",
61
+ "inkjet",
62
+ "thermal",
63
+ "kantoorprinter",
64
+ "overig",
65
+ ],
15
66
  quote_status: [
16
67
  "concept",
17
68
  "verzonden",
@@ -0,0 +1,46 @@
1
+ import type { SupabaseClient } from "@supabase/supabase-js";
2
+ import type { Database } from "../integrations/supabase/types";
3
+ type AdminClient = SupabaseClient<Database>;
4
+ export interface ModuleAiConfig {
5
+ moduleKey: string;
6
+ providerSlug: string;
7
+ providerName: string;
8
+ baseUrl: string;
9
+ modelSlug: string;
10
+ modelName: string;
11
+ apiKey: string;
12
+ credentialLabel: string | null;
13
+ /**
14
+ * Vingerafdruk van de gekoppelde AI-agent (binding + model + provider +
15
+ * sleutel, inclusief hun updated_at). Wijzigt de agent, dan wijzigt deze
16
+ * string — daarop detecteren we dat er opnieuw geparsed moet worden.
17
+ */
18
+ fingerprint: string;
19
+ }
20
+ export type ModuleAiResolution = {
21
+ ok: true;
22
+ config: ModuleAiConfig;
23
+ } | {
24
+ ok: false;
25
+ reason: string;
26
+ };
27
+ /**
28
+ * Zoekt de actieve AI-binding voor een module. Retourneert nooit een throw:
29
+ * de aanroeper beslist zelf wat er gebeurt als AI niet beschikbaar is.
30
+ */
31
+ export declare function resolveModuleAi(admin: AdminClient, moduleKey: string): Promise<ModuleAiResolution>;
32
+ export interface AiCallLogInput {
33
+ moduleKey: string;
34
+ providerSlug: string;
35
+ modelSlug: string;
36
+ status: "success" | "error";
37
+ inputTokens?: number | null;
38
+ outputTokens?: number | null;
39
+ durationMs?: number | null;
40
+ errorCode?: string | null;
41
+ errorMessage?: string | null;
42
+ }
43
+ /** Schrijft één regel in ai_call_log. Faalt nooit hard. */
44
+ export declare function logAiCall(admin: AdminClient, input: AiCallLogInput): Promise<void>;
45
+ export {};
46
+ //# sourceMappingURL=ai-config.server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-config.server.d.ts","sourceRoot":"","sources":["../../src/lib/ai-config.server.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAE9D,KAAK,WAAW,GAAG,cAAc,CAAC,QAAQ,CAAC,CAAC;AAE5C,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,kBAAkB,GAC1B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,cAAc,CAAA;CAAE,GACpC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAElC;;;GAGG;AACH,wBAAsB,eAAe,CACnC,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,kBAAkB,CAAC,CAoE7B;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,2DAA2D;AAC3D,wBAAsB,SAAS,CAAC,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBxF"}
@@ -0,0 +1,99 @@
1
+ // Server-only: haalt de AI-configuratie voor een module op uit de centrale
2
+ // AI-Authenticatie module (tabellen ai_modules / ai_module_bindings /
3
+ // ai_models / ai_providers / ai_credentials).
4
+ //
5
+ // De API-sleutel verlaat NOOIT de server. Dit bestand mag alleen vanuit
6
+ // server-function handlers of andere *.server.ts helpers geladen worden.
7
+ /**
8
+ * Zoekt de actieve AI-binding voor een module. Retourneert nooit een throw:
9
+ * de aanroeper beslist zelf wat er gebeurt als AI niet beschikbaar is.
10
+ */
11
+ export async function resolveModuleAi(admin, moduleKey) {
12
+ const { data: mod, error: modErr } = await admin
13
+ .from("ai_modules")
14
+ .select("module_key, ai_enabled")
15
+ .eq("module_key", moduleKey)
16
+ .maybeSingle();
17
+ if (modErr)
18
+ return { ok: false, reason: `ai_modules: ${modErr.message}` };
19
+ if (!mod)
20
+ return { ok: false, reason: `Module '${moduleKey}' bestaat niet in de AI-module.` };
21
+ if (!mod.ai_enabled)
22
+ return { ok: false, reason: "AI staat uit voor deze module." };
23
+ const { data: bindings, error: bindErr } = await admin
24
+ .from("ai_module_bindings")
25
+ .select("id, is_default, label, notes, updated_at, model_id, credential_id, ai_models(slug, display_name, active, updated_at, provider_id, ai_providers(slug, name, base_url, active, updated_at)), ai_credentials(id, api_key, label, active, updated_at)")
26
+ .eq("consumer_module_id", moduleKey)
27
+ .order("is_default", { ascending: false });
28
+ if (bindErr)
29
+ return { ok: false, reason: `ai_module_bindings: ${bindErr.message}` };
30
+ const list = Array.isArray(bindings) ? bindings : [];
31
+ if (list.length === 0) {
32
+ return { ok: false, reason: `Geen AI-koppeling ingesteld voor '${moduleKey}'.` };
33
+ }
34
+ for (const b of list) {
35
+ const model = b.ai_models;
36
+ const cred = b.ai_credentials;
37
+ const provider = model?.ai_providers;
38
+ if (!model?.active || !cred?.active || !provider?.active)
39
+ continue;
40
+ const apiKey = typeof cred.api_key === "string" ? cred.api_key.trim() : "";
41
+ const baseUrl = typeof provider.base_url === "string" ? provider.base_url.replace(/\/+$/, "") : "";
42
+ if (!apiKey || !baseUrl || !model.slug || !provider.slug)
43
+ continue;
44
+ const fingerprint = [
45
+ "v1",
46
+ b.id,
47
+ b.updated_at ?? "",
48
+ b.label ?? "",
49
+ b.notes ?? "",
50
+ model.slug,
51
+ model.updated_at ?? "",
52
+ provider.slug,
53
+ baseUrl,
54
+ provider.updated_at ?? "",
55
+ cred.id ?? "",
56
+ cred.updated_at ?? "",
57
+ // sleutelwissel zonder updated_at-bump moet ook tellen
58
+ String(apiKey.length),
59
+ apiKey.slice(-4),
60
+ ].join("|");
61
+ return {
62
+ ok: true,
63
+ config: {
64
+ moduleKey,
65
+ providerSlug: String(provider.slug),
66
+ providerName: String(provider.name ?? provider.slug),
67
+ baseUrl,
68
+ modelSlug: String(model.slug),
69
+ modelName: String(model.display_name ?? model.slug),
70
+ apiKey,
71
+ credentialLabel: cred.label ?? null,
72
+ fingerprint,
73
+ },
74
+ };
75
+ }
76
+ return { ok: false, reason: "Geen actieve combinatie van model + sleutel gevonden." };
77
+ }
78
+ /** Schrijft één regel in ai_call_log. Faalt nooit hard. */
79
+ export async function logAiCall(admin, input) {
80
+ try {
81
+ const inTok = input.inputTokens ?? null;
82
+ const outTok = input.outputTokens ?? null;
83
+ await admin.from("ai_call_log").insert({
84
+ consumer_module_id: input.moduleKey,
85
+ provider_slug: input.providerSlug,
86
+ model_slug: input.modelSlug,
87
+ status: input.status,
88
+ input_tokens: inTok ?? undefined,
89
+ output_tokens: outTok ?? undefined,
90
+ total_tokens: inTok !== null || outTok !== null ? (inTok ?? 0) + (outTok ?? 0) : undefined,
91
+ duration_ms: input.durationMs ?? null,
92
+ error_code: input.errorCode ?? null,
93
+ error_message: input.errorMessage ? String(input.errorMessage).slice(0, 500) : null,
94
+ });
95
+ }
96
+ catch {
97
+ // logging mag de pipeline nooit breken
98
+ }
99
+ }
@@ -0,0 +1,37 @@
1
+ export interface PickupMoment {
2
+ signup_time?: string | null;
3
+ signup_days?: number | null;
4
+ pickup_time?: string | null;
5
+ pickup_days?: number | null;
6
+ delivery_time?: string | null;
7
+ delivery_days?: number | null;
8
+ }
9
+ export interface CarrierSchedule {
10
+ delivery_days?: number | null;
11
+ pickup_weekdays?: number[] | null;
12
+ delivery_weekdays?: number[] | null;
13
+ pickup_moments?: PickupMoment[] | null;
14
+ }
15
+ export declare function toISODate(d: Date): string;
16
+ export interface EarliestDeliveryResult {
17
+ date: Date;
18
+ iso: string;
19
+ /** Moment waarop de bestelling uiterlijk aangemeld moet zijn, indien bekend. */
20
+ signupAt: Date | null;
21
+ /** true wanneer de datum uit de aanmeldmomenten van de vervoerder volgt. */
22
+ fromSchedule: boolean;
23
+ }
24
+ /**
25
+ * Eerst mogelijke bezorgdatum:
26
+ * - Alleen ophaaldagen die de vervoerder rijdt tellen mee.
27
+ * - Een aanmeldmoment telt alleen als het deadline-tijdstip nog niet verstreken is.
28
+ * - De bezorgdag moet in delivery_weekdays vallen.
29
+ * - Orderpicktijd (pickDays) verschuift de vroegst toegestane bezorgdag.
30
+ */
31
+ export declare function computeEarliestDelivery(carrier: CarrierSchedule | null | undefined, pickDays?: number, now?: Date): EarliestDeliveryResult;
32
+ /**
33
+ * Is deze ISO-datum een geldige bezorgdag voor deze vervoerder?
34
+ * Controleert de toegestane bezorgweekdagen en de eerst mogelijke datum.
35
+ */
36
+ export declare function isValidDeliveryDate(carrier: CarrierSchedule | null | undefined, iso: string, pickDays?: number, now?: Date): boolean;
37
+ //# sourceMappingURL=delivery-date.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delivery-date.d.ts","sourceRoot":"","sources":["../../src/lib/delivery-date.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,YAAY;IAC3B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,eAAe;IAC9B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,eAAe,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAClC,iBAAiB,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACpC,cAAc,CAAC,EAAE,YAAY,EAAE,GAAG,IAAI,CAAC;CACxC;AAED,wBAAgB,SAAS,CAAC,CAAC,EAAE,IAAI,GAAG,MAAM,CAEzC;AA0BD,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,IAAI,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,gFAAgF;IAChF,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAC;IACtB,4EAA4E;IAC5E,YAAY,EAAE,OAAO,CAAC;CACvB;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,eAAe,GAAG,IAAI,GAAG,SAAS,EAC3C,QAAQ,SAAI,EACZ,GAAG,GAAE,IAAiB,GACrB,sBAAsB,CA0CxB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,eAAe,GAAG,IAAI,GAAG,SAAS,EAC3C,GAAG,EAAE,MAAM,EACX,QAAQ,SAAI,EACZ,GAAG,GAAE,IAAiB,GACrB,OAAO,CAWT"}
@@ -0,0 +1,96 @@
1
+ // Berekening van de eerst mogelijke bezorgdatum op basis van de aanmeldmomenten
2
+ // (pickup_moments) van de vervoerder. Pure functies — bruikbaar in browser én server.
3
+ export function toISODate(d) {
4
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
5
+ }
6
+ function addDays(d, n) {
7
+ const x = new Date(d);
8
+ x.setDate(x.getDate() + n);
9
+ return x;
10
+ }
11
+ function isoWeekday(d) {
12
+ const dow = d.getDay(); // 0 = zondag
13
+ return dow === 0 ? 7 : dow;
14
+ }
15
+ function parseTime(t) {
16
+ if (typeof t !== "string")
17
+ return null;
18
+ const match = t.match(/^(\d{1,2}):(\d{2})/);
19
+ if (!match)
20
+ return null;
21
+ return { h: Number(match[1]), m: Number(match[2]) };
22
+ }
23
+ function atTime(day, time) {
24
+ const x = new Date(day);
25
+ x.setHours(time.h, time.m, 0, 0);
26
+ return x;
27
+ }
28
+ /**
29
+ * Eerst mogelijke bezorgdatum:
30
+ * - Alleen ophaaldagen die de vervoerder rijdt tellen mee.
31
+ * - Een aanmeldmoment telt alleen als het deadline-tijdstip nog niet verstreken is.
32
+ * - De bezorgdag moet in delivery_weekdays vallen.
33
+ * - Orderpicktijd (pickDays) verschuift de vroegst toegestane bezorgdag.
34
+ */
35
+ export function computeEarliestDelivery(carrier, pickDays = 0, now = new Date()) {
36
+ const carrierDeliveryDays = Math.max(0, Number(carrier?.delivery_days ?? 0));
37
+ const minDay = addDays(new Date(now.getFullYear(), now.getMonth(), now.getDate()), Math.max(0, pickDays));
38
+ const moments = Array.isArray(carrier?.pickup_moments) ? carrier.pickup_moments : [];
39
+ const pickupWeekdays = Array.isArray(carrier?.pickup_weekdays) && carrier.pickup_weekdays.length
40
+ ? carrier.pickup_weekdays.map(Number)
41
+ : null;
42
+ const deliveryWeekdays = Array.isArray(carrier?.delivery_weekdays) && carrier.delivery_weekdays.length
43
+ ? carrier.delivery_weekdays.map(Number)
44
+ : null;
45
+ const usable = moments.filter((m) => parseTime(m?.signup_time));
46
+ if (usable.length > 0) {
47
+ for (let offset = 0; offset <= 60; offset++) {
48
+ const pickupDay = addDays(new Date(now.getFullYear(), now.getMonth(), now.getDate()), offset);
49
+ if (pickupWeekdays && !pickupWeekdays.includes(isoWeekday(pickupDay)))
50
+ continue;
51
+ let best = null;
52
+ for (const m of usable) {
53
+ const time = parseTime(m.signup_time);
54
+ const signupAt = atTime(addDays(pickupDay, -Math.max(0, Number(m.signup_days ?? 0))), time);
55
+ if (signupAt.getTime() <= now.getTime())
56
+ continue; // deadline verstreken
57
+ const deliveryDay = addDays(pickupDay, Math.max(0, Number(m.delivery_days ?? carrierDeliveryDays)));
58
+ if (deliveryWeekdays && !deliveryWeekdays.includes(isoWeekday(deliveryDay)))
59
+ continue;
60
+ if (deliveryDay.getTime() < minDay.getTime())
61
+ continue;
62
+ if (!best || deliveryDay.getTime() < best.date.getTime()) {
63
+ best = { date: deliveryDay, iso: toISODate(deliveryDay), signupAt, fromSchedule: true };
64
+ }
65
+ }
66
+ if (best)
67
+ return best;
68
+ }
69
+ }
70
+ // Geen aanmeldmomenten bekend: val terug op picktijd + levertijd vervoerder.
71
+ let fallback = addDays(minDay, carrierDeliveryDays);
72
+ for (let i = 0; i < 14 && deliveryWeekdays && !deliveryWeekdays.includes(isoWeekday(fallback)); i++) {
73
+ fallback = addDays(fallback, 1);
74
+ }
75
+ return { date: fallback, iso: toISODate(fallback), signupAt: null, fromSchedule: false };
76
+ }
77
+ /**
78
+ * Is deze ISO-datum een geldige bezorgdag voor deze vervoerder?
79
+ * Controleert de toegestane bezorgweekdagen en de eerst mogelijke datum.
80
+ */
81
+ export function isValidDeliveryDate(carrier, iso, pickDays = 0, now = new Date()) {
82
+ if (!iso)
83
+ return false;
84
+ const earliest = computeEarliestDelivery(carrier, pickDays, now);
85
+ if (iso < earliest.iso)
86
+ return false;
87
+ const deliveryWeekdays = Array.isArray(carrier?.delivery_weekdays) && carrier.delivery_weekdays.length
88
+ ? carrier.delivery_weekdays.map(Number)
89
+ : null;
90
+ if (!deliveryWeekdays)
91
+ return true;
92
+ const [y, m, d] = iso.split("-").map(Number);
93
+ if (!y || !m || !d)
94
+ return false;
95
+ return deliveryWeekdays.includes(isoWeekday(new Date(y, m - 1, d)));
96
+ }
@@ -0,0 +1,26 @@
1
+ export interface EngineLoadCarrier {
2
+ code: string;
3
+ label: string;
4
+ enabled: boolean;
5
+ isDefault: boolean;
6
+ }
7
+ export interface AllowedLoadCarriers {
8
+ allowed: string[];
9
+ default: string | null;
10
+ carriers: EngineLoadCarrier[];
11
+ }
12
+ export declare const EMPTY_LOAD_CARRIERS: AllowedLoadCarriers;
13
+ /** Normaliseert de Json-uitvoer van de Engine naar een vaste vorm. */
14
+ export declare function normalizeAllowedLoadCarriers(raw: unknown): AllowedLoadCarriers;
15
+ /** Alleen de dragers die de Engine toestaat (enabled === true). */
16
+ export declare function enabledLoadCarriers(list: AllowedLoadCarriers): EngineLoadCarrier[];
17
+ export declare function loadCarrierLabel(list: AllowedLoadCarriers, code: string | null): string | null;
18
+ export declare function isLoadCarrierAllowed(list: AllowedLoadCarriers, code: string | null): boolean;
19
+ /**
20
+ * Herkent een ladingdrager in vrije tekst. De kandidaten komen uit de
21
+ * Engine-lijst (ook uitgeschakelde dragers, zodat we kunnen melden dat de mail
22
+ * een uitgeschakelde drager noemt). Er wordt niets vertaald naar codes die de
23
+ * Engine niet kent.
24
+ */
25
+ export declare function detectLoadCarrierInText(list: AllowedLoadCarriers, text: string | null | undefined): string | null;
26
+ //# sourceMappingURL=load-carriers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-carriers.d.ts","sourceRoot":"","sources":["../../src/lib/load-carriers.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,iBAAiB,EAAE,CAAC;CAC/B;AAED,eAAO,MAAM,mBAAmB,EAAE,mBAIjC,CAAC;AAEF,sEAAsE;AACtE,wBAAgB,4BAA4B,CAAC,GAAG,EAAE,OAAO,GAAG,mBAAmB,CAsB9E;AAED,mEAAmE;AACnE,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,mBAAmB,GAAG,iBAAiB,EAAE,CAElF;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,mBAAmB,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAG9F;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,mBAAmB,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAG5F;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,mBAAmB,EACzB,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC9B,MAAM,GAAG,IAAI,CAkBf"}
@@ -0,0 +1,73 @@
1
+ // Ladingdragers komen UITSLUITEND uit de Logistics Engine
2
+ // (RPC logistics_allowed_load_carriers). Deze module bevat geen eigen lijst:
3
+ // alleen types en herkenning van tekst op basis van de Engine-uitvoer.
4
+ export const EMPTY_LOAD_CARRIERS = {
5
+ allowed: [],
6
+ default: null,
7
+ carriers: [],
8
+ };
9
+ /** Normaliseert de Json-uitvoer van de Engine naar een vaste vorm. */
10
+ export function normalizeAllowedLoadCarriers(raw) {
11
+ const r = (raw ?? {});
12
+ const carriers = Array.isArray(r.carriers)
13
+ ? r.carriers
14
+ .filter((c) => c && typeof c.code === "string")
15
+ .map((c) => ({
16
+ code: String(c.code),
17
+ label: typeof c.label === "string" && c.label ? c.label : String(c.code),
18
+ enabled: c.enabled === true,
19
+ isDefault: c.isDefault === true,
20
+ }))
21
+ : [];
22
+ const allowed = Array.isArray(r.allowed)
23
+ ? r.allowed.filter((c) => typeof c === "string")
24
+ : carriers.filter((c) => c.enabled).map((c) => c.code);
25
+ const def = typeof r.default === "string" && r.default
26
+ ? r.default
27
+ : (carriers.find((c) => c.isDefault && c.enabled)?.code ??
28
+ carriers.find((c) => c.enabled)?.code ??
29
+ null);
30
+ return { allowed, default: def, carriers };
31
+ }
32
+ /** Alleen de dragers die de Engine toestaat (enabled === true). */
33
+ export function enabledLoadCarriers(list) {
34
+ return list.carriers.filter((c) => c.enabled && list.allowed.includes(c.code));
35
+ }
36
+ export function loadCarrierLabel(list, code) {
37
+ if (!code)
38
+ return null;
39
+ return list.carriers.find((c) => c.code === code)?.label ?? code;
40
+ }
41
+ export function isLoadCarrierAllowed(list, code) {
42
+ if (!code)
43
+ return false;
44
+ return list.allowed.includes(code);
45
+ }
46
+ /**
47
+ * Herkent een ladingdrager in vrije tekst. De kandidaten komen uit de
48
+ * Engine-lijst (ook uitgeschakelde dragers, zodat we kunnen melden dat de mail
49
+ * een uitgeschakelde drager noemt). Er wordt niets vertaald naar codes die de
50
+ * Engine niet kent.
51
+ */
52
+ export function detectLoadCarrierInText(list, text) {
53
+ if (!text)
54
+ return null;
55
+ const hay = ` ${text.toLowerCase().replace(/[\s\u00a0]+/g, " ")} `;
56
+ // Langste labels eerst, zodat "Euro-rolcontainer" wint van "rolcontainer".
57
+ const candidates = [...list.carriers].sort((a, b) => b.label.length - a.label.length);
58
+ for (const c of candidates) {
59
+ const variants = new Set([
60
+ c.label.toLowerCase(),
61
+ c.label.toLowerCase().replace(/-/g, " "),
62
+ c.code.toLowerCase().replace(/_/g, " "),
63
+ c.code.toLowerCase(),
64
+ ]);
65
+ for (const v of variants) {
66
+ if (v.length < 4)
67
+ continue;
68
+ if (hay.includes(` ${v} `) || hay.includes(` ${v}s `) || hay.includes(`${v},`))
69
+ return c.code;
70
+ }
71
+ }
72
+ return null;
73
+ }
@@ -0,0 +1,9 @@
1
+ /** Standaard-systeemprompt. Beheerders kunnen deze in Instellingen overschrijven. */
2
+ export declare const DEFAULT_MAILBOX_SYSTEM_PROMPT: string;
3
+ /** Velden die standaard verplicht uit de mail moeten komen. */
4
+ export declare const DEFAULT_REQUIRED_FIELDS: string[];
5
+ export declare const REQUIRED_FIELD_CHOICES: Array<{
6
+ key: string;
7
+ label: string;
8
+ }>;
9
+ //# sourceMappingURL=mailbox-ai-prompt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mailbox-ai-prompt.d.ts","sourceRoot":"","sources":["../../src/lib/mailbox-ai-prompt.ts"],"names":[],"mappings":"AAEA,qFAAqF;AACrF,eAAO,MAAM,6BAA6B,QAW9B,CAAC;AAEb,+DAA+D;AAC/D,eAAO,MAAM,uBAAuB,UAKnC,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,KAAK,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAQxE,CAAC"}
@@ -0,0 +1,30 @@
1
+ // Client-veilige constanten voor de mailbox-AI (gedeeld door server en UI).
2
+ /** Standaard-systeemprompt. Beheerders kunnen deze in Instellingen overschrijven. */
3
+ export const DEFAULT_MAILBOX_SYSTEM_PROMPT = [
4
+ "Je bent een orderverwerker voor een plantenkwekerij.",
5
+ "Je leest een bestelmail (Nederlands of Engels) en zet die om in gestructureerde JSON.",
6
+ "Verzin NOOIT gegevens. Wat niet in de mail staat is null met confidence 0 en komt in validation.missing.",
7
+ "Haal ALTIJD op als de mail ze noemt: klant, gewenste bezorgdatum, artikelen, aantallen, plantsoort/variëteit en afleverlocatie.",
8
+ "Bezorgdatum: reken relatieve aanduidingen ('morgen', 'overmorgen', 'aanstaande maandag', 'volgende week vrijdag', 'week 32', 'begin volgende maand') altijd om naar één concrete datum in YYYY-MM-DD, gebruikmakend van de meegegeven datumcontext. Kun je het niet eenduidig bepalen, zet dan null met needs_review = true en zet het veld in validation.uncertain.",
9
+ "Plantsoort: herken de plantsoort/variëteit (bijv. 'Acer palmatum', 'Hydrangea Annabelle') en kies uitsluitend een naam uit de meegegeven plant_types-lijst; is er geen goede match, geef dan de letterlijke tekst uit de mail terug en zet de regel in validation.uncertain.",
10
+ "product_id moet een id uit de productcatalogus zijn; twijfel je, dan null met een lage match_confidence.",
11
+ "Voor velden met een 'allowed'-lijst gebruik je uitsluitend een id uit die lijst, anders null.",
12
+ "Zet needs_review op true bij confidence onder 0.8.",
13
+ "Controleer aan het eind zelf of alle verplichte velden (required_fields) zijn ingevuld en betrouwbaar zijn; noem ontbrekende velden in validation.missing en twijfelgevallen in validation.uncertain, met een korte toelichting in validation.notes. Gok nooit een waarde om een veld te vullen.",
14
+ ].join("\n");
15
+ /** Velden die standaard verplicht uit de mail moeten komen. */
16
+ export const DEFAULT_REQUIRED_FIELDS = [
17
+ "customer",
18
+ "delivery_date",
19
+ "delivery_location",
20
+ "lines",
21
+ ];
22
+ export const REQUIRED_FIELD_CHOICES = [
23
+ { key: "customer", label: "Klant" },
24
+ { key: "delivery_date", label: "Bezorgdatum" },
25
+ { key: "delivery_location", label: "Afleveradres" },
26
+ { key: "pickup_location", label: "Afhaallocatie / Tuin" },
27
+ { key: "lines", label: "Bestelregels (artikelen + aantallen)" },
28
+ { key: "reference", label: "Referentie klant" },
29
+ { key: "notes", label: "Opmerking" },
30
+ ];
@@ -0,0 +1,23 @@
1
+ export declare const getMailboxAiSettings: import("@tanstack/start-client-core").OptionalFetcher<readonly [import("@tanstack/start-client-core").FunctionMiddlewareAfterServer<{}, unknown, undefined, {
2
+ supabase: import("@supabase/supabase-js").SupabaseClient<import("../integrations/supabase/types").Database>;
3
+ userId: string;
4
+ claims: import("@supabase/auth-js").User;
5
+ }, undefined, undefined, undefined>], undefined, Promise<{
6
+ id: any;
7
+ enabled: any;
8
+ system_prompt: string | null;
9
+ required_fields: string[];
10
+ default_prompt: string;
11
+ }>>;
12
+ export declare const saveMailboxAiSettings: import("@tanstack/start-client-core").OptionalFetcher<readonly [import("@tanstack/start-client-core").FunctionMiddlewareAfterServer<{}, unknown, undefined, {
13
+ supabase: import("@supabase/supabase-js").SupabaseClient<import("../integrations/supabase/types").Database>;
14
+ userId: string;
15
+ claims: import("@supabase/auth-js").User;
16
+ }, undefined, undefined, undefined>], (input: unknown) => {
17
+ system_prompt: string | null;
18
+ required_fields: string[];
19
+ }, Promise<{
20
+ ok: true;
21
+ id: string;
22
+ }>>;
23
+ //# sourceMappingURL=mailbox-ai-settings.functions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mailbox-ai-settings.functions.d.ts","sourceRoot":"","sources":["../../src/lib/mailbox-ai-settings.functions.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,oBAAoB;;;;;;;;;;GAgB7B,CAAC;AAOL,eAAO,MAAM,qBAAqB;;;;8CAER,OAAO;;;;;;GA8B7B,CAAC"}
@@ -0,0 +1,58 @@
1
+ // Beheer van de centrale systeemprompt en verplichte velden voor de mailbox-AI.
2
+ import { createServerFn } from "@tanstack/react-start";
3
+ import { requireSupabaseAuth } from "../integrations/supabase/auth-middleware";
4
+ import { z } from "zod";
5
+ import { DEFAULT_MAILBOX_SYSTEM_PROMPT, DEFAULT_REQUIRED_FIELDS } from "./mailbox-ai-prompt";
6
+ export const getMailboxAiSettings = createServerFn({ method: "GET" })
7
+ .middleware([requireSupabaseAuth])
8
+ .handler(async ({ context }) => {
9
+ const { data } = await context.supabase
10
+ .from("mailbox_ai_settings")
11
+ .select("id, enabled, system_prompt, required_fields")
12
+ .limit(1)
13
+ .maybeSingle();
14
+ const req = Array.isArray(data?.required_fields) ? data.required_fields : [];
15
+ return {
16
+ id: data?.id ?? null,
17
+ enabled: data?.enabled ?? true,
18
+ system_prompt: data?.system_prompt ?? null,
19
+ required_fields: req.length > 0 ? req : DEFAULT_REQUIRED_FIELDS,
20
+ default_prompt: DEFAULT_MAILBOX_SYSTEM_PROMPT,
21
+ };
22
+ });
23
+ const SaveInput = z.object({
24
+ system_prompt: z.string().max(20000).nullable(),
25
+ required_fields: z.array(z.string().max(64)).max(30),
26
+ });
27
+ export const saveMailboxAiSettings = createServerFn({ method: "POST" })
28
+ .middleware([requireSupabaseAuth])
29
+ .inputValidator((input) => SaveInput.parse(input))
30
+ .handler(async ({ data, context }) => {
31
+ const prompt = (data.system_prompt ?? "").trim();
32
+ const payload = {
33
+ system_prompt: prompt.length > 0 ? prompt : null,
34
+ required_fields: data.required_fields.length > 0 ? data.required_fields : DEFAULT_REQUIRED_FIELDS,
35
+ };
36
+ const { data: existing } = await context.supabase
37
+ .from("mailbox_ai_settings")
38
+ .select("id")
39
+ .limit(1)
40
+ .maybeSingle();
41
+ if (existing?.id) {
42
+ const { error } = await context.supabase
43
+ .from("mailbox_ai_settings")
44
+ .update(payload)
45
+ .eq("id", existing.id);
46
+ if (error)
47
+ throw new Error(error.message);
48
+ return { ok: true, id: existing.id };
49
+ }
50
+ const { data: inserted, error } = await context.supabase
51
+ .from("mailbox_ai_settings")
52
+ .insert({ ...payload, singleton: true })
53
+ .select("id")
54
+ .single();
55
+ if (error)
56
+ throw new Error(error.message);
57
+ return { ok: true, id: inserted.id };
58
+ });