@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.
- package/dist-lib/components/verkoop/CreateOrderPage.d.ts.map +1 -1
- package/dist-lib/components/verkoop/CreateOrderPage.js +12 -12
- package/dist-lib/components/verkoop/EditLocalOrderDialog.d.ts +6 -0
- package/dist-lib/components/verkoop/EditLocalOrderDialog.d.ts.map +1 -0
- package/dist-lib/components/verkoop/EditLocalOrderDialog.js +102 -0
- package/dist-lib/components/verkoop/OrderKarindelingCard.d.ts.map +1 -1
- package/dist-lib/components/verkoop/OrderKarindelingCard.js +26 -28
- package/dist-lib/components/verkoop/OrderShipmentsCard.d.ts +5 -0
- package/dist-lib/components/verkoop/OrderShipmentsCard.d.ts.map +1 -0
- package/dist-lib/components/verkoop/OrderShipmentsCard.js +137 -0
- package/dist-lib/components/verkoop/SalesOrderDetailPage.d.ts.map +1 -1
- package/dist-lib/components/verkoop/SalesOrderDetailPage.js +8 -2
- package/dist-lib/components/verkoop/SalesOrderDetailView.d.ts.map +1 -1
- package/dist-lib/components/verkoop/SalesOrderDetailView.js +2 -1
- package/dist-lib/components/verkoop/SalesOrdersFilters.d.ts.map +1 -1
- package/dist-lib/components/verkoop/SalesOrdersFilters.js +2 -8
- package/dist-lib/components/verkoop/SalesOrdersTable.d.ts +4 -1
- package/dist-lib/components/verkoop/SalesOrdersTable.d.ts.map +1 -1
- package/dist-lib/components/verkoop/SalesOrdersTable.js +2 -2
- package/dist-lib/components/verkoop/VerkoopOrdersPage.d.ts.map +1 -1
- package/dist-lib/components/verkoop/VerkoopOrdersPage.js +43 -1
- package/dist-lib/index.js +2 -2
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +607 -169
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +607 -169
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +607 -169
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +623 -164
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/lib/cart-lock.functions.d.ts +607 -169
- package/dist-lib/lib/cart-lock.functions.d.ts.map +1 -1
- package/dist-lib/lib/cart-replan.functions.d.ts +1221 -345
- package/dist-lib/lib/cart-replan.functions.d.ts.map +1 -1
- package/dist-lib/lib/create-order.functions.d.ts +40700 -1397
- package/dist-lib/lib/create-order.functions.d.ts.map +1 -1
- package/dist-lib/lib/create-order.functions.js +792 -52
- package/dist-lib/lib/expand-group-order-items.d.ts +13 -0
- package/dist-lib/lib/expand-group-order-items.d.ts.map +1 -0
- package/dist-lib/lib/expand-group-order-items.js +216 -0
- package/dist-lib/lib/floriday-auto-fulfillment.functions.d.ts +1767 -453
- package/dist-lib/lib/floriday-auto-fulfillment.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-logistic-labels.functions.d.ts +607 -169
- package/dist-lib/lib/floriday-logistic-labels.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-public.functions.d.ts +4123 -1057
- package/dist-lib/lib/floriday-public.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-writes.functions.d.ts +2945 -755
- package/dist-lib/lib/floriday-writes.functions.d.ts.map +1 -1
- package/dist-lib/lib/order-shipments.functions.d.ts +47158 -0
- package/dist-lib/lib/order-shipments.functions.d.ts.map +1 -0
- package/dist-lib/lib/order-shipments.functions.js +178 -0
- package/dist-lib/lib/salesorders-mapping.d.ts +1 -0
- package/dist-lib/lib/salesorders-mapping.d.ts.map +1 -1
- package/dist-lib/lib/salesorders.functions.d.ts +1767 -453
- package/dist-lib/lib/salesorders.functions.d.ts.map +1 -1
- package/dist-lib/lib/tradeitems.functions.d.ts +607 -169
- package/dist-lib/lib/tradeitems.functions.d.ts.map +1 -1
- package/dist-lib/lib/useSalesOrdersQuery.d.ts.map +1 -1
- package/dist-lib/lib/useSalesOrdersQuery.js +39 -2
- 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
|
|
20
|
-
// Bron =
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
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,
|
|
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("
|
|
35
|
-
.select("
|
|
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
|
-
|
|
38
|
-
|
|
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
|
|
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
|
|
58
|
-
|
|
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
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const physical =
|
|
68
|
-
const reserved =
|
|
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
|
-
|
|
71
|
-
id
|
|
170
|
+
byId.set(id, {
|
|
171
|
+
id,
|
|
72
172
|
product: name,
|
|
73
|
-
article_code:
|
|
173
|
+
article_code: articleCode,
|
|
74
174
|
unit: "stuks",
|
|
75
175
|
sale_price: null,
|
|
76
|
-
plants_per_crate:
|
|
77
|
-
crates_per_layer:
|
|
78
|
-
layers_per_cart:
|
|
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
|
-
|
|
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.
|
|
203
|
-
|
|
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 (=
|
|
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
|
|
211
|
-
.from("products").select("id").in("id", productIds)
|
|
212
|
-
|
|
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
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
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
|
+
});
|