@flowselections/layout-maker 1.3.2

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 (145) hide show
  1. package/README.md +24 -0
  2. package/dist-lib/_core-safelist.d.ts +2 -0
  3. package/dist-lib/_core-safelist.d.ts.map +1 -0
  4. package/dist-lib/_core-safelist.js +18 -0
  5. package/dist-lib/components/AfdrukKnop.d.ts +13 -0
  6. package/dist-lib/components/AfdrukKnop.d.ts.map +1 -0
  7. package/dist-lib/components/AfdrukKnop.js +38 -0
  8. package/dist-lib/components/AiLayoutImportDialog.d.ts +6 -0
  9. package/dist-lib/components/AiLayoutImportDialog.d.ts.map +1 -0
  10. package/dist-lib/components/AiLayoutImportDialog.js +133 -0
  11. package/dist-lib/components/BlokOntwerperPage.d.ts +5 -0
  12. package/dist-lib/components/BlokOntwerperPage.d.ts.map +1 -0
  13. package/dist-lib/components/BlokOntwerperPage.js +67 -0
  14. package/dist-lib/components/DocumentSettingsPanel.d.ts +13 -0
  15. package/dist-lib/components/DocumentSettingsPanel.d.ts.map +1 -0
  16. package/dist-lib/components/DocumentSettingsPanel.js +65 -0
  17. package/dist-lib/components/FormaatOrientatieBar.d.ts +10 -0
  18. package/dist-lib/components/FormaatOrientatieBar.d.ts.map +1 -0
  19. package/dist-lib/components/FormaatOrientatieBar.js +51 -0
  20. package/dist-lib/components/LayoutsPage.d.ts +2 -0
  21. package/dist-lib/components/LayoutsPage.d.ts.map +1 -0
  22. package/dist-lib/components/LayoutsPage.js +196 -0
  23. package/dist-lib/components/LogoBibliotheekDialog.d.ts +7 -0
  24. package/dist-lib/components/LogoBibliotheekDialog.d.ts.map +1 -0
  25. package/dist-lib/components/LogoBibliotheekDialog.js +27 -0
  26. package/dist-lib/components/LogosPage.d.ts +7 -0
  27. package/dist-lib/components/LogosPage.d.ts.map +1 -0
  28. package/dist-lib/components/LogosPage.js +131 -0
  29. package/dist-lib/components/PaginaDocument.d.ts +22 -0
  30. package/dist-lib/components/PaginaDocument.d.ts.map +1 -0
  31. package/dist-lib/components/PaginaDocument.js +80 -0
  32. package/dist-lib/components/PakbonBuilderPage.d.ts +5 -0
  33. package/dist-lib/components/PakbonBuilderPage.d.ts.map +1 -0
  34. package/dist-lib/components/PakbonBuilderPage.js +69 -0
  35. package/dist-lib/components/PakbonPreview.d.ts +13 -0
  36. package/dist-lib/components/PakbonPreview.d.ts.map +1 -0
  37. package/dist-lib/components/PakbonPreview.js +423 -0
  38. package/dist-lib/components/PakbonSectiesPanel.d.ts +18 -0
  39. package/dist-lib/components/PakbonSectiesPanel.d.ts.map +1 -0
  40. package/dist-lib/components/PakbonSectiesPanel.js +156 -0
  41. package/dist-lib/components/PreviewFit.d.ts +9 -0
  42. package/dist-lib/components/PreviewFit.d.ts.map +1 -0
  43. package/dist-lib/components/PreviewFit.js +38 -0
  44. package/dist-lib/components/PrintQueuePage.d.ts +2 -0
  45. package/dist-lib/components/PrintQueuePage.d.ts.map +1 -0
  46. package/dist-lib/components/PrintQueuePage.js +59 -0
  47. package/dist-lib/components/PrinterSetupWizard.d.ts +6 -0
  48. package/dist-lib/components/PrinterSetupWizard.d.ts.map +1 -0
  49. package/dist-lib/components/PrinterSetupWizard.js +119 -0
  50. package/dist-lib/components/PrintersPage.d.ts +2 -0
  51. package/dist-lib/components/PrintersPage.d.ts.map +1 -0
  52. package/dist-lib/components/PrintersPage.js +60 -0
  53. package/dist-lib/components/ProductiebonBlokkenPanel.d.ts +7 -0
  54. package/dist-lib/components/ProductiebonBlokkenPanel.d.ts.map +1 -0
  55. package/dist-lib/components/ProductiebonBlokkenPanel.js +85 -0
  56. package/dist-lib/components/ProductiebonBuilderPage.d.ts +5 -0
  57. package/dist-lib/components/ProductiebonBuilderPage.d.ts.map +1 -0
  58. package/dist-lib/components/ProductiebonBuilderPage.js +48 -0
  59. package/dist-lib/components/ProductiebonPreview.d.ts +12 -0
  60. package/dist-lib/components/ProductiebonPreview.d.ts.map +1 -0
  61. package/dist-lib/components/ProductiebonPreview.js +237 -0
  62. package/dist-lib/components/TemplateModuleLogo.d.ts +6 -0
  63. package/dist-lib/components/TemplateModuleLogo.d.ts.map +1 -0
  64. package/dist-lib/components/TemplateModuleLogo.js +4 -0
  65. package/dist-lib/components/settings/MijnModuleSettingsCard.d.ts +2 -0
  66. package/dist-lib/components/settings/MijnModuleSettingsCard.d.ts.map +1 -0
  67. package/dist-lib/components/settings/MijnModuleSettingsCard.js +42 -0
  68. package/dist-lib/hooks/useMijnModuleData.d.ts +13 -0
  69. package/dist-lib/hooks/useMijnModuleData.d.ts.map +1 -0
  70. package/dist-lib/hooks/useMijnModuleData.js +49 -0
  71. package/dist-lib/index.d.ts +38 -0
  72. package/dist-lib/index.d.ts.map +1 -0
  73. package/dist-lib/index.js +56 -0
  74. package/dist-lib/integrations/supabase/auth-attacher.d.ts +2 -0
  75. package/dist-lib/integrations/supabase/auth-attacher.d.ts.map +1 -0
  76. package/dist-lib/integrations/supabase/auth-attacher.js +12 -0
  77. package/dist-lib/integrations/supabase/auth-middleware.d.ts +19847 -0
  78. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -0
  79. package/dist-lib/integrations/supabase/auth-middleware.js +52 -0
  80. package/dist-lib/integrations/supabase/client.d.ts +2 -0
  81. package/dist-lib/integrations/supabase/client.d.ts.map +1 -0
  82. package/dist-lib/integrations/supabase/client.js +3 -0
  83. package/dist-lib/integrations/supabase/client.server.d.ts +19843 -0
  84. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -0
  85. package/dist-lib/integrations/supabase/client.server.js +30 -0
  86. package/dist-lib/integrations/supabase/types.d.ts +20423 -0
  87. package/dist-lib/integrations/supabase/types.d.ts.map +1 -0
  88. package/dist-lib/integrations/supabase/types.js +78 -0
  89. package/dist-lib/lib/aiLayout.functions.d.ts +19864 -0
  90. package/dist-lib/lib/aiLayout.functions.d.ts.map +1 -0
  91. package/dist-lib/lib/aiLayout.functions.js +28 -0
  92. package/dist-lib/lib/aiLayout.server.d.ts +14 -0
  93. package/dist-lib/lib/aiLayout.server.d.ts.map +1 -0
  94. package/dist-lib/lib/aiLayout.server.js +197 -0
  95. package/dist-lib/lib/browserPrint.d.ts +38 -0
  96. package/dist-lib/lib/browserPrint.d.ts.map +1 -0
  97. package/dist-lib/lib/browserPrint.js +104 -0
  98. package/dist-lib/lib/documentProfiles.d.ts +50 -0
  99. package/dist-lib/lib/documentProfiles.d.ts.map +1 -0
  100. package/dist-lib/lib/documentProfiles.js +458 -0
  101. package/dist-lib/lib/documentTypes.d.ts +123 -0
  102. package/dist-lib/lib/documentTypes.d.ts.map +1 -0
  103. package/dist-lib/lib/documentTypes.js +74 -0
  104. package/dist-lib/lib/fieldRegistry.d.ts +20 -0
  105. package/dist-lib/lib/fieldRegistry.d.ts.map +1 -0
  106. package/dist-lib/lib/fieldRegistry.js +123 -0
  107. package/dist-lib/lib/formaatPrinters.d.ts +14 -0
  108. package/dist-lib/lib/formaatPrinters.d.ts.map +1 -0
  109. package/dist-lib/lib/formaatPrinters.js +39 -0
  110. package/dist-lib/lib/layoutPreview.d.ts +18 -0
  111. package/dist-lib/lib/layoutPreview.d.ts.map +1 -0
  112. package/dist-lib/lib/layoutPreview.js +134 -0
  113. package/dist-lib/lib/logoLibrary.d.ts +80 -0
  114. package/dist-lib/lib/logoLibrary.d.ts.map +1 -0
  115. package/dist-lib/lib/logoLibrary.js +264 -0
  116. package/dist-lib/lib/pakbonBuilder.d.ts +210 -0
  117. package/dist-lib/lib/pakbonBuilder.d.ts.map +1 -0
  118. package/dist-lib/lib/pakbonBuilder.js +910 -0
  119. package/dist-lib/lib/pakbonHtml.d.ts +48 -0
  120. package/dist-lib/lib/pakbonHtml.d.ts.map +1 -0
  121. package/dist-lib/lib/pakbonHtml.js +754 -0
  122. package/dist-lib/lib/printApi.d.ts +45 -0
  123. package/dist-lib/lib/printApi.d.ts.map +1 -0
  124. package/dist-lib/lib/printApi.js +98 -0
  125. package/dist-lib/lib/productiebonBuilder.d.ts +137 -0
  126. package/dist-lib/lib/productiebonBuilder.d.ts.map +1 -0
  127. package/dist-lib/lib/productiebonBuilder.js +661 -0
  128. package/dist-lib/lib/supabaseClient.d.ts +4 -0
  129. package/dist-lib/lib/supabaseClient.d.ts.map +1 -0
  130. package/dist-lib/lib/supabaseClient.js +10 -0
  131. package/dist-lib/lib/useDocumentData.d.ts +97 -0
  132. package/dist-lib/lib/useDocumentData.d.ts.map +1 -0
  133. package/dist-lib/lib/useDocumentData.js +286 -0
  134. package/dist-lib/lib/useLayoutIdParam.d.ts +7 -0
  135. package/dist-lib/lib/useLayoutIdParam.d.ts.map +1 -0
  136. package/dist-lib/lib/useLayoutIdParam.js +10 -0
  137. package/dist-lib/lib/utils.d.ts +3 -0
  138. package/dist-lib/lib/utils.d.ts.map +1 -0
  139. package/dist-lib/lib/utils.js +5 -0
  140. package/dist-lib/lib/validationSchemas.d.ts +15 -0
  141. package/dist-lib/lib/validationSchemas.d.ts.map +1 -0
  142. package/dist-lib/lib/validationSchemas.js +25 -0
  143. package/dist-lib/styles.css +1 -0
  144. package/package.json +117 -0
  145. package/public/logo.png +0 -0
