@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,100 @@
1
+ export function valideerFunnelPeriode(value) {
2
+ if (value !== 'week' && value !== 'maand' && value !== 'jaar')
3
+ throw new Error('Ongeldige periode');
4
+ return value;
5
+ }
6
+ // Invoer van het vak kan helemaal ontbreken (bijvoorbeeld een aanroep zonder
7
+ // gegevens). Een ontbrekende keuze valt terug op de standaardperiode; een
8
+ // echt verkeerde waarde wordt nog steeds afgewezen.
9
+ export function valideerFunnelInvoer(input) {
10
+ const waarde = (input && typeof input === 'object' ? input.periode : undefined) ?? 'jaar';
11
+ return valideerFunnelPeriode(waarde);
12
+ }
13
+ export function normaliseerFunnel(raw) {
14
+ if (!raw || typeof raw !== 'object')
15
+ throw new Error('Werkbonstatistieken ontbreken');
16
+ const r = raw;
17
+ const number = (key) => {
18
+ const value = r[key];
19
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0)
20
+ throw new Error('Ongeldige werkbonstatistieken');
21
+ return value;
22
+ };
23
+ if (typeof r.vanaf !== 'string' || typeof r.tot !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(r.vanaf) || !/^\d{4}-\d{2}-\d{2}$/.test(r.tot))
24
+ throw new Error('Periode ontbreekt');
25
+ if (!Array.isArray(r.soorten))
26
+ throw new Error('Herkomst ontbreekt');
27
+ const result = {
28
+ periode: valideerFunnelPeriode(r.periode), vanaf: r.vanaf, tot: r.tot,
29
+ totaal: number('totaal'), afgerond: number('afgerond'), betaald: number('betaald'),
30
+ extraWerk: number('extraWerk'), extraWerkBetaald: number('extraWerkBetaald'), onbekend: number('onbekend'),
31
+ extraMeldingen: number('extraMeldingen'), extraAfgerond: number('extraAfgerond'), extraNietGeaccepteerd: number('extraNietGeaccepteerd'),
32
+ soorten: r.soorten.map((item) => {
33
+ if (!item || typeof item !== 'object')
34
+ throw new Error('Herkomst ongeldig');
35
+ const s = item;
36
+ if (typeof s.soort !== 'string' || typeof s.aantal !== 'number' || !Number.isSafeInteger(s.aantal) || s.aantal < 0)
37
+ throw new Error('Herkomst ongeldig');
38
+ return { soort: s.soort, aantal: s.aantal };
39
+ }),
40
+ };
41
+ if (result.betaald > result.afgerond || result.afgerond > result.totaal || result.extraWerk > result.afgerond || result.extraWerkBetaald > result.extraWerk || result.extraAfgerond + result.extraNietGeaccepteerd > result.extraMeldingen || result.soorten.reduce((sum, s) => sum + s.aantal, 0) !== result.totaal)
42
+ throw new Error('Werkbonstatistieken sluiten niet aan');
43
+ return result;
44
+ }
45
+ export const FUNNEL_PAARS = 'var(--funnel-extra)';
46
+ export const FUNNEL_NAVY = 'var(--funnel-navy)';
47
+ export const FUNNEL_HERKOMST = {
48
+ standby: { label: 'Standby (SB)', kleur: 'var(--chart-1)' },
49
+ mail: { label: 'Uit de mail', kleur: 'var(--chart-2)' },
50
+ extra_werk: { label: 'Extra werk', kleur: 'var(--chart-3)' },
51
+ handmatig: { label: 'Handmatig', kleur: 'var(--chart-4)' },
52
+ };
53
+ // Eindstaten die nog open zijn, komen altijd onderaan hun kolom te staan, ongeacht het aantal.
54
+ export const FUNNEL_ONDERAAN = ['Nog niet afgerond', 'Nog niet betaald', 'Extra werk in behandeling', 'Extra werk niet geaccepteerd'];
55
+ // Extra-werkmeldingen staan altijd onder Betaald en Nog niet betaald; het paarse blok wordt
56
+ // in de grafiek visueel uitgerekt tot de gezamenlijke hoogte van die twee (zie sankey-chart).
57
+ export const FUNNEL_EXTRA = 'Extra werk meldingen';
58
+ export function rangFunnelNode(name) {
59
+ if (name === FUNNEL_EXTRA)
60
+ return 2;
61
+ if (name === 'Nog niet betaald')
62
+ return 1;
63
+ return FUNNEL_ONDERAAN.includes(name) ? 1 : 0;
64
+ }
65
+ export function maakFunnelStromen(data) {
66
+ const nodes = data.soorten.filter(s => s.aantal > 0).map(s => {
67
+ return { name: FUNNEL_HERKOMST[s.soort]?.label ?? s.soort, value: s.aantal, kleur: FUNNEL_HERKOMST[s.soort]?.kleur ?? 'var(--muted-foreground)' };
68
+ });
69
+ const totaal = nodes.length;
70
+ nodes.push({ name: 'Aangemaakt', value: data.totaal, kleur: FUNNEL_NAVY });
71
+ const links = nodes.slice(0, totaal).map((s, i) => ({ source: i, target: totaal, value: s.value }));
72
+ const voegToe = (source, name, value, paars = false) => {
73
+ if (value <= 0)
74
+ return -1;
75
+ const target = nodes.length;
76
+ nodes.push({ name, value, kleur: paars ? FUNNEL_PAARS : '', paars });
77
+ links.push({ source, target, value });
78
+ return target;
79
+ };
80
+ const afgerond = voegToe(totaal, 'Afgerond', data.afgerond);
81
+ voegToe(totaal, 'Nog niet afgerond', data.totaal - data.afgerond);
82
+ if (afgerond >= 0) {
83
+ voegToe(afgerond, 'Betaald', data.betaald);
84
+ voegToe(afgerond, 'Nog niet betaald', data.afgerond - data.betaald);
85
+ // Alle extra-werkmeldingen van monteurs, gesplitst naar afgerond / niet geaccepteerd.
86
+ const extra = voegToe(afgerond, FUNNEL_EXTRA, data.extraMeldingen, true);
87
+ if (extra >= 0) {
88
+ voegToe(extra, 'Extra werk afgerond', data.extraAfgerond, true);
89
+ voegToe(extra, 'Extra werk in behandeling', data.extraMeldingen - data.extraAfgerond - data.extraNietGeaccepteerd, true);
90
+ voegToe(extra, 'Extra werk niet geaccepteerd', data.extraNietGeaccepteerd, true);
91
+ }
92
+ }
93
+ return { nodes, links };
94
+ }
95
+ /** Tekst en knop per foutstatus; bij 'ok' is er niets te melden. */
96
+ export function funnelFoutMelding(status) {
97
+ return status === 'sessie'
98
+ ? { tekst: 'Je sessie is verlopen. Log opnieuw in.', actie: 'inloggen' }
99
+ : { tekst: 'Werkbonstatistieken konden niet worden geladen.', actie: 'opnieuw' };
100
+ }
@@ -0,0 +1,13 @@
1
+ import { type FunnelPeriode, type FunnelStatus } from './dashboard-werkbon-funnel';
2
+ /** Haalt de bearer-token uit een verzoekheader; alles anders dan "Bearer ..." is leeg. */
3
+ export declare function leesBearerToken(headers: Headers | undefined): string;
4
+ export interface FunnelOpvraag {
5
+ periode: FunnelPeriode;
6
+ token: string;
7
+ /** Alleen voor controles; in de module komen url en key uit de omgeving. */
8
+ url?: string;
9
+ key?: string;
10
+ }
11
+ /** Werkbonstatistieken van de eigenaar van het token, of de reden dat het niet lukte. */
12
+ export declare function haalWerkbonFunnel({ periode, token, url, key }: FunnelOpvraag): Promise<FunnelStatus>;
13
+ //# sourceMappingURL=dashboard-werkbon-funnel.server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard-werkbon-funnel.server.d.ts","sourceRoot":"","sources":["../../src/lib/dashboard-werkbon-funnel.server.ts"],"names":[],"mappings":"AAeA,OAAO,EAAqB,KAAK,aAAa,EAAE,KAAK,YAAY,EAA0B,MAAM,4BAA4B,CAAC;AAE9H,0FAA0F;AAC1F,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAGpE;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,aAAa,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,yFAAyF;AACzF,wBAAsB,iBAAiB,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CA+B1G"}
@@ -0,0 +1,53 @@
1
+ // ============================================================================
2
+ // src/lib/dashboard-werkbon-funnel.server.ts
3
+ // ============================================================================
4
+ // Ophaal-laag voor de werkbonstatistieken, buiten de server-function zodat de
5
+ // mislukroutes los te controleren zijn.
6
+ //
7
+ // VASTE REGEL: geen enkele mislukte stap gooit. Een ontbrekende configuratie,
8
+ // een lege, vervalste of verlopen sessie en een mislukte opvraging leveren allen
9
+ // een status terug. Een gooiende server-function blankeerde het volledige
10
+ // dashboard. 'sessie' betekent dat de inlog niet meer gold en vraagt de gebruiker
11
+ // om opnieuw in te loggen; 'data' betekent dat de cijfers niet opgevraagd of niet
12
+ // volledig waren en biedt de opvraging opnieuw aan.
13
+ // De sessie van de aanroeper wordt nooit geraden of vervangen: zonder geldig
14
+ // token wordt er niets opgevraagd (fail closed).
15
+ // ============================================================================
16
+ import { normaliseerFunnel } from './dashboard-werkbon-funnel';
17
+ /** Haalt de bearer-token uit een verzoekheader; alles anders dan "Bearer ..." is leeg. */
18
+ export function leesBearerToken(headers) {
19
+ const auth = headers?.get('authorization') ?? '';
20
+ return auth.startsWith('Bearer ') ? auth.slice(7) : '';
21
+ }
22
+ /** Werkbonstatistieken van de eigenaar van het token, of de reden dat het niet lukte. */
23
+ export async function haalWerkbonFunnel({ periode, token, url, key }) {
24
+ const basis = url === undefined ? process.env.SUPABASE_URL : url;
25
+ const sleutel = key === undefined ? process.env.SUPABASE_PUBLISHABLE_KEY : key;
26
+ if (!basis || !sleutel) {
27
+ console.error('[funnel] SUPABASE_URL / SUPABASE_PUBLISHABLE_KEY ontbreken');
28
+ return { status: 'data' };
29
+ }
30
+ if (!token)
31
+ return { status: 'sessie' };
32
+ const { createClient } = await import('@supabase/supabase-js');
33
+ const client = createClient(basis, sleutel, {
34
+ global: { headers: { Authorization: `Bearer ${token}` } },
35
+ auth: { storage: undefined, persistSession: false, autoRefreshToken: false },
36
+ });
37
+ const user = await client.auth.getUser(token);
38
+ if (user.error || !user.data.user)
39
+ return { status: 'sessie' };
40
+ const resultaat = await client.rpc('dashboard_werkbon_funnel', { _periode: periode });
41
+ if (resultaat.error) {
42
+ console.error('[funnel] opvragen mislukt:', resultaat.error.message);
43
+ return { status: 'data' };
44
+ }
45
+ try {
46
+ const funnel = normaliseerFunnel(resultaat.data);
47
+ return { status: 'ok', funnel };
48
+ }
49
+ catch (fout) {
50
+ console.error('[funnel] onvolledige statistieken:', fout.message);
51
+ return { status: 'data' };
52
+ }
53
+ }
@@ -0,0 +1,13 @@
1
+ export declare const FUNNEL_BALKEN: readonly ["Standby (SB)", "Uit de mail", "Extra werk", "Handmatig", "Aangemaakt", "Afgerond", "Nog niet afgerond", "Betaald", "Nog niet betaald", "Extra werk meldingen", "Extra werk afgerond", "Extra werk in behandeling", "Extra werk niet geaccepteerd"];
2
+ export type FunnelIndeling = Partial<Record<typeof FUNNEL_BALKEN[number], number>>;
3
+ export declare const STANDAARD_INDELING: FunnelIndeling;
4
+ export interface IndelingBalk {
5
+ naam: string;
6
+ x: number;
7
+ y0: number;
8
+ y1: number;
9
+ paars?: boolean;
10
+ }
11
+ export declare function valideerIndeling(raw: unknown): FunnelIndeling;
12
+ export declare function plaatsIndeling(balken: IndelingBalk[], voorkeur: FunnelIndeling, hoogte: number): IndelingBalk[];
13
+ //# sourceMappingURL=funnel-indeling.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"funnel-indeling.d.ts","sourceRoot":"","sources":["../../src/lib/funnel-indeling.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,aAAa,+PAAgQ,CAAC;AAC3R,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,aAAa,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AAKnF,eAAO,MAAM,kBAAkB,EAAE,cAchC,CAAC;AACF,MAAM,WAAW,YAAY;IAAG,IAAI,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE;AAClG,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,cAAc,CAS7D;AACD,wBAAgB,cAAc,CAAC,MAAM,EAAE,YAAY,EAAE,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,YAAY,EAAE,CA4B/G"}
@@ -0,0 +1,74 @@
1
+ export const FUNNEL_BALKEN = ['Standby (SB)', 'Uit de mail', 'Extra werk', 'Handmatig', 'Aangemaakt', 'Afgerond', 'Nog niet afgerond', 'Betaald', 'Nog niet betaald', 'Extra werk meldingen', 'Extra werk afgerond', 'Extra werk in behandeling', 'Extra werk niet geaccepteerd'];
2
+ // De vaste standaardindeling van de funnel: de balkposities die zijn gekozen en
3
+ // daarmee het uiterlijk van de module bepalen. Persoonlijke indelingen worden niet
4
+ // meer gelezen of geschreven; deze waarden gelden voor elke gebruiker bij elke klant.
5
+ export const STANDAARD_INDELING = {
6
+ 'Standby (SB)': 0.5977696337950241,
7
+ 'Uit de mail': 0.4492378737706331,
8
+ 'Extra werk': 0,
9
+ 'Handmatig': 0.23219074328449324,
10
+ 'Aangemaakt': 0.3173956136726815,
11
+ 'Afgerond': 0.1681476323291577,
12
+ 'Nog niet afgerond': 0.5066738243153883,
13
+ 'Betaald': 0.03344231329512559,
14
+ 'Nog niet betaald': 0.20742258065625346,
15
+ 'Extra werk meldingen': 0.3628957403273809,
16
+ 'Extra werk afgerond': 0.3289375620039681,
17
+ 'Extra werk in behandeling': 0.3830808221726188,
18
+ 'Extra werk niet geaccepteerd': 0.46444103422619026,
19
+ };
20
+ export function valideerIndeling(raw) {
21
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
22
+ throw new Error('Ongeldige indeling');
23
+ const result = {};
24
+ for (const [key, value] of Object.entries(raw)) {
25
+ const naam = FUNNEL_BALKEN.find(n => n === key);
26
+ if (!naam || typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > 1)
27
+ throw new Error('Ongeldige balkpositie');
28
+ result[naam] = value;
29
+ }
30
+ return result;
31
+ }
32
+ export function plaatsIndeling(balken, voorkeur, hoogte) {
33
+ const result = balken.map(b => ({ ...b }));
34
+ const groepen = new Map();
35
+ for (const b of result) {
36
+ const groep = groepen.get(b.x) ?? [];
37
+ groep.push(b);
38
+ groepen.set(b.x, groep);
39
+ }
40
+ for (const groep of groepen.values()) {
41
+ groep.sort((a, b) => a.y0 - b.y0);
42
+ const totaal = groep.reduce((som, b) => som + b.y1 - b.y0, 0);
43
+ const gap = Math.min(6, Math.max(0, (hoogte - 8 - totaal) / Math.max(1, groep.length - 1)));
44
+ let onder = 4;
45
+ for (const b of groep) {
46
+ const h = b.y1 - b.y0;
47
+ const key = FUNNEL_BALKEN.find(n => n === b.naam);
48
+ const gewenst = key && voorkeur[key] !== undefined ? (voorkeur[key] ?? 0) * hoogte - h / 2 : b.y0;
49
+ b.y0 = Math.max(onder, gewenst);
50
+ b.y1 = b.y0 + h;
51
+ onder = b.y1 + gap;
52
+ }
53
+ const extraHoogte = result.find(b => b.naam === 'Extra werk meldingen');
54
+ const extraRuimte = extraHoogte ? extraHoogte.y1 - extraHoogte.y0 + 6 : 0;
55
+ const betaalIndex = groep.reduce((laatste, b, i) => b.naam === 'Betaald' || b.naam === 'Nog niet betaald' ? i : laatste, -1);
56
+ let boven = hoogte - 4;
57
+ for (const b of [...groep].reverse()) {
58
+ if (groep.indexOf(b) === betaalIndex)
59
+ boven = Math.min(boven, hoogte - 4 - extraRuimte);
60
+ const h = b.y1 - b.y0;
61
+ b.y1 = Math.min(b.y1, boven);
62
+ b.y0 = b.y1 - h;
63
+ boven = b.y0 - gap;
64
+ }
65
+ }
66
+ const betaaldBodem = Math.max(4, ...result.filter(b => b.naam === 'Betaald' || b.naam === 'Nog niet betaald').map(b => b.y1));
67
+ const extra = result.find(b => b.naam === 'Extra werk meldingen');
68
+ if (extra) {
69
+ const h = extra.y1 - extra.y0;
70
+ extra.y0 = Math.min(hoogte - 4 - h, Math.max(extra.y0, betaaldBodem + 6));
71
+ extra.y1 = extra.y0 + h;
72
+ }
73
+ return result;
74
+ }
@@ -0,0 +1,25 @@
1
+ export interface DoekGeometrie {
2
+ x0: number;
3
+ y0: number;
4
+ x1: number;
5
+ y1: number;
6
+ breedte: number;
7
+ }
8
+ export interface DoekVeer {
9
+ positie: number;
10
+ snelheid: number;
11
+ }
12
+ export declare function doekProfiel(g: DoekGeometrie): {
13
+ radius: number;
14
+ };
15
+ export declare function doekPunt(g: DoekGeometrie, u: number): {
16
+ x: number;
17
+ y: number;
18
+ };
19
+ export declare function stapDoekVeer(veer: DoekVeer, doel: number, dt: number, stijfheid?: number, demping?: number): DoekVeer;
20
+ export declare function doekNabijheid(g: DoekGeometrie, x: number, y: number): {
21
+ t: number;
22
+ kracht: number;
23
+ afstand: number;
24
+ };
25
+ //# sourceMappingURL=sankey-doek.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sankey-doek.d.ts","sourceRoot":"","sources":["../../src/lib/sankey-doek.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAAG,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE;AAClG,MAAM,WAAW,QAAQ;IAAG,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE;AAI/D,wBAAgB,WAAW,CAAC,CAAC,EAAE,aAAa;;EAG3C;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,aAAa,EAAE,CAAC,EAAE,MAAM;;;EAMnD;AAGD,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,SAAS,SAAM,EAAE,OAAO,SAAK,GAAG,QAAQ,CAS9G;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,aAAa,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM;;;;EAUnE"}
@@ -0,0 +1,38 @@
1
+ // Afmeting van de zachte masker-ellipse rond de donkere markering: de straal
2
+ // groeit met de lijndikte en wordt begrensd door de lijnlengte.
3
+ export function doekProfiel(g) {
4
+ const lengte = Math.max(0, g.x1 - g.x0);
5
+ return { radius: Math.min(lengte * 0.44, Math.max(32, 32 + Math.max(0, g.breedte) * 1.1)) };
6
+ }
7
+ export function doekPunt(g, u) {
8
+ const v = 1 - u, mx = (g.x0 + g.x1) / 2;
9
+ return {
10
+ x: v ** 3 * g.x0 + 3 * v * v * u * mx + 3 * v * u * u * mx + u ** 3 * g.x1,
11
+ y: v ** 3 * g.y0 + 3 * v * v * u * g.y0 + 3 * v * u * u * g.y1 + u ** 3 * g.y1,
12
+ };
13
+ }
14
+ // Semi-implicit spring integration, independent of display refresh rate.
15
+ export function stapDoekVeer(veer, doel, dt, stijfheid = 150, demping = 21) {
16
+ let { positie, snelheid } = veer;
17
+ const stappen = Math.max(1, Math.ceil(Math.min(dt, 0.05) / (1 / 120)));
18
+ const h = Math.min(dt, 0.05) / stappen;
19
+ for (let i = 0; i < stappen; i++) {
20
+ snelheid += ((doel - positie) * stijfheid - snelheid * demping) * h;
21
+ positie += snelheid * h;
22
+ }
23
+ return { positie, snelheid };
24
+ }
25
+ export function doekNabijheid(g, x, y) {
26
+ let afstand = Infinity, t = 0;
27
+ for (let i = 0; i <= 100; i++) {
28
+ const u = i / 100, p = doekPunt(g, u), d = Math.hypot(x - p.x, y - p.y);
29
+ if (d < afstand) {
30
+ afstand = d;
31
+ t = u;
32
+ }
33
+ }
34
+ // Distance to the ribbon edge, not its centre: thick ribbons remain interactive.
35
+ const buiten = Math.max(0, afstand - g.breedte / 2);
36
+ const nabij = Math.max(0, 1 - buiten / 28);
37
+ return { t, kracht: nabij * nabij * (3 - 2 * nabij), afstand: buiten };
38
+ }
package/package.json CHANGED
@@ -1,15 +1,74 @@
1
1
  {
2
2
  "name": "@flowselections/dashboard-planner-installatie-techniek",
3
- "version": "1.0.3",
3
+ "version": "1.0.37",
4
4
  "flowselections": {
5
- "moduleId": "dashboard-installatietechniek",
5
+ "moduleId": "dashboard-planner-installatie-techniek",
6
6
  "pages": [
7
7
  {
8
- "export": "DashboardInstallatieTechniekPage",
9
- "route": "dashboard-installatietechniek",
8
+ "export": "DashboardPlannerInstallatieTechniekPage",
9
+ "route": "dashboard-planner-installatie-techniek",
10
10
  "title": "Installatietechniek"
11
+ },
12
+ {
13
+ "export": "ModulePlaceholderPage",
14
+ "route": "dashboard-planner-installatie-techniek/module",
15
+ "title": "Module"
11
16
  }
12
- ]
17
+ ],
18
+ "handover": {
19
+ "titel": "Dashboard Planner Installatietechniek",
20
+ "samenvatting": "Dashboard met moduleaantallen en statistieken; werkbonsoorten via blijvende dagtellers en geschreven uren over de dagelijks opschuivende afgelopen twaalf maanden. Betaalpercentage op aantallen invoices met sent_at in de afgelopen 28 Nederlandse kalenderdagen en status betaald, via exacte tellingen. Facturentegel telt werkbonnen zonder factuur (invoices.source_ref) plus facturen in status concept; de urengrafiek toont gewerkte uren van deze week (ma–zo) uit de werkbon-klok werkbon_time_logs (activity werken), overuren uit time_logs. Werkbonnenfunnel volgt aanmaakcohorten voor week, maand of opschuivend jaar naar afgerond en volledig betaald, met afzonderlijke extra-werktak en blijvende minimale historie. Snelkoppelingen mogen interne paden of externe http(s)-links zijn.",
21
+ "eigenTabellen": [
22
+ "dashboard_werkbon_tellers",
23
+ "dashboard_werkbon_dagtellers",
24
+ "dashboard_werkbon_historie",
25
+ "dashboard_werkbon_factuurhistorie",
26
+ "dashboard_werkbon_mailhistorie",
27
+ "dashboard_snelkoppelingen"
28
+ ],
29
+ "leestTabellen": [
30
+ "werkbonnen",
31
+ "itk_offertes",
32
+ "invoices",
33
+ "werkbon_mail_user_settings",
34
+ "email_auth_connections",
35
+ "email_auth_events",
36
+ "werkbon_email_state",
37
+ "werkbon_proposals",
38
+ "planning_items",
39
+ "leave_requests",
40
+ "uren_overuren_verklaringen",
41
+ "uren_standaardweek_wijzigingen",
42
+ "time_logs",
43
+ "werkbon_time_logs",
44
+ "dashboard_werkbon_tellers",
45
+ "dashboard_werkbon_dagtellers",
46
+ "dashboard_werkbon_historie",
47
+ "dashboard_werkbon_factuurhistorie",
48
+ "dashboard_werkbon_mailhistorie"
49
+ ],
50
+ "schrijftTabellen": [
51
+ "dashboard_werkbon_tellers",
52
+ "dashboard_werkbon_dagtellers",
53
+ "dashboard_werkbon_historie",
54
+ "dashboard_werkbon_factuurhistorie",
55
+ "dashboard_werkbon_mailhistorie",
56
+ "dashboard_snelkoppelingen"
57
+ ],
58
+ "biedtFuncties": [
59
+ "dashboard_werkbon_jaarstats",
60
+ "dashboard_werkbon_funnel",
61
+ "dashboard_funnel_betaald"
62
+ ],
63
+ "gebruiktFuncties": [
64
+ "dashboard_werkbon_jaarstats",
65
+ "dashboard_werkbon_funnel",
66
+ "dashboard_funnel_betaald",
67
+ "my_module_permissions",
68
+ "uren_heeft_pagina_toegang"
69
+ ],
70
+ "herzieningstermijnDagen": 90
71
+ }
13
72
  },
14
73
  "private": false,
15
74
  "sideEffects": false,
@@ -50,13 +109,17 @@
50
109
  "sonner": "^2.0.0"
51
110
  },
52
111
  "dependencies": {
53
- "@supabase/supabase-js": "^2.112.3"
112
+ "@supabase/supabase-js": "^2.112.3",
113
+ "@visx/event": "^4.0.0",
114
+ "@visx/responsive": "^4.0.0",
115
+ "@visx/sankey": "^4.0.0",
116
+ "motion": "^14.1.1"
54
117
  },
55
118
  "devDependencies": {
56
119
  "@cloudflare/vite-plugin": "^1.25.5",
57
120
  "@eslint/js": "^9.32.0",
58
- "@flowselections/core": "1.0.18",
59
- "@lovable.dev/vite-tanstack-config": "2.13.1",
121
+ "@flowselections/core": "1.0.29",
122
+ "@lovable.dev/vite-tanstack-config": "2.25.3",
60
123
  "@rsbuild/core": "^1.0.0",
61
124
  "@tailwindcss/vite": "^4.2.1",
62
125
  "@tanstack/react-query": "^5.83.0",
Binary file