@flowselections/dashboard-planner-installatie-techniek 1.0.3 → 1.0.37

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 (93) hide show
  1. package/README.md +0 -1
  2. package/dist-lib/components/DashboardPlannerInstallatieTechniekPage.d.ts +11 -0
  3. package/dist-lib/components/DashboardPlannerInstallatieTechniekPage.d.ts.map +1 -0
  4. package/dist-lib/components/DashboardPlannerInstallatieTechniekPage.js +173 -0
  5. package/dist-lib/components/DashboardSnelkoppelingen.d.ts +16 -0
  6. package/dist-lib/components/DashboardSnelkoppelingen.d.ts.map +1 -0
  7. package/dist-lib/components/DashboardSnelkoppelingen.js +123 -0
  8. package/dist-lib/components/DashboardStatistieken.d.ts +9 -0
  9. package/dist-lib/components/DashboardStatistieken.d.ts.map +1 -0
  10. package/dist-lib/components/DashboardStatistieken.js +80 -0
  11. package/dist-lib/components/ModulePlaceholderPage.d.ts +4 -0
  12. package/dist-lib/components/ModulePlaceholderPage.d.ts.map +1 -0
  13. package/dist-lib/components/ModulePlaceholderPage.js +11 -0
  14. package/dist-lib/components/PlannerInstallatieTechniekLogo.d.ts +6 -0
  15. package/dist-lib/components/PlannerInstallatieTechniekLogo.d.ts.map +1 -0
  16. package/dist-lib/components/PlannerInstallatieTechniekLogo.js +4 -0
  17. package/dist-lib/components/PlannerPaginaKiezer.d.ts +10 -0
  18. package/dist-lib/components/PlannerPaginaKiezer.d.ts.map +1 -0
  19. package/dist-lib/components/PlannerPaginaKiezer.js +15 -0
  20. package/dist-lib/components/WerkbonnenFunnel.d.ts +8 -0
  21. package/dist-lib/components/WerkbonnenFunnel.d.ts.map +1 -0
  22. package/dist-lib/components/WerkbonnenFunnel.js +68 -0
  23. package/dist-lib/components/WerkurenGrafiek.d.ts +26 -0
  24. package/dist-lib/components/WerkurenGrafiek.d.ts.map +1 -0
  25. package/dist-lib/components/WerkurenGrafiek.js +71 -0
  26. package/dist-lib/components/dashboard-viewport.d.ts +2 -0
  27. package/dist-lib/components/dashboard-viewport.d.ts.map +1 -0
  28. package/dist-lib/components/dashboard-viewport.js +97 -0
  29. package/dist-lib/components/ui/sankey-chart.d.ts +30 -0
  30. package/dist-lib/components/ui/sankey-chart.d.ts.map +1 -0
  31. package/dist-lib/components/ui/sankey-chart.js +292 -0
  32. package/dist-lib/index.d.ts +9 -3
  33. package/dist-lib/index.d.ts.map +1 -1
  34. package/dist-lib/index.js +9 -5
  35. package/dist-lib/integrations/supabase/auth-middleware.d.ts +10 -22015
  36. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  37. package/dist-lib/integrations/supabase/auth-middleware.js +1 -1
  38. package/dist-lib/integrations/supabase/client.d.ts +2 -22012
  39. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  40. package/dist-lib/integrations/supabase/client.js +2 -1
  41. package/dist-lib/integrations/supabase/client.server.d.ts +2 -22012
  42. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  43. package/dist-lib/integrations/supabase/previewAuthStorage.d.ts +6 -0
  44. package/dist-lib/integrations/supabase/previewAuthStorage.d.ts.map +1 -0
  45. package/dist-lib/integrations/supabase/previewAuthStorage.js +99 -0
  46. package/dist-lib/integrations/supabase/types.d.ts +23780 -10104
  47. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  48. package/dist-lib/integrations/supabase/types.js +1 -0
  49. package/dist-lib/lib/attach-core-auth.d.ts +2 -0
  50. package/dist-lib/lib/attach-core-auth.d.ts.map +1 -0
  51. package/dist-lib/lib/attach-core-auth.js +15 -0
  52. package/dist-lib/lib/dashboard-constanten.d.ts +4 -0
  53. package/dist-lib/lib/dashboard-constanten.d.ts.map +1 -0
  54. package/dist-lib/lib/dashboard-constanten.js +8 -0
  55. package/dist-lib/lib/dashboard-planner-installatie-techniek.functions.d.ts +13 -0
  56. package/dist-lib/lib/dashboard-planner-installatie-techniek.functions.d.ts.map +1 -0
  57. package/dist-lib/lib/dashboard-planner-installatie-techniek.functions.js +59 -0
  58. package/dist-lib/lib/dashboard-planner-installatie-techniek.modules.d.ts +11 -0
  59. package/dist-lib/lib/dashboard-planner-installatie-techniek.modules.d.ts.map +1 -0
  60. package/dist-lib/lib/dashboard-planner-installatie-techniek.modules.js +11 -0
  61. package/dist-lib/lib/dashboard-planner-installatie-techniek.server.d.ts +17 -0
  62. package/dist-lib/lib/dashboard-planner-installatie-techniek.server.d.ts.map +1 -0
  63. package/dist-lib/lib/dashboard-planner-installatie-techniek.server.js +239 -0
  64. package/dist-lib/lib/dashboard-planner-installatie-techniek.stats.server.d.ts +19 -0
  65. package/dist-lib/lib/dashboard-planner-installatie-techniek.stats.server.d.ts.map +1 -0
  66. package/dist-lib/lib/dashboard-planner-installatie-techniek.stats.server.js +214 -0
  67. package/dist-lib/lib/dashboard-planner-installatie-techniek.types.d.ts +68 -0
  68. package/dist-lib/lib/dashboard-planner-installatie-techniek.types.d.ts.map +1 -0
  69. package/dist-lib/lib/dashboard-planner-installatie-techniek.types.js +7 -0
  70. package/dist-lib/lib/dashboard-snelkoppelingen.catalogus.d.ts +27 -0
  71. package/dist-lib/lib/dashboard-snelkoppelingen.catalogus.d.ts.map +1 -0
  72. package/dist-lib/lib/dashboard-snelkoppelingen.catalogus.js +102 -0
  73. package/dist-lib/lib/dashboard-snelkoppelingen.d.ts +49 -0
  74. package/dist-lib/lib/dashboard-snelkoppelingen.d.ts.map +1 -0
  75. package/dist-lib/lib/dashboard-snelkoppelingen.js +55 -0
  76. package/dist-lib/lib/dashboard-tellingen.d.ts +12 -0
  77. package/dist-lib/lib/dashboard-tellingen.d.ts.map +1 -0
  78. package/dist-lib/lib/dashboard-tellingen.js +22 -0
  79. package/dist-lib/lib/dashboard-werkbon-funnel.d.ts +60 -0
  80. package/dist-lib/lib/dashboard-werkbon-funnel.d.ts.map +1 -0
  81. package/dist-lib/lib/dashboard-werkbon-funnel.js +100 -0
  82. package/dist-lib/lib/dashboard-werkbon-funnel.server.d.ts +13 -0
  83. package/dist-lib/lib/dashboard-werkbon-funnel.server.d.ts.map +1 -0
  84. package/dist-lib/lib/dashboard-werkbon-funnel.server.js +53 -0
  85. package/dist-lib/lib/funnel-indeling.d.ts +13 -0
  86. package/dist-lib/lib/funnel-indeling.d.ts.map +1 -0
  87. package/dist-lib/lib/funnel-indeling.js +74 -0
  88. package/dist-lib/lib/sankey-doek.d.ts +25 -0
  89. package/dist-lib/lib/sankey-doek.d.ts.map +1 -0
  90. package/dist-lib/lib/sankey-doek.js +38 -0
  91. package/package.json +71 -8
  92. package/public/favicon.png +0 -0
  93. /package/public/{logo.png → flowselections-assets/dashboard-planner-installatie-techniek/logo.png} +0 -0
