@flowselections/floriday-verkoop-module 1.0.16 → 1.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist-lib/components/verkoop/CreateOrderPage.d.ts.map +1 -1
  2. package/dist-lib/components/verkoop/CreateOrderPage.js +12 -12
  3. package/dist-lib/components/verkoop/EditLocalOrderDialog.d.ts +6 -0
  4. package/dist-lib/components/verkoop/EditLocalOrderDialog.d.ts.map +1 -0
  5. package/dist-lib/components/verkoop/EditLocalOrderDialog.js +102 -0
  6. package/dist-lib/components/verkoop/OrderKarindelingCard.d.ts.map +1 -1
  7. package/dist-lib/components/verkoop/OrderKarindelingCard.js +26 -28
  8. package/dist-lib/components/verkoop/OrderShipmentsCard.d.ts +5 -0
  9. package/dist-lib/components/verkoop/OrderShipmentsCard.d.ts.map +1 -0
  10. package/dist-lib/components/verkoop/OrderShipmentsCard.js +137 -0
  11. package/dist-lib/components/verkoop/SalesOrderDetailPage.d.ts.map +1 -1
  12. package/dist-lib/components/verkoop/SalesOrderDetailPage.js +8 -2
  13. package/dist-lib/components/verkoop/SalesOrderDetailView.d.ts.map +1 -1
  14. package/dist-lib/components/verkoop/SalesOrderDetailView.js +2 -1
  15. package/dist-lib/components/verkoop/SalesOrdersFilters.d.ts.map +1 -1
  16. package/dist-lib/components/verkoop/SalesOrdersFilters.js +2 -8
  17. package/dist-lib/components/verkoop/SalesOrdersTable.d.ts +4 -1
  18. package/dist-lib/components/verkoop/SalesOrdersTable.d.ts.map +1 -1
  19. package/dist-lib/components/verkoop/SalesOrdersTable.js +2 -2
  20. package/dist-lib/components/verkoop/VerkoopOrdersPage.d.ts.map +1 -1
  21. package/dist-lib/components/verkoop/VerkoopOrdersPage.js +43 -1
  22. package/dist-lib/index.js +2 -2
  23. package/dist-lib/integrations/supabase/auth-middleware.d.ts +607 -169
  24. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  25. package/dist-lib/integrations/supabase/client.d.ts +607 -169
  26. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  27. package/dist-lib/integrations/supabase/client.server.d.ts +607 -169
  28. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  29. package/dist-lib/integrations/supabase/types.d.ts +623 -164
  30. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  31. package/dist-lib/lib/cart-lock.functions.d.ts +607 -169
  32. package/dist-lib/lib/cart-lock.functions.d.ts.map +1 -1
  33. package/dist-lib/lib/cart-replan.functions.d.ts +1221 -345
  34. package/dist-lib/lib/cart-replan.functions.d.ts.map +1 -1
  35. package/dist-lib/lib/create-order.functions.d.ts +40700 -1397
  36. package/dist-lib/lib/create-order.functions.d.ts.map +1 -1
  37. package/dist-lib/lib/create-order.functions.js +792 -52
  38. package/dist-lib/lib/expand-group-order-items.d.ts +13 -0
  39. package/dist-lib/lib/expand-group-order-items.d.ts.map +1 -0
  40. package/dist-lib/lib/expand-group-order-items.js +216 -0
  41. package/dist-lib/lib/floriday-auto-fulfillment.functions.d.ts +1767 -453
  42. package/dist-lib/lib/floriday-auto-fulfillment.functions.d.ts.map +1 -1
  43. package/dist-lib/lib/floriday-logistic-labels.functions.d.ts +607 -169
  44. package/dist-lib/lib/floriday-logistic-labels.functions.d.ts.map +1 -1
  45. package/dist-lib/lib/floriday-public.functions.d.ts +4123 -1057
  46. package/dist-lib/lib/floriday-public.functions.d.ts.map +1 -1
  47. package/dist-lib/lib/floriday-writes.functions.d.ts +2945 -755
  48. package/dist-lib/lib/floriday-writes.functions.d.ts.map +1 -1
  49. package/dist-lib/lib/order-shipments.functions.d.ts +47158 -0
  50. package/dist-lib/lib/order-shipments.functions.d.ts.map +1 -0
  51. package/dist-lib/lib/order-shipments.functions.js +178 -0
  52. package/dist-lib/lib/salesorders-mapping.d.ts +1 -0
  53. package/dist-lib/lib/salesorders-mapping.d.ts.map +1 -1
  54. package/dist-lib/lib/salesorders.functions.d.ts +1767 -453
  55. package/dist-lib/lib/salesorders.functions.d.ts.map +1 -1
  56. package/dist-lib/lib/tradeitems.functions.d.ts +607 -169
  57. package/dist-lib/lib/tradeitems.functions.d.ts.map +1 -1
  58. package/dist-lib/lib/useSalesOrdersQuery.d.ts.map +1 -1
  59. package/dist-lib/lib/useSalesOrdersQuery.js +39 -2
  60. package/package.json +2 -2
@@ -16,28 +16,61 @@ export const listCustomersForCreateOrder = createServerFn({ method: "GET" })
16
16
  .range(0, 4999);
17
17
  return { items: data ?? [] };
18
18
  });
19
- // ─── Producten (uit voorraadmodule: alle actieve Floriday trade items) ─────
20
- // Bron = zelfde data als de Voorraad-pagina: floriday_trade_items_cache
21
- // gejoined met trade_item_stock. Items zonder voorraad worden ook getoond
22
- // (grijs in de UI, met melding "niet op voorraad"), zodat gebruikers ook
23
- // vooruit kunnen bestellen op verwachte voorraad.
19
+ // ─── Producten (uit voorraadmodule) ────────────────────────────────────────
20
+ // Bron = de volledige voorraad, ongeacht Floriday-publicatie:
21
+ // 1) floriday_trade_items_cache — gepubliceerde Floriday trade items
22
+ // 2) trade_item_drafts — interne artikelen (o.a. BV-artikelcodes)
23
+ // 3) product_groups — productgroepen / samenstellingen
24
+ // 4) products — losse voorraadartikelen (fallback)
25
+ // Stock komt uit trade_item_stock (key-formaten: `<floriday_id>`,
26
+ // `draft:<uuid>`, `product:<uuid>`, `group:<uuid>`).
24
27
  export const listProductsForCreateOrder = createServerFn({ method: "GET" })
25
28
  .middleware([requireSupabaseAuth])
