@flowselections/floriday-klanten-module 1.0.13 → 1.0.15

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 (42) hide show
  1. package/dist-lib/components/CustomerAvatar.js +1 -1
  2. package/dist-lib/components/klanten/ImportKlanten.d.ts +2 -0
  3. package/dist-lib/components/klanten/ImportKlanten.d.ts.map +1 -0
  4. package/dist-lib/components/klanten/ImportKlanten.js +438 -0
  5. package/dist-lib/components/klanten/KlantenOverzicht.d.ts.map +1 -1
  6. package/dist-lib/components/klanten/KlantenOverzicht.js +2 -2
  7. package/dist-lib/components/klanten/NetwerkPagina.d.ts.map +1 -1
  8. package/dist-lib/components/klanten/NetwerkPagina.js +38 -9
  9. package/dist-lib/components/klanten/NieuweKlant.d.ts.map +1 -1
  10. package/dist-lib/components/klanten/NieuweKlant.js +10 -1
  11. package/dist-lib/index.d.ts +1 -0
  12. package/dist-lib/index.d.ts.map +1 -1
  13. package/dist-lib/index.js +1 -0
  14. package/dist-lib/integrations/supabase/auth-middleware.d.ts +372 -1
  15. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  16. package/dist-lib/integrations/supabase/client.d.ts +372 -1
  17. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  18. package/dist-lib/integrations/supabase/client.server.d.ts +372 -1
  19. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  20. package/dist-lib/integrations/supabase/types.d.ts +376 -1
  21. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  22. package/dist-lib/lib/accounting.functions.d.ts +1116 -3
  23. package/dist-lib/lib/accounting.functions.d.ts.map +1 -1
  24. package/dist-lib/lib/customer-analytics.functions.d.ts +372 -1
  25. package/dist-lib/lib/customer-analytics.functions.d.ts.map +1 -1
  26. package/dist-lib/lib/customer-schemas.d.ts +147 -0
  27. package/dist-lib/lib/customer-schemas.d.ts.map +1 -1
  28. package/dist-lib/lib/customer-schemas.js +106 -7
  29. package/dist-lib/lib/customers.functions.d.ts +14184 -15
  30. package/dist-lib/lib/customers.functions.d.ts.map +1 -1
  31. package/dist-lib/lib/customers.functions.js +130 -23
  32. package/dist-lib/lib/delivery-locations.functions.d.ts +1488 -4
  33. package/dist-lib/lib/delivery-locations.functions.d.ts.map +1 -1
  34. package/dist-lib/lib/external-customers.functions.d.ts +1488 -4
  35. package/dist-lib/lib/external-customers.functions.d.ts.map +1 -1
  36. package/dist-lib/lib/floriday-discounts.functions.d.ts +744 -2
  37. package/dist-lib/lib/floriday-discounts.functions.d.ts.map +1 -1
  38. package/dist-lib/lib/local-discounts.functions.d.ts +2604 -7
  39. package/dist-lib/lib/local-discounts.functions.d.ts.map +1 -1
  40. package/dist-lib/lib/overdue-invoices.functions.d.ts +372 -1
  41. package/dist-lib/lib/overdue-invoices.functions.d.ts.map +1 -1
  42. package/package.json +10 -3
@@ -12,7 +12,7 @@ function initials(name) {
12
12
  }
