@flowselections/mailbox-orders 1.0.8 → 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.
- package/dist-lib/components/MailboxAuthBanner.d.ts +16 -0
- package/dist-lib/components/MailboxAuthBanner.d.ts.map +1 -0
- package/dist-lib/components/MailboxAuthBanner.js +14 -0
- package/dist-lib/components/MailboxProcessedPage.d.ts.map +1 -1
- package/dist-lib/components/MailboxProcessedPage.js +10 -2
- package/dist-lib/components/MailboxProposalsPage.d.ts.map +1 -1
- package/dist-lib/components/MailboxProposalsPage.js +425 -104
- package/dist-lib/components/SearchableSelect.d.ts +1 -0
- package/dist-lib/components/SearchableSelect.d.ts.map +1 -1
- package/dist-lib/components/SearchableSelect.js +2 -1
- package/dist-lib/components/settings/MailboxAiPromptCard.d.ts +2 -0
- package/dist-lib/components/settings/MailboxAiPromptCard.d.ts.map +1 -0
- package/dist-lib/components/settings/MailboxAiPromptCard.js +39 -0
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +2 -0
- package/dist-lib/integrations/supabase/auth-attacher.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/auth-attacher.js +5 -1
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +2 -11202
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/auth-middleware.js +8 -7
- package/dist-lib/integrations/supabase/client.d.ts +16514 -7163
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +16516 -7164
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +16768 -7202
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.js +59 -8
- package/dist-lib/lib/ai-config.server.d.ts +46 -0
- package/dist-lib/lib/ai-config.server.d.ts.map +1 -0
- package/dist-lib/lib/ai-config.server.js +99 -0
- package/dist-lib/lib/delivery-date.d.ts +37 -0
- package/dist-lib/lib/delivery-date.d.ts.map +1 -0
- package/dist-lib/lib/delivery-date.js +96 -0
- package/dist-lib/lib/load-carriers.d.ts +26 -0
- package/dist-lib/lib/load-carriers.d.ts.map +1 -0
- package/dist-lib/lib/load-carriers.js +73 -0
- package/dist-lib/lib/mailbox-ai-prompt.d.ts +9 -0
- package/dist-lib/lib/mailbox-ai-prompt.d.ts.map +1 -0
- package/dist-lib/lib/mailbox-ai-prompt.js +30 -0
- package/dist-lib/lib/mailbox-ai-settings.functions.d.ts +23 -0
- package/dist-lib/lib/mailbox-ai-settings.functions.d.ts.map +1 -0
- package/dist-lib/lib/mailbox-ai-settings.functions.js +58 -0
- package/dist-lib/lib/mailbox-ai.server.d.ts +49 -4
- package/dist-lib/lib/mailbox-ai.server.d.ts.map +1 -1
- package/dist-lib/lib/mailbox-ai.server.js +284 -6
- package/dist-lib/lib/mailbox-body.d.ts +7 -0
- package/dist-lib/lib/mailbox-body.d.ts.map +1 -0
- package/dist-lib/lib/mailbox-body.js +38 -0
- package/dist-lib/lib/mailbox-ingest.server.d.ts +76 -0
- package/dist-lib/lib/mailbox-ingest.server.d.ts.map +1 -0
- package/dist-lib/lib/mailbox-ingest.server.js +863 -0
- package/dist-lib/lib/mailbox-template.functions.d.ts +7 -67213
- package/dist-lib/lib/mailbox-template.functions.d.ts.map +1 -1
- package/dist-lib/lib/mailbox.functions.d.ts +107 -190506
- package/dist-lib/lib/mailbox.functions.d.ts.map +1 -1
- package/dist-lib/lib/mailbox.functions.js +544 -270
- package/package.json +4 -3
|
@@ -3,6 +3,9 @@ import { createServerFn } from "@tanstack/react-start";
|
|
|
3
3
|
import { requireSupabaseAuth } from "../integrations/supabase/auth-middleware";
|
|
4
4
|
import { createClient } from "@supabase/supabase-js";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
+
import { computeEarliestDelivery } from "../lib/delivery-date";
|
|
7
|
+
import { emailBodyText } from "./mailbox-body";
|
|
8
|
+
import { EMPTY_LOAD_CARRIERS, isLoadCarrierAllowed, loadCarrierLabel, normalizeAllowedLoadCarriers, } from "./load-carriers";
|
|
6
9
|
// ─── Actieve mailbox (uit gedeelde e-mail-auth module) ──────────────────────
|
|
7
10
|
// Geen auth-middleware: de RPC is SECURITY DEFINER en EXECUTE staat ook aan
|
|
8
11
|
// `anon`. Zo blijft de status-banner werken als de bearer-token tijdelijk
|
|
@@ -37,242 +40,80 @@ export const getActiveOrdersMailbox = createServerFn({ method: "GET" })
|
|
|
37
40
|
needs_reauth: !!row.needs_reauth,
|
|
38
41
|
};
|
|
39
42
|
});
|
|
40
|
-
// ───
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
.maybeSingle();
|
|
59
|
-
if (existing?.id) {
|
|
60
|
-
return { inserted: false, messageId: existing.id };
|
|
43
|
+
// ─── Ingest-triggers ────────────────────────────────────────────────────────
|
|
44
|
+
// De volledige ingest-pipeline (events → messages → stub-proposals) leeft in
|
|
45
|
+
// `src/lib/mailbox-ingest.server.ts`. Onderstaande server-fns zijn dunne
|
|
46
|
+
// wrappers zodat de client dezelfde pipeline kan afvuren als pg_cron.
|
|
47
|
+
// Bewust ZONDER `requireSupabaseAuth`-middleware: die gooit een raw `Response`
|
|
48
|
+
// (401/500) die in de browser als onleesbare "[object Response]"-runtimefout met
|
|
49
|
+
// blanco scherm eindigt. We verifiëren de bearer-token hier zelf en geven altijd
|
|
50
|
+
// nette JSON terug, zodat de UI de fout gewoon kan tonen.
|
|
51
|
+
export const syncMailboxNow = createServerFn({ method: "POST" })
|
|
52
|
+
.inputValidator((input) => z.object({ trigger: z.enum(["manual", "realtime"]).default("manual") }).parse(input ?? {}))
|
|
53
|
+
.handler(async ({ data }) => {
|
|
54
|
+
try {
|
|
55
|
+
const { getRequest } = await import("@tanstack/react-start/server");
|
|
56
|
+
const token = getRequest()?.headers?.get("authorization")?.replace(/^Bearer\s+/i, "") ?? "";
|
|
57
|
+
const url = process.env.SUPABASE_URL;
|
|
58
|
+
const key = process.env.SUPABASE_PUBLISHABLE_KEY;
|
|
59
|
+
if (!token || !url || !key) {
|
|
60
|
+
return { ok: false, error: "unauthorized", processed: 0, inserted: 0, errors: 1 };
|
|
61
61
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const receivedAt = pickStr(p, "received_at", "receivedAt", "date", "internalDate") ?? ev.received_at ?? new Date().toISOString();
|
|
69
|
-
const { data: inserted, error: insErr } = await supabaseAdmin
|
|
70
|
-
.from("mailbox_messages")
|
|
71
|
-
.insert({
|
|
72
|
-
external_id: externalId,
|
|
73
|
-
message_id: pickStr(p, "message_id", "messageId") ?? externalId,
|
|
74
|
-
connection_id: connectionId,
|
|
75
|
-
from_email: fromEmail,
|
|
76
|
-
from_name: fromName,
|
|
77
|
-
subject,
|
|
78
|
-
body_text: bodyText,
|
|
79
|
-
body_html: bodyHtml,
|
|
80
|
-
received_at: receivedAt,
|
|
81
|
-
status: "new",
|
|
82
|
-
})
|
|
83
|
-
.select("id")
|
|
84
|
-
.single();
|
|
85
|
-
if (insErr) {
|
|
86
|
-
// Duplicate race → markeer alsnog als consumed
|
|
87
|
-
if (insErr.code === "23505") {
|
|
88
|
-
return { inserted: false, messageId: null };
|
|
62
|
+
const sb = createClient(url, key, {
|
|
63
|
+
auth: { storage: undefined, persistSession: false, autoRefreshToken: false },
|
|
64
|
+
});
|
|
65
|
+
const { data: userData, error: userErr } = await sb.auth.getUser(token);
|
|
66
|
+
if (userErr || !userData?.user) {
|
|
67
|
+
return { ok: false, error: "unauthorized", processed: 0, inserted: 0, errors: 1 };
|
|
89
68
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// Maak direct een leeg voorstel aan (AI staat uit) — match klant op afzender
|
|
93
|
-
await createStubProposal(supabaseAdmin, inserted.id, fromEmail);
|
|
94
|
-
return { inserted: true, messageId: inserted.id };
|
|
95
|
-
}
|
|
96
|
-
async function createStubProposal(supabaseAdmin, messageId, fromEmail) {
|
|
97
|
-
const { data: existing } = await supabaseAdmin
|
|
98
|
-
.from("mailbox_order_proposals").select("id").eq("message_id", messageId).maybeSingle();
|
|
99
|
-
if (existing?.id)
|
|
100
|
-
return;
|
|
101
|
-
const matchResult = await matchCustomerByEmail(supabaseAdmin, fromEmail);
|
|
102
|
-
await supabaseAdmin.from("mailbox_order_proposals").insert({
|
|
103
|
-
message_id: messageId,
|
|
104
|
-
matched_customer_id: matchResult.customerId,
|
|
105
|
-
confidence: matchResult.customerId ? 1 : 0,
|
|
106
|
-
status: "pending",
|
|
107
|
-
parsed_payload: {
|
|
108
|
-
stub: true,
|
|
109
|
-
reason: "ai_disabled",
|
|
110
|
-
auto_matched_customer: matchResult.customerId !== null,
|
|
111
|
-
auto_match_source: matchResult.source,
|
|
112
|
-
auto_match_email: fromEmail,
|
|
113
|
-
auto_match_warning: matchResult.warning,
|
|
114
|
-
auto_match_candidates: matchResult.candidates,
|
|
115
|
-
},
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
// Exact (case-insensitive) match op afzender-e-mailadres.
|
|
119
|
-
// Eerst customers.email, dan customer_contacts.email als fallback.
|
|
120
|
-
// Bij meerdere treffers: geen automatische koppeling, wel waarschuwing.
|
|
121
|
-
async function matchCustomerByEmail(supabaseAdmin, fromEmail) {
|
|
122
|
-
if (!fromEmail) {
|
|
123
|
-
return { customerId: null, source: null, warning: null, candidates: [] };
|
|
124
|
-
}
|
|
125
|
-
const email = fromEmail.trim();
|
|
126
|
-
if (!email) {
|
|
127
|
-
return { customerId: null, source: null, warning: null, candidates: [] };
|
|
128
|
-
}
|
|
129
|
-
// 1) customers.email (case-insensitive exact)
|
|
130
|
-
const { data: directHits } = await supabaseAdmin
|
|
131
|
-
.from("customers")
|
|
132
|
-
.select("id, company_name")
|
|
133
|
-
.ilike("email", email)
|
|
134
|
-
.limit(5);
|
|
135
|
-
const direct = Array.isArray(directHits) ? directHits : [];
|
|
136
|
-
if (direct.length === 1) {
|
|
137
|
-
return {
|
|
138
|
-
customerId: direct[0].id,
|
|
139
|
-
source: "customers",
|
|
140
|
-
warning: null,
|
|
141
|
-
candidates: direct.map((c) => ({ id: c.id, name: c.company_name })),
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
if (direct.length > 1) {
|
|
145
|
-
return {
|
|
146
|
-
customerId: null,
|
|
147
|
-
source: "customers",
|
|
148
|
-
warning: `Meerdere klanten met e-mailadres ${email} — kies handmatig.`,
|
|
149
|
-
candidates: direct.map((c) => ({ id: c.id, name: c.company_name })),
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
// 2) customer_contacts.email (case-insensitive exact)
|
|
153
|
-
const { data: contactHits } = await supabaseAdmin
|
|
154
|
-
.from("customer_contacts")
|
|
155
|
-
.select("customer_id")
|
|
156
|
-
.ilike("email", email)
|
|
157
|
-
.limit(5);
|
|
158
|
-
const contacts = Array.isArray(contactHits) ? contactHits : [];
|
|
159
|
-
const uniqueCustomerIds = Array.from(new Set(contacts.map((c) => c.customer_id).filter(Boolean)));
|
|
160
|
-
if (uniqueCustomerIds.length === 0) {
|
|
161
|
-
return { customerId: null, source: null, warning: null, candidates: [] };
|
|
69
|
+
const { runIngest } = await import("../lib/mailbox-ingest.server");
|
|
70
|
+
return await runIngest(data.trigger);
|
|
162
71
|
}
|
|
163
|
-
|
|
164
|
-
.from("customers")
|
|
165
|
-
.select("id, company_name")
|
|
166
|
-
.in("id", uniqueCustomerIds);
|
|
167
|
-
const cands = (Array.isArray(custRows) ? custRows : []).map((c) => ({
|
|
168
|
-
id: c.id,
|
|
169
|
-
name: c.company_name,
|
|
170
|
-
}));
|
|
171
|
-
if (uniqueCustomerIds.length === 1) {
|
|
72
|
+
catch (e) {
|
|
172
73
|
return {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
74
|
+
ok: false,
|
|
75
|
+
error: String(e?.message ?? e),
|
|
76
|
+
processed: 0,
|
|
77
|
+
inserted: 0,
|
|
78
|
+
errors: 1,
|
|
177
79
|
};
|
|
178
80
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
source: "customer_contacts",
|
|
182
|
-
warning: `Meerdere klanten gevonden via contactpersoon met e-mailadres ${email} — kies handmatig.`,
|
|
183
|
-
candidates: cands,
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
// ─── Ingest één specifiek event (vanuit realtime subscription) ───────────────
|
|
187
|
-
export const ingestEmailEvent = createServerFn({ method: "POST" })
|
|
81
|
+
});
|
|
82
|
+
export const getLastIngestRun = createServerFn({ method: "GET" })
|
|
188
83
|
.middleware([requireSupabaseAuth])
|
|
189
|
-
.
|
|
190
|
-
.handler(async ({ data }) => {
|
|
84
|
+
.handler(async () => {
|
|
191
85
|
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
192
|
-
const { data
|
|
193
|
-
.from("
|
|
194
|
-
.select("id,
|
|
195
|
-
.
|
|
86
|
+
const { data } = await supabaseAdmin
|
|
87
|
+
.from("mailbox_ingest_runs")
|
|
88
|
+
.select("id, trigger, started_at, finished_at, processed, inserted, errors, error_sample")
|
|
89
|
+
.order("started_at", { ascending: false })
|
|
90
|
+
.limit(1)
|
|
196
91
|
.maybeSingle();
|
|
197
|
-
|
|
198
|
-
throw new Error(evErr.message);
|
|
199
|
-
if (!ev)
|
|
200
|
-
return { ok: false, error: "Event niet gevonden" };
|
|
201
|
-
if (ev.consumed_at)
|
|
202
|
-
return { ok: true, inserted: 0, autoProcessed: 0, autoApproved: 0 };
|
|
203
|
-
const res = await ingestSingleEvent(supabaseAdmin, ev.connection_id, ev);
|
|
204
|
-
await supabaseAdmin
|
|
205
|
-
.from("email_auth_events")
|
|
206
|
-
.update({ consumed_at: new Date().toISOString() })
|
|
207
|
-
.eq("id", ev.id);
|
|
208
|
-
// AI-verwerking uitgeschakeld — wacht op externe model-koppeling
|
|
209
|
-
const autoProcessed = 0;
|
|
210
|
-
const autoApproved = 0;
|
|
211
|
-
return { ok: true, inserted: res.inserted ? 1 : 0, autoProcessed, autoApproved };
|
|
92
|
+
return { run: data ?? null };
|
|
212
93
|
});
|
|
213
|
-
|
|
214
|
-
export const
|
|
94
|
+
/** Handmatig één voorstel opnieuw laten analyseren met de huidige AI-agent. */
|
|
95
|
+
export const reparseProposalNow = createServerFn({ method: "POST" })
|
|
215
96
|
.middleware([requireSupabaseAuth])
|
|
216
|
-
.
|
|
97
|
+
.inputValidator((input) => z.object({ proposalId: z.string().uuid() }).parse(input))
|
|
98
|
+
.handler(async ({ data }) => {
|
|
217
99
|
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const { data: events, error: evErr } = await supabaseAdmin
|
|
229
|
-
.from("email_auth_events")
|
|
230
|
-
.select("id, connection_id, external_id, payload, received_at")
|
|
231
|
-
.eq("connection_id", row.id)
|
|
232
|
-
.is("consumed_at", null)
|
|
233
|
-
.order("received_at", { ascending: true })
|
|
234
|
-
.limit(200);
|
|
235
|
-
if (evErr)
|
|
236
|
-
return { ok: false, error: evErr.message };
|
|
237
|
-
let inserted = 0;
|
|
238
|
-
const consumedIds = [];
|
|
239
|
-
for (const ev of events ?? []) {
|
|
240
|
-
try {
|
|
241
|
-
const res = await ingestSingleEvent(supabaseAdmin, ev.connection_id, ev);
|
|
242
|
-
if (res.inserted)
|
|
243
|
-
inserted++;
|
|
244
|
-
consumedIds.push(ev.id);
|
|
245
|
-
}
|
|
246
|
-
catch {
|
|
247
|
-
// Fout → niet markeren als consumed, volgende ronde opnieuw proberen
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
if (consumedIds.length > 0) {
|
|
251
|
-
await supabaseAdmin
|
|
252
|
-
.from("email_auth_events")
|
|
253
|
-
.update({ consumed_at: new Date().toISOString() })
|
|
254
|
-
.in("id", consumedIds);
|
|
255
|
-
}
|
|
256
|
-
// Maak ook stubs voor eerder ontvangen berichten zonder voorstel
|
|
257
|
-
const { data: orphanMsgs } = await supabaseAdmin
|
|
258
|
-
.from("mailbox_messages")
|
|
259
|
-
.select("id, from_email, mailbox_order_proposals(id)")
|
|
260
|
-
.order("received_at", { ascending: false })
|
|
261
|
-
.limit(200);
|
|
262
|
-
let stubbed = 0;
|
|
263
|
-
for (const m of (orphanMsgs ?? [])) {
|
|
264
|
-
if (Array.isArray(m.mailbox_order_proposals) && m.mailbox_order_proposals.length > 0)
|
|
265
|
-
continue;
|
|
266
|
-
await createStubProposal(supabaseAdmin, m.id, m.from_email ?? null);
|
|
267
|
-
stubbed++;
|
|
268
|
-
}
|
|
269
|
-
return { ok: true, inserted: inserted + stubbed, autoProcessed: 0, autoApproved: 0 };
|
|
100
|
+
const { reparseProposal } = await import("../lib/mailbox-ingest.server");
|
|
101
|
+
return reparseProposal(supabaseAdmin, data.proposalId);
|
|
102
|
+
});
|
|
103
|
+
/** Detecteert een gewijzigde AI-agent en herverwerkt openstaande voorstellen. */
|
|
104
|
+
export const refreshProposalsForAgentChange = createServerFn({ method: "POST" })
|
|
105
|
+
.middleware([requireSupabaseAuth])
|
|
106
|
+
.handler(async () => {
|
|
107
|
+
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
108
|
+
const { reparseChangedAgentProposals } = await import("../lib/mailbox-ingest.server");
|
|
109
|
+
return reparseChangedAgentProposals(supabaseAdmin);
|
|
270
110
|
});
|
|
271
111
|
// ─── Messages & Proposals ────────────────────────────────────────────────────
|
|
272
112
|
export const listMessages = createServerFn({ method: "GET" })
|
|
273
113
|
.middleware([requireSupabaseAuth])
|
|
274
|
-
.handler(async (
|
|
275
|
-
const {
|
|
114
|
+
.handler(async () => {
|
|
115
|
+
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
116
|
+
const { data, error } = await supabaseAdmin
|
|
276
117
|
.from("mailbox_messages")
|
|
277
118
|
.select("id, uid, from_email, from_name, subject, received_at, status, error_message, created_at")
|
|
278
119
|
.order("received_at", { ascending: false })
|
|
@@ -286,6 +127,7 @@ export const parseMessage = createServerFn({ method: "POST" })
|
|
|
286
127
|
.inputValidator((input) => z.object({ messageId: z.string().uuid() }).parse(input))
|
|
287
128
|
.handler(async ({ data }) => {
|
|
288
129
|
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
130
|
+
const { createStubProposal } = await import("../lib/mailbox-ingest.server");
|
|
289
131
|
const { data: msg, error: msgErr } = await supabaseAdmin
|
|
290
132
|
.from("mailbox_messages").select("*").eq("id", data.messageId).single();
|
|
291
133
|
if (msgErr || !msg)
|
|
@@ -299,41 +141,41 @@ export const parseMessage = createServerFn({ method: "POST" })
|
|
|
299
141
|
export const listProposals = createServerFn({ method: "GET" })
|
|
300
142
|
.middleware([requireSupabaseAuth])
|
|
301
143
|
.inputValidator((input) => z.object({ status: z.enum(["pending", "approved", "rejected"]).optional() }).parse(input ?? {}))
|
|
302
|
-
.handler(async ({ data
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const activeRow = Array.isArray(active) ? active[0] : null;
|
|
309
|
-
const activeConnectionId = activeRow?.id ?? null;
|
|
310
|
-
if (!activeConnectionId)
|
|
311
|
-
return { items: [] };
|
|
312
|
-
// Stap 1: message-ids van de actieve mailbox
|
|
313
|
-
const { data: msgs, error: msgErr } = await context.supabase
|
|
314
|
-
.from("mailbox_messages")
|
|
315
|
-
.select("id")
|
|
316
|
-
.eq("connection_id", activeConnectionId)
|
|
317
|
-
.order("received_at", { ascending: false })
|
|
318
|
-
.limit(500);
|
|
319
|
-
if (msgErr)
|
|
320
|
-
throw new Error(msgErr.message);
|
|
321
|
-
const messageIds = (msgs ?? []).map((m) => m.id);
|
|
322
|
-
if (messageIds.length === 0)
|
|
323
|
-
return { items: [] };
|
|
324
|
-
// Stap 2: voorstellen op die berichten
|
|
325
|
-
let q = context.supabase
|
|
144
|
+
.handler(async ({ data }) => {
|
|
145
|
+
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
146
|
+
// Alle voorstellen uit de database, ongeacht via welke mailbox ze binnenkwamen.
|
|
147
|
+
// Bewust server-side: mailbox-voorstellen mogen niet rechtstreeks door andere
|
|
148
|
+
// browsermodules als verkooporders worden gelezen.
|
|
149
|
+
let q = supabaseAdmin
|
|
326
150
|
.from("mailbox_order_proposals")
|
|
327
|
-
.select("*, mailbox_messages(subject, from_email, from_name, body_text, received_at, connection_id), mailbox_proposal_lines(*), mailbox_proposal_field_values(*)")
|
|
328
|
-
.in("message_id", messageIds)
|
|
151
|
+
.select("*, mailbox_messages(subject, from_email, from_name, body_text, body_html, received_at, connection_id), mailbox_proposal_lines(*), mailbox_proposal_field_values(*)")
|
|
329
152
|
.order("created_at", { ascending: false })
|
|
330
|
-
.limit(
|
|
153
|
+
.limit(500);
|
|
331
154
|
if (data.status)
|
|
332
155
|
q = q.eq("status", data.status);
|
|
333
156
|
const { data: rows, error } = await q;
|
|
334
157
|
if (error)
|
|
335
158
|
throw new Error(error.message);
|
|
336
|
-
|
|
159
|
+
const items = (Array.isArray(rows) ? rows : []).map((r) => {
|
|
160
|
+
const m = r?.mailbox_messages;
|
|
161
|
+
if (!m)
|
|
162
|
+
return r;
|
|
163
|
+
const { body_html, ...rest } = m;
|
|
164
|
+
return { ...r, mailbox_messages: { ...rest, body_text: emailBodyText(m.body_text, body_html) } };
|
|
165
|
+
});
|
|
166
|
+
return { items };
|
|
167
|
+
});
|
|
168
|
+
// ─── Ladingdragers uit de Logistics Engine ─────────────────────────────────
|
|
169
|
+
// Nooit cachen: de instelling kan op elk moment wijzigen.
|
|
170
|
+
export const listAllowedLoadCarriers = createServerFn({ method: "GET" })
|
|
171
|
+
.middleware([requireSupabaseAuth])
|
|
172
|
+
.handler(async ({ context }) => {
|
|
173
|
+
const { data, error } = await context.supabase.rpc("logistics_allowed_load_carriers", {
|
|
174
|
+
p_strategy_id: undefined,
|
|
175
|
+
});
|
|
176
|
+
if (error)
|
|
177
|
+
return { ...EMPTY_LOAD_CARRIERS, error: error.message };
|
|
178
|
+
return { ...normalizeAllowedLoadCarriers(data), error: null };
|
|
337
179
|
});
|
|
338
180
|
const LineEdit = z.object({
|
|
339
181
|
id: z.string().uuid().optional(),
|
|
@@ -363,12 +205,29 @@ export const approveProposal = createServerFn({ method: "POST" })
|
|
|
363
205
|
(data.fieldValues ?? []).forEach((fv) => {
|
|
364
206
|
customFields[fv.field_key] = fv.value;
|
|
365
207
|
});
|
|
208
|
+
// ─── Ladingdrager: uitsluitend wat de Engine toestaat ───────────────────
|
|
209
|
+
const { data: lcRaw, error: lcErr } = await context.supabase.rpc("logistics_allowed_load_carriers", { p_strategy_id: undefined });
|
|
210
|
+
if (lcErr)
|
|
211
|
+
throw new Error(`Ladingdragers ophalen mislukt: ${lcErr.message}`);
|
|
212
|
+
const lcList = normalizeAllowedLoadCarriers(lcRaw);
|
|
213
|
+
const requestedCarrier = typeof customFields["load_carrier"] === "string" && customFields["load_carrier"]
|
|
214
|
+
? customFields["load_carrier"]
|
|
215
|
+
: null;
|
|
216
|
+
const loadCarrierType = requestedCarrier ?? lcList.default;
|
|
217
|
+
if (!isLoadCarrierAllowed(lcList, loadCarrierType)) {
|
|
218
|
+
throw new Error(`Ladingdrager "${loadCarrierLabel(lcList, loadCarrierType) ?? "onbekend"}" is uitgeschakeld in de Rule Engine — bestelling niet aangemaakt.`);
|
|
219
|
+
}
|
|
220
|
+
customFields["load_carrier"] = loadCarrierType;
|
|
366
221
|
// ─── Validatie eerst mogelijke leverdatum ────────────────────────────────
|
|
367
222
|
const carrierId = typeof customFields["carrier"] === "string" ? customFields["carrier"] : null;
|
|
368
223
|
let carrierDeliveryDays = 0;
|
|
224
|
+
let carrierSchedule = null;
|
|
369
225
|
if (carrierId) {
|
|
370
226
|
const { data: c } = await context.supabase
|
|
371
|
-
.from("carriers")
|
|
227
|
+
.from("carriers")
|
|
228
|
+
.select("delivery_days, pickup_weekdays, delivery_weekdays, pickup_moments")
|
|
229
|
+
.eq("id", carrierId).maybeSingle();
|
|
230
|
+
carrierSchedule = c ?? null;
|
|
372
231
|
carrierDeliveryDays = Math.max(0, Number(c?.delivery_days ?? 0));
|
|
373
232
|
}
|
|
374
233
|
const { data: settings } = await context.supabase
|
|
@@ -377,10 +236,45 @@ export const approveProposal = createServerFn({ method: "POST" })
|
|
|
377
236
|
.order("updated_at", { ascending: false })
|
|
378
237
|
.limit(1).maybeSingle();
|
|
379
238
|
const pickDays = Math.max(0, Number(settings?.pick_days_before_transport ?? 0));
|
|
380
|
-
const
|
|
381
|
-
const earliestISO = toISODate(earliest);
|
|
239
|
+
const earliestISO = computeEarliestDelivery(carrierSchedule, pickDays).iso;
|
|
382
240
|
if (data.deliveryDate < earliestISO) {
|
|
383
|
-
throw new Error(`Leverdatum (${data.deliveryDate}) ligt voor
|
|
241
|
+
throw new Error(`Leverdatum (${data.deliveryDate}) ligt voor het eerstvolgende aanmeldmoment van de vervoerder (${earliestISO}).`);
|
|
242
|
+
}
|
|
243
|
+
// ─── Voorraadcontrole door de Logistics Engine (nooit zelf beslissen) ────
|
|
244
|
+
const checkLines = data.lines
|
|
245
|
+
.filter((l) => l.product_id)
|
|
246
|
+
.map((l) => ({
|
|
247
|
+
productId: l.product_id,
|
|
248
|
+
quantity: Math.max(1, Math.round(l.quantity ?? 1)),
|
|
249
|
+
loadCarrierType,
|
|
250
|
+
}));
|
|
251
|
+
let shortages = [];
|
|
252
|
+
if (checkLines.length > 0) {
|
|
253
|
+
const { data: check, error: checkErr } = await context.supabase.rpc("logistics_check_order_allowed", { p_lines: checkLines });
|
|
254
|
+
if (checkErr)
|
|
255
|
+
throw new Error(`Voorraadcontrole mislukt: ${checkErr.message}`);
|
|
256
|
+
const result = (check ?? {});
|
|
257
|
+
shortages = Array.isArray(result.shortages) ? result.shortages : [];
|
|
258
|
+
if (result.allowed === false) {
|
|
259
|
+
const detail = shortages
|
|
260
|
+
.map((s) => `${s.product ?? s.productId}: besteld ${s.wanted}, beschikbaar ${s.available}, tekort ${s.missing}`)
|
|
261
|
+
.join("\n");
|
|
262
|
+
const blockText = `Geblokkeerd - onvoldoende voorraad\n${detail}`;
|
|
263
|
+
await supabaseAdmin
|
|
264
|
+
.from("mailbox_order_proposals")
|
|
265
|
+
.update({ notes: blockText })
|
|
266
|
+
.eq("id", data.proposalId);
|
|
267
|
+
const { data: blockedProp } = await supabaseAdmin
|
|
268
|
+
.from("mailbox_order_proposals")
|
|
269
|
+
.select("message_id").eq("id", data.proposalId).maybeSingle();
|
|
270
|
+
if (blockedProp?.message_id) {
|
|
271
|
+
await supabaseAdmin
|
|
272
|
+
.from("mailbox_messages")
|
|
273
|
+
.update({ status: "blocked_insufficient_stock" })
|
|
274
|
+
.eq("id", blockedProp.message_id);
|
|
275
|
+
}
|
|
276
|
+
throw new Error(blockText);
|
|
277
|
+
}
|
|
384
278
|
}
|
|
385
279
|
const orderNumber = `MB-${Date.now()}`;
|
|
386
280
|
const { data: order, error: orderErr } = await supabaseAdmin
|
|
@@ -429,7 +323,26 @@ export const approveProposal = createServerFn({ method: "POST" })
|
|
|
429
323
|
if (prop?.message_id) {
|
|
430
324
|
await supabaseAdmin.from("mailbox_messages").update({ status: "converted" }).eq("id", prop.message_id);
|
|
431
325
|
}
|
|
432
|
-
|
|
326
|
+
// ─── Gebeurtenis melden aan de Logistics Engine ─────────────────────────
|
|
327
|
+
const deliveryLocationRef = typeof customFields["delivery_location"] === "string" ? customFields["delivery_location"] : null;
|
|
328
|
+
await context.supabase.rpc("logistics_emit_event", {
|
|
329
|
+
p_source_module: "mailbox",
|
|
330
|
+
p_event_type: "order.received",
|
|
331
|
+
p_order_ref: orderNumber,
|
|
332
|
+
p_payload: {
|
|
333
|
+
checkinAt: data.deliveryDate,
|
|
334
|
+
pickupAt: data.deliveryDate,
|
|
335
|
+
customerRef: data.customerId,
|
|
336
|
+
deliveryLocationRef,
|
|
337
|
+
bufferRef: null,
|
|
338
|
+
loadCarrierType,
|
|
339
|
+
loadCarriers: [loadCarrierType],
|
|
340
|
+
ruleVersion: null,
|
|
341
|
+
},
|
|
342
|
+
p_shadow: true,
|
|
343
|
+
p_idempotency_key: `mailbox:${orderNumber}:order.received:v1`,
|
|
344
|
+
});
|
|
345
|
+
return { ok: true, orderId: order.id, orderNumber, shortages };
|
|
433
346
|
});
|
|
434
347
|
export const rejectProposal = createServerFn({ method: "POST" })
|
|
435
348
|
.middleware([requireSupabaseAuth])
|
|
@@ -464,13 +377,107 @@ export const listCustomersForPicker = createServerFn({ method: "GET" })
|
|
|
464
377
|
.from("customers").select("id, company_name, email").eq("is_active", true).order("company_name").range(0, 4999);
|
|
465
378
|
return { items: data ?? [] };
|
|
466
379
|
});
|
|
467
|
-
// Producten voor de picker —
|
|
468
|
-
//
|
|
469
|
-
//
|
|
380
|
+
// Producten voor de picker — ALLE artikelen uit de voorraad:
|
|
381
|
+
// 1. alles in `products`
|
|
382
|
+
// 2. elk handelsitem uit `floriday_trade_items_cache` (Voorraad Module) dat nog
|
|
383
|
+
// geen products-rij heeft, wordt hier automatisch aangemaakt (id = floriday_id)
|
|
384
|
+
// zodat bestelregels, belading en orders er gewoon naar kunnen verwijzen
|
|
385
|
+
// Voorraad komt uit `trade_item_stock` en `trade_item_stock_view`.
|
|
470
386
|
export const listProductsForPicker = createServerFn({ method: "GET" })
|
|
471
387
|
.middleware([requireSupabaseAuth])
|
|
472
388
|
.handler(async ({ context }) => {
|
|
473
|
-
const {
|
|
389
|
+
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
390
|
+
// ── 1. Handelsitems uit de Voorraad Module materialiseren ───────────────
|
|
391
|
+
const { data: cacheRows } = await supabaseAdmin
|
|
392
|
+
.from("floriday_trade_items_cache")
|
|
393
|
+
.select("floriday_id, is_deleted, data")
|
|
394
|
+
.range(0, 4999);
|
|
395
|
+
const tradeItems = (Array.isArray(cacheRows) ? cacheRows : []).filter((r) => !r?.is_deleted && typeof r?.floriday_id === "string");
|
|
396
|
+
const { data: existingRows } = await supabaseAdmin
|
|
397
|
+
.from("products")
|
|
398
|
+
.select("id, custom_fields, floriday_trade_item_id")
|
|
399
|
+
.range(0, 9999);
|
|
400
|
+
const existing = Array.isArray(existingRows) ? existingRows : [];
|
|
401
|
+
const known = new Set();
|
|
402
|
+
for (const p of existing) {
|
|
403
|
+
known.add(String(p.id));
|
|
404
|
+
const fid = p?.custom_fields?.floriday_id ?? p?.floriday_trade_item_id;
|
|
405
|
+
if (typeof fid === "string" && fid)
|
|
406
|
+
known.add(fid);
|
|
407
|
+
}
|
|
408
|
+
const norm = (v) => Math.max(1, Math.round(Number(v ?? 1)) || 1);
|
|
409
|
+
const nameOf = (d) => {
|
|
410
|
+
const n = d?.tradeItemName;
|
|
411
|
+
if (typeof n === "string" && n.trim())
|
|
412
|
+
return n.trim();
|
|
413
|
+
if (n && typeof n === "object") {
|
|
414
|
+
const v = n.nl ?? n.en ?? Object.values(n).find((x) => typeof x === "string");
|
|
415
|
+
if (typeof v === "string" && v.trim())
|
|
416
|
+
return v.trim();
|
|
417
|
+
}
|
|
418
|
+
return typeof d?.tradeItemReference === "string" && d.tradeItemReference
|
|
419
|
+
? d.tradeItemReference
|
|
420
|
+
: "Onbekend artikel";
|
|
421
|
+
};
|
|
422
|
+
const missing = tradeItems.filter((r) => !known.has(r.floriday_id));
|
|
423
|
+
const buildRow = (r) => {
|
|
424
|
+
const d = r.data ?? {};
|
|
425
|
+
const pk = Array.isArray(d.packingConfigurations) ? d.packingConfigurations[0] ?? {} : {};
|
|
426
|
+
return {
|
|
427
|
+
id: r.floriday_id,
|
|
428
|
+
product: nameOf(d),
|
|
429
|
+
// batch + location zijn NOT NULL zonder default in de Voorraad Module
|
|
430
|
+
batch: "",
|
|
431
|
+
location: "",
|
|
432
|
+
unit: "stuks",
|
|
433
|
+
quantity: 0,
|
|
434
|
+
plants_per_crate: norm(pk?.piecesPerPackage ?? pk?.unitsPerPackage),
|
|
435
|
+
crates_per_layer: norm(pk?.packagesPerLayer),
|
|
436
|
+
layers_per_cart: norm(pk?.layersPerLoadCarrier),
|
|
437
|
+
floriday_trade_item_id: r.floriday_id,
|
|
438
|
+
article_number: typeof d?.supplierArticleCode === "string" ? d.supplierArticleCode : null,
|
|
439
|
+
custom_fields: { floriday_id: r.floriday_id, source: "floriday_trade_items_cache" },
|
|
440
|
+
};
|
|
441
|
+
};
|
|
442
|
+
// Concept-artikelen uit de Voorraad Module (nog niet in Floriday) ook tonen
|
|
443
|
+
const { data: draftRowsAll } = await supabaseAdmin
|
|
444
|
+
.from("trade_item_drafts")
|
|
445
|
+
.select("id, data, floriday_id")
|
|
446
|
+
.range(0, 4999);
|
|
447
|
+
const draftMissing = (Array.isArray(draftRowsAll) ? draftRowsAll : []).filter((d) => typeof d?.id === "string" && !known.has(d.id) &&
|
|
448
|
+
!(typeof d?.floriday_id === "string" && known.has(d.floriday_id)));
|
|
449
|
+
const buildDraftRow = (r) => {
|
|
450
|
+
const d = r.data ?? {};
|
|
451
|
+
const pk = Array.isArray(d.packingConfigurations) ? d.packingConfigurations[0] ?? {} : {};
|
|
452
|
+
return {
|
|
453
|
+
id: r.id,
|
|
454
|
+
product: nameOf(d),
|
|
455
|
+
batch: "",
|
|
456
|
+
location: "",
|
|
457
|
+
unit: "stuks",
|
|
458
|
+
quantity: 0,
|
|
459
|
+
plants_per_crate: norm(pk?.unitsPerPackage ?? pk?.piecesPerPackage),
|
|
460
|
+
crates_per_layer: norm(pk?.packagesPerLayer),
|
|
461
|
+
layers_per_cart: norm(pk?.layersPerLoadCarrier),
|
|
462
|
+
article_number: typeof d?.supplierArticleCode === "string" ? d.supplierArticleCode : null,
|
|
463
|
+
custom_fields: { draft_id: r.id, source: "trade_item_drafts" },
|
|
464
|
+
};
|
|
465
|
+
};
|
|
466
|
+
const toInsert = [...missing.map(buildRow), ...draftMissing.map(buildDraftRow)];
|
|
467
|
+
// Artikelen die (nog) niet in `products` staan, tonen we hoe dan ook —
|
|
468
|
+
// ook als materialiseren faalt.
|
|
469
|
+
let virtualRows = [];
|
|
470
|
+
if (toInsert.length > 0) {
|
|
471
|
+
const { error: upsertError } = await supabaseAdmin
|
|
472
|
+
.from("products")
|
|
473
|
+
.upsert(toInsert, { onConflict: "id" });
|
|
474
|
+
if (upsertError) {
|
|
475
|
+
console.error("[listProductsForPicker] materialiseren mislukt:", upsertError.message);
|
|
476
|
+
virtualRows = toInsert;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
// ── 2. Voorraad verzamelen ──────────────────────────────────────────────
|
|
480
|
+
const { data: stockRows } = await supabaseAdmin
|
|
474
481
|
.from("trade_item_stock")
|
|
475
482
|
.select("trade_item_key, physical_stock, reserved_stock");
|
|
476
483
|
const availByProductId = new Map();
|
|
@@ -499,12 +506,14 @@ export const listProductsForPicker = createServerFn({ method: "GET" })
|
|
|
499
506
|
}
|
|
500
507
|
// Drafts uit de Voorraad Module: koppel via linkedProductId → products.id
|
|
501
508
|
if (availByDraftId.size > 0) {
|
|
502
|
-
const { data: drafts } = await
|
|
509
|
+
const { data: drafts } = await supabaseAdmin
|
|
503
510
|
.from("trade_item_drafts")
|
|
504
511
|
.select("id, data, floriday_id")
|
|
505
512
|
.in("id", Array.from(availByDraftId.keys()));
|
|
506
513
|
for (const d of Array.isArray(drafts) ? drafts : []) {
|
|
507
514
|
const qty = availByDraftId.get(d.id) ?? 0;
|
|
515
|
+
// Concepten worden ook als product gematerialiseerd op hun eigen id
|
|
516
|
+
addAvail(availByProductId, d.id, qty);
|
|
508
517
|
const linked = d?.data?.linkedProductId;
|
|
509
518
|
if (typeof linked === "string" && linked)
|
|
510
519
|
addAvail(availByProductId, linked, qty);
|
|
@@ -513,19 +522,76 @@ export const listProductsForPicker = createServerFn({ method: "GET" })
|
|
|
513
522
|
addAvail(availByFloridayId, fid, qty);
|
|
514
523
|
}
|
|
515
524
|
}
|
|
516
|
-
|
|
525
|
+
// Voorraadoverzicht van de Voorraad Module (view)
|
|
526
|
+
try {
|
|
527
|
+
const { data: viewRows } = await supabaseAdmin
|
|
528
|
+
.from("trade_item_stock_view")
|
|
529
|
+
.select("trade_item_id, stock");
|
|
530
|
+
for (const v of Array.isArray(viewRows) ? viewRows : []) {
|
|
531
|
+
const id = v?.trade_item_id;
|
|
532
|
+
if (typeof id !== "string")
|
|
533
|
+
continue;
|
|
534
|
+
if (!availByFloridayId.has(id) && !availByProductId.has(id)) {
|
|
535
|
+
availByFloridayId.set(id, Number(v?.stock ?? 0));
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
catch {
|
|
540
|
+
// view optioneel
|
|
541
|
+
}
|
|
542
|
+
// ── 3. Alle producten teruggeven ────────────────────────────────────────
|
|
543
|
+
const { data } = await supabaseAdmin
|
|
517
544
|
.from("products")
|
|
518
|
-
.select("id, product, unit, plants_per_crate, crates_per_layer, layers_per_cart, custom_fields")
|
|
519
|
-
.order("product")
|
|
520
|
-
|
|
545
|
+
.select("id, product, unit, plants_per_crate, crates_per_layer, layers_per_cart, custom_fields, floriday_trade_item_id")
|
|
546
|
+
.order("product")
|
|
547
|
+
.range(0, 9999);
|
|
548
|
+
const dbRows = Array.isArray(data) ? data : [];
|
|
549
|
+
const seen = new Set(dbRows.map((p) => String(p.id)));
|
|
550
|
+
const allRows = [
|
|
551
|
+
...dbRows,
|
|
552
|
+
...virtualRows.filter((v) => !seen.has(String(v.id))),
|
|
553
|
+
];
|
|
554
|
+
const items = allRows
|
|
521
555
|
.map((p) => {
|
|
522
|
-
const fid = p?.custom_fields?.floriday_id;
|
|
556
|
+
const fid = p?.custom_fields?.floriday_id ?? p?.floriday_trade_item_id;
|
|
523
557
|
const byProd = availByProductId.get(p.id) ?? 0;
|
|
524
558
|
const byFid = typeof fid === "string" ? (availByFloridayId.get(fid) ?? 0) : 0;
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
559
|
+
return { ...p, available_stock: byProd + byFid };
|
|
560
|
+
})
|
|
561
|
+
.sort((a, b) => String(a.product ?? "").localeCompare(String(b.product ?? "")));
|
|
562
|
+
// ── 4. Rule Engine: mag er doorverkocht worden zonder voorraad? ─────────
|
|
563
|
+
// `logistics_rule_configs.settings.order_verwerking.fields.allow_backorder`
|
|
564
|
+
// bepaalt of artikelen zonder voorraad selecteerbaar blijven. Er is niet
|
|
565
|
+
// altijd een config met is_active = true; val dan terug op de laatst
|
|
566
|
+
// bijgewerkte werkwijze zodat de instelling toch wordt gevolgd.
|
|
567
|
+
let allowBackorder = false;
|
|
568
|
+
try {
|
|
569
|
+
const readFlag = (rows) => {
|
|
570
|
+
const cfg = (Array.isArray(rows) ? rows : [])[0];
|
|
571
|
+
const raw = cfg?.settings?.order_verwerking?.fields?.allow_backorder;
|
|
572
|
+
return raw === undefined ? undefined : raw === true || raw === "true";
|
|
573
|
+
};
|
|
574
|
+
const { data: activeRows } = await supabaseAdmin
|
|
575
|
+
.from("logistics_rule_configs")
|
|
576
|
+
.select("settings, is_active, updated_at")
|
|
577
|
+
.eq("is_active", true)
|
|
578
|
+
.order("updated_at", { ascending: false })
|
|
579
|
+
.limit(1);
|
|
580
|
+
let flag = readFlag(activeRows);
|
|
581
|
+
if (flag === undefined) {
|
|
582
|
+
const { data: latestRows } = await supabaseAdmin
|
|
583
|
+
.from("logistics_rule_configs")
|
|
584
|
+
.select("settings, updated_at")
|
|
585
|
+
.order("updated_at", { ascending: false })
|
|
586
|
+
.limit(1);
|
|
587
|
+
flag = readFlag(latestRows);
|
|
588
|
+
}
|
|
589
|
+
allowBackorder = flag === true;
|
|
590
|
+
}
|
|
591
|
+
catch (e) {
|
|
592
|
+
console.error("[listProductsForPicker] rule engine lezen mislukt:", e);
|
|
593
|
+
}
|
|
594
|
+
return { items, allowBackorder };
|
|
529
595
|
});
|
|
530
596
|
export const planAndSaveAutoCarts = createServerFn({ method: "POST" })
|
|
531
597
|
.middleware([requireSupabaseAuth])
|
|
@@ -731,9 +797,14 @@ export const listPickupLocations = createServerFn({ method: "GET" })
|
|
|
731
797
|
const label = nameRaw.length > 0
|
|
732
798
|
? nameRaw
|
|
733
799
|
: [addr, city].filter(Boolean).join(", ") || "(naamloos)";
|
|
734
|
-
return {
|
|
800
|
+
return {
|
|
801
|
+
id: r.floriday_id,
|
|
802
|
+
name: label,
|
|
803
|
+
is_default: r?.data?.isDefault === true,
|
|
804
|
+
sort_order: 0,
|
|
805
|
+
};
|
|
735
806
|
})
|
|
736
|
-
.sort((a, b) => a.name.localeCompare(b.name, "nl"));
|
|
807
|
+
.sort((a, b) => (a.is_default === b.is_default ? a.name.localeCompare(b.name, "nl") : a.is_default ? -1 : 1));
|
|
737
808
|
return { items };
|
|
738
809
|
});
|
|
739
810
|
export const listCarriersForPicker = createServerFn({ method: "GET" })
|
|
@@ -741,7 +812,7 @@ export const listCarriersForPicker = createServerFn({ method: "GET" })
|
|
|
741
812
|
.handler(async ({ context }) => {
|
|
742
813
|
const { data } = await context.supabase
|
|
743
814
|
.from("carriers")
|
|
744
|
-
.select("id, name, is_default, sort_order, delivery_days")
|
|
815
|
+
.select("id, name, is_default, sort_order, delivery_days, pickup_weekdays, delivery_weekdays, pickup_moments")
|
|
745
816
|
.order("sort_order", { ascending: true, nullsFirst: false })
|
|
746
817
|
.order("name");
|
|
747
818
|
return { items: data ?? [] };
|
|
@@ -766,3 +837,206 @@ function addCalendarDays(base, n) {
|
|
|
766
837
|
function toISODate(d) {
|
|
767
838
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
768
839
|
}
|
|
840
|
+
// ─── Beladingen per product ────────────────────────────────────────────────
|
|
841
|
+
// Zelfde bronnen als de Floriday Verkoop-module: packing-configuraties uit de
|
|
842
|
+
// trade item cache, concept-artikelen, groepsleden en de products-tabel
|
|
843
|
+
// (inclusief de opgeslagen Floriday-payload). Daarnaast worden ALLE in het
|
|
844
|
+
// systeem bekende beladingen meegegeven, ook de niet-gekoppelde.
|
|
845
|
+
export const listBeladingOptionsForProducts = createServerFn({ method: "POST" })
|
|
846
|
+
.middleware([requireSupabaseAuth])
|
|
847
|
+
.inputValidator((input) => z.object({ productIds: z.array(z.string()).max(200) }).parse(input))
|
|
848
|
+
.handler(async ({ data }) => {
|
|
849
|
+
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
850
|
+
const ids = Array.from(new Set((data.productIds ?? []).filter(Boolean)));
|
|
851
|
+
const byProduct = {};
|
|
852
|
+
function sortCombos(list) {
|
|
853
|
+
return [...list]
|
|
854
|
+
.sort((a, b) => a.plants_per_crate - b.plants_per_crate ||
|
|
855
|
+
a.crates_per_layer - b.crates_per_layer ||
|
|
856
|
+
a.layers_per_cart - b.layers_per_cart)
|
|
857
|
+
.map((v) => ({
|
|
858
|
+
...v,
|
|
859
|
+
crates_per_cart: v.crates_per_layer * v.layers_per_cart,
|
|
860
|
+
plants_per_cart: v.plants_per_crate * v.crates_per_layer * v.layers_per_cart,
|
|
861
|
+
}));
|
|
862
|
+
}
|
|
863
|
+
// ── Namen van fusten (VBN-verpakkingscodes) en karren (load carriers) ───
|
|
864
|
+
const CARRIER_NAMES = {
|
|
865
|
+
DANISH_TROLLEY: "Deense container",
|
|
866
|
+
DANISH_CONTAINER: "Deense container",
|
|
867
|
+
DANISH_TROLLEY_NON_CC_RFID: "Deense container (non-CC RFID)",
|
|
868
|
+
DANISH_TROLLEY_WATER_TRAY: "Deense container met waterschaal",
|
|
869
|
+
ONE_THIRD_DANISH_TROLLEY: "1/3 Deense container",
|
|
870
|
+
AUCTION_TROLLEY: "Veilingstapelwagen",
|
|
871
|
+
EURO_TROLLEY: "Euro-stapelwagen",
|
|
872
|
+
CC_CONTAINER: "CC-container",
|
|
873
|
+
PALLET: "Pallet",
|
|
874
|
+
EURO_PALLET: "Europallet",
|
|
875
|
+
BOX: "Doos",
|
|
876
|
+
NONE: "Losse fusten",
|
|
877
|
+
};
|
|
878
|
+
const { data: floricodeRows } = await supabaseAdmin
|
|
879
|
+
.from("floricode_records")
|
|
880
|
+
.select("data")
|
|
881
|
+
.eq("resource", "LoadCarrier")
|
|
882
|
+
.range(0, 4999);
|
|
883
|
+
const crateNameByCode = new Map();
|
|
884
|
+
for (const r of Array.isArray(floricodeRows) ? floricodeRows : []) {
|
|
885
|
+
const d = r?.data;
|
|
886
|
+
if (d?.code == null)
|
|
887
|
+
continue;
|
|
888
|
+
const descr = typeof d?.description === "string" ? d.description.trim() : "";
|
|
889
|
+
if (descr)
|
|
890
|
+
crateNameByCode.set(String(d.code), descr);
|
|
891
|
+
}
|
|
892
|
+
const crateInfo = (p) => {
|
|
893
|
+
const raw = p?.package?.vbnPackageCode ?? p?.vbnPackageCode ?? p?.package?.customPackageId ?? null;
|
|
894
|
+
const code = raw == null || raw === "" ? null : String(raw);
|
|
895
|
+
return { code, name: code ? crateNameByCode.get(code) ?? `Fust ${code}` : null };
|
|
896
|
+
};
|
|
897
|
+
const carrierInfo = (p) => {
|
|
898
|
+
const raw = typeof p?.loadCarrier === "string" ? p.loadCarrier : (p?.loadCarrier?.type ?? null);
|
|
899
|
+
const code = typeof raw === "string" && raw ? raw : null;
|
|
900
|
+
return { code, name: code ? CARRIER_NAMES[code.toUpperCase()] ?? code : null };
|
|
901
|
+
};
|
|
902
|
+
const norm = (v) => Math.max(1, Math.round(Number(v ?? 1)) || 1);
|
|
903
|
+
const emptyMeta = { crate_code: null, crate_name: null, carrier_code: null, carrier_name: null };
|
|
904
|
+
const sameCombo = (x, a, b, c, m) => x.plants_per_crate === a && x.crates_per_layer === b && x.layers_per_cart === c &&
|
|
905
|
+
(x.crate_code ?? null) === (m.crate_code ?? null) &&
|
|
906
|
+
(x.carrier_code ?? null) === (m.carrier_code ?? null);
|
|
907
|
+
const packsById = new Map();
|
|
908
|
+
const push = (key, ppc, cpl, lpc, isDefault, meta = emptyMeta) => {
|
|
909
|
+
const a = norm(ppc), b = norm(cpl), c = norm(lpc);
|
|
910
|
+
const arr = packsById.get(key) ?? [];
|
|
911
|
+
const hit = arr.find((x) => sameCombo(x, a, b, c, meta));
|
|
912
|
+
if (hit) {
|
|
913
|
+
if (isDefault)
|
|
914
|
+
hit.is_default = true;
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
arr.push({ plants_per_crate: a, crates_per_layer: b, layers_per_cart: c, is_default: isDefault, ...meta });
|
|
918
|
+
packsById.set(key, arr);
|
|
919
|
+
};
|
|
920
|
+
const pushPack = (key, p, ppc, isDefault) => {
|
|
921
|
+
const cr = crateInfo(p), ca = carrierInfo(p);
|
|
922
|
+
push(key, ppc, p?.packagesPerLayer, p?.layersPerLoadCarrier, isDefault, {
|
|
923
|
+
crate_code: cr.code, crate_name: cr.name, carrier_code: ca.code, carrier_name: ca.name,
|
|
924
|
+
});
|
|
925
|
+
};
|
|
926
|
+
// ── Alle beladingen in het systeem (ook niet-gekoppeld) ─────────────────
|
|
927
|
+
const globalCombos = new Map();
|
|
928
|
+
const addGlobal = (ppc, cpl, lpc, meta = emptyMeta) => {
|
|
929
|
+
const a = norm(ppc), b = norm(cpl), c = norm(lpc);
|
|
930
|
+
if (a === 1 && b === 1 && c === 1)
|
|
931
|
+
return;
|
|
932
|
+
const k = `${a}x${b}x${c}|${meta.crate_code ?? ""}|${meta.carrier_code ?? ""}`;
|
|
933
|
+
if (globalCombos.has(k))
|
|
934
|
+
return;
|
|
935
|
+
globalCombos.set(k, { plants_per_crate: a, crates_per_layer: b, layers_per_cart: c, is_default: false, ...meta });
|
|
936
|
+
};
|
|
937
|
+
const addGlobalPack = (p, ppc) => {
|
|
938
|
+
const cr = crateInfo(p), ca = carrierInfo(p);
|
|
939
|
+
addGlobal(ppc, p?.packagesPerLayer, p?.layersPerLoadCarrier, {
|
|
940
|
+
crate_code: cr.code, crate_name: cr.name, carrier_code: ca.code, carrier_name: ca.name,
|
|
941
|
+
});
|
|
942
|
+
};
|
|
943
|
+
const addAllPackings = (d, fn) => {
|
|
944
|
+
const packs = Array.isArray(d?.packingConfigurations) ? d.packingConfigurations : [];
|
|
945
|
+
for (const p of packs)
|
|
946
|
+
fn(p);
|
|
947
|
+
};
|
|
948
|
+
const [allTiRes, allDraftRes, allProdRes] = await Promise.all([
|
|
949
|
+
supabaseAdmin.from("floriday_trade_items_cache").select("data").range(0, 4999),
|
|
950
|
+
supabaseAdmin.from("trade_item_drafts").select("data").range(0, 4999),
|
|
951
|
+
supabaseAdmin
|
|
952
|
+
.from("products")
|
|
953
|
+
.select("plants_per_crate, crates_per_layer, layers_per_cart, custom_fields")
|
|
954
|
+
.range(0, 9999),
|
|
955
|
+
]);
|
|
956
|
+
for (const r of Array.isArray(allTiRes.data) ? allTiRes.data : []) {
|
|
957
|
+
addAllPackings(r?.data, (p) => addGlobalPack(p, p?.piecesPerPackage ?? p?.unitsPerPackage));
|
|
958
|
+
}
|
|
959
|
+
for (const r of Array.isArray(allDraftRes.data) ? allDraftRes.data : []) {
|
|
960
|
+
addAllPackings(r?.data, (p) => addGlobalPack(p, p?.unitsPerPackage ?? p?.piecesPerPackage));
|
|
961
|
+
}
|
|
962
|
+
for (const p of Array.isArray(allProdRes.data) ? allProdRes.data : []) {
|
|
963
|
+
addGlobal(p.plants_per_crate, p.crates_per_layer, p.layers_per_cart);
|
|
964
|
+
addAllPackings(p?.custom_fields?.floriday_payload, (pc) => addGlobalPack(pc, pc?.unitsPerPackage ?? pc?.piecesPerPackage));
|
|
965
|
+
}
|
|
966
|
+
// Verwijder "kale" varianten (zonder fust-/kartype) als dezelfde afmetingen
|
|
967
|
+
// ook met fust- en kartype bekend zijn — anders zie je dubbele regels.
|
|
968
|
+
const dims = (c) => `${c.plants_per_crate}x${c.crates_per_layer}x${c.layers_per_cart}`;
|
|
969
|
+
const dropBare = (list) => {
|
|
970
|
+
const rich = new Set(list.filter((c) => c.crate_code || c.carrier_code).map(dims));
|
|
971
|
+
return list.filter((c) => c.crate_code || c.carrier_code || !rich.has(dims(c)));
|
|
972
|
+
};
|
|
973
|
+
const globalList = dropBare(Array.from(globalCombos.values()));
|
|
974
|
+
if (!ids.length)
|
|
975
|
+
return { byProduct, allBeladingen: sortCombos(globalList) };
|
|
976
|
+
// ── Gekoppelde beladingen per product ───────────────────────────────────
|
|
977
|
+
const { data: giRows } = await supabaseAdmin
|
|
978
|
+
.from("product_group_items")
|
|
979
|
+
.select("product_group_id, trade_item_id")
|
|
980
|
+
.in("product_group_id", ids);
|
|
981
|
+
const membersByGroup = new Map();
|
|
982
|
+
for (const gi of Array.isArray(giRows) ? giRows : []) {
|
|
983
|
+
const gid = gi.product_group_id;
|
|
984
|
+
const arr = membersByGroup.get(gid) ?? [];
|
|
985
|
+
arr.push(gi.trade_item_id);
|
|
986
|
+
membersByGroup.set(gid, arr);
|
|
987
|
+
}
|
|
988
|
+
const lookupIds = Array.from(new Set([...ids, ...Array.from(membersByGroup.values()).flat()]));
|
|
989
|
+
const [tiRes, draftRes, prodRes] = await Promise.all([
|
|
990
|
+
supabaseAdmin.from("floriday_trade_items_cache").select("floriday_id, data").in("floriday_id", lookupIds),
|
|
991
|
+
supabaseAdmin.from("trade_item_drafts").select("id, data").in("id", lookupIds),
|
|
992
|
+
supabaseAdmin
|
|
993
|
+
.from("products")
|
|
994
|
+
.select("id, plants_per_crate, crates_per_layer, layers_per_cart, custom_fields, trade_item_key")
|
|
995
|
+
.in("id", lookupIds),
|
|
996
|
+
]);
|
|
997
|
+
for (const r of Array.isArray(tiRes.data) ? tiRes.data : []) {
|
|
998
|
+
const key = r.floriday_id;
|
|
999
|
+
addAllPackings(r?.data, (p) => pushPack(key, p, p?.piecesPerPackage ?? p?.unitsPerPackage, !!(p?.primary || p?.isPrimary)));
|
|
1000
|
+
}
|
|
1001
|
+
for (const r of Array.isArray(draftRes.data) ? draftRes.data : []) {
|
|
1002
|
+
const key = r.id;
|
|
1003
|
+
addAllPackings(r?.data, (p) => pushPack(key, p, p?.unitsPerPackage ?? p?.piecesPerPackage, !!(p?.primary || p?.isPrimary)));
|
|
1004
|
+
}
|
|
1005
|
+
for (const p of Array.isArray(prodRes.data) ? prodRes.data : []) {
|
|
1006
|
+
const key = p.id;
|
|
1007
|
+
push(key, p.plants_per_crate, p.crates_per_layer, p.layers_per_cart, true);
|
|
1008
|
+
const fid = p.custom_fields?.floriday_id;
|
|
1009
|
+
if (fid)
|
|
1010
|
+
push(fid, p.plants_per_crate, p.crates_per_layer, p.layers_per_cart, false);
|
|
1011
|
+
const tk = p.trade_item_key;
|
|
1012
|
+
if (typeof tk === "string" && tk.startsWith("group:")) {
|
|
1013
|
+
push(tk.slice("group:".length), p.plants_per_crate, p.crates_per_layer, p.layers_per_cart, true);
|
|
1014
|
+
}
|
|
1015
|
+
addAllPackings(p?.custom_fields?.floriday_payload, (pc) => {
|
|
1016
|
+
const isPrim = !!(pc?.isPrimary || pc?.primary);
|
|
1017
|
+
const ppc = pc?.unitsPerPackage ?? pc?.piecesPerPackage;
|
|
1018
|
+
pushPack(key, pc, ppc, isPrim);
|
|
1019
|
+
if (fid)
|
|
1020
|
+
pushPack(fid, pc, ppc, isPrim);
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
for (const id of ids) {
|
|
1024
|
+
const own = packsById.get(id) ?? [];
|
|
1025
|
+
const memberCombos = [];
|
|
1026
|
+
for (const m of membersByGroup.get(id) ?? []) {
|
|
1027
|
+
for (const c of packsById.get(m) ?? [])
|
|
1028
|
+
memberCombos.push(c);
|
|
1029
|
+
}
|
|
1030
|
+
const merged = [];
|
|
1031
|
+
for (const c of [...own, ...memberCombos, ...globalList]) {
|
|
1032
|
+
if (merged.some((x) => sameCombo(x, c.plants_per_crate, c.crates_per_layer, c.layers_per_cart, c)))
|
|
1033
|
+
continue;
|
|
1034
|
+
merged.push({ ...c, is_default: c.is_default && own.includes(c) });
|
|
1035
|
+
}
|
|
1036
|
+
const cleaned = dropBare(merged);
|
|
1037
|
+
if (cleaned.length && !cleaned.some((x) => x.is_default))
|
|
1038
|
+
cleaned[0].is_default = true;
|
|
1039
|
+
byProduct[id] = sortCombos(cleaned);
|
|
1040
|
+
}
|
|
1041
|
+
return { byProduct, allBeladingen: sortCombos(globalList) };
|
|
1042
|
+
});
|