@flowselections/floriday-voorraad 1.0.13 → 1.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/dist-lib/assets/floriday-logo.png.asset.json +11 -0
  2. package/dist-lib/components/voorraad/ArtikelWizard.d.ts +5 -0
  3. package/dist-lib/components/voorraad/ArtikelWizard.d.ts.map +1 -0
  4. package/dist-lib/components/voorraad/ArtikelWizard.js +1229 -0
  5. package/dist-lib/components/voorraad/EditArtikelPage.d.ts +4 -0
  6. package/dist-lib/components/voorraad/EditArtikelPage.d.ts.map +1 -0
  7. package/dist-lib/components/voorraad/EditArtikelPage.js +51 -0
  8. package/dist-lib/components/voorraad/NieuwArtikelPage.d.ts +2 -0
  9. package/dist-lib/components/voorraad/NieuwArtikelPage.d.ts.map +1 -0
  10. package/dist-lib/components/voorraad/NieuwArtikelPage.js +5 -0
  11. package/dist-lib/components/voorraad/StockBulkButtons.d.ts +7 -0
  12. package/dist-lib/components/voorraad/StockBulkButtons.d.ts.map +1 -0
  13. package/dist-lib/components/voorraad/StockBulkButtons.js +356 -0
  14. package/dist-lib/components/voorraad/ValidationOverviewTable.d.ts +7 -0
  15. package/dist-lib/components/voorraad/ValidationOverviewTable.d.ts.map +1 -0
  16. package/dist-lib/components/voorraad/ValidationOverviewTable.js +87 -0
  17. package/dist-lib/components/voorraad/ValidationPage.d.ts +2 -0
  18. package/dist-lib/components/voorraad/ValidationPage.d.ts.map +1 -0
  19. package/dist-lib/components/voorraad/ValidationPage.js +12 -0
  20. package/dist-lib/components/voorraad/VoorraadDashboard.d.ts +6 -0
  21. package/dist-lib/components/voorraad/VoorraadDashboard.d.ts.map +1 -0
  22. package/dist-lib/components/voorraad/VoorraadDashboard.js +42 -0
  23. package/dist-lib/components/voorraad/VoorraadPage.d.ts.map +1 -1
  24. package/dist-lib/components/voorraad/VoorraadPage.js +70 -11
  25. package/dist-lib/components/voorraad/VoorraadPagination.d.ts +10 -0
  26. package/dist-lib/components/voorraad/VoorraadPagination.d.ts.map +1 -0
  27. package/dist-lib/components/voorraad/VoorraadPagination.js +12 -0
  28. package/dist-lib/components/voorraad/VoorraadStockCard.d.ts +8 -0
  29. package/dist-lib/components/voorraad/VoorraadStockCard.d.ts.map +1 -0
  30. package/dist-lib/components/voorraad/VoorraadStockCard.js +33 -0
  31. package/dist-lib/components/voorraad/VoorraadTable.d.ts.map +1 -1
  32. package/dist-lib/components/voorraad/VoorraadTable.js +16 -5
  33. package/dist-lib/hooks/useFloriday.d.ts +55 -0
  34. package/dist-lib/hooks/useFloriday.d.ts.map +1 -0
  35. package/dist-lib/hooks/useFloriday.js +298 -0
  36. package/dist-lib/hooks/useLocalTradeItems.d.ts +17 -1
  37. package/dist-lib/hooks/useLocalTradeItems.d.ts.map +1 -1
  38. package/dist-lib/hooks/useLocalTradeItems.js +54 -10
  39. package/dist-lib/hooks/useTradeItemSpec.d.ts +42 -0
  40. package/dist-lib/hooks/useTradeItemSpec.d.ts.map +1 -0
  41. package/dist-lib/hooks/useTradeItemSpec.js +448 -0
  42. package/dist-lib/hooks/useTradeItemStock.d.ts +19 -0
  43. package/dist-lib/hooks/useTradeItemStock.d.ts.map +1 -0
  44. package/dist-lib/hooks/useTradeItemStock.js +78 -0
  45. package/dist-lib/hooks/useVbnRegulatoryFeatures.d.ts +3 -0
  46. package/dist-lib/hooks/useVbnRegulatoryFeatures.d.ts.map +1 -0
  47. package/dist-lib/hooks/useVbnRegulatoryFeatures.js +19 -0
  48. package/dist-lib/hooks/useVoorraadData.d.ts +2 -1
  49. package/dist-lib/hooks/useVoorraadData.d.ts.map +1 -1
  50. package/dist-lib/hooks/useVoorraadData.js +39 -11
  51. package/dist-lib/index.d.ts +4 -0
  52. package/dist-lib/index.d.ts.map +1 -1
  53. package/dist-lib/index.js +6 -5
  54. package/dist-lib/integrations/supabase/auth-middleware.d.ts +8346 -3367
  55. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  56. package/dist-lib/integrations/supabase/client.d.ts +8346 -3367
  57. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  58. package/dist-lib/integrations/supabase/client.server.d.ts +8346 -3367
  59. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  60. package/dist-lib/integrations/supabase/types.d.ts +8460 -3341
  61. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  62. package/dist-lib/integrations/supabase/types.js +10 -1
  63. package/dist-lib/lib/__tests__/floricode-required-features.test.d.ts +2 -0
  64. package/dist-lib/lib/__tests__/floricode-required-features.test.d.ts.map +1 -0
  65. package/dist-lib/lib/__tests__/floricode-required-features.test.js +186 -0
  66. package/dist-lib/lib/floricode-client.d.ts +15 -1
  67. package/dist-lib/lib/floricode-client.d.ts.map +1 -1
  68. package/dist-lib/lib/floricode-client.js +42 -4
  69. package/dist-lib/lib/floricode-required-features.functions.d.ts +10026 -0
  70. package/dist-lib/lib/floricode-required-features.functions.d.ts.map +1 -0
  71. package/dist-lib/lib/floricode-required-features.functions.js +170 -0
  72. package/dist-lib/lib/floriday-client.d.ts +36 -8
  73. package/dist-lib/lib/floriday-client.d.ts.map +1 -1
  74. package/dist-lib/lib/floriday-client.js +206 -25
  75. package/dist-lib/lib/floriday-env.d.ts +29 -0
  76. package/dist-lib/lib/floriday-env.d.ts.map +1 -0
  77. package/dist-lib/lib/floriday-env.js +42 -0
  78. package/dist-lib/lib/floriday-env.server.d.ts +15 -0
  79. package/dist-lib/lib/floriday-env.server.d.ts.map +1 -0
  80. package/dist-lib/lib/floriday-env.server.js +13 -0
  81. package/dist-lib/lib/floriday-field-sources.d.ts +18 -0
  82. package/dist-lib/lib/floriday-field-sources.d.ts.map +1 -0
  83. package/dist-lib/lib/floriday-field-sources.js +45 -0
  84. package/dist-lib/lib/floriday-gateway-entities.d.ts +44 -0
  85. package/dist-lib/lib/floriday-gateway-entities.d.ts.map +1 -0
  86. package/dist-lib/lib/floriday-gateway-entities.js +51 -0
  87. package/dist-lib/lib/floriday-gateway.functions.d.ts +39999 -0
  88. package/dist-lib/lib/floriday-gateway.functions.d.ts.map +1 -0
  89. package/dist-lib/lib/floriday-gateway.functions.js +68 -0
  90. package/dist-lib/lib/floriday-gateway.server.d.ts +122 -0
  91. package/dist-lib/lib/floriday-gateway.server.d.ts.map +1 -0
  92. package/dist-lib/lib/floriday-gateway.server.js +230 -0
  93. package/dist-lib/lib/floriday-import-validator.d.ts +26 -0
  94. package/dist-lib/lib/floriday-import-validator.d.ts.map +1 -0
  95. package/dist-lib/lib/floriday-import-validator.js +41 -0
  96. package/dist-lib/lib/floriday-payload.d.ts +47 -3
  97. package/dist-lib/lib/floriday-payload.d.ts.map +1 -1
  98. package/dist-lib/lib/floriday-payload.js +179 -7
  99. package/dist-lib/lib/floriday-schema-to-zod.d.ts +9 -0
  100. package/dist-lib/lib/floriday-schema-to-zod.d.ts.map +1 -0
  101. package/dist-lib/lib/floriday-schema-to-zod.js +53 -0
  102. package/dist-lib/lib/floriday-writes.functions.d.ts +19998 -0
  103. package/dist-lib/lib/floriday-writes.functions.d.ts.map +1 -0
  104. package/dist-lib/lib/floriday-writes.functions.js +74 -0
  105. package/dist-lib/lib/image-resize.d.ts +43 -0
  106. package/dist-lib/lib/image-resize.d.ts.map +1 -0
  107. package/dist-lib/lib/image-resize.js +212 -0
  108. package/dist-lib/lib/normalize-errors-middleware.d.ts +2 -0
  109. package/dist-lib/lib/normalize-errors-middleware.d.ts.map +1 -0
  110. package/dist-lib/lib/normalize-errors-middleware.js +24 -0
  111. package/dist-lib/lib/wizard-step-fields.d.ts +15 -0
  112. package/dist-lib/lib/wizard-step-fields.d.ts.map +1 -0
  113. package/dist-lib/lib/wizard-step-fields.js +51 -0
  114. package/package.json +29 -14