@@ -0,0 +1,26 @@
1
+ export declare const MAX_WEKEN_TERUG = 6;
2
+ /** Maandag (NL) van de week waarin `nu` valt, als YYYY-MM-DD. */
3
+ export declare function maandagVanWeek(nu: Date): string;
4
+ /** ISO-weeknummer van een YYYY-MM-DD datum. */
5
+ export declare function isoWeek(dag: string): number;
6
+ /** "Week 42 · 12–18 okt" voor de week die op deze maandag begint. */
7
+ export declare function weekLabel(maandag: string): string;
8
+ export declare function werkurenWeek(dagen: {
9
+ dag: string;
10
+ uren: number;
11
+ }[], nu: Date, wekenTerug?: number): {
12
+ dag: string;
13
+ uren: number;
14
+ toekomst: boolean;
15
+ label: string;
16
+ }[];
17
+ export declare function WerkurenGrafiek({ dagen, beschikbaar, wekenTerug, onWeek }: {
18
+ dagen: {
19
+ dag: string;
20
+ uren: number;
21
+ }[];
22
+ beschikbaar: boolean;
23
+ wekenTerug?: number;
24
+ onWeek?: (wekenTerug: number) => void;
25
+ }): import("react").JSX.Element;
26
+ //# sourceMappingURL=WerkurenGrafiek.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WerkurenGrafiek.d.ts","sourceRoot":"","sources":["../../src/components/WerkurenGrafiek.tsx"],"names":[],"mappings":"AAIA,eAAO,MAAM,eAAe,IAAI,CAAC;AAWjC,iEAAiE;AACjE,wBAAgB,cAAc,CAAC,EAAE,EAAE,IAAI,GAAG,MAAM,CAI/C;AAED,+CAA+C;AAC/C,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAO3C;AAID,qEAAqE;AACrE,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAWjD;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,UAAU,SAAI;;;;;IAc5F;AAED,wBAAgB,eAAe,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,UAAc,EAAE,MAAM,EAAE,EAAE;IAC9E,KAAK,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACvC,WAAW,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC,+BAyCA"}
@@ -0,0 +1,71 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { ChevronLeft, ChevronRight } from 'lucide-react';
4
+ import { Button } from '@flowselections/core';
5
+ export const MAX_WEKEN_TERUG = 6;
6
+ import { DASHBOARD_TIJDZONE } from '../lib/dashboard-constanten';
7
+ const TZ = DASHBOARD_TIJDZONE;
8
+ function nlDatum(nu) {
9
+ const parts = new Intl.DateTimeFormat('en-CA', { timeZone: TZ, year: 'numeric', month: '2-digit', day: '2-digit' }).formatToParts(nu);
10
+ const value = (type) => parts.find(p => p.type === type)?.value ?? '';
11
+ return `${value('year')}-${value('month')}-${value('day')}`;
12
+ }
13
+ /** Maandag (NL) van de week waarin `nu` valt, als YYYY-MM-DD. */
14
+ export function maandagVanWeek(nu) {
15
+ const maandag = new Date(`${nlDatum(nu)}T12:00:00Z`);
16
+ maandag.setUTCDate(maandag.getUTCDate() - ((maandag.getUTCDay() + 6) % 7));
17
+ return maandag.toISOString().slice(0, 10);
18
+ }
19
+ /** ISO-weeknummer van een YYYY-MM-DD datum. */
20
+ export function isoWeek(dag) {
21
+ const d = new Date(`${dag}T12:00:00Z`);
22
+ const donderdag = new Date(d);
23
+ donderdag.setUTCDate(d.getUTCDate() + 3 - ((d.getUTCDay() + 6) % 7));
24
+ const eersteDonderdag = new Date(Date.UTC(donderdag.getUTCFullYear(), 0, 4));
25
+ eersteDonderdag.setUTCDate(eersteDonderdag.getUTCDate() + 3 - ((eersteDonderdag.getUTCDay() + 6) % 7));
26
+ return 1 + Math.round((donderdag.getTime() - eersteDonderdag.getTime()) / (7 * 86400000));
27
+ }
28
+ const MAANDEN = ['jan', 'feb', 'mrt', 'apr', 'mei', 'jun', 'jul', 'aug', 'sep', 'okt', 'nov', 'dec'];
29
+ /** "Week 42 · 12–18 okt" voor de week die op deze maandag begint. */
30
+ export function weekLabel(maandag) {
31
+ const dag = (d) => {
32
+ const [, m, dd] = d.split('-').map(Number);
33
+ return { dag: dd, maand: MAANDEN[m - 1] };
34
+ };
35
+ const zondag = new Date(`${maandag}T12:00:00Z`);
36
+ zondag.setUTCDate(zondag.getUTCDate() + 6);
37
+ const a = dag(maandag);
38
+ const b = dag(zondag.toISOString().slice(0, 10));
39
+ const bereik = a.maand === b.maand ? `${a.dag}–${b.dag} ${b.maand}` : `${a.dag} ${a.maand}–${b.dag} ${b.maand}`;
40
+ return `Week ${isoWeek(maandag)} · ${bereik}`;
41
+ }
42
+ export function werkurenWeek(dagen, nu, wekenTerug = 0) {
43
+ const vandaag = nlDatum(nu);
44
+ const offset = Math.min(Math.max(wekenTerug, 0), MAX_WEKEN_TERUG);
45
+ const doel = new Date(nu.getTime() - offset * 7 * 86400000);
46
+ const maandag = new Date(`${maandagVanWeek(doel)}T12:00:00Z`);
47
+ return Array.from({ length: 7 }, (_, i) => {
48
+ const datum = new Date(maandag);
49
+ datum.setUTCDate(datum.getUTCDate() + i);
50
+ const dag = datum.toISOString().slice(0, 10);
51
+ // Toekomst-markering alleen in de huidige week; oude weken zijn volledig voorbij.
52
+ const toekomst = offset === 0 && dag > vandaag;
53
+ const uren = toekomst ? 0 : Math.max(0, dagen.find(d => d.dag === dag)?.uren ?? 0);
54
+ return { dag, uren, toekomst, label: ['ma', 'di', 'wo', 'do', 'vr', 'za', 'zo'][i] };
55
+ });
56
+ }
57
+ export function WerkurenGrafiek({ dagen, beschikbaar, wekenTerug = 0, onWeek }) {
58
+ const [nu, setNu] = useState(null);
59
+ useEffect(() => {
60
+ setNu(new Date());
61
+ const timer = window.setInterval(() => setNu(new Date()), 60000);
62
+ return () => window.clearInterval(timer);
63
+ }, []);
64
+ const week = nu ? werkurenWeek(Array.isArray(dagen) ? dagen : [], nu, wekenTerug) : [];
65
+ const max = Math.max(1, ...week.map(d => d.uren));
66
+ const urenLabel = (uren) => `${uren.toLocaleString('nl-NL', { maximumFractionDigits: 1 })} u`;
67
+ const maandag = week[0]?.dag ?? '';
68
+ const knop = 'inline-flex h-7 w-7 items-center justify-center rounded-md border border-border text-muted-foreground transition-colors hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-40';
69
+ return _jsxs("div", { className: "dashboard-hours space-y-3", children: [_jsxs("div", { className: "dashboard-hours-heading flex items-center justify-between gap-2", children: [_jsx("p", { className: "text-xs font-medium text-muted-foreground", children: "Tot gewerkt" }), onWeek && _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Button, { type: "button", variant: "outline", size: "icon", className: knop, "aria-label": "Een week terug", disabled: wekenTerug >= MAX_WEKEN_TERUG, onClick: () => onWeek(wekenTerug + 1), children: _jsx(ChevronLeft, { className: "h-4 w-4" }) }), _jsx("span", { className: "min-w-32 text-center text-xs font-medium tabular-nums", "aria-live": "polite", children: maandag ? weekLabel(maandag) : '' }), _jsx(Button, { type: "button", variant: "outline", size: "icon", className: knop, "aria-label": "Een week vooruit", disabled: wekenTerug <= 0, onClick: () => onWeek(wekenTerug - 1), children: _jsx(ChevronRight, { className: "h-4 w-4" }) })] })] }), !beschikbaar ? _jsx("p", { className: "text-xs text-muted-foreground", children: "Uren konden niet worden geladen." }) :
70
+ _jsxs("div", { className: "dashboard-hours-chart relative h-56", role: "img", "aria-label": `Gewerkte uren van alle monteurs, ${maandag ? weekLabel(maandag) : 'deze week'}, maandag tot zondag`, children: [_jsx("div", { "aria-hidden": "true", className: "pointer-events-none absolute inset-x-0 bottom-6 top-7 flex flex-col justify-between", children: [0, 1, 2, 3].map(n => _jsx("div", { className: "border-t border-border/60" }, n)) }), _jsx("div", { className: "relative grid h-full grid-cols-7 gap-2", children: week.map(d => _jsxs("div", { className: "flex min-w-0 flex-col", "aria-label": `${d.label}: ${d.toekomst ? 'nog niet gewerkt' : urenLabel(d.uren)}`, children: [_jsxs("div", { className: "flex min-h-0 flex-1 flex-col justify-end", children: [_jsx("span", { className: "mb-1.5 text-center text-[10px] font-medium tabular-nums", children: d.toekomst ? '-' : urenLabel(d.uren) }), _jsx("div", { className: `mx-auto w-full max-w-10 shrink-0 rounded-t-md ${d.toekomst ? 'bg-muted' : 'bg-primary/80'}`, style: { height: d.uren > 0 ? `${(d.uren / max) * 78}%` : '3px' } })] }), _jsx("span", { className: "h-6 pt-1.5 text-center text-xs font-medium text-muted-foreground", children: d.label })] }, d.dag)) })] })] });
71
+ }
@@ -0,0 +1,2 @@
1
+ export declare const dashboardViewportStyles = "\n.planner-dashboard {\n --dashboard-gap: 12px;\n}\n.dashboard-shortcuts { max-width: 100%; }\n.dashboard-shortcut-items { display: flex; flex-wrap: wrap; align-items: stretch; gap: 6px; }\n.dashboard-shortcut-items > div { display: flex; min-width: 0; max-width: 100%; }\n.dashboard-shortcut-items a { width: 100%; height: 100%; min-height: 32px; max-width: 100%; white-space: normal; overflow-wrap: anywhere; padding: 5px 28px 5px 10px; line-height: 16px; border: 1px dashed color-mix(in oklab, var(--muted-foreground) 45%, transparent); }\n.dashboard-shortcut-items a:hover { border-color: color-mix(in oklab, var(--muted-foreground) 85%, transparent); }\n.dashboard-hours-heading { flex-wrap: wrap; }\n@media (max-width: 1023px) {\n .dashboard-toolbar { display: grid; grid-template-columns: minmax(0, 1fr) auto; }\n .dashboard-toolbar > p { grid-column: 1; grid-row: 1; }\n .dashboard-toolbar > .dashboard-shortcuts { grid-column: 1 / -1; grid-row: 2; }\n .dashboard-toolbar > button { grid-column: 2; grid-row: 1; }\n .dashboard-shortcuts { grid-column: 1 / -1; flex-wrap: wrap; }\n .dashboard-shortcut-items { flex-basis: 100%; }\n}\n\n@media (min-width: 1024px) {\n .planner-dashboard {\n display: flex;\n flex-direction: column;\n gap: var(--dashboard-gap);\n min-height: var(--dashboard-height, auto);\n padding-bottom: 12px;\n }\n .planner-dashboard > * { margin-block: 0; }\n .dashboard-toolbar { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 6px 12px; }\n .dashboard-toolbar > p { grid-column: 1; grid-row: 1; }\n .dashboard-toolbar > .dashboard-shortcuts { grid-column: 1 / -1; grid-row: 2; }\n .dashboard-toolbar > button { grid-column: 2; grid-row: 1; }\n .dashboard-shortcuts { padding-block: 0; }\n .dashboard-shortcut-items a { font-size: 11px; line-height: 14px; }\n .dashboard-module-grid { grid-template-columns: repeat(6, minmax(0, 1fr)); }\n .dashboard-module-content { min-height: 156px; padding: 12px; }\n .dashboard-module-icon { width: 40px; height: 40px; border-radius: 12px; }\n .dashboard-module-icon svg { width: 24px; height: 24px; }\n .dashboard-module-title { margin-top: 8px; font-size: 14px; overflow-wrap: anywhere; }\n .dashboard-module-count { padding-top: 8px; }\n .dashboard-stats { flex: 1; display: flex; flex-direction: column; gap: 8px; }\n .dashboard-stats > * { margin-block: 0; }\n .dashboard-stats-grid { flex: 1; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }\n .dashboard-stat-content { padding: 12px; }\n .dashboard-stat-content > * { margin-block: 0; }\n .dashboard-stat-content > * + * { margin-top: 8px; }\n .dashboard-financial > div > * { margin-block: 0; }\n .dashboard-financial > div > * + * { margin-top: 8px; }\n .dashboard-financial > div > div > * { margin-block: 0; }\n .dashboard-financial > div > div > * + * { margin-top: 4px; }\n .dashboard-financial .dashboard-offers { padding-top: 10px; }\n .dashboard-financial .dashboard-amount { font-size: 26px; line-height: 32px; }\n .dashboard-hours-content { height: 100%; display: flex; flex-direction: column; gap: 8px; }\n .dashboard-hours-content > * { margin-block: 0; }\n .dashboard-hours-content > dl { flex-shrink: 0; padding-top: 8px; gap: 8px; }\n .dashboard-hours { flex: 1; display: flex; flex-direction: column; gap: 8px; }\n .dashboard-hours > * { margin-block: 0; }\n .dashboard-hours-heading { flex-wrap: wrap; }\n .dashboard-hours-chart { height: 160px; flex: 1; min-height: 130px; }\n .dashboard-funnel-chart { max-height: 200px; }\n .dashboard-funnel-tip { min-height: 48px; }\n .dashboard-stats h2 { font-size: 16px; line-height: 20px; }\n .dashboard-funnel-loading { height: 240px; }\n}\n\n@media (min-width: 1024px) and (max-height: 800px) {\n .planner-dashboard { --dashboard-gap: 8px; }\n .dashboard-module-content { min-height: 112px; padding: 8px; }\n .dashboard-module-icon { width: 32px; height: 32px; }\n .dashboard-module-icon svg { width: 20px; height: 20px; }\n .dashboard-module-title { margin-top: 0; }\n .dashboard-module-count { padding-top: 4px; }\n .dashboard-module-count > div { padding: 3px 8px; }\n .dashboard-module-count > div > span:first-child { font-size: 18px; line-height: 20px; }\n .dashboard-stat-content { padding: 10px; }\n .dashboard-stat-content > * + * { margin-top: 4px; }\n .dashboard-stat-content > div:first-child { min-height: 28px; }\n .dashboard-financial > div > * + * { margin-top: 4px; }\n .dashboard-financial > div:first-child > div { width: 28px; height: 28px; }\n .dashboard-financial .dashboard-amount { font-size: 24px; line-height: 28px; }\n .dashboard-financial .dashboard-offers { padding-top: 8px; }\n .dashboard-funnel-chart { max-height: 130px; }\n .dashboard-hours-chart { height: 140px; }\n}\n\n@media (min-width: 1024px) and (max-height: 740px) {\n .dashboard-module-icon { width: 24px; height: 24px; }\n .dashboard-module-icon svg { width: 16px; height: 16px; }\n .dashboard-module-content { min-height: 104px; }\n .dashboard-financial .dashboard-offers dl { gap: 4px; }\n .dashboard-stat-content { padding: 8px; }\n .dashboard-financial > div > * + * { margin-top: 2px; }\n .dashboard-financial > div > div > * + * { margin-top: 2px; }\n .dashboard-hours-content { gap: 4px; }\n .dashboard-hours-chart { min-height: 116px; }\n .dashboard-funnel-chart { max-height: 118px; }\n}";
2
+ //# sourceMappingURL=dashboard-viewport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard-viewport.d.ts","sourceRoot":"","sources":["../../src/components/dashboard-viewport.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,uBAAuB,sxKAgGlC,CAAC"}
@@ -0,0 +1,97 @@
1
+ export const dashboardViewportStyles = `
2
+ .planner-dashboard {
3
+ --dashboard-gap: 12px;
4
+ }
5
+ .dashboard-shortcuts { max-width: 100%; }
6
+ .dashboard-shortcut-items { display: flex; flex-wrap: wrap; align-items: stretch; gap: 6px; }
7
+ .dashboard-shortcut-items > div { display: flex; min-width: 0; max-width: 100%; }
8
+ .dashboard-shortcut-items a { width: 100%; height: 100%; min-height: 32px; max-width: 100%; white-space: normal; overflow-wrap: anywhere; padding: 5px 28px 5px 10px; line-height: 16px; border: 1px dashed color-mix(in oklab, var(--muted-foreground) 45%, transparent); }
9
+ .dashboard-shortcut-items a:hover { border-color: color-mix(in oklab, var(--muted-foreground) 85%, transparent); }
10
+ .dashboard-hours-heading { flex-wrap: wrap; }
11
+ @media (max-width: 1023px) {
12
+ .dashboard-toolbar { display: grid; grid-template-columns: minmax(0, 1fr) auto; }
13
+ .dashboard-toolbar > p { grid-column: 1; grid-row: 1; }
14
+ .dashboard-toolbar > .dashboard-shortcuts { grid-column: 1 / -1; grid-row: 2; }
15
+ .dashboard-toolbar > button { grid-column: 2; grid-row: 1; }
16
+ .dashboard-shortcuts { grid-column: 1 / -1; flex-wrap: wrap; }
17
+ .dashboard-shortcut-items { flex-basis: 100%; }
18
+ }
19
+
20
+ @media (min-width: 1024px) {
21
+ .planner-dashboard {
22
+ display: flex;
23
+ flex-direction: column;
24
+ gap: var(--dashboard-gap);
25
+ min-height: var(--dashboard-height, auto);
26
+ padding-bottom: 12px;
27
+ }
28
+ .planner-dashboard > * { margin-block: 0; }
29
+ .dashboard-toolbar { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 6px 12px; }
30
+ .dashboard-toolbar > p { grid-column: 1; grid-row: 1; }
31
+ .dashboard-toolbar > .dashboard-shortcuts { grid-column: 1 / -1; grid-row: 2; }
32
+ .dashboard-toolbar > button { grid-column: 2; grid-row: 1; }
33
+ .dashboard-shortcuts { padding-block: 0; }
34
+ .dashboard-shortcut-items a { font-size: 11px; line-height: 14px; }
35
+ .dashboard-module-grid { grid-template-columns: repeat(6, minmax(0, 1fr)); }
36
+ .dashboard-module-content { min-height: 156px; padding: 12px; }
37
+ .dashboard-module-icon { width: 40px; height: 40px; border-radius: 12px; }
38
+ .dashboard-module-icon svg { width: 24px; height: 24px; }
39
+ .dashboard-module-title { margin-top: 8px; font-size: 14px; overflow-wrap: anywhere; }
40
+ .dashboard-module-count { padding-top: 8px; }
41
+ .dashboard-stats { flex: 1; display: flex; flex-direction: column; gap: 8px; }
42
+ .dashboard-stats > * { margin-block: 0; }
43
+ .dashboard-stats-grid { flex: 1; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
44
+ .dashboard-stat-content { padding: 12px; }
45
+ .dashboard-stat-content > * { margin-block: 0; }
46
+ .dashboard-stat-content > * + * { margin-top: 8px; }
47
+ .dashboard-financial > div > * { margin-block: 0; }
48
+ .dashboard-financial > div > * + * { margin-top: 8px; }
49
+ .dashboard-financial > div > div > * { margin-block: 0; }
50
+ .dashboard-financial > div > div > * + * { margin-top: 4px; }
51
+ .dashboard-financial .dashboard-offers { padding-top: 10px; }
52
+ .dashboard-financial .dashboard-amount { font-size: 26px; line-height: 32px; }
53
+ .dashboard-hours-content { height: 100%; display: flex; flex-direction: column; gap: 8px; }
54
+ .dashboard-hours-content > * { margin-block: 0; }
55
+ .dashboard-hours-content > dl { flex-shrink: 0; padding-top: 8px; gap: 8px; }
56
+ .dashboard-hours { flex: 1; display: flex; flex-direction: column; gap: 8px; }
57
+ .dashboard-hours > * { margin-block: 0; }
58
+ .dashboard-hours-heading { flex-wrap: wrap; }
59
+ .dashboard-hours-chart { height: 160px; flex: 1; min-height: 130px; }
60
+ .dashboard-funnel-chart { max-height: 200px; }
61
+ .dashboard-funnel-tip { min-height: 48px; }
62
+ .dashboard-stats h2 { font-size: 16px; line-height: 20px; }
63
+ .dashboard-funnel-loading { height: 240px; }
64
+ }
65
+
66
+ @media (min-width: 1024px) and (max-height: 800px) {
67
+ .planner-dashboard { --dashboard-gap: 8px; }
68
+ .dashboard-module-content { min-height: 112px; padding: 8px; }
69
+ .dashboard-module-icon { width: 32px; height: 32px; }
70
+ .dashboard-module-icon svg { width: 20px; height: 20px; }
71
+ .dashboard-module-title { margin-top: 0; }
72
+ .dashboard-module-count { padding-top: 4px; }
73
+ .dashboard-module-count > div { padding: 3px 8px; }
74
+ .dashboard-module-count > div > span:first-child { font-size: 18px; line-height: 20px; }
75
+ .dashboard-stat-content { padding: 10px; }
76
+ .dashboard-stat-content > * + * { margin-top: 4px; }
77
+ .dashboard-stat-content > div:first-child { min-height: 28px; }
78
+ .dashboard-financial > div > * + * { margin-top: 4px; }
79
+ .dashboard-financial > div:first-child > div { width: 28px; height: 28px; }
80
+ .dashboard-financial .dashboard-amount { font-size: 24px; line-height: 28px; }
81
+ .dashboard-financial .dashboard-offers { padding-top: 8px; }
82
+ .dashboard-funnel-chart { max-height: 130px; }
83
+ .dashboard-hours-chart { height: 140px; }
84
+ }
85
+
86
+ @media (min-width: 1024px) and (max-height: 740px) {
87
+ .dashboard-module-icon { width: 24px; height: 24px; }
88
+ .dashboard-module-icon svg { width: 16px; height: 16px; }
89
+ .dashboard-module-content { min-height: 104px; }
90
+ .dashboard-financial .dashboard-offers dl { gap: 4px; }
91
+ .dashboard-stat-content { padding: 8px; }
92
+ .dashboard-financial > div > * + * { margin-top: 2px; }
93
+ .dashboard-financial > div > div > * + * { margin-top: 2px; }
94
+ .dashboard-hours-content { gap: 4px; }
95
+ .dashboard-hours-chart { min-height: 116px; }
96
+ .dashboard-funnel-chart { max-height: 118px; }
97
+ }`;
@@ -0,0 +1,30 @@
1
+ import { type FunnelIndeling } from '../../lib/funnel-indeling';
2
+ interface Node {
3
+ name: string;
4
+ value: number;
5
+ kleur: string;
6
+ paars?: boolean;
7
+ aantal?: number;
8
+ }
9
+ interface Link {
10
+ source: number;
11
+ target: number;
12
+ value: number;
13
+ echt?: number;
14
+ }
15
+ export interface SankeyChartData {
16
+ nodes: Node[];
17
+ links: Link[];
18
+ }
19
+ export declare function SankeyChart({ data, total, onKleuren, indeling, onTip }: {
20
+ data: SankeyChartData;
21
+ total: number;
22
+ onKleuren?: (k: Record<string, string>) => void;
23
+ indeling?: FunnelIndeling;
24
+ onTip?: (t: {
25
+ titel: string;
26
+ tekst: string;
27
+ } | null) => void;
28
+ }): import("react").JSX.Element;
29
+ export {};
30
+ //# sourceMappingURL=sankey-chart.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sankey-chart.d.ts","sourceRoot":"","sources":["../../../src/components/ui/sankey-chart.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAsC,KAAK,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAEpG,UAAU,IAAI;IAAG,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE;AAC/F,UAAU,IAAI;IAAG,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE;AAC/E,MAAM,WAAW,eAAe;IAAG,KAAK,EAAE,IAAI,EAAE,CAAC;IAAC,KAAK,EAAE,IAAI,EAAE,CAAA;CAAE;AA0TjE,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,QAA6B,EAAE,KAAK,EAAE,EAAE;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,IAAI,CAAC;IAAC,QAAQ,CAAC,EAAE,cAAc,CAAC;IAAC,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,KAAK,IAAI,CAAA;CAAE,+BA6B/Q"}
@@ -0,0 +1,292 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Sankey, sankeyLeft } from '@visx/sankey';
3
+ import { ParentSize } from '@visx/responsive';
4
+ import { useEffect, useId, useRef, useState } from 'react';
5
+ import { doekPunt, doekProfiel, doekNabijheid, stapDoekVeer } from '../../lib/sankey-doek';
6
+ import { rangFunnelNode, FUNNEL_EXTRA } from '../../lib/dashboard-werkbon-funnel';
7
+ import { plaatsIndeling, STANDAARD_INDELING } from '../../lib/funnel-indeling';
8
+ // Duidelijk verschillende stappen: boven Aangemaakt steeds lichter blauw, eronder steeds grijzer.
9
+ const BLAUW = ['var(--funnel-blue-1)', 'var(--funnel-blue-2)', 'var(--funnel-blue-3)'];
10
+ const GRIJS = ['var(--funnel-gray-1)', 'var(--funnel-gray-2)', 'var(--funnel-gray-3)', 'var(--funnel-gray-4)', 'var(--funnel-gray-5)'];
11
+ const NAVY = 'var(--funnel-navy)';
12
+ // De extra-werk-tak is bewust veel dunner dan de hoofdstromen: dezelfde afgevlakte
13
+ // schaal, maar met een eigen factor zodat de verhoudingen binnen de tak gelijk blijven.
14
+ const EXTRA_SCHAAL = 0.3;
15
+ function kleurNodes(nodes) {
16
+ const midden = (n) => ((n.y0 ?? 0) + (n.y1 ?? 0)) / 2;
17
+ const basis = nodes.find(n => n.name === 'Aangemaakt');
18
+ const basisY = basis ? midden(basis) : 0;
19
+ const kleuren = nodes.map(n => {
20
+ if ((n.depth ?? 0) === 0 || n.paars)
21
+ return n.kleur;
22
+ if (n.name === 'Aangemaakt')
23
+ return NAVY;
24
+ if (n.name === 'Betaald')
25
+ return 'var(--funnel-paid)';
26
+ if (n.name === 'Nog niet betaald')
27
+ return 'var(--funnel-unpaid)';
28
+ return '';
29
+ });
30
+ const boven = nodes.map((n, i) => ({ n, i })).filter(({ n, i }) => !kleuren[i] && midden(n) <= basisY).sort((a, b) => midden(b.n) - midden(a.n));
31
+ const onder = nodes.map((n, i) => ({ n, i })).filter(({ n, i }) => !kleuren[i] && midden(n) > basisY).sort((a, b) => midden(a.n) - midden(b.n));
32
+ boven.forEach(({ i }, k) => { kleuren[i] = BLAUW[Math.min(k, BLAUW.length - 1)]; });
33
+ onder.forEach(({ i }, k) => { kleuren[i] = GRIJS[Math.min(k, GRIJS.length - 1)]; });
34
+ return kleuren;
35
+ }
36
+ function useMarkering() {
37
+ const [doek, setDoek] = useState({});
38
+ const toestanden = useRef({});
39
+ const doel = useRef(null);
40
+ const raf = useRef(null);
41
+ const tijd = useRef(null);
42
+ const stil = useRef(false);
43
+ useEffect(() => {
44
+ const media = window.matchMedia('(prefers-reduced-motion: reduce)');
45
+ const wijzig = () => {
46
+ stil.current = media.matches;
47
+ if (media.matches) {
48
+ if (raf.current !== null)
49
+ cancelAnimationFrame(raf.current);
50
+ raf.current = null;
51
+ tijd.current = null;
52
+ doel.current = null;
53
+ toestanden.current = {};
54
+ setDoek({});
55
+ }
56
+ };
57
+ wijzig();
58
+ media.addEventListener('change', wijzig);
59
+ return () => {
60
+ media.removeEventListener('change', wijzig);
61
+ if (raf.current !== null)
62
+ cancelAnimationFrame(raf.current);
63
+ };
64
+ }, []);
65
+ const lus = (nu) => {
66
+ const dt = tijd.current === null ? 1 / 60 : Math.min(0.05, (nu - tijd.current) / 1000);
67
+ tijd.current = nu;
68
+ const d = doel.current, volgende = {};
69
+ let beweegt = false;
70
+ for (const [key, vorige] of Object.entries(toestanden.current)) {
71
+ const i = Number(key), target = d?.index === i ? d : null;
72
+ const t = stapDoekVeer(vorige.t, target?.t ?? vorige.t.positie, dt, 100, 20);
73
+ const sterkte = stapDoekVeer(vorige.sterkte, target?.kracht ?? 0, dt);
74
+ const onrust = Math.abs(sterkte.snelheid) > 0.01 || Math.abs(sterkte.positie - (target?.kracht ?? 0)) > 0.002
75
+ || Math.abs(t.snelheid) > 0.0001 || Math.abs(t.positie - (target?.t ?? t.positie)) > 0.0001;
76
+ if (target || Math.abs(sterkte.positie) > 0.002 || onrust)
77
+ volgende[i] = { t, sterkte };
78
+ beweegt || (beweegt = onrust);
79
+ }
80
+ toestanden.current = volgende;
81
+ setDoek(volgende);
82
+ raf.current = beweegt ? requestAnimationFrame(lus) : null;
83
+ if (!beweegt)
84
+ tijd.current = null;
85
+ };
86
+ const start = () => { if (raf.current === null)
87
+ raf.current = requestAnimationFrame(lus); };
88
+ return {
89
+ doek,
90
+ beweeg: (index, t, kracht) => {
91
+ if (stil.current) {
92
+ // Verminderde beweging: markering direct op de muis, zonder animatie.
93
+ toestanden.current = { [index]: { t: { positie: t, snelheid: 0 }, sterkte: { positie: kracht, snelheid: 0 } } };
94
+ doel.current = { index, t, kracht };
95
+ setDoek(toestanden.current);
96
+ return;
97
+ }
98
+ doel.current = { index, t, kracht };
99
+ if (!toestanden.current[index])
100
+ toestanden.current[index] = {
101
+ t: { positie: t, snelheid: 0 }, sterkte: { positie: 0, snelheid: 0 },
102
+ };
103
+ start();
104
+ },
105
+ los: () => {
106
+ doel.current = null;
107
+ if (stil.current) {
108
+ toestanden.current = {};
109
+ setDoek({});
110
+ return;
111
+ }
112
+ start();
113
+ },
114
+ };
115
+ }
116
+ function Grafiek({ graph, createPath, total, onKleuren, setTip, indeling, hoogte, onInhoud, textScale }) {
117
+ const [actief, setActief] = useState(null);
118
+ const effectId = useId().replace(/[^a-zA-Z0-9_-]/g, '');
119
+ const kleuren = kleurNodes(graph.nodes);
120
+ const sleutel = graph.nodes.map((n, i) => `${n.name}=${kleuren[i]}`).join('|');
121
+ useEffect(() => { onKleuren?.(Object.fromEntries(graph.nodes.map((n, i) => [n.name, kleuren[i]]))); }, [sleutel]); // eslint-disable-line react-hooks/exhaustive-deps
122
+ const idx = (n) => graph.nodes.indexOf(n);
123
+ const pct = (v) => `${Math.round(v / total * 100)}%`;
124
+ const linkAan = (i) => {
125
+ if (!actief)
126
+ return true;
127
+ if (actief.soort === 'link')
128
+ return actief.index === i;
129
+ const l = graph.links[i];
130
+ return idx(l.source) === actief.index || idx(l.target) === actief.index;
131
+ };
132
+ const nodeAan = (i) => {
133
+ if (!actief)
134
+ return true;
135
+ if (actief.soort === 'node')
136
+ return actief.index === i;
137
+ const l = graph.links[actief.index];
138
+ return idx(l.source) === i || idx(l.target) === i;
139
+ };
140
+ const { doek, beweeg, los } = useMarkering();
141
+ const uit = () => { setActief(null); setTip(null); };
142
+ // One display layout for nodes, links and cloth hit testing. The extra branch
143
+ // shares the payment endpoint column, with its own space beneath the main flow.
144
+ const betaaldNode = graph.nodes.find(n => n.name === 'Betaald');
145
+ const nnbNode = graph.nodes.find(n => n.name === 'Nog niet betaald');
146
+ const afgerondNode = graph.nodes.find(n => n.name === 'Afgerond');
147
+ const paarsEinden = graph.nodes.filter(n => n.paars && n.name !== FUNNEL_EXTRA);
148
+ const heeftExtra = paarsEinden.length > 0;
149
+ const linksX = Math.min(...graph.nodes.map(n => n.x0 ?? 0));
150
+ const rechtsX = Math.max(...graph.nodes.map(n => n.x0 ?? 0));
151
+ const betalingX = betaaldNode?.x0 ?? nnbNode?.x0 ?? rechtsX;
152
+ const xSchaal = heeftExtra && betalingX > linksX ? (rechtsX - linksX) / (betalingX - linksX) : 1;
153
+ const plaatsX = (x) => linksX + (x - linksX) * xSchaal;
154
+ // Lijnen iets dunner dan de ruimte die de indeling biedt.
155
+ const ySchaal = 0.75;
156
+ const aangemaakt = graph.nodes.find(n => n.name === 'Aangemaakt');
157
+ // Afgevlakte schaal: dikte groeit met aantal^0,75 zodat verhoudingen zachter
158
+ // worden (een lijn van 50 is duidelijk dikker dan 10, maar niet exact 5×).
159
+ const DRUK = 0.75;
160
+ const druk = (v) => Math.pow(Math.max(0, v), DRUK);
161
+ const k = aangemaakt && (aangemaakt.aantal ?? 0) > 0 ? ((aangemaakt.y1 ?? 0) - (aangemaakt.y0 ?? 0)) / druk(aangemaakt.aantal ?? 1) : 1;
162
+ const dikte = (v) => Math.max(2, k * druk(v));
163
+ const diktePaars = (v) => Math.max(1.5, k * druk(v) * EXTRA_SCHAAL);
164
+ const aantalVan = (n) => n.aantal ?? n.value ?? 0;
165
+ const plaatsY = (y) => 14 + (y - 14) * ySchaal;
166
+ const posities = new Map();
167
+ graph.nodes.forEach(n => posities.set(n, {
168
+ x: plaatsX(n.x0 ?? 0), y0: plaatsY(n.y0 ?? 0), y1: plaatsY(n.y1 ?? 0),
169
+ }));
170
+ if (heeftExtra) {
171
+ const hoofdbodem = Math.max(...graph.nodes.filter(n => !n.paars).map(n => plaatsY(n.y1 ?? 0)));
172
+ const startY = hoofdbodem + 18, gat = 8;
173
+ let y = startY;
174
+ for (const n of paarsEinden) {
175
+ const h = diktePaars(aantalVan(n));
176
+ posities.set(n, { x: rechtsX, y0: y, y1: y + h });
177
+ y += h + gat;
178
+ }
179
+ const extra = graph.nodes.find(n => n.name === FUNNEL_EXTRA);
180
+ if (extra) {
181
+ const h = diktePaars(aantalVan(extra));
182
+ const midden = (startY + y - gat) / 2;
183
+ posities.set(extra, { x: (plaatsX(afgerondNode?.x0 ?? linksX) + rechtsX) / 2,
184
+ y0: midden - h / 2, y1: midden + h / 2 });
185
+ }
186
+ }
187
+ const basisPosities = new Map(posities);
188
+ // De paarse extra-werklijn sluit aan onder de Afgerond-balk. Maak die balk
189
+ // aan de onderkant precies zo lang als de paarse lijn dik is, zodat de lijn
190
+ // aansluit op het verlengde van de balk in plaats van eraf te hangen, en
191
+ // schuif de balken eronder in dezelfde kolom met dezelfde afstand mee.
192
+ if (afgerondNode) {
193
+ const extraLinkNaarExtra = graph.links.find(l => l.target.name === FUNNEL_EXTRA && l.source === afgerondNode);
194
+ const extraLang = extraLinkNaarExtra ? diktePaars(extraLinkNaarExtra.echt ?? 0) : 0;
195
+ const balk = posities.get(afgerondNode);
196
+ if (extraLang > 0 && balk) {
197
+ posities.set(afgerondNode, { ...balk, y1: balk.y1 + extraLang });
198
+ graph.nodes.forEach(n => {
199
+ if (n === afgerondNode || n.paars)
200
+ return;
201
+ const q = posities.get(n);
202
+ if (q && q.x === balk.x && q.y0 >= balk.y1 - 0.5)
203
+ posities.set(n, { ...q, y0: q.y0 + extraLang, y1: q.y1 + extraLang });
204
+ });
205
+ }
206
+ }
207
+ if (Object.keys(indeling).length > 0) {
208
+ const geplaatst = plaatsIndeling(graph.nodes.map(n => {
209
+ const p = posities.get(n);
210
+ return { naam: n.name, x: p?.x ?? 0, y0: p?.y0 ?? 0, y1: p?.y1 ?? 0, paars: n.paars };
211
+ }), indeling, hoogte);
212
+ graph.nodes.forEach((n, i) => { const p = geplaatst[i]; posities.set(n, { x: p.x, y0: p.y0, y1: p.y1 }); });
213
+ }
214
+ // Onderkant van wat er echt getekend wordt; het omringende vak krimpt hierop
215
+ // zodat er geen lege strook tussen funnel en pop-up blijft.
216
+ const bodem = Math.max(40, ...[...posities.values()].map(p => p.y1));
217
+ useEffect(() => { onInhoud?.(bodem); }, [bodem]); // eslint-disable-line react-hooks/exhaustive-deps
218
+ const geometrie = graph.links.map(link => {
219
+ const s = link.source, t = link.target;
220
+ const sp = posities.get(s), tp = posities.get(t);
221
+ return { x0: (sp?.x ?? s.x0 ?? 0) + 10, x1: tp?.x ?? t.x0 ?? 0,
222
+ y0: s.paars && sp ? (sp.y0 + sp.y1) / 2 : t.name === FUNNEL_EXTRA && sp ? sp.y1 - diktePaars(link.echt ?? 0) / 2 : plaatsY(link.y0 ?? 0) + (sp?.y0 ?? 0) - (basisPosities.get(s)?.y0 ?? 0),
223
+ y1: t.paars && tp ? (tp.y0 + tp.y1) / 2 : plaatsY(link.y1 ?? 0) + (tp?.y0 ?? 0) - (basisPosities.get(t)?.y0 ?? 0),
224
+ breedte: t.paars && t.name !== FUNNEL_EXTRA && tp ? tp.y1 - tp.y0 : t.name === FUNNEL_EXTRA ? diktePaars(link.echt ?? 0) : (link.width ?? 0) * ySchaal };
225
+ });
226
+ const muis = (e) => {
227
+ const svg = e.currentTarget.ownerSVGElement, matrix = svg?.getScreenCTM();
228
+ if (!svg || !matrix)
229
+ return;
230
+ const punt = svg.createSVGPoint();
231
+ punt.x = e.clientX;
232
+ punt.y = e.clientY;
233
+ const lokaal = punt.matrixTransform(matrix.inverse());
234
+ let beste = null;
235
+ geometrie.forEach((g, index) => {
236
+ if (lokaal.x <= g.x0 + 3 || lokaal.x >= g.x1 - 3)
237
+ return;
238
+ const nabij = doekNabijheid(g, lokaal.x, lokaal.y);
239
+ if (nabij.kracht > 0 && (!beste || nabij.afstand < beste.afstand))
240
+ beste = { index, ...nabij };
241
+ });
242
+ if (beste) {
243
+ const gevonden = beste;
244
+ beweeg(gevonden.index, gevonden.t, gevonden.kracht);
245
+ }
246
+ else
247
+ los();
248
+ };
249
+ return _jsxs("g", { onMouseMove: muis, onMouseLeave: () => { uit(); los(); }, children: [_jsxs("defs", { children: [_jsx("filter", { id: `${effectId}-glow`, x: "-60%", y: "-150%", width: "220%", height: "400%", colorInterpolationFilters: "sRGB", children: _jsx("feGaussianBlur", { stdDeviation: 3 }) }), _jsxs("filter", { id: `${effectId}-shadow`, x: "-60%", y: "-150%", width: "220%", height: "400%", colorInterpolationFilters: "sRGB", children: [_jsx("feGaussianBlur", { stdDeviation: 2.5 }), _jsx("feOffset", { dy: 3 })] }), _jsx("filter", { id: `${effectId}-soft-mask`, x: "-50%", y: "-50%", width: "200%", height: "200%", children: _jsx("feGaussianBlur", { stdDeviation: 5 }) })] }), _jsx("rect", { x: 0, y: 0, width: "100%", height: "100%", fill: "transparent", pointerEvents: "all" }), graph.links.map((link, i) => {
250
+ const s = link.source, t = link.target;
251
+ const kleur = (s.depth ?? 0) === 0 ? kleuren[idx(s)] : kleuren[idx(t)];
252
+ const titel = `${s.name} → ${t.name}`, tekst = `${link.echt ?? link.value} ${t.paars ? 'meldingen' : 'werkbonnen'} · ${pct(link.echt ?? link.value)}`;
253
+ const toon = () => { setActief({ soort: 'link', index: i }); setTip({ titel, tekst }); };
254
+ const g = geometrie[i];
255
+ const w = g.breedte;
256
+ const markering = doek[i];
257
+ const d = createPath({ ...link, source: { ...s, x1: g.x0 }, target: { ...t, x0: g.x1 }, y0: g.y0, y1: g.y1, width: w }) ?? '';
258
+ const profiel = doekProfiel(g);
259
+ const sterkte = Math.min(1, Math.max(0, markering?.sterkte.positie ?? 0));
260
+ const midden = doekPunt(g, markering?.t.positie ?? 0.5);
261
+ const maskId = `${effectId}-local-${i}`;
262
+ return _jsxs("g", { children: [sterkte > 0.002 && _jsxs(_Fragment, { children: [_jsx("defs", { children: _jsx("mask", { id: maskId, maskUnits: "userSpaceOnUse", x: g.x0 - 30, y: Math.min(g.y0, g.y1) - w - 40, width: g.x1 - g.x0 + 60, height: Math.abs(g.y1 - g.y0) + 2 * w + 80, style: { maskType: 'alpha' }, children: _jsx("ellipse", { cx: midden.x, cy: midden.y, rx: profiel.radius * 0.8, ry: w / 2 + 16, fill: kleur, filter: `url(#${effectId}-soft-mask)` }) }) }), _jsxs("g", { pointerEvents: "none", "aria-hidden": "true", mask: `url(#${maskId})`, children: [_jsx("path", { d: d, fill: "none", stroke: "var(--foreground)", strokeWidth: Math.max(1, w), strokeOpacity: sterkte * 0.18, filter: `url(#${effectId}-shadow)` }), _jsx("path", { d: d, fill: "none", stroke: kleur, strokeWidth: Math.max(1, w) + 5, strokeOpacity: sterkte * 0.55, filter: `url(#${effectId}-glow)` })] })] }), _jsx("path", { d: d, fill: "none", stroke: kleur, tabIndex: 0, "aria-label": `${titel}: ${tekst}`, strokeOpacity: actief ? (linkAan(i) ? 0.8 : 0.07) : 0.35, strokeWidth: Math.max(1, w), style: { transition: 'stroke-opacity 150ms' }, className: "cursor-pointer outline-none", onMouseEnter: toon, onMouseLeave: uit, onFocus: toon, onBlur: uit }), sterkte > 0.002 && _jsx("path", { d: d, fill: "none", stroke: kleur, strokeWidth: Math.max(1, w), strokeOpacity: sterkte * 0.25, mask: `url(#${maskId})`, pointerEvents: "none", "aria-hidden": "true" })] }, i);
263
+ }), graph.nodes.map((node, i) => {
264
+ const positie = posities.get(node);
265
+ const x = positie?.x ?? node.x0 ?? 0;
266
+ const y = positie?.y0 ?? node.y0 ?? 0;
267
+ const h = Math.max(2, (positie?.y1 ?? node.y1 ?? y) - y);
268
+ const tekst = `${aantalVan(node)} ${node.paars ? 'meldingen' : 'werkbonnen'}${node.paars ? '' : ` · ${pct(aantalVan(node))} van aangemaakt`}`;
269
+ const toon = () => { setActief({ soort: 'node', index: i }); setTip({ titel: node.name, tekst }); };
270
+ return _jsxs("g", { tabIndex: 0, role: "img", "aria-label": `${node.name}: ${tekst}`, className: "cursor-pointer outline-none", opacity: nodeAan(i) ? 1 : 0.3, style: { transition: 'opacity 150ms' }, onMouseEnter: toon, onMouseLeave: uit, onFocus: toon, onBlur: uit, children: [_jsx("rect", { x: x - 4, y: y - 4, width: 24, height: Math.max(12, h + 8), fill: "transparent" }), _jsx("rect", { x: x, y: y, width: 10, height: h, rx: 3, fill: kleuren[i] }), _jsx("text", { x: x + 14, y: y + h / 2, dominantBaseline: "central", textAnchor: "start", fill: "var(--foreground)", stroke: "var(--background)", strokeWidth: 4, paintOrder: "stroke", strokeLinejoin: "round", fontSize: 11 * textScale, fontWeight: 600, children: aantalVan(node) })] }, i);
271
+ })] });
272
+ }
273
+ export function SankeyChart({ data, total, onKleuren, indeling = STANDAARD_INDELING, onTip }) {
274
+ const setTip = (t) => onTip?.(t);
275
+ const extraNode = data.nodes.findIndex(n => n.name === FUNNEL_EXTRA);
276
+ const extraAantal = extraNode >= 0 ? data.nodes[extraNode].value : 0;
277
+ const eindAantal = data.nodes.filter(n => n.paars && n.name !== FUNNEL_EXTRA).length;
278
+ const [inhoud, setInhoud] = useState(288);
279
+ // De Sankey-layout rekent op een vaste hoogte; het zichtbare vak krimpt op de
280
+ // getekende funnel zodat de pop-up direct onder de funnel komt te staan.
281
+ const LAYOUT = 288;
282
+ return _jsx("div", { className: "dashboard-funnel-chart relative w-full", style: { height: inhoud }, "aria-label": "Werkbonnen van herkomst tot betaald", children: _jsx(ParentSize, { debounceTime: 30, children: ({ width, height }) => {
283
+ if (width < 40 || height < 40 || total <= 0)
284
+ return null;
285
+ // Ruimte onder de hoofdstroom voor de paarse tak, op de afgevlakte schaal
286
+ // met de extra dunne factor van de extra-werk-tak.
287
+ const druk = (v) => Math.pow(Math.max(0, v), 0.75);
288
+ const extraDruk = druk(extraAantal) * EXTRA_SCHAAL;
289
+ const reserve = extraAantal > 0 ? (LAYOUT - 28 - 18 - 8 * Math.max(0, eindAantal - 1)) * (extraDruk / (druk(total) + extraDruk)) + 18 + 8 * Math.max(0, eindAantal - 1) : 0;
290
+ return _jsx("svg", { "data-sankey": true, width: width, height: height, viewBox: `0 0 ${width} ${inhoud}`, preserveAspectRatio: "none", role: "img", "aria-label": "Werkbonnenfunnel", children: _jsx(Sankey, { root: { nodes: data.nodes.map(n => ({ ...n, aantal: n.value, value: Math.pow(Math.max(0, n.value), 0.75) })), links: data.links.map(l => l.target === extraNode ? { ...l, echt: l.value, value: 1e-4 } : { ...l, echt: l.value, value: Math.pow(Math.max(0, l.value), 0.75) }) }, nodeWidth: 10, nodePadding: 12, nodeAlign: sankeyLeft, nodeSort: (a, b) => rangFunnelNode(a.name) - rangFunnelNode(b.name), extent: [[8, 14], [width - 28, LAYOUT - 14 - reserve]], children: ({ graph, createPath }) => _jsx(Grafiek, { graph: graph, createPath: createPath, total: total, onKleuren: onKleuren, setTip: setTip, indeling: indeling, hoogte: LAYOUT, textScale: inhoud / height, onInhoud: h => setInhoud(h + 8) }) }) });
291
+ } }) });
292
+ }
@@ -1,6 +1,12 @@
1
1
  import type { FlowModule } from "@flowselections/core";
