@flowselections/floriday-voorraad 1.0.113 → 1.0.115
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-lib/components/voorraad/ArtikelWizard.d.ts.map +1 -1
- package/dist-lib/components/voorraad/ArtikelWizard.js +63 -7
- package/dist-lib/components/voorraad/PublishProgressCard.d.ts +5 -0
- package/dist-lib/components/voorraad/PublishProgressCard.d.ts.map +1 -1
- package/dist-lib/components/voorraad/PublishProgressCard.js +16 -1
- package/dist-lib/components/voorraad/VoorraadPage.d.ts +13 -0
- package/dist-lib/components/voorraad/VoorraadPage.d.ts.map +1 -1
- package/dist-lib/components/voorraad/VoorraadPage.js +28 -17
- package/dist-lib/lib/__tests__/publish-114.test.d.ts +2 -0
- package/dist-lib/lib/__tests__/publish-114.test.d.ts.map +1 -0
- package/dist-lib/lib/__tests__/publish-114.test.js +31 -0
- package/dist-lib/lib/publish-errors.d.ts +6 -1
- package/dist-lib/lib/publish-errors.d.ts.map +1 -1
- package/dist-lib/lib/publish-errors.js +22 -2
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ArtikelWizard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/ArtikelWizard.tsx"],"names":[],"mappings":"AA+MA,wBAAgB,aAAa,CAAC,EAC5B,OAAO,EAAE,cAAc,EACvB,WAAW,EACX,SAAS,EACT,WAAW,GACZ,GAAE;IACD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACjB,+
|
|
1
|
+
{"version":3,"file":"ArtikelWizard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/ArtikelWizard.tsx"],"names":[],"mappings":"AA+MA,wBAAgB,aAAa,CAAC,EAC5B,OAAO,EAAE,cAAc,EACvB,WAAW,EACX,SAAS,EACT,WAAW,GACZ,GAAE;IACD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACjB,+BAwwEL"}
|
|
@@ -1195,6 +1195,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
1195
1195
|
}
|
|
1196
1196
|
// Publiceren begint altijd met een lokale opslag, zodat een afgebroken of
|
|
1197
1197
|
// mislukte publicatie nooit tot verlies van ingevoerde gegevens leidt.
|
|
1198
|
+
let savedLocalId = draftId ?? null;
|
|
1198
1199
|
try {
|
|
1199
1200
|
const localId = await upsertLocalTradeItem({
|
|
1200
1201
|
id: draftId,
|
|
@@ -1203,6 +1204,8 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
1203
1204
|
floridayTradeItemId: (publishedTradeItemId || draftFloridayId || null),
|
|
1204
1205
|
sync: { dirty: true, last_error: null },
|
|
1205
1206
|
});
|
|
1207
|
+
if (localId)
|
|
1208
|
+
savedLocalId = localId;
|
|
1206
1209
|
if (localId && localId !== draftId)
|
|
1207
1210
|
setDraftId(localId);
|
|
1208
1211
|
}
|
|
@@ -1210,6 +1213,50 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
1210
1213
|
toast.error(`Lokaal opslaan mislukt: ${e.message}`);
|
|
1211
1214
|
return;
|
|
1212
1215
|
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Na een mislukte eerste publicatie: zet de ingevoerde voorraad en prijs op
|
|
1218
|
+
* het lokale artikel, alleen als daar nog niets staat (nooit overschrijven).
|
|
1219
|
+
*/
|
|
1220
|
+
const keepLocalInputAfterFailure = async () => {
|
|
1221
|
+
if (!savedLocalId)
|
|
1222
|
+
return;
|
|
1223
|
+
try {
|
|
1224
|
+
const { data: prod } = await supabase
|
|
1225
|
+
.from("products")
|
|
1226
|
+
.select("sale_price")
|
|
1227
|
+
.eq("id", savedLocalId)
|
|
1228
|
+
.maybeSingle();
|
|
1229
|
+
if (data.salePricePerPiece > 0 && !(Number(prod?.sale_price) > 0)) {
|
|
1230
|
+
await supabase
|
|
1231
|
+
.from("products")
|
|
1232
|
+
.update({ sale_price: data.salePricePerPiece })
|
|
1233
|
+
.eq("id", savedLocalId);
|
|
1234
|
+
}
|
|
1235
|
+
if (data.saleStockPieces > 0) {
|
|
1236
|
+
const { data: cur } = await supabase.rpc("get_product_stock", {
|
|
1237
|
+
p_product_id: savedLocalId,
|
|
1238
|
+
});
|
|
1239
|
+
const row = Array.isArray(cur) ? cur[0] : cur;
|
|
1240
|
+
if (!(Number(row?.physical_stock) > 0)) {
|
|
1241
|
+
const { error } = await supabase.rpc("set_product_stock", {
|
|
1242
|
+
p_product_id: savedLocalId,
|
|
1243
|
+
p_physical_stock: Math.trunc(data.saleStockPieces),
|
|
1244
|
+
p_minimum_stock: null,
|
|
1245
|
+
p_incoming_stock: null,
|
|
1246
|
+
p_notes: "Ingevoerd in wizard; publiceren mislukt",
|
|
1247
|
+
p_change_source: "NEW_ARTICLE",
|
|
1248
|
+
});
|
|
1249
|
+
if (error)
|
|
1250
|
+
throw new Error(error.message);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
catch (e) {
|
|
1255
|
+
toast.error("Voorraad of prijs kon niet lokaal bewaard worden", {
|
|
1256
|
+
description: e.message,
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
};
|
|
1213
1260
|
try {
|
|
1214
1261
|
const serverConnection = await getFloridayReadinessFn();
|
|
1215
1262
|
const browserEnvironment = activeConnection?.active_environment;
|
|
@@ -1304,6 +1351,9 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
|
|
|
1304
1351
|
return { tradeItemId: createdId };
|
|
1305
1352
|
}, stepOpts);
|
|
1306
1353
|
if (!core.ok) {
|
|
1354
|
+
// 1.0.114: lokale invoer nooit verliezen door een mislukte publicatie.
|
|
1355
|
+
if (!publishedTradeItemId)
|
|
1356
|
+
await keepLocalInputAfterFailure();
|
|
1307
1357
|
await handleStepFailure(core.error);
|
|
1308
1358
|
if (jobId)
|
|
1309
1359
|
await finishPublishJob(jobId);
|
|
@@ -2592,9 +2642,10 @@ function PackingConfigDialog({ open, onOpenChange, initial, isFirst, packagingTy
|
|
|
2592
2642
|
source: "VBN",
|
|
2593
2643
|
loadCarrier: "AUCTION_TROLLEY",
|
|
2594
2644
|
packagingType: "",
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2645
|
+
// Geen standaardwaarden: de gebruiker vult alle drie aantallen zelf in.
|
|
2646
|
+
unitsPerPackage: 0,
|
|
2647
|
+
packagesPerLayer: 0,
|
|
2648
|
+
layersPerLoadCarrier: 0,
|
|
2598
2649
|
isCustomerSpecific: false,
|
|
2599
2650
|
customerOrganizationIds: [],
|
|
2600
2651
|
isPrimary: isFirst,
|
|
@@ -2635,7 +2686,12 @@ function PackingConfigDialog({ open, onOpenChange, initial, isFirst, packagingTy
|
|
|
2635
2686
|
const materialInvalid = draft.source === "SUPPLIER" &&
|
|
2636
2687
|
!!(draft.packagingMaterialId ?? "").trim() &&
|
|
2637
2688
|
!materialIsUuid;
|
|
2638
|
-
|
|
2689
|
+
// 1.0.114: aantallen worden nooit vooraf ingevuld; alle drie verplicht ≥ 1.
|
|
2690
|
+
const countsComplete = draft.unitsPerPackage >= 1 &&
|
|
2691
|
+
draft.packagesPerLayer >= 1 &&
|
|
2692
|
+
draft.layersPerLoadCarrier >= 1;
|
|
2693
|
+
const canSave = countsComplete &&
|
|
2694
|
+
draft.loadCarrier.length > 0 &&
|
|
2639
2695
|
(hasCustomPkg || draft.packagingType.length > 0) &&
|
|
2640
2696
|
!materialInvalid &&
|
|
2641
2697
|
(draft.source === "VBN" || Boolean(draft.packagingSupplierId)) &&
|
|
@@ -2693,7 +2749,7 @@ function PackingConfigDialog({ open, onOpenChange, initial, isFirst, packagingTy
|
|
|
2693
2749
|
? (packagingTypes.find((r) => codeOf(r) === draft.packagingType)
|
|
2694
2750
|
? `${labelOf(packagingTypes.find((r) => codeOf(r) === draft.packagingType))} (${draft.packagingType})`
|
|
2695
2751
|
: draft.packagingType)
|
|
2696
|
-
: "Selecteer fust..." }), _jsx(ChevronDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })] }) }), _jsxs(PopoverContent, { className: "w-[380px] p-0", align: "start", children: [_jsx("div", { className: "border-b p-2", children: _jsx(Input, { placeholder: "Zoek op naam of code...", value: pkgSearch, onChange: (e) => setPkgSearch(e.target.value) }) }), _jsx("div", { className: "max-h-64 overflow-y-auto", children: filteredPkg.length === 0 ? (_jsx("div", { className: "px-3 py-4 text-center text-xs text-muted-foreground", children: "Geen resultaten" })) : (filteredPkg.map((r) => (_jsxs("button", { type: "button", onClick: () => patch("packagingType", codeOf(r)), className: `flex w-full items-start gap-2 px-3 py-2 text-left text-sm hover:bg-muted ${draft.packagingType === codeOf(r) ? "bg-muted" : ""}`, children: [_jsx("span", { className: "font-mono text-xs text-muted-foreground w-14 shrink-0", children: codeOf(r) }), _jsx("span", { className: "truncate", children: labelOf(r) })] }, r.record_id)))) })] })] })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: "Ladingdrager" }), _jsxs(Select, { value: draft.loadCarrier || "AUCTION_TROLLEY", onValueChange: (v) => patch("loadCarrier", v), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies een ladingdrager" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "AUCTION_TROLLEY", children: "Stapelwagen" }), _jsx(SelectItem, { value: "DANISH_TROLLEY", children: "Deense container" }), _jsx(SelectItem, { value: "EURO_TROLLEY", children: "Euro-rolcontainer" }), _jsx(SelectItem, { value: "PALLET", children: "Pallet" }), _jsx(SelectItem, { value: "EURO_PALLET", children: "Euro-pallet" }), _jsx(SelectItem, { value: "NONE", children: "Geen ladingdrager" })] })] })] })] }), _jsxs("div", { className: "grid grid-cols-3 gap-3", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: "Stuks per fust" }), _jsx(Input, { type: "number", min: 1, value: draft.unitsPerPackage, onChange: (e) => patch("unitsPerPackage", Number(e.target.value) ||
|
|
2752
|
+
: "Selecteer fust..." }), _jsx(ChevronDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })] }) }), _jsxs(PopoverContent, { className: "w-[380px] p-0", align: "start", children: [_jsx("div", { className: "border-b p-2", children: _jsx(Input, { placeholder: "Zoek op naam of code...", value: pkgSearch, onChange: (e) => setPkgSearch(e.target.value) }) }), _jsx("div", { className: "max-h-64 overflow-y-auto", children: filteredPkg.length === 0 ? (_jsx("div", { className: "px-3 py-4 text-center text-xs text-muted-foreground", children: "Geen resultaten" })) : (filteredPkg.map((r) => (_jsxs("button", { type: "button", onClick: () => patch("packagingType", codeOf(r)), className: `flex w-full items-start gap-2 px-3 py-2 text-left text-sm hover:bg-muted ${draft.packagingType === codeOf(r) ? "bg-muted" : ""}`, children: [_jsx("span", { className: "font-mono text-xs text-muted-foreground w-14 shrink-0", children: codeOf(r) }), _jsx("span", { className: "truncate", children: labelOf(r) })] }, r.record_id)))) })] })] })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: "Ladingdrager" }), _jsxs(Select, { value: draft.loadCarrier || "AUCTION_TROLLEY", onValueChange: (v) => patch("loadCarrier", v), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies een ladingdrager" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "AUCTION_TROLLEY", children: "Stapelwagen" }), _jsx(SelectItem, { value: "DANISH_TROLLEY", children: "Deense container" }), _jsx(SelectItem, { value: "EURO_TROLLEY", children: "Euro-rolcontainer" }), _jsx(SelectItem, { value: "PALLET", children: "Pallet" }), _jsx(SelectItem, { value: "EURO_PALLET", children: "Euro-pallet" }), _jsx(SelectItem, { value: "NONE", children: "Geen ladingdrager" })] })] })] })] }), _jsxs("div", { className: "grid grid-cols-3 gap-3", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: "Stuks per fust" }), _jsx(Input, { type: "number", min: 1, placeholder: "Invullen", value: draft.unitsPerPackage > 0 ? draft.unitsPerPackage : "", onChange: (e) => patch("unitsPerPackage", Math.max(0, Math.floor(Number(e.target.value) || 0))) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: "Fusten per laag" }), _jsx(Input, { type: "number", min: 1, placeholder: "Invullen", value: draft.packagesPerLayer > 0 ? draft.packagesPerLayer : "", onChange: (e) => patch("packagesPerLayer", Math.max(0, Math.floor(Number(e.target.value) || 0))) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: "Lagen per drager" }), _jsx(Input, { type: "number", min: 1, placeholder: "Invullen", value: draft.layersPerLoadCarrier > 0 ? draft.layersPerLoadCarrier : "", onChange: (e) => patch("layersPerLoadCarrier", Math.max(0, Math.floor(Number(e.target.value) || 0))) })] }), !countsComplete && (_jsx("p", { className: "col-span-3 text-xs text-muted-foreground", children: "Vul stuks per fust, fusten per laag en lagen per drager in; er wordt niets vooraf ingevuld." }))] }), _jsxs("div", { className: "rounded-md border p-3 space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-medium", children: "Klantspecifieke belading" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Alleen zichtbaar voor geselecteerde klantorganisaties." })] }), _jsx(Switch, { checked: draft.isCustomerSpecific, onCheckedChange: (v) => patch("isCustomerSpecific", Boolean(v)) })] }), draft.isCustomerSpecific && (_jsx("div", { className: "max-h-40 overflow-y-auto rounded border divide-y", children: floridayCustomers.length === 0 ? (_jsx("div", { className: "p-3 text-xs text-muted-foreground", children: "Geen klantorganisaties geladen." })) : (floridayCustomers.map((c) => (_jsxs("label", { className: "flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted cursor-pointer", children: [_jsx("input", { type: "checkbox", checked: draft.customerOrganizationIds.includes(c.id), onChange: () => toggleCustomer(c.id) }), _jsx("span", { className: "truncate", children: c.name })] }, c.id)))) }))] }), _jsxs("div", { className: "rounded-md border p-3 space-y-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-medium", children: "Foto van dit fust (optioneel)" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "JPG of PNG \u00B7 min. 640 \u00D7 640 px \u00B7 max. 5 MB" })] }), _jsx(Button, { type: "button", variant: "outline", size: "sm", disabled: uploadingPhoto || !activeId, onClick: () => photoInputRef.current?.click(), children: uploadingPhoto ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), " Uploaden"] })) : (_jsxs(_Fragment, { children: [_jsx(Upload, { className: "mr-2 h-4 w-4" }), " Foto kiezen"] })) }), _jsx("input", { ref: photoInputRef, type: "file", accept: "image/png,image/jpeg", className: "hidden", onChange: (e) => {
|
|
2697
2753
|
onUploadPhoto(e.target.files?.[0]);
|
|
2698
2754
|
e.target.value = "";
|
|
2699
2755
|
} })] }), (draft.photoPreviewUrl || draft.photoUrl) && (_jsxs("div", { className: "relative inline-block", children: [_jsx("img", { src: draft.photoPreviewUrl || draft.photoUrl, alt: "Belading foto", className: "h-24 rounded-md border object-cover" }), _jsx("button", { type: "button", onClick: () => setDraft((d) => ({
|
|
@@ -2859,9 +2915,9 @@ function StepAanbieden({ data, update, priceGroups, parentProductId }) {
|
|
|
2859
2915
|
}
|
|
2860
2916
|
update("priceGroupPrices", next);
|
|
2861
2917
|
};
|
|
2862
|
-
return (_jsxs("div", { className: "space-y-6", children: [
|
|
2918
|
+
return (_jsxs("div", { className: "space-y-6", children: [_jsx("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. Na publiceren staat het artikel bij Floriday, krijgen je klanten een aanbod met de basisprijs per stuk, en wordt je voorraad doorgegeven. Prijzen per prijsgroep worden apart doorgegeven als prijs per week." }), _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
|
|
2863
2919
|
? "Aanbod wordt aangemaakt direct na publiceren."
|
|
2864
|
-
: "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
|
|
2920
|
+
: "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 liggende voorraad geboekt en daarna aan Floriday doorgegeven. Mislukt het publiceren, dan blijft dit aantal hier bewaard." })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Basisprijs per stuk (EUR, excl. btw)" }), _jsx(Input, { type: "number", inputMode: "decimal", min: 0, step: "0.01", value: priceStr, onChange: (e) => update("salePricePerPiece", Number(e.target.value) || 0) }), _jsx("p", { className: "text-xs text-muted-foreground", children: "De standaardprijs per week voor klanten zonder eigen prijsgroep. Prijzen per prijsgroep vul je hieronder apart in." })] }), _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
|
|
2865
2921
|
? `${data.customerOrganizationIds.length} geselecteerde klant(en)`
|
|
2866
2922
|
: "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-koppeling." })) : (_jsx("div", { className: "grid gap-3 sm:grid-cols-2 pt-2", children: priceGroups.map((g) => {
|
|
2867
2923
|
const val = data.priceGroupPrices?.[g.id];
|
|
@@ -9,4 +9,9 @@ export declare function PublishProgressCard({ job, steps, onRetry, retrying, onF
|
|
|
9
9
|
retrying?: boolean;
|
|
10
10
|
onFixStep?: (stepKey: string) => void;
|
|
11
11
|
}): import("react").JSX.Element | null;
|
|
12
|
+
/**
|
|
13
|
+
* Een job die nog op "running" staat terwijl geen stap meer loopt en er een
|
|
14
|
+
* stap mislukt is, telt als mislukt (scherm nooit blijven hangen op "Bezig").
|
|
15
|
+
*/
|
|
16
|
+
export declare function effectiveJobStatus(jobStatus: string, steps: Array<Pick<PublishStepRow, "status" | "step_key">>): string;
|
|
12
17
|
//# sourceMappingURL=PublishProgressCard.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PublishProgressCard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/PublishProgressCard.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAoBhF;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,GAAG,EACH,KAAK,EACL,OAAO,EACP,QAAQ,EACR,SAAS,GACV,EAAE;IACD,GAAG,EAAE,aAAa,GAAG,IAAI,CAAC;IAC1B,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC,
|
|
1
|
+
{"version":3,"file":"PublishProgressCard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/PublishProgressCard.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAoBhF;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,GAAG,EACH,KAAK,EACL,OAAO,EACP,QAAQ,EACR,SAAS,GACV,EAAE;IACD,GAAG,EAAE,aAAa,GAAG,IAAI,CAAC;IAC1B,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC,sCA0DA;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,cAAc,EAAE,QAAQ,GAAG,UAAU,CAAC,CAAC,GACxD,MAAM,CAMR"}
|
|
@@ -26,5 +26,20 @@ export function PublishProgressCard({ job, steps, onRetry, retrying, onFixStep,
|
|
|
26
26
|
return null;
|
|
27
27
|
const visible = steps.filter((s) => s.status !== "skipped");
|
|
28
28
|
const failed = visible.filter((s) => s.status === "failed");
|
|
29
|
-
|
|
29
|
+
const status = effectiveJobStatus(job.status, visible);
|
|
30
|
+
return (_jsxs(Card, { className: "p-4 space-y-3", children: [_jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-medium", children: JOB_TITLE[status] ?? "Publicatie" }), _jsxs("p", { className: "text-xs text-muted-foreground", children: ["Referentie ", job.reference_code] })] }), status !== "running" && failed.length > 0 && onRetry && (_jsxs(Button, { size: "sm", variant: "outline", onClick: onRetry, disabled: retrying, children: [retrying ? (_jsx(Loader2, { className: "mr-1 h-3 w-3 animate-spin" })) : (_jsx(RefreshCw, { className: "mr-1 h-3 w-3" })), "Opnieuw proberen"] }))] }), _jsx("ul", { className: "space-y-2", children: visible.map((s) => (_jsxs("li", { className: "flex items-start gap-2 text-sm", children: [_jsx("span", { className: "mt-0.5", children: _jsx(StatusIcon, { status: s.status }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: s.status === "done" ? "text-muted-foreground" : "", children: s.label }), s.status === "failed" && s.error_message && (_jsx("div", { className: "text-xs text-destructive", children: s.error_message }))] }), s.status === "failed" && onFixStep && (_jsx(Button, { size: "sm", variant: "ghost", className: "h-6 px-2 text-xs", onClick: () => onFixStep(s.step_key), children: "Oplossen" }))] }, s.id))) })] }));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Een job die nog op "running" staat terwijl geen stap meer loopt en er een
|
|
34
|
+
* stap mislukt is, telt als mislukt (scherm nooit blijven hangen op "Bezig").
|
|
35
|
+
*/
|
|
36
|
+
export function effectiveJobStatus(jobStatus, steps) {
|
|
37
|
+
if (jobStatus !== "running")
|
|
38
|
+
return jobStatus;
|
|
39
|
+
const anyRunning = steps.some((s) => s.status === "running");
|
|
40
|
+
if (anyRunning)
|
|
41
|
+
return jobStatus;
|
|
42
|
+
if (steps.some((s) => s.status === "failed" && s.step_key === "trade-item"))
|
|
43
|
+
return "failed";
|
|
44
|
+
return jobStatus;
|
|
30
45
|
}
|
|
@@ -1,2 +1,15 @@
|
|
|
1
|
+
import type { TradeItem } from "../../lib/floriday-client.js";
|
|
2
|
+
/**
|
|
3
|
+
* 1.0.114: "Op voorraad" telt liggende voorraad (fysiek > 0), niet vrij
|
|
4
|
+
* beschikbaar. Reserveringen voor orders op "Nog te picken" lieten anders alles
|
|
5
|
+
* als "Niet op voorraad" zien zonder dat er iets was afgeboekt.
|
|
6
|
+
*/
|
|
7
|
+
export declare function countStock(items: Array<Pick<TradeItem, "quantity" | "availableQuantity" | "minQuantity">>): {
|
|
8
|
+
total: number;
|
|
9
|
+
inStock: number;
|
|
10
|
+
outOfStock: number;
|
|
11
|
+
belowMin: number;
|
|
12
|
+
fullyReserved: number;
|
|
13
|
+
};
|
|
1
14
|
export declare function VoorraadPage(): import("react").JSX.Element;
|
|
2
15
|
//# sourceMappingURL=VoorraadPage.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VoorraadPage.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/VoorraadPage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"VoorraadPage.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/VoorraadPage.tsx"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAiF3D;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,UAAU,GAAG,mBAAmB,GAAG,aAAa,CAAC,CAAC;;;;;;EAczG;AAED,wBAAgB,YAAY,gCAiW3B"}
|
|
@@ -44,6 +44,30 @@ function KpiCard({ icon, label, count, tone, active, onClick, }) {
|
|
|
44
44
|
function availableOf(i) {
|
|
45
45
|
return i.availableQuantity ?? i.quantity ?? 0;
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* 1.0.114: "Op voorraad" telt liggende voorraad (fysiek > 0), niet vrij
|
|
49
|
+
* beschikbaar. Reserveringen voor orders op "Nog te picken" lieten anders alles
|
|
50
|
+
* als "Niet op voorraad" zien zonder dat er iets was afgeboekt.
|
|
51
|
+
*/
|
|
52
|
+
export function countStock(items) {
|
|
53
|
+
let inStock = 0;
|
|
54
|
+
let outOfStock = 0;
|
|
55
|
+
let belowMin = 0;
|
|
56
|
+
let fullyReserved = 0;
|
|
57
|
+
for (const i of items) {
|
|
58
|
+
const physical = Number(i.quantity ?? 0);
|
|
59
|
+
const avail = availableOf(i);
|
|
60
|
+
if (physical > 0)
|
|
61
|
+
inStock++;
|
|
62
|
+
else
|
|
63
|
+
outOfStock++;
|
|
64
|
+
if (physical > 0 && avail <= 0)
|
|
65
|
+
fullyReserved++;
|
|
66
|
+
if ((i.minQuantity ?? 0) > 0 && avail < (i.minQuantity ?? 0))
|
|
67
|
+
belowMin++;
|
|
68
|
+
}
|
|
69
|
+
return { total: items.length, inStock, outOfStock, belowMin, fullyReserved };
|
|
70
|
+
}
|
|
47
71
|
export function VoorraadPage() {
|
|
48
72
|
const auth = useAuthState();
|
|
49
73
|
const conn = useFloridayConnection();
|
|
@@ -80,30 +104,17 @@ export function VoorraadPage() {
|
|
|
80
104
|
setDrawerIds(ids);
|
|
81
105
|
setDrawerOpen(true);
|
|
82
106
|
};
|
|
83
|
-
const counts = useMemo(() =>
|
|
84
|
-
let inStock = 0;
|
|
85
|
-
let outOfStock = 0;
|
|
86
|
-
let belowMin = 0;
|
|
87
|
-
for (const i of items) {
|
|
88
|
-
const avail = availableOf(i);
|
|
89
|
-
if (avail > 0)
|
|
90
|
-
inStock++;
|
|
91
|
-
else
|
|
92
|
-
outOfStock++;
|
|
93
|
-
if ((i.minQuantity ?? 0) > 0 && avail < i.minQuantity)
|
|
94
|
-
belowMin++;
|
|
95
|
-
}
|
|
96
|
-
return { total: items.length, inStock, outOfStock, belowMin };
|
|
97
|
-
}, [items]);
|
|
107
|
+
const counts = useMemo(() => countStock(items), [items]);
|
|
98
108
|
const filtered = useMemo(() => {
|
|
99
109
|
const q = search.trim().toLowerCase();
|
|
100
110
|
return items.filter((i) => {
|
|
101
111
|
if (noPriceOnly && !(i.productId && noPriceIds.has(i.productId)))
|
|
102
112
|
return false;
|
|
103
113
|
const avail = availableOf(i);
|
|
104
|
-
|
|
114
|
+
const physicalQty = Number(i.quantity ?? 0);
|
|
115
|
+
if (stockFilter === "in_stock" && physicalQty <= 0)
|
|
105
116
|
return false;
|
|
106
|
-
if (stockFilter === "out_of_stock" &&
|
|
117
|
+
if (stockFilter === "out_of_stock" && physicalQty > 0)
|
|
107
118
|
return false;
|
|
108
119
|
if (stockFilter === "below_min" &&
|
|
109
120
|
!((i.minQuantity ?? 0) > 0 && avail < i.minQuantity))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publish-114.test.d.ts","sourceRoot":"","sources":["../../../src/lib/__tests__/publish-114.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { translatePublishError } from "../publish-errors.js";
|
|
3
|
+
import { effectiveJobStatus } from "../../components/voorraad/PublishProgressCard.js";
|
|
4
|
+
import { countStock } from "../../components/voorraad/VoorraadPage.js";
|
|
5
|
+
describe("1.0.114 publiceren en telling", () => {
|
|
6
|
+
it("toont de echte reden als de poort schrijven tegenhoudt", () => {
|
|
7
|
+
const f = translatePublishError(new Error("publishTradeItem: gateway /api/public/gateway/write → Tegengehouden door poort: in de Floriday-kopie wijkt de verpakking af van Floriday."));
|
|
8
|
+
expect(f.code).toBe("GATE_BLOCKED");
|
|
9
|
+
expect(f.message).toContain("wijkt de verpakking af");
|
|
10
|
+
});
|
|
11
|
+
it("onbekende fout toont de technische reden", () => {
|
|
12
|
+
const f = translatePublishError(new Error("stap x → iets raars"));
|
|
13
|
+
expect(f.message).toContain("iets raars");
|
|
14
|
+
});
|
|
15
|
+
it("job blijft niet op bezig na mislukte productstap", () => {
|
|
16
|
+
expect(effectiveJobStatus("running", [
|
|
17
|
+
{ status: "failed", step_key: "trade-item" },
|
|
18
|
+
{ status: "pending", step_key: "stock" },
|
|
19
|
+
])).toBe("failed");
|
|
20
|
+
expect(effectiveJobStatus("running", [{ status: "running", step_key: "trade-item" }])).toBe("running");
|
|
21
|
+
});
|
|
22
|
+
it("volledig gereserveerd telt als op voorraad", () => {
|
|
23
|
+
const c = countStock([
|
|
24
|
+
{ quantity: 100, availableQuantity: 0, minQuantity: 0 },
|
|
25
|
+
{ quantity: 0, availableQuantity: 0, minQuantity: 0 },
|
|
26
|
+
]);
|
|
27
|
+
expect(c.inStock).toBe(1);
|
|
28
|
+
expect(c.outOfStock).toBe(1);
|
|
29
|
+
expect(c.fullyReserved).toBe(1);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Woorden als API, gateway, Floriday-intern jargon en modulenamen komen hier
|
|
7
7
|
* bewust niet in voor.
|
|
8
8
|
*/
|
|
9
|
-
export type PublishErrorCode = "MISSING_CHARACTERISTICS" | "NO_CONNECTION" | "WRONG_ENVIRONMENT" | "NOT_LOGGED_IN" | "INVALID_VALUE" | "DUPLICATE" | "NOT_FOUND" | "TEMPORARY" | "TIMEOUT" | "RATE_LIMIT" | "UNKNOWN";
|
|
9
|
+
export type PublishErrorCode = "MISSING_CHARACTERISTICS" | "NO_CONNECTION" | "WRONG_ENVIRONMENT" | "NOT_LOGGED_IN" | "INVALID_VALUE" | "DUPLICATE" | "NOT_FOUND" | "TEMPORARY" | "TIMEOUT" | "RATE_LIMIT" | "GATE_BLOCKED" | "UNKNOWN";
|
|
10
10
|
export interface PublishErrorAction {
|
|
11
11
|
label: string;
|
|
12
12
|
/** Interne route waar de gebruiker het kan oplossen. */
|
|
@@ -34,4 +34,9 @@ export declare function translatePublishError(error: unknown, opts?: {
|
|
|
34
34
|
characteristicLabels?: Record<string, string>;
|
|
35
35
|
}): FriendlyPublishError;
|
|
36
36
|
export declare function friendlyFieldName(field?: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Haalt de leesbare reden uit een technische foutketen
|
|
39
|
+
* ("stap: route → reden") zodat de gebruiker altijd de echte oorzaak ziet.
|
|
40
|
+
*/
|
|
41
|
+
export declare function realReason(raw: string): string;
|
|
37
42
|
//# sourceMappingURL=publish-errors.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"publish-errors.d.ts","sourceRoot":"","sources":["../../src/lib/publish-errors.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,MAAM,gBAAgB,GACxB,yBAAyB,GACzB,eAAe,GACf,mBAAmB,GACnB,eAAe,GACf,eAAe,GACf,WAAW,GACX,WAAW,GACX,WAAW,GACX,SAAS,GACT,YAAY,GACZ,SAAS,CAAC;AAEd,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,wDAAwD;IACxD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,2EAA2E;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,gBAAgB,CAAC;IACvB,8BAA8B;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,yCAAyC;IACzC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,iDAAiD;IACjD,SAAS,EAAE,OAAO,CAAC;IACnB,2DAA2D;IAC3D,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,uDAAuD;IACvD,MAAM,EAAE,MAAM,CAAC;CAChB;AAiBD,8EAA8E;AAC9E,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EAAE,EACf,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC9B,MAAM,CAIR;AAED,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,OAAO,EACd,IAAI,CAAC,EAAE;IAAE,oBAAoB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GACvD,oBAAoB,
|
|
1
|
+
{"version":3,"file":"publish-errors.d.ts","sourceRoot":"","sources":["../../src/lib/publish-errors.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,MAAM,gBAAgB,GACxB,yBAAyB,GACzB,eAAe,GACf,mBAAmB,GACnB,eAAe,GACf,eAAe,GACf,WAAW,GACX,WAAW,GACX,WAAW,GACX,SAAS,GACT,YAAY,GACZ,cAAc,GACd,SAAS,CAAC;AAEd,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,wDAAwD;IACxD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,2EAA2E;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,gBAAgB,CAAC;IACvB,8BAA8B;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,yCAAyC;IACzC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,iDAAiD;IACjD,SAAS,EAAE,OAAO,CAAC;IACnB,2DAA2D;IAC3D,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,uDAAuD;IACvD,MAAM,EAAE,MAAM,CAAC;CAChB;AAiBD,8EAA8E;AAC9E,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EAAE,EACf,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC9B,MAAM,CAIR;AAED,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,OAAO,EACd,IAAI,CAAC,EAAE;IAAE,oBAAoB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GACvD,oBAAoB,CAoJtB;AAgBD,wBAAgB,iBAAiB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAGxD;AAaD;;;GAGG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAK9C"}
|
|
@@ -140,10 +140,20 @@ export function translatePublishError(error, opts) {
|
|
|
140
140
|
detail: raw,
|
|
141
141
|
};
|
|
142
142
|
}
|
|
143
|
+
// Floriday-auth houdt schrijven tegen (floriday_write_mode read_only): toon de echte reden.
|
|
144
|
+
if (lower.includes("tegengehouden door poort") || lower.includes("blocked_by_gate")) {
|
|
145
|
+
return {
|
|
146
|
+
code: "GATE_BLOCKED",
|
|
147
|
+
message: `Floriday-koppeling houdt versturen tegen: ${realReason(raw)}`,
|
|
148
|
+
hint: "Los het verschil op bij Floriday koppeling → Verpakking en probeer opnieuw. Je ingevoerde gegevens zijn bewaard.",
|
|
149
|
+
retryable: false,
|
|
150
|
+
detail: raw,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
143
153
|
return {
|
|
144
154
|
code: "UNKNOWN",
|
|
145
|
-
message: "Het publiceren is niet gelukt.",
|
|
146
|
-
hint: "Probeer het opnieuw.
|
|
155
|
+
message: raw ? `Het publiceren is niet gelukt: ${realReason(raw)}` : "Het publiceren is niet gelukt.",
|
|
156
|
+
hint: "Probeer het opnieuw. Je ingevoerde gegevens zijn bewaard.",
|
|
147
157
|
retryable: false,
|
|
148
158
|
detail: raw,
|
|
149
159
|
};
|
|
@@ -181,3 +191,13 @@ function stepForFieldName(field) {
|
|
|
181
191
|
return "vbn";
|
|
182
192
|
return "artikel";
|
|
183
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* Haalt de leesbare reden uit een technische foutketen
|
|
196
|
+
* ("stap: route → reden") zodat de gebruiker altijd de echte oorzaak ziet.
|
|
197
|
+
*/
|
|
198
|
+
export function realReason(raw) {
|
|
199
|
+
const parts = raw.split("→");
|
|
200
|
+
const last = (parts[parts.length - 1] ?? raw).trim();
|
|
201
|
+
const text = last.replace(/^[a-zA-Z]+:\s*/, "").trim() || raw.trim();
|
|
202
|
+
return text.length > 300 ? `${text.slice(0, 297)}…` : text;
|
|
203
|
+
}
|