@@ -0,0 +1,4 @@
1
+ export declare function EditArtikelPage({ id }: {
2
+ id: string;
3
+ }): import("react/jsx-runtime").JSX.Element;
4
+ //# sourceMappingURL=EditArtikelPage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EditArtikelPage.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/EditArtikelPage.tsx"],"names":[],"mappings":"AAWA,wBAAgB,eAAe,CAAC,EAAE,EAAE,EAAE,EAAE;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,2CA0DrD"}
@@ -0,0 +1,51 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { Card } from "@flowselections/core";
4
+ import { Loader2 } from "lucide-react";
5
+ import { ArtikelWizard } from "./ArtikelWizard";
6
+ import { supabase } from "../../integrations/supabase/client";
7
+ export function EditArtikelPage({ id }) {
8
+ const [resolved, setResolved] = useState(null);
9
+ useEffect(() => {
10
+ let cancelled = false;
11
+ setResolved(null);
12
+ (async () => {
13
+ const draftRes = await supabase
14
+ .from("trade_item_drafts")
15
+ .select("id")
16
+ .eq("id", id)
17
+ .maybeSingle();
18
+ if (cancelled)
19
+ return;
20
+ if (draftRes.data) {
21
+ setResolved({ kind: "draft", id });
22
+ return;
23
+ }
24
+ const pubRes = await supabase
25
+ .from("floriday_trade_items_cache")
26
+ .select("floriday_id")
27
+ .eq("floriday_id", id)
28
+ .maybeSingle();
29
+ if (cancelled)
30
+ return;
31
+ if (pubRes.data) {
32
+ setResolved({ kind: "published", id });
33
+ return;
34
+ }
35
+ setResolved({ kind: "notfound" });
36
+ })();
37
+ return () => {
38
+ cancelled = true;
39
+ };
40
+ }, [id]);
41
+ if (!resolved) {
42
+ return (_jsx("main", { className: "p-6", children: _jsxs(Card, { className: "p-6 flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), "Artikel zoeken..."] }) }));
43
+ }
44
+ if (resolved.kind === "notfound") {
45
+ return (_jsx("main", { className: "p-6", children: _jsx(Card, { className: "p-6 text-sm", children: "Artikel niet gevonden. Het is mogelijk verwijderd of nog niet gesynchroniseerd vanuit Floriday." }) }));
46
+ }
47
+ if (resolved.kind === "draft") {
48
+ return _jsx(ArtikelWizard, { draftId: resolved.id });
49
+ }
50
+ return _jsx(ArtikelWizard, { tradeItemId: resolved.id });
51
+ }
@@ -0,0 +1,2 @@
1
+ export declare function NieuwArtikelPage(): import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=NieuwArtikelPage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NieuwArtikelPage.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/NieuwArtikelPage.tsx"],"names":[],"mappings":"AAEA,wBAAgB,gBAAgB,4CAE/B"}
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { ArtikelWizard } from "./ArtikelWizard";
3
+ export function NieuwArtikelPage() {
4
+ return _jsx(ArtikelWizard, {});
5
+ }
@@ -0,0 +1,7 @@
1
+ import type { TradeItem } from "../../lib/floriday-client";
2
+ export declare function StockBulkButtons({ items, activeConnectionId, onImported, }: {
3
+ items: TradeItem[];
4
+ activeConnectionId: string | null;
5
+ onImported: () => void | Promise<void>;
6
+ }): import("react/jsx-runtime").JSX.Element;
7
+ //# sourceMappingURL=StockBulkButtons.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StockBulkButtons.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/StockBulkButtons.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAsD3D,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EACL,kBAAkB,EAClB,UAAU,GACX,EAAE;IACD,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,UAAU,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,2CA8dA"}
@@ -0,0 +1,356 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useMemo, useRef, useState } from "react";
3
+ import * as XLSX from "xlsx";
4
+ import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Checkbox, Label, toast, } from "@flowselections/core";
5
+ import { Download, FileDown, Upload, Loader2 } from "lucide-react";
6
+ import { supabase } from "../../integrations/supabase/client";
7
+ import { emptyWizardData } from "../../lib/floriday-payload";
8
+ import { upsertLocalTradeItem, updateDraftMeta } from "../../hooks/useLocalTradeItems";
9
+ import { validateDraftForPublish } from "../../lib/floriday-import-validator";
10
+ import { floriday } from "../../lib/floriday-client";
11
+ const COLUMNS = [
12
+ "Artikelcode",
13
+ "Artikelnaam",
14
+ "VBN-code",
15
+ "Botanische naam",
16
+ "Land van oorsprong",
17
+ "GTIN",
18
+ "Liggende voorraad",
19
+ "Minimum voorraad",
20
+ "Gereserveerde voorraad",
21
+ "Inkomende voorraad",
22
+ ];
23
+ function toKey(item) {
24
+ return item.localId ? `draft:${item.localId}` : item.id;
25
+ }
26
+ function parseNumber(val) {
27
+ if (val === undefined || val === null || val === "")
28
+ return { value: null };
29
+ const n = typeof val === "number" ? val : Number(String(val).replace(",", "."));
30
+ if (!Number.isFinite(n))
31
+ return { value: null, error: "geen numerieke waarde" };
32
+ return { value: n };
33
+ }
34
+ function productGroupForVbn(vbn) {
35
+ const s = vbn.trim();
36
+ if (s.startsWith("10"))
37
+ return "CUT_FLOWER";
38
+ if (s.startsWith("20"))
39
+ return "POT_PLANT";
40
+ if (s.startsWith("30"))
41
+ return "GARDEN_PLANT";
42
+ return null;
43
+ }
44
+ export function StockBulkButtons({ items, activeConnectionId, onImported, }) {
45
+ const fileRef = useRef(null);
46
+ const [open, setOpen] = useState(false);
47
+ const [importing, setImporting] = useState(false);
48
+ const [progress, setProgress] = useState(0);
49
+ const allowNegative = true;
50
+ const [autoPublish, setAutoPublish] = useState(false);
51
+ const [result, setResult] = useState(null);
52
+ const byCode = useMemo(() => {
53
+ const map = new Map();
54
+ for (const it of items) {
55
+ const code = (it.articleNumber ?? "").trim();
56
+ if (code)
57
+ map.set(code, it);
58
+ }
59
+ return map;
60
+ }, [items]);
61
+ /** Published Floriday items only (voor auto-koppeling van nieuwe artikelen). */
62
+ const publishedByCode = useMemo(() => {
63
+ const map = new Map();
64
+ for (const it of items) {
65
+ if (!it.published)
66
+ continue;
67
+ const code = (it.articleNumber ?? "").trim();
68
+ if (!code)
69
+ continue;
70
+ const arr = map.get(code) ?? [];
71
+ arr.push(it);
72
+ map.set(code, arr);
73
+ }
74
+ return map;
75
+ }, [items]);
76
+ const downloadTemplate = () => {
77
+ const rows = items.map((i) => {
78
+ const d = i.raw?.data;
79
+ return {
80
+ Artikelcode: i.articleNumber ?? "",
81
+ Artikelnaam: i.name,
82
+ "VBN-code": d?.vbnCode ?? i.raw?.vbnProductCode ?? "",
83
+ "Botanische naam": d?.botanicalName ?? "",
84
+ "Land van oorsprong": d?.countryOfOrigin ?? "",
85
+ GTIN: d?.gtin ?? "",
86
+ "Liggende voorraad": i.quantity,
87
+ "Minimum voorraad": i.minQuantity,
88
+ "Gereserveerde voorraad": i.reservedQuantity,
89
+ "Inkomende voorraad": i.incomingQuantity,
90
+ };
91
+ });
92
+ const ws = XLSX.utils.json_to_sheet(rows, { header: [...COLUMNS] });
93
+ ws["!cols"] = COLUMNS.map((c) => ({
94
+ wch: c === "Artikelnaam" ? 40 : c === "Artikelcode" ? 18 : 20,
95
+ }));
96
+ const wb = XLSX.utils.book_new();
97
+ XLSX.utils.book_append_sheet(wb, ws, "Voorraad");
98
+ const stamp = new Date().toISOString().slice(0, 10);
99
+ XLSX.writeFile(wb, `voorraad-template-${stamp}.xlsx`);
100
+ };
101
+ const downloadErrors = () => {
102
+ if (!result?.errors.length)
103
+ return;
104
+ const rows = result.errors.map((e) => ({
105
+ Regel: e.row,
106
+ Artikelcode: e.articleCode,
107
+ Artikelnaam: e.articleName,
108
+ Fout: e.reden,
109
+ }));
110
+ const ws = XLSX.utils.json_to_sheet(rows);
111
+ const wb = XLSX.utils.book_new();
112
+ XLSX.utils.book_append_sheet(wb, ws, "Fouten");
113
+ XLSX.writeFile(wb, `voorraad-import-fouten-${new Date().toISOString().slice(0, 10)}.xlsx`);
114
+ };
115
+ const handleFile = async (file) => {
116
+ if (!activeConnectionId) {
117
+ toast.error("Geen actieve Floriday-connectie");
118
+ return;
119
+ }
120
+ setImporting(true);
121
+ setProgress(0);
122
+ setResult(null);
123
+ const errors = [];
124
+ let updated = 0;
125
+ let created = 0;
126
+ let linked = 0;
127
+ let published = 0;
128
+ let waiting = 0;
129
+ try {
130
+ const buf = await file.arrayBuffer();
131
+ const wb = XLSX.read(buf, { type: "array" });
132
+ const sheet = wb.Sheets[wb.SheetNames[0]];
133
+ const rows = XLSX.utils.sheet_to_json(sheet, {
134
+ defval: "",
135
+ });
136
+ const userRes = await supabase.auth.getUser();
137
+ const userId = userRes.data.user?.id;
138
+ if (!userId)
139
+ throw new Error("Niet ingelogd");
140
+ // "Nieuw sinds laatste import" — timestamp opslaan
141
+ try {
142
+ localStorage.setItem(`voorraad_last_import_${activeConnectionId}`, new Date().toISOString());
143
+ }
144
+ catch {
145
+ /* ignore quota */
146
+ }
147
+ const stockBatch = [];
148
+ for (let idx = 0; idx < rows.length; idx++) {
149
+ const row = rows[idx];
150
+ const excelRow = idx + 2;
151
+ const code = String(row["Artikelcode"] ?? "").trim();
152
+ const name = String(row["Artikelnaam"] ?? "").trim();
153
+ if (!code) {
154
+ errors.push({
155
+ row: excelRow,
156
+ articleCode: "",
157
+ articleName: name,
158
+ reden: "Artikelcode ontbreekt",
159
+ });
160
+ continue;
161
+ }
162
+ // Voorraadvelden parsen
163
+ const stockFields = [
164
+ ["physical_stock", "Liggende voorraad"],
165
+ ["minimum_stock", "Minimum voorraad"],
166
+ ["reserved_stock", "Gereserveerde voorraad"],
167
+ ["incoming_stock", "Inkomende voorraad"],
168
+ ];
169
+ const stockValues = {};
170
+ let stockError = null;
171
+ for (const [key, col] of stockFields) {
172
+ const parsed = parseNumber(row[col]);
173
+ if (parsed.error) {
174
+ stockError = `${col}: ${parsed.error}`;
175
+ break;
176
+ }
177
+ if (parsed.value !== null && !allowNegative && parsed.value < 0) {
178
+ stockError = `${col}: negatieve waarde niet toegestaan`;
179
+ break;
180
+ }
181
+ stockValues[key] = parsed.value;
182
+ }
183
+ if (stockError) {
184
+ errors.push({
185
+ row: excelRow,
186
+ articleCode: code,
187
+ articleName: name,
188
+ reden: stockError,
189
+ });
190
+ continue;
191
+ }
192
+ const existing = byCode.get(code);
193
+ // === Bestaand artikel: alleen stock bijwerken ===
194
+ if (existing) {
195
+ const applied = {
196
+ physical_stock: stockValues.physical_stock ?? existing.quantity,
197
+ minimum_stock: stockValues.minimum_stock ?? existing.minQuantity,
198
+ reserved_stock: stockValues.reserved_stock ?? existing.reservedQuantity,
199
+ incoming_stock: stockValues.incoming_stock ?? existing.incomingQuantity,
200
+ };
201
+ const connId = existing.raw?.connection_id ?? activeConnectionId;
202
+ stockBatch.push({
203
+ user_id: userId,
204
+ connection_id: connId,
205
+ trade_item_key: toKey(existing),
206
+ ...applied,
207
+ });
208
+ updated++;
209
+ setProgress(Math.round(((idx + 1) / rows.length) * 100));
210
+ continue;
211
+ }
212
+ // === Nieuw artikel aanmaken ===
213
+ const vbnCode = String(row["VBN-code"] ?? "").trim();
214
+ const botanical = String(row["Botanische naam"] ?? "").trim();
215
+ const country = String(row["Land van oorsprong"] ?? "")
216
+ .trim()
217
+ .toUpperCase();
218
+ const gtin = String(row["GTIN"] ?? "").trim();
219
+ const data = {
220
+ ...emptyWizardData,
221
+ articleCode: code,
222
+ name: name || code,
223
+ vbnCode,
224
+ vbnName: "",
225
+ productGroup: productGroupForVbn(vbnCode),
226
+ botanicalName: botanical,
227
+ countryOfOrigin: country,
228
+ gtin,
229
+ };
230
+ // Zoek in cache van gepubliceerde Floriday-items
231
+ const matches = publishedByCode.get(code) ?? [];
232
+ let linkStatus = "unlinked";
233
+ let floridayId = null;
234
+ let publishError = null;
235
+ if (matches.length === 1) {
236
+ linkStatus = "linked";
237
+ floridayId = matches[0].id;
238
+ linked++;
239
+ }
240
+ else if (matches.length > 1) {
241
+ linkStatus = "ambiguous_match";
242
+ }
243
+ else {
244
+ // Geen match — evalueer validatie
245
+ const validation = validateDraftForPublish(data);
246
+ if (validation.ok && autoPublish) {
247
+ // Poging tot publiceren
248
+ try {
249
+ const { buildAddTradeItemPayload } = await import("../../lib/floriday-payload");
250
+ const payload = buildAddTradeItemPayload(data);
251
+ const res = await floriday.createTradeItem(activeConnectionId, payload);
252
+ floridayId = res?.tradeItemId ?? null;
253
+ linkStatus = "published";
254
+ published++;
255
+ }
256
+ catch (e) {
257
+ publishError = e?.message ?? String(e);
258
+ linkStatus = "publish_failed";
259
+ }
260
+ }
261
+ else if (validation.ok) {
262
+ linkStatus = "ready_to_publish";
263
+ }
264
+ else {
265
+ linkStatus = "waiting_for_data";
266
+ waiting++;
267
+ }
268
+ }
269
+ // Maak draft aan
270
+ try {
271
+ const validation = validateDraftForPublish(data);
272
+ const draftId = await upsertLocalTradeItem({
273
+ connectionId: activeConnectionId,
274
+ data,
275
+ meta: {
276
+ floridayLinkStatus: linkStatus,
277
+ floridayId,
278
+ floridayLinkError: publishError,
279
+ missingRequiredFields: validation.missing,
280
+ lastFloridaySyncAt: new Date().toISOString(),
281
+ source: "excel_import",
282
+ },
283
+ });
284
+ created++;
285
+ // Stock koppelen aan de nieuwe draft
286
+ const anyStock = stockValues.physical_stock !== null ||
287
+ stockValues.minimum_stock !== null ||
288
+ stockValues.reserved_stock !== null ||
289
+ stockValues.incoming_stock !== null;
290
+ if (anyStock) {
291
+ stockBatch.push({
292
+ user_id: userId,
293
+ connection_id: activeConnectionId,
294
+ trade_item_key: `draft:${draftId}`,
295
+ physical_stock: stockValues.physical_stock ?? 0,
296
+ minimum_stock: stockValues.minimum_stock ?? 0,
297
+ reserved_stock: stockValues.reserved_stock ?? 0,
298
+ incoming_stock: stockValues.incoming_stock ?? 0,
299
+ });
300
+ }
301
+ }
302
+ catch (e) {
303
+ errors.push({
304
+ row: excelRow,
305
+ articleCode: code,
306
+ articleName: name,
307
+ reden: `Aanmaken mislukt: ${e?.message ?? e}`,
308
+ });
309
+ }
310
+ setProgress(Math.round(((idx + 1) / rows.length) * 100));
311
+ }
312
+ // Stock batch upserten
313
+ const CHUNK = 500;
314
+ for (let i = 0; i < stockBatch.length; i += CHUNK) {
315
+ const slice = stockBatch.slice(i, i + CHUNK);
316
+ const { error } = await supabase
317
+ .from("trade_item_stock")
318
+ .upsert(slice, { onConflict: "connection_id,trade_item_key" });
319
+ if (error) {
320
+ errors.push({
321
+ row: 0,
322
+ articleCode: "(voorraad-batch)",
323
+ articleName: "",
324
+ reden: `Database-fout: ${error.message}`,
325
+ });
326
+ }
327
+ }
328
+ setResult({ updated, created, linked, published, waiting, errors });
329
+ if (updated + created > 0) {
330
+ toast.success(`${updated} bijgewerkt · ${created} nieuw`);
331
+ await onImported();
332
+ }
333
+ if (errors.length && updated + created === 0) {
334
+ toast.error("Geen artikelen verwerkt");
335
+ }
336
+ }
337
+ catch (e) {
338
+ toast.error(`Import mislukt: ${e.message}`);
339
+ }
340
+ finally {
341
+ setImporting(false);
342
+ if (fileRef.current)
343
+ fileRef.current.value = "";
344
+ }
345
+ };
346
+ // Silence unused-import warning if updateDraftMeta remains for later use
347
+ void updateDraftMeta;
348
+ return (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "outline", onClick: downloadTemplate, disabled: items.length === 0, children: [_jsx(FileDown, { className: "mr-2 h-4 w-4" }), "Template downloaden"] }), _jsxs(Button, { variant: "outline", onClick: () => setOpen(true), children: [_jsx(Upload, { className: "mr-2 h-4 w-4" }), "Voorraad importeren"] }), _jsx(Dialog, { open: open, onOpenChange: (o) => !importing && setOpen(o), children: _jsxs(DialogContent, { className: "sm:max-w-lg", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Voorraad importeren" }), _jsxs(DialogDescription, { children: ["Upload een Excel-bestand (op basis van de template). Bestaande artikelen worden gematcht op ", _jsx("strong", { children: "Artikelcode" }), " en alleen de voorraadvelden worden bijgewerkt. Onbekende artikelcodes worden als nieuwe artikelen aangemaakt en waar mogelijk automatisch gekoppeld aan Floriday."] })] }), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: "auto-publish", checked: autoPublish, onCheckedChange: (v) => setAutoPublish(Boolean(v)), disabled: importing }), _jsx(Label, { htmlFor: "auto-publish", className: "text-sm font-normal", children: "Nieuwe producten automatisch naar Floriday publiceren (indien alle verplichte gegevens aanwezig zijn)" })] }), _jsx("input", { ref: fileRef, type: "file", accept: ".xlsx,.xls", className: "block w-full text-sm file:mr-3 file:rounded-md file:border-0 file:bg-primary file:px-3 file:py-2 file:text-primary-foreground hover:file:bg-primary/90 disabled:opacity-50", disabled: importing, onChange: (e) => {
349
+ const f = e.target.files?.[0];
350
+ if (f)
351
+ void handleFile(f);
352
+ } }), importing && (_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "h-2 w-full overflow-hidden rounded bg-muted", children: _jsx("div", { className: "h-full bg-primary transition-all", style: { width: `${progress}%` } }) }), _jsxs("p", { className: "text-xs text-muted-foreground flex items-center gap-2", children: [_jsx(Loader2, { className: "h-3 w-3 animate-spin" }), "Bezig met importeren\u2026 ", progress, "%"] })] })), result && (_jsxs("div", { className: "rounded-md border bg-muted/30 p-3 text-sm space-y-2", children: [_jsx("div", { className: "font-medium", children: "Importoverzicht" }), _jsxs("ul", { className: "text-xs space-y-1", children: [_jsxs("li", { children: ["Bestaande artikelen bijgewerkt:", " ", _jsx("span", { className: "tabular-nums font-medium text-foreground", children: result.updated })] }), _jsxs("li", { children: ["Nieuwe artikelen aangemaakt:", " ", _jsx("span", { className: "tabular-nums font-medium text-foreground", children: result.created })] }), _jsxs("li", { children: ["Automatisch gekoppeld met Floriday:", " ", _jsx("span", { className: "tabular-nums font-medium text-foreground", children: result.linked })] }), _jsxs("li", { children: ["Gepubliceerd naar Floriday:", " ", _jsx("span", { className: "tabular-nums font-medium text-foreground", children: result.published })] }), _jsxs("li", { children: ["Wacht op Floriday-publicatie (gegevens incompleet):", " ", _jsx("span", { className: "tabular-nums font-medium text-foreground", children: result.waiting })] }), _jsxs("li", { children: ["Fouten:", " ", _jsx("span", { className: "tabular-nums font-medium text-foreground", children: result.errors.length })] })] }), result.errors.length > 0 && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "max-h-40 overflow-y-auto rounded border bg-background p-2 text-xs space-y-1", children: [result.errors.slice(0, 20).map((e, i) => (_jsxs("div", { className: "text-destructive", children: ["Regel ", e.row, " \u2014 ", e.articleCode || "(geen code)", " \u2014", " ", e.reden] }, i))), result.errors.length > 20 && (_jsxs("div", { className: "text-muted-foreground", children: ["\u2026 en ", result.errors.length - 20, " meer. Download het foutbestand voor de volledige lijst."] }))] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: downloadErrors, children: [_jsx(Download, { className: "mr-2 h-4 w-4" }), "Download foutbestand"] })] }))] }))] }), _jsx(DialogFooter, { children: _jsx(Button, { variant: "ghost", onClick: () => {
353
+ setOpen(false);
354
+ setResult(null);
355
+ }, disabled: importing, children: "Sluiten" }) })] }) })] }));
356
+ }
@@ -0,0 +1,7 @@
1
+ import type { TradeItem } from "../../lib/floriday-client";
2
+ export declare function ValidationOverviewTable({ items, connectionId, onChanged, }: {
3
+ items: TradeItem[];
4
+ connectionId: string | null;
5
+ onChanged: () => void | Promise<void>;
6
+ }): import("react/jsx-runtime").JSX.Element;
7
+ //# sourceMappingURL=ValidationOverviewTable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ValidationOverviewTable.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/ValidationOverviewTable.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAsB,MAAM,2BAA2B,CAAC;AAwB/E,wBAAgB,uBAAuB,CAAC,EACtC,KAAK,EACL,YAAY,EACZ,SAAS,GACV,EAAE;IACD,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC,2CA0IA"}
@@ -0,0 +1,87 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo } from "react";
3
+ import { useNavigate } from "@tanstack/react-router";
4
+ import { Badge, Button } from "@flowselections/core";
5
+ import { Pencil, RefreshCw, Send } from "lucide-react";
6
+ import { validateDraftForPublish } from "../../lib/floriday-import-validator";
7
+ import { updateDraftMeta } from "../../hooks/useLocalTradeItems";
8
+ import { floriday } from "../../lib/floriday-client";
9
+ import { toast } from "@flowselections/core";
10
+ function statusBadge(s) {
11
+ switch (s) {
12
+ case "linked":
13
+ case "published":
14
+ return _jsx(Badge, { className: "bg-emerald-500/15 text-emerald-700 border-emerald-500/30", children: "Gekoppeld" });
15
+ case "ambiguous_match":
16
+ return _jsx(Badge, { className: "bg-amber-500/15 text-amber-700 border-amber-500/30", children: "Meerdere matches" });
17
+ case "waiting_for_data":
18
+ return _jsx(Badge, { className: "bg-amber-500/15 text-amber-700 border-amber-500/30", children: "Wacht op gegevens" });
19
+ case "ready_to_publish":
20
+ return _jsx(Badge, { className: "bg-blue-500/15 text-blue-700 border-blue-500/30", children: "Klaar voor publicatie" });
21
+ case "publish_failed":
22
+ return _jsx(Badge, { variant: "destructive", children: "Publicatie mislukt" });
23
+ default:
24
+ return _jsx(Badge, { variant: "outline", children: "Niet gekoppeld" });
25
+ }
26
+ }
27
+ export function ValidationOverviewTable({ items, connectionId, onChanged, }) {
28
+ const navigate = useNavigate();
29
+ const rows = useMemo(() => items.filter((i) => !i.published &&
30
+ i.linkStatus !== "linked" &&
31
+ i.linkStatus !== "published"), [items]);
32
+ const revalidate = async (item) => {
33
+ if (!item.localId)
34
+ return;
35
+ const d = item.raw?.data ?? {};
36
+ const v = validateDraftForPublish(d);
37
+ try {
38
+ await updateDraftMeta(item.localId, {
39
+ missingRequiredFields: v.missing,
40
+ floridayLinkStatus: v.ok ? "ready_to_publish" : "waiting_for_data",
41
+ lastFloridaySyncAt: new Date().toISOString(),
42
+ });
43
+ toast.success("Validatie bijgewerkt");
44
+ await onChanged();
45
+ }
46
+ catch (e) {
47
+ toast.error(e?.message ?? "Bijwerken mislukt");
48
+ }
49
+ };
50
+ const republish = async (item) => {
51
+ if (!item.localId || !connectionId)
52
+ return;
53
+ const d = item.raw?.data;
54
+ const v = validateDraftForPublish(d);
55
+ if (!v.ok) {
56
+ toast.error("Nog niet compleet: " + v.missing.join(", "));
57
+ return;
58
+ }
59
+ try {
60
+ const { buildAddTradeItemPayload } = await import("../../lib/floriday-payload");
61
+ const payload = buildAddTradeItemPayload(d);
62
+ const res = await floriday.createTradeItem(connectionId, payload);
63
+ await updateDraftMeta(item.localId, {
64
+ floridayLinkStatus: "published",
65
+ floridayId: res?.tradeItemId ?? null,
66
+ floridayLinkError: null,
67
+ lastFloridaySyncAt: new Date().toISOString(),
68
+ });
69
+ toast.success("Gepubliceerd naar Floriday");
70
+ await onChanged();
71
+ }
72
+ catch (e) {
73
+ await updateDraftMeta(item.localId, {
74
+ floridayLinkStatus: "publish_failed",
75
+ floridayLinkError: e?.message ?? String(e),
76
+ lastFloridaySyncAt: new Date().toISOString(),
77
+ });
78
+ toast.error(e?.message ?? "Publicatie mislukt");
79
+ await onChanged();
80
+ }
81
+ };
82
+ if (rows.length === 0) {
83
+ return (_jsx("div", { className: "p-8 text-center text-sm text-muted-foreground", children: "Alle artikelen zijn gekoppeld aan Floriday." }));
84
+ }
85
+ return (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/50 text-left", children: _jsxs("tr", { children: [_jsx("th", { className: "p-3", children: "Artikelcode" }), _jsx("th", { className: "p-3", children: "Artikelnaam" }), _jsx("th", { className: "p-3", children: "Status" }), _jsx("th", { className: "p-3", children: "Ontbrekende gegevens" }), _jsx("th", { className: "p-3", children: "Laatste sync" }), _jsx("th", { className: "p-3", children: "Foutmelding" }), _jsx("th", { className: "p-3 text-right", children: "Acties" })] }) }), _jsx("tbody", { children: rows.map((it) => (_jsxs("tr", { className: "border-t align-top", children: [_jsx("td", { className: "p-3 font-mono text-xs", children: it.articleNumber || "—" }), _jsx("td", { className: "p-3", children: it.name }), _jsx("td", { className: "p-3", children: statusBadge(it.linkStatus) }), _jsx("td", { className: "p-3", children: (it.missingFields ?? []).length === 0 ? (_jsx("span", { className: "text-xs text-muted-foreground", children: "\u2014" })) : (_jsx("div", { className: "flex flex-wrap gap-1", children: (it.missingFields ?? []).map((m) => (_jsx(Badge, { variant: "outline", className: "text-[10px]", children: m }, m))) })) }), _jsx("td", { className: "p-3 text-xs text-muted-foreground", children: it.lastSyncAt ? new Date(it.lastSyncAt).toLocaleString("nl-NL") : "—" }), _jsx("td", { className: "p-3 text-xs text-destructive max-w-[240px] truncate", children: it.linkError ?? "—" }), _jsx("td", { className: "p-3", children: _jsxs("div", { className: "flex justify-end gap-1", children: [_jsxs(Button, { size: "sm", variant: "outline", onClick: () => it.localId &&
86
+ navigate({ to: "/voorraad/$id", params: { id: it.localId } }), children: [_jsx(Pencil, { className: "h-3 w-3 mr-1" }), "Bewerken"] }), _jsxs(Button, { size: "sm", variant: "outline", onClick: () => revalidate(it), children: [_jsx(RefreshCw, { className: "h-3 w-3 mr-1" }), "Valideer"] }), _jsxs(Button, { size: "sm", onClick: () => republish(it), children: [_jsx(Send, { className: "h-3 w-3 mr-1" }), "Publiceer"] })] }) })] }, it.id))) })] }) }));
87
+ }
@@ -0,0 +1,2 @@
1
+ export declare function ValidationPage(): import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=ValidationPage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ValidationPage.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/ValidationPage.tsx"],"names":[],"mappings":"AAOA,wBAAgB,cAAc,4CAiC7B"}
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Link } from "@tanstack/react-router";
3
+ import { Card } from "@flowselections/core";
4
+ import { ArrowLeft } from "lucide-react";
5
+ import { useFloridayConnection } from "../../hooks/useFloridayConnection";
6
+ import { useVoorraadData } from "../../hooks/useVoorraadData";
7
+ import { ValidationOverviewTable } from "./ValidationOverviewTable";
8
+ export function ValidationPage() {
9
+ const { activeId } = useFloridayConnection();
10
+ const { items, loading, reload } = useVoorraadData(activeId);
11
+ return (_jsxs("main", { className: "p-6 space-y-4", children: [_jsx("div", { className: "flex items-center gap-2", children: _jsxs(Link, { to: "/voorraad", className: "inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground", children: [_jsx(ArrowLeft, { className: "h-4 w-4" }), "Terug naar voorraad"] }) }), _jsxs("div", { children: [_jsx("h1", { className: "text-xl font-semibold", children: "Floriday-koppeling \u2014 validatie" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Alle artikelen die nog niet succesvol met Floriday gekoppeld zijn." })] }), _jsx(Card, { className: "overflow-hidden", children: loading ? (_jsx("div", { className: "p-6 text-sm text-muted-foreground", children: "Laden\u2026" })) : (_jsx(ValidationOverviewTable, { items: items, connectionId: activeId, onChanged: reload })) })] }));
12
+ }
@@ -0,0 +1,6 @@
1
+ import type { TradeItem } from "../../lib/floriday-client";
2
+ export declare function VoorraadDashboard({ items, connectionId, }: {
3
+ items: TradeItem[];
4
+ connectionId: string | null;
5
+ }): import("react/jsx-runtime").JSX.Element;
6
+ //# sourceMappingURL=VoorraadDashboard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VoorraadDashboard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/VoorraadDashboard.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AA2B3D,wBAAgB,iBAAiB,CAAC,EAChC,KAAK,EACL,YAAY,GACb,EAAE;IACD,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B,2CA4DA"}
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Link } from "@tanstack/react-router";
3
+ import { Card } from "@flowselections/core";
4
+ function Stat({ label, value, tone = "default", }) {
5
+ const toneCls = tone === "danger"
6
+ ? "text-destructive"
7
+ : tone === "warn"
8
+ ? "text-amber-600"
9
+ : tone === "good"
10
+ ? "text-emerald-600"
11
+ : "text-foreground";
12
+ return (_jsxs("div", { children: [_jsx("div", { className: "text-xs text-muted-foreground", children: label }), _jsx("div", { className: `text-2xl font-semibold tabular-nums ${toneCls}`, children: value })] }));
13
+ }
14
+ export function VoorraadDashboard({ items, connectionId, }) {
15
+ const total = items.length;
16
+ const linked = items.filter((i) => i.published).length +
17
+ items.filter((i) => !i.published && i.linkStatus === "linked").length;
18
+ const waiting = items.filter((i) => !i.published && i.linkStatus === "waiting_for_data").length;
19
+ const validationErrors = items.filter((i) => !i.published && (i.missingFields?.length ?? 0) > 0 && i.linkStatus !== "linked").length;
20
+ const publishErrors = items.filter((i) => !i.published && i.linkStatus === "publish_failed").length;
21
+ // Nieuw sinds laatste import
22
+ let lastImport = null;
23
+ try {
24
+ if (connectionId) {
25
+ const s = localStorage.getItem(`voorraad_last_import_${connectionId}`);
26
+ if (s)
27
+ lastImport = new Date(s);
28
+ }
29
+ }
30
+ catch {
31
+ /* ignore */
32
+ }
33
+ const newSinceImport = lastImport
34
+ ? items.filter((i) => {
35
+ const raw = i.raw;
36
+ const src = raw?.source ?? raw?.data?.source;
37
+ const created = raw?.created_at ? new Date(raw.created_at) : null;
38
+ return src === "excel_import" && created && created >= lastImport;
39
+ }).length
40
+ : 0;
41
+ return (_jsxs(Card, { className: "p-4", children: [_jsxs("div", { className: "flex items-start justify-between gap-3 mb-3", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-medium", children: "Floriday-koppeling overzicht" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Status van alle artikelen ten opzichte van Floriday" })] }), _jsx(Link, { to: "/voorraad/validatie", className: "text-xs text-primary hover:underline", children: "Openstaande artikelen bekijken \u2192" })] }), _jsxs("div", { className: "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-4", children: [_jsx(Stat, { label: "Artikelen", value: total }), _jsx(Stat, { label: "Gekoppeld", value: linked, tone: "good" }), _jsx(Stat, { label: "Wacht op koppeling", value: waiting, tone: "warn" }), _jsx(Stat, { label: "Validatiefouten", value: validationErrors, tone: "warn" }), _jsx(Stat, { label: "Publicatiefouten", value: publishErrors, tone: "danger" }), _jsx(Stat, { label: "Nieuw sinds import", value: newSinceImport })] })] }));
42
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"VoorraadPage.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/VoorraadPage.tsx"],"names":[],"mappings":"AAqBA,wBAAgB,YAAY,4CAqF3B"}
1
+ {"version":3,"file":"VoorraadPage.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/VoorraadPage.tsx"],"names":[],"mappings":"AA6EA,wBAAgB,YAAY,4CAiM3B"}