2
2
  export * from "./_core-safelist";
3
- export { DashboardInstallatieTechniekPage } from "./components/DashboardInstallatieTechniekPage";
4
- export { InstallatieTechniekLogo } from "./components/InstallatieTechniekLogo";
5
- export declare const dashboardInstallatieTechniekModule: FlowModule;
3
+ export { DashboardPlannerInstallatieTechniekPage } from "./components/DashboardPlannerInstallatieTechniekPage";
4
+ export { DashboardSnelkoppelingen, SnelkoppelingToevoegenDialog } from "./components/DashboardSnelkoppelingen";
5
+ export { PlannerPaginaKiezer } from "./components/PlannerPaginaKiezer";
6
+ export { ModulePlaceholderPage } from "./components/ModulePlaceholderPage";
7
+ export { PlannerInstallatieTechniekLogo } from "./components/PlannerInstallatieTechniekLogo";
8
+ export { WerkbonnenFunnel, WerkbonnenFunnelInhoud } from './components/WerkbonnenFunnel';
9
+ export type { WerkbonFunnelData, FunnelPeriode } from './lib/dashboard-werkbon-funnel';
10
+ export type { FunnelIndeling } from './lib/funnel-indeling';
11
+ export declare const dashboardPlannerInstallatieTechniekModule: FlowModule;
6
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,gCAAgC,EAAE,MAAM,+CAA+C,CAAC;AACjG,OAAO,EAAE,uBAAuB,EAAE,MAAM,sCAAsC,CAAC;AAE/E,eAAO,MAAM,kCAAkC,EAAE,UAYhD,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,uCAAuC,EAAE,MAAM,sDAAsD,CAAC;AAC/G,OAAO,EAAE,wBAAwB,EAAE,4BAA4B,EAAE,MAAM,uCAAuC,CAAC;AAC/G,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AACvE,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAC3E,OAAO,EAAE,8BAA8B,EAAE,MAAM,6CAA6C,CAAC;AAC7F,OAAO,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACzF,YAAY,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AACvF,YAAY,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,eAAO,MAAM,yCAAyC,EAAE,UAYvD,CAAC"}
package/dist-lib/index.js CHANGED
@@ -5,15 +5,19 @@
5
5
  // ============================================================================