13
13
  export function CustomerAvatar({ logoUrl, name, size = "md", className, }) {
14
14
  const sizeClass = size === "sm" ? "h-8 w-8 text-xs" : size === "lg" ? "h-16 w-16 text-lg" : "h-10 w-10 text-sm";
15
- return (_jsx("div", { className: cn("relative shrink-0 overflow-hidden rounded-full bg-muted flex items-center justify-center font-medium text-muted-foreground border border-border", sizeClass, className), children: logoUrl ? (_jsx("img", { src: logoUrl, alt: name ?? "", className: "h-full w-full object-cover", onError: (e) => {
15
+ return (_jsx("div", { className: cn("relative shrink-0 overflow-hidden rounded-md bg-white flex items-center justify-center font-medium text-muted-foreground border border-border", sizeClass, className), children: logoUrl ? (_jsx("img", { src: logoUrl, alt: name ?? "", className: "h-full w-full object-contain p-1", onError: (e) => {
16
16
  e.currentTarget.style.display = "none";
17
17
  } })) : (_jsx("span", { children: initials(name) })) }));
18
18
  }
@@ -0,0 +1,2 @@
1
+ export declare function ImportKlanten(): import("react").JSX.Element;
2
+ //# sourceMappingURL=ImportKlanten.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ImportKlanten.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/ImportKlanten.tsx"],"names":[],"mappings":"AA4MA,wBAAgB,aAAa,gCAmc5B"}
@@ -0,0 +1,438 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useNavigate } from "@tanstack/react-router";
3
+ import { useEffect, useMemo, useState } from "react";
4
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
5
+ import { useServerFn } from "@tanstack/react-start";
6
+ import { Button, Card, CardContent, CardHeader, CardTitle, Input, Label, toast, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Badge, Checkbox, } from "@flowselections/core";
7
+ import { ArrowLeft, Upload, Download, FileText, CheckCircle2, AlertTriangle, XCircle } from "lucide-react";
8
+ import * as XLSX from "xlsx";
9
+ import { bulkImportCustomers } from "../../lib/customers.functions";
10
+ import { customerImportRowSchema } from "../../lib/customer-schemas";
11
+ // Alleen `company_name` is écht verplicht om te kunnen importeren. Overige
12
+ // velden mogen leeg zijn — ontbrekende gegevens worden na de import getoond
13
+ // als waarschuwing zodat je ze later kunt aanvullen.
14
+ const REQUIRED = ["company_name"];
15
+ const RECOMMENDED_FIELDS = [
16
+ { key: "email", label: "E-mailadres" },
17
+ { key: "street", label: "Straat" },
18
+ { key: "house_number", label: "Huisnummer" },
19
+ { key: "country", label: "Land" },
20
+ ];
21
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
22
+ // Alle interne doelvelden die de importer ondersteunt (voor handmatige mapping)
23
+ const TARGET_FIELDS = [
24
+ { key: "company_name", label: "Bedrijfsnaam", required: true },
25
+ { key: "email", label: "E-mailadres" },
26
+ { key: "street", label: "Straat" },
27
+ { key: "house_number", label: "Huisnummer" },
28
+ { key: "country", label: "Land" },
29
+ { key: "customer_number", label: "Klantnummer" },
30
+ { key: "language", label: "Taal" },
31
+ { key: "website", label: "Website" },
32
+ { key: "billing_email", label: "E-mailadres voor facturering" },
33
+ { key: "address_type", label: "Adrestype" },
34
+ { key: "address_line1", label: "Adresregel 1" },
35
+ { key: "address_line2", label: "Adresregel 2" },
36
+ { key: "postal_code", label: "Postcode" },
37
+ { key: "city", label: "Plaats" },
38
+ { key: "icp_country_code", label: "ICP landcode" },
39
+ { key: "attention_to", label: "T.a.v." },
40
+ { key: "phone", label: "Telefoonnummer" },
41
+ { key: "fax", label: "Faxnummer" },
42
+ { key: "vat_number", label: "Btw-nummer" },
43
+ { key: "coc_number", label: "KvK-nummer" },
44
+ { key: "contact_person", label: "Contactpersoon" },
45
+ { key: "customer_class", label: "Klantklasse" },
46
+ { key: "notes", label: "Notities" },
47
+ { key: "is_active", label: "Actief" },
48
+ { key: "bank_name", label: "Bank - Naam" },
49
+ { key: "bank_account_holder", label: "Bank - Rekeninghouder" },
50
+ { key: "bank_account_number", label: "Bank - Rekeningnummer" },
51
+ { key: "bank_iban", label: "Bank - IBAN" },
52
+ { key: "bank_bic", label: "Bank - BIC" },
53
+ { key: "bank_national_code", label: "Bank - Nationale bankcode" },
54
+ { key: "bank_address_line1", label: "Bank - Adresregel 1" },
55
+ { key: "bank_address_line2", label: "Bank - Adresregel 2" },
56
+ { key: "bank_house_number", label: "Bank - Huisnummer" },
57
+ { key: "bank_postal_code", label: "Bank - Postcode" },
58
+ { key: "bank_city", label: "Bank - Plaats" },
59
+ { key: "bank_state", label: "Bank - Staat" },
60
+ { key: "bank_country", label: "Bank - Land" },
61
+ { key: "discount_percentage", label: "Kortingspercentage" },
62
+ { key: "discount_days", label: "Aantal kortingsdagen" },
63
+ { key: "payment_term_days", label: "Aantal vervaldagen" },
64
+ { key: "vat_display", label: "Btw-weergave op factuur" },
65
+ ];
66
+ // Mapping van herkende (Nederlandse of Engelse) CSV-kolomnamen -> intern veld.
67
+ // Alles wordt case-insensitive vergeleken.
68
+ const HEADER_ALIASES = {
69
+ // basis
70
+ "nummer": "customer_number", "customer_number": "customer_number",
71
+ "naam": "company_name", "bedrijfsnaam": "company_name", "company_name": "company_name",
72
+ "taal": "language", "language": "language",
73
+ "website": "website",
74
+ "e-mailadres voor facturering": "billing_email", "billing_email": "billing_email",
75
+ "e-mailadres": "email", "email": "email", "e-mail": "email",
76
+ "adrestype": "address_type", "address_type": "address_type",
77
+ "adresregel 1": "address_line1", "adresregel1": "address_line1", "address_line1": "address_line1",
78
+ "adresregel 2": "address_line2", "adresregel2": "address_line2", "address_line2": "address_line2",
79
+ "straat": "street", "street": "street",
80
+ "huisnummer": "house_number", "house_number": "house_number",
81
+ "postcode": "postal_code", "postal_code": "postal_code",
82
+ "plaats": "city", "stad": "city", "city": "city",
83
+ "land": "country", "country": "country",
84
+ "icp land code": "icp_country_code", "icp landcode": "icp_country_code", "icp_country_code": "icp_country_code",
85
+ "t.a.v.": "attention_to", "tav": "attention_to", "attention_to": "attention_to",
86
+ "telefoonnummer": "phone", "telefoon": "phone", "phone": "phone",
87
+ "faxnummer": "fax", "fax": "fax",
88
+ "btw-nummer": "vat_number", "btw nummer": "vat_number", "vat_number": "vat_number",
89
+ "kvk-nummer": "coc_number", "kvk nummer": "coc_number", "coc_number": "coc_number",
90
+ "contactpersoon": "contact_person", "contact_person": "contact_person",
91
+ "klantklasse": "customer_class", "customer_class": "customer_class",
92
+ "notities": "notes", "notes": "notes",
93
+ // bank
94
+ "bank - naam": "bank_name", "bank naam": "bank_name", "bank_name": "bank_name",
95
+ "bank - rekeninghouder": "bank_account_holder", "bank_account_holder": "bank_account_holder",
96
+ "bank - rekeningnummer": "bank_account_number", "bank_account_number": "bank_account_number",
97
+ "bank - iban": "bank_iban", "bank iban": "bank_iban", "bank_iban": "bank_iban", "iban": "bank_iban",
98
+ "bank - bic": "bank_bic", "bic": "bank_bic", "bank_bic": "bank_bic",
99
+ "bank - nationale bankcode": "bank_national_code", "bank_national_code": "bank_national_code",
100
+ "bank - adresregel 1": "bank_address_line1", "bank_address_line1": "bank_address_line1",
101
+ "bank - adresregel 2": "bank_address_line2", "bank_address_line2": "bank_address_line2",
102
+ "bank - huisnummer": "bank_house_number", "bank_house_number": "bank_house_number",
103
+ "bank - postcode": "bank_postal_code", "bank_postal_code": "bank_postal_code",
104
+ "bank - plaats": "bank_city", "bank_city": "bank_city",
105
+ "bank - staat": "bank_state", "bank_state": "bank_state",
106
+ "bank - land": "bank_country", "bank_country": "bank_country",
107
+ // facturering
108
+ "kortingspercentage": "discount_percentage", "discount_percentage": "discount_percentage",
109
+ "aantal kortingsdagen": "discount_days", "discount_days": "discount_days",
110
+ "aantal vervaldagen": "payment_term_days", "payment_term_days": "payment_term_days",
111
+ "btw-weergave op factuur": "vat_display", "btw weergave op factuur": "vat_display", "vat_display": "vat_display",
112
+ // status
113
+ "actief": "is_active", "is_active": "is_active",
114
+ };
115
+ const TEMPLATE_HEADERS = [
116
+ "Nummer", "Naam", "Taal", "Website", "E-mailadres voor facturering",
117
+ "Adrestype", "Adresregel 1", "Adresregel 2", "Postcode", "Plaats", "Land",
118
+ "ICP land code", "T.a.v.", "Telefoonnummer", "Faxnummer",
119
+ "Btw-nummer", "KvK-nummer", "E-mailadres",
120
+ "Bank - Naam", "Bank - Rekeninghouder", "Bank - Rekeningnummer",
121
+ "Bank - IBAN", "Bank - BIC", "Bank - Nationale bankcode",
122
+ "Bank - Adresregel 1", "Bank - Adresregel 2", "Bank - Huisnummer",
123
+ "Bank - Postcode", "Bank - Plaats", "Bank - Staat", "Bank - Land",
124
+ "Kortingspercentage", "Aantal kortingsdagen", "Aantal vervaldagen",
125
+ "Btw-weergave op factuur",
126
+ "Straat", "Huisnummer", "Contactpersoon", "Klantklasse", "Notities", "Actief",
127
+ ];
128
+ const TEMPLATE_CSV = TEMPLATE_HEADERS.map((h) => `"${h}"`).join(",") + "\n" +
129
+ `"K-001","Voorbeeld B.V.","nl","https://voorbeeld.nl","facturen@voorbeeld.nl","Bezoek","Hoofdstraat 12","","1234AB","Amsterdam","Nederland","NL","Jan Jansen","+31612345678","","NL123456789B01","12345678","info@voorbeeld.nl","ING","Voorbeeld B.V.","","NL91INGB0002445588","INGBNL2A","","Adres 1","","1","1000AA","Amsterdam","","Nederland","2","14","30","Exclusief","Hoofdstraat","12","Jan Jansen","Groothandel","","true"\n`;
130
+ function parseCSV(text) {
131
+ const rows = [];
132
+ let cur = [];
133
+ let field = "";
134
+ let inQuotes = false;
135
+ for (let i = 0; i < text.length; i++) {
136
+ const ch = text[i];
137
+ if (inQuotes) {
138
+ if (ch === '"') {
139
+ if (text[i + 1] === '"') {
140
+ field += '"';
141
+ i++;
142
+ }
143
+ else {
144
+ inQuotes = false;
145
+ }
146
+ }
147
+ else
148
+ field += ch;
149
+ }
150
+ else {
151
+ if (ch === '"')
152
+ inQuotes = true;
153
+ else if (ch === "," || ch === ";" || ch === "\t") {
154
+ cur.push(field);
155
+ field = "";
156
+ }
157
+ else if (ch === "\n" || ch === "\r") {
158
+ if (field !== "" || cur.length > 0) {
159
+ cur.push(field);
160
+ rows.push(cur);
161
+ cur = [];
162
+ field = "";
163
+ }
164
+ if (ch === "\r" && text[i + 1] === "\n")
165
+ i++;
166
+ }
167
+ else
168
+ field += ch;
169
+ }
170
+ }
171
+ if (field !== "" || cur.length > 0) {
172
+ cur.push(field);
173
+ rows.push(cur);
174
+ }
175
+ if (rows.length === 0)
176
+ return { headers: [], rows: [] };
177
+ const headers = rows[0].map((h) => h.trim());
178
+ const dataRows = rows.slice(1)
179
+ .filter((r) => r.some((c) => c.trim() !== ""))
180
+ .map((r) => {
181
+ const obj = {};
182
+ headers.forEach((h, idx) => { obj[h] = (r[idx] ?? "").trim(); });
183
+ return obj;
184
+ });
185
+ return { headers, rows: dataRows };
186
+ }
187
+ function toBool(v) {
188
+ if (v === undefined || v === "")
189
+ return true;
190
+ return !["false", "0", "nee", "no"].includes(v.toLowerCase());
191
+ }
192
+ function rowsFromXlsx(buf) {
193
+ const wb = XLSX.read(buf, { type: "array" });
194
+ const sheet = wb.Sheets[wb.SheetNames[0]];
195
+ if (!sheet)
196
+ return { headers: [], rows: [] };
197
+ const matrix = XLSX.utils.sheet_to_json(sheet, {
198
+ header: 1, raw: false, defval: "",
199
+ });
200
+ if (matrix.length === 0)
201
+ return { headers: [], rows: [] };
202
+ const headers = matrix[0].map((h) => String(h ?? "").trim());
203
+ const dataRows = matrix.slice(1)
204
+ .filter((r) => r.some((c) => String(c ?? "").trim() !== ""))
205
+ .map((r) => {
206
+ const obj = {};
207
+ headers.forEach((h, idx) => { obj[h] = String(r[idx] ?? "").trim(); });
208
+ return obj;
209
+ });
210
+ return { headers, rows: dataRows };
211
+ }
212
+ export function ImportKlanten() {
213
+ const navigate = useNavigate();
214
+ const importFn = useServerFn(bulkImportCustomers);
215
+ const queryClient = useQueryClient();
216
+ const [sourceHeaders, setSourceHeaders] = useState([]);
217
+ const [sourceRows, setSourceRows] = useState([]);
218
+ const [mapping, setMapping] = useState({});
219
+ const [rows, setRows] = useState([]);
220
+ const [fileName, setFileName] = useState("");
221
+ const [skipDuplicates, setSkipDuplicates] = useState(true);
222
+ const [result, setResult] = useState(null);
223
+ const mut = useMutation({
224
+ mutationFn: (validRows) => importFn({ data: { rows: validRows, skipDuplicates } }),
225
+ onSuccess: (r) => {
226
+ setResult(r);
227
+ if (r.created > 0)
228
+ toast.success(`${r.created} klant${r.created === 1 ? "" : "en"} geïmporteerd`);
229
+ if (r.failed > 0)
230
+ toast.error(`${r.failed} rij${r.failed === 1 ? "" : "en"} mislukt`);
231
+ if (r.created > 0) {
232
+ queryClient.invalidateQueries({ queryKey: ["customers-local"] });
233
+ queryClient.invalidateQueries({ queryKey: ["customers-external"] });
234
+ }
235
+ },
236
+ onError: (e) => toast.error(e instanceof Error ? e.message : "Import mislukt"),
237
+ });
238
+ const validCount = useMemo(() => rows.filter((r) => r.data && r.errors.length === 0).length, [rows]);
239
+ const invalidCount = rows.length - validCount;
240
+ const handleFile = async (file) => {
241
+ setResult(null);
242
+ setRows([]);
243
+ setFileName(file.name);
244
+ const isXlsx = /\.xlsx?$/i.test(file.name)
245
+ || file.type.includes("spreadsheetml")
246
+ || file.type.includes("ms-excel");
247
+ const { headers, rows: raw } = isXlsx
248
+ ? rowsFromXlsx(await file.arrayBuffer())
249
+ : parseCSV(await file.text());
250
+ if (raw.length === 0) {
251
+ toast.error("Geen rijen gevonden in bestand");
252
+ setSourceHeaders([]);
253
+ setSourceRows([]);
254
+ setMapping({});
255
+ return;
256
+ }
257
+ // Auto-map: probeer elke bronkolom via HEADER_ALIASES te matchen aan een intern veld.
258
+ const auto = {};
259
+ const used = new Set();
260
+ for (const h of headers) {
261
+ const key = h.trim().toLowerCase();
262
+ const target = HEADER_ALIASES[key];
263
+ if (target && !used.has(target)) {
264
+ auto[h] = target;
265
+ used.add(target);
266
+ }
267
+ else {
268
+ auto[h] = "";
269
+ }
270
+ }
271
+ setSourceHeaders(headers);
272
+ setSourceRows(raw);
273
+ setMapping(auto);
274
+ };
275
+ const buildParsed = (srcRows, map) => {
276
+ return srcRows.map((sr) => {
277
+ // Herbouw rij op basis van huidige mapping
278
+ const r = {};
279
+ for (const [srcHeader, target] of Object.entries(map)) {
280
+ if (!target)
281
+ continue;
282
+ const val = sr[srcHeader];
283
+ if (val !== undefined && val !== "")
284
+ r[target] = val;
285
+ }
286
+ // Fallback: als straat ontbreekt maar adresregel 1 is gevuld, gebruik die.
287
+ const street = r.street || r.address_line1 || "";
288
+ const houseNumber = r.house_number || "";
289
+ // E-mail sanitair: alleen doorlaten als het qua vorm lijkt op een
290
+ // e-mailadres. Ongeldige waarden worden weggelaten en als
291
+ // waarschuwing gerapporteerd.
292
+ const rawEmail = (r.email || "").trim();
293
+ const emailValid = rawEmail !== "" && EMAIL_RE.test(rawEmail);
294
+ const email = emailValid ? rawEmail : "";
295
+ const candidate = {
296
+ company_name: r.company_name ?? "",
297
+ email,
298
+ street,
299
+ house_number: houseNumber,
300
+ country: r.country ?? "",
301
+ vat_number: r.vat_number ?? "",
302
+ contact_person: r.contact_person ?? "",
303
+ phone: r.phone ?? "",
304
+ postal_code: r.postal_code ?? "",
305
+ city: r.city ?? "",
306
+ customer_class: r.customer_class ?? "",
307
+ notes: r.notes ?? "",
308
+ is_active: toBool(r.is_active),
309
+ customer_number: r.customer_number ?? "",
310
+ language: r.language ?? "",
311
+ website: r.website ?? "",
312
+ billing_email: r.billing_email ?? "",
313
+ address_type: r.address_type ?? "",
314
+ address_line1: r.address_line1 ?? "",
315
+ address_line2: r.address_line2 ?? "",
316
+ icp_country_code: r.icp_country_code ?? "",
317
+ attention_to: r.attention_to ?? "",
318
+ fax: r.fax ?? "",
319
+ coc_number: r.coc_number ?? "",
320
+ bank_name: r.bank_name ?? "",
321
+ bank_account_holder: r.bank_account_holder ?? "",
322
+ bank_account_number: r.bank_account_number ?? "",
323
+ bank_iban: r.bank_iban ?? "",
324
+ bank_bic: r.bank_bic ?? "",
325
+ bank_national_code: r.bank_national_code ?? "",
326
+ bank_address_line1: r.bank_address_line1 ?? "",
327
+ bank_address_line2: r.bank_address_line2 ?? "",
328
+ bank_house_number: r.bank_house_number ?? "",
329
+ bank_postal_code: r.bank_postal_code ?? "",
330
+ bank_city: r.bank_city ?? "",
331
+ bank_state: r.bank_state ?? "",
332
+ bank_country: r.bank_country ?? "",
333
+ discount_percentage: r.discount_percentage ?? "",
334
+ discount_days: r.discount_days ?? "",
335
+ payment_term_days: r.payment_term_days ?? "",
336
+ vat_display: r.vat_display ?? "",
337
+ };
338
+ const warnings = [];
339
+ for (const f of RECOMMENDED_FIELDS) {
340
+ const v = candidate[f.key];
341
+ if (v === undefined || v === null || v === "") {
342
+ warnings.push(`${f.label} ontbreekt`);
343
+ }
344
+ }
345
+ if (rawEmail !== "" && !emailValid) {
346
+ warnings.push("E-mailadres ongeldig — leeg gelaten");
347
+ }
348
+ const res = customerImportRowSchema.safeParse(candidate);
349
+ if (res.success)
350
+ return { raw: r, data: res.data, errors: [], warnings };
351
+ return {
352
+ raw: r,
353
+ data: null,
354
+ errors: res.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`),
355
+ warnings,
356
+ };
357
+ });
358
+ };
359
+ // Preview automatisch bijwerken wanneer bron of mapping verandert.
360
+ useEffect(() => {
361
+ if (sourceRows.length === 0) {
362
+ setRows([]);
363
+ return;
364
+ }
365
+ setRows(buildParsed(sourceRows, mapping));
366
+ // eslint-disable-next-line react-hooks/exhaustive-deps
367
+ }, [sourceRows, mapping]);
368
+ const downloadTemplateCsv = () => {
369
+ const blob = new Blob([TEMPLATE_CSV], { type: "text/csv;charset=utf-8" });
370
+ const url = URL.createObjectURL(blob);
371
+ const a = document.createElement("a");
372
+ a.href = url;
373
+ a.download = "klanten-import-template.csv";
374
+ a.click();
375
+ URL.revokeObjectURL(url);
376
+ };
377
+ const downloadTemplateXlsx = () => {
378
+ const sampleRow = {
379
+ "Nummer": "K-001", "Naam": "Voorbeeld B.V.", "Taal": "nl",
380
+ "Website": "https://voorbeeld.nl",
381
+ "E-mailadres voor facturering": "facturen@voorbeeld.nl",
382
+ "Adrestype": "Bezoek", "Adresregel 1": "Hoofdstraat 12", "Adresregel 2": "",
383
+ "Postcode": "1234AB", "Plaats": "Amsterdam", "Land": "Nederland",
384
+ "ICP land code": "NL", "T.a.v.": "Jan Jansen",
385
+ "Telefoonnummer": "+31612345678", "Faxnummer": "",
386
+ "Btw-nummer": "NL123456789B01", "KvK-nummer": "12345678",
387
+ "E-mailadres": "info@voorbeeld.nl",
388
+ "Bank - Naam": "ING", "Bank - Rekeninghouder": "Voorbeeld B.V.",
389
+ "Bank - Rekeningnummer": "", "Bank - IBAN": "NL91INGB0002445588",
390
+ "Bank - BIC": "INGBNL2A", "Bank - Nationale bankcode": "",
391
+ "Bank - Adresregel 1": "Adres 1", "Bank - Adresregel 2": "",
392
+ "Bank - Huisnummer": "1", "Bank - Postcode": "1000AA",
393
+ "Bank - Plaats": "Amsterdam", "Bank - Staat": "", "Bank - Land": "Nederland",
394
+ "Kortingspercentage": "2", "Aantal kortingsdagen": "14",
395
+ "Aantal vervaldagen": "30", "Btw-weergave op factuur": "Exclusief",
396
+ "Straat": "Hoofdstraat", "Huisnummer": "12",
397
+ "Contactpersoon": "Jan Jansen", "Klantklasse": "Groothandel",
398
+ "Notities": "", "Actief": "true",
399
+ };
400
+ const ws = XLSX.utils.json_to_sheet([sampleRow], { header: TEMPLATE_HEADERS });
401
+ const wb = XLSX.utils.book_new();
402
+ XLSX.utils.book_append_sheet(wb, ws, "Klanten");
403
+ XLSX.writeFile(wb, "klanten-import-template.xlsx");
404
+ };
405
+ const runImport = () => {
406
+ const valid = rows.filter((r) => r.data && r.errors.length === 0).map((r) => r.data);
407
+ if (valid.length === 0) {
408
+ toast.error("Geen geldige rijen om te importeren");
409
+ return;
410
+ }
411
+ mut.mutate(valid);
412
+ };
413
+ return (_jsxs("main", { className: "p-6 max-w-5xl mx-auto space-y-4", children: [_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigate({ to: "/klanten" }), children: [_jsx(ArrowLeft, { className: "h-4 w-4 mr-2" }), " Terug"] }), _jsxs("div", { className: "flex items-start justify-between gap-4 flex-wrap", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: "Klanten importeren" }), _jsxs("p", { className: "text-sm text-muted-foreground mt-1", children: ["Upload een CSV of Excel om meerdere klanten tegelijk toe te voegen. Alleen ", _jsx("strong", { children: "bedrijfsnaam" }), " is verplicht \u2014 ontbrekende gegevens zoals e-mail, adres of land worden na de import als waarschuwing getoond zodat je ze later kunt aanvullen."] })] }), _jsxs("div", { className: "flex gap-2", children: [_jsxs(Button, { variant: "outline", onClick: downloadTemplateXlsx, children: [_jsx(Download, { className: "h-4 w-4 mr-2" }), " Template (.xlsx)"] }), _jsxs(Button, { variant: "outline", onClick: downloadTemplateCsv, children: [_jsx(Download, { className: "h-4 w-4 mr-2" }), " Template (.csv)"] })] })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "1. Bestand kiezen" }) }), _jsxs(CardContent, { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center gap-3 flex-wrap", children: [_jsx(Label, { htmlFor: "csvfile", className: "sr-only", children: "Excel- of CSV-bestand" }), _jsx(Input, { id: "csvfile", type: "file", accept: ".csv,.xlsx,.xls,text/csv,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel", onChange: (e) => { const f = e.target.files?.[0]; if (f)
414
+ handleFile(f); }, className: "max-w-sm" }), fileName && (_jsxs("span", { className: "inline-flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx(FileText, { className: "h-4 w-4" }), " ", fileName] }))] }), _jsxs("label", { className: "inline-flex items-center gap-2 text-sm", children: [_jsx(Checkbox, { checked: skipDuplicates, onCheckedChange: (v) => setSkipDuplicates(v === true) }), "Duplicaten overslaan (op e-mail, BTW of vergelijkbare bedrijfsnaam)"] })] })] }), sourceHeaders.length > 0 && (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsxs(CardTitle, { className: "text-base", children: ["2. Velden koppelen (", sourceHeaders.length, " kolommen)"] }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Kies per bronkolom naar welk klantveld deze moet worden ge\u00EFmporteerd." })] }), _jsx(CardContent, { className: "p-0", children: _jsx("div", { className: "max-h-[420px] overflow-auto", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Bronkolom (uit bestand)" }), _jsx(TableHead, { children: "Voorbeeldwaarde" }), _jsx(TableHead, { className: "w-[280px]", children: "Koppelen aan klantveld" })] }) }), _jsx(TableBody, { children: sourceHeaders.map((h) => {
415
+ const sample = sourceRows.find((r) => r[h])?.[h] ?? "";
416
+ return (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "text-sm font-medium", children: h || _jsx("span", { className: "text-muted-foreground italic", children: "(zonder naam)" }) }), _jsx(TableCell, { className: "text-xs text-muted-foreground truncate max-w-[280px]", children: sample }), _jsx(TableCell, { children: _jsxs("select", { className: "w-full h-9 rounded-md border border-input bg-background px-2 text-sm", value: mapping[h] ?? "", onChange: (e) => setMapping((m) => ({ ...m, [h]: e.target.value })), children: [_jsx("option", { value: "", children: "\u2014 Negeren \u2014" }), TARGET_FIELDS.map((f) => (_jsxs("option", { value: f.key, children: [f.label, f.required ? " *" : ""] }, f.key)))] }) })] }, h));
417
+ }) })] }) }) }), _jsxs("div", { className: "flex items-center justify-between gap-2 p-4 border-t", children: [_jsxs("div", { className: "text-xs text-muted-foreground", children: ["Verplicht: ", TARGET_FIELDS.filter((f) => f.required).map((f) => f.label).join(", ")] }), _jsx(Button, { variant: "outline", onClick: () => {
418
+ setSourceHeaders([]);
419
+ setSourceRows([]);
420
+ setMapping({});
421
+ setRows([]);
422
+ setFileName("");
423
+ setResult(null);
424
+ }, children: "Wissen" })] })] })), rows.length > 0 && (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsxs(CardTitle, { className: "text-base", children: ["3. Voorbeeld (", rows.length, " rijen)"] }), _jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsxs(Badge, { variant: "secondary", className: "bg-success/10 text-success", children: [_jsx(CheckCircle2, { className: "h-3 w-3 mr-1" }), " ", validCount, " geldig"] }), rows.filter((r) => r.errors.length === 0 && r.warnings.length > 0).length > 0 && (_jsxs(Badge, { variant: "secondary", className: "bg-warning/10 text-warning", children: [_jsx(AlertTriangle, { className: "h-3 w-3 mr-1" }), rows.filter((r) => r.errors.length === 0 && r.warnings.length > 0).length, " met waarschuwingen"] })), invalidCount > 0 && (_jsxs(Badge, { variant: "secondary", className: "bg-destructive/10 text-destructive", children: [_jsx(XCircle, { className: "h-3 w-3 mr-1" }), " ", invalidCount, " ongeldig"] }))] })] }), _jsx(CardContent, { className: "p-0", children: _jsx("div", { className: "max-h-[420px] overflow-auto", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { className: "w-10", children: "#" }), _jsx(TableHead, { children: "Bedrijf" }), _jsx(TableHead, { children: "E-mail" }), _jsx(TableHead, { children: "Plaats" }), _jsx(TableHead, { children: "Land" }), _jsx(TableHead, { children: "Status" })] }) }), _jsx(TableBody, { children: rows.map((r, i) => (_jsxs(TableRow, { className: r.errors.length > 0
425
+ ? "bg-destructive/5"
426
+ : r.warnings.length > 0
427
+ ? "bg-warning/5"
428
+ : "", children: [_jsx(TableCell, { className: "text-xs text-muted-foreground", children: i + 1 }), _jsx(TableCell, { className: "text-sm", children: r.raw.company_name || _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }), _jsx(TableCell, { className: "text-sm", children: r.raw.email || _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }), _jsx(TableCell, { className: "text-sm", children: r.raw.city || _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }), _jsx(TableCell, { className: "text-sm", children: r.raw.country || _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }), _jsx(TableCell, { children: r.errors.length > 0 ? (_jsxs("span", { className: "inline-flex items-center gap-1 text-xs text-destructive", title: r.errors.join("\n"), children: [_jsx(AlertTriangle, { className: "h-3.5 w-3.5" }), " ", r.errors[0]] })) : r.warnings.length > 0 ? (_jsxs("span", { className: "inline-flex items-center gap-1 text-xs text-warning", title: r.warnings.join("\n"), children: [_jsx(AlertTriangle, { className: "h-3.5 w-3.5" }), r.warnings.length === 1
429
+ ? r.warnings[0]
430
+ : `${r.warnings.length} waarschuwingen`] })) : (_jsxs("span", { className: "inline-flex items-center gap-1 text-xs text-success", children: [_jsx(CheckCircle2, { className: "h-3.5 w-3.5" }), " OK"] })) })] }, i))) })] }) }) }), _jsxs("div", { className: "flex justify-end gap-2 p-4 border-t", children: [_jsx(Button, { variant: "outline", onClick: () => {
431
+ setRows([]);
432
+ setFileName("");
433
+ setResult(null);
434
+ setSourceHeaders([]);
435
+ setSourceRows([]);
436
+ setMapping({});
437
+ }, children: "Wissen" }), _jsxs(Button, { onClick: runImport, disabled: mut.isPending || validCount === 0, children: [_jsx(Upload, { className: "h-4 w-4 mr-2" }), mut.isPending ? "Importeren..." : `Importeer ${validCount} klant${validCount === 1 ? "" : "en"}`] })] })] })), result && (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Resultaat" }) }), _jsxs(CardContent, { className: "space-y-3", children: [_jsxs("div", { className: "flex gap-2 flex-wrap", children: [_jsxs(Badge, { className: "bg-success/10 text-success", children: [result.created, " aangemaakt"] }), _jsxs(Badge, { className: "bg-warning/10 text-warning", children: [result.skipped, " overgeslagen"] }), _jsxs(Badge, { className: "bg-destructive/10 text-destructive", children: [result.failed, " mislukt"] })] }), result.errors.length > 0 && (_jsx("div", { className: "max-h-64 overflow-auto border rounded-md", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { className: "w-10", children: "#" }), _jsx(TableHead, { children: "Bedrijf" }), _jsx(TableHead, { children: "Reden" })] }) }), _jsx(TableBody, { children: result.errors.map((e, i) => (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "text-xs", children: e.row }), _jsx(TableCell, { className: "text-sm", children: e.company_name }), _jsx(TableCell, { className: "text-sm text-muted-foreground", children: e.reason })] }, i))) })] }) })), _jsx("div", { className: "flex justify-end", children: _jsx(Button, { onClick: () => navigate({ to: "/klanten" }), children: "Naar klantenlijst" }) })] })] }))] }));
438
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"KlantenOverzicht.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantenOverzicht.tsx"],"names":[],"mappings":"AAwCA,wBAAgB,gBAAgB,gCAwZ/B"}
1
+ {"version":3,"file":"KlantenOverzicht.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantenOverzicht.tsx"],"names":[],"mappings":"AAwCA,wBAAgB,gBAAgB,gCA2Z/B"}
@@ -4,7 +4,7 @@ import { useQuery } from "@tanstack/react-query";
4
4
  import { useServerFn } from "@tanstack/react-start";
5
5
  import { useMemo, useState } from "react";
6
6
  import { Button, Input, Card, CardContent, Badge, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Tabs, TabsList, TabsTrigger, Skeleton, } from "@flowselections/core";
7
- import { Plus, Search, Network, Users, UserCheck, UserX, AlertCircle, X, ArrowUpDown, Mail, MapPin, ChevronLeft, ChevronRight, } from "lucide-react";
7
+ import { Plus, Search, Network, Users, UserCheck, UserX, AlertCircle, X, ArrowUpDown, Mail, MapPin, Upload, ChevronLeft, ChevronRight, } from "lucide-react";
8
8
  import * as TooltipPrimitive from "@radix-ui/react-tooltip";
9
9
  import { listCustomers, listCustomerClasses } from "../../lib/customers.functions";
10
10
  import { listExternalCustomers } from "../../lib/external-customers.functions";
@@ -157,7 +157,7 @@ export function KlantenOverzicht() {
157
157
  activeFilterChips.push({ key: "co", label: `Land: ${country}`, clear: () => { setCountry("__all__"); resetPage(); } });
158
158
  const toggleSort = (col) => setSort((s) => (s.col === col ? { col, dir: s.dir === "asc" ? "desc" : "asc" } : { col, dir: "asc" }));
159
159
  const lastUpdated = externalData?.last_fetched_at ?? null;
160
- return (_jsx(TooltipPrimitive.Provider, { delayDuration: 200, children: _jsxs("main", { className: "p-6 space-y-5", children: [_jsxs("div", { className: "flex items-start justify-between gap-4 flex-wrap", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: "Klanten" }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: "Beheer en doorzoek al je klanten." })] }), _jsxs("div", { className: "flex gap-2", children: [_jsxs(Button, { variant: "outline", onClick: () => navigate({ to: "/klanten/netwerk" }), children: [_jsx(Network, { className: "h-4 w-4 mr-2" }), " Klanten toevoegen uit netwerk"] }), _jsxs(Button, { onClick: () => navigate({ to: "/klanten/nieuw" }), children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), " Nieuwe klant"] })] })] }), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3", children: [_jsx(StatCard, { icon: _jsx(Users, { className: "h-5 w-5" }), tint: "muted", label: "Alle klanten", value: totals.all, tooltip: "Totaal aantal klanten (lokaal + gekoppelde bronnen).", active: view === "all", onClick: () => { setView("all"); resetPage(); } }), _jsx(StatCard, { icon: _jsx(UserCheck, { className: "h-5 w-5" }), tint: "success", label: "Actieve klanten", value: totals.active, tooltip: "Klanten met minimaal \u00E9\u00E9n bestelling in de afgelopen 12 maanden.", active: view === "active", onClick: () => { setView("active"); resetPage(); } }), _jsx(StatCard, { icon: _jsx(UserX, { className: "h-5 w-5" }), tint: "warning", label: "Inactieve klanten", value: totals.inactive, tooltip: "Klanten zonder omzet in de afgelopen 12 maanden.", active: view === "inactive", onClick: () => { setView("inactive"); resetPage(); } }), _jsx(StatCard, { icon: _jsx(AlertCircle, { className: "h-5 w-5" }), tint: "destructive", label: "Achterstallige betalingen", value: totals.overdue, tooltip: (overdueData
160
+ return (_jsx(TooltipPrimitive.Provider, { delayDuration: 200, children: _jsxs("main", { className: "p-6 space-y-5", children: [_jsxs("div", { className: "flex items-start justify-between gap-4 flex-wrap", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: "Klanten" }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: "Beheer en doorzoek al je klanten." })] }), _jsxs("div", { className: "flex gap-2", children: [_jsxs(Button, { variant: "outline", onClick: () => navigate({ to: "/klanten/netwerk" }), children: [_jsx(Network, { className: "h-4 w-4 mr-2" }), " Klanten toevoegen uit netwerk"] }), _jsxs(Button, { variant: "outline", onClick: () => navigate({ to: "/klanten/import" }), children: [_jsx(Upload, { className: "h-4 w-4 mr-2" }), " Importeren"] }), _jsxs(Button, { onClick: () => navigate({ to: "/klanten/nieuw" }), children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), " Nieuwe klant"] })] })] }), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3", children: [_jsx(StatCard, { icon: _jsx(Users, { className: "h-5 w-5" }), tint: "muted", label: "Alle klanten", value: totals.all, tooltip: "Totaal aantal klanten (lokaal + gekoppelde bronnen).", active: view === "all", onClick: () => { setView("all"); resetPage(); } }), _jsx(StatCard, { icon: _jsx(UserCheck, { className: "h-5 w-5" }), tint: "success", label: "Actieve klanten", value: totals.active, tooltip: "Klanten met minimaal \u00E9\u00E9n bestelling in de afgelopen 12 maanden.", active: view === "active", onClick: () => { setView("active"); resetPage(); } }), _jsx(StatCard, { icon: _jsx(UserX, { className: "h-5 w-5" }), tint: "warning", label: "Inactieve klanten", value: totals.inactive, tooltip: "Klanten zonder omzet in de afgelopen 12 maanden.", active: view === "inactive", onClick: () => { setView("inactive"); resetPage(); } }), _jsx(StatCard, { icon: _jsx(AlertCircle, { className: "h-5 w-5" }), tint: "destructive", label: "Achterstallige betalingen", value: totals.overdue, tooltip: (overdueData
161
161
  ? `Klanten met openstaande facturen waarvan de vervaldatum is verstreken.\nTotaal openstaand: ${formatEuro(overdueData.totals.amount)} · ${overdueData.totals.invoices} facturen\nBron: Twinfield & Exact · bijgewerkt ${relativeTime(overdueData.as_of)}`
162
162
  : "Klanten met openstaande facturen waarvan de vervaldatum is verstreken."), active: view === "overdue", onClick: () => { setView("overdue"); resetPage(); } })] }), _jsxs(Card, { children: [_jsx("div", { className: "border-b px-4 pt-3", children: _jsx(Tabs, { value: view, onValueChange: (v) => { setView(v); resetPage(); }, children: _jsxs(TabsList, { className: "flex-wrap", children: [_jsxs(TabsTrigger, { value: "all", children: ["Alle ", _jsx("span", { className: "ml-1.5 text-xs text-muted-foreground", children: totals.all ?? 0 })] }), _jsxs(TabsTrigger, { value: "active", children: ["Actief ", _jsx("span", { className: "ml-1.5 text-xs text-muted-foreground", children: totals.active ?? 0 })] }), _jsxs(TabsTrigger, { value: "inactive", children: ["Inactief ", _jsx("span", { className: "ml-1.5 text-xs text-muted-foreground", children: totals.inactive ?? 0 })] }), _jsxs(TabsTrigger, { value: "overdue", children: ["Achterstallig ", _jsx("span", { className: "ml-1.5 text-xs text-muted-foreground", children: totals.overdue ?? 0 })] })] }) }) }), _jsxs("div", { className: "flex items-center gap-2 flex-wrap p-4 border-b", children: [_jsxs("div", { className: "relative flex-1 min-w-[240px] max-w-md", children: [_jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" }), _jsx(Input, { placeholder: "Zoek op bedrijf, contactpersoon of e-mail...", value: search, onChange: (e) => { setSearch(e.target.value); resetPage(); }, className: "pl-9" })] }), _jsxs(Select, { value: customerClass, onValueChange: (v) => { setCustomerClass(v); resetPage(); }, children: [_jsx(SelectTrigger, { className: "w-40", children: _jsx(SelectValue, { placeholder: "Klasse" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "__all__", children: "Alle klassen" }), (classData?.classes ?? []).map((c) => (_jsx(SelectItem, { value: c, children: c }, c)))] })] }), _jsxs(Select, { value: country, onValueChange: (v) => { setCountry(v); resetPage(); }, children: [_jsx(SelectTrigger, { className: "w-36", children: _jsx(SelectValue, { placeholder: "Land" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "__all__", children: "Alle landen" }), countries.map((c) => (_jsx(SelectItem, { value: c, children: c }, c)))] })] })] }), activeFilterChips.length > 0 && (_jsxs("div", { className: "flex items-center gap-2 flex-wrap px-4 py-2 border-b bg-muted/30", children: [activeFilterChips.map((chip) => (_jsxs("button", { type: "button", onClick: chip.clear, className: "inline-flex items-center gap-1.5 rounded-full border border-border bg-card px-3 py-1 text-xs hover:bg-muted", children: [chip.label, " ", _jsx(X, { className: "h-3 w-3" })] }, chip.key))), _jsx("button", { type: "button", onClick: () => { setSearch(""); setCustomerClass("__all__"); setCountry("__all__"); resetPage(); }, className: "text-xs text-muted-foreground hover:text-foreground underline", children: "Alles wissen" })] })), _jsx(CardContent, { className: "p-0", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: _jsx(SortHeader, { label: "Bedrijf", col: "company_name", sort: sort, onClick: toggleSort }) }), _jsx(TableHead, { children: "Contact" }), _jsx(TableHead, { children: _jsx(SortHeader, { label: "Klasse", col: "customer_class", sort: sort, onClick: toggleSort }) }), _jsx(TableHead, { children: _jsx(SortHeader, { label: "Land", col: "country", sort: sort, onClick: toggleSort }) }), _jsx(TableHead, { children: "Status" })] }) }), _jsx(TableBody, { children: isLoading ? (Array.from({ length: 6 }).map((_, i) => (_jsx(TableRow, { children: Array.from({ length: 5 }).map((__, j) => (_jsx(TableCell, { className: "py-4", children: _jsx(Skeleton, { className: "h-4 w-full max-w-[160px]" }) }, j))) }, i)))) : pageRows.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 5, children: _jsx(EmptyState, { onAdd: () => navigate({ to: "/klanten/nieuw" }) }) }) })) : pageRows.map((c) => {
163
163
  const secondary = c.commercial_name && c.commercial_name !== c.company_name
@@ -1 +1 @@
1
- {"version":3,"file":"NetwerkPagina.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/NetwerkPagina.tsx"],"names":[],"mappings":"AAqBA,wBAAgB,aAAa,gCAyH5B"}
1
+ {"version":3,"file":"NetwerkPagina.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/NetwerkPagina.tsx"],"names":[],"mappings":"AA8BA,wBAAgB,aAAa,gCAwO5B"}
@@ -1,23 +1,52 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Link } from "@tanstack/react-router";
3
3
  import { useQuery } from "@tanstack/react-query";
4
4
  import { useServerFn } from "@tanstack/react-start";
5
5
  import { useState } from "react";
6
6
  import { Input, Card, CardContent, Badge, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Switch, Label, } from "@flowselections/core";
7
- import { ArrowLeft, Search } from "lucide-react";
7
+ import { ArrowLeft, Search, MapPin, Globe } from "lucide-react";
8
8
  import { listFloridayNetwork } from "../../lib/customers.functions";
9
+ import { CustomerAvatar } from "../CustomerAvatar";
10
+ const TYPE_OPTIONS = [
11
+ { value: "CUSTOMER", label: "Klant" },
12
+ { value: "AGENT", label: "Agent" },
13
+ { value: "SUPPLIER", label: "Leverancier" },
14
+ { value: "CARRIER", label: "Vervoerder" },
15
+ { value: "OTHER", label: "Overig" },
16
+ ];
9
17
  export function NetwerkPagina() {
10
18
  const [search, setSearch] = useState("");
11
- const [hideUnknown, setHideUnknown] = useState(true);
19
+ const [hideUnknown, setHideUnknown] = useState(false);
20
+ const [types, setTypes] = useState(["CUSTOMER", "AGENT"]);
21
+ const [includeExpired, setIncludeExpired] = useState(false);
12
22
  const list = useServerFn(listFloridayNetwork);
13
23
  const { data, isLoading } = useQuery({
14
- queryKey: ["floriday-network", search],
15
- queryFn: () => list({ data: { search } }),
24
+ queryKey: ["floriday-network", search, types, includeExpired],
25
+ queryFn: () => list({ data: { search, types, includeExpired, limit: 1000 } }),
16
26
  });
17
27
  const all = data?.organizations ?? [];
18
- const orgs = hideUnknown
19
- ? all.filter((o) => o.organization_name && o.organization_name.trim() !== "")
20
- : all;
28
+ const named = all.filter((o) => o.organization_name && o.organization_name.trim() !== "");
29
+ const orgs = hideUnknown ? named : all;
21
30
  const hiddenCount = all.length - orgs.length;
22
- return (_jsxs("main", { className: "p-6 space-y-4", children: [_jsx("div", { className: "flex items-center justify-between gap-4", children: _jsxs("div", { children: [_jsxs(Link, { to: "/klanten", className: "text-sm text-muted-foreground hover:text-foreground inline-flex items-center gap-1", children: [_jsx(ArrowLeft, { className: "h-3 w-3" }), " Terug naar klanten"] }), _jsx("h1", { className: "text-2xl font-semibold mt-1", children: "Klanten toevoegen uit netwerk" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Doorzoek alle organisaties die via het netwerk beschikbaar zijn." })] }) }), _jsxs("div", { className: "relative max-w-md", children: [_jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" }), _jsx(Input, { placeholder: "Zoek op organisatienaam of GLN...", value: search, onChange: (e) => setSearch(e.target.value), className: "pl-9" })] }), _jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx(Switch, { id: "hide-unknown", checked: hideUnknown, onCheckedChange: setHideUnknown }), _jsx(Label, { htmlFor: "hide-unknown", className: "cursor-pointer", children: "Verberg organisaties zonder naam" }), hideUnknown && hiddenCount > 0 ? (_jsxs("span", { className: "text-muted-foreground", children: ["(", hiddenCount, " verborgen)"] })) : null] }), _jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Organisatie" }), _jsx(TableHead, { children: "GLN" }), _jsx(TableHead, { children: "Plaats" }), _jsx(TableHead, { children: "Land" }), _jsx(TableHead, { children: "Bijgewerkt" })] }) }), _jsx(TableBody, { children: isLoading ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 5, className: "text-center py-8 text-muted-foreground", children: "Laden..." }) })) : orgs.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 5, className: "text-center py-8 text-muted-foreground", children: "Geen organisaties gevonden." }) })) : (orgs.map((o) => (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "font-medium", children: o.organization_name && o.organization_name.trim() !== "" ? (o.organization_name) : (_jsx("span", { className: "italic text-muted-foreground", children: "Onbekende organisatie" })) }), _jsx(TableCell, { children: o.gln ? (_jsx(Badge, { variant: "outline", children: o.gln })) : ("—") }), _jsx(TableCell, { children: o.city ?? "—" }), _jsx(TableCell, { children: o.country ?? "—" }), _jsx(TableCell, { className: "text-muted-foreground text-sm", children: new Date(o.fetched_at).toLocaleDateString("nl-NL") })] }, `${o.connection_id}-${o.organization_id}`)))) })] }) }) })] }));
31
+ const totalMatching = data?.total ?? all.length;
32
+ const truncated = !!data?.truncated;
33
+ const toggleType = (value) => {
34
+ setTypes((prev) => prev.includes(value) ? prev.filter((t) => t !== value) : [...prev, value]);
35
+ };
36
+ return (_jsxs("main", { className: "p-6 space-y-4", children: [_jsx("div", { className: "flex items-center justify-between gap-4", children: _jsxs("div", { children: [_jsxs(Link, { to: "/klanten", className: "text-sm text-muted-foreground hover:text-foreground inline-flex items-center gap-1", children: [_jsx(ArrowLeft, { className: "h-3 w-3" }), " Terug naar klanten"] }), _jsx("h1", { className: "text-2xl font-semibold mt-1", children: "Klanten toevoegen uit netwerk" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Doorzoek alle organisaties die via het netwerk beschikbaar zijn." })] }) }), _jsxs("div", { className: "relative max-w-md", children: [_jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" }), _jsx(Input, { placeholder: "Zoek op organisatienaam of GLN...", value: search, onChange: (e) => setSearch(e.target.value), className: "pl-9" })] }), _jsx("div", { className: "flex items-center gap-2 flex-wrap", children: TYPE_OPTIONS.map((opt) => {
37
+ const active = types.includes(opt.value);
38
+ return (_jsx("button", { type: "button", onClick: () => toggleType(opt.value), className: "inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs transition-colors " +
39
+ (active
40
+ ? "bg-primary text-primary-foreground border-primary"
41
+ : "bg-card text-muted-foreground border-border hover:bg-muted"), children: opt.label }, opt.value));
42
+ }) }), _jsxs("div", { className: "flex items-center gap-6 flex-wrap text-sm", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: "include-expired", checked: includeExpired, onCheckedChange: setIncludeExpired }), _jsx(Label, { htmlFor: "include-expired", className: "cursor-pointer", children: "Toon be\u00EBindigde relaties" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: "hide-unknown", checked: hideUnknown, onCheckedChange: setHideUnknown }), _jsx(Label, { htmlFor: "hide-unknown", className: "cursor-pointer", children: "Verberg organisaties zonder naam" }), hideUnknown && hiddenCount > 0 ? (_jsxs("span", { className: "text-muted-foreground", children: ["(", hiddenCount, " verborgen)"] })) : null] })] }), _jsx("div", { className: "text-xs text-muted-foreground", children: isLoading ? ("Laden…") : (_jsxs(_Fragment, { children: [_jsx("span", { className: "font-medium text-foreground", children: orgs.length.toLocaleString("nl-NL") }), " ", "weergegeven van", " ", _jsx("span", { className: "font-medium text-foreground", children: totalMatching.toLocaleString("nl-NL") }), " ", "organisaties \u00B7 bron: Floriday", truncated && (_jsx("span", { className: "ml-1 text-warning", children: "(verfijn de zoekopdracht om alles te zien)" }))] })) }), _jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Organisatie" }), _jsx(TableHead, { children: "GLN" }), _jsx(TableHead, { children: "Adres" }), _jsx(TableHead, { children: "Land" }), _jsx(TableHead, { children: "Type" }), _jsx(TableHead, { children: "Bijgewerkt" })] }) }), _jsx(TableBody, { children: isLoading ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 6, className: "text-center py-8 text-muted-foreground", children: "Laden..." }) })) : orgs.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 6, className: "text-center py-8 text-muted-foreground", children: "Geen organisaties gevonden." }) })) : (orgs.map((o) => {
43
+ const displayName = o.organization_name && o.organization_name.trim() !== ""
44
+ ? o.organization_name
45
+ : null;
46
+ const addressBits = [
47
+ o.address_line,
48
+ [o.postal_code, o.city].filter(Boolean).join(" "),
49
+ ].filter((x) => x && String(x).trim() !== "");
50
+ return (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "py-3", children: _jsxs("div", { className: "flex items-center gap-3 min-w-0", children: [_jsx(CustomerAvatar, { logoUrl: o.logo_url, name: displayName ?? "?", size: "sm" }), _jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "font-medium truncate flex items-center gap-2", children: [displayName ? (_jsx("span", { className: "truncate", children: displayName })) : (_jsx("span", { className: "italic text-muted-foreground", children: "Onbekende organisatie" })), o.is_expired && (_jsx(Badge, { variant: "outline", className: "text-[10px] uppercase tracking-wide", children: "be\u00EBindigd" }))] }), o.commercial_name && (_jsx("div", { className: "text-xs text-muted-foreground truncate", children: o.commercial_name })), o.website && (_jsxs("a", { href: o.website.startsWith("http") ? o.website : `https://${o.website}`, target: "_blank", rel: "noreferrer", className: "text-xs text-muted-foreground inline-flex items-center gap-1 hover:underline", children: [_jsx(Globe, { className: "h-3 w-3" }), " ", o.website] }))] })] }) }), _jsx(TableCell, { children: o.gln ? _jsx(Badge, { variant: "outline", children: o.gln }) : "—" }), _jsx(TableCell, { className: "text-sm", children: addressBits.length ? (_jsx("div", { className: "min-w-0", children: _jsxs("div", { className: "inline-flex items-start gap-1 text-muted-foreground", children: [_jsx(MapPin, { className: "h-3 w-3 mt-0.5 shrink-0" }), _jsx("span", { className: "truncate", children: addressBits.join(", ") })] }) })) : ("—") }), _jsx(TableCell, { children: o.country ?? "—" }), _jsx(TableCell, { className: "text-xs text-muted-foreground uppercase tracking-wide", children: o.organization_type ?? "—" }), _jsx(TableCell, { className: "text-muted-foreground text-sm", children: new Date(o.fetched_at).toLocaleDateString("nl-NL") })] }, `${o.connection_id}-${o.organization_id}`));
51
+ })) })] }) }) })] }));
23
52
  }
@@ -1 +1 @@
1
- {"version":3,"file":"NieuweKlant.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/NieuweKlant.tsx"],"names":[],"mappings":"AAgBA,wBAAgB,WAAW,gCA4H1B"}
1
+ {"version":3,"file":"NieuweKlant.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/NieuweKlant.tsx"],"names":[],"mappings":"AAgBA,wBAAgB,WAAW,gCAiN1B"}