26
29
  .handler(async ({ context }) => {
27
- const [tradeItemsRes, stockRes] = await Promise.all([
30
+ const [tradeItemsRes, draftsRes, groupsRes, groupItemsRes, productsRes] = await Promise.all([
28
31
  context.supabase
29
32
  .from("floriday_trade_items_cache")
30
33
  .select("floriday_id, data")
31
34
  .eq("is_deleted", false)
32
35
  .range(0, 4999),
33
36
  context.supabase
34
- .from("trade_item_stock")
35
- .select("trade_item_key, physical_stock, reserved_stock, incoming_stock"),
37
+ .from("trade_item_drafts")
38
+ .select("id, data, floriday_id")
39
+ .range(0, 4999),
40
+ context.supabase
41
+ .from("product_groups")
42
+ .select("id, name, description, is_active, head_trade_item_floriday_id, head_product_id")
43
+ .eq("is_active", true)
44
+ .range(0, 4999),
45
+ context.supabase
46
+ .from("product_group_items")
47
+ .select("product_group_id, trade_item_id, sort_order")
48
+ .order("sort_order", { ascending: true })
49
+ .range(0, 9999),
50
+ context.supabase
51
+ .from("products")
52
+ .select("id, product, unit, sale_price, plants_per_crate, crates_per_layer, layers_per_cart, quantity, custom_fields")
53
+ .range(0, 4999),
36
54
  ]);
37
- const tradeItems = tradeItemsRes.data;
38
- const stockRows = stockRes.data;
55
+ // Supabase geeft standaard max. 1000 rijen terug. Voor productgroepen kan de
56
+ // relevante voorraad op een member-key verderop in trade_item_stock staan,
57
+ // dus voorraad pagineren we expliciet volledig uit.
58
+ const stockRows = [];
59
+ const stockPageSize = 1000;
60
+ for (let from = 0; from < 50000; from += stockPageSize) {
61
+ const { data: batch, error } = await context.supabase
62
+ .from("trade_item_stock")
63
+ .select("trade_item_key, physical_stock, reserved_stock, incoming_stock")
64
+ .range(from, from + stockPageSize - 1);
65
+ if (error)
66
+ throw error;
67
+ const rows = Array.isArray(batch) ? batch : [];
68
+ stockRows.push(...rows);
69
+ if (rows.length < stockPageSize)
70
+ break;
71
+ }
39
72
  const stockByKey = new Map();
40
- for (const s of Array.isArray(stockRows) ? stockRows : []) {
73
+ for (const s of stockRows) {
41
74
  const k = s?.trade_item_key;
42
75
  if (typeof k !== "string")
43
76
  continue;
@@ -54,35 +87,278 @@ export const listProductsForCreateOrder = createServerFn({ method: "GET" })
54
87
  return n;
55
88
  return String(n.nl ?? n.en ?? n.de ?? Object.values(n)[0] ?? "");
56
89
  };
57
- const items = (Array.isArray(tradeItems) ? tradeItems : [])
58
- .map((r) => {
90
+ const byId = new Map();
91
+ // Dedup-index op logische identiteit (article_code of genormaliseerde naam),
92
+ // zodat hetzelfde product niet twee keer verschijnt als het zowel als
93
+ // Floriday-item, draft én products-rij bestaat.
94
+ const identityIndex = new Map(); // identityKey -> byId key
95
+ const norm = (s) => s.trim().toLowerCase().replace(/\s+/g, " ");
96
+ const identityKeysFor = (name, articleCode) => {
97
+ const keys = [];
98
+ if (articleCode && articleCode.trim())
99
+ keys.push(`ac:${norm(articleCode)}`);
100
+ if (name && name.trim())
101
+ keys.push(`nm:${norm(name)}`);
102
+ return keys;
103
+ };
104
+ const registerIdentity = (id, name, articleCode) => {
105
+ for (const k of identityKeysFor(name, articleCode)) {
106
+ if (!identityIndex.has(k))
107
+ identityIndex.set(k, id);
108
+ }
109
+ };
110
+ const findExistingId = (name, articleCode) => {
111
+ for (const k of identityKeysFor(name, articleCode)) {
112
+ const hit = identityIndex.get(k);
113
+ if (hit)
114
+ return hit;
115
+ }
116
+ return null;
117
+ };
118
+ const stockOf = (key) => {
119
+ const s = stockByKey.get(key);
120
+ const physical = s?.physical ?? 0;
121
+ const reserved = s?.reserved ?? 0;
122
+ return { physical, reserved, available: Math.max(0, physical - reserved) };
123
+ };
124
+ const stockOfAliases = (keys) => {
125
+ let fallback = null;
126
+ for (const k of keys) {
127
+ const s = stockByKey.get(k);
128
+ if (!s)
129
+ continue;
130
+ fallback ?? (fallback = s);
131
+ if (s.physical !== 0 || s.reserved !== 0 || s.incoming !== 0)
132
+ return s;
133
+ }
134
+ return fallback ?? { physical: 0, reserved: 0, incoming: 0 };
135
+ };
136
+ // Bouw index: draft_id/floriday_id → mirror products.id. De voorraad kan
137
+ // op de mirror-key (`product:<mirrorId>`) staan i.p.v. op `draft:<id>`
138
+ // of het bare Floriday-id.
139
+ const mirrorProductIdsByDraft = new Map();
140
+ const mirrorProductIdsByFloriday = new Map();
141
+ for (const p of Array.isArray(productsRes.data) ? productsRes.data : []) {
142
+ const cf = (p.custom_fields ?? {});
143
+ const pid = p.id;
144
+ const draftId = typeof cf.draft_id === "string" ? cf.draft_id : null;
145
+ const floridayId = typeof cf.floriday_id === "string" ? cf.floriday_id : null;
146
+ if (draftId) {
147
+ const arr = mirrorProductIdsByDraft.get(draftId) ?? [];
148
+ arr.push(pid);
149
+ mirrorProductIdsByDraft.set(draftId, arr);
150
+ }
151
+ if (floridayId) {
152
+ const arr = mirrorProductIdsByFloriday.get(floridayId) ?? [];
153
+ arr.push(pid);
154
+ mirrorProductIdsByFloriday.set(floridayId, arr);
155
+ }
156
+ }
157
+ // 1) Floriday trade items
158
+ for (const r of Array.isArray(tradeItemsRes.data) ? tradeItemsRes.data : []) {
59
159
  const d = r?.data ?? {};
60
160
  const name = pickName(d.tradeItemName) || d.tradeItemReference || "(naamloos)";
61
161
  const packs = Array.isArray(d.packingConfigurations) ? d.packingConfigurations : [];
62
- const primary = packs.find((p) => p?.primary) ?? packs[0] ?? {};
63
- const ppc = Math.max(1, Number(primary?.piecesPerPackage ?? 1));
64
- const cpl = Math.max(1, Number(primary?.packagesPerLayer ?? 1));
65
- const lpc = Math.max(1, Number(primary?.layersPerLoadCarrier ?? 1));
66
- const stock = stockByKey.get(r.floriday_id);
67
- const physical = stock?.physical ?? 0;
68
- const reserved = stock?.reserved ?? 0;
162
+ const primary = packs.find((p) => p?.primary || p?.isPrimary) ?? packs[0] ?? {};
163
+ const id = r.floriday_id;
164
+ const articleCode = d.supplierArticleCode ?? null;
165
+ const mirrorKeys = (mirrorProductIdsByFloriday.get(id) ?? []).map((pid) => `product:${pid}`);
166
+ const sAgg = stockOfAliases([id, ...mirrorKeys]);
167
+ const physical = sAgg.physical;
168
+ const reserved = sAgg.reserved;
69
169
  const available = Math.max(0, physical - reserved);
70
- return {
71
- id: r.floriday_id,
170
+ byId.set(id, {
171
+ id,
72
172
  product: name,
73
- article_code: d.supplierArticleCode ?? null,
173
+ article_code: articleCode,
74
174
  unit: "stuks",
75
175
  sale_price: null,
76
- plants_per_crate: ppc,
77
- crates_per_layer: cpl,
78
- layers_per_cart: lpc,
176
+ plants_per_crate: Math.max(1, Number(primary?.piecesPerPackage ?? primary?.unitsPerPackage ?? 1)),
177
+ crates_per_layer: Math.max(1, Number(primary?.packagesPerLayer ?? 1)),
178
+ layers_per_cart: Math.max(1, Number(primary?.layersPerLoadCarrier ?? 1)),
79
179
  physical_stock: physical,
80
180
  reserved_stock: reserved,
81
181
  available_stock: available,
82
182
  in_stock: available > 0,
83
- };
84
- })
85
- .sort((a, b) => {
183
+ source: "floriday",
184
+ });
185
+ registerIdentity(id, name, articleCode);
186
+ }
187
+ // 2) Interne concept-artikelen (BV-codes, Excel-import, etc.)
188
+ for (const r of Array.isArray(draftsRes.data) ? draftsRes.data : []) {
189
+ const d = r?.data ?? {};
190
+ // Sla drafts over die al als Floriday-item verschijnen
191
+ const linkedFloridayId = r.floriday_id;
192
+ if (linkedFloridayId && byId.has(linkedFloridayId))
193
+ continue;
194
+ const id = r.id;
195
+ const name = pickName(d.name) || pickName(d.tradeItemName) || d.articleCode || "(naamloos concept)";
196
+ const articleCode = d.articleCode ?? d.supplierArticleCode ?? null;
197
+ // Skip als er al een logisch identiek item bestaat (zelfde artikelcode of naam)
198
+ if (findExistingId(name, articleCode))
199
+ continue;
200
+ const packs = Array.isArray(d.packingConfigurations) ? d.packingConfigurations : [];
201
+ const primary = packs.find((p) => p?.primary || p?.isPrimary) ?? packs[0] ?? {};
202
+ const draftMirrorKeys = (mirrorProductIdsByDraft.get(id) ?? []).map((pid) => `product:${pid}`);
203
+ const sAggD = stockOfAliases([`draft:${id}`, ...draftMirrorKeys]);
204
+ const physical = sAggD.physical;
205
+ const reserved = sAggD.reserved;
206
+ const available = Math.max(0, physical - reserved);
207
+ byId.set(id, {
208
+ id,
209
+ product: name,
210
+ article_code: articleCode,
211
+ unit: "stuks",
212
+ sale_price: typeof d.salePricePerPiece === "number" ? d.salePricePerPiece : null,
213
+ // Drafts gebruiken Floriday-veldnamen: unitsPerPackage / packagesPerLayer / layersPerLoadCarrier
214
+ plants_per_crate: Math.max(1, Number(primary?.unitsPerPackage ?? primary?.piecesPerPackage ?? 1)),
215
+ crates_per_layer: Math.max(1, Number(primary?.packagesPerLayer ?? 1)),
216
+ layers_per_cart: Math.max(1, Number(primary?.layersPerLoadCarrier ?? 1)),
217
+ physical_stock: physical,
218
+ reserved_stock: reserved,
219
+ available_stock: available,
220
+ in_stock: available > 0,
221
+ source: "draft",
222
+ });
223
+ registerIdentity(id, name, articleCode);
224
+ }
225
+ // 3) Productgroepen (samenstellingen) — belading komt van het eerste
226
+ // (hoogst gerangschikte) product uit de verkoopranglijst binnen de groep.
227
+ // Bij een wijziging in sort_order past de belading zich automatisch aan
228
+ // omdat we hier live opnieuw kijken naar de bovenste rij.
229
+ const groupItemsByGroup = new Map();
230
+ for (const gi of Array.isArray(groupItemsRes.data) ? groupItemsRes.data : []) {
231
+ const gid = gi.product_group_id;
232
+ const arr = groupItemsByGroup.get(gid) ?? [];
233
+ arr.push({
234
+ trade_item_id: gi.trade_item_id,
235
+ sort_order: Number(gi.sort_order ?? 0),
236
+ });
237
+ groupItemsByGroup.set(gid, arr);
238
+ }
239
+ // Products-rijen ook indexeren, zodat groep-members die naar een products.id
240
+ // wijzen (i.p.v. floriday_id of draft-id) alsnog gevonden worden.
241
+ const productsById = new Map();
242
+ for (const p of Array.isArray(productsRes.data) ? productsRes.data : []) {
243
+ productsById.set(p.id, p);
244
+ }
245
+ const packingFromProductRow = (p) => ({
246
+ plants_per_crate: Math.max(1, Number(p?.plants_per_crate ?? 1)),
247
+ crates_per_layer: Math.max(1, Number(p?.crates_per_layer ?? 1)),
248
+ layers_per_cart: Math.max(1, Number(p?.layers_per_cart ?? 1)),
249
+ });
250
+ for (const r of Array.isArray(groupsRes.data) ? groupsRes.data : []) {
251
+ const id = r.id;
252
+ if (byId.has(id))
253
+ continue;
254
+ const groupName = r.name ?? "(naamloos)";
255
+ // Groepen laten we niet dupliceren met een gelijknamig product/draft:
256
+ // de groep wint (want geeft samenstelling). Verwijder een eerder toegevoegd
257
+ // los product met dezelfde identiteit.
258
+ const displayName = `[Groep] ${groupName}`;
259
+ const dup = findExistingId(groupName, null);
260
+ if (dup && byId.has(dup))
261
+ byId.delete(dup);
262
+ // Voorraad: eigen key `group:<id>` óf gesommeerd over de members
263
+ // (voorraad kan op de member-id, `draft:<id>` of `product:<id>` staan).
264
+ let { physical, reserved, available } = stockOf(`group:${id}`);
265
+ const members = (groupItemsByGroup.get(id) ?? [])
266
+ .slice()
267
+ .sort((a, b) => a.sort_order - b.sort_order);
268
+ if (physical === 0 && reserved === 0) {
269
+ let phySum = 0;
270
+ let resSum = 0;
271
+ for (const m of members) {
272
+ const s = stockOfAliases([`draft:${m.trade_item_id}`, `product:${m.trade_item_id}`, m.trade_item_id]);
273
+ phySum += s.physical;
274
+ resSum += s.reserved;
275
+ }
276
+ physical = phySum;
277
+ reserved = resSum;
278
+ available = Math.max(0, physical - reserved);
279
+ }
280
+ // Probeer belading van de bovenste member; val terug op head_trade_item /
281
+ // head_product_id als de member niet in byId of products staat.
282
+ let packing = { plants_per_crate: 1, crates_per_layer: 1, layers_per_cart: 1 };
283
+ const candidates = [
284
+ members[0]?.trade_item_id ?? null,
285
+ r.head_trade_item_floriday_id ?? null,
286
+ r.head_product_id ?? null,
287
+ ];
288
+ for (const cand of candidates) {
289
+ if (!cand)
290
+ continue;
291
+ const fromBy = byId.get(cand);
292
+ if (fromBy) {
293
+ packing = {
294
+ plants_per_crate: fromBy.plants_per_crate,
295
+ crates_per_layer: fromBy.crates_per_layer,
296
+ layers_per_cart: fromBy.layers_per_cart,
297
+ };
298
+ break;
299
+ }
300
+ const fromProd = productsById.get(cand);
301
+ if (fromProd) {
302
+ packing = packingFromProductRow(fromProd);
303
+ break;
304
+ }
305
+ }
306
+ byId.set(id, {
307
+ id,
308
+ product: displayName,
309
+ article_code: null,
310
+ unit: "stuks",
311
+ sale_price: null,
312
+ plants_per_crate: packing.plants_per_crate,
313
+ crates_per_layer: packing.crates_per_layer,
314
+ layers_per_cart: packing.layers_per_cart,
315
+ physical_stock: physical,
316
+ reserved_stock: reserved,
317
+ available_stock: available,
318
+ in_stock: available > 0,
319
+ source: "group",
320
+ });
321
+ registerIdentity(id, groupName, null);
322
+ }
323
+ // 4) Losse voorraadartikelen (products-tabel) — fallback
324
+ for (const r of Array.isArray(productsRes.data) ? productsRes.data : []) {
325
+ const id = r.id;
326
+ if (byId.has(id))
327
+ continue;
328
+ const cf = (r.custom_fields ?? {});
329
+ // Sla mirrors van Floriday/draft items over
330
+ if (typeof cf.floriday_id === "string" && byId.has(cf.floriday_id))
331
+ continue;
332
+ if (typeof cf.draft_id === "string" && byId.has(cf.draft_id))
333
+ continue;
334
+ const name = r.product ?? "(naamloos)";
335
+ const articleCode = cf.article_code ?? null;
336
+ // Skip als er al een logisch identiek item (draft/floriday/group) bestaat
337
+ if (findExistingId(name, articleCode))
338
+ continue;
339
+ const stockA = stockOf(`product:${id}`);
340
+ const stockB = stockOf(id);
341
+ const physical = stockA.physical || stockB.physical || Number(r.quantity ?? 0);
342
+ const reserved = stockA.reserved || stockB.reserved || 0;
343
+ const available = Math.max(0, physical - reserved);
344
+ byId.set(id, {
345
+ id,
346
+ product: name,
347
+ article_code: articleCode,
348
+ unit: r.unit ?? "stuks",
349
+ sale_price: r.sale_price ?? null,
350
+ plants_per_crate: Math.max(1, Number(r.plants_per_crate ?? 1)),
351
+ crates_per_layer: Math.max(1, Number(r.crates_per_layer ?? 1)),
352
+ layers_per_cart: Math.max(1, Number(r.layers_per_cart ?? 1)),
353
+ physical_stock: physical,
354
+ reserved_stock: reserved,
355
+ available_stock: available,
356
+ in_stock: available > 0,
357
+ source: "product",
358
+ });
359
+ registerIdentity(id, name, articleCode);
360
+ }
361
+ const items = Array.from(byId.values()).sort((a, b) => {
86
362
  if (a.in_stock !== b.in_stock)
87
363
  return a.in_stock ? -1 : 1;
88
364
  return a.product.localeCompare(b.product, "nl");
@@ -172,7 +448,8 @@ const CreateManualOrderInput = z.object({
172
448
  deliveryLocationId: z.string().uuid(),
173
449
  pickupLocationId: z.string().min(1),
174
450
  carrierId: z.string().uuid(),
175
- deliveryDate: z.string().min(1).max(50),
451
+ deliveryDate: z.string().min(1).max(50).nullable().optional(),
452
+ isReservation: z.boolean().optional(),
176
453
  notes: z.string().max(2000).nullable().optional(),
177
454
  lines: z.array(LineInput).min(1),
178
455
  });
@@ -199,38 +476,106 @@ export const createManualOrder = createServerFn({ method: "POST" })
199
476
  .limit(1).maybeSingle();
200
477
  const pickDays = Math.max(0, Number(settings?.pick_days_before_transport ?? 0));
201
478
  const earliestISO = toISODate(addCalendarDays(new Date(), pickDays + carrierDeliveryDays));
202
- if (data.deliveryDate < earliestISO) {
203
- throw new Error(`Leverdatum (${data.deliveryDate}) ligt voor de eerst mogelijke datum (${earliestISO}) o.b.v. orderpicktijd (${pickDays}d) + levertijd vervoerder (${carrierDeliveryDays}d).`);
479
+ if (!data.isReservation) {
480
+ if (!data.deliveryDate)
481
+ throw new Error("Leverdatum ontbreekt.");
482
+ if (data.deliveryDate < earliestISO) {
483
+ throw new Error(`Leverdatum (${data.deliveryDate}) ligt voor de eerst mogelijke datum (${earliestISO}) o.b.v. orderpicktijd (${pickDays}d) + levertijd vervoerder (${carrierDeliveryDays}d).`);
484
+ }
204
485
  }
205
486
  const { supabaseAdmin } = await import("../integrations/supabase/client.server");
206
- // Zorg dat elk product_id (= trade_item_key uit voorraad) een rij in
207
- // `products` heeft — de order_items FK vereist dit én de karindeling
487
+ // Zorg dat elk product_id (= identiteit uit de voorraadmodule) een rij
488
+ // in `products` heeft — de order_items FK vereist dit én de karindeling
208
489
  // (planAndSaveAutoCarts) joined op products voor de packing-waarden.
490
+ //
491
+ // BELANGRIJK: de wizard geeft als product_id een identiteit terug die uit
492
+ // 1 van 4 bronnen komt: floriday_trade_items_cache.floriday_id,
493
+ // trade_item_drafts.id, product_groups.id, of products.id. Om te
494
+ // voorkomen dat de voorraadmodule onze "spiegelrijen" verwart met
495
+ // eigen data, zetten we per bron het juiste `trade_item_key`-formaat,
496
+ // een niet-misleidend `article_number` en een duidelijke `source` in
497
+ // `custom_fields` (zodat de voorraadmodule ze veilig kan negeren of
498
+ // opruimen).
209
499
  const productIds = Array.from(new Set(data.lines.map((l) => l.product_id)));
210
- const { data: existingProducts } = await supabaseAdmin
211
- .from("products").select("id").in("id", productIds);
212
- const existingIds = new Set((existingProducts ?? []).map((p) => p.id));
500
+ const [existingProductsRes, draftsRes, floridayRes, groupsRes] = await Promise.all([
501
+ supabaseAdmin.from("products").select("id").in("id", productIds),
502
+ supabaseAdmin.from("trade_item_drafts").select("id, data").in("id", productIds),
503
+ supabaseAdmin.from("floriday_trade_items_cache").select("floriday_id, data").in("floriday_id", productIds),
504
+ supabaseAdmin.from("product_groups").select("id, name").in("id", productIds),
505
+ ]);
506
+ const existingIds = new Set((existingProductsRes.data ?? []).map((p) => p.id));
507
+ const draftById = new Map();
508
+ for (const r of draftsRes.data ?? [])
509
+ draftById.set(r.id, r.data ?? {});
510
+ const floridayById = new Map();
511
+ for (const r of floridayRes.data ?? [])
512
+ floridayById.set(r.floriday_id, r.data ?? {});
513
+ const groupById = new Map();
514
+ for (const r of groupsRes.data ?? [])
515
+ groupById.set(r.id, r);
213
516
  const missingLines = data.lines.filter((l) => !existingIds.has(l.product_id));
214
517
  if (missingLines.length > 0) {
518
+ const shortRand = () => Array.from({ length: 6 }, () => Math.floor(Math.random() * 16).toString(16).toUpperCase()).join("");
215
519
  const seen = new Set();
216
520
  const toInsert = missingLines.filter((l) => {
217
521
  if (seen.has(l.product_id))
218
522
  return false;
219
523
  seen.add(l.product_id);
220
524
  return true;
221
- }).map((l) => ({
222
- id: l.product_id,
223
- product: l.product_name,
224
- batch: "",
225
- location: "",
226
- unit: l.unit ?? "stuks",
227
- plants_per_crate: Math.max(1, Number(l.plants_per_crate ?? 1)),
228
- crates_per_layer: Math.max(1, Number(l.crates_per_layer ?? 1)),
229
- layers_per_cart: Math.max(1, Number(l.layers_per_cart ?? 1)),
230
- sale_price: l.unit_price ?? null,
231
- trade_item_key: l.product_id,
232
- custom_fields: { floriday_id: l.product_id, source: "auto-from-voorraad" },
233
- }));
525
+ }).map((l) => {
526
+ const id = l.product_id;
527
+ let source = "unknown";
528
+ let tradeItemKey = id;
529
+ let articleNumber = null;
530
+ const customFields = {};
531
+ if (draftById.has(id)) {
532
+ source = "draft";
533
+ const draft = draftById.get(id) ?? {};
534
+ tradeItemKey = `draft:${id}`;
535
+ articleNumber = (typeof draft.articleCode === "string" && draft.articleCode.trim())
536
+ ? String(draft.articleCode).trim()
537
+ : `DRAFT-${shortRand()}`;
538
+ customFields.draft_id = id;
539
+ customFields.source = "sales-module-draft-mirror";
540
+ }
541
+ else if (floridayById.has(id)) {
542
+ source = "floriday";
543
+ const fd = floridayById.get(id) ?? {};
544
+ tradeItemKey = id; // Floriday-format is de kale floriday_id
545
+ articleNumber = (typeof fd.supplierArticleCode === "string" && fd.supplierArticleCode.trim())
546
+ ? String(fd.supplierArticleCode).trim()
547
+ : `FLR-${shortRand()}`;
548
+ customFields.floriday_id = id;
549
+ customFields.source = "sales-module-floriday-mirror";
550
+ }
551
+ else if (groupById.has(id)) {
552
+ source = "group";
553
+ tradeItemKey = `group:${id}`;
554
+ articleNumber = l.article_code?.trim() || `GRP-${shortRand()}`;
555
+ customFields.group_id = id;
556
+ customFields.source = "sales-module-group-mirror";
557
+ }
558
+ else {
559
+ source = "unknown";
560
+ tradeItemKey = `product:${id}`;
561
+ articleNumber = l.article_code?.trim() || `ART-${shortRand()}`;
562
+ customFields.source = "sales-module-unknown-mirror";
563
+ }
564
+ return {
565
+ id,
566
+ product: l.product_name,
567
+ batch: "",
568
+ location: "",
569
+ unit: l.unit ?? "stuks",
570
+ plants_per_crate: Math.max(1, Number(l.plants_per_crate ?? 1)),
571
+ crates_per_layer: Math.max(1, Number(l.crates_per_layer ?? 1)),
572
+ layers_per_cart: Math.max(1, Number(l.layers_per_cart ?? 1)),
573
+ sale_price: l.unit_price ?? null,
574
+ trade_item_key: tradeItemKey,
575
+ article_number: articleNumber,
576
+ custom_fields: { ...customFields, mirror_kind: source },
577
+ };
578
+ });
234
579
  const { error: prodErr } = await supabaseAdmin.from("products").insert(toInsert);
235
580
  if (prodErr)
236
581
  throw new Error(`Product aanmaken faalde: ${prodErr.message}`);
@@ -240,8 +585,9 @@ export const createManualOrder = createServerFn({ method: "POST" })
240
585
  customer_name: data.customerName,
241
586
  customer_id: data.customerId,
242
587
  order_date: new Date().toISOString().slice(0, 10),
243
- delivery_date: data.deliveryDate,
244
- status: "nieuw",
588
+ delivery_date: data.isReservation ? null : data.deliveryDate,
589
+ is_reservation: !!data.isReservation,
590
+ status: data.isReservation ? "gereserveerd" : "nieuw",
245
591
  total: String(total.toFixed(2)),
246
592
  notes: data.notes ?? null,
247
593
  custom_fields: {
@@ -249,6 +595,7 @@ export const createManualOrder = createServerFn({ method: "POST" })
249
595
  delivery_location: data.deliveryLocationId,
250
596
  pickup_location: data.pickupLocationId,
251
597
  carrier: data.carrierId,
598
+ is_reservation: !!data.isReservation,
252
599
  },
253
600
  }).select("id, order_number").single();
254
601
  if (orderErr)
@@ -275,6 +622,112 @@ export const createManualOrder = createServerFn({ method: "POST" })
275
622
  void context; // reserveren indien later audit-log nodig is
276
623
  return { ok: true, orderId: order.id };
277
624
  });
625
+ // ─── Verwijderen van een lokale bestelling ────────────────────────────────
626
+ // Draait alle voorraadmutaties van deze order terug (fysiek + gereserveerd)
627
+ // en verwijdert de order (order_items, order_carts en cart_replan_queue via
628
+ // cascade). FO-orders (Floriday) worden geweigerd — die kunnen alleen via
629
+ // Floriday zelf worden geannuleerd.
630
+ export const deleteLocalOrder = createServerFn({ method: "POST" })
631
+ .middleware([requireSupabaseAuth])
632
+ .inputValidator((input) => z.object({ orderId: z.string().uuid() }).parse(input))
633
+ .handler(async ({ data, context }) => {
634
+ const { supabaseAdmin } = await import("../integrations/supabase/client.server");
635
+ const { data: order, error: oErr } = await supabaseAdmin
636
+ .from("orders")
637
+ .select("id, custom_fields, order_source")
638
+ .eq("id", data.orderId)
639
+ .maybeSingle();
640
+ if (oErr)
641
+ throw new Error(oErr.message);
642
+ if (!order)
643
+ throw new Error("Bestelling niet gevonden");
644
+ const cf = order.custom_fields ?? {};
645
+ const src = String(cf?.source ?? order.order_source ?? "").toLowerCase();
646
+ if (src === "floriday" || src === "fo") {
647
+ throw new Error("FO-orders kunnen niet verwijderd worden. Annuleer deze in Floriday.");
648
+ }
649
+ // Voorraadmutaties terugdraaien.
650
+ const { data: mutations, error: mErr } = await supabaseAdmin
651
+ .from("stock_mutation_log")
652
+ .select("trade_item_key, qty_delta, physical_before, physical_after, reserved_before, reserved_after")
653
+ .eq("order_id", data.orderId);
654
+ if (mErr)
655
+ throw new Error(`Voorraadmutaties ophalen faalde: ${mErr.message}`);
656
+ const perKey = new Map();
657
+ for (const m of mutations ?? []) {
658
+ const key = m.trade_item_key;
659
+ if (!key)
660
+ continue;
661
+ const pd = Number(m.physical_after ?? 0) - Number(m.physical_before ?? 0);
662
+ const rd = Number(m.reserved_after ?? 0) - Number(m.reserved_before ?? 0);
663
+ const qd = Number(m.qty_delta ?? 0);
664
+ const cur = perKey.get(key) ?? { physicalDelta: 0, reservedDelta: 0, qtyDelta: 0 };
665
+ cur.physicalDelta += pd;
666
+ cur.reservedDelta += rd;
667
+ cur.qtyDelta += qd;
668
+ perKey.set(key, cur);
669
+ }
670
+ const reversalRows = [];
671
+ for (const [key, agg] of perKey.entries()) {
672
+ if (agg.physicalDelta === 0 && agg.reservedDelta === 0)
673
+ continue;
674
+ const { data: stockRows } = await supabaseAdmin
675
+ .from("trade_item_stock")
676
+ .select("id, connection_id, physical_stock, reserved_stock")
677
+ .eq("trade_item_key", key);
678
+ for (const s of stockRows ?? []) {
679
+ const physBefore = Number(s.physical_stock ?? 0);
680
+ const resvBefore = Number(s.reserved_stock ?? 0);
681
+ const physAfter = physBefore - agg.physicalDelta;
682
+ const resvAfter = Math.max(0, resvBefore - agg.reservedDelta);
683
+ const { error: uErr } = await supabaseAdmin.from("trade_item_stock")
684
+ .update({
685
+ physical_stock: physAfter,
686
+ reserved_stock: resvAfter,
687
+ updated_at: new Date().toISOString(),
688
+ })
689
+ .eq("id", s.id);
690
+ if (uErr)
691
+ throw new Error(`Voorraad terugdraaien faalde: ${uErr.message}`);
692
+ reversalRows.push({
693
+ trade_item_key: key,
694
+ connection_id: s.connection_id,
695
+ order_id: data.orderId,
696
+ order_source: src || "manual",
697
+ mutation_type: "reversal",
698
+ qty_delta: -agg.qtyDelta,
699
+ physical_before: physBefore,
700
+ physical_after: physAfter,
701
+ reserved_before: resvBefore,
702
+ reserved_after: resvAfter,
703
+ reason: "Bestelling verwijderd — mutaties teruggedraaid",
704
+ created_by: context.userId,
705
+ });
706
+ }
707
+ }
708
+ if (reversalRows.length > 0) {
709
+ const { error: lErr } = await supabaseAdmin.from("stock_mutation_log").insert(reversalRows);
710
+ if (lErr)
711
+ throw new Error(`Reversal-log schrijven faalde: ${lErr.message}`);
712
+ }
713
+ // Eerst order_items verwijderen — de AFTER DELETE trigger op order_items
714
+ // enqueue't naar cart_replan_queue met order_id, dus de orders-rij moet
715
+ // op dat moment nog bestaan (anders FK-violation).
716
+ await supabaseAdmin.from("order_items").delete().eq("order_id", data.orderId);
717
+ // Queue-entries opruimen die de trigger zojuist heeft aangemaakt.
718
+ await supabaseAdmin.from("cart_replan_queue").delete().eq("order_id", data.orderId);
719
+ const { error: dErr } = await supabaseAdmin
720
+ .from("orders")
721
+ .delete()
722
+ .eq("id", data.orderId);
723
+ if (dErr)
724
+ throw new Error(`Bestelling verwijderen faalde: ${dErr.message}`);
725
+ return {
726
+ ok: true,
727
+ reversedKeys: perKey.size,
728
+ reversalEntries: reversalRows.length,
729
+ };
730
+ });
278
731
  export const planAndSaveAutoCarts = createServerFn({ method: "POST" })
279
732
  .middleware([requireSupabaseAuth])
280
733
  .inputValidator((input) => z.object({ orderId: z.string().uuid() }).parse(input))
@@ -424,3 +877,290 @@ export const saveManualCarts = createServerFn({ method: "POST" })
424
877
  }
425
878
  return { ok: true };
426
879
  });
880
+ // ─── Lokale bestelling bewerken (aantal + leverdatum) ─────────────────────
881
+ // Alleen voor lokale/handmatige bestellingen. FO-orders worden geweigerd.
882
+ // Stock (reserved) en karindeling worden automatisch bijgewerkt door
883
+ // DB-triggers op order_items / orders (trg_order_items_reserve,
884
+ // trg_orders_recompute_reserved, orders_replan_trg, order_items_replan_trg).
885
+ const UpdateLocalOrderInput = z.object({
886
+ orderId: z.string().uuid(),
887
+ deliveryDate: z.string().min(1).max(50).optional(),
888
+ items: z
889
+ .array(z.object({
890
+ itemId: z.string().uuid(),
891
+ quantity: z.number().int().positive(),
892
+ }))
893
+ .optional(),
894
+ });
895
+ export const getLocalOrderForEdit = createServerFn({ method: "POST" })
896
+ .middleware([requireSupabaseAuth])
897
+ .inputValidator((input) => z.object({ orderId: z.string().uuid() }).parse(input))
898
+ .handler(async ({ data, context }) => {
899
+ const { data: order, error: oErr } = await context.supabase
900
+ .from("orders")
901
+ .select("id, delivery_date, custom_fields, order_source, order_number, is_reservation, status")
902
+ .eq("id", data.orderId)
903
+ .maybeSingle();
904
+ if (oErr)
905
+ throw new Error(oErr.message);
906
+ if (!order)
907
+ throw new Error("Bestelling niet gevonden");
908
+ const cf = order.custom_fields ?? {};
909
+ const src = String(cf?.source ?? order.order_source ?? "").toLowerCase();
910
+ if (src === "floriday" || src === "fo") {
911
+ throw new Error("FO-orders kunnen niet bewerkt worden.");
912
+ }
913
+ const { data: items, error: iErr } = await context.supabase
914
+ .from("order_items")
915
+ .select("id, product_id, product_name, quantity, unit, products(plants_per_crate)")
916
+ .eq("order_id", data.orderId)
917
+ .order("id");
918
+ if (iErr)
919
+ throw new Error(iErr.message);
920
+ // Voor group order-items (en items die naar drafts/trade_items verwijzen)
921
+ // levert de products-join niets op. Resolve belading via:
922
+ // 1) product_groups → product_group_items → belading van eerste member
923
+ // (via products / trade_item_drafts / floriday_trade_items_cache)
924
+ // 2) direct via drafts/cache voor niet-group items
925
+ const rows = (items ?? []);
926
+ const GROUP_PREFIX = /^\s*\[Groep\]\s*/i;
927
+ // Verzamel group-ids (per id of per naam)
928
+ const groupCandidateIds = rows
929
+ .filter((it) => it.product_id && !it.products)
930
+ .map((it) => it.product_id);
931
+ const groupNameCandidates = Array.from(new Set(rows
932
+ .filter((it) => !it.product_id && GROUP_PREFIX.test(it.product_name ?? ""))
933
+ .map((it) => it.product_name.replace(GROUP_PREFIX, "").trim())
934
+ .filter(Boolean)));
935
+ const groupIdSet = new Set();
936
+ const groupIdByName = new Map();
937
+ if (groupCandidateIds.length || groupNameCandidates.length) {
938
+ const [byId, byName] = await Promise.all([
939
+ groupCandidateIds.length
940
+ ? context.supabase.from("product_groups").select("id, name").in("id", groupCandidateIds)
941
+ : Promise.resolve({ data: [] }),
942
+ groupNameCandidates.length
943
+ ? context.supabase.from("product_groups").select("id, name").in("name", groupNameCandidates)
944
+ : Promise.resolve({ data: [] }),
945
+ ]);
946
+ for (const g of byId.data ?? [])
947
+ groupIdSet.add(g.id);
948
+ for (const g of byName.data ?? []) {
949
+ groupIdSet.add(g.id);
950
+ groupIdByName.set(g.name.trim().toLowerCase(), g.id);
951
+ }
952
+ }
953
+ // Members per group
954
+ const memberMap = new Map();
955
+ const memberIds = new Set();
956
+ if (groupIdSet.size) {
957
+ const { data: gis } = await context.supabase
958
+ .from("product_group_items")
959
+ .select("product_group_id, trade_item_id, sort_order")
960
+ .in("product_group_id", Array.from(groupIdSet));
961
+ for (const gi of gis ?? []) {
962
+ const gid = gi.product_group_id;
963
+ const arr = memberMap.get(gid) ?? [];
964
+ arr.push({
965
+ trade_item_id: gi.trade_item_id,
966
+ sort_order: Number(gi.sort_order ?? 0),
967
+ });
968
+ memberMap.set(gid, arr);
969
+ memberIds.add(gi.trade_item_id);
970
+ }
971
+ }
972
+ // Ook non-group items zonder products-join proberen we via drafts/cache.
973
+ const directIds = rows
974
+ .filter((it) => it.product_id && !it.products && !groupIdSet.has(it.product_id))
975
+ .map((it) => it.product_id);
976
+ for (const id of directIds)
977
+ memberIds.add(id);
978
+ const ppcById = new Map();
979
+ if (memberIds.size) {
980
+ const ids = Array.from(memberIds);
981
+ const [prodRes, draftRes, tradeRes] = await Promise.all([
982
+ context.supabase
983
+ .from("products")
984
+ .select("id, plants_per_crate, custom_fields")
985
+ .or(`id.in.(${ids.join(",")}),custom_fields->>floriday_id.in.(${ids.join(",")})`),
986
+ context.supabase.from("trade_item_drafts").select("id, data").in("id", ids),
987
+ context.supabase.from("floriday_trade_items_cache").select("floriday_id, data").in("floriday_id", ids),
988
+ ]);
989
+ for (const p of prodRes.data ?? []) {
990
+ const ppc = Math.max(1, Number(p.plants_per_crate ?? 1));
991
+ ppcById.set(p.id, ppc);
992
+ const fid = p.custom_fields?.floriday_id;
993
+ if (fid && !ppcById.has(fid))
994
+ ppcById.set(fid, ppc);
995
+ }
996
+ for (const d of draftRes.data ?? []) {
997
+ if (ppcById.has(d.id))
998
+ continue;
999
+ const cfg = Array.isArray(d.data?.packingConfigurations)
1000
+ ? (d.data.packingConfigurations.find((c) => c?.isPrimary) ??
1001
+ d.data.packingConfigurations[0])
1002
+ : null;
1003
+ const ppc = Math.max(1, Number(cfg?.unitsPerPackage ?? cfg?.piecesPerPackage ?? 1));
1004
+ if (ppc > 1)
1005
+ ppcById.set(d.id, ppc);
1006
+ }
1007
+ for (const t of tradeRes.data ?? []) {
1008
+ if (ppcById.has(t.floriday_id))
1009
+ continue;
1010
+ const cfg = t.data?.packingConfiguration;
1011
+ const ppc = Math.max(1, Number(cfg?.piecesPerPackage ?? cfg?.unitsPerPackage ?? 1));
1012
+ if (ppc > 1)
1013
+ ppcById.set(t.floriday_id, ppc);
1014
+ }
1015
+ }
1016
+ const groupPpc = (groupId) => {
1017
+ const members = (memberMap.get(groupId) ?? []).slice().sort((a, b) => a.sort_order - b.sort_order);
1018
+ for (const m of members) {
1019
+ const p = ppcById.get(m.trade_item_id);
1020
+ if (p && p > 1)
1021
+ return p;
1022
+ }
1023
+ return 1;
1024
+ };
1025
+ // Reeds geleverde/geplande aantallen per order-item — nodig zodat het
1026
+ // bewerkscherm niet onder deze ondergrens kan gaan.
1027
+ const itemIds = rows.map((r) => r.id);
1028
+ const shippedByItem = new Map();
1029
+ const plannedByItem = new Map();
1030
+ if (itemIds.length > 0) {
1031
+ const { data: sRows } = await context.supabase
1032
+ .from("order_shipment_items")
1033
+ .select("order_item_id, quantity, order_shipments!inner(status)")
1034
+ .in("order_item_id", itemIds);
1035
+ for (const r of sRows ?? []) {
1036
+ const st = r.order_shipments?.status;
1037
+ const oi = r.order_item_id;
1038
+ const q = Number(r.quantity ?? 0);
1039
+ if (st === "shipped")
1040
+ shippedByItem.set(oi, (shippedByItem.get(oi) ?? 0) + q);
1041
+ else if (st === "planned")
1042
+ plannedByItem.set(oi, (plannedByItem.get(oi) ?? 0) + q);
1043
+ }
1044
+ }
1045
+ return {
1046
+ orderId: order.id,
1047
+ orderNumber: order.order_number,
1048
+ deliveryDate: order.delivery_date,
1049
+ isReservation: !!order.is_reservation,
1050
+ status: order.status,
1051
+ items: rows.map((it) => {
1052
+ let ppc = Math.max(1, Number(it.products?.plants_per_crate ?? 1));
1053
+ if (ppc === 1 && it.product_id && groupIdSet.has(it.product_id)) {
1054
+ ppc = groupPpc(it.product_id);
1055
+ }
1056
+ else if (ppc === 1 && !it.product_id && GROUP_PREFIX.test(it.product_name ?? "")) {
1057
+ const key = it.product_name.replace(GROUP_PREFIX, "").trim().toLowerCase();
1058
+ const gid = groupIdByName.get(key);
1059
+ if (gid)
1060
+ ppc = groupPpc(gid);
1061
+ }
1062
+ else if (ppc === 1 && it.product_id && ppcById.has(it.product_id)) {
1063
+ ppc = ppcById.get(it.product_id);
1064
+ }
1065
+ const shipped = shippedByItem.get(it.id) ?? 0;
1066
+ const planned = plannedByItem.get(it.id) ?? 0;
1067
+ return {
1068
+ id: it.id,
1069
+ productId: it.product_id,
1070
+ productName: it.product_name,
1071
+ quantity: Number(it.quantity ?? 0),
1072
+ unit: it.unit,
1073
+ plantsPerCrate: ppc,
1074
+ shippedQuantity: shipped,
1075
+ plannedQuantity: planned,
1076
+ minQuantity: shipped + planned,
1077
+ };
1078
+ }),
1079
+ };
1080
+ });
1081
+ export const updateLocalOrder = createServerFn({ method: "POST" })
1082
+ .middleware([requireSupabaseAuth])
1083
+ .inputValidator((input) => UpdateLocalOrderInput.parse(input))
1084
+ .handler(async ({ data }) => {
1085
+ const { supabaseAdmin } = await import("../integrations/supabase/client.server");
1086
+ const { data: order, error: oErr } = await supabaseAdmin
1087
+ .from("orders")
1088
+ .select("id, custom_fields, order_source, is_reservation")
1089
+ .eq("id", data.orderId)
1090
+ .maybeSingle();
1091
+ if (oErr)
1092
+ throw new Error(oErr.message);
1093
+ if (!order)
1094
+ throw new Error("Bestelling niet gevonden");
1095
+ const cf = order.custom_fields ?? {};
1096
+ const src = String(cf?.source ?? order.order_source ?? "").toLowerCase();
1097
+ if (src === "floriday" || src === "fo") {
1098
+ throw new Error("FO-orders kunnen niet bewerkt worden.");
1099
+ }
1100
+ // Valideer per regel dat het nieuwe aantal niet onder de reeds geleverde
1101
+ // of geplande hoeveelheid zakt — anders zouden bezorgmomenten negatief
1102
+ // openstaand krijgen.
1103
+ if (data.items && data.items.length > 0) {
1104
+ const itemIds = data.items.map((i) => i.itemId);
1105
+ const { data: existing } = await supabaseAdmin
1106
+ .from("order_items")
1107
+ .select("id, product_name")
1108
+ .in("id", itemIds);
1109
+ const nameById = new Map((existing ?? []).map((r) => [r.id, r.product_name]));
1110
+ const { data: sRows } = await supabaseAdmin
1111
+ .from("order_shipment_items")
1112
+ .select("order_item_id, quantity, order_shipments!inner(status)")
1113
+ .in("order_item_id", itemIds);
1114
+ const minByItem = new Map();
1115
+ for (const r of sRows ?? []) {
1116
+ const st = r.order_shipments?.status;
1117
+ if (st !== "shipped" && st !== "planned")
1118
+ continue;
1119
+ const oi = r.order_item_id;
1120
+ minByItem.set(oi, (minByItem.get(oi) ?? 0) + Number(r.quantity ?? 0));
1121
+ }
1122
+ for (const it of data.items) {
1123
+ const min = minByItem.get(it.itemId) ?? 0;
1124
+ if (it.quantity < min) {
1125
+ const name = nameById.get(it.itemId) ?? "Regel";
1126
+ throw new Error(`"${name}" kan niet naar ${it.quantity} — er is al ${min} gepland/geleverd op bezorgmomenten. Verwijder of verlaag eerst een bezorgmoment.`);
1127
+ }
1128
+ }
1129
+ }
1130
+ // Leverdatum bijwerken (alleen als niet-reservering).
1131
+ if (data.deliveryDate && !order.is_reservation) {
1132
+ const { error: uErr } = await supabaseAdmin.from("orders")
1133
+ .update({ delivery_date: data.deliveryDate, updated_at: new Date().toISOString() })
1134
+ .eq("id", data.orderId);
1135
+ if (uErr)
1136
+ throw new Error(`Leverdatum bijwerken faalde: ${uErr.message}`);
1137
+ }
1138
+ if (data.items && data.items.length > 0) {
1139
+ for (const it of data.items) {
1140
+ const { error: iErr } = await supabaseAdmin.from("order_items")
1141
+ .update({ quantity: it.quantity })
1142
+ .eq("id", it.itemId)
1143
+ .eq("order_id", data.orderId);
1144
+ if (iErr)
1145
+ throw new Error(`Aantal bijwerken faalde: ${iErr.message}`);
1146
+ }
1147
+ // Hertellen ordertotaal op basis van items × unit_price.
1148
+ const { data: itemsAll } = await supabaseAdmin
1149
+ .from("order_items")
1150
+ .select("quantity, unit_price")
1151
+ .eq("order_id", data.orderId);
1152
+ const total = (itemsAll ?? []).reduce((s, r) => s + Number(r.unit_price ?? 0) * Number(r.quantity ?? 0), 0);
1153
+ await supabaseAdmin.from("orders")
1154
+ .update({ total: String(total.toFixed(2)), updated_at: new Date().toISOString() })
1155
+ .eq("id", data.orderId);
1156
+ }
1157
+ // Karindeling opnieuw draaien zodat het scherm de nieuwe belading toont.
1158
+ try {
1159
+ const { replanOrderCartsFromCron } = await import("./cart-replan.functions");
1160
+ await replanOrderCartsFromCron(data.orderId);
1161
+ }
1162
+ catch (err) {
1163
+ console.error("[updateLocalOrder] karindeling faalde:", err);
1164
+ }
1165
+ return { ok: true };
1166
+ });