@@ -0,0 +1,196 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useMemo, useState } from "react";
3
+ import { useNavigate } from "@tanstack/react-router";
4
+ import { Badge, Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Separator, Switch, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Textarea, toast, } from "@flowselections/core";
5
+ import { Copy, FileText, Images, Pencil, Plus, Search, Settings2, Trash2 } from "lucide-react";
6
+ import { dupliceerLayout, maakLayout, updateLayout, useLayouts, verwijderLayout, } from "../lib/useDocumentData";
7
+ import { DOCUMENT_TYPES, PAPIERFORMATEN, documentTypeLabel } from "../lib/documentTypes";
8
+ import { DOCUMENT_PROFIELEN, documentProfiel, primaireProfielen, standaardConfig, } from "../lib/documentProfiles";
9
+ function klantOntwerpLayout(l) {
10
+ if (l.documenttype !== "sticker")
11
+ return false;
12
+ const cfg = (l.config ?? {});
13
+ return Boolean(cfg?.klantlayout?.gebruik_klantlayout);
14
+ }
15
+ export function LayoutsPage() {
16
+ const { layouts, loading, reload } = useLayouts();
17
+ const navigate = useNavigate();
18
+ const [zoek, setZoek] = useState("");
19
+ const [typeFilter, setTypeFilter] = useState("alle");
20
+ const [statusFilter, setStatusFilter] = useState("alle");
21
+ const [wizardOpen, setWizardOpen] = useState(false);
22
+ const [stap, setStap] = useState(1);
23
+ const [naam, setNaam] = useState("");
24
+ const [documenttype, setDocumenttype] = useState("pakbon");
25
+ const [omschrijving, setOmschrijving] = useState("");
26
+ const [formaatCode, setFormaatCode] = useState("A4");
27
+ const [orientatie, setOrientatie] = useState("portrait");
28
+ const [klantLayout, setKlantLayout] = useState(false);
29
+ const [bezig, setBezig] = useState(false);
30
+ const klantOntwerp = documenttype === "sticker" && klantLayout;
31
+ // Bewerken van een bestaande layout
32
+ const [bewerkId, setBewerkId] = useState(null);
33
+ const [bNaam, setBNaam] = useState("");
34
+ const [bType, setBType] = useState("pakbon");
35
+ const [bOms, setBOms] = useState("");
36
+ const [bFormaat, setBFormaat] = useState("A4");
37
+ const [bOrientatie, setBOrientatie] = useState("portrait");
38
+ const [bBezig, setBBezig] = useState(false);
39
+ const openBewerken = (l) => {
40
+ setBewerkId(l.id);
41
+ setBNaam(l.naam ?? "");
42
+ setBType(l.documenttype);
43
+ setBOms(l.omschrijving ?? "");
44
+ setBFormaat(l.papierformaat ?? "A4");
45
+ setBOrientatie(l.orientatie === "landscape" ? "landscape" : "portrait");
46
+ };
47
+ const bewaarBewerking = async () => {
48
+ if (!bewerkId)
49
+ return;
50
+ if (!bNaam.trim()) {
51
+ toast.error("Geef de layout een naam");
52
+ return;
53
+ }
54
+ const profiel = documentProfiel(bType);
55
+ const formaat = profiel.formaten.find((f) => f.code === bFormaat) ??
56
+ PAPIERFORMATEN.find((f) => f.code === bFormaat) ??
57
+ profiel.formaten[0];
58
+ setBBezig(true);
59
+ try {
60
+ await updateLayout(bewerkId, {
61
+ naam: bNaam.trim(),
62
+ documenttype: bType,
63
+ omschrijving: bOms.trim(),
64
+ papierformaat: formaat.code,
65
+ breedte_mm: formaat.breedte,
66
+ hoogte_mm: formaat.hoogte,
67
+ orientatie: bOrientatie,
68
+ });
69
+ toast.success("Layout bijgewerkt");
70
+ setBewerkId(null);
71
+ void reload();
72
+ }
73
+ catch {
74
+ toast.error("Opslaan mislukt");
75
+ }
76
+ finally {
77
+ setBBezig(false);
78
+ }
79
+ };
80
+ const gefilterd = useMemo(() => layouts.filter((l) => {
81
+ const q = zoek.trim().toLowerCase();
82
+ if (q && !`${l.naam} ${l.omschrijving ?? ""}`.toLowerCase().includes(q))
83
+ return false;
84
+ if (typeFilter !== "alle" && l.documenttype !== typeFilter)
85
+ return false;
86
+ if (statusFilter === "actief" && !l.actief)
87
+ return false;
88
+ if (statusFilter === "inactief" && l.actief)
89
+ return false;
90
+ return true;
91
+ }), [layouts, zoek, typeFilter, statusFilter]);
92
+ const aanmaken = async () => {
93
+ if (!naam.trim()) {
94
+ toast.error("Geef de layout een naam");
95
+ return;
96
+ }
97
+ const profiel = documentProfiel(documenttype);
98
+ const formaat = profiel.formaten.find((f) => f.code === formaatCode) ??
99
+ PAPIERFORMATEN.find((f) => f.code === formaatCode) ??
100
+ profiel.formaten[0];
101
+ setBezig(true);
102
+ try {
103
+ const id = await maakLayout({
104
+ naam: naam.trim(),
105
+ documenttype,
106
+ omschrijving: omschrijving.trim(),
107
+ papierformaat: formaat.code,
108
+ breedte_mm: formaat.breedte,
109
+ hoogte_mm: formaat.hoogte,
110
+ orientatie,
111
+ config: (() => {
112
+ const c = standaardConfig(profiel);
113
+ if (documenttype === "sticker") {
114
+ c.klantlayout = { ...(c.klantlayout ?? {}), gebruik_klantlayout: klantLayout };
115
+ }
116
+ return c;
117
+ })(),
118
+ });
119
+ toast.success("Layout aangemaakt");
120
+ setWizardOpen(false);
121
+ setStap(1);
122
+ setNaam("");
123
+ setOmschrijving("");
124
+ const wasKlantOntwerp = klantOntwerp;
125
+ setKlantLayout(false);
126
+ void reload();
127
+ if (wasKlantOntwerp)
128
+ return;
129
+ void navigate(documenttype === "productiebon"
130
+ ? { to: "/layouts-printers/productiebon/$layoutId", params: { layoutId: id } }
131
+ : { to: "/layouts-printers/pakbon/$layoutId", params: { layoutId: id } });
132
+ }
133
+ catch {
134
+ toast.error("Aanmaken mislukt");
135
+ }
136
+ finally {
137
+ setBezig(false);
138
+ }
139
+ };
140
+ return (_jsxs("main", { className: "p-6 space-y-6", children: [_jsxs("div", { className: "flex flex-wrap items-end justify-between gap-4", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-xl font-semibold", children: "Documentlayouts" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Ontwerp pakbonnen, facturen, etiketten en alle andere documenten." })] }), _jsxs("div", { className: "flex gap-2", children: [_jsxs(Button, { variant: "outline", onClick: () => navigate({ to: "/layouts-printers/logos" }), children: [_jsx(Images, { className: "mr-2 h-4 w-4" }), " Logo's"] }), _jsxs(Button, { onClick: () => setWizardOpen(true), children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), " Nieuwe layout"] })] })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "pb-3", children: [_jsx(CardTitle, { className: "text-base", children: "Overzicht" }), _jsx(CardDescription, { children: loading ? "Laden…" : `${gefilterd.length} van ${layouts.length} layouts` })] }), _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { className: "grid gap-3 md:grid-cols-4", children: [_jsxs("div", { className: "relative md:col-span-2", children: [_jsx(Search, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { className: "pl-9", placeholder: "Zoek op naam of omschrijving", value: zoek, onChange: (e) => setZoek(e.target.value) })] }), _jsxs(Select, { value: typeFilter, onValueChange: setTypeFilter, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Documenttype" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "alle", children: "Alle documenttypes" }), DOCUMENT_TYPES.map((d) => (_jsx(SelectItem, { value: d.value, children: d.label }, d.value)))] })] }), _jsxs(Select, { value: statusFilter, onValueChange: setStatusFilter, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Status" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "alle", children: "Alle statussen" }), _jsx(SelectItem, { value: "actief", children: "Actief" }), _jsx(SelectItem, { value: "inactief", children: "Inactief" })] })] })] }), _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Naam" }), _jsx(TableHead, { children: "Documenttype" }), _jsx(TableHead, { children: "Formaat" }), _jsx(TableHead, { children: "Versie" }), _jsx(TableHead, { children: "Actief" }), _jsx(TableHead, { className: "text-right", children: "Acties" })] }) }), _jsxs(TableBody, { children: [gefilterd.length === 0 && (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 6, className: "py-10 text-center text-sm text-muted-foreground", children: "Geen layouts gevonden." }) })), gefilterd.map((l) => (_jsxs(TableRow, { children: [_jsxs(TableCell, { children: [_jsxs("div", { className: "flex items-center gap-2 font-medium", children: [_jsx(FileText, { className: "h-4 w-4 text-muted-foreground" }), l.naam, l.is_standaard && _jsx(Badge, { variant: "secondary", children: "Standaard" })] }), l.omschrijving && (_jsx("span", { className: "text-xs text-muted-foreground", children: l.omschrijving }))] }), _jsx(TableCell, { children: documentTypeLabel(l.documenttype) }), _jsxs(TableCell, { className: "text-sm text-muted-foreground", children: [l.papierformaat, " \u00B7 ", Number(l.breedte_mm), " \u00D7 ", Number(l.hoogte_mm), " mm"] }), _jsxs(TableCell, { children: ["v", l.huidige_versie] }), _jsx(TableCell, { children: _jsx(Switch, { checked: l.actief, onCheckedChange: async (v) => {
141
+ await updateLayout(l.id, { actief: v });
142
+ void reload();
143
+ } }) }), _jsx(TableCell, { className: "text-right", children: _jsxs("div", { className: "flex justify-end gap-1", children: [klantOntwerpLayout(l) ? (_jsx(Badge, { variant: "outline", children: "Ontwerp van klant" })) : (_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigate(l.documenttype === "productiebon"
144
+ ? {
145
+ to: "/layouts-printers/productiebon/$layoutId",
146
+ params: { layoutId: l.id },
147
+ }
148
+ : { to: "/layouts-printers/pakbon/$layoutId", params: { layoutId: l.id } }), children: [_jsx(Pencil, { className: "mr-2 h-4 w-4" }), " Ontwerpen"] })), _jsx(Button, { variant: "ghost", size: "sm", title: "Naam, documenttype en formaat wijzigen", onClick: () => openBewerken(l), children: _jsx(Settings2, { className: "h-4 w-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: async () => {
149
+ try {
150
+ await dupliceerLayout(l.id);
151
+ toast.success("Layout gedupliceerd");
152
+ void reload();
153
+ }
154
+ catch {
155
+ toast.error("Dupliceren mislukt");
156
+ }
157
+ }, children: _jsx(Copy, { className: "h-4 w-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: async () => {
158
+ try {
159
+ await verwijderLayout(l.id);
160
+ toast.success("Layout verwijderd");
161
+ void reload();
162
+ }
163
+ catch {
164
+ toast.error("Verwijderen mislukt");
165
+ }
166
+ }, children: _jsx(Trash2, { className: "h-4 w-4" }) })] }) })] }, l.id)))] })] })] })] }), _jsx(Dialog, { open: wizardOpen, onOpenChange: (v) => {
167
+ setWizardOpen(v);
168
+ if (!v)
169
+ setStap(1);
170
+ }, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: stap === 1 ? "Welk document wil je ontwerpen?" : "Nieuwe layout" }), _jsx(DialogDescription, { children: stap === 1
171
+ ? "Iedere documentsoort heeft eigen velden, instellingen en printregels."
172
+ : `${documentProfiel(documenttype).label} — kies naam en formaat.` })] }), stap === 1 ? (_jsxs("div", { className: "space-y-4", children: [_jsx("div", { className: "grid gap-3 sm:grid-cols-2", children: primaireProfielen().map((p) => (_jsxs("button", { type: "button", onClick: () => {
173
+ setDocumenttype(p.id);
174
+ setFormaatCode(p.standaardFormaat);
175
+ setOrientatie(p.standaardOrientatie);
176
+ setStap(2);
177
+ }, className: "rounded-md border p-4 text-left transition hover:border-primary hover:bg-accent", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(FileText, { className: "h-4 w-4 text-muted-foreground" }), _jsx("span", { className: "font-medium", children: p.label })] }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: p.omschrijving })] }, p.id))) }), _jsx(Separator, {}), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Andere documentsoort" }), _jsxs(Select, { value: documenttype, onValueChange: (v) => {
178
+ const p = documentProfiel(v);
179
+ setDocumenttype(v);
180
+ setFormaatCode(p.standaardFormaat);
181
+ setOrientatie(p.standaardOrientatie);
182
+ setStap(2);
183
+ }, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies documentsoort" }) }), _jsx(SelectContent, { children: DOCUMENT_PROFIELEN.filter((p) => !p.primair).map((p) => (_jsx(SelectItem, { value: p.id, children: p.label }, p.id))) })] })] })] })) : (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "l-naam", children: "Naam" }), _jsx(Input, { id: "l-naam", value: naam, onChange: (e) => setNaam(e.target.value), placeholder: `${documentProfiel(documenttype).label} 2026` })] }), documenttype === "sticker" && (_jsxs("div", { className: "flex items-start justify-between gap-4 rounded-lg border p-3", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "l-klantlayout", children: "Kies altijd de layout van de klant" }), _jsx("p", { className: "text-xs text-muted-foreground", children: "De klant levert het ontwerp aan. Formaat en ontwerpen zijn dan niet nodig." })] }), _jsx(Switch, { id: "l-klantlayout", checked: klantLayout, onCheckedChange: setKlantLayout })] })), !klantOntwerp && (_jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Formaat" }), _jsxs(Select, { value: formaatCode, onValueChange: setFormaatCode, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: documentProfiel(documenttype).formaten.map((f) => (_jsx(SelectItem, { value: f.code, children: f.label }, f.code))) })] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Ori\u00EBntatie" }), _jsxs(Select, { value: orientatie, onValueChange: (v) => setOrientatie(v), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "portrait", children: "Staand" }), _jsx(SelectItem, { value: "landscape", children: "Liggend" })] })] })] })] })), _jsx(Separator, {}), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "l-oms", children: "Omschrijving" }), _jsx(Textarea, { id: "l-oms", value: omschrijving, onChange: (e) => setOmschrijving(e.target.value), placeholder: "Waar wordt dit document voor gebruikt?" })] })] })), _jsx(DialogFooter, { children: stap === 2 ? (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "outline", onClick: () => setStap(1), children: "Vorige" }), _jsx(Button, { onClick: aanmaken, disabled: bezig, children: klantOntwerp ? "Aanmaken" : "Aanmaken en ontwerpen" })] })) : (_jsx(Button, { variant: "outline", onClick: () => setWizardOpen(false), children: "Annuleren" })) })] }) }), _jsx(Dialog, { open: bewerkId !== null, onOpenChange: (v) => !v && setBewerkId(null), children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Layout wijzigen" }), _jsx(DialogDescription, { children: "Pas de naam, het documenttype en het formaat aan." })] }), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "b-naam", children: "Naam" }), _jsx(Input, { id: "b-naam", value: bNaam, onChange: (e) => setBNaam(e.target.value) })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Documenttype" }), _jsxs(Select, { value: bType, onValueChange: (v) => {
184
+ const p = documentProfiel(v);
185
+ setBType(v);
186
+ if (!p.formaten.some((f) => f.code === bFormaat)) {
187
+ setBFormaat(p.standaardFormaat);
188
+ }
189
+ }, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: DOCUMENT_PROFIELEN.map((p) => (_jsx(SelectItem, { value: p.id, children: p.label }, p.id))) })] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Formaat" }), _jsxs(Select, { value: bFormaat, onValueChange: setBFormaat, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: (() => {
190
+ const lijst = documentProfiel(bType).formaten;
191
+ const extra = lijst.some((f) => f.code === bFormaat)
192
+ ? []
193
+ : PAPIERFORMATEN.filter((f) => f.code === bFormaat);
194
+ return [...lijst, ...extra].map((f) => (_jsx(SelectItem, { value: f.code, children: f.label }, f.code)));
195
+ })() })] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Ori\u00EBntatie" }), _jsxs(Select, { value: bOrientatie, onValueChange: (v) => setBOrientatie(v), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "portrait", children: "Staand" }), _jsx(SelectItem, { value: "landscape", children: "Liggend" })] })] })] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "b-oms", children: "Omschrijving" }), _jsx(Textarea, { id: "b-oms", value: bOms, onChange: (e) => setBOms(e.target.value) })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setBewerkId(null), children: "Annuleren" }), _jsx(Button, { onClick: bewaarBewerking, disabled: bBezig, children: "Opslaan" })] })] }) })] }));
196
+ }
@@ -0,0 +1,7 @@
1
+ import { type LogoModel } from "../lib/logoLibrary";
2
+ export declare function LogoBibliotheekDialog({ open, onOpenChange, onKies, }: {
3
+ open: boolean;
4
+ onOpenChange: (v: boolean) => void;
5
+ onKies: (logo: LogoModel) => void;
6
+ }): import("react").JSX.Element;
7
+ //# sourceMappingURL=LogoBibliotheekDialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LogoBibliotheekDialog.d.ts","sourceRoot":"","sources":["../../src/components/LogoBibliotheekDialog.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAAkD,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAGpG,wBAAgB,qBAAqB,CAAC,EACpC,IAAI,EACJ,YAAY,EACZ,MAAM,GACP,EAAE;IACD,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,MAAM,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAC;CACnC,+BAgGA"}
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo, useState } from "react";
3
+ import { Badge, Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@flowselections/core";
4
+ import { LOGO_CATEGORIEEN, logoCategorieLabel, useLogos } from "../lib/logoLibrary";
5
+ import { LogoThumbnail } from "./LogosPage";
6
+ export function LogoBibliotheekDialog({ open, onOpenChange, onKies, }) {
7
+ const { logos, loading } = useLogos();
8
+ const [zoek, setZoek] = useState("");
9
+ const [categorie, setCategorie] = useState("alle");
10
+ const [gekozen, setGekozen] = useState(null);
11
+ const zichtbaar = useMemo(() => {
12
+ const q = zoek.trim().toLowerCase();
13
+ return logos.filter((l) => (categorie === "alle" || l.categorie === categorie) && (!q || l.naam.toLowerCase().includes(q)));
14
+ }, [logos, zoek, categorie]);
15
+ const bevestig = () => {
16
+ const logo = logos.find((l) => l.id === gekozen);
17
+ if (!logo)
18
+ return;
19
+ onKies(logo);
20
+ onOpenChange(false);
21
+ };
22
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-3xl", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Logo Bibliotheek" }), _jsx(DialogDescription, { children: "Kies een logo. Het element blijft gekoppeld aan de bibliotheek, dus wijzigingen werken automatisch door." })] }), _jsxs("div", { className: "flex flex-wrap gap-3", children: [_jsx(Input, { placeholder: "Zoek op naam\u2026", value: zoek, onChange: (e) => setZoek(e.target.value), className: "max-w-xs" }), _jsxs(Select, { value: categorie, onValueChange: setCategorie, children: [_jsx(SelectTrigger, { className: "w-48", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "alle", children: "Alle categorie\u00EBn" }), LOGO_CATEGORIEEN.map((c) => (_jsx(SelectItem, { value: c.value, children: c.label }, c.value)))] })] })] }), _jsxs("div", { className: "grid max-h-96 grid-cols-2 gap-3 overflow-auto sm:grid-cols-3", children: [loading && _jsx("p", { className: "text-sm text-muted-foreground", children: "Laden\u2026" }), !loading && zichtbaar.length === 0 && (_jsx("p", { className: "text-sm text-muted-foreground", children: "Nog geen logo's in de bibliotheek." })), zichtbaar.map((l) => (_jsxs("button", { type: "button", onClick: () => setGekozen(l.id), onDoubleClick: () => {
23
+ setGekozen(l.id);
24
+ onKies(l);
25
+ onOpenChange(false);
26
+ }, className: `flex flex-col items-start gap-2 rounded-md border p-3 text-left transition hover:bg-accent ${gekozen === l.id ? "border-primary ring-2 ring-primary" : ""}`, children: [_jsx(LogoThumbnail, { logo: l, className: "h-16 w-full" }), _jsx("span", { className: "text-sm font-medium", children: l.naam }), _jsxs("div", { className: "flex flex-wrap gap-1", children: [_jsx(Badge, { variant: "outline", children: logoCategorieLabel(l.categorie) }), _jsx(Badge, { variant: "secondary", className: "uppercase", children: l.bestandstype })] })] }, l.id)))] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), children: "Annuleren" }), _jsx(Button, { onClick: bevestig, disabled: !gekozen, children: "Logo gebruiken" })] })] }) }));
27
+ }
@@ -0,0 +1,7 @@
1
+ import { type LogoModel } from "../lib/logoLibrary";
2
+ export declare function LogoThumbnail({ logo, className }: {
3
+ logo: LogoModel;
4
+ className?: string;
5
+ }): import("react").JSX.Element;
6
+ export declare function LogosPage(): import("react").JSX.Element;
7
+ //# sourceMappingURL=LogosPage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LogosPage.d.ts","sourceRoot":"","sources":["../../src/components/LogosPage.tsx"],"names":[],"mappings":"AAiCA,OAAO,EAWL,KAAK,SAAS,EACf,MAAM,oBAAoB,CAAC;AAE5B,wBAAgB,aAAa,CAAC,EAAE,IAAI,EAAE,SAAc,EAAE,EAAE;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,+BAY9F;AASD,wBAAgB,SAAS,gCA4KxB"}
@@ -0,0 +1,131 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo, useState } from "react";
3
+ import { Badge, Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Textarea, toast, } from "@flowselections/core";
4
+ import { Download, ImagePlus, Pencil, RefreshCw, Trash2, Upload } from "lucide-react";
5
+ import { LOGO_CATEGORIEEN, TOEGESTANE_LOGO_TYPES, downloadLogo, formatteerBytes, logoCategorieLabel, maakLogo, useLogos, vervangLogoBestand, verwijderLogo, werkLogoBij, } from "../lib/logoLibrary";
6
+ export function LogoThumbnail({ logo, className = "" }) {
7
+ return (_jsx("div", { className: `flex h-12 w-16 items-center justify-center overflow-hidden rounded border bg-[repeating-conic-gradient(#e5e7eb_0%_25%,#fff_0%_50%)] bg-[length:10px_10px] ${className}`, children: logo.viewUrl ? (_jsx("img", { src: logo.viewUrl, alt: logo.naam, className: "max-h-full max-w-full object-contain" })) : (_jsx(ImagePlus, { className: "h-4 w-4 text-muted-foreground" })) }));
8
+ }
9
+ export function LogosPage() {
10
+ const { logos, loading, reload } = useLogos();
11
+ const [zoek, setZoek] = useState("");
12
+ const [categorie, setCategorie] = useState("alle");
13
+ const [uploadOpen, setUploadOpen] = useState(false);
14
+ const [bewerk, setBewerk] = useState(null);
15
+ const zichtbaar = useMemo(() => {
16
+ const q = zoek.trim().toLowerCase();
17
+ return logos.filter((l) => (categorie === "alle" || l.categorie === categorie) &&
18
+ (!q || l.naam.toLowerCase().includes(q) || (l.omschrijving ?? "").toLowerCase().includes(q)));
19
+ }, [logos, zoek, categorie]);
20
+ const vervang = async (logo, file) => {
21
+ try {
22
+ await vervangLogoBestand(logo, file);
23
+ toast.success("Logo vervangen — alle layouts zijn bijgewerkt");
24
+ await reload();
25
+ }
26
+ catch (e) {
27
+ toast.error(e instanceof Error ? e.message : "Vervangen mislukt");
28
+ }
29
+ };
30
+ const wis = async (logo) => {
31
+ if (!window.confirm(`Logo "${logo.naam}" verwijderen?`))
32
+ return;
33
+ try {
34
+ await verwijderLogo(logo);
35
+ toast.success("Logo verwijderd");
36
+ await reload();
37
+ }
38
+ catch {
39
+ toast.error("Verwijderen mislukt");
40
+ }
41
+ };
42
+ return (_jsxs("main", { className: "flex-1 space-y-6 p-6", children: [_jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex-row flex-wrap items-end justify-between gap-3 space-y-0", children: [_jsxs("div", { children: [_jsx(CardTitle, { children: "Logo Bibliotheek" }), _jsx(CardDescription, { children: "Centraal beheer van alle bedrijfslogo's. Layouts verwijzen naar deze bibliotheek, dus een vervangen logo werkt direct door in elke layout." })] }), _jsxs(Button, { onClick: () => setUploadOpen(true), children: [_jsx(Upload, { className: "mr-2 h-4 w-4" }), " Logo's uploaden"] })] }), _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { className: "flex flex-wrap gap-3", children: [_jsx(Input, { placeholder: "Zoek op naam of omschrijving\u2026", value: zoek, onChange: (e) => setZoek(e.target.value), className: "max-w-xs" }), _jsxs(Select, { value: categorie, onValueChange: setCategorie, children: [_jsx(SelectTrigger, { className: "w-52", children: _jsx(SelectValue, { placeholder: "Categorie" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "alle", children: "Alle categorie\u00EBn" }), LOGO_CATEGORIEEN.map((c) => (_jsx(SelectItem, { value: c.value, children: c.label }, c.value)))] })] })] }), _jsx("div", { className: "rounded-md border", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Voorbeeld" }), _jsx(TableHead, { children: "Naam" }), _jsx(TableHead, { children: "Omschrijving" }), _jsx(TableHead, { children: "Categorie" }), _jsx(TableHead, { children: "Formaat" }), _jsx(TableHead, { children: "Resolutie" }), _jsx(TableHead, { children: "Transparant" }), _jsx(TableHead, { children: "Toegevoegd" }), _jsx(TableHead, { children: "Gewijzigd" }), _jsx(TableHead, { className: "text-right", children: "Acties" })] }) }), _jsxs(TableBody, { children: [loading && (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 10, children: "Laden\u2026" }) })), !loading && zichtbaar.length === 0 && (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 10, className: "text-muted-foreground", children: "Nog geen logo's gevonden." }) })), zichtbaar.map((l) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx(LogoThumbnail, { logo: l }) }), _jsxs(TableCell, { className: "font-medium", children: [l.naam, l.is_standaard && (_jsx(Badge, { variant: "secondary", className: "ml-2", children: "Standaard" }))] }), _jsx(TableCell, { className: "max-w-56 truncate text-muted-foreground", children: l.omschrijving }), _jsx(TableCell, { children: logoCategorieLabel(l.categorie) }), _jsx(TableCell, { className: "uppercase", children: l.bestandstype }), _jsxs(TableCell, { children: [l.breedte && l.hoogte ? `${l.breedte} × ${l.hoogte} px` : "—", _jsx("div", { className: "text-xs text-muted-foreground", children: formatteerBytes(l.bestandsgrootte) })] }), _jsx(TableCell, { children: l.transparant ? "Ja" : "Nee" }), _jsx(TableCell, { children: new Date(l.created_at).toLocaleDateString("nl-NL") }), _jsx(TableCell, { children: new Date(l.updated_at).toLocaleDateString("nl-NL") }), _jsx(TableCell, { children: _jsxs("div", { className: "flex justify-end gap-1", children: [_jsx(Button, { variant: "ghost", size: "icon", title: "Bewerken", onClick: () => setBewerk(l), children: _jsx(Pencil, { className: "h-4 w-4" }) }), _jsxs("label", { title: "Vervangen", children: [_jsx("input", { type: "file", className: "hidden", accept: TOEGESTANE_LOGO_TYPES.join(","), onChange: (e) => {
43
+ const f = e.target.files?.[0];
44
+ if (f)
45
+ void vervang(l, f);
46
+ e.target.value = "";
47
+ } }), _jsx("span", { className: "inline-flex h-9 w-9 cursor-pointer items-center justify-center rounded-md hover:bg-accent", children: _jsx(RefreshCw, { className: "h-4 w-4" }) })] }), _jsx(Button, { variant: "ghost", size: "icon", title: "Downloaden", onClick: () => void downloadLogo(l), children: _jsx(Download, { className: "h-4 w-4" }) }), _jsx(Button, { variant: "ghost", size: "icon", title: "Verwijderen", onClick: () => void wis(l), children: _jsx(Trash2, { className: "h-4 w-4" }) })] }) })] }, l.id)))] })] }) })] })] }), _jsx(LogoUploadDialog, { open: uploadOpen, onOpenChange: setUploadOpen, onKlaar: reload }), _jsx(LogoBewerkDialog, { logo: bewerk, onOpenChange: () => setBewerk(null), onKlaar: reload })] }));
48
+ }
49
+ function LogoUploadDialog({ open, onOpenChange, onKlaar, }) {
50
+ const [bestanden, setBestanden] = useState([]);
51
+ const [form, setForm] = useState({
52
+ naam: "",
53
+ omschrijving: "",
54
+ categorie: "hoofdlogo",
55
+ is_standaard: false,
56
+ });
57
+ const [bezig, setBezig] = useState(false);
58
+ const reset = () => {
59
+ setBestanden([]);
60
+ setForm({ naam: "", omschrijving: "", categorie: "hoofdlogo", is_standaard: false });
61
+ };
62
+ const upload = async () => {
63
+ if (!bestanden.length) {
64
+ toast.error("Kies eerst één of meer bestanden");
65
+ return;
66
+ }
67
+ setBezig(true);
68
+ let gelukt = 0;
69
+ for (const [i, file] of bestanden.entries()) {
70
+ const naam = bestanden.length === 1
71
+ ? form.naam.trim() || file.name.replace(/\.[^.]+$/, "")
72
+ : `${form.naam.trim() ? `${form.naam.trim()} — ` : ""}${file.name.replace(/\.[^.]+$/, "")}`;
73
+ try {
74
+ await maakLogo({
75
+ file,
76
+ naam,
77
+ omschrijving: form.omschrijving,
78
+ categorie: form.categorie,
79
+ is_standaard: form.is_standaard && i === 0,
80
+ });
81
+ gelukt += 1;
82
+ }
83
+ catch (e) {
84
+ toast.error(e instanceof Error ? e.message : `Upload van ${file.name} mislukt`);
85
+ }
86
+ }
87
+ setBezig(false);
88
+ if (gelukt) {
89
+ toast.success(`${gelukt} logo${gelukt === 1 ? "" : "'s"} toegevoegd`);
90
+ reset();
91
+ onOpenChange(false);
92
+ await onKlaar();
93
+ }
94
+ };
95
+ return (_jsx(Dialog, { open: open, onOpenChange: (v) => {
96
+ if (!v)
97
+ reset();
98
+ onOpenChange(v);
99
+ }, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Logo's uploaden" }), _jsx(DialogDescription, { children: "SVG, PNG, JPG en WebP worden ondersteund. Identieke bestanden worden automatisch herkend." })] }), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "logo-bestanden", children: "Bestanden" }), _jsx(Input, { id: "logo-bestanden", type: "file", multiple: true, accept: TOEGESTANE_LOGO_TYPES.join(","), onChange: (e) => setBestanden(Array.from(e.target.files ?? [])) }), bestanden.length > 1 && (_jsxs("p", { className: "text-xs text-muted-foreground", children: [bestanden.length, " bestanden \u2014 de naam van elk bestand wordt gebruikt."] }))] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "logo-naam", children: "Naam" }), _jsx(Input, { id: "logo-naam", value: form.naam, placeholder: "Bijv. Bedrijfslogo wit", onChange: (e) => setForm({ ...form, naam: e.target.value }) })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "logo-omschrijving", children: "Omschrijving" }), _jsx(Textarea, { id: "logo-omschrijving", rows: 2, value: form.omschrijving, onChange: (e) => setForm({ ...form, omschrijving: e.target.value }) })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Categorie" }), _jsxs(Select, { value: form.categorie, onValueChange: (v) => setForm({ ...form, categorie: v }), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: LOGO_CATEGORIEEN.map((c) => (_jsx(SelectItem, { value: c.value, children: c.label }, c.value))) })] })] }), _jsxs("div", { className: "flex items-center justify-between rounded-md border p-3", children: [_jsxs("div", { children: [_jsx(Label, { children: "Standaardlogo" }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Wordt voorgesteld bij nieuwe layouts." })] }), _jsx(Switch, { checked: form.is_standaard, onCheckedChange: (v) => setForm({ ...form, is_standaard: v }) })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), children: "Annuleren" }), _jsx(Button, { onClick: () => void upload(), disabled: bezig, children: bezig ? "Bezig…" : "Uploaden" })] })] }) }));
100
+ }
101
+ function LogoBewerkDialog({ logo, onOpenChange, onKlaar, }) {
102
+ const [form, setForm] = useState(null);
103
+ const huidig = form ??
104
+ (logo
105
+ ? {
106
+ naam: logo.naam,
107
+ omschrijving: logo.omschrijving ?? "",
108
+ categorie: logo.categorie,
109
+ is_standaard: logo.is_standaard,
110
+ }
111
+ : { naam: "", omschrijving: "", categorie: "overig", is_standaard: false });
112
+ const opslaan = async () => {
113
+ if (!logo)
114
+ return;
115
+ try {
116
+ await werkLogoBij(logo.id, huidig);
117
+ toast.success("Logo bijgewerkt");
118
+ setForm(null);
119
+ onOpenChange(false);
120
+ await onKlaar();
121
+ }
122
+ catch {
123
+ toast.error("Opslaan mislukt");
124
+ }
125
+ };
126
+ return (_jsx(Dialog, { open: !!logo, onOpenChange: (v) => {
127
+ if (!v)
128
+ setForm(null);
129
+ onOpenChange(v);
130
+ }, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Logo bewerken" }), _jsx(DialogDescription, { children: "Naam, omschrijving en categorie aanpassen." })] }), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "bw-naam", children: "Naam" }), _jsx(Input, { id: "bw-naam", value: huidig.naam, onChange: (e) => setForm({ ...huidig, naam: e.target.value }) })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "bw-oms", children: "Omschrijving" }), _jsx(Textarea, { id: "bw-oms", rows: 2, value: huidig.omschrijving, onChange: (e) => setForm({ ...huidig, omschrijving: e.target.value }) })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Categorie" }), _jsxs(Select, { value: huidig.categorie, onValueChange: (v) => setForm({ ...huidig, categorie: v }), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: LOGO_CATEGORIEEN.map((c) => (_jsx(SelectItem, { value: c.value, children: c.label }, c.value))) })] })] }), _jsxs("div", { className: "flex items-center justify-between rounded-md border p-3", children: [_jsx(Label, { children: "Standaardlogo" }), _jsx(Switch, { checked: huidig.is_standaard, onCheckedChange: (v) => setForm({ ...huidig, is_standaard: v }) })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), children: "Annuleren" }), _jsx(Button, { onClick: () => void opslaan(), children: "Opslaan" })] })] }) }));
131
+ }
@@ -0,0 +1,22 @@
1
+ import { type ReactNode } from "react";
2
+ type Item = {
3
+ id: string;
4
+ node: ReactNode;
5
+ breakBefore?: boolean;
6
+ };
7
+ /**
8
+ * Verdeelt losse blokken over echte pagina's van het gekozen formaat en toont
9
+ * er telkens één, met pijltjes om te bladeren. De pagina schaalt automatisch
10
+ * mee zodat hij volledig binnen het kader past.
11
+ */
12
+ export declare function PaginaDocument({ items, breedteMm, hoogteMm, padMm, leeg, voet, }: {
13
+ items: Item[];
14
+ breedteMm: number;
15
+ hoogteMm: number;
16
+ padMm?: number;
17
+ leeg?: ReactNode;
18
+ /** Voettekst die onder elke pagina komt te staan. */
19
+ voet?: (pagina: number, totaal: number) => ReactNode;
20
+ }): import("react").JSX.Element;
21
+ export {};
22
+ //# sourceMappingURL=PaginaDocument.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PaginaDocument.d.ts","sourceRoot":"","sources":["../../src/components/PaginaDocument.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAgD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAIrF,KAAK,IAAI,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,SAAS,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAEnE;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,SAAS,EACT,QAAQ,EACR,KAAU,EACV,IAAI,EACJ,IAAI,GACL,EAAE;IACD,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,qDAAqD;IACrD,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,SAAS,CAAC;CACtD,+BA0IA"}
@@ -0,0 +1,80 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
3
+ import { Button } from "@flowselections/core";
4
+ import { ChevronLeft, ChevronRight } from "lucide-react";
5
+ /**
6
+ * Verdeelt losse blokken over echte pagina's van het gekozen formaat en toont
7
+ * er telkens één, met pijltjes om te bladeren. De pagina schaalt automatisch
8
+ * mee zodat hij volledig binnen het kader past.
9
+ */
10
+ export function PaginaDocument({ items, breedteMm, hoogteMm, padMm = 12, leeg, voet, }) {
11
+ const meetRef = useRef(null);
12
+ const mmRef = useRef(null);
13
+ const voetMeetRef = useRef(null);
14
+ const buiten = useRef(null);
15
+ const pagina = useRef(null);
16
+ const [paginas, setPaginas] = useState([items]);
17
+ const [index, setIndex] = useState(0);
18
+ const [schaal, setSchaal] = useState(1);
19
+ // Blokken over pagina's verdelen op basis van gemeten hoogtes.
20
+ useLayoutEffect(() => {
21
+ const meet = meetRef.current;
22
+ const mm = mmRef.current;
23
+ if (!meet || !mm)
24
+ return;
25
+ const pxPerMm = mm.getBoundingClientRect().height / 100;
26
+ if (!pxPerMm)
27
+ return;
28
+ const voetPx = voetMeetRef.current?.getBoundingClientRect().height ?? 0;
29
+ const maxPx = (hoogteMm - padMm * 2) * pxPerMm - (voetPx ? voetPx + 8 : 0);
30
+ const kinderen = Array.from(meet.children);
31
+ const result = [];
32
+ let huidig = [];
33
+ let hoogte = 0;
34
+ kinderen.forEach((el, i) => {
35
+ const h = el.getBoundingClientRect().height + 12;
36
+ const forceer = items[i]?.breakBefore && huidig.length > 0;
37
+ // Een blok dat zelfs alleen niet op een pagina past, verhuist niet:
38
+ // anders zou de vorige pagina onnodig leeg blijven.
39
+ const past = h <= maxPx;
40
+ if (forceer || (huidig.length > 0 && past && hoogte + h > maxPx)) {
41
+ result.push(huidig);
42
+ huidig = [];
43
+ hoogte = 0;
44
+ }
45
+ huidig.push(items[i]);
46
+ hoogte += h;
47
+ });
48
+ if (huidig.length)
49
+ result.push(huidig);
50
+ setPaginas(result.length ? result : [[]]);
51
+ setIndex((i) => Math.min(i, Math.max(0, result.length - 1)));
52
+ }, [items, hoogteMm, breedteMm, padMm, voet]);
53
+ // Pagina passend schalen op breedte; hoogte mag doorlopen zodat je kunt scrollen.
54
+ const pas = () => {
55
+ const b = buiten.current;
56
+ const p = pagina.current;
57
+ if (!b || !p)
58
+ return;
59
+ const s = Math.min(b.clientWidth / p.offsetWidth, 1);
60
+ if (s > 0)
61
+ setSchaal(s);
62
+ };
63
+ useLayoutEffect(pas);
64
+ useEffect(() => {
65
+ const ro = new ResizeObserver(() => pas());
66
+ if (buiten.current)
67
+ ro.observe(buiten.current);
68
+ return () => ro.disconnect();
69
+ }, []);
70
+ const totaal = paginas.length;
71
+ const huidige = paginas[Math.min(index, totaal - 1)] ?? [];
72
+ return (_jsxs("div", { className: "flex h-full w-full flex-col gap-2", children: [_jsxs("div", { className: "pointer-events-none absolute -z-10 opacity-0", "aria-hidden": true, children: [_jsx("div", { ref: mmRef, style: { height: "100mm", width: "1px" } }), _jsx("div", { ref: meetRef, className: "bg-white text-[11px] text-neutral-900", style: { width: `${breedteMm - padMm * 2}mm` }, children: items.map((it) => (_jsx("div", { children: it.node }, it.id))) }), _jsx("div", { ref: voetMeetRef, className: "bg-white text-[11px] text-neutral-900", style: { width: `${breedteMm - padMm * 2}mm` }, children: voet?.(1, 1) })] }), _jsx("div", { ref: buiten, className: "flex min-h-0 flex-1 items-start justify-center overflow-auto", children: _jsxs("div", { ref: pagina, className: "flex flex-col bg-white text-[11px] text-neutral-900 shadow-sm", style: {
73
+ width: `${breedteMm}mm`,
74
+ height: `${hoogteMm}mm`,
75
+ padding: `${padMm}mm`,
76
+ transform: `scale(${schaal})`,
77
+ transformOrigin: "top center",
78
+ overflow: "hidden",
79
+ }, children: [_jsxs("div", { className: "min-h-0 flex-1 space-y-3", children: [huidige.map((it) => (_jsx("div", { children: it.node }, it.id))), huidige.length === 0 && (_jsx("p", { className: "py-10 text-center text-neutral-400", children: leeg ?? "Nog niets te tonen." }))] }), voet && _jsx("div", { className: "mt-2 shrink-0", children: voet(Math.min(index + 1, totaal), totaal) })] }) }), _jsxs("div", { className: "flex shrink-0 items-center justify-center gap-3", children: [_jsx(Button, { type: "button", variant: "outline", size: "icon", onClick: () => setIndex((i) => Math.max(0, i - 1)), disabled: index <= 0, "aria-label": "Vorige pagina", children: _jsx(ChevronLeft, { className: "h-4 w-4" }) }), _jsxs("span", { className: "text-xs text-muted-foreground", children: ["Pagina ", Math.min(index + 1, totaal), " van ", totaal] }), _jsx(Button, { type: "button", variant: "outline", size: "icon", onClick: () => setIndex((i) => Math.min(totaal - 1, i + 1)), disabled: index >= totaal - 1, "aria-label": "Volgende pagina", children: _jsx(ChevronRight, { className: "h-4 w-4" }) })] })] }));
80
+ }
@@ -0,0 +1,5 @@
1
+ /** Sectiegebaseerde pakbon-builder met live voorbeeld op echte ordergegevens. */
2
+ export declare function PakbonBuilderPage({ layoutId: layoutIdProp }?: {
3
+ layoutId?: string;
4
+ }): import("react").JSX.Element;
5
+ //# sourceMappingURL=PakbonBuilderPage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PakbonBuilderPage.d.ts","sourceRoot":"","sources":["../../src/components/PakbonBuilderPage.tsx"],"names":[],"mappings":"AAwBA,iFAAiF;AACjF,wBAAgB,iBAAiB,CAAC,EAAE,QAAQ,EAAE,YAAY,EAAE,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,+BA0IvF"}
@@ -0,0 +1,69 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState } from "react";
3
+ import { Link } from "@tanstack/react-router";
4
+ import { Badge, Button, Card, CardContent, CardDescription, CardHeader, CardTitle, toast, } from "@flowselections/core";
5
+ import { ArrowLeft, Loader2, Printer, Save } from "lucide-react";
6
+ import { PakbonSectiesPanel } from "./PakbonSectiesPanel";
7
+ import { PakbonPreview } from "./PakbonPreview";
8
+ import { FormaatOrientatieBar } from "./FormaatOrientatieBar";
9
+ import { leesPakbonConfig } from "../lib/pakbonBuilder";
10
+ import { printPakbon, voorbeeldPakbonOrder } from "../lib/pakbonHtml";
11
+ import { haalPrintLogos } from "../lib/logoLibrary";
12
+ import { updateLayout, useLayoutDetail } from "../lib/useDocumentData";
13
+ import { useLayoutIdParam } from "../lib/useLayoutIdParam";
14
+ /** Sectiegebaseerde pakbon-builder met live voorbeeld op echte ordergegevens. */
15
+ export function PakbonBuilderPage({ layoutId: layoutIdProp } = {}) {
16
+ const layoutId = useLayoutIdParam(layoutIdProp);
17
+ const { layout, setLayout, loading } = useLayoutDetail(layoutId ?? "");
18
+ const [config, setConfig] = useState(null);
19
+ const [bezig, setBezig] = useState(false);
20
+ const [printen, setPrinten] = useState(false);
21
+ useEffect(() => {
22
+ if (layout)
23
+ setConfig(leesPakbonConfig(layout.config));
24
+ }, [layout?.id]);
25
+ const breedte = useMemo(() => (layout?.orientatie === "landscape" ? layout?.hoogte_mm : layout?.breedte_mm) ?? 210, [layout]);
26
+ const hoogte = useMemo(() => (layout?.orientatie === "landscape" ? layout?.breedte_mm : layout?.hoogte_mm) ?? 297, [layout]);
27
+ const opslaan = async () => {
28
+ if (!layout || !config)
29
+ return;
30
+ setBezig(true);
31
+ try {
32
+ await updateLayout(layout.id, { config: { ...(layout.config ?? {}), pakbon: config } });
33
+ toast.success("Pakbonopbouw opgeslagen");
34
+ }
35
+ catch {
36
+ toast.error("Opslaan mislukt");
37
+ }
38
+ finally {
39
+ setBezig(false);
40
+ }
41
+ };
42
+ const voorbeeldPrinten = async () => {
43
+ if (!config)
44
+ return;
45
+ setPrinten(true);
46
+ try {
47
+ const logos = await haalPrintLogos();
48
+ printPakbon(config, voorbeeldPakbonOrder(), {
49
+ logos,
50
+ breedteMm: breedte,
51
+ hoogteMm: hoogte,
52
+ titel: layout?.naam ?? "Pakbon",
53
+ });
54
+ }
55
+ catch {
56
+ toast.error("Voorbeeld printen mislukt");
57
+ }
58
+ finally {
59
+ setPrinten(false);
60
+ }
61
+ };
62
+ if (!layoutId)
63
+ return (_jsxs("main", { className: "p-6 text-sm text-muted-foreground", children: ["Kies eerst een layout via ", _jsx(Link, { to: "/layouts-printers", className: "underline", children: "Layouts" }), "."] }));
64
+ if (loading)
65
+ return _jsx("main", { className: "p-6 text-sm text-muted-foreground", children: "Layout laden\u2026" });
66
+ if (!layout || !config)
67
+ return _jsx("main", { className: "p-6 text-sm text-muted-foreground", children: "Layout niet gevonden." });
68
+ return (_jsxs("main", { className: "space-y-4 p-6", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Button, { variant: "ghost", size: "sm", asChild: true, children: _jsxs(Link, { to: "/layouts-printers", children: [_jsx(ArrowLeft, { className: "mr-2 h-4 w-4" }), " Layouts"] }) }), _jsxs("div", { children: [_jsx("h1", { className: "text-lg font-semibold", children: layout.naam }), _jsxs("p", { className: "text-xs text-muted-foreground", children: ["Pakbon opbouwen uit secties \u00B7 ", config.secties.length, " secties"] })] })] }), _jsxs("div", { className: "flex gap-2", children: [_jsxs(Button, { size: "sm", variant: "outline", onClick: () => void voorbeeldPrinten(), disabled: printen, children: [printen ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(Printer, { className: "mr-2 h-4 w-4" })), "Voorbeeld printen"] }), _jsx(Button, { size: "sm", variant: "outline", asChild: true, children: _jsx(Link, { to: "/layouts-printers/blokken/$layoutId", params: { layoutId }, children: "Blokken ontwerpen" }) }), _jsxs(Button, { size: "sm", onClick: opslaan, disabled: bezig, children: [_jsx(Save, { className: "mr-2 h-4 w-4" }), " Opslaan"] })] })] }), _jsx(FormaatOrientatieBar, { layout: layout, onChange: (patch) => setLayout((l) => (l ? { ...l, ...patch } : l)) }), _jsxs("div", { className: "grid items-start gap-4 lg:grid-cols-[400px_1fr]", children: [_jsxs(Card, { className: "flex h-[calc(100vh-14rem)] min-h-[420px] flex-col overflow-hidden lg:sticky lg:top-4", children: [_jsxs(CardHeader, { className: "pb-3", children: [_jsx(CardTitle, { className: "text-base", children: "Secties" }), _jsx(CardDescription, { children: "Voeg blokken toe, wijzig de volgorde en stel elk blok afzonderlijk in." })] }), _jsx(CardContent, { className: "min-h-0 flex-1 overflow-y-auto", children: _jsx(PakbonSectiesPanel, { config: config, onChange: setConfig }) })] }), _jsxs(Card, { className: "flex h-[calc(100vh-14rem)] min-h-[420px] flex-col overflow-hidden", children: [_jsxs(CardHeader, { className: "flex-row items-center justify-between space-y-0 pb-3", children: [_jsxs("div", { children: [_jsx(CardTitle, { className: "text-base", children: "Live voorbeeld" }), _jsx(CardDescription, { children: "Voorbeeldorder met 2 karren, 3 lagen en fusten." })] }), _jsx(Badge, { variant: "secondary", children: "Direct bijgewerkt" })] }), _jsx(CardContent, { className: "min-h-0 flex-1 overflow-hidden", children: _jsx("div", { className: "h-full overflow-auto rounded-md border bg-muted/30 p-2", children: _jsx(PakbonPreview, { config: config, breedteMm: breedte, hoogteMm: hoogte }) }) })] })] })] }));
69
+ }
@@ -0,0 +1,13 @@
1
+ import { type PakbonConfig, type PakbonSectie } from "../lib/pakbonBuilder";
2
+ export declare function PakbonPreview({ config, breedteMm, hoogteMm, }: {
3
+ config: PakbonConfig;
4
+ breedteMm?: number;
5
+ hoogteMm?: number;
6
+ }): import("react").JSX.Element;
7
+ /** Losse weergave van één blok — gebruikt door de blokontwerper. */
8
+ export declare function PakbonSectiePreview({ sectie, config, breedteMm, }: {
9
+ sectie: PakbonSectie;
10
+ config: PakbonConfig;
11
+ breedteMm?: number;
12
+ }): import("react").JSX.Element;
13
+ //# sourceMappingURL=PakbonPreview.d.ts.map