@flowselections/floriday-voorraad 1.0.23 → 1.0.26
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/assets/floriday-logo.png.asset.json +4 -4
- package/dist-lib/components/voorraad/ArtikelWizard.d.ts.map +1 -1
- package/dist-lib/components/voorraad/ArtikelWizard.js +633 -101
- package/dist-lib/components/voorraad/StepSamenstelling.d.ts +10 -0
- package/dist-lib/components/voorraad/StepSamenstelling.d.ts.map +1 -0
- package/dist-lib/components/voorraad/StepSamenstelling.js +333 -0
- package/dist-lib/components/voorraad/VoorraadPage.d.ts.map +1 -1
- package/dist-lib/components/voorraad/VoorraadPage.js +14 -3
- package/dist-lib/components/voorraad/VoorraadTable.d.ts.map +1 -1
- package/dist-lib/components/voorraad/VoorraadTable.js +6 -13
- package/dist-lib/hooks/useFloridayPriceGroups.d.ts +20 -0
- package/dist-lib/hooks/useFloridayPriceGroups.d.ts.map +1 -0
- package/dist-lib/hooks/useFloridayPriceGroups.js +55 -0
- package/dist-lib/hooks/useFloridayVbnRequired.d.ts +5 -0
- package/dist-lib/hooks/useFloridayVbnRequired.d.ts.map +1 -0
- package/dist-lib/hooks/useFloridayVbnRequired.js +16 -0
- package/dist-lib/hooks/useLocalTradeItems.d.ts.map +1 -1
- package/dist-lib/hooks/useLocalTradeItems.js +14 -0
- package/dist-lib/hooks/useTradeItemSpec.d.ts.map +1 -1
- package/dist-lib/hooks/useTradeItemSpec.js +48 -1
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +2378 -361
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +2378 -361
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +2378 -361
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +2412 -344
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.js +11 -9
- package/dist-lib/lib/centralSecrets.server.d.ts +21 -0
- package/dist-lib/lib/centralSecrets.server.d.ts.map +1 -0
- package/dist-lib/lib/centralSecrets.server.js +111 -0
- package/dist-lib/lib/floricode-required-features.functions.d.ts +2378 -361
- package/dist-lib/lib/floricode-required-features.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-customer-offer.functions.d.ts +2378 -361
- package/dist-lib/lib/floriday-customer-offer.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-gateway.functions.d.ts +10760 -675
- package/dist-lib/lib/floriday-gateway.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-gateway.server.d.ts.map +1 -1
- package/dist-lib/lib/floriday-gateway.server.js +34 -9
- package/dist-lib/lib/floriday-payload.d.ts +21 -3
- package/dist-lib/lib/floriday-payload.d.ts.map +1 -1
- package/dist-lib/lib/floriday-payload.js +167 -34
- package/dist-lib/lib/floriday-vbn-required.functions.d.ts +27769 -0
- package/dist-lib/lib/floriday-vbn-required.functions.d.ts.map +1 -0
- package/dist-lib/lib/floriday-vbn-required.functions.js +85 -0
- package/dist-lib/lib/floriday-warehouse.functions.d.ts +6542 -491
- package/dist-lib/lib/floriday-warehouse.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-writes.functions.d.ts +4647 -612
- package/dist-lib/lib/floriday-writes.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-writes.functions.js +36 -1
- package/dist-lib/lib/trade-item-prices.d.ts +43 -0
- package/dist-lib/lib/trade-item-prices.d.ts.map +1 -0
- package/dist-lib/lib/trade-item-prices.js +229 -0
- package/package.json +3 -3
|
@@ -2,26 +2,33 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { Link, useNavigate } from "@tanstack/react-router";
|
|
4
4
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, Button, Card, CardContent, Checkbox, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, Popover, PopoverContent, PopoverTrigger, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, toast, } from "@flowselections/core";
|
|
5
|
-
import { ArrowLeft, ArrowRight, Check, ChevronDown, ClipboardList, Info, Loader2, Plus, Search, Trash2, Upload, UploadCloud, Users, X, } from "lucide-react";
|
|
5
|
+
import { ArrowLeft, ArrowRight, Check, ChevronDown, ClipboardList, Info, Loader2, Plus, Search, Trash2, Upload, UploadCloud, Users, X, Star, } from "lucide-react";
|
|
6
6
|
import { useFloridayConnection } from "../../hooks/useFloridayConnection";
|
|
7
7
|
import { useFlorydayCustomers } from "../../hooks/useFlorydayCustomers";
|
|
8
8
|
import { floriday } from "../../lib/floriday-client";
|
|
9
|
-
import { listFloricode } from "../../lib/floricode-client";
|
|
9
|
+
import { listFloricode, listFloricodeByField } from "../../lib/floricode-client";
|
|
10
10
|
import { supabase } from "../../integrations/supabase/client";
|
|
11
|
-
import { buildAddTradeItemPayload, buildUpdateTradeItemPayload, buildPrimaryPackingBase, emptyWizardData, floridayCacheToWizardData, isValidGtin13, PACKAGING_SUPPLIERS, validateStep, } from "../../lib/floriday-payload";
|
|
11
|
+
import { buildAddTradeItemPayload, buildUpdateTradeItemPayload, buildPrimaryPackingBase, emptyWizardData, floridayCacheToWizardData, isValidGtin13, normalizeFloridayProductGroup, PACKAGING_SUPPLIERS, validateStep, } from "../../lib/floriday-payload";
|
|
12
12
|
import { AdditionalPackagingDialog } from "./AdditionalPackagingDialog";
|
|
13
13
|
import { publishCustomerOfferFn } from "../../lib/floriday-customer-offer.functions";
|
|
14
14
|
import { upsertSupplyLineForTradeItem } from "../../lib/floriday-supply-line";
|
|
15
15
|
import { deleteLocalTradeItem, getLocalTradeItem, upsertLocalTradeItem, } from "../../hooks/useLocalTradeItems";
|
|
16
16
|
import { useTradeItemSpec } from "../../hooks/useTradeItemSpec";
|
|
17
|
+
import { isMissingCharacteristicsError } from "../../lib/floriday-writes.functions";
|
|
17
18
|
import { useVbnRegulatoryFeatures } from "../../hooks/useVbnRegulatoryFeatures";
|
|
19
|
+
import { useFloridayVbnRequired } from "../../hooks/useFloridayVbnRequired";
|
|
20
|
+
import { saveFloridayVbnRequired } from "../../lib/floriday-vbn-required.functions";
|
|
21
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
18
22
|
import { useCodeList, useTradeItemFormSchema } from "../../hooks/useFloriday";
|
|
23
|
+
import { useFloridayPriceGroups } from "../../hooks/useFloridayPriceGroups";
|
|
24
|
+
import { readTradeItemPrices, syncTradeItemPrices } from "../../lib/trade-item-prices";
|
|
19
25
|
const floridayLogo = "/flowselections-assets/Voorraad/floriday-logo.png";
|
|
20
26
|
import { VoorraadStockCard } from "../../components/voorraad/VoorraadStockCard";
|
|
21
27
|
import { ProductLinkCard } from "../../components/voorraad/ProductLinkCard";
|
|
22
28
|
import { PriceStatusBadge } from "../../components/voorraad/PriceStatusBadge";
|
|
23
29
|
import { OrderHistoryCard } from "../../components/voorraad/OrderHistoryCard";
|
|
24
30
|
import { ArtikelCompositionCard } from "../../components/voorraad/ArtikelCompositionCard";
|
|
31
|
+
import { StepSamenstelling } from "../../components/voorraad/StepSamenstelling";
|
|
25
32
|
import { FloridayInfoCard } from "../../components/voorraad/FloridayInfoCard";
|
|
26
33
|
import { stockKeyFor } from "../../hooks/useTradeItemStock";
|
|
27
34
|
const empty = emptyWizardData;
|
|
@@ -71,10 +78,23 @@ function warehouseSubLabel(raw) {
|
|
|
71
78
|
const city = addr.city ?? "";
|
|
72
79
|
return [line, city].filter(Boolean).join(", ") || undefined;
|
|
73
80
|
}
|
|
81
|
+
async function lookupProductGroupForVbn(vbnCode) {
|
|
82
|
+
const code = String(vbnCode ?? "").trim();
|
|
83
|
+
if (!code)
|
|
84
|
+
return null;
|
|
85
|
+
const { data } = await supabase
|
|
86
|
+
.from("floricode_records")
|
|
87
|
+
.select("record_id,resource,data")
|
|
88
|
+
.eq("resource", "VbnProduct")
|
|
89
|
+
.eq("record_id", code)
|
|
90
|
+
.maybeSingle();
|
|
91
|
+
return data ? groupOfRecord(data) : null;
|
|
92
|
+
}
|
|
74
93
|
export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId, } = {}) {
|
|
75
94
|
const navigate = useNavigate();
|
|
76
95
|
const { activeId, activeConnection, loading: connLoading } = useFloridayConnection();
|
|
77
96
|
const { customers: floridayCustomers } = useFlorydayCustomers(activeId);
|
|
97
|
+
const { priceGroups } = useFloridayPriceGroups(activeId);
|
|
78
98
|
const [step, setStep] = useState(0);
|
|
79
99
|
const [data, setData] = useState(empty);
|
|
80
100
|
const [submitting, setSubmitting] = useState(false);
|
|
@@ -89,10 +109,73 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
89
109
|
const [productMeta, setProductMeta] = useState({ floridayActive: false, floridayTradeItemId: null, physicalStock: 0 });
|
|
90
110
|
const [deleteAck, setDeleteAck] = useState(false);
|
|
91
111
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
112
|
+
// Codes die Floriday's server-side check bij een eerdere publish-poging als
|
|
113
|
+
// "missing" terugmeldde. Wordt uit een gedeelde Supabase-tabel gelezen
|
|
114
|
+
// (`floriday_vbn_required_characteristics`) zodat iedere gebruiker profiteert
|
|
115
|
+
// van elke geleerde eis, en de gebruiker nooit dezelfde 400 twee keer krijgt.
|
|
116
|
+
const queryClient = useQueryClient();
|
|
92
117
|
const isEditingDraft = Boolean(initialDraftId);
|
|
93
118
|
const isEditingPublished = Boolean(tradeItemId);
|
|
94
119
|
const isEditingProduct = Boolean(productId);
|
|
95
120
|
const isEditing = isEditingDraft || isEditingPublished || isEditingProduct;
|
|
121
|
+
const lastSavedRef = useRef(null);
|
|
122
|
+
// Zolang de post-load hydraties (product/draft/published + prijs- en features-
|
|
123
|
+
// hydratie) nog kunnen vuren, tellen setData-aanroepen niet als user-edits.
|
|
124
|
+
// Voorkomt dat een automatische Floriday-PUT vlak na openen wordt gestart.
|
|
125
|
+
const hydrationSettledRef = useRef(false);
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
lastSavedRef.current = null;
|
|
128
|
+
hydrationSettledRef.current = false;
|
|
129
|
+
}, [productId, tradeItemId, initialDraftId, activeId]);
|
|
130
|
+
// Zodra alle load-fases klaar zijn, geven we hydraties nog één micro-tick de
|
|
131
|
+
// tijd om `setData` te doen; daarna beschouwen we volgende mutaties als edits.
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (draftLoading || publishedLoading || productLoading)
|
|
134
|
+
return;
|
|
135
|
+
const t = window.setTimeout(() => {
|
|
136
|
+
hydrationSettledRef.current = true;
|
|
137
|
+
}, 50);
|
|
138
|
+
return () => window.clearTimeout(t);
|
|
139
|
+
}, [draftLoading, publishedLoading, productLoading]);
|
|
140
|
+
// Nieuw artikel: default de Warehouse-select op het voorkeursmagazijn uit de
|
|
141
|
+
// actieve Floriday-connectie. Alleen wanneer de gebruiker nog niets heeft
|
|
142
|
+
// gekozen en het magazijn ook echt in de Floriday warehouses-cache van deze
|
|
143
|
+
// connectie voorkomt (anders zou de push naar Suppliers API 2026v1 een
|
|
144
|
+
// onbekende warehouseId meesturen).
|
|
145
|
+
const isNewArticle = !productId && !tradeItemId && !initialDraftId;
|
|
146
|
+
const preferredWarehouseId = activeConnection?.preferred_warehouse_id ?? null;
|
|
147
|
+
const resolvePreferredWarehouseId = useCallback(async () => {
|
|
148
|
+
if (!activeId || !preferredWarehouseId)
|
|
149
|
+
return null;
|
|
150
|
+
const { data: wh } = await supabase
|
|
151
|
+
.from("floriday_warehouses_cache")
|
|
152
|
+
.select("floriday_id, data")
|
|
153
|
+
.eq("connection_id", activeId)
|
|
154
|
+
.eq("floriday_id", preferredWarehouseId)
|
|
155
|
+
.maybeSingle();
|
|
156
|
+
if (!wh)
|
|
157
|
+
return null;
|
|
158
|
+
const raw = wh.data ?? {};
|
|
159
|
+
if (raw.isExternal === true || raw.isDeleted === true)
|
|
160
|
+
return null;
|
|
161
|
+
return String(wh.floriday_id);
|
|
162
|
+
}, [activeId, preferredWarehouseId]);
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
if (!isNewArticle)
|
|
165
|
+
return;
|
|
166
|
+
let cancelled = false;
|
|
167
|
+
(async () => {
|
|
168
|
+
const preferred = await resolvePreferredWarehouseId();
|
|
169
|
+
if (cancelled || !preferred)
|
|
170
|
+
return;
|
|
171
|
+
setData((prev) => prev.warehouseId && prev.warehouseId.length > 0
|
|
172
|
+
? prev
|
|
173
|
+
: { ...prev, warehouseId: preferred });
|
|
174
|
+
})();
|
|
175
|
+
return () => {
|
|
176
|
+
cancelled = true;
|
|
177
|
+
};
|
|
178
|
+
}, [isNewArticle, resolvePreferredWarehouseId]);
|
|
96
179
|
const saveWarehousePreference = useCallback(async (targetTradeItemId, warehouseId) => {
|
|
97
180
|
if (!activeId || !targetTradeItemId || !warehouseId)
|
|
98
181
|
return;
|
|
@@ -142,6 +225,9 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
142
225
|
.maybeSingle();
|
|
143
226
|
warehouseId = pref?.warehouse_id ?? warehouseId;
|
|
144
227
|
}
|
|
228
|
+
if (!warehouseId) {
|
|
229
|
+
warehouseId = (await resolvePreferredWarehouseId()) ?? "";
|
|
230
|
+
}
|
|
145
231
|
if (cancelled)
|
|
146
232
|
return;
|
|
147
233
|
setData({ ...empty, ...row.data, warehouseId });
|
|
@@ -155,8 +241,13 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
155
241
|
return () => {
|
|
156
242
|
cancelled = true;
|
|
157
243
|
};
|
|
158
|
-
}, [initialDraftId, activeId]);
|
|
244
|
+
}, [initialDraftId, activeId, resolvePreferredWarehouseId]);
|
|
159
245
|
// Load existing ERP product directly into the same Floriday field wizard.
|
|
246
|
+
// Wanneer het product al aan een Floriday trade item is gekoppeld, halen we
|
|
247
|
+
// de authoritative Floriday-payload uit `floriday_trade_items_cache` en
|
|
248
|
+
// mergen die over de lokaal opgeslagen `floriday_payload` heen, zodat elke
|
|
249
|
+
// wizardstap (paspoort, kenmerken, foto's, beladingen, warehouse, …) direct
|
|
250
|
+
// is voorgevuld met wat er in Floriday staat.
|
|
160
251
|
useEffect(() => {
|
|
161
252
|
if (!productId)
|
|
162
253
|
return;
|
|
@@ -166,7 +257,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
166
257
|
try {
|
|
167
258
|
const { data: product, error } = await supabase
|
|
168
259
|
.from("products")
|
|
169
|
-
.select("id,product,barcode,article_number,quantity,sale_price,image_url,plants_per_crate,crates_per_layer,layers_per_cart,floriday_active,floriday_trade_item_id,floriday_payload")
|
|
260
|
+
.select("id,product,barcode,article_number,quantity,sale_price,vat_rate,vat_code,image_url,plants_per_crate,crates_per_layer,layers_per_cart,floriday_active,floriday_trade_item_id,floriday_payload")
|
|
170
261
|
.eq("id", productId)
|
|
171
262
|
.maybeSingle();
|
|
172
263
|
if (cancelled)
|
|
@@ -184,48 +275,90 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
184
275
|
const cpl = Number(product.crates_per_layer) || 0;
|
|
185
276
|
const lpc = Number(product.layers_per_cart) || 0;
|
|
186
277
|
const payload = (product.floriday_payload ?? {});
|
|
278
|
+
const fid = (product.floriday_trade_item_id ?? null);
|
|
279
|
+
const fActive = Boolean(product.floriday_active);
|
|
280
|
+
// Floriday-cache + warehouse-voorkeur parallel ophalen wanneer we een
|
|
281
|
+
// gekoppeld trade-item hebben. Missing cache blokkeert de wizard niet;
|
|
282
|
+
// dan valt hij terug op wat er lokaal is opgeslagen.
|
|
283
|
+
let cacheData = null;
|
|
284
|
+
let warehousePref = null;
|
|
285
|
+
if (fid && activeId) {
|
|
286
|
+
try {
|
|
287
|
+
const [cacheRes, prefRes] = await Promise.all([
|
|
288
|
+
supabase
|
|
289
|
+
.from("floriday_trade_items_cache")
|
|
290
|
+
.select("data")
|
|
291
|
+
.eq("connection_id", activeId)
|
|
292
|
+
.eq("floriday_id", fid)
|
|
293
|
+
.maybeSingle(),
|
|
294
|
+
supabase
|
|
295
|
+
.from("trade_item_warehouse_preferences")
|
|
296
|
+
.select("warehouse_id")
|
|
297
|
+
.eq("connection_id", activeId)
|
|
298
|
+
.eq("trade_item_id", fid)
|
|
299
|
+
.maybeSingle(),
|
|
300
|
+
]);
|
|
301
|
+
if (cacheRes.data?.data) {
|
|
302
|
+
cacheData = floridayCacheToWizardData(cacheRes.data.data);
|
|
303
|
+
if (!cacheData.productGroup) {
|
|
304
|
+
const group = await lookupProductGroupForVbn(cacheData.vbnCode);
|
|
305
|
+
if (group)
|
|
306
|
+
cacheData = { ...cacheData, productGroup: group };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
warehousePref = prefRes.data?.warehouse_id ?? null;
|
|
310
|
+
}
|
|
311
|
+
catch (e) {
|
|
312
|
+
// Niet-fataal: wizard laadt door met lokale gegevens.
|
|
313
|
+
console.warn("Floriday-cache laden mislukt", e);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const cachePackings = cacheData?.packingConfigurations ?? [];
|
|
187
317
|
const payloadHasPacking = Array.isArray(payload.packingConfigurations) && payload.packingConfigurations.length > 0;
|
|
188
|
-
const seededPacking =
|
|
189
|
-
?
|
|
190
|
-
:
|
|
191
|
-
?
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
:
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
salePricePerPiece: Number(product.sale_price ?? payload.salePricePerPiece) || 0,
|
|
215
|
-
saleStockPieces: Number(product.quantity ?? payload.saleStockPieces) || 0,
|
|
216
|
-
images: Array.isArray(payload.images) && payload.images.length > 0
|
|
318
|
+
const seededPacking = cachePackings.length > 0
|
|
319
|
+
? cachePackings
|
|
320
|
+
: payloadHasPacking
|
|
321
|
+
? payload.packingConfigurations
|
|
322
|
+
: ppc > 0 || cpl > 0 || lpc > 0
|
|
323
|
+
? [
|
|
324
|
+
{
|
|
325
|
+
id: typeof crypto !== "undefined" && "randomUUID" in crypto
|
|
326
|
+
? crypto.randomUUID()
|
|
327
|
+
: `pk-${Date.now()}`,
|
|
328
|
+
source: "VBN",
|
|
329
|
+
loadCarrier: "DANISH_TROLLEY",
|
|
330
|
+
packagingType: "",
|
|
331
|
+
unitsPerPackage: ppc || 1,
|
|
332
|
+
packagesPerLayer: cpl || 1,
|
|
333
|
+
layersPerLoadCarrier: lpc || 1,
|
|
334
|
+
isPrimary: true,
|
|
335
|
+
isCustomerSpecific: false,
|
|
336
|
+
customerOrganizationIds: [],
|
|
337
|
+
},
|
|
338
|
+
]
|
|
339
|
+
: [];
|
|
340
|
+
const cacheImages = cacheData?.images ?? [];
|
|
341
|
+
const mergedImages = cacheImages.length > 0
|
|
342
|
+
? cacheImages
|
|
343
|
+
: Array.isArray(payload.images) && payload.images.length > 0
|
|
217
344
|
? payload.images
|
|
218
345
|
: imageUrl
|
|
219
346
|
? [{ url: imageUrl, name: product.product ?? undefined, primary: true }]
|
|
220
|
-
: []
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
const
|
|
228
|
-
|
|
347
|
+
: [];
|
|
348
|
+
const normalizedPayloadProductGroup = normalizeFloridayProductGroup(payload.productGroup) ??
|
|
349
|
+
(await lookupProductGroupForVbn(cacheData?.vbnCode ?? payload.vbnCode));
|
|
350
|
+
const cacheWarehouseId = cacheData?.warehouseId && cacheData.warehouseId.length > 0
|
|
351
|
+
? cacheData.warehouseId
|
|
352
|
+
: "";
|
|
353
|
+
const payloadWarehouseId = payload.warehouseId ?? "";
|
|
354
|
+
const preferredWarehouseFallback = warehousePref || cacheWarehouseId || payloadWarehouseId
|
|
355
|
+
? null
|
|
356
|
+
: await resolvePreferredWarehouseId();
|
|
357
|
+
// ── Voorraad ophalen VOOR setData zodat `saleStockPieces` de
|
|
358
|
+
// werkelijke voorraad reflecteert (i.p.v. de altijd-0
|
|
359
|
+
// `products.quantity`-kolom, die de wizard anders bij publiceren
|
|
360
|
+
// terug zou schrijven en zo een bestaande voorraad zou overschrijven
|
|
361
|
+
// met 0).
|
|
229
362
|
let physical = 0;
|
|
230
363
|
try {
|
|
231
364
|
const { data: stockRow } = await supabase
|
|
@@ -236,6 +369,61 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
236
369
|
physical = Number(stockRow?.physical_stock ?? 0);
|
|
237
370
|
}
|
|
238
371
|
catch { /* stock niet gevonden = 0 */ }
|
|
372
|
+
setData({
|
|
373
|
+
...empty,
|
|
374
|
+
...payload,
|
|
375
|
+
// Floriday-cache is leidend voor alle Floriday-eigen velden. Alleen
|
|
376
|
+
// niet-lege waarden overschrijven, zodat een leeg veld in de cache
|
|
377
|
+
// de lokale fallback niet onnodig wist.
|
|
378
|
+
...(cacheData
|
|
379
|
+
? Object.fromEntries(Object.entries(cacheData).filter(([, v]) => {
|
|
380
|
+
if (v === null || v === undefined)
|
|
381
|
+
return false;
|
|
382
|
+
if (typeof v === "string")
|
|
383
|
+
return v.length > 0;
|
|
384
|
+
if (Array.isArray(v))
|
|
385
|
+
return v.length > 0;
|
|
386
|
+
if (typeof v === "object")
|
|
387
|
+
return Object.keys(v).length > 0;
|
|
388
|
+
return true;
|
|
389
|
+
}))
|
|
390
|
+
: {}),
|
|
391
|
+
name: (cacheData?.name && cacheData.name.length > 0
|
|
392
|
+
? cacheData.name
|
|
393
|
+
: product.product) ?? payload.name ?? "",
|
|
394
|
+
articleCode: (cacheData?.articleCode && cacheData.articleCode.length > 0
|
|
395
|
+
? cacheData.articleCode
|
|
396
|
+
: String(product.article_number ?? payload.articleCode ?? "")).trim(),
|
|
397
|
+
productGroup: cacheData?.productGroup ??
|
|
398
|
+
normalizedPayloadProductGroup ??
|
|
399
|
+
empty.productGroup,
|
|
400
|
+
gtin: cacheData?.gtin && cacheData.gtin.length > 0
|
|
401
|
+
? cacheData.gtin
|
|
402
|
+
: validGtin
|
|
403
|
+
? barcode
|
|
404
|
+
: (payload.gtin ?? ""),
|
|
405
|
+
salePricePerPiece: Number(product.sale_price ?? payload.salePricePerPiece) || 0,
|
|
406
|
+
// Voorkeur: werkelijke voorraad uit trade_item_stock.
|
|
407
|
+
// Fallback: opgeslagen payload-waarde. `products.quantity` wordt
|
|
408
|
+
// bewust NIET meer gelezen — die staat structureel op 0.
|
|
409
|
+
saleStockPieces: physical > 0
|
|
410
|
+
? physical
|
|
411
|
+
: Number(payload.saleStockPieces ?? 0) || 0,
|
|
412
|
+
vatRate: product.vat_rate !== null && product.vat_rate !== undefined
|
|
413
|
+
? Number(product.vat_rate)
|
|
414
|
+
: (payload.vatRate ?? null),
|
|
415
|
+
vatCode: product.vat_code ?? payload.vatCode ?? null,
|
|
416
|
+
warehouseId: warehousePref ||
|
|
417
|
+
cacheWarehouseId ||
|
|
418
|
+
payloadWarehouseId ||
|
|
419
|
+
preferredWarehouseFallback ||
|
|
420
|
+
"",
|
|
421
|
+
images: mergedImages,
|
|
422
|
+
linkedProductId: product.id,
|
|
423
|
+
packingConfigurations: seededPacking,
|
|
424
|
+
});
|
|
425
|
+
setDraftId(product.id);
|
|
426
|
+
setDraftFloridayId(fid);
|
|
239
427
|
setProductMeta({
|
|
240
428
|
floridayActive: fActive,
|
|
241
429
|
floridayTradeItemId: fid,
|
|
@@ -254,7 +442,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
254
442
|
return () => {
|
|
255
443
|
cancelled = true;
|
|
256
444
|
};
|
|
257
|
-
}, [productId]);
|
|
445
|
+
}, [productId, activeId, resolvePreferredWarehouseId]);
|
|
258
446
|
// Load existing published trade item from the shared cache
|
|
259
447
|
useEffect(() => {
|
|
260
448
|
if (!tradeItemId || !activeId)
|
|
@@ -286,9 +474,15 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
286
474
|
return;
|
|
287
475
|
}
|
|
288
476
|
const wizardData = floridayCacheToWizardData(row.data);
|
|
477
|
+
const productGroup = wizardData.productGroup ??
|
|
478
|
+
(await lookupProductGroupForVbn(wizardData.vbnCode));
|
|
479
|
+
const prefWarehouseId = pref?.warehouse_id ?? "";
|
|
480
|
+
const cachedWarehouseId = wizardData.warehouseId ?? "";
|
|
481
|
+
const preferredWarehouseFallback = prefWarehouseId || cachedWarehouseId ? null : await resolvePreferredWarehouseId();
|
|
289
482
|
setData({
|
|
290
483
|
...wizardData,
|
|
291
|
-
|
|
484
|
+
productGroup,
|
|
485
|
+
warehouseId: prefWarehouseId || cachedWarehouseId || preferredWarehouseFallback || "",
|
|
292
486
|
});
|
|
293
487
|
setPublishToFloriday(true);
|
|
294
488
|
}
|
|
@@ -304,7 +498,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
304
498
|
return () => {
|
|
305
499
|
cancelled = true;
|
|
306
500
|
};
|
|
307
|
-
}, [tradeItemId, activeId]);
|
|
501
|
+
}, [tradeItemId, activeId, resolvePreferredWarehouseId]);
|
|
308
502
|
// Floricode data (PascalCase resource namen zoals ze in floricode_records staan)
|
|
309
503
|
const [vbnProducts, setVbnProducts] = useState([]);
|
|
310
504
|
const [featureTypes, setFeatureTypes] = useState([]);
|
|
@@ -348,7 +542,45 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
348
542
|
values: r.values,
|
|
349
543
|
}));
|
|
350
544
|
}, [regulatoryQuery.data]);
|
|
351
|
-
const
|
|
545
|
+
const specRaw = useTradeItemSpec(schemaQuery.data, data.vbnCode, data.productGroup, regulatoryFeatures);
|
|
546
|
+
// Gedeelde cache uit `floriday_vbn_required_characteristics`: codes die
|
|
547
|
+
// Floriday's server-side check bij een eerdere publish-poging (door welke
|
|
548
|
+
// gebruiker dan ook) als "missing" terugmeldde. Zodra de VBN-code bekend
|
|
549
|
+
// is, halen we de bekende eisen op zodat het formulier ze meteen toont —
|
|
550
|
+
// ook bij een nieuw artikel dat deze gebruiker nog nooit heeft gepubliceerd.
|
|
551
|
+
const vbnRequiredQuery = useFloridayVbnRequired(data.vbnCode);
|
|
552
|
+
const serverRequiredCodes = useMemo(() => vbnRequiredQuery.data?.requiredCodes ?? [], [vbnRequiredQuery.data]);
|
|
553
|
+
// Voeg de server-required codes toe aan spec.characteristics (als optionele
|
|
554
|
+
// codes al bestaan → forceer required; onbekende codes → nieuw toevoegen met
|
|
555
|
+
// label = code en zonder allowedValues). Zo gedraagt de rest van de wizard
|
|
556
|
+
// (validatie + rendering) zich alsof Floriday deze eisen zelf publiceerde.
|
|
557
|
+
const spec = useMemo(() => {
|
|
558
|
+
if (serverRequiredCodes.length === 0)
|
|
559
|
+
return specRaw;
|
|
560
|
+
const byCode = new Map(specRaw.characteristics.map((c) => [c.code, c]));
|
|
561
|
+
for (const code of serverRequiredCodes) {
|
|
562
|
+
const existing = byCode.get(code);
|
|
563
|
+
if (existing) {
|
|
564
|
+
byCode.set(code, { ...existing, required: true });
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
byCode.set(code, {
|
|
568
|
+
code,
|
|
569
|
+
label: code,
|
|
570
|
+
required: true,
|
|
571
|
+
listName: code,
|
|
572
|
+
allowedValues: [],
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
const characteristics = Array.from(byCode.values()).sort((a, b) => {
|
|
577
|
+
if (a.required !== b.required)
|
|
578
|
+
return Number(b.required) - Number(a.required);
|
|
579
|
+
return a.code.localeCompare(b.code);
|
|
580
|
+
});
|
|
581
|
+
const requiredCharacteristicCodes = new Set(characteristics.filter((c) => c.required).map((c) => c.code));
|
|
582
|
+
return { ...specRaw, characteristics, requiredCharacteristicCodes };
|
|
583
|
+
}, [specRaw, serverRequiredCodes]);
|
|
352
584
|
// Set van VBN-product-codes die Floriday tijdens write accepteert.
|
|
353
585
|
// VBN-codes zijn 5- of 6-cijferig en staan in `vbn-product-code` (NIET in
|
|
354
586
|
// `vbn-product-group`, dat bevat 8-cijferige groeps-ID's).
|
|
@@ -372,7 +604,49 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
372
604
|
productId: !tradeItemId ? linkedProductId : null,
|
|
373
605
|
draftId: !tradeItemId && !linkedProductId ? (draftId ?? null) : null,
|
|
374
606
|
});
|
|
607
|
+
// Hydrateer basisprijs + prijsgroep-prijzen uit de lokale spiegel wanneer
|
|
608
|
+
// we bewerken en de trade_item_key bekend is.
|
|
609
|
+
const pricesHydratedRef = useRef("");
|
|
610
|
+
useEffect(() => {
|
|
611
|
+
if (!localTradeItemKey)
|
|
612
|
+
return;
|
|
613
|
+
if (pricesHydratedRef.current === localTradeItemKey)
|
|
614
|
+
return;
|
|
615
|
+
if (!isEditing)
|
|
616
|
+
return;
|
|
617
|
+
let cancelled = false;
|
|
618
|
+
pricesHydratedRef.current = localTradeItemKey;
|
|
619
|
+
readTradeItemPrices(localTradeItemKey)
|
|
620
|
+
.then((rows) => {
|
|
621
|
+
if (cancelled || rows.length === 0)
|
|
622
|
+
return;
|
|
623
|
+
setData((d) => {
|
|
624
|
+
const next = { ...d };
|
|
625
|
+
const groupPrices = { ...(d.priceGroupPrices ?? {}) };
|
|
626
|
+
for (const r of rows) {
|
|
627
|
+
if (r.price_group_id) {
|
|
628
|
+
groupPrices[r.price_group_id] = Number(r.price_per_piece);
|
|
629
|
+
}
|
|
630
|
+
else if (!(d.salePricePerPiece > 0)) {
|
|
631
|
+
next.salePricePerPiece = Number(r.price_per_piece);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
next.priceGroupPrices = groupPrices;
|
|
635
|
+
return next;
|
|
636
|
+
});
|
|
637
|
+
})
|
|
638
|
+
.catch(() => {
|
|
639
|
+
pricesHydratedRef.current = "";
|
|
640
|
+
});
|
|
641
|
+
return () => {
|
|
642
|
+
cancelled = true;
|
|
643
|
+
};
|
|
644
|
+
}, [localTradeItemKey, isEditing]);
|
|
375
645
|
// Bij wissel van VBN: gooi features weg die niet meer bij het schema horen.
|
|
646
|
+
// BELANGRIJK: de eerste keer dat we een vbnCode zien (initiële hydratie vanuit
|
|
647
|
+
// Floriday-cache of lokale draft) mogen we NIETS droppen — anders verdwijnen
|
|
648
|
+
// stil karakteristieken die Floriday wel kent maar ons lokale schema (nog) niet.
|
|
649
|
+
// Alleen bij een échte user-driven VBN-wissel prunen we.
|
|
376
650
|
const lastPrunedVbnRef = useRef("");
|
|
377
651
|
useEffect(() => {
|
|
378
652
|
if (!data.vbnCode)
|
|
@@ -381,6 +655,16 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
381
655
|
return;
|
|
382
656
|
if (spec.characteristics.length === 0)
|
|
383
657
|
return; // wachten tot spec geladen is
|
|
658
|
+
if (spec.characteristicsAreFallback) {
|
|
659
|
+
// Fallback-schema is onbetrouwbaar → alleen markeren, niet droppen.
|
|
660
|
+
lastPrunedVbnRef.current = data.vbnCode;
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
if (lastPrunedVbnRef.current === "") {
|
|
664
|
+
// Eerste run per component-instance = hydratie. Markeer en skip.
|
|
665
|
+
lastPrunedVbnRef.current = data.vbnCode;
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
384
668
|
lastPrunedVbnRef.current = data.vbnCode;
|
|
385
669
|
const allowedByCode = new Map(spec.characteristics.map((c) => [c.code, c]));
|
|
386
670
|
setData((d) => {
|
|
@@ -401,7 +685,38 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
401
685
|
}
|
|
402
686
|
return { ...d, features: next };
|
|
403
687
|
});
|
|
404
|
-
}, [data.vbnCode, spec.characteristics]);
|
|
688
|
+
}, [data.vbnCode, spec.characteristics, spec.characteristicsAreFallback]);
|
|
689
|
+
// Hydrateer vbnName wanneer we een code hebben maar (nog) geen naam — komt
|
|
690
|
+
// voor bij edit-flow: Floriday geeft alleen `vbnProductCode` terug, niet de
|
|
691
|
+
// omschrijving. Lookup gebeurt exact op record_id in de Floricode-cache.
|
|
692
|
+
const lastResolvedVbnNameRef = useRef("");
|
|
693
|
+
useEffect(() => {
|
|
694
|
+
const code = String(data.vbnCode ?? "").trim();
|
|
695
|
+
if (!code)
|
|
696
|
+
return;
|
|
697
|
+
if (data.vbnName && data.vbnName.length > 0)
|
|
698
|
+
return;
|
|
699
|
+
if (lastResolvedVbnNameRef.current === code)
|
|
700
|
+
return;
|
|
701
|
+
lastResolvedVbnNameRef.current = code;
|
|
702
|
+
let cancelled = false;
|
|
703
|
+
(async () => {
|
|
704
|
+
try {
|
|
705
|
+
const rows = await listFloricodeByField("VbnProduct", "record_id", code, 1);
|
|
706
|
+
const name = rows[0]?.data?.name ??
|
|
707
|
+
rows[0]?.data?.description ??
|
|
708
|
+
"";
|
|
709
|
+
if (!cancelled && name)
|
|
710
|
+
update("vbnName", name);
|
|
711
|
+
}
|
|
712
|
+
catch {
|
|
713
|
+
/* stille faal — trigger-label valt terug op de code */
|
|
714
|
+
}
|
|
715
|
+
})();
|
|
716
|
+
return () => {
|
|
717
|
+
cancelled = true;
|
|
718
|
+
};
|
|
719
|
+
}, [data.vbnCode, data.vbnName]);
|
|
405
720
|
const validateEigenschappen = (d) => {
|
|
406
721
|
const hasRegulatoryLookup = /^\d{5,6}$/.test(String(d.vbnCode ?? "").trim());
|
|
407
722
|
if (hasRegulatoryLookup && regulatoryQuery.isLoading) {
|
|
@@ -558,6 +873,35 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
558
873
|
toast.error(`Prijs bijwerken mislukt: ${e.message}`);
|
|
559
874
|
}
|
|
560
875
|
}
|
|
876
|
+
// Basisprijs + prijzen per prijsgroep (weekly-base-supply +
|
|
877
|
+
// weekly-supply-lines) spiegelen naar trade_item_prices en pushen.
|
|
878
|
+
try {
|
|
879
|
+
const primary = data.packingConfigurations.find((p) => p.isPrimary) ??
|
|
880
|
+
data.packingConfigurations[0];
|
|
881
|
+
const priceSync = await syncTradeItemPrices({
|
|
882
|
+
tradeItemId,
|
|
883
|
+
tradeItemKey: tradeItemId,
|
|
884
|
+
availableQuantity: data.saleStockPieces,
|
|
885
|
+
currency: "EUR",
|
|
886
|
+
periodStart: data.offerStartDate
|
|
887
|
+
? new Date(`${data.offerStartDate}T00:00:00Z`).toISOString()
|
|
888
|
+
: undefined,
|
|
889
|
+
basePricePerPiece: data.salePricePerPiece,
|
|
890
|
+
pricesByGroupId: data.priceGroupPrices ?? {},
|
|
891
|
+
priceGroups,
|
|
892
|
+
packingConfigurationId: primary?.id,
|
|
893
|
+
});
|
|
894
|
+
const failed = priceSync.groupResults.filter((r) => !r.ok);
|
|
895
|
+
if (failed.length) {
|
|
896
|
+
toast.error(`Prijsgroep(en) niet gepusht: ${failed.map((r) => r.name).join(", ")}`);
|
|
897
|
+
}
|
|
898
|
+
else if (priceSync.groupResults.length) {
|
|
899
|
+
toast.success(`${priceSync.groupResults.length} prijsgroep-prijs(zen) bijgewerkt`);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
catch (e) {
|
|
903
|
+
toast.error(`Prijzen synchroniseren mislukt: ${e.message}`);
|
|
904
|
+
}
|
|
561
905
|
}
|
|
562
906
|
else if (publishToFloriday) {
|
|
563
907
|
const payload = buildAddTradeItemPayload(data);
|
|
@@ -585,23 +929,36 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
585
929
|
// 1) Seed lokale voorraad (physical_stock) — de continuous-stock
|
|
586
930
|
// trigger pusht dit naar Floriday zodra de trade item in de
|
|
587
931
|
// cache verschijnt.
|
|
932
|
+
// GUARD: nooit een BESTAANDE voorraad-rij overschrijven. Als
|
|
933
|
+
// er al een rij is (bv. bij herpubliceren of bij een lokaal
|
|
934
|
+
// product met historische voorraad), laten we die met rust
|
|
935
|
+
// om ongewild op 0 zetten te voorkomen. Voorraad wordt
|
|
936
|
+
// beheerd via de Voorraadkaart, niet via de wizard.
|
|
588
937
|
try {
|
|
589
938
|
const key = stockKeyFor({ tradeItemId });
|
|
590
939
|
if (key && data.saleStockPieces > 0) {
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
940
|
+
const { data: existing } = await supabase
|
|
941
|
+
.from("trade_item_stock")
|
|
942
|
+
.select("trade_item_key")
|
|
943
|
+
.eq("connection_id", activeId)
|
|
944
|
+
.eq("trade_item_key", key)
|
|
945
|
+
.maybeSingle();
|
|
946
|
+
if (!existing) {
|
|
947
|
+
const userRes = await supabase.auth.getUser();
|
|
948
|
+
const userId = userRes.data.user?.id;
|
|
949
|
+
if (userId) {
|
|
950
|
+
await supabase
|
|
951
|
+
.from("trade_item_stock")
|
|
952
|
+
.insert({
|
|
953
|
+
user_id: userId,
|
|
954
|
+
connection_id: activeId,
|
|
955
|
+
trade_item_key: key,
|
|
956
|
+
physical_stock: data.saleStockPieces,
|
|
957
|
+
minimum_stock: 0,
|
|
958
|
+
reserved_stock: 0,
|
|
959
|
+
incoming_stock: 0,
|
|
960
|
+
});
|
|
961
|
+
}
|
|
605
962
|
}
|
|
606
963
|
}
|
|
607
964
|
}
|
|
@@ -657,6 +1014,31 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
657
1014
|
catch (e) {
|
|
658
1015
|
toast.error(`Prijs online zetten mislukt: ${e.message}`);
|
|
659
1016
|
}
|
|
1017
|
+
// Basisprijs + prijzen per prijsgroep spiegelen + pushen.
|
|
1018
|
+
try {
|
|
1019
|
+
const primary = data.packingConfigurations.find((p) => p.isPrimary) ??
|
|
1020
|
+
data.packingConfigurations[0];
|
|
1021
|
+
const priceSync = await syncTradeItemPrices({
|
|
1022
|
+
tradeItemId,
|
|
1023
|
+
tradeItemKey: tradeItemId,
|
|
1024
|
+
availableQuantity: data.saleStockPieces,
|
|
1025
|
+
currency: "EUR",
|
|
1026
|
+
periodStart: data.offerStartDate
|
|
1027
|
+
? new Date(`${data.offerStartDate}T00:00:00Z`).toISOString()
|
|
1028
|
+
: undefined,
|
|
1029
|
+
basePricePerPiece: data.salePricePerPiece,
|
|
1030
|
+
pricesByGroupId: data.priceGroupPrices ?? {},
|
|
1031
|
+
priceGroups,
|
|
1032
|
+
packingConfigurationId: primary?.id,
|
|
1033
|
+
});
|
|
1034
|
+
const failed = priceSync.groupResults.filter((r) => !r.ok);
|
|
1035
|
+
if (failed.length) {
|
|
1036
|
+
toast.error(`Prijsgroep(en) niet gepusht: ${failed.map((r) => r.name).join(", ")}`);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
catch (e) {
|
|
1040
|
+
toast.error(`Prijzen synchroniseren mislukt: ${e.message}`);
|
|
1041
|
+
}
|
|
660
1042
|
}
|
|
661
1043
|
else {
|
|
662
1044
|
toast.success("Artikel aangemaakt in Floriday");
|
|
@@ -677,7 +1059,27 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
677
1059
|
}
|
|
678
1060
|
catch (e) {
|
|
679
1061
|
const msg = e.message;
|
|
680
|
-
|
|
1062
|
+
const missingCodes = isMissingCharacteristicsError(msg);
|
|
1063
|
+
if (missingCodes.length > 0) {
|
|
1064
|
+
// Floriday's server-side check gaf terug welke karakteristieken nog
|
|
1065
|
+
// ontbreken. Schrijf ze naar de gedeelde Supabase-cache zodat iedere
|
|
1066
|
+
// gebruiker deze eisen vanaf nu vóór de call te zien krijgt, en
|
|
1067
|
+
// navigeer terug naar Eigenschappen zodat ze meteen invulbaar zijn.
|
|
1068
|
+
if (data.vbnCode) {
|
|
1069
|
+
try {
|
|
1070
|
+
const res = await saveFloridayVbnRequired({
|
|
1071
|
+
data: { vbnCode: data.vbnCode, codes: missingCodes },
|
|
1072
|
+
});
|
|
1073
|
+
queryClient.setQueryData(["floriday-vbn-required", data.vbnCode], { vbnCode: res.vbnCode, requiredCodes: res.requiredCodes });
|
|
1074
|
+
}
|
|
1075
|
+
catch { /* cache-write mag stilletjes falen */ }
|
|
1076
|
+
}
|
|
1077
|
+
const eigenschappenIdx = visibleSteps(data).findIndex((s) => s.key === "eigenschappen");
|
|
1078
|
+
if (eigenschappenIdx >= 0)
|
|
1079
|
+
setStep(eigenschappenIdx);
|
|
1080
|
+
toast.error(`Floriday vereist nog: ${missingCodes.join(", ")}. Vul deze eigenschappen in en probeer opnieuw.`);
|
|
1081
|
+
}
|
|
1082
|
+
else if (msg.includes("NO_ACTIVE_FLORIDAY_CONNECTION")) {
|
|
681
1083
|
toast.error("Geen actieve Floriday-connectie. Ga naar Instellingen → Floriday.");
|
|
682
1084
|
}
|
|
683
1085
|
else {
|
|
@@ -742,7 +1144,6 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
742
1144
|
// een PUT /trade-items/{id} naar Floriday gestuurd en de lokale cache
|
|
743
1145
|
// meteen bijgewerkt, zodat aanpassingen (o.a. beladingen) direct overal in
|
|
744
1146
|
// het systeem beschikbaar zijn en niet verloren raken bij navigatie.
|
|
745
|
-
const lastSavedRef = useRef(null);
|
|
746
1147
|
useEffect(() => {
|
|
747
1148
|
if (!activeId)
|
|
748
1149
|
return;
|
|
@@ -752,7 +1153,10 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
752
1153
|
if (!draftId && !pushTargetId)
|
|
753
1154
|
return;
|
|
754
1155
|
const serialized = JSON.stringify(data);
|
|
755
|
-
if (lastSavedRef.current === null) {
|
|
1156
|
+
if (lastSavedRef.current === null || !hydrationSettledRef.current) {
|
|
1157
|
+
// Hydratie-effects (product/draft/cache load, prijzen, VBN-features prune)
|
|
1158
|
+
// mogen niet als user-edit gelden — schuif de baseline mee tot de wizard
|
|
1159
|
+
// echt tot rust is gekomen.
|
|
756
1160
|
lastSavedRef.current = serialized;
|
|
757
1161
|
return;
|
|
758
1162
|
}
|
|
@@ -766,8 +1170,22 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
766
1170
|
if (draftId) {
|
|
767
1171
|
await upsertLocalTradeItem({ id: draftId, connectionId: activeId, data });
|
|
768
1172
|
}
|
|
1173
|
+
if (pushTargetId && !data.productGroup) {
|
|
1174
|
+
setSyncState(draftId ? "local" : "idle");
|
|
1175
|
+
lastSavedRef.current = serialized;
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
769
1178
|
// Push naar Floriday als er een gekoppeld/gepubliceerd trade-item is
|
|
770
1179
|
if (pushTargetId) {
|
|
1180
|
+
// Geen warehouse gekozen? Sla stil lokaal op — geen toast, geen rode
|
|
1181
|
+
// pill. Zodra de gebruiker een warehouse kiest gaat de sync verder.
|
|
1182
|
+
const whId = (data.warehouseId ?? "").trim();
|
|
1183
|
+
const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(whId);
|
|
1184
|
+
if (!isUuid) {
|
|
1185
|
+
setSyncState(draftId ? "local" : "idle");
|
|
1186
|
+
lastSavedRef.current = serialized;
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
771
1189
|
await saveWarehousePreference(pushTargetId, data.warehouseId);
|
|
772
1190
|
const payload = buildUpdateTradeItemPayload(data);
|
|
773
1191
|
const res = await floriday.updateTradeItem(activeId, pushTargetId, payload);
|
|
@@ -830,7 +1248,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
830
1248
|
if (!activeConnection) {
|
|
831
1249
|
return (_jsx("main", { className: "p-6", children: _jsx(Card, { className: "p-6", children: "Geen actieve Floriday-connectie gevonden. Voeg er eerst een toe in Instellingen." }) }));
|
|
832
1250
|
}
|
|
833
|
-
return (_jsxs("main", { className: "
|
|
1251
|
+
return (_jsxs("main", { className: "p-6 space-y-6", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Button, { variant: "ghost", size: "sm", asChild: true, children: _jsxs(Link, { to: "/voorraad", children: [_jsx(ArrowLeft, { className: "mr-2 h-4 w-4" }), "Terug naar voorraad"] }) }), !isEditingPublished && (_jsxs(Button, { variant: "outline", size: "sm", className: "ml-auto", onClick: saveDraft, disabled: submitting || (!draftId && !vbnValid), children: [submitting ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(ClipboardList, { className: "mr-2 h-4 w-4" })), "Product opslaan"] })), (isEditingDraft || isEditingProduct) && (_jsxs(AlertDialog, { open: deleteOpen, onOpenChange: (o) => {
|
|
834
1252
|
setDeleteOpen(o);
|
|
835
1253
|
if (!o)
|
|
836
1254
|
setDeleteAck(false);
|
|
@@ -860,7 +1278,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
860
1278
|
? "border-primary bg-primary text-primary-foreground"
|
|
861
1279
|
: status === "invalid"
|
|
862
1280
|
? "border-destructive bg-destructive text-destructive-foreground"
|
|
863
|
-
: "border-muted-foreground/30 text-muted-foreground"}`, children: status === "valid" ? (_jsx(Check, { className: "h-4 w-4" })) : status === "invalid" ? ("!") : (i + 1) }), _jsx("span", { className: `min-w-0 truncate font-medium ${status === "untouched" ? "text-muted-foreground" : ""}`, children: s.label }), !isOpen && summary && (_jsx("span", { className: `max-w-xs truncate text-xs ${status === "invalid" ? "text-destructive" : "text-muted-foreground"}`, children: status === "invalid" ? `Onvolledig — ${summary}` : summary }))] }), isOpen && (_jsxs(CardContent, { className: "space-y-4 pt-0", children: [_jsx("div", { "data-wizard-form-content": true, className: "w-full min-w-0", children: _jsx(StepBody, { stepKey: s.key, data: data, update: update, vbnProducts: vbnProducts, featureTypes: featureTypes, packagingTypes: packagingTypes, loadCarriers: loadCarriers, floridayCustomers: floridayCustomers, schemaQuery: schemaQuery, regulatoryQuery: regulatoryQuery, spec: spec, collectionsList: collectionsList, setCollectionsList: setCollectionsList, activeId: activeId, acceptedVbnCodes: acceptedVbnCodes }) }), isLastStep && !isEditingPublished && (_jsxs("div", { className: "rounded-lg border bg-muted/30 p-4 flex items-start gap-3", children: [_jsx("img", { src: floridayLogo, alt: "Floriday", className: "h-7 w-7 shrink-0 mt-0.5" }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx(Label, { htmlFor: "publish-to-floriday", className: "font-medium cursor-pointer", children: "Publiceren naar Floriday" }), _jsx("p", { className: "text-xs text-muted-foreground mt-1", children: publishToFloriday
|
|
1281
|
+
: "border-muted-foreground/30 text-muted-foreground"}`, children: status === "valid" ? (_jsx(Check, { className: "h-4 w-4" })) : status === "invalid" ? ("!") : (i + 1) }), _jsx("span", { className: `min-w-0 truncate font-medium ${status === "untouched" ? "text-muted-foreground" : ""}`, children: s.label }), !isOpen && summary && (_jsx("span", { className: `max-w-xs truncate text-xs ${status === "invalid" ? "text-destructive" : "text-muted-foreground"}`, children: status === "invalid" ? `Onvolledig — ${summary}` : summary }))] }), isOpen && (_jsxs(CardContent, { className: "space-y-4 pt-0", children: [_jsx("div", { "data-wizard-form-content": true, className: "w-full min-w-0", children: _jsx(StepBody, { stepKey: s.key, data: data, update: update, vbnProducts: vbnProducts, featureTypes: featureTypes, packagingTypes: packagingTypes, loadCarriers: loadCarriers, floridayCustomers: floridayCustomers, schemaQuery: schemaQuery, regulatoryQuery: regulatoryQuery, spec: spec, collectionsList: collectionsList, setCollectionsList: setCollectionsList, activeId: activeId, acceptedVbnCodes: acceptedVbnCodes, priceGroups: priceGroups, parentProductId: linkedProductId ?? draftId ?? null, parentFloridayId: draftFloridayId ?? tradeItemId ?? null, articleCodeLocked: Boolean(draftFloridayId || tradeItemId) }) }), isLastStep && !isEditingPublished && (_jsxs("div", { className: "rounded-lg border bg-muted/30 p-4 flex items-start gap-3", children: [_jsx("img", { src: floridayLogo, alt: "Floriday", className: "h-7 w-7 shrink-0 mt-0.5" }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx(Label, { htmlFor: "publish-to-floriday", className: "font-medium cursor-pointer", children: "Publiceren naar Floriday" }), _jsx("p", { className: "text-xs text-muted-foreground mt-1", children: publishToFloriday
|
|
864
1282
|
? "Het artikel wordt aangemaakt in Floriday en is binnen enkele minuten zichtbaar voor klanten."
|
|
865
1283
|
: "Het artikel wordt alleen lokaal opgeslagen. Je kunt het later vanuit de voorraadlijst alsnog publiceren naar Floriday." })] }), _jsx(Switch, { id: "publish-to-floriday", checked: publishToFloriday, onCheckedChange: setPublishToFloriday })] })), (isLastStep || isEditing) && invalidStepCount > 0 && (_jsxs("div", { className: "rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2 text-xs text-destructive flex items-center justify-between gap-3", children: [_jsxs("span", { children: ["Nog ", invalidStepCount, " stap", invalidStepCount === 1 ? "" : "pen", " onvolledig"] }), firstInvalidIndex >= 0 && (_jsxs("button", { type: "button", onClick: () => setStep(firstInvalidIndex), className: "underline font-medium", children: ["Ga naar ", steps[firstInvalidIndex].label] }))] })), _jsx("div", { className: "flex justify-end pt-2", children: isLastStep || isEditing ? (_jsxs(Button, { onClick: submit, disabled: submitting || invalidStepCount > 0, children: [submitting ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(Check, { className: "mr-2 h-4 w-4" })), isEditingPublished
|
|
866
1284
|
? "Wijzigingen opslaan"
|
|
@@ -915,9 +1333,13 @@ function summaryFor(key, d) {
|
|
|
915
1333
|
? `${d.additionalPackaging.length} item(s)`
|
|
916
1334
|
: "Geen extra verpakking";
|
|
917
1335
|
case "samenstelling":
|
|
918
|
-
return "
|
|
1336
|
+
return "Basis + per belading";
|
|
919
1337
|
case "beschikbaarheid":
|
|
920
|
-
|
|
1338
|
+
if (d.availabilityMode === "YEAR_ROUND")
|
|
1339
|
+
return "Jaarrond";
|
|
1340
|
+
if (d.availabilityMode === "UNSPECIFIED")
|
|
1341
|
+
return "Niet gespecificeerd";
|
|
1342
|
+
return `${d.weeks.length} week/weken`;
|
|
921
1343
|
case "aanbieden":
|
|
922
1344
|
if (!d.offerForSale)
|
|
923
1345
|
return "Niet aanbieden";
|
|
@@ -950,7 +1372,10 @@ function StepBody(p) {
|
|
|
950
1372
|
case "verpakking":
|
|
951
1373
|
return _jsx(StepVerpakking, { ...p });
|
|
952
1374
|
case "samenstelling":
|
|
953
|
-
return (_jsx(
|
|
1375
|
+
return (_jsx(StepSamenstelling, { productId: p.parentProductId, floridayId: p.parentFloridayId, packings: p.data.packingConfigurations, packagingTypeLabel: (code) => {
|
|
1376
|
+
const rec = p.packagingTypes.find((r) => codeOf(r) === code);
|
|
1377
|
+
return rec ? labelOf(rec) : code;
|
|
1378
|
+
} }));
|
|
954
1379
|
case "beschikbaarheid":
|
|
955
1380
|
return _jsx(StepBeschikbaarheid, { ...p });
|
|
956
1381
|
case "aanbieden":
|
|
@@ -985,11 +1410,11 @@ function groupOfRecord(r) {
|
|
|
985
1410
|
const n = Number(raw);
|
|
986
1411
|
if (Number.isFinite(n) && n > 0) {
|
|
987
1412
|
const s = String(Math.floor(n));
|
|
988
|
-
if (s.startsWith("10"))
|
|
1413
|
+
if (s.startsWith("10") || s.startsWith("11"))
|
|
989
1414
|
return "CUT_FLOWER";
|
|
990
|
-
if (s.startsWith("20"))
|
|
1415
|
+
if (s.startsWith("20") || s.startsWith("21") || s.startsWith("29"))
|
|
991
1416
|
return "POT_PLANT";
|
|
992
|
-
if (s.startsWith("30"))
|
|
1417
|
+
if (s.startsWith("30") || s.startsWith("31"))
|
|
993
1418
|
return "GARDEN_PLANT";
|
|
994
1419
|
return null;
|
|
995
1420
|
}
|
|
@@ -1086,8 +1511,12 @@ function StepVbn({ data, update, acceptedVbnCodes }) {
|
|
|
1086
1511
|
setQuery("");
|
|
1087
1512
|
};
|
|
1088
1513
|
const triggerLabel = data.vbnName
|
|
1089
|
-
? data.
|
|
1090
|
-
|
|
1514
|
+
? data.vbnCode
|
|
1515
|
+
? `${data.vbnCode} – ${data.vbnName}`
|
|
1516
|
+
: data.vbnName
|
|
1517
|
+
: data.vbnCode
|
|
1518
|
+
? data.vbnCode
|
|
1519
|
+
: "Selecteer een VBN productcode";
|
|
1091
1520
|
return (_jsxs("div", { className: "space-y-5", children: [_jsxs("div", { children: [_jsx(Label, { className: "mb-2 block", children: "Productcode" }), _jsxs("div", { className: "grid gap-3 md:grid-cols-2", children: [_jsxs(Select, { value: groupValue, onValueChange: (v) => {
|
|
1092
1521
|
const next = v === "ALL" ? null : v;
|
|
1093
1522
|
update("productGroup", next);
|
|
@@ -1122,7 +1551,7 @@ function StepVbn({ data, update, acceptedVbnCodes }) {
|
|
|
1122
1551
|
function InfoTip({ text }) {
|
|
1123
1552
|
return (_jsx("span", { title: text, className: "inline-flex cursor-help text-muted-foreground", children: _jsx(Info, { className: "h-3.5 w-3.5" }) }));
|
|
1124
1553
|
}
|
|
1125
|
-
function StepArtikel({ data, update, collectionsList, setCollectionsList }) {
|
|
1554
|
+
function StepArtikel({ data, update, collectionsList, setCollectionsList, articleCodeLocked }) {
|
|
1126
1555
|
const [newCollection, setNewCollection] = useState("");
|
|
1127
1556
|
const [showNewCollection, setShowNewCollection] = useState(false);
|
|
1128
1557
|
const addCollection = async (name) => {
|
|
@@ -1152,7 +1581,7 @@ function StepArtikel({ data, update, collectionsList, setCollectionsList }) {
|
|
|
1152
1581
|
icon: ClipboardList,
|
|
1153
1582
|
},
|
|
1154
1583
|
];
|
|
1155
|
-
return (_jsxs("div", { className: "grid gap-5", children: [_jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { className: "flex items-center gap-1.5", children: ["Artikelcode ", _jsx("span", { className: "text-destructive", children: "*" }), _jsx(InfoTip, { text: "Dit is je eigen referentie naar dit artikel, ook wel bekend als de kweker-artikelcode." })] }), _jsx(Input, { value: data.articleCode, onChange: (e) => update("articleCode", e.target.value) })] }), _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { children: ["Naam ", _jsx("span", { className: "text-destructive", children: "*" })] }), _jsx(Input, { value: data.name, onChange: (e) => update("name", e.target.value) })] }), _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { className: "flex items-center gap-1.5", children: ["GTIN ", _jsx("span", { className: "text-muted-foreground font-normal", children: "(optioneel)" }), _jsx(InfoTip, { text: "GTIN staat voor 'Global Trade Item Number', een 13-cijferige barcode" })] }), _jsx(Input, { value: data.gtin, onChange: (e) => update("gtin", e.target.value) })] }), _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { children: ["Collecties ", _jsx("span", { className: "text-muted-foreground font-normal", children: "(optioneel)" })] }), data.collections.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-1.5", children: data.collections.map((c) => (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-muted px-2 py-1 text-xs", children: [c, _jsx("button", { type: "button", onClick: () => removeCollection(c), className: "hover:text-destructive", children: _jsx(X, { className: "h-3 w-3" }) })] }, c))) })), !showNewCollection ? (_jsxs(Select, { value: "", onValueChange: (v) => {
|
|
1584
|
+
return (_jsxs("div", { className: "grid gap-5", children: [_jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { className: "flex items-center gap-1.5", children: ["Artikelcode ", !articleCodeLocked && _jsx("span", { className: "text-destructive", children: "*" }), _jsx(InfoTip, { text: "Dit is je eigen referentie naar dit artikel, ook wel bekend als de kweker-artikelcode." })] }), _jsx(Input, { value: data.articleCode, onChange: (e) => update("articleCode", e.target.value), disabled: articleCodeLocked, readOnly: articleCodeLocked }), articleCodeLocked && (_jsx("p", { className: "text-xs text-muted-foreground", children: "Artikelcode ligt vast in Floriday en kan niet meer gewijzigd worden." }))] }), _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { children: ["Naam ", _jsx("span", { className: "text-destructive", children: "*" })] }), _jsx(Input, { value: data.name, onChange: (e) => update("name", e.target.value) })] }), _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { className: "flex items-center gap-1.5", children: ["GTIN ", _jsx("span", { className: "text-muted-foreground font-normal", children: "(optioneel)" }), _jsx(InfoTip, { text: "GTIN staat voor 'Global Trade Item Number', een 13-cijferige barcode" })] }), _jsx(Input, { value: data.gtin, onChange: (e) => update("gtin", e.target.value) })] }), _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { children: ["Collecties ", _jsx("span", { className: "text-muted-foreground font-normal", children: "(optioneel)" })] }), data.collections.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-1.5", children: data.collections.map((c) => (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-muted px-2 py-1 text-xs", children: [c, _jsx("button", { type: "button", onClick: () => removeCollection(c), className: "hover:text-destructive", children: _jsx(X, { className: "h-3 w-3" }) })] }, c))) })), !showNewCollection ? (_jsxs(Select, { value: "", onValueChange: (v) => {
|
|
1156
1585
|
if (v === "__new__") {
|
|
1157
1586
|
setShowNewCollection(true);
|
|
1158
1587
|
return;
|
|
@@ -1174,7 +1603,7 @@ function StepArtikel({ data, update, collectionsList, setCollectionsList }) {
|
|
|
1174
1603
|
} }), _jsx(Button, { type: "button", size: "sm", onClick: () => addCollection(newCollection), children: _jsx(Plus, { className: "h-4 w-4" }) }), _jsx(Button, { type: "button", size: "sm", variant: "outline", onClick: () => {
|
|
1175
1604
|
setShowNewCollection(false);
|
|
1176
1605
|
setNewCollection("");
|
|
1177
|
-
}, children: "Annuleer" })] }))] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Type artikel" }), _jsx("div", { className: "grid grid-cols-2 gap-3", children: articleTypes.map((t) => {
|
|
1606
|
+
}, children: "Annuleer" })] })), _jsx("p", { className: "text-xs text-muted-foreground", children: "Collecties worden voorlopig lokaal opgeslagen; koppeling met Floriday-collecties volgt in een aparte update." })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Type artikel" }), _jsx("div", { className: "grid grid-cols-2 gap-3", children: articleTypes.map((t) => {
|
|
1178
1607
|
const Icon = t.icon;
|
|
1179
1608
|
const active = data.articleType === t.value;
|
|
1180
1609
|
return (_jsxs("button", { type: "button", onClick: () => update("articleType", t.value), className: `flex flex-col items-center gap-2 rounded-lg border p-4 text-center transition-colors ${active
|
|
@@ -1237,7 +1666,10 @@ function StepPaspoort({ data, update }) {
|
|
|
1237
1666
|
: "truncate", children: countryLabel }), _jsx(ChevronDown, { className: "h-4 w-4 shrink-0 opacity-50" })] }) }), _jsxs(PopoverContent, { align: "start", sideOffset: 4, className: "w-[var(--radix-popover-trigger-width)] p-0", children: [_jsxs("div", { className: "flex items-center gap-2 border-b px-3 py-2", children: [_jsx(Search, { className: "h-4 w-4 text-muted-foreground" }), _jsx("input", { autoFocus: true, value: countryQuery, onChange: (e) => setCountryQuery(e.target.value), placeholder: "Zoek land\u2026", className: "h-7 w-full border-0 bg-transparent text-sm outline-none placeholder:text-muted-foreground" })] }), _jsxs("div", { className: "max-h-72 overflow-y-auto py-1", children: [filteredCountries.length === 0 && (_jsx("div", { className: "px-3 py-3 text-sm text-muted-foreground", children: "Geen resultaten." })), filteredCountries.map((c) => {
|
|
1238
1667
|
const checked = selectedCountries.includes(c.code);
|
|
1239
1668
|
return (_jsxs("button", { type: "button", onClick: () => toggleCountry(c.code), className: "flex w-full items-center gap-3 px-3 py-2 text-left text-sm hover:bg-muted", children: [_jsx(Checkbox, { checked: checked }), _jsx("span", { className: "flex-1", children: c.name }), _jsx("span", { className: "font-mono text-xs text-muted-foreground", children: c.code })] }, c.code));
|
|
1240
|
-
})] })] })] })
|
|
1669
|
+
})] })] })] }), selectedCountries.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2 pt-1", children: selectedCountries.map((code) => {
|
|
1670
|
+
const name = COUNTRIES.find((o) => o.code === code)?.name ?? code;
|
|
1671
|
+
return (_jsxs("span", { className: "inline-flex items-center gap-2 rounded-md border border-input bg-background px-2.5 py-1 text-sm", children: [name, _jsx("button", { type: "button", "aria-label": `Verwijder ${name}`, onClick: () => toggleCountry(code), className: "flex h-4 w-4 items-center justify-center rounded-full bg-muted-foreground/70 text-background transition hover:bg-destructive", children: _jsx(X, { className: "h-3 w-3" }) })] }, code));
|
|
1672
|
+
}) }))] })] }));
|
|
1241
1673
|
}
|
|
1242
1674
|
function StepEigenschappen({ data, update, spec, schemaQuery, regulatoryQuery }) {
|
|
1243
1675
|
if (!data.vbnCode) {
|
|
@@ -1295,6 +1727,11 @@ function EigenschappenBody({ data, update, spec, }) {
|
|
|
1295
1727
|
const hasValue = (code) => Boolean((data.features[code] ?? "").trim());
|
|
1296
1728
|
const visibleFields = spec.characteristics.filter((c) => c.required || visibleOptional.has(c.code) || hasValue(c.code));
|
|
1297
1729
|
const hiddenOptional = spec.characteristics.filter((c) => !c.required && !visibleOptional.has(c.code) && !hasValue(c.code));
|
|
1730
|
+
// Codes die Floriday teruggeeft maar die het lokale spec-schema niet kent.
|
|
1731
|
+
// Deze mogen NIET stil worden gedropt — we tonen ze als bewerkbaar veld zodat
|
|
1732
|
+
// de gebruiker ze kan wijzigen of verwijderen, en de PUT-payload behoudt ze.
|
|
1733
|
+
const knownCodes = new Set(spec.characteristics.map((c) => c.code));
|
|
1734
|
+
const extraCodes = Object.keys(data.features).filter((code) => hasValue(code) && !knownCodes.has(code));
|
|
1298
1735
|
const filteredHidden = hiddenOptional.filter((c) => {
|
|
1299
1736
|
if (!addSearch.trim())
|
|
1300
1737
|
return true;
|
|
@@ -1321,7 +1758,13 @@ function EigenschappenBody({ data, update, spec, }) {
|
|
|
1321
1758
|
delete nextFeatures[code];
|
|
1322
1759
|
update("features", nextFeatures);
|
|
1323
1760
|
};
|
|
1324
|
-
return (_jsxs("div", { className: "grid gap-4", children: [spec.characteristicsAreFallback && (_jsx("p", { className: "rounded-md border border-amber-200 bg-amber-50 p-3 text-xs text-amber-900", children: "Floriday levert voor deze VBN-code nog geen specifieke verplichte karakteristieken aan. Voeg via \"+ Eigenschap toevoegen\" de karakteristieken toe die van toepassing zijn." })), visibleFields.length === 0 && !spec.characteristicsAreFallback && (_jsx("p", { className: "text-sm text-muted-foreground", children: "Er zijn geen verplichte karakteristieken voor deze VBN-code. Voeg optioneel eigenschappen toe via de knop hieronder." })), visibleFields.map((f) => (_jsx(CharacteristicField, { field: f, value: data.features[f.code] || "", onChange: (v) => update("features", { ...data.features, [f.code]: v }), onRemove: f.required ? undefined : () => removeField(f.code) }, f.code))),
|
|
1761
|
+
return (_jsxs("div", { className: "grid gap-4", children: [spec.characteristicsAreFallback && (_jsx("p", { className: "rounded-md border border-amber-200 bg-amber-50 p-3 text-xs text-amber-900", children: "Floriday levert voor deze VBN-code nog geen specifieke verplichte karakteristieken aan. Voeg via \"+ Eigenschap toevoegen\" de karakteristieken toe die van toepassing zijn." })), visibleFields.length === 0 && !spec.characteristicsAreFallback && (_jsx("p", { className: "text-sm text-muted-foreground", children: "Er zijn geen verplichte karakteristieken voor deze VBN-code. Voeg optioneel eigenschappen toe via de knop hieronder." })), visibleFields.map((f) => (_jsx(CharacteristicField, { field: f, value: data.features[f.code] || "", onChange: (v) => update("features", { ...data.features, [f.code]: v }), onRemove: f.required ? undefined : () => removeField(f.code) }, f.code))), extraCodes.map((code) => (_jsx(CharacteristicField, { field: {
|
|
1762
|
+
code,
|
|
1763
|
+
label: code,
|
|
1764
|
+
required: false,
|
|
1765
|
+
listName: code,
|
|
1766
|
+
allowedValues: [],
|
|
1767
|
+
}, value: data.features[code] || "", onChange: (v) => update("features", { ...data.features, [code]: v }), onRemove: () => removeField(code) }, code))), _jsx("div", { children: _jsxs(Popover, { open: addOpen, onOpenChange: (o) => { setAddOpen(o); if (!o)
|
|
1325
1768
|
setAddSearch(""); }, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", disabled: hiddenOptional.length === 0, className: "gap-2", children: [_jsx(Plus, { className: "h-4 w-4" }), "Eigenschap toevoegen", hiddenOptional.length > 0 && (_jsxs("span", { className: "text-xs text-muted-foreground", children: ["(", hiddenOptional.length, ")"] }))] }) }), _jsxs(PopoverContent, { align: "start", className: "w-[380px] p-0", children: [_jsx("div", { className: "border-b p-2", children: _jsxs("div", { className: "relative", children: [_jsx(Search, { className: "absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { autoFocus: true, value: addSearch, onChange: (e) => setAddSearch(e.target.value), placeholder: "Zoek op code of naam\u2026", className: "h-8 pl-7 text-sm" })] }) }), _jsx("div", { className: "max-h-64 overflow-y-auto p-1", children: filteredHidden.length === 0 ? (_jsx("p", { className: "p-3 text-xs text-muted-foreground", children: hiddenOptional.length === 0
|
|
1326
1769
|
? "Alle beschikbare karakteristieken zijn al toegevoegd."
|
|
1327
1770
|
: "Geen resultaten." })) : (filteredHidden.map((c) => (_jsxs("button", { type: "button", onClick: () => showField(c.code), className: "flex w-full items-center justify-between gap-2 rounded px-2 py-1.5 text-left text-sm hover:bg-accent", children: [_jsx("span", { className: "truncate", children: c.label }), _jsx("span", { className: "font-mono text-xs text-muted-foreground", children: c.code })] }, c.code)))) })] })] }) })] }));
|
|
@@ -1502,7 +1945,34 @@ function StepFotos({ data, update, activeId }) {
|
|
|
1502
1945
|
}, className: `flex items-center gap-6 rounded-md border-2 border-dashed px-8 py-10 transition-colors ${isDragging ? "border-primary bg-primary/5" : "border-border bg-background"}`, children: [_jsxs("div", { className: "relative shrink-0", children: [_jsx(UploadCloud, { className: "h-16 w-16 text-muted-foreground/60", strokeWidth: 1.25 }), _jsx("div", { className: "absolute -top-1 -right-1 flex h-5 w-5 items-center justify-center rounded-full border border-muted-foreground/40 bg-background", children: _jsx(Plus, { className: "h-3 w-3 text-muted-foreground" }) })] }), _jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "text-base font-semibold text-foreground", children: uploading ? "Uploaden..." : "Sleep afbeeldingen in dit venster om ze te uploaden" }), _jsxs("div", { className: "mt-1 text-sm text-muted-foreground", children: ["Of kies", " ", _jsx("button", { type: "button", onClick: () => inputRef.current?.click(), className: "text-foreground underline-offset-2 hover:underline", children: "Bestanden selecteren" })] }), _jsx("div", { className: "mt-2 text-xs text-muted-foreground", children: "JPG of PNG \u00B7 min. 640 \u00D7 640 px \u00B7 max. 5 MB per bestand" })] }), _jsx("input", { ref: inputRef, type: "file", multiple: true, accept: "image/png,image/jpeg", className: "hidden", onChange: (e) => {
|
|
1503
1946
|
onFiles(e.target.files);
|
|
1504
1947
|
e.target.value = "";
|
|
1505
|
-
} })] }), data.images.length > 0 && (_jsx("div", { className: "grid grid-cols-4 gap-
|
|
1948
|
+
} })] }), data.images.length > 0 && (_jsx("div", { className: "grid grid-cols-4 gap-3", children: data.images.map((img, idx) => {
|
|
1949
|
+
const setImages = (fn) => update("images", fn(data.images));
|
|
1950
|
+
const removeAt = () => setImages((arr) => arr.filter((_, i) => i !== idx));
|
|
1951
|
+
const setPrimary = () => setImages((arr) => arr.map((it, k) => ({ ...it, primary: k === idx })));
|
|
1952
|
+
const move = (dir) => setImages((arr) => {
|
|
1953
|
+
const target = idx + dir;
|
|
1954
|
+
if (target < 0 || target >= arr.length)
|
|
1955
|
+
return arr;
|
|
1956
|
+
const next = arr.slice();
|
|
1957
|
+
[next[idx], next[target]] = [next[target], next[idx]];
|
|
1958
|
+
return next;
|
|
1959
|
+
});
|
|
1960
|
+
const setType = (v) => setImages((arr) => arr.map((it, k) => (k === idx ? { ...it, type: v } : it)));
|
|
1961
|
+
return (_jsxs("div", { className: "relative group rounded-md overflow-hidden border bg-background", children: [_jsxs("div", { className: "relative", children: [_jsx("img", { src: img.previewUrl || img.url, alt: img.name, className: "h-28 w-full object-cover" }), img.primary && (_jsxs("span", { className: "absolute top-1 left-1 inline-flex items-center gap-1 rounded-full bg-primary px-2 py-0.5 text-[10px] font-medium text-primary-foreground", children: [_jsx(Star, { className: "h-3 w-3 fill-current" }), " Hoofdfoto"] })), _jsx("button", { type: "button", onClick: removeAt, "aria-label": "Verwijder foto", className: "absolute top-1 right-1 rounded-full bg-background/80 p-1 opacity-0 transition group-hover:opacity-100", children: _jsx(X, { className: "h-3 w-3" }) })] }), _jsxs("div", { className: "grid gap-1.5 p-2", children: [_jsxs(Select, { value: img.type ?? "WEBSHOPFRIENDLY", onValueChange: setType, children: [_jsx(SelectTrigger, { className: "h-8 text-xs", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: [
|
|
1962
|
+
["WEBSHOPFRIENDLY", "Standaard (webshop)"],
|
|
1963
|
+
["PIECE", "Stuk"],
|
|
1964
|
+
["TRAY", "Tray"],
|
|
1965
|
+
["DETAIL", "Detail"],
|
|
1966
|
+
["RULER", "Meetlat"],
|
|
1967
|
+
["SCENE", "Scène"],
|
|
1968
|
+
["SEASON", "Seizoen"],
|
|
1969
|
+
["STEM", "Steel"],
|
|
1970
|
+
["BUNCH", "Bos"],
|
|
1971
|
+
["CONTAINER", "Container"],
|
|
1972
|
+
["CLOCK", "Klok"],
|
|
1973
|
+
["UNKNOWN", "Onbekend"],
|
|
1974
|
+
].map(([value, label]) => (_jsx(SelectItem, { value: value, children: label }, value))) })] }), _jsxs("div", { className: "flex items-center justify-between gap-1", children: [_jsxs("div", { className: "flex gap-1", children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7 w-7 p-0", onClick: () => move(-1), disabled: idx === 0, "aria-label": "Verplaats naar links", children: _jsx(ArrowLeft, { className: "h-3.5 w-3.5" }) }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7 w-7 p-0", onClick: () => move(1), disabled: idx === data.images.length - 1, "aria-label": "Verplaats naar rechts", children: _jsx(ArrowRight, { className: "h-3.5 w-3.5" }) })] }), !img.primary && (_jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "h-7 px-2 text-[11px]", onClick: setPrimary, children: [_jsx(Star, { className: "mr-1 h-3 w-3" }), " Hoofdfoto"] }))] })] })] }, img.url));
|
|
1975
|
+
}) }))] }));
|
|
1506
1976
|
}
|
|
1507
1977
|
/**
|
|
1508
1978
|
* Warehouse-stap — verplicht in Floriday Suppliers API 2026v1.
|
|
@@ -1601,12 +2071,12 @@ function StepBeladingen({ data, update, packagingTypes, loadCarriers, floridayCu
|
|
|
1601
2071
|
const editing = editingId
|
|
1602
2072
|
? data.packingConfigurations.find((p) => p.id === editingId) ?? null
|
|
1603
2073
|
: null;
|
|
1604
|
-
return (_jsxs("div", { className: "space-y-4", children: [data.packingConfigurations.length === 0 ? (_jsx("div", { className: "rounded-md border border-dashed p-6 text-center", children: _jsx("p", { className: "text-sm text-muted-foreground", children: "Nog geen beladingsconfiguratie toegevoegd." }) })) : (_jsx("div", { className: "overflow-hidden rounded-md border", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/50 text-left text-xs uppercase text-muted-foreground", children: _jsxs("tr", { children: [_jsx("th", { className: "px-3 py-2 font-medium", children: "Primair" }), _jsx("th", { className: "px-3 py-2 font-medium", children: "Fust" }), _jsx("th", { className: "px-3 py-2 font-medium", children: "Ladingdrager" }), _jsx("th", { className: "px-3 py-2 font-medium", children: "
|
|
2074
|
+
return (_jsxs("div", { className: "space-y-4", children: [data.packingConfigurations.length === 0 ? (_jsx("div", { className: "rounded-md border border-dashed p-6 text-center", children: _jsx("p", { className: "text-sm text-muted-foreground", children: "Nog geen beladingsconfiguratie toegevoegd." }) })) : (_jsx("div", { className: "overflow-hidden rounded-md border", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/50 text-left text-xs uppercase text-muted-foreground", children: _jsxs("tr", { children: [_jsx("th", { className: "px-3 py-2 font-medium", children: "Primair" }), _jsx("th", { className: "px-3 py-2 font-medium", children: "Fust" }), _jsx("th", { className: "px-3 py-2 font-medium", children: "Ladingdrager" }), _jsx("th", { className: "px-3 py-2 font-medium", children: "Belading" }), _jsx("th", { className: "px-3 py-2 font-medium", children: "Type" }), _jsx("th", { className: "px-3 py-2" })] }) }), _jsx("tbody", { className: "divide-y", children: data.packingConfigurations.map((p) => {
|
|
1605
2075
|
const pkgLabel = packagingTypes.find((r) => codeOf(r) === p.packagingType);
|
|
1606
2076
|
const lcLabel = loadCarriers.find((r) => codeOf(r) === p.loadCarrier);
|
|
1607
2077
|
return (_jsxs("tr", { className: "hover:bg-muted/30", children: [_jsx("td", { className: "px-3 py-2", children: _jsx("input", { type: "radio", name: "primary-packing", checked: Boolean(p.isPrimary), onChange: () => setPrimary(p.id), "aria-label": "Als primaire belading instellen" }) }), _jsxs("td", { className: "px-3 py-2", children: [_jsx("div", { className: "font-medium", children: pkgLabel
|
|
1608
2078
|
? `${codeOf(pkgLabel)}: ${labelOf(pkgLabel)}`
|
|
1609
|
-
: p.packagingType || "—" }), p.source === "SUPPLIER" && (_jsx("div", { className: "text-xs text-muted-foreground", children: p.packagingSupplierName ?? p.packagingSupplierId }))] }), _jsx("td", { className: "px-3 py-2", children: LOAD_CARRIER_LABELS[p.loadCarrier] ?? p.loadCarrier ?? "—" }), _jsxs("td", { className: "px-3 py-2 tabular-nums", children: [p.
|
|
2079
|
+
: p.packagingType || "—" }), p.source === "SUPPLIER" && (_jsx("div", { className: "text-xs text-muted-foreground", children: p.packagingSupplierName ?? p.packagingSupplierId }))] }), _jsx("td", { className: "px-3 py-2", children: LOAD_CARRIER_LABELS[p.loadCarrier] ?? p.loadCarrier ?? "—" }), _jsxs("td", { className: "px-3 py-2 tabular-nums", children: [p.layersPerLoadCarrier, "\u00D7", p.packagesPerLayer, "\u00D7", p.unitsPerPackage] }), _jsx("td", { className: "px-3 py-2", children: _jsxs("div", { className: "flex flex-wrap items-center gap-1", children: [p.isCustomerSpecific ? (_jsxs("span", { className: "rounded bg-amber-100 px-2 py-0.5 text-xs text-amber-900", children: ["Klantspecifiek (", p.customerOrganizationIds.length, ")"] })) : (_jsx("span", { className: "text-xs text-muted-foreground", children: "Regulier" })), p.isHiddenForDirectSales && (_jsx("span", { className: "rounded bg-slate-200 px-2 py-0.5 text-xs text-slate-700", children: "Verborgen" }))] }) }), _jsx("td", { className: "px-3 py-2 text-right", children: _jsxs("div", { className: "flex justify-end gap-1", children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => openEdit(p.id), children: "Bewerk" }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => remove(p.id), children: _jsx(Trash2, { className: "h-4 w-4" }) })] }) })] }, p.id));
|
|
1610
2080
|
}) })] }) })), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: openNew, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), " Belading toevoegen"] }), _jsx(PackingConfigDialog, { open: dialogOpen, onOpenChange: (o) => {
|
|
1611
2081
|
setDialogOpen(o);
|
|
1612
2082
|
if (!o)
|
|
@@ -1628,6 +2098,7 @@ function PackingConfigDialog({ open, onOpenChange, initial, isFirst, packagingTy
|
|
|
1628
2098
|
isCustomerSpecific: false,
|
|
1629
2099
|
customerOrganizationIds: [],
|
|
1630
2100
|
isPrimary: isFirst,
|
|
2101
|
+
isHiddenForDirectSales: false,
|
|
1631
2102
|
});
|
|
1632
2103
|
const [draft, setDraft] = useState(emptyItem);
|
|
1633
2104
|
const [pkgSearch, setPkgSearch] = useState("");
|
|
@@ -1657,8 +2128,16 @@ function PackingConfigDialog({ open, onOpenChange, initial, isFirst, packagingTy
|
|
|
1657
2128
|
? draft.customerOrganizationIds.filter((x) => x !== id)
|
|
1658
2129
|
: [...draft.customerOrganizationIds, id]);
|
|
1659
2130
|
};
|
|
2131
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2132
|
+
const materialIsUuid = UUID_RE.test((draft.packagingMaterialId ?? "").trim());
|
|
2133
|
+
const supplierIsUuid = UUID_RE.test((draft.packagingSupplierId ?? "").trim());
|
|
2134
|
+
const hasCustomPkg = draft.source === "SUPPLIER" && materialIsUuid;
|
|
2135
|
+
const materialInvalid = draft.source === "SUPPLIER" &&
|
|
2136
|
+
!!(draft.packagingMaterialId ?? "").trim() &&
|
|
2137
|
+
!materialIsUuid;
|
|
1660
2138
|
const canSave = draft.loadCarrier.length > 0 &&
|
|
1661
|
-
draft.packagingType.length > 0 &&
|
|
2139
|
+
(hasCustomPkg || draft.packagingType.length > 0) &&
|
|
2140
|
+
!materialInvalid &&
|
|
1662
2141
|
(draft.source === "VBN" || Boolean(draft.packagingSupplierId)) &&
|
|
1663
2142
|
(!draft.isCustomerSpecific || draft.customerOrganizationIds.length > 0);
|
|
1664
2143
|
const onUploadPhoto = async (file) => {
|
|
@@ -1695,14 +2174,22 @@ function PackingConfigDialog({ open, onOpenChange, initial, isFirst, packagingTy
|
|
|
1695
2174
|
? "border-primary bg-primary/5"
|
|
1696
2175
|
: "hover:border-primary/40"}`, children: [_jsx("div", { className: "font-medium", children: "VBN fust" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Standaard veilingfust uit de VBN codelijst." })] }), _jsxs("button", { type: "button", onClick: () => setDraft((d) => ({ ...d, source: "SUPPLIER" })), className: `flex-1 rounded-md border px-3 py-2 text-sm text-left ${draft.source === "SUPPLIER"
|
|
1697
2176
|
? "border-primary bg-primary/5"
|
|
1698
|
-
: "hover:border-primary/40"}`, children: [_jsx("div", { className: "font-medium", children: "Verpakkingsleverancier" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Aangesloten leverancier (Modiform, Paardekooper, \u2026)." })] })] })] }), draft.source === "SUPPLIER" && (_jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Verpakkingsleverancier" }), _jsxs(
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
2177
|
+
: "hover:border-primary/40"}`, children: [_jsx("div", { className: "font-medium", children: "Verpakkingsleverancier" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Aangesloten leverancier (Modiform, Paardekooper, \u2026)." })] })] })] }), draft.source === "SUPPLIER" && (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Verpakkingsleverancier" }), supplierIsUuid ? (_jsxs("div", { className: "rounded-md border bg-muted/40 px-3 py-2 text-sm", children: [_jsx("div", { className: "font-medium", children: draft.packagingSupplierName ?? "Gekoppelde leverancier" }), _jsxs("div", { className: "mt-1 flex items-center gap-2 text-xs text-muted-foreground", children: [_jsx("span", { className: "font-mono truncate", children: draft.packagingSupplierId }), _jsx("button", { type: "button", className: "text-primary underline", onClick: () => setDraft((d) => ({
|
|
2178
|
+
...d,
|
|
2179
|
+
packagingSupplierId: undefined,
|
|
2180
|
+
packagingSupplierName: undefined,
|
|
2181
|
+
})), children: "Wijzig" })] })] })) : (_jsxs(Select, { value: draft.packagingSupplierId || undefined, onValueChange: (v) => {
|
|
2182
|
+
const sup = PACKAGING_SUPPLIERS.find((s) => s.id === v);
|
|
2183
|
+
setDraft((d) => ({
|
|
2184
|
+
...d,
|
|
2185
|
+
packagingSupplierId: v,
|
|
2186
|
+
packagingSupplierName: sup?.name,
|
|
2187
|
+
}));
|
|
2188
|
+
}, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies leverancier..." }) }), _jsx(SelectContent, { children: PACKAGING_SUPPLIERS.map((s) => (_jsx(SelectItem, { value: s.id, children: s.name }, s.id))) })] }))] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Floricode VRS-materiaal UUID (optioneel)" }), _jsx(Input, { value: draft.packagingMaterialId ?? "", onChange: (e) => patch("packagingMaterialId", e.target.value.trim()), placeholder: "00000000-0000-0000-0000-000000000000", className: materialInvalid ? "border-destructive" : "" }), materialInvalid ? (_jsx("p", { className: "text-xs text-destructive", children: "Ongeldig UUID-formaat. Laat leeg of plak een echte VRS-materiaal UUID uit Floricode." })) : (_jsxs("p", { className: "text-xs text-muted-foreground", children: ["Wanneer ingevuld gaat het als ", _jsx("code", { children: "customPackageId" }), "naar Floriday en vervangt het de VBN-fust."] }))] })] }), hasCustomPkg && (_jsx("div", { className: "rounded-md border border-primary/30 bg-primary/5 px-3 py-2 text-xs", children: "Custom package actief \u2014 VBN-fust hieronder wordt genegeerd in de Floriday-payload." }))] })), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: hasCustomPkg
|
|
2189
|
+
? "VBN-fust (genegeerd — custom package actief)"
|
|
2190
|
+
: draft.source === "SUPPLIER"
|
|
2191
|
+
? "Fallback VBN-fust"
|
|
2192
|
+
: "Fusttype" }), _jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", className: "w-full justify-between", children: [_jsx("span", { className: "truncate", children: draft.packagingType
|
|
1706
2193
|
? (packagingTypes.find((r) => codeOf(r) === draft.packagingType)
|
|
1707
2194
|
? `${labelOf(packagingTypes.find((r) => codeOf(r) === draft.packagingType))} (${draft.packagingType})`
|
|
1708
2195
|
: draft.packagingType)
|
|
@@ -1714,7 +2201,7 @@ function PackingConfigDialog({ open, onOpenChange, initial, isFirst, packagingTy
|
|
|
1714
2201
|
photoUrl: undefined,
|
|
1715
2202
|
photoImageId: undefined,
|
|
1716
2203
|
photoPreviewUrl: undefined,
|
|
1717
|
-
})), className: "absolute top-1 right-1 rounded-full bg-background/80 p-1", children: _jsx(X, { className: "h-3 w-3" }) })] }))] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: Boolean(draft.isPrimary), onChange: (e) => patch("isPrimary", e.target.checked) }), "Als primaire belading gebruiken"] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => onOpenChange(false), children: "Annuleren" }), _jsx(Button, { type: "button", disabled: !canSave, onClick: () => onSave(draft), children: "Opslaan" })] })] }) }));
|
|
2204
|
+
})), className: "absolute top-1 right-1 rounded-full bg-background/80 p-1", children: _jsx(X, { className: "h-3 w-3" }) })] }))] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: Boolean(draft.isPrimary), onChange: (e) => patch("isPrimary", e.target.checked) }), "Als primaire belading gebruiken"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: Boolean(draft.isHiddenForDirectSales), onChange: (e) => patch("isHiddenForDirectSales", e.target.checked) }), "Verbergen voor Direct Sale (isHiddenForDirectSales)"] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => onOpenChange(false), children: "Annuleren" }), _jsx(Button, { type: "button", disabled: !canSave, onClick: () => onSave(draft), children: "Opslaan" })] })] }) }));
|
|
1718
2205
|
}
|
|
1719
2206
|
function StepVerpakking({ data, update }) {
|
|
1720
2207
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
@@ -1723,14 +2210,14 @@ function StepVerpakking({ data, update }) {
|
|
|
1723
2210
|
return (_jsxs("div", { className: "space-y-3", children: [_jsxs("p", { className: "text-xs text-muted-foreground bg-muted/40 rounded-md p-2 border", children: ["Voeg extra verpakkingsmateriaal (hoes, steker, label, potfolie, \u2026) uit de VRS-catalogus toe. Materialen met een geldige Floriday-UUID worden automatisch meegestuurd naar Floriday als", _jsx("code", { className: "mx-1", children: "additionalPackagingMaterialIds" }), "op het TradeItem."] }), data.additionalPackaging.length > 0 && (_jsx("div", { className: "border rounded-md divide-y", children: data.additionalPackaging.map((p) => (_jsxs("div", { className: "grid grid-cols-[1fr_1fr_1fr_auto] gap-3 items-center px-3 py-2 text-sm", children: [_jsxs("div", { children: [_jsx("div", { className: "text-xs text-muted-foreground", children: "Leverancier" }), _jsx("div", { children: p.packagingSupplierName })] }), _jsxs("div", { children: [_jsx("div", { className: "text-xs text-muted-foreground", children: "Type" }), _jsx("div", { children: p.materialTypeName })] }), _jsxs("div", { children: [_jsx("div", { className: "text-xs text-muted-foreground", children: "Materiaal" }), _jsx("div", { children: p.packagingMaterialName })] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => remove(p.id), children: _jsx(Trash2, { className: "h-4 w-4" }) })] }, p.id))) })), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setDialogOpen(true), children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Verpakkingsmateriaal toevoegen"] }), _jsx(AdditionalPackagingDialog, { open: dialogOpen, onOpenChange: setDialogOpen, onSave: addItem })] }));
|
|
1724
2211
|
}
|
|
1725
2212
|
function StepBeschikbaarheid({ data, update }) {
|
|
1726
|
-
const weeks = Array.from({ length:
|
|
2213
|
+
const weeks = Array.from({ length: 53 }, (_, i) => i + 1);
|
|
1727
2214
|
// Drag-selectie: bij mousedown bepalen we of we in "select"- of "deselect"-
|
|
1728
2215
|
// modus zitten (op basis van de startcel) en breiden we de selectie uit
|
|
1729
2216
|
// terwijl de gebruiker over de weken sleept, zoals in Floriday.
|
|
1730
2217
|
const dragStateRef = useRef(null);
|
|
1731
2218
|
const [dragHover, setDragHover] = useState(null);
|
|
1732
2219
|
const setSelection = (list) => {
|
|
1733
|
-
const unique = Array.from(new Set(list.filter((w) => w >= 1 && w <=
|
|
2220
|
+
const unique = Array.from(new Set(list.filter((w) => w >= 1 && w <= 53)))
|
|
1734
2221
|
.sort((a, b) => a - b);
|
|
1735
2222
|
update("weeks", unique);
|
|
1736
2223
|
};
|
|
@@ -1836,7 +2323,14 @@ function StepBeschikbaarheid({ data, update }) {
|
|
|
1836
2323
|
pointerXRef.current = e.clientX;
|
|
1837
2324
|
startAutoScrollLoop();
|
|
1838
2325
|
};
|
|
1839
|
-
return (_jsxs("div", { className: "space-y-4", children: [
|
|
2326
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsx("div", { className: "flex flex-wrap gap-2", children: [
|
|
2327
|
+
{ value: "YEAR_ROUND", label: "Jaarrond" },
|
|
2328
|
+
{ value: "SPECIFIC", label: "Specifieke periode" },
|
|
2329
|
+
{ value: "UNSPECIFIED", label: "Niet gespecificeerd" },
|
|
2330
|
+
].map((opt) => {
|
|
2331
|
+
const active = data.availabilityMode === opt.value;
|
|
2332
|
+
return (_jsx("button", { type: "button", onClick: () => update("availabilityMode", opt.value), className: `rounded-md border px-3 py-1.5 text-sm ${active ? "border-primary bg-primary/5" : "hover:border-primary/40"}`, children: opt.label }, opt.value));
|
|
2333
|
+
}) }), data.availabilityMode === "SPECIFIC" && (_jsxs("div", { children: [_jsxs("div", { className: "mb-2 flex items-center justify-between gap-3", children: [_jsx(Label, { className: "shrink-0", children: "Selecteer weken" }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("span", { className: "text-xs text-muted-foreground", children: ["Sleep om meerdere weken te selecteren", data.weeks.length > 0 && ` — ${data.weeks.length} week/weken`] }), _jsx("button", { type: "button", onClick: () => setSelection([]), disabled: data.weeks.length === 0, className: "rounded-md border px-2.5 py-1 text-xs text-destructive hover:border-destructive/40 hover:bg-destructive/5 disabled:cursor-not-allowed disabled:opacity-40", children: "Selectie leegmaken" })] })] }), _jsx("div", { ref: scrollRef, className: "overflow-x-auto pb-2", children: _jsx("div", { className: "flex select-none gap-1 min-w-max", children: weeks.map((w) => {
|
|
1840
2334
|
const selected = data.weeks.includes(w);
|
|
1841
2335
|
const isHover = dragHover === w;
|
|
1842
2336
|
return (_jsx("button", { type: "button", "data-week": w, onMouseDown: (e) => onWeekMouseDownWithScroll(w, e), onMouseEnter: () => onWeekMouseEnter(w), className: `h-9 w-11 shrink-0 rounded border text-xs transition-colors ${selected
|
|
@@ -1847,18 +2341,56 @@ function StepBeschikbaarheid({ data, update }) {
|
|
|
1847
2341
|
// ------------------------------------------------------------
|
|
1848
2342
|
// Stap "Aanbieden & voorraad" — Direct Sale via CustomerOffer
|
|
1849
2343
|
// ------------------------------------------------------------
|
|
1850
|
-
function StepAanbieden({ data, update }) {
|
|
2344
|
+
function StepAanbieden({ data, update, priceGroups }) {
|
|
1851
2345
|
const priceStr = Number.isFinite(data.salePricePerPiece)
|
|
1852
2346
|
? String(data.salePricePerPiece)
|
|
1853
2347
|
: "";
|
|
1854
2348
|
const stockStr = Number.isFinite(data.saleStockPieces)
|
|
1855
2349
|
? String(data.saleStockPieces)
|
|
1856
2350
|
: "";
|
|
1857
|
-
|
|
2351
|
+
const setGroupPrice = (groupId, raw) => {
|
|
2352
|
+
const next = { ...(data.priceGroupPrices ?? {}) };
|
|
2353
|
+
const parsed = Number(raw);
|
|
2354
|
+
if (!raw || !Number.isFinite(parsed) || parsed <= 0) {
|
|
2355
|
+
delete next[groupId];
|
|
2356
|
+
}
|
|
2357
|
+
else {
|
|
2358
|
+
next[groupId] = parsed;
|
|
2359
|
+
}
|
|
2360
|
+
update("priceGroupPrices", next);
|
|
2361
|
+
};
|
|
2362
|
+
return (_jsxs("div", { className: "space-y-6", children: [_jsxs("div", { className: "rounded-md border bg-muted/30 p-3 text-sm text-muted-foreground", children: ["Zet dit product direct in de verkoop bij je gekoppelde Floriday-klanten. We sturen een ", _jsx("code", { children: "TradeItem" }), " + ", _jsx("code", { children: "CustomerOffer" }), " met vaste basisprijs per stuk en publiceren de opgegeven voorraad via de continuous-stock feed. Prijzen per prijsgroep worden als aparte", " ", _jsx("code", { children: "weekly-supply-lines" }), " naar Floriday gepusht."] }), _jsxs("div", { className: "flex items-center justify-between rounded-md border p-3", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-medium", children: "Direct aanbieden voor verkoop" }), _jsx("div", { className: "text-xs text-muted-foreground", children: data.offerForSale
|
|
1858
2363
|
? "Aanbod wordt aangemaakt direct na publiceren."
|
|
1859
|
-
: "Alleen artikel publiceren, geen aanbod aanmaken." })] }), _jsx(Switch, { checked: data.offerForSale, onCheckedChange: (v) => update("offerForSale", Boolean(v)) })] }), data.offerForSale && (_jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Voorraad (stuks)" }), _jsx(Input, { type: "number", inputMode: "numeric", min: 0, step: 1, value: stockStr, onChange: (e) => update("saleStockPieces", Math.max(0, Math.floor(Number(e.target.value) || 0))) }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Wordt als fysieke voorraad geboekt en via continuous-stock naar Floriday gepusht." })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "
|
|
1860
|
-
|
|
1861
|
-
|
|
2364
|
+
: "Alleen artikel publiceren, geen aanbod aanmaken." })] }), _jsx(Switch, { checked: data.offerForSale, onCheckedChange: (v) => update("offerForSale", Boolean(v)) })] }), data.offerForSale && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Voorraad (stuks)" }), _jsx(Input, { type: "number", inputMode: "numeric", min: 0, step: 1, value: stockStr, onChange: (e) => update("saleStockPieces", Math.max(0, Math.floor(Number(e.target.value) || 0))) }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Wordt als fysieke voorraad geboekt en via continuous-stock naar Floriday gepusht." })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Basisprijs per stuk (EUR)" }), _jsx(Input, { type: "number", inputMode: "decimal", min: 0, step: "0.01", value: priceStr, onChange: (e) => update("salePricePerPiece", Number(e.target.value) || 0) }), _jsxs("p", { className: "text-xs text-muted-foreground", children: ["Wordt gepusht als ", _jsx("code", { children: "weekly-base-supply" }), " \u2014 de standaard catalogusprijs voor klanten zonder eigen prijsgroep."] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Aanbod geldig vanaf" }), _jsx(Input, { type: "date", value: data.offerStartDate, onChange: (e) => update("offerStartDate", e.target.value) }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Aanbod loopt automatisch tot 1 jaar na deze datum." })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Zichtbaar voor" }), _jsx("div", { className: "rounded-md border bg-muted/20 px-3 py-2 text-sm", children: data.customerOrganizationIds.length
|
|
2365
|
+
? `${data.customerOrganizationIds.length} geselecteerde klant(en)`
|
|
2366
|
+
: "Alle gekoppelde Floriday-klanten" }), _jsxs("p", { className: "text-xs text-muted-foreground", children: ["Klantselectie beheer je in de stap ", _jsx("em", { children: "Klanten" }), "."] })] })] }), _jsxs("div", { className: "space-y-2 rounded-md border p-4", children: [_jsxs("div", { className: "flex items-baseline justify-between", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-medium", children: "Prijzen per Floriday-prijsgroep" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Laat leeg om voor die groep g\u00E9\u00E9n afwijkende prijs te publiceren. Basisprijs blijft dan gelden." })] }), _jsxs("div", { className: "text-xs text-muted-foreground", children: [priceGroups.length, " groep", priceGroups.length === 1 ? "" : "en"] })] }), priceGroups.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground py-4", children: "Geen actieve prijsgroepen gevonden voor deze Floriday-connectie." })) : (_jsx("div", { className: "grid gap-3 sm:grid-cols-2 pt-2", children: priceGroups.map((g) => {
|
|
2367
|
+
const val = data.priceGroupPrices?.[g.id];
|
|
2368
|
+
return (_jsxs("div", { className: "space-y-1", children: [_jsxs(Label, { className: "text-sm", children: [g.name, _jsxs("span", { className: "ml-2 text-xs font-normal text-muted-foreground", children: [g.customerOrganizationIds.length, " klant", g.customerOrganizationIds.length === 1 ? "" : "en"] })] }), _jsx(Input, { type: "number", inputMode: "decimal", min: 0, step: "0.01", placeholder: "\u20AC", value: val ? String(val) : "", onChange: (e) => setGroupPrice(g.id, e.target.value) })] }, g.id));
|
|
2369
|
+
}) }))] })] })), _jsx(VatSection, { data: data, update: update })] }));
|
|
2370
|
+
}
|
|
2371
|
+
// ------------------------------------------------------------
|
|
2372
|
+
// BTW-blok (facturatie) — lokaal, niet naar Floriday
|
|
2373
|
+
// ------------------------------------------------------------
|
|
2374
|
+
const TWINFIELD_VAT_OPTIONS = [
|
|
2375
|
+
{ code: "VH", label: "VH – Hoog (21%)", rate: 21 },
|
|
2376
|
+
{ code: "VL", label: "VL – Laag (9%)", rate: 9 },
|
|
2377
|
+
{ code: "VN", label: "VN – Nul (0%)", rate: 0 },
|
|
2378
|
+
{ code: "VG", label: "VG – Geen / vrijgesteld", rate: 0 },
|
|
2379
|
+
];
|
|
2380
|
+
function VatSection({ data, update }) {
|
|
2381
|
+
const code = data.vatCode ?? "";
|
|
2382
|
+
const onCodeChange = (next) => {
|
|
2383
|
+
if (next === "__default__") {
|
|
2384
|
+
update("vatCode", null);
|
|
2385
|
+
update("vatRate", null);
|
|
2386
|
+
return;
|
|
2387
|
+
}
|
|
2388
|
+
update("vatCode", next);
|
|
2389
|
+
const match = TWINFIELD_VAT_OPTIONS.find((o) => o.code === next);
|
|
2390
|
+
if (match)
|
|
2391
|
+
update("vatRate", match.rate);
|
|
2392
|
+
};
|
|
2393
|
+
return (_jsxs("div", { className: "space-y-3 rounded-md border p-4", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-medium", children: "BTW (facturatie)" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Wordt gebruikt bij het maken van facturen. Leeg = standaardwaarde uit de instellingen (default 9% / VL)." })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "BTW-tarief (%)" }), _jsxs(Select, { value: code || "__default__", onValueChange: onCodeChange, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "__default__", children: "Standaard (VL \u2013 9%)" }), TWINFIELD_VAT_OPTIONS.map((o) => (_jsx(SelectItem, { value: o.code, children: o.label }, o.code)))] })] })] })] }));
|
|
1862
2394
|
}
|
|
1863
2395
|
// ============================================================
|
|
1864
2396
|
// Shared
|