@flowselections/mailbox-orders 1.0.3 → 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/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 +2 -4
- 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 +2762 -145
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +2762 -145
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +2762 -145
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +2849 -149
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.js +2 -0
- package/dist-lib/lib/mailbox-template.functions.d.ts +34602 -18900
- package/dist-lib/lib/mailbox-template.functions.d.ts.map +1 -1
- package/dist-lib/lib/mailbox.functions.d.ts +130480 -33017
- package/dist-lib/lib/mailbox.functions.d.ts.map +1 -1
- package/dist-lib/lib/mailbox.functions.js +392 -18
- package/package.json +2 -7
|
@@ -71,6 +71,7 @@ async function ingestSingleEvent(supabaseAdmin, connectionId, ev) {
|
|
|
71
71
|
.insert({
|
|
72
72
|
external_id: externalId,
|
|
73
73
|
message_id: pickStr(p, "message_id", "messageId") ?? externalId,
|
|
74
|
+
connection_id: connectionId,
|
|
74
75
|
from_email: fromEmail,
|
|
75
76
|
from_name: fromName,
|
|
76
77
|
subject,
|
|
@@ -88,8 +89,100 @@ async function ingestSingleEvent(supabaseAdmin, connectionId, ev) {
|
|
|
88
89
|
}
|
|
89
90
|
throw new Error(insErr.message);
|
|
90
91
|
}
|
|
92
|
+
// Maak direct een leeg voorstel aan (AI staat uit) — match klant op afzender
|
|
93
|
+
await createStubProposal(supabaseAdmin, inserted.id, fromEmail);
|
|
91
94
|
return { inserted: true, messageId: inserted.id };
|
|
92
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: [] };
|
|
162
|
+
}
|
|
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
|
+
};
|
|
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
|
+
}
|
|
93
186
|
// ─── Ingest één specifiek event (vanuit realtime subscription) ───────────────
|
|
94
187
|
export const ingestEmailEvent = createServerFn({ method: "POST" })
|
|
95
188
|
.middleware([requireSupabaseAuth])
|
|
@@ -160,10 +253,20 @@ export const backfillRecentEvents = createServerFn({ method: "POST" })
|
|
|
160
253
|
.update({ consumed_at: new Date().toISOString() })
|
|
161
254
|
.in("id", consumedIds);
|
|
162
255
|
}
|
|
163
|
-
//
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
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 };
|
|
167
270
|
});
|
|
168
271
|
// ─── Messages & Proposals ────────────────────────────────────────────────────
|
|
169
272
|
export const listMessages = createServerFn({ method: "GET" })
|
|
@@ -187,27 +290,42 @@ export const parseMessage = createServerFn({ method: "POST" })
|
|
|
187
290
|
.from("mailbox_messages").select("*").eq("id", data.messageId).single();
|
|
188
291
|
if (msgErr || !msg)
|
|
189
292
|
throw new Error("Bericht niet gevonden");
|
|
190
|
-
// AI
|
|
191
|
-
|
|
192
|
-
const msg2 = msg;
|
|
293
|
+
// AI staat uit: maak een leeg voorstel zodat het bericht in "Voorstellen" verschijnt.
|
|
294
|
+
await createStubProposal(supabaseAdmin, msg.id, msg.from_email ?? null);
|
|
193
295
|
await supabaseAdmin.from("mailbox_messages")
|
|
194
|
-
.update({
|
|
195
|
-
|
|
196
|
-
error_message: "AI-parsing is tijdelijk uitgeschakeld — wacht op externe model-koppeling.",
|
|
197
|
-
})
|
|
198
|
-
.eq("id", msg2.id);
|
|
199
|
-
return {
|
|
200
|
-
ok: false,
|
|
201
|
-
error: "AI-parsing is tijdelijk uitgeschakeld — wacht op externe model-koppeling.",
|
|
202
|
-
};
|
|
296
|
+
.update({ status: "parsed", error_message: null }).eq("id", msg.id);
|
|
297
|
+
return { ok: true };
|
|
203
298
|
});
|
|
204
299
|
export const listProposals = createServerFn({ method: "GET" })
|
|
205
300
|
.middleware([requireSupabaseAuth])
|
|
206
301
|
.inputValidator((input) => z.object({ status: z.enum(["pending", "approved", "rejected"]).optional() }).parse(input ?? {}))
|
|
207
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
|
|
208
325
|
let q = context.supabase
|
|
209
326
|
.from("mailbox_order_proposals")
|
|
210
|
-
.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)
|
|
211
329
|
.order("created_at", { ascending: false })
|
|
212
330
|
.limit(100);
|
|
213
331
|
if (data.status)
|
|
@@ -245,6 +363,25 @@ export const approveProposal = createServerFn({ method: "POST" })
|
|
|
245
363
|
(data.fieldValues ?? []).forEach((fv) => {
|
|
246
364
|
customFields[fv.field_key] = fv.value;
|
|
247
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
|
+
}
|
|
248
385
|
const orderNumber = `MB-${Date.now()}`;
|
|
249
386
|
const { data: order, error: orderErr } = await supabaseAdmin
|
|
250
387
|
.from("orders").insert({
|
|
@@ -307,6 +444,19 @@ export const rejectProposal = createServerFn({ method: "POST" })
|
|
|
307
444
|
}).eq("id", data.proposalId);
|
|
308
445
|
return { ok: true };
|
|
309
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
|
+
});
|
|
310
460
|
export const listCustomersForPicker = createServerFn({ method: "GET" })
|
|
311
461
|
.middleware([requireSupabaseAuth])
|
|
312
462
|
.handler(async ({ context }) => {
|
|
@@ -318,6 +468,230 @@ export const listProductsForPicker = createServerFn({ method: "GET" })
|
|
|
318
468
|
.middleware([requireSupabaseAuth])
|
|
319
469
|
.handler(async ({ context }) => {
|
|
320
470
|
const { data } = await context.supabase
|
|
321
|
-
.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);
|
|
474
|
+
return { items: data ?? [] };
|
|
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");
|
|
322
676
|
return { items: data ?? [] };
|
|
323
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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowselections/mailbox-orders",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"type": "module",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"@cloudflare/vite-plugin": "^1.25.5",
|
|
47
47
|
"@eslint/js": "^9.32.0",
|
|
48
48
|
"@flowselections/core": "^1.0.11",
|
|
49
|
-
"@lovable.dev/vite-tanstack-config": "2.
|
|
49
|
+
"@lovable.dev/vite-tanstack-config": "2.6.4",
|
|
50
50
|
"@rsbuild/core": "^1.0.0",
|
|
51
51
|
"@tailwindcss/vite": "^4.2.1",
|
|
52
52
|
"@tanstack/react-query": "^5.83.0",
|
|
@@ -77,11 +77,6 @@
|
|
|
77
77
|
"flowselections": {
|
|
78
78
|
"moduleId": "Mailbox",
|
|
79
79
|
"pages": [
|
|
80
|
-
{
|
|
81
|
-
"export": "MailboxInboxPage",
|
|
82
|
-
"route": "mailbox-orders",
|
|
83
|
-
"title": "Inbox"
|
|
84
|
-
},
|
|
85
80
|
{
|
|
86
81
|
"export": "MailboxProposalsPage",
|
|
87
82
|
"route": "mailbox-orders/voorstellen",
|