@flowselections/floriday-voorraad 1.0.22 → 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 +655 -93
- package/dist-lib/components/voorraad/FloridayInfoCard.d.ts +12 -0
- package/dist-lib/components/voorraad/FloridayInfoCard.d.ts.map +1 -0
- package/dist-lib/components/voorraad/FloridayInfoCard.js +85 -0
- 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 +8 -1
- 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/hooks/useVoorraadData.d.ts.map +1 -1
- package/dist-lib/hooks/useVoorraadData.js +36 -5
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +2388 -345
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +2388 -345
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +2388 -345
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +2437 -343
- 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 +2388 -345
- package/dist-lib/lib/floricode-required-features.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-client.d.ts +4 -0
- package/dist-lib/lib/floriday-client.d.ts.map +1 -1
- package/dist-lib/lib/floriday-customer-offer.functions.d.ts +2388 -345
- package/dist-lib/lib/floriday-customer-offer.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-gateway.functions.d.ts +10860 -645
- 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-info.d.ts +54 -0
- package/dist-lib/lib/floriday-info.d.ts.map +1 -0
- package/dist-lib/lib/floriday-info.js +114 -0
- 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 +6602 -473
- package/dist-lib/lib/floriday-warehouse.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-writes.functions.d.ts +4681 -594
- 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,34 @@ 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";
|
|
32
|
+
import { FloridayInfoCard } from "../../components/voorraad/FloridayInfoCard";
|
|
25
33
|
import { stockKeyFor } from "../../hooks/useTradeItemStock";
|
|
26
34
|
const empty = emptyWizardData;
|
|
27
35
|
const LOAD_CARRIER_LABELS = {
|
|
@@ -70,10 +78,23 @@ function warehouseSubLabel(raw) {
|
|
|
70
78
|
const city = addr.city ?? "";
|
|
71
79
|
return [line, city].filter(Boolean).join(", ") || undefined;
|
|
72
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
|
+
}
|
|
73
93
|
export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId, } = {}) {
|
|
74
94
|
const navigate = useNavigate();
|
|
75
95
|
const { activeId, activeConnection, loading: connLoading } = useFloridayConnection();
|
|
76
96
|
const { customers: floridayCustomers } = useFlorydayCustomers(activeId);
|
|
97
|
+
const { priceGroups } = useFloridayPriceGroups(activeId);
|
|
77
98
|
const [step, setStep] = useState(0);
|
|
78
99
|
const [data, setData] = useState(empty);
|
|
79
100
|
const [submitting, setSubmitting] = useState(false);
|
|
@@ -85,10 +106,76 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
85
106
|
const [productLoading, setProductLoading] = useState(Boolean(productId));
|
|
86
107
|
const [syncState, setSyncState] = useState("idle");
|
|
87
108
|
const [syncError, setSyncError] = useState(null);
|
|
109
|
+
const [productMeta, setProductMeta] = useState({ floridayActive: false, floridayTradeItemId: null, physicalStock: 0 });
|
|
110
|
+
const [deleteAck, setDeleteAck] = useState(false);
|
|
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();
|
|
88
117
|
const isEditingDraft = Boolean(initialDraftId);
|
|
89
118
|
const isEditingPublished = Boolean(tradeItemId);
|
|
90
119
|
const isEditingProduct = Boolean(productId);
|
|
91
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]);
|
|
92
179
|
const saveWarehousePreference = useCallback(async (targetTradeItemId, warehouseId) => {
|
|
93
180
|
if (!activeId || !targetTradeItemId || !warehouseId)
|
|
94
181
|
return;
|
|
@@ -138,6 +225,9 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
138
225
|
.maybeSingle();
|
|
139
226
|
warehouseId = pref?.warehouse_id ?? warehouseId;
|
|
140
227
|
}
|
|
228
|
+
if (!warehouseId) {
|
|
229
|
+
warehouseId = (await resolvePreferredWarehouseId()) ?? "";
|
|
230
|
+
}
|
|
141
231
|
if (cancelled)
|
|
142
232
|
return;
|
|
143
233
|
setData({ ...empty, ...row.data, warehouseId });
|
|
@@ -151,8 +241,13 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
151
241
|
return () => {
|
|
152
242
|
cancelled = true;
|
|
153
243
|
};
|
|
154
|
-
}, [initialDraftId, activeId]);
|
|
244
|
+
}, [initialDraftId, activeId, resolvePreferredWarehouseId]);
|
|
155
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.
|
|
156
251
|
useEffect(() => {
|
|
157
252
|
if (!productId)
|
|
158
253
|
return;
|
|
@@ -162,7 +257,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
162
257
|
try {
|
|
163
258
|
const { data: product, error } = await supabase
|
|
164
259
|
.from("products")
|
|
165
|
-
.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")
|
|
166
261
|
.eq("id", productId)
|
|
167
262
|
.maybeSingle();
|
|
168
263
|
if (cancelled)
|
|
@@ -180,45 +275,160 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
180
275
|
const cpl = Number(product.crates_per_layer) || 0;
|
|
181
276
|
const lpc = Number(product.layers_per_cart) || 0;
|
|
182
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 ?? [];
|
|
183
317
|
const payloadHasPacking = Array.isArray(payload.packingConfigurations) && payload.packingConfigurations.length > 0;
|
|
184
|
-
const seededPacking =
|
|
185
|
-
?
|
|
186
|
-
:
|
|
187
|
-
?
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
:
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
344
|
+
? payload.images
|
|
345
|
+
: imageUrl
|
|
346
|
+
? [{ url: imageUrl, name: product.product ?? undefined, primary: true }]
|
|
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).
|
|
362
|
+
let physical = 0;
|
|
363
|
+
try {
|
|
364
|
+
const { data: stockRow } = await supabase
|
|
365
|
+
.from("trade_item_stock")
|
|
366
|
+
.select("physical_stock")
|
|
367
|
+
.eq("trade_item_key", `product:${product.id}`)
|
|
368
|
+
.maybeSingle();
|
|
369
|
+
physical = Number(stockRow?.physical_stock ?? 0);
|
|
370
|
+
}
|
|
371
|
+
catch { /* stock niet gevonden = 0 */ }
|
|
204
372
|
setData({
|
|
205
373
|
...empty,
|
|
206
374
|
...payload,
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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 ?? ""),
|
|
210
405
|
salePricePerPiece: Number(product.sale_price ?? payload.salePricePerPiece) || 0,
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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,
|
|
217
422
|
linkedProductId: product.id,
|
|
218
423
|
packingConfigurations: seededPacking,
|
|
219
424
|
});
|
|
220
425
|
setDraftId(product.id);
|
|
221
|
-
setDraftFloridayId(
|
|
426
|
+
setDraftFloridayId(fid);
|
|
427
|
+
setProductMeta({
|
|
428
|
+
floridayActive: fActive,
|
|
429
|
+
floridayTradeItemId: fid,
|
|
430
|
+
physicalStock: physical,
|
|
431
|
+
});
|
|
222
432
|
}
|
|
223
433
|
catch (e) {
|
|
224
434
|
if (!cancelled)
|
|
@@ -232,7 +442,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
232
442
|
return () => {
|
|
233
443
|
cancelled = true;
|
|
234
444
|
};
|
|
235
|
-
}, [productId]);
|
|
445
|
+
}, [productId, activeId, resolvePreferredWarehouseId]);
|
|
236
446
|
// Load existing published trade item from the shared cache
|
|
237
447
|
useEffect(() => {
|
|
238
448
|
if (!tradeItemId || !activeId)
|
|
@@ -264,9 +474,15 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
264
474
|
return;
|
|
265
475
|
}
|
|
266
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();
|
|
267
482
|
setData({
|
|
268
483
|
...wizardData,
|
|
269
|
-
|
|
484
|
+
productGroup,
|
|
485
|
+
warehouseId: prefWarehouseId || cachedWarehouseId || preferredWarehouseFallback || "",
|
|
270
486
|
});
|
|
271
487
|
setPublishToFloriday(true);
|
|
272
488
|
}
|
|
@@ -282,7 +498,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
282
498
|
return () => {
|
|
283
499
|
cancelled = true;
|
|
284
500
|
};
|
|
285
|
-
}, [tradeItemId, activeId]);
|
|
501
|
+
}, [tradeItemId, activeId, resolvePreferredWarehouseId]);
|
|
286
502
|
// Floricode data (PascalCase resource namen zoals ze in floricode_records staan)
|
|
287
503
|
const [vbnProducts, setVbnProducts] = useState([]);
|
|
288
504
|
const [featureTypes, setFeatureTypes] = useState([]);
|
|
@@ -326,7 +542,45 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
326
542
|
values: r.values,
|
|
327
543
|
}));
|
|
328
544
|
}, [regulatoryQuery.data]);
|
|
329
|
-
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]);
|
|
330
584
|
// Set van VBN-product-codes die Floriday tijdens write accepteert.
|
|
331
585
|
// VBN-codes zijn 5- of 6-cijferig en staan in `vbn-product-code` (NIET in
|
|
332
586
|
// `vbn-product-group`, dat bevat 8-cijferige groeps-ID's).
|
|
@@ -350,7 +604,49 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
350
604
|
productId: !tradeItemId ? linkedProductId : null,
|
|
351
605
|
draftId: !tradeItemId && !linkedProductId ? (draftId ?? null) : null,
|
|
352
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]);
|
|
353
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.
|
|
354
650
|
const lastPrunedVbnRef = useRef("");
|
|
355
651
|
useEffect(() => {
|
|
356
652
|
if (!data.vbnCode)
|
|
@@ -359,6 +655,16 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
359
655
|
return;
|
|
360
656
|
if (spec.characteristics.length === 0)
|
|
361
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
|
+
}
|
|
362
668
|
lastPrunedVbnRef.current = data.vbnCode;
|
|
363
669
|
const allowedByCode = new Map(spec.characteristics.map((c) => [c.code, c]));
|
|
364
670
|
setData((d) => {
|
|
@@ -379,7 +685,38 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
379
685
|
}
|
|
380
686
|
return { ...d, features: next };
|
|
381
687
|
});
|
|
382
|
-
}, [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]);
|
|
383
720
|
const validateEigenschappen = (d) => {
|
|
384
721
|
const hasRegulatoryLookup = /^\d{5,6}$/.test(String(d.vbnCode ?? "").trim());
|
|
385
722
|
if (hasRegulatoryLookup && regulatoryQuery.isLoading) {
|
|
@@ -536,6 +873,35 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
536
873
|
toast.error(`Prijs bijwerken mislukt: ${e.message}`);
|
|
537
874
|
}
|
|
538
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
|
+
}
|
|
539
905
|
}
|
|
540
906
|
else if (publishToFloriday) {
|
|
541
907
|
const payload = buildAddTradeItemPayload(data);
|
|
@@ -563,23 +929,36 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
563
929
|
// 1) Seed lokale voorraad (physical_stock) — de continuous-stock
|
|
564
930
|
// trigger pusht dit naar Floriday zodra de trade item in de
|
|
565
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.
|
|
566
937
|
try {
|
|
567
938
|
const key = stockKeyFor({ tradeItemId });
|
|
568
939
|
if (key && data.saleStockPieces > 0) {
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
+
}
|
|
583
962
|
}
|
|
584
963
|
}
|
|
585
964
|
}
|
|
@@ -635,6 +1014,31 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
635
1014
|
catch (e) {
|
|
636
1015
|
toast.error(`Prijs online zetten mislukt: ${e.message}`);
|
|
637
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
|
+
}
|
|
638
1042
|
}
|
|
639
1043
|
else {
|
|
640
1044
|
toast.success("Artikel aangemaakt in Floriday");
|
|
@@ -655,7 +1059,27 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
655
1059
|
}
|
|
656
1060
|
catch (e) {
|
|
657
1061
|
const msg = e.message;
|
|
658
|
-
|
|
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")) {
|
|
659
1083
|
toast.error("Geen actieve Floriday-connectie. Ga naar Instellingen → Floriday.");
|
|
660
1084
|
}
|
|
661
1085
|
else {
|
|
@@ -720,7 +1144,6 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
720
1144
|
// een PUT /trade-items/{id} naar Floriday gestuurd en de lokale cache
|
|
721
1145
|
// meteen bijgewerkt, zodat aanpassingen (o.a. beladingen) direct overal in
|
|
722
1146
|
// het systeem beschikbaar zijn en niet verloren raken bij navigatie.
|
|
723
|
-
const lastSavedRef = useRef(null);
|
|
724
1147
|
useEffect(() => {
|
|
725
1148
|
if (!activeId)
|
|
726
1149
|
return;
|
|
@@ -730,7 +1153,10 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
730
1153
|
if (!draftId && !pushTargetId)
|
|
731
1154
|
return;
|
|
732
1155
|
const serialized = JSON.stringify(data);
|
|
733
|
-
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.
|
|
734
1160
|
lastSavedRef.current = serialized;
|
|
735
1161
|
return;
|
|
736
1162
|
}
|
|
@@ -744,8 +1170,22 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
744
1170
|
if (draftId) {
|
|
745
1171
|
await upsertLocalTradeItem({ id: draftId, connectionId: activeId, data });
|
|
746
1172
|
}
|
|
1173
|
+
if (pushTargetId && !data.productGroup) {
|
|
1174
|
+
setSyncState(draftId ? "local" : "idle");
|
|
1175
|
+
lastSavedRef.current = serialized;
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
747
1178
|
// Push naar Floriday als er een gekoppeld/gepubliceerd trade-item is
|
|
748
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
|
+
}
|
|
749
1189
|
await saveWarehousePreference(pushTargetId, data.warehouseId);
|
|
750
1190
|
const payload = buildUpdateTradeItemPayload(data);
|
|
751
1191
|
const res = await floriday.updateTradeItem(activeId, pushTargetId, payload);
|
|
@@ -808,7 +1248,15 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
808
1248
|
if (!activeConnection) {
|
|
809
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." }) }));
|
|
810
1250
|
}
|
|
811
|
-
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) => {
|
|
1252
|
+
setDeleteOpen(o);
|
|
1253
|
+
if (!o)
|
|
1254
|
+
setDeleteAck(false);
|
|
1255
|
+
}, children: [_jsx(AlertDialogTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "sm", className: "text-destructive hover:text-destructive", children: [_jsx(Trash2, { className: "mr-2 h-4 w-4" }), "Verwijderen"] }) }), _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Product verwijderen?" }), _jsx(AlertDialogDescription, { asChild: true, children: _jsxs("div", { className: "space-y-3 text-sm", children: [_jsxs("div", { children: ["Je staat op het punt om ", _jsx("strong", { children: data.name || "dit artikel" }), data.articleCode ? _jsxs(_Fragment, { children: [" (artikelnummer ", _jsx("code", { children: data.articleCode }), ")"] }) : null, " ", "lokaal uit de voorraad te verwijderen."] }), _jsxs("div", { className: "rounded-md border p-3 bg-muted/40 space-y-1", children: [_jsxs("div", { children: ["Huidige voorraad: ", _jsx("strong", { children: productMeta.physicalStock }), " stuks"] }), _jsxs("div", { children: ["Floriday-koppeling:", " ", productMeta.floridayActive || productMeta.floridayTradeItemId ? (_jsx("span", { className: "text-emerald-700 font-medium", children: "actief" })) : (_jsx("span", { className: "text-muted-foreground", children: "geen" }))] })] }), (productMeta.floridayActive || productMeta.floridayTradeItemId) && (_jsxs("div", { className: "rounded-md border border-amber-300 bg-amber-50 p-3 text-amber-900", children: [_jsx("strong", { children: "Let op:" }), " dit artikel is gekoppeld met Floriday. Als je alleen de lokale kant verwijdert, blijft het artikel op Floriday bestaan en verschijnt het bij de volgende sync weer terug in de lijst. Trek het artikel daarna zelf in via het Floriday-portaal als je het echt weg wilt."] })), productMeta.physicalStock > 0 && (_jsxs("div", { className: "rounded-md border border-rose-300 bg-rose-50 p-3 text-rose-900", children: ["Er staat nog voorraad (", productMeta.physicalStock, " stuks) op dit product. Deze voorraad wordt \u00F3\u00F3k verwijderd en kan niet automatisch worden hersteld."] })), _jsxs("label", { className: "flex items-start gap-2 pt-1", children: [_jsx(Checkbox, { checked: deleteAck, onCheckedChange: (v) => setDeleteAck(Boolean(v)) }), _jsx("span", { children: "Ik weet zeker dat ik dit product wil verwijderen en begrijp de gevolgen." })] })] }) })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Annuleren" }), _jsx(AlertDialogAction, { disabled: !deleteAck, onClick: handleDeleteDraft, className: "bg-destructive text-destructive-foreground hover:bg-destructive/90", children: "Definitief verwijderen" })] })] })] }))] }), isEditing && (data.name || data.vbnName) && (_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: "Artikel" }), _jsxs("div", { className: "flex items-center gap-3 flex-wrap", children: [_jsx("h1", { className: "text-2xl font-semibold leading-tight", children: data.name || data.vbnName }), (isEditingPublished || draftFloridayId) ? (syncState === "saving" ? (_jsxs("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground", children: [_jsx(Loader2, { className: "h-3 w-3 animate-spin" }), " Synchroniseren\u2026"] })) : syncState === "saved" ? (_jsxs("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-emerald-100 text-emerald-700", children: [_jsx(Check, { className: "h-3 w-3" }), " Gesynchroniseerd"] })) : syncState === "error" ? (_jsx("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-red-100 text-red-700", title: syncError ?? undefined, children: "Sync mislukt \u2014 klik Opslaan om opnieuw te proberen" })) : null) : (_jsx("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground", children: "Nog niet gepubliceerd naar Floriday" })), _jsx(PriceStatusBadge, { tradeItemId: tradeItemId ?? draftFloridayId ?? null, offerForSale: data.offerForSale })] }), data.name && data.vbnName && data.name !== data.vbnName && (_jsx("div", { className: "text-sm text-muted-foreground", children: data.vbnName }))] })), isEditing && (_jsx(Card, { className: "p-4 bg-muted/30 flex items-center gap-3 text-sm", children: isEditingPublished ? (_jsxs(_Fragment, { children: [_jsx("img", { src: floridayLogo, alt: "Floriday", className: "h-6 w-6 shrink-0" }), _jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "font-medium", children: "Gepubliceerd artikel bewerken" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Wijzigingen worden direct naar Floriday verstuurd. Je kunt vrij tussen alle secties navigeren." })] }), _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" })), "Wijzigingen opslaan"] })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "font-medium", children: "Lokaal artikel bewerken" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Niet gepubliceerd. Je kunt vrij tussen alle secties navigeren." })] }), _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" })), publishToFloriday ? "Publiceren naar Floriday" : "Wijzigingen opslaan"] })] })) })), isEditing && (_jsxs(_Fragment, { children: [_jsx(ArtikelCompositionCard, { productId: linkedProductId, floridayId: tradeItemId ?? draftFloridayId ?? null }), _jsx(FloridayInfoCard, { floridayTradeItemId: tradeItemId ?? draftFloridayId ?? null, local: {
|
|
1256
|
+
name: data.name || data.vbnName,
|
|
1257
|
+
articleNumber: data.articleCode,
|
|
1258
|
+
gtin: data.gtin,
|
|
1259
|
+
} }), _jsx(VoorraadStockCard, { connectionId: activeId, tradeItemId: tradeItemId ?? null, draftId: draftId ?? null, productId: linkedProductId }), _jsx(OrderHistoryCard, { tradeItemKey: localTradeItemKey, productName: data.name || data.vbnName || null, productId: linkedProductId ?? null }), _jsx(ProductLinkCard, { tradeItemKey: localTradeItemKey, productName: data.vbnName || null })] })), _jsx("ol", { className: "space-y-3", children: steps.map((s, i) => {
|
|
812
1260
|
const isOpen = i === step;
|
|
813
1261
|
const isValid = stepValidity[i];
|
|
814
1262
|
const isVisited = i < step || isEditing;
|
|
@@ -830,7 +1278,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
830
1278
|
? "border-primary bg-primary text-primary-foreground"
|
|
831
1279
|
: status === "invalid"
|
|
832
1280
|
? "border-destructive bg-destructive text-destructive-foreground"
|
|
833
|
-
: "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
|
|
834
1282
|
? "Het artikel wordt aangemaakt in Floriday en is binnen enkele minuten zichtbaar voor klanten."
|
|
835
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
|
|
836
1284
|
? "Wijzigingen opslaan"
|
|
@@ -885,9 +1333,13 @@ function summaryFor(key, d) {
|
|
|
885
1333
|
? `${d.additionalPackaging.length} item(s)`
|
|
886
1334
|
: "Geen extra verpakking";
|
|
887
1335
|
case "samenstelling":
|
|
888
|
-
return "
|
|
1336
|
+
return "Basis + per belading";
|
|
889
1337
|
case "beschikbaarheid":
|
|
890
|
-
|
|
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`;
|
|
891
1343
|
case "aanbieden":
|
|
892
1344
|
if (!d.offerForSale)
|
|
893
1345
|
return "Niet aanbieden";
|
|
@@ -920,7 +1372,10 @@ function StepBody(p) {
|
|
|
920
1372
|
case "verpakking":
|
|
921
1373
|
return _jsx(StepVerpakking, { ...p });
|
|
922
1374
|
case "samenstelling":
|
|
923
|
-
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
|
+
} }));
|
|
924
1379
|
case "beschikbaarheid":
|
|
925
1380
|
return _jsx(StepBeschikbaarheid, { ...p });
|
|
926
1381
|
case "aanbieden":
|
|
@@ -955,11 +1410,11 @@ function groupOfRecord(r) {
|
|
|
955
1410
|
const n = Number(raw);
|
|
956
1411
|
if (Number.isFinite(n) && n > 0) {
|
|
957
1412
|
const s = String(Math.floor(n));
|
|
958
|
-
if (s.startsWith("10"))
|
|
1413
|
+
if (s.startsWith("10") || s.startsWith("11"))
|
|
959
1414
|
return "CUT_FLOWER";
|
|
960
|
-
if (s.startsWith("20"))
|
|
1415
|
+
if (s.startsWith("20") || s.startsWith("21") || s.startsWith("29"))
|
|
961
1416
|
return "POT_PLANT";
|
|
962
|
-
if (s.startsWith("30"))
|
|
1417
|
+
if (s.startsWith("30") || s.startsWith("31"))
|
|
963
1418
|
return "GARDEN_PLANT";
|
|
964
1419
|
return null;
|
|
965
1420
|
}
|
|
@@ -1056,8 +1511,12 @@ function StepVbn({ data, update, acceptedVbnCodes }) {
|
|
|
1056
1511
|
setQuery("");
|
|
1057
1512
|
};
|
|
1058
1513
|
const triggerLabel = data.vbnName
|
|
1059
|
-
? data.
|
|
1060
|
-
|
|
1514
|
+
? data.vbnCode
|
|
1515
|
+
? `${data.vbnCode} – ${data.vbnName}`
|
|
1516
|
+
: data.vbnName
|
|
1517
|
+
: data.vbnCode
|
|
1518
|
+
? data.vbnCode
|
|
1519
|
+
: "Selecteer een VBN productcode";
|
|
1061
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) => {
|
|
1062
1521
|
const next = v === "ALL" ? null : v;
|
|
1063
1522
|
update("productGroup", next);
|
|
@@ -1092,7 +1551,7 @@ function StepVbn({ data, update, acceptedVbnCodes }) {
|
|
|
1092
1551
|
function InfoTip({ text }) {
|
|
1093
1552
|
return (_jsx("span", { title: text, className: "inline-flex cursor-help text-muted-foreground", children: _jsx(Info, { className: "h-3.5 w-3.5" }) }));
|
|
1094
1553
|
}
|
|
1095
|
-
function StepArtikel({ data, update, collectionsList, setCollectionsList }) {
|
|
1554
|
+
function StepArtikel({ data, update, collectionsList, setCollectionsList, articleCodeLocked }) {
|
|
1096
1555
|
const [newCollection, setNewCollection] = useState("");
|
|
1097
1556
|
const [showNewCollection, setShowNewCollection] = useState(false);
|
|
1098
1557
|
const addCollection = async (name) => {
|
|
@@ -1122,7 +1581,7 @@ function StepArtikel({ data, update, collectionsList, setCollectionsList }) {
|
|
|
1122
1581
|
icon: ClipboardList,
|
|
1123
1582
|
},
|
|
1124
1583
|
];
|
|
1125
|
-
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) => {
|
|
1126
1585
|
if (v === "__new__") {
|
|
1127
1586
|
setShowNewCollection(true);
|
|
1128
1587
|
return;
|
|
@@ -1144,7 +1603,7 @@ function StepArtikel({ data, update, collectionsList, setCollectionsList }) {
|
|
|
1144
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: () => {
|
|
1145
1604
|
setShowNewCollection(false);
|
|
1146
1605
|
setNewCollection("");
|
|
1147
|
-
}, 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) => {
|
|
1148
1607
|
const Icon = t.icon;
|
|
1149
1608
|
const active = data.articleType === t.value;
|
|
1150
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
|
|
@@ -1207,7 +1666,10 @@ function StepPaspoort({ data, update }) {
|
|
|
1207
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) => {
|
|
1208
1667
|
const checked = selectedCountries.includes(c.code);
|
|
1209
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));
|
|
1210
|
-
})] })] })] })
|
|
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
|
+
}) }))] })] }));
|
|
1211
1673
|
}
|
|
1212
1674
|
function StepEigenschappen({ data, update, spec, schemaQuery, regulatoryQuery }) {
|
|
1213
1675
|
if (!data.vbnCode) {
|
|
@@ -1265,6 +1727,11 @@ function EigenschappenBody({ data, update, spec, }) {
|
|
|
1265
1727
|
const hasValue = (code) => Boolean((data.features[code] ?? "").trim());
|
|
1266
1728
|
const visibleFields = spec.characteristics.filter((c) => c.required || visibleOptional.has(c.code) || hasValue(c.code));
|
|
1267
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));
|
|
1268
1735
|
const filteredHidden = hiddenOptional.filter((c) => {
|
|
1269
1736
|
if (!addSearch.trim())
|
|
1270
1737
|
return true;
|
|
@@ -1291,7 +1758,13 @@ function EigenschappenBody({ data, update, spec, }) {
|
|
|
1291
1758
|
delete nextFeatures[code];
|
|
1292
1759
|
update("features", nextFeatures);
|
|
1293
1760
|
};
|
|
1294
|
-
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)
|
|
1295
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
|
|
1296
1769
|
? "Alle beschikbare karakteristieken zijn al toegevoegd."
|
|
1297
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)))) })] })] }) })] }));
|
|
@@ -1472,7 +1945,34 @@ function StepFotos({ data, update, activeId }) {
|
|
|
1472
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) => {
|
|
1473
1946
|
onFiles(e.target.files);
|
|
1474
1947
|
e.target.value = "";
|
|
1475
|
-
} })] }), 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
|
+
}) }))] }));
|
|
1476
1976
|
}
|
|
1477
1977
|
/**
|
|
1478
1978
|
* Warehouse-stap — verplicht in Floriday Suppliers API 2026v1.
|
|
@@ -1571,12 +2071,12 @@ function StepBeladingen({ data, update, packagingTypes, loadCarriers, floridayCu
|
|
|
1571
2071
|
const editing = editingId
|
|
1572
2072
|
? data.packingConfigurations.find((p) => p.id === editingId) ?? null
|
|
1573
2073
|
: null;
|
|
1574
|
-
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) => {
|
|
1575
2075
|
const pkgLabel = packagingTypes.find((r) => codeOf(r) === p.packagingType);
|
|
1576
2076
|
const lcLabel = loadCarriers.find((r) => codeOf(r) === p.loadCarrier);
|
|
1577
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
|
|
1578
2078
|
? `${codeOf(pkgLabel)}: ${labelOf(pkgLabel)}`
|
|
1579
|
-
: 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));
|
|
1580
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) => {
|
|
1581
2081
|
setDialogOpen(o);
|
|
1582
2082
|
if (!o)
|
|
@@ -1598,6 +2098,7 @@ function PackingConfigDialog({ open, onOpenChange, initial, isFirst, packagingTy
|
|
|
1598
2098
|
isCustomerSpecific: false,
|
|
1599
2099
|
customerOrganizationIds: [],
|
|
1600
2100
|
isPrimary: isFirst,
|
|
2101
|
+
isHiddenForDirectSales: false,
|
|
1601
2102
|
});
|
|
1602
2103
|
const [draft, setDraft] = useState(emptyItem);
|
|
1603
2104
|
const [pkgSearch, setPkgSearch] = useState("");
|
|
@@ -1627,8 +2128,16 @@ function PackingConfigDialog({ open, onOpenChange, initial, isFirst, packagingTy
|
|
|
1627
2128
|
? draft.customerOrganizationIds.filter((x) => x !== id)
|
|
1628
2129
|
: [...draft.customerOrganizationIds, id]);
|
|
1629
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;
|
|
1630
2138
|
const canSave = draft.loadCarrier.length > 0 &&
|
|
1631
|
-
draft.packagingType.length > 0 &&
|
|
2139
|
+
(hasCustomPkg || draft.packagingType.length > 0) &&
|
|
2140
|
+
!materialInvalid &&
|
|
1632
2141
|
(draft.source === "VBN" || Boolean(draft.packagingSupplierId)) &&
|
|
1633
2142
|
(!draft.isCustomerSpecific || draft.customerOrganizationIds.length > 0);
|
|
1634
2143
|
const onUploadPhoto = async (file) => {
|
|
@@ -1665,14 +2174,22 @@ function PackingConfigDialog({ open, onOpenChange, initial, isFirst, packagingTy
|
|
|
1665
2174
|
? "border-primary bg-primary/5"
|
|
1666
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"
|
|
1667
2176
|
? "border-primary bg-primary/5"
|
|
1668
|
-
: "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(
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
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
|
|
1676
2193
|
? (packagingTypes.find((r) => codeOf(r) === draft.packagingType)
|
|
1677
2194
|
? `${labelOf(packagingTypes.find((r) => codeOf(r) === draft.packagingType))} (${draft.packagingType})`
|
|
1678
2195
|
: draft.packagingType)
|
|
@@ -1684,7 +2201,7 @@ function PackingConfigDialog({ open, onOpenChange, initial, isFirst, packagingTy
|
|
|
1684
2201
|
photoUrl: undefined,
|
|
1685
2202
|
photoImageId: undefined,
|
|
1686
2203
|
photoPreviewUrl: undefined,
|
|
1687
|
-
})), 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" })] })] }) }));
|
|
1688
2205
|
}
|
|
1689
2206
|
function StepVerpakking({ data, update }) {
|
|
1690
2207
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
@@ -1693,14 +2210,14 @@ function StepVerpakking({ data, update }) {
|
|
|
1693
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 })] }));
|
|
1694
2211
|
}
|
|
1695
2212
|
function StepBeschikbaarheid({ data, update }) {
|
|
1696
|
-
const weeks = Array.from({ length:
|
|
2213
|
+
const weeks = Array.from({ length: 53 }, (_, i) => i + 1);
|
|
1697
2214
|
// Drag-selectie: bij mousedown bepalen we of we in "select"- of "deselect"-
|
|
1698
2215
|
// modus zitten (op basis van de startcel) en breiden we de selectie uit
|
|
1699
2216
|
// terwijl de gebruiker over de weken sleept, zoals in Floriday.
|
|
1700
2217
|
const dragStateRef = useRef(null);
|
|
1701
2218
|
const [dragHover, setDragHover] = useState(null);
|
|
1702
2219
|
const setSelection = (list) => {
|
|
1703
|
-
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)))
|
|
1704
2221
|
.sort((a, b) => a - b);
|
|
1705
2222
|
update("weeks", unique);
|
|
1706
2223
|
};
|
|
@@ -1806,7 +2323,14 @@ function StepBeschikbaarheid({ data, update }) {
|
|
|
1806
2323
|
pointerXRef.current = e.clientX;
|
|
1807
2324
|
startAutoScrollLoop();
|
|
1808
2325
|
};
|
|
1809
|
-
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) => {
|
|
1810
2334
|
const selected = data.weeks.includes(w);
|
|
1811
2335
|
const isHover = dragHover === w;
|
|
1812
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
|
|
@@ -1817,18 +2341,56 @@ function StepBeschikbaarheid({ data, update }) {
|
|
|
1817
2341
|
// ------------------------------------------------------------
|
|
1818
2342
|
// Stap "Aanbieden & voorraad" — Direct Sale via CustomerOffer
|
|
1819
2343
|
// ------------------------------------------------------------
|
|
1820
|
-
function StepAanbieden({ data, update }) {
|
|
2344
|
+
function StepAanbieden({ data, update, priceGroups }) {
|
|
1821
2345
|
const priceStr = Number.isFinite(data.salePricePerPiece)
|
|
1822
2346
|
? String(data.salePricePerPiece)
|
|
1823
2347
|
: "";
|
|
1824
2348
|
const stockStr = Number.isFinite(data.saleStockPieces)
|
|
1825
2349
|
? String(data.saleStockPieces)
|
|
1826
2350
|
: "";
|
|
1827
|
-
|
|
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
|
|
1828
2363
|
? "Aanbod wordt aangemaakt direct na publiceren."
|
|
1829
|
-
: "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: "
|
|
1830
|
-
|
|
1831
|
-
|
|
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)))] })] })] })] }));
|
|
1832
2394
|
}
|
|
1833
2395
|
// ============================================================
|
|
1834
2396
|
// Shared
|