@flowselections/mailbox-orders 1.0.2 → 1.0.4
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/CartAssignmentWizard.d.ts +6 -0
- package/dist-lib/components/CartAssignmentWizard.d.ts.map +1 -0
- package/dist-lib/components/CartAssignmentWizard.js +199 -0
- package/dist-lib/components/MailboxInboxPage.d.ts.map +1 -1
- package/dist-lib/components/MailboxInboxPage.js +78 -19
- package/dist-lib/components/MailboxProcessedPage.d.ts.map +1 -1
- package/dist-lib/components/MailboxProcessedPage.js +25 -4
- package/dist-lib/components/MailboxProposalsPage.d.ts.map +1 -1
- package/dist-lib/components/MailboxProposalsPage.js +272 -34
- package/dist-lib/components/SearchableSelect.d.ts +2 -1
- package/dist-lib/components/SearchableSelect.d.ts.map +1 -1
- package/dist-lib/components/SearchableSelect.js +4 -3
- package/dist-lib/index.d.ts +0 -1
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +3 -7
- package/dist-lib/integrations/supabase/auth-attacher.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/auth-attacher.js +37 -3
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +7662 -1413
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +7662 -1413
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +7662 -1413
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +7837 -1406
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.js +20 -1
- package/dist-lib/lib/mailbox-ai.server.d.ts +1 -1
- package/dist-lib/lib/mailbox-ai.server.d.ts.map +1 -1
- package/dist-lib/lib/mailbox-ai.server.js +6 -106
- package/dist-lib/lib/mailbox-template.functions.d.ts +49077 -11583
- package/dist-lib/lib/mailbox-template.functions.d.ts.map +1 -1
- package/dist-lib/lib/mailbox.functions.d.ts +143316 -19105
- package/dist-lib/lib/mailbox.functions.d.ts.map +1 -1
- package/dist-lib/lib/mailbox.functions.js +536 -173
- package/package.json +4 -8
|
@@ -1,85 +1,272 @@
|
|
|
1
1
|
// Server functions voor de Mailbox-bestellingen module
|
|
2
2
|
import { createServerFn } from "@tanstack/react-start";
|
|
3
3
|
import { requireSupabaseAuth } from "../integrations/supabase/auth-middleware";
|
|
4
|
+
import { createClient } from "@supabase/supabase-js";
|
|
4
5
|
import { z } from "zod";
|
|
5
|
-
// ───
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
// ─── Actieve mailbox (uit gedeelde e-mail-auth module) ──────────────────────
|
|
7
|
+
// Geen auth-middleware: de RPC is SECURITY DEFINER en EXECUTE staat ook aan
|
|
8
|
+
// `anon`. Zo blijft de status-banner werken als de bearer-token tijdelijk
|
|
9
|
+
// ontbreekt of verlopen is — anders crasht de UI met een 401.
|
|
10
|
+
export const getActiveOrdersMailbox = createServerFn({ method: "GET" })
|
|
11
|
+
.handler(async () => {
|
|
12
|
+
const url = process.env.SUPABASE_URL;
|
|
13
|
+
const key = process.env.SUPABASE_PUBLISHABLE_KEY;
|
|
14
|
+
if (!url || !key)
|
|
15
|
+
return { connected: false, error: "missing_env" };
|
|
16
|
+
const sb = createClient(url, key, {
|
|
17
|
+
auth: { storage: undefined, persistSession: false, autoRefreshToken: false },
|
|
18
|
+
});
|
|
19
|
+
const { data, error } = await sb.rpc("email_auth_for_purpose", {
|
|
20
|
+
_purpose: "orders",
|
|
21
|
+
_direction: "receive",
|
|
22
|
+
});
|
|
14
23
|
if (error)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
return { connected: false, error: error.message };
|
|
25
|
+
const row = Array.isArray(data) ? data[0] : null;
|
|
26
|
+
if (!row)
|
|
27
|
+
return { connected: false };
|
|
28
|
+
return {
|
|
29
|
+
connected: true,
|
|
30
|
+
id: row.id,
|
|
31
|
+
provider: row.provider,
|
|
32
|
+
account_email: row.account_email,
|
|
33
|
+
display_name: (row.display_name ?? null),
|
|
34
|
+
status: (row.status ?? null),
|
|
35
|
+
last_error: (row.last_error ?? null),
|
|
36
|
+
last_verified_at: (row.last_verified_at ?? null),
|
|
37
|
+
needs_reauth: !!row.needs_reauth,
|
|
38
|
+
};
|
|
25
39
|
});
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
// ─── Event ingest helpers ────────────────────────────────────────────────────
|
|
41
|
+
function pickStr(obj, ...keys) {
|
|
42
|
+
for (const k of keys) {
|
|
43
|
+
const v = k.split(".").reduce((a, p) => (a == null ? a : a[p]), obj);
|
|
44
|
+
if (typeof v === "string" && v.length > 0)
|
|
45
|
+
return v;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
async function ingestSingleEvent(supabaseAdmin, connectionId, ev) {
|
|
50
|
+
const p = ev.payload ?? {};
|
|
51
|
+
const externalId = ev.external_id ?? pickStr(p, "message_id", "messageId", "id") ?? ev.id;
|
|
52
|
+
// Skip als deze external_id al bestaat
|
|
53
|
+
if (externalId) {
|
|
54
|
+
const { data: existing } = await supabaseAdmin
|
|
55
|
+
.from("mailbox_messages")
|
|
56
|
+
.select("id")
|
|
57
|
+
.eq("external_id", externalId)
|
|
58
|
+
.maybeSingle();
|
|
59
|
+
if (existing?.id) {
|
|
60
|
+
return { inserted: false, messageId: existing.id };
|
|
61
|
+
}
|
|
37
62
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
63
|
+
const fromEmail = pickStr(p, "from_email", "fromEmail", "from.address", "from.email", "sender.address") ?? "";
|
|
64
|
+
const fromName = pickStr(p, "from_name", "fromName", "from.name", "sender.name");
|
|
65
|
+
const subject = pickStr(p, "subject", "title") ?? "";
|
|
66
|
+
const bodyText = pickStr(p, "body_text", "bodyText", "text", "body.text", "snippet");
|
|
67
|
+
const bodyHtml = pickStr(p, "body_html", "bodyHtml", "html", "body.html");
|
|
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 };
|
|
89
|
+
}
|
|
90
|
+
throw new Error(insErr.message);
|
|
43
91
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
+
},
|
|
49
116
|
});
|
|
50
|
-
if (error)
|
|
51
|
-
throw new Error(error.message ?? "Edge function aanroep mislukt");
|
|
52
|
-
return data;
|
|
53
117
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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: [] };
|
|
59
162
|
}
|
|
60
|
-
|
|
61
|
-
|
|
163
|
+
const { data: custRows } = await supabaseAdmin
|
|
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) {
|
|
172
|
+
return {
|
|
173
|
+
customerId: uniqueCustomerIds[0],
|
|
174
|
+
source: "customer_contacts",
|
|
175
|
+
warning: null,
|
|
176
|
+
candidates: cands,
|
|
177
|
+
};
|
|
62
178
|
}
|
|
179
|
+
return {
|
|
180
|
+
customerId: null,
|
|
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" })
|
|
188
|
+
.middleware([requireSupabaseAuth])
|
|
189
|
+
.inputValidator((input) => z.object({ eventId: z.string().uuid() }).parse(input))
|
|
190
|
+
.handler(async ({ data }) => {
|
|
191
|
+
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
192
|
+
const { data: ev, error: evErr } = await supabaseAdmin
|
|
193
|
+
.from("email_auth_events")
|
|
194
|
+
.select("id, connection_id, external_id, payload, received_at, consumed_at")
|
|
195
|
+
.eq("id", data.eventId)
|
|
196
|
+
.maybeSingle();
|
|
197
|
+
if (evErr)
|
|
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 };
|
|
63
212
|
});
|
|
64
|
-
// ───
|
|
65
|
-
export const
|
|
213
|
+
// ─── Backfill: haal alle nog niet-consumed events op voor 'orders/receive' ───
|
|
214
|
+
export const backfillRecentEvents = createServerFn({ method: "POST" })
|
|
66
215
|
.middleware([requireSupabaseAuth])
|
|
67
216
|
.handler(async ({ context }) => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
217
|
+
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
218
|
+
// Welke connection is gekoppeld aan orders/receive?
|
|
219
|
+
const { data: active, error: actErr } = await context.supabase.rpc("email_auth_for_purpose", {
|
|
220
|
+
_purpose: "orders",
|
|
221
|
+
_direction: "receive",
|
|
222
|
+
});
|
|
223
|
+
if (actErr)
|
|
224
|
+
return { ok: false, error: actErr.message };
|
|
225
|
+
const row = Array.isArray(active) ? active[0] : null;
|
|
226
|
+
if (!row?.id)
|
|
227
|
+
return { ok: false, error: "Geen actieve koppeling voor orders/receive" };
|
|
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
|
|
77
248
|
}
|
|
78
|
-
return { ok: !!res.ok, inserted: res.inserted ?? 0, autoProcessed, autoApproved, error: res.error };
|
|
79
249
|
}
|
|
80
|
-
|
|
81
|
-
|
|
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++;
|
|
82
268
|
}
|
|
269
|
+
return { ok: true, inserted: inserted + stubbed, autoProcessed: 0, autoApproved: 0 };
|
|
83
270
|
});
|
|
84
271
|
// ─── Messages & Proposals ────────────────────────────────────────────────────
|
|
85
272
|
export const listMessages = createServerFn({ method: "GET" })
|
|
@@ -103,120 +290,42 @@ export const parseMessage = createServerFn({ method: "POST" })
|
|
|
103
290
|
.from("mailbox_messages").select("*").eq("id", data.messageId).single();
|
|
104
291
|
if (msgErr || !msg)
|
|
105
292
|
throw new Error("Bericht niet gevonden");
|
|
293
|
+
// AI staat uit: maak een leeg voorstel zodat het bericht in "Voorstellen" verschijnt.
|
|
294
|
+
await createStubProposal(supabaseAdmin, msg.id, msg.from_email ?? null);
|
|
106
295
|
await supabaseAdmin.from("mailbox_messages")
|
|
107
|
-
.update({ status: "
|
|
108
|
-
|
|
109
|
-
// Templatevelden ophalen
|
|
110
|
-
const { data: fields } = await supabaseAdmin
|
|
111
|
-
.from("mailbox_order_template_fields")
|
|
112
|
-
.select("*")
|
|
113
|
-
.order("sort_order", { ascending: true });
|
|
114
|
-
const templateFields = (fields ?? []).filter((f) => f.ai_enabled);
|
|
115
|
-
// Databron-catalogi
|
|
116
|
-
const [{ data: products }, { data: customers }, { data: suppliers }] = await Promise.all([
|
|
117
|
-
supabaseAdmin.from("products").select("id, product, product_type").order("product").range(0, 1999),
|
|
118
|
-
supabaseAdmin.from("customers").select("id, company_name, email").eq("is_active", true).order("company_name").range(0, 4999),
|
|
119
|
-
supabaseAdmin.from("suppliers").select("id, name").order("name").range(0, 999),
|
|
120
|
-
]);
|
|
121
|
-
const catalog = (products ?? []).map((p) => ({ id: p.id, name: p.product }));
|
|
122
|
-
const plantTypeSet = new Set();
|
|
123
|
-
(products ?? []).forEach((p) => { if (p.product_type)
|
|
124
|
-
plantTypeSet.add(p.product_type); });
|
|
125
|
-
const plantTypes = Array.from(plantTypeSet).sort().map((v) => ({ id: v, name: v }));
|
|
126
|
-
// Bouw spec met allowed values per database-veld
|
|
127
|
-
const spec = templateFields.map((f) => {
|
|
128
|
-
let allowed;
|
|
129
|
-
if (f.source === "database") {
|
|
130
|
-
switch (f.field_type) {
|
|
131
|
-
case "customer":
|
|
132
|
-
allowed = (customers ?? []).map((c) => ({ id: c.id, name: c.company_name }));
|
|
133
|
-
break;
|
|
134
|
-
case "product":
|
|
135
|
-
allowed = catalog;
|
|
136
|
-
break;
|
|
137
|
-
case "plant_type":
|
|
138
|
-
allowed = plantTypes;
|
|
139
|
-
break;
|
|
140
|
-
case "supplier":
|
|
141
|
-
allowed = (suppliers ?? []).map((s) => ({ id: s.id, name: s.name }));
|
|
142
|
-
break;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
return {
|
|
146
|
-
key: f.key,
|
|
147
|
-
label: f.label,
|
|
148
|
-
field_type: f.field_type,
|
|
149
|
-
ai_hint: f.ai_hint,
|
|
150
|
-
allowed,
|
|
151
|
-
};
|
|
152
|
-
});
|
|
153
|
-
const { parseOrderEmail } = await import("./mailbox-ai.server");
|
|
154
|
-
const parsed = await parseOrderEmail(msg.body_text ?? msg.body_html ?? "", msg.subject ?? "", spec, catalog);
|
|
155
|
-
// Backwards-compat: leid klant/leverdatum/notes af voor de bestaande kolommen
|
|
156
|
-
const customerId = parsed.fields["customer"]?.value;
|
|
157
|
-
const deliveryDate = parsed.fields["delivery_date"]?.value;
|
|
158
|
-
const notesValue = parsed.fields["notes"]?.value;
|
|
159
|
-
const matchedCustomerId = typeof customerId === "string" && customerId.length === 36 ? customerId : null;
|
|
160
|
-
// Fallback: match op e-mail als AI niets vond
|
|
161
|
-
let finalCustomerId = matchedCustomerId;
|
|
162
|
-
if (!finalCustomerId && msg.from_email) {
|
|
163
|
-
const { data: customer } = await supabaseAdmin
|
|
164
|
-
.from("customers").select("id").eq("email", msg.from_email).maybeSingle();
|
|
165
|
-
finalCustomerId = customer?.id ?? null;
|
|
166
|
-
}
|
|
167
|
-
const { data: proposal, error: propErr } = await supabaseAdmin
|
|
168
|
-
.from("mailbox_order_proposals").insert({
|
|
169
|
-
message_id: msg.id,
|
|
170
|
-
matched_customer_id: finalCustomerId,
|
|
171
|
-
confidence: parsed.overall_confidence,
|
|
172
|
-
delivery_date: typeof deliveryDate === "string" ? deliveryDate : null,
|
|
173
|
-
notes: typeof notesValue === "string" ? notesValue : null,
|
|
174
|
-
parsed_payload: parsed,
|
|
175
|
-
status: "pending",
|
|
176
|
-
}).select("id").single();
|
|
177
|
-
if (propErr)
|
|
178
|
-
throw new Error(propErr.message);
|
|
179
|
-
// Sla alle veldwaarden op
|
|
180
|
-
const fieldRows = Object.entries(parsed.fields).map(([key, v]) => ({
|
|
181
|
-
proposal_id: proposal.id,
|
|
182
|
-
field_key: key,
|
|
183
|
-
value: v.value === undefined ? null : v.value,
|
|
184
|
-
ai_confidence: v.confidence,
|
|
185
|
-
ai_filled: v.value !== null && v.value !== undefined,
|
|
186
|
-
needs_review: v.needs_review,
|
|
187
|
-
}));
|
|
188
|
-
if (fieldRows.length > 0) {
|
|
189
|
-
await supabaseAdmin.from("mailbox_proposal_field_values").insert(fieldRows);
|
|
190
|
-
}
|
|
191
|
-
if (parsed.lines.length > 0) {
|
|
192
|
-
const linesToInsert = parsed.lines.map((l, idx) => ({
|
|
193
|
-
proposal_id: proposal.id,
|
|
194
|
-
product_id: l.product_id,
|
|
195
|
-
raw_product_text: l.raw_product_text,
|
|
196
|
-
quantity: l.quantity,
|
|
197
|
-
unit: l.unit,
|
|
198
|
-
match_confidence: l.match_confidence,
|
|
199
|
-
sort_order: idx,
|
|
200
|
-
}));
|
|
201
|
-
await supabaseAdmin.from("mailbox_proposal_lines").insert(linesToInsert);
|
|
202
|
-
}
|
|
203
|
-
await supabaseAdmin.from("mailbox_messages")
|
|
204
|
-
.update({ status: "parsed" }).eq("id", msg.id);
|
|
205
|
-
return { ok: true, proposalId: proposal.id };
|
|
206
|
-
}
|
|
207
|
-
catch (e) {
|
|
208
|
-
await supabaseAdmin.from("mailbox_messages")
|
|
209
|
-
.update({ status: "failed", error_message: e?.message ?? "Parsefout" }).eq("id", msg.id);
|
|
210
|
-
return { ok: false, error: e?.message ?? "Parsefout" };
|
|
211
|
-
}
|
|
296
|
+
.update({ status: "parsed", error_message: null }).eq("id", msg.id);
|
|
297
|
+
return { ok: true };
|
|
212
298
|
});
|
|
213
299
|
export const listProposals = createServerFn({ method: "GET" })
|
|
214
300
|
.middleware([requireSupabaseAuth])
|
|
215
301
|
.inputValidator((input) => z.object({ status: z.enum(["pending", "approved", "rejected"]).optional() }).parse(input ?? {}))
|
|
216
302
|
.handler(async ({ data, context }) => {
|
|
303
|
+
// Beperk tot berichten die binnenkwamen via de actieve orders/receive mailbox
|
|
304
|
+
const { data: active } = await context.supabase.rpc("email_auth_for_purpose", {
|
|
305
|
+
_purpose: "orders",
|
|
306
|
+
_direction: "receive",
|
|
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
|
|
217
325
|
let q = context.supabase
|
|
218
326
|
.from("mailbox_order_proposals")
|
|
219
|
-
.select("*, mailbox_messages(subject, from_email, from_name, body_text, received_at), mailbox_proposal_lines(*), mailbox_proposal_field_values(*)")
|
|
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)
|
|
220
329
|
.order("created_at", { ascending: false })
|
|
221
330
|
.limit(100);
|
|
222
331
|
if (data.status)
|
|
@@ -250,11 +359,29 @@ export const approveProposal = createServerFn({ method: "POST" })
|
|
|
250
359
|
}).parse(input))
|
|
251
360
|
.handler(async ({ data, context }) => {
|
|
252
361
|
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
253
|
-
// Bouw custom_fields map vanuit alle templatevelden
|
|
254
362
|
const customFields = {};
|
|
255
363
|
(data.fieldValues ?? []).forEach((fv) => {
|
|
256
364
|
customFields[fv.field_key] = fv.value;
|
|
257
365
|
});
|
|
366
|
+
// ─── Validatie eerst mogelijke leverdatum ────────────────────────────────
|
|
367
|
+
const carrierId = typeof customFields["carrier"] === "string" ? customFields["carrier"] : null;
|
|
368
|
+
let carrierDeliveryDays = 0;
|
|
369
|
+
if (carrierId) {
|
|
370
|
+
const { data: c } = await context.supabase
|
|
371
|
+
.from("carriers").select("delivery_days").eq("id", carrierId).maybeSingle();
|
|
372
|
+
carrierDeliveryDays = Math.max(0, Number(c?.delivery_days ?? 0));
|
|
373
|
+
}
|
|
374
|
+
const { data: settings } = await context.supabase
|
|
375
|
+
.from("afhandeling_settings")
|
|
376
|
+
.select("pick_days_before_transport")
|
|
377
|
+
.order("updated_at", { ascending: false })
|
|
378
|
+
.limit(1).maybeSingle();
|
|
379
|
+
const pickDays = Math.max(0, Number(settings?.pick_days_before_transport ?? 0));
|
|
380
|
+
const earliest = addCalendarDays(new Date(), pickDays + carrierDeliveryDays);
|
|
381
|
+
const earliestISO = toISODate(earliest);
|
|
382
|
+
if (data.deliveryDate < earliestISO) {
|
|
383
|
+
throw new Error(`Leverdatum (${data.deliveryDate}) ligt voor de eerst mogelijke datum (${earliestISO}) o.b.v. orderpicktijd (${pickDays}d) + levertijd vervoerder (${carrierDeliveryDays}d).`);
|
|
384
|
+
}
|
|
258
385
|
const orderNumber = `MB-${Date.now()}`;
|
|
259
386
|
const { data: order, error: orderErr } = await supabaseAdmin
|
|
260
387
|
.from("orders").insert({
|
|
@@ -280,7 +407,6 @@ export const approveProposal = createServerFn({ method: "POST" })
|
|
|
280
407
|
const { error: itemsErr } = await supabaseAdmin.from("order_items").insert(items);
|
|
281
408
|
if (itemsErr)
|
|
282
409
|
throw new Error(itemsErr.message);
|
|
283
|
-
// Sla finale veldwaarden op (overschrijft AI-waarden)
|
|
284
410
|
if (data.fieldValues && data.fieldValues.length > 0) {
|
|
285
411
|
for (const fv of data.fieldValues) {
|
|
286
412
|
await supabaseAdmin.from("mailbox_proposal_field_values").upsert({
|
|
@@ -318,6 +444,19 @@ export const rejectProposal = createServerFn({ method: "POST" })
|
|
|
318
444
|
}).eq("id", data.proposalId);
|
|
319
445
|
return { ok: true };
|
|
320
446
|
});
|
|
447
|
+
// Een afgewezen (of goedgekeurd) voorstel terugzetten naar "te controleren".
|
|
448
|
+
export const reopenProposal = createServerFn({ method: "POST" })
|
|
449
|
+
.middleware([requireSupabaseAuth])
|
|
450
|
+
.inputValidator((input) => z.object({ proposalId: z.string().uuid() }).parse(input))
|
|
451
|
+
.handler(async ({ data }) => {
|
|
452
|
+
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
453
|
+
await supabaseAdmin.from("mailbox_order_proposals").update({
|
|
454
|
+
status: "pending",
|
|
455
|
+
reviewed_by: null,
|
|
456
|
+
reviewed_at: null,
|
|
457
|
+
}).eq("id", data.proposalId);
|
|
458
|
+
return { ok: true };
|
|
459
|
+
});
|
|
321
460
|
export const listCustomersForPicker = createServerFn({ method: "GET" })
|
|
322
461
|
.middleware([requireSupabaseAuth])
|
|
323
462
|
.handler(async ({ context }) => {
|
|
@@ -329,6 +468,230 @@ export const listProductsForPicker = createServerFn({ method: "GET" })
|
|
|
329
468
|
.middleware([requireSupabaseAuth])
|
|
330
469
|
.handler(async ({ context }) => {
|
|
331
470
|
const { data } = await context.supabase
|
|
332
|
-
.from("products")
|
|
471
|
+
.from("products")
|
|
472
|
+
.select("id, product, unit, plants_per_crate, crates_per_layer, layers_per_cart")
|
|
473
|
+
.order("product").range(0, 4999);
|
|
333
474
|
return { items: data ?? [] };
|
|
334
475
|
});
|
|
476
|
+
export const planAndSaveAutoCarts = createServerFn({ method: "POST" })
|
|
477
|
+
.middleware([requireSupabaseAuth])
|
|
478
|
+
.inputValidator((input) => z.object({ orderId: z.string().uuid() }).parse(input))
|
|
479
|
+
.handler(async ({ data, context }) => {
|
|
480
|
+
// Items met productbelading ophalen
|
|
481
|
+
const { data: items, error: itErr } = await context.supabase
|
|
482
|
+
.from("order_items")
|
|
483
|
+
.select("id, product_id, product_name, quantity, products(plants_per_crate, crates_per_layer, layers_per_cart)")
|
|
484
|
+
.eq("order_id", data.orderId);
|
|
485
|
+
if (itErr)
|
|
486
|
+
throw new Error(itErr.message);
|
|
487
|
+
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
488
|
+
// Reset eventuele eerdere auto-karren (idempotent)
|
|
489
|
+
await supabaseAdmin.from("order_carts").delete().eq("order_id", data.orderId).eq("is_auto", true);
|
|
490
|
+
let cartNumber = 1;
|
|
491
|
+
// Eerst hoogste bestaande nr ophalen (voor manueel toegevoegde karren)
|
|
492
|
+
const { data: existingMax } = await supabaseAdmin
|
|
493
|
+
.from("order_carts").select("cart_number").eq("order_id", data.orderId)
|
|
494
|
+
.order("cart_number", { ascending: false }).limit(1);
|
|
495
|
+
if (existingMax && existingMax[0])
|
|
496
|
+
cartNumber = existingMax[0].cart_number + 1;
|
|
497
|
+
const autoCarts = [];
|
|
498
|
+
const remainders = [];
|
|
499
|
+
for (const it of items ?? []) {
|
|
500
|
+
const prod = it.products ?? {};
|
|
501
|
+
const ppc = Math.max(1, Number(prod.plants_per_crate ?? 1));
|
|
502
|
+
const cpl = Math.max(1, Number(prod.crates_per_layer ?? 1));
|
|
503
|
+
const lpc = Math.max(1, Number(prod.layers_per_cart ?? 1));
|
|
504
|
+
const cratesPerCart = cpl * lpc;
|
|
505
|
+
// quantity is in PLANTEN (zie approveProposal)
|
|
506
|
+
const totalPlants = Math.max(0, Number(it.quantity ?? 0));
|
|
507
|
+
const totalCrates = Math.ceil(totalPlants / ppc);
|
|
508
|
+
const fullCarts = Math.floor(totalCrates / cratesPerCart);
|
|
509
|
+
const remainder = totalCrates - fullCarts * cratesPerCart;
|
|
510
|
+
for (let i = 0; i < fullCarts; i++) {
|
|
511
|
+
const { data: cart, error: cErr } = await supabaseAdmin
|
|
512
|
+
.from("order_carts").insert({
|
|
513
|
+
order_id: data.orderId,
|
|
514
|
+
cart_number: cartNumber,
|
|
515
|
+
is_auto: true,
|
|
516
|
+
is_complete: true,
|
|
517
|
+
}).select("id").single();
|
|
518
|
+
if (cErr)
|
|
519
|
+
throw new Error(cErr.message);
|
|
520
|
+
await supabaseAdmin.from("order_cart_items").insert({
|
|
521
|
+
cart_id: cart.id,
|
|
522
|
+
order_item_id: it.id,
|
|
523
|
+
product_id: it.product_id,
|
|
524
|
+
product_name: it.product_name,
|
|
525
|
+
crates: cratesPerCart,
|
|
526
|
+
plants_per_crate: ppc,
|
|
527
|
+
crates_per_layer: cpl,
|
|
528
|
+
});
|
|
529
|
+
autoCarts.push({
|
|
530
|
+
cart_number: cartNumber,
|
|
531
|
+
product_name: it.product_name,
|
|
532
|
+
crates: cratesPerCart,
|
|
533
|
+
plants: cratesPerCart * ppc,
|
|
534
|
+
capacity: cratesPerCart,
|
|
535
|
+
});
|
|
536
|
+
cartNumber++;
|
|
537
|
+
}
|
|
538
|
+
if (remainder > 0) {
|
|
539
|
+
remainders.push({
|
|
540
|
+
order_item_id: it.id,
|
|
541
|
+
product_id: it.product_id,
|
|
542
|
+
product_name: it.product_name,
|
|
543
|
+
crates_remaining: remainder,
|
|
544
|
+
plants_per_crate: ppc,
|
|
545
|
+
crates_per_layer: cpl,
|
|
546
|
+
layers_per_cart: lpc,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
// Bestaande handmatige karren ophalen + crates per order_item aftrekken van remainders,
|
|
551
|
+
// zodat heropenen van de wizard niet dezelfde fusten opnieuw vraagt.
|
|
552
|
+
const { data: manualCartsRaw } = await supabaseAdmin
|
|
553
|
+
.from("order_carts")
|
|
554
|
+
.select("id, cart_number, notes, order_cart_items(order_item_id, product_id, product_name, crates, plants_per_crate, crates_per_layer)")
|
|
555
|
+
.eq("order_id", data.orderId).eq("is_auto", false)
|
|
556
|
+
.order("cart_number");
|
|
557
|
+
const existingManualCarts = (manualCartsRaw ?? []).map((c) => ({
|
|
558
|
+
cart_number: c.cart_number,
|
|
559
|
+
notes: c.notes,
|
|
560
|
+
items: (c.order_cart_items ?? []).map((i) => ({
|
|
561
|
+
order_item_id: i.order_item_id,
|
|
562
|
+
product_id: i.product_id,
|
|
563
|
+
product_name: i.product_name,
|
|
564
|
+
crates: Number(i.crates ?? 0),
|
|
565
|
+
plants_per_crate: Number(i.plants_per_crate ?? 1),
|
|
566
|
+
crates_per_layer: Number(i.crates_per_layer ?? 1),
|
|
567
|
+
})),
|
|
568
|
+
}));
|
|
569
|
+
const assignedByItem = {};
|
|
570
|
+
existingManualCarts.forEach((c) => c.items.forEach((i) => {
|
|
571
|
+
if (!i.order_item_id)
|
|
572
|
+
return;
|
|
573
|
+
assignedByItem[i.order_item_id] = (assignedByItem[i.order_item_id] ?? 0) + Number(i.crates ?? 0);
|
|
574
|
+
}));
|
|
575
|
+
const adjustedRemainders = remainders
|
|
576
|
+
.map((r) => ({ ...r, crates_remaining: Math.max(0, r.crates_remaining - (assignedByItem[r.order_item_id] ?? 0)) }))
|
|
577
|
+
.filter((r) => r.crates_remaining > 0);
|
|
578
|
+
return { ok: true, autoCarts, remainders: adjustedRemainders, existingManualCarts, nextCartNumber: cartNumber };
|
|
579
|
+
});
|
|
580
|
+
const ManualCartItem = z.object({
|
|
581
|
+
order_item_id: z.string().uuid().nullable(),
|
|
582
|
+
product_id: z.string().uuid().nullable(),
|
|
583
|
+
product_name: z.string().min(1).max(255),
|
|
584
|
+
crates: z.number().int().min(1),
|
|
585
|
+
plants_per_crate: z.number().int().min(1),
|
|
586
|
+
crates_per_layer: z.number().int().min(1),
|
|
587
|
+
});
|
|
588
|
+
const ManualCart = z.object({
|
|
589
|
+
is_complete: z.boolean().optional(),
|
|
590
|
+
notes: z.string().max(500).nullable().optional(),
|
|
591
|
+
items: z.array(ManualCartItem).min(1),
|
|
592
|
+
});
|
|
593
|
+
export const saveManualCarts = createServerFn({ method: "POST" })
|
|
594
|
+
.middleware([requireSupabaseAuth])
|
|
595
|
+
.inputValidator((input) => z.object({ orderId: z.string().uuid(), carts: z.array(ManualCart) }).parse(input))
|
|
596
|
+
.handler(async ({ data }) => {
|
|
597
|
+
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
598
|
+
// Verwijder eerdere handmatige karren (replace strategy)
|
|
599
|
+
await supabaseAdmin.from("order_carts").delete()
|
|
600
|
+
.eq("order_id", data.orderId).eq("is_auto", false);
|
|
601
|
+
const { data: existingMax } = await supabaseAdmin
|
|
602
|
+
.from("order_carts").select("cart_number").eq("order_id", data.orderId)
|
|
603
|
+
.order("cart_number", { ascending: false }).limit(1);
|
|
604
|
+
let cartNumber = existingMax && existingMax[0] ? existingMax[0].cart_number + 1 : 1;
|
|
605
|
+
for (const cart of data.carts) {
|
|
606
|
+
const { data: inserted, error: cErr } = await supabaseAdmin
|
|
607
|
+
.from("order_carts").insert({
|
|
608
|
+
order_id: data.orderId,
|
|
609
|
+
cart_number: cartNumber,
|
|
610
|
+
is_auto: false,
|
|
611
|
+
is_complete: !!cart.is_complete,
|
|
612
|
+
notes: cart.notes ?? null,
|
|
613
|
+
}).select("id").single();
|
|
614
|
+
if (cErr)
|
|
615
|
+
throw new Error(cErr.message);
|
|
616
|
+
const rows = cart.items.map((i) => ({
|
|
617
|
+
cart_id: inserted.id,
|
|
618
|
+
order_item_id: i.order_item_id,
|
|
619
|
+
product_id: i.product_id,
|
|
620
|
+
product_name: i.product_name,
|
|
621
|
+
crates: i.crates,
|
|
622
|
+
plants_per_crate: i.plants_per_crate,
|
|
623
|
+
crates_per_layer: i.crates_per_layer,
|
|
624
|
+
}));
|
|
625
|
+
const { error: iErr } = await supabaseAdmin.from("order_cart_items").insert(rows);
|
|
626
|
+
if (iErr)
|
|
627
|
+
throw new Error(iErr.message);
|
|
628
|
+
cartNumber++;
|
|
629
|
+
}
|
|
630
|
+
return { ok: true };
|
|
631
|
+
});
|
|
632
|
+
export const listOrderCarts = createServerFn({ method: "POST" })
|
|
633
|
+
.middleware([requireSupabaseAuth])
|
|
634
|
+
.inputValidator((d) => d)
|
|
635
|
+
.handler(async ({ data, context }) => {
|
|
636
|
+
const { data: carts } = await context.supabase
|
|
637
|
+
.from("order_carts")
|
|
638
|
+
.select("id, cart_number, is_auto, is_complete, notes, order_cart_items(*)")
|
|
639
|
+
.eq("order_id", data.orderId)
|
|
640
|
+
.order("cart_number");
|
|
641
|
+
return { items: carts ?? [] };
|
|
642
|
+
});
|
|
643
|
+
export const listDeliveryLocationsForCustomer = createServerFn({ method: "POST" })
|
|
644
|
+
.middleware([requireSupabaseAuth])
|
|
645
|
+
.inputValidator((d) => d)
|
|
646
|
+
.handler(async ({ data, context }) => {
|
|
647
|
+
if (!data.customerId)
|
|
648
|
+
return { items: [] };
|
|
649
|
+
const { data: rows } = await context.supabase
|
|
650
|
+
.from("customer_delivery_locations")
|
|
651
|
+
.select("id, name, address_line, street, house_number, postal_code, city, province, country, is_primary, carrier_id, carriers(id, name)")
|
|
652
|
+
.eq("customer_id", data.customerId)
|
|
653
|
+
.order("is_primary", { ascending: false })
|
|
654
|
+
.order("name");
|
|
655
|
+
return { items: Array.isArray(rows) ? rows : [] };
|
|
656
|
+
});
|
|
657
|
+
// Onze eigen bedrijfslocaties (tuinen / afhaallocaties). Staan los van klantlocaties.
|
|
658
|
+
export const listPickupLocations = createServerFn({ method: "GET" })
|
|
659
|
+
.middleware([requireSupabaseAuth])
|
|
660
|
+
.handler(async ({ context }) => {
|
|
661
|
+
const { data } = await context.supabase
|
|
662
|
+
.from("locations")
|
|
663
|
+
.select("id, name, sort_order")
|
|
664
|
+
.order("sort_order", { ascending: true })
|
|
665
|
+
.order("name");
|
|
666
|
+
return { items: Array.isArray(data) ? data : [] };
|
|
667
|
+
});
|
|
668
|
+
export const listCarriersForPicker = createServerFn({ method: "GET" })
|
|
669
|
+
.middleware([requireSupabaseAuth])
|
|
670
|
+
.handler(async ({ context }) => {
|
|
671
|
+
const { data } = await context.supabase
|
|
672
|
+
.from("carriers")
|
|
673
|
+
.select("id, name, is_default, sort_order, delivery_days")
|
|
674
|
+
.order("sort_order", { ascending: true, nullsFirst: false })
|
|
675
|
+
.order("name");
|
|
676
|
+
return { items: data ?? [] };
|
|
677
|
+
});
|
|
678
|
+
// Orderpicktijd — gebruikt voor het berekenen van de eerst mogelijke leverdatum
|
|
679
|
+
export const getOrderPickDays = createServerFn({ method: "GET" })
|
|
680
|
+
.middleware([requireSupabaseAuth])
|
|
681
|
+
.handler(async ({ context }) => {
|
|
682
|
+
const { data } = await context.supabase
|
|
683
|
+
.from("afhandeling_settings")
|
|
684
|
+
.select("pick_days_before_transport")
|
|
685
|
+
.order("updated_at", { ascending: false })
|
|
686
|
+
.limit(1)
|
|
687
|
+
.maybeSingle();
|
|
688
|
+
return { pickDays: Math.max(0, Number(data?.pick_days_before_transport ?? 0)) };
|
|
689
|
+
});
|
|
690
|
+
function addCalendarDays(base, n) {
|
|
691
|
+
const x = new Date(base);
|
|
692
|
+
x.setDate(x.getDate() + n);
|
|
693
|
+
return x;
|
|
694
|
+
}
|
|
695
|
+
function toISODate(d) {
|
|
696
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
697
|
+
}
|