6
6
  import { Gauge } from "lucide-react";
7
7
  export * from "./_core-safelist";
8
- export { DashboardInstallatieTechniekPage } from "./components/DashboardInstallatieTechniekPage";
9
- export { InstallatieTechniekLogo } from "./components/InstallatieTechniekLogo";
10
- export const dashboardInstallatieTechniekModule = {
11
- id: "dashboard-installatietechniek",
8
+ export { DashboardPlannerInstallatieTechniekPage } from "./components/DashboardPlannerInstallatieTechniekPage";
9
+ export { DashboardSnelkoppelingen, SnelkoppelingToevoegenDialog } from "./components/DashboardSnelkoppelingen";
10
+ export { PlannerPaginaKiezer } from "./components/PlannerPaginaKiezer";
11
+ export { ModulePlaceholderPage } from "./components/ModulePlaceholderPage";
12
+ export { PlannerInstallatieTechniekLogo } from "./components/PlannerInstallatieTechniekLogo";
13
+ export { WerkbonnenFunnel, WerkbonnenFunnelInhoud } from './components/WerkbonnenFunnel';
14
+ export const dashboardPlannerInstallatieTechniekModule = {
15
+ id: "dashboard-planner-installatie-techniek",
12
16
  name: "Installatietechniek",
13
17
  version: "1.0.0",
14
18
  nav: {
15
19
  label: "Installatietechniek",
16
- href: "/dashboard-installatietechniek",
20
+ href: "/dashboard-planner-installatie-techniek",
17
21
  icon: Gauge,
18
22
  },
19
23
  settingsCards: [],