@flowselections/dashboard-planner-installatie-techniek 1.0.2 → 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.
- package/README.md +0 -1
- package/dist-lib/components/DashboardPlannerInstallatieTechniekPage.d.ts +11 -0
- package/dist-lib/components/DashboardPlannerInstallatieTechniekPage.d.ts.map +1 -0
- package/dist-lib/components/DashboardPlannerInstallatieTechniekPage.js +173 -0
- package/dist-lib/components/DashboardSnelkoppelingen.d.ts +16 -0
- package/dist-lib/components/DashboardSnelkoppelingen.d.ts.map +1 -0
- package/dist-lib/components/DashboardSnelkoppelingen.js +123 -0
- package/dist-lib/components/DashboardStatistieken.d.ts +9 -0
- package/dist-lib/components/DashboardStatistieken.d.ts.map +1 -0
- package/dist-lib/components/DashboardStatistieken.js +80 -0
- package/dist-lib/components/ModulePlaceholderPage.d.ts +4 -0
- package/dist-lib/components/ModulePlaceholderPage.d.ts.map +1 -0
- package/dist-lib/components/ModulePlaceholderPage.js +11 -0
- package/dist-lib/components/PlannerInstallatieTechniekLogo.d.ts +6 -0
- package/dist-lib/components/PlannerInstallatieTechniekLogo.d.ts.map +1 -0
- package/dist-lib/components/PlannerInstallatieTechniekLogo.js +4 -0
- package/dist-lib/components/PlannerPaginaKiezer.d.ts +10 -0
- package/dist-lib/components/PlannerPaginaKiezer.d.ts.map +1 -0
- package/dist-lib/components/PlannerPaginaKiezer.js +15 -0
- package/dist-lib/components/WerkbonnenFunnel.d.ts +8 -0
- package/dist-lib/components/WerkbonnenFunnel.d.ts.map +1 -0
- package/dist-lib/components/WerkbonnenFunnel.js +68 -0
- package/dist-lib/components/WerkurenGrafiek.d.ts +26 -0
- package/dist-lib/components/WerkurenGrafiek.d.ts.map +1 -0
- package/dist-lib/components/WerkurenGrafiek.js +71 -0
- package/dist-lib/components/dashboard-viewport.d.ts +2 -0
- package/dist-lib/components/dashboard-viewport.d.ts.map +1 -0
- package/dist-lib/components/dashboard-viewport.js +97 -0
- package/dist-lib/components/ui/sankey-chart.d.ts +30 -0
- package/dist-lib/components/ui/sankey-chart.d.ts.map +1 -0
- package/dist-lib/components/ui/sankey-chart.js +292 -0
- package/dist-lib/index.d.ts +9 -3
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +9 -5
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +10 -22015
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/auth-middleware.js +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +2 -22012
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.js +2 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +2 -22012
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/previewAuthStorage.d.ts +6 -0
- package/dist-lib/integrations/supabase/previewAuthStorage.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/previewAuthStorage.js +99 -0
- package/dist-lib/integrations/supabase/types.d.ts +23780 -10104
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.js +1 -0
- package/dist-lib/lib/attach-core-auth.d.ts +2 -0
- package/dist-lib/lib/attach-core-auth.d.ts.map +1 -0
- package/dist-lib/lib/attach-core-auth.js +15 -0
- package/dist-lib/lib/dashboard-constanten.d.ts +4 -0
- package/dist-lib/lib/dashboard-constanten.d.ts.map +1 -0
- package/dist-lib/lib/dashboard-constanten.js +8 -0
- package/dist-lib/lib/dashboard-planner-installatie-techniek.functions.d.ts +13 -0
- package/dist-lib/lib/dashboard-planner-installatie-techniek.functions.d.ts.map +1 -0
- package/dist-lib/lib/dashboard-planner-installatie-techniek.functions.js +59 -0
- package/dist-lib/lib/dashboard-planner-installatie-techniek.modules.d.ts +11 -0
- package/dist-lib/lib/dashboard-planner-installatie-techniek.modules.d.ts.map +1 -0
- package/dist-lib/lib/dashboard-planner-installatie-techniek.modules.js +11 -0
- package/dist-lib/lib/dashboard-planner-installatie-techniek.server.d.ts +17 -0
- package/dist-lib/lib/dashboard-planner-installatie-techniek.server.d.ts.map +1 -0
- package/dist-lib/lib/dashboard-planner-installatie-techniek.server.js +239 -0
- package/dist-lib/lib/dashboard-planner-installatie-techniek.stats.server.d.ts +19 -0
- package/dist-lib/lib/dashboard-planner-installatie-techniek.stats.server.d.ts.map +1 -0
- package/dist-lib/lib/dashboard-planner-installatie-techniek.stats.server.js +214 -0
- package/dist-lib/lib/dashboard-planner-installatie-techniek.types.d.ts +68 -0
- package/dist-lib/lib/dashboard-planner-installatie-techniek.types.d.ts.map +1 -0
- package/dist-lib/lib/dashboard-planner-installatie-techniek.types.js +7 -0
- package/dist-lib/lib/dashboard-snelkoppelingen.catalogus.d.ts +27 -0
- package/dist-lib/lib/dashboard-snelkoppelingen.catalogus.d.ts.map +1 -0
- package/dist-lib/lib/dashboard-snelkoppelingen.catalogus.js +102 -0
- package/dist-lib/lib/dashboard-snelkoppelingen.d.ts +49 -0
- package/dist-lib/lib/dashboard-snelkoppelingen.d.ts.map +1 -0
- package/dist-lib/lib/dashboard-snelkoppelingen.js +55 -0
- package/dist-lib/lib/dashboard-tellingen.d.ts +12 -0
- package/dist-lib/lib/dashboard-tellingen.d.ts.map +1 -0
- package/dist-lib/lib/dashboard-tellingen.js +22 -0
- package/dist-lib/lib/dashboard-werkbon-funnel.d.ts +60 -0
- package/dist-lib/lib/dashboard-werkbon-funnel.d.ts.map +1 -0
- package/dist-lib/lib/dashboard-werkbon-funnel.js +100 -0
- package/dist-lib/lib/dashboard-werkbon-funnel.server.d.ts +13 -0
- package/dist-lib/lib/dashboard-werkbon-funnel.server.d.ts.map +1 -0
- package/dist-lib/lib/dashboard-werkbon-funnel.server.js +53 -0
- package/dist-lib/lib/funnel-indeling.d.ts +13 -0
- package/dist-lib/lib/funnel-indeling.d.ts.map +1 -0
- package/dist-lib/lib/funnel-indeling.js +74 -0
- package/dist-lib/lib/sankey-doek.d.ts +25 -0
- package/dist-lib/lib/sankey-doek.d.ts.map +1 -0
- package/dist-lib/lib/sankey-doek.js +38 -0
- package/package.json +71 -8
- package/public/favicon.png +0 -0
- /package/public/{logo.png → flowselections-assets/dashboard-planner-installatie-techniek/logo.png} +0 -0
package/README.md
CHANGED
|
@@ -6,7 +6,6 @@ This project was built with [Lovable](https://lovable.dev).
|
|
|
6
6
|
|
|
7
7
|
## Build with Lovable
|
|
8
8
|
|
|
9
|
-
Continue developing this project in the [Lovable editor](https://lovable.dev/projects/74cd71d5-b2ff-473a-8ed5-9a68ede2ca92).
|
|
10
9
|
|
|
11
10
|
- **Ship faster**: describe what you want to build and Lovable handles the code.
|
|
12
11
|
- **Stay in sync**: every change made in Lovable is committed straight to this repository.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DashboardData } from "../lib/dashboard-planner-installatie-techniek.types";
|
|
2
|
+
import type { FlowModule } from "@flowselections/core";
|
|
3
|
+
/** `modules`: optioneel door de shell meegegeven, voor de paginakeuze bij snelkoppelingen. */
|
|
4
|
+
export declare function DashboardPlannerInstallatieTechniekPage({ modules }?: {
|
|
5
|
+
modules?: FlowModule[];
|
|
6
|
+
}): import("react").JSX.Element;
|
|
7
|
+
export declare function DashboardModuleNavigatie({ kpis, loading }: {
|
|
8
|
+
kpis: DashboardData["kpis"];
|
|
9
|
+
loading: boolean;
|
|
10
|
+
}): import("react").JSX.Element;
|
|
11
|
+
//# sourceMappingURL=DashboardPlannerInstallatieTechniekPage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DashboardPlannerInstallatieTechniekPage.d.ts","sourceRoot":"","sources":["../../src/components/DashboardPlannerInstallatieTechniekPage.tsx"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qDAAqD,CAAC;AAIzF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AA0FvD,8FAA8F;AAC9F,wBAAgB,uCAAuC,CAAC,EAAE,OAAO,EAAE,GAAE;IAAE,OAAO,CAAC,EAAE,UAAU,EAAE,CAAA;CAAO,+BAyGnG;AAED,wBAAgB,wBAAwB,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE;IAAE,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,+BAiF5G"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// ============================================================================
|
|
3
|
+
// src/components/DashboardPlannerInstallatieTechniekPage.tsx
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Dashboard: één tegel per Installatie Techniek-module, met een telling uit
|
|
6
|
+
// de database. GEEN <Header /> — de shell levert die.
|
|
7
|
+
// ============================================================================
|
|
8
|
+
import { DASHBOARD_TIJDZONE } from "../lib/dashboard-constanten";
|
|
9
|
+
import { useEffect, useRef, useState } from "react";
|
|
10
|
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
11
|
+
import { useServerFn } from "@tanstack/react-start";
|
|
12
|
+
import { Link } from "@tanstack/react-router";
|
|
13
|
+
import { AlertTriangle, ArrowUpRight, CalendarDays, Clock, FileText, Mail, Receipt, RefreshCw, Wrench, } from "lucide-react";
|
|
14
|
+
import { Button, Card, CardContent, cn, Skeleton, toast, useAuth } from "@flowselections/core";
|
|
15
|
+
import { getDashboardData } from "../lib/dashboard-planner-installatie-techniek.functions";
|
|
16
|
+
import { MODULE_TEGELS } from "../lib/dashboard-planner-installatie-techniek.modules";
|
|
17
|
+
import { DashboardStatistieken, normaliseerStats } from "./DashboardStatistieken";
|
|
18
|
+
import { DashboardSnelkoppelingen } from "./DashboardSnelkoppelingen";
|
|
19
|
+
import { dashboardViewportStyles } from "./dashboard-viewport";
|
|
20
|
+
const ICONEN = {
|
|
21
|
+
planning: CalendarDays,
|
|
22
|
+
werkbonnen: Wrench,
|
|
23
|
+
urenregistratie: Clock,
|
|
24
|
+
"e-mail": Mail,
|
|
25
|
+
offertes: FileText,
|
|
26
|
+
facturen: Receipt,
|
|
27
|
+
};
|
|
28
|
+
const MODULE_KLEUREN = {
|
|
29
|
+
planning: {
|
|
30
|
+
badge: "bg-blue-500/10 text-blue-600 dark:text-blue-300",
|
|
31
|
+
chip: "bg-blue-500/5",
|
|
32
|
+
tint: "from-blue-500/10",
|
|
33
|
+
hoverBorder: "group-hover:border-blue-500/45",
|
|
34
|
+
hoverText: "group-hover:text-blue-600 dark:group-hover:text-blue-300",
|
|
35
|
+
},
|
|
36
|
+
werkbonnen: {
|
|
37
|
+
badge: "bg-orange-500/10 text-orange-600 dark:text-orange-300",
|
|
38
|
+
chip: "bg-orange-500/5",
|
|
39
|
+
tint: "from-orange-500/10",
|
|
40
|
+
hoverBorder: "group-hover:border-orange-500/45",
|
|
41
|
+
hoverText: "group-hover:text-orange-600 dark:group-hover:text-orange-300",
|
|
42
|
+
},
|
|
43
|
+
urenregistratie: {
|
|
44
|
+
badge: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-300",
|
|
45
|
+
chip: "bg-emerald-500/5",
|
|
46
|
+
tint: "from-emerald-500/10",
|
|
47
|
+
hoverBorder: "group-hover:border-emerald-500/45",
|
|
48
|
+
hoverText: "group-hover:text-emerald-600 dark:group-hover:text-emerald-300",
|
|
49
|
+
},
|
|
50
|
+
"e-mail": {
|
|
51
|
+
badge: "bg-violet-500/10 text-violet-600 dark:text-violet-300",
|
|
52
|
+
chip: "bg-violet-500/5",
|
|
53
|
+
tint: "from-violet-500/10",
|
|
54
|
+
hoverBorder: "group-hover:border-violet-500/45",
|
|
55
|
+
hoverText: "group-hover:text-violet-600 dark:group-hover:text-violet-300",
|
|
56
|
+
},
|
|
57
|
+
offertes: {
|
|
58
|
+
badge: "bg-slate-500/10 text-slate-600 dark:text-slate-300",
|
|
59
|
+
chip: "bg-slate-500/5",
|
|
60
|
+
tint: "from-slate-500/10",
|
|
61
|
+
hoverBorder: "group-hover:border-slate-500/45",
|
|
62
|
+
hoverText: "group-hover:text-slate-600 dark:group-hover:text-slate-300",
|
|
63
|
+
},
|
|
64
|
+
facturen: {
|
|
65
|
+
badge: "bg-red-500/10 text-red-600 dark:text-red-300",
|
|
66
|
+
chip: "bg-red-500/5",
|
|
67
|
+
tint: "from-red-500/10",
|
|
68
|
+
hoverBorder: "group-hover:border-red-500/45",
|
|
69
|
+
hoverText: "group-hover:text-red-600 dark:group-hover:text-red-300",
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
const STANDAARD_KLEUR = MODULE_KLEUREN.planning;
|
|
73
|
+
const EMPTY = {
|
|
74
|
+
kpis: {
|
|
75
|
+
planningNietIngepland: null,
|
|
76
|
+
werkbonnenOpen: null,
|
|
77
|
+
urenTeBeoordelen: null,
|
|
78
|
+
mailsTeControleren: null,
|
|
79
|
+
offertesMeerwerk: null,
|
|
80
|
+
facturenTeFactureren: null,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
function normaliseerDashboard(data) {
|
|
84
|
+
const d = (data ?? {});
|
|
85
|
+
const raw = (d.kpis && typeof d.kpis === "object" ? d.kpis : {});
|
|
86
|
+
const kpis = { ...EMPTY.kpis };
|
|
87
|
+
for (const k of Object.keys(kpis)) {
|
|
88
|
+
const v = raw[k];
|
|
89
|
+
kpis[k] = typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
90
|
+
}
|
|
91
|
+
return { kpis };
|
|
92
|
+
}
|
|
93
|
+
/** `modules`: optioneel door de shell meegegeven, voor de paginakeuze bij snelkoppelingen. */
|
|
94
|
+
export function DashboardPlannerInstallatieTechniekPage({ modules } = {}) {
|
|
95
|
+
const { user, profile } = useAuth();
|
|
96
|
+
const queryClient = useQueryClient();
|
|
97
|
+
const fetchDashboard = useServerFn(getDashboardData);
|
|
98
|
+
const [wekenTerug, setWekenTerug] = useState(0);
|
|
99
|
+
const mainRef = useRef(null);
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
const main = mainRef.current;
|
|
102
|
+
if (!main)
|
|
103
|
+
return;
|
|
104
|
+
const measure = () => {
|
|
105
|
+
const top = main.getBoundingClientRect().top + window.scrollY;
|
|
106
|
+
main.style.setProperty("--dashboard-height", `${Math.max(0, window.innerHeight - top)}px`);
|
|
107
|
+
};
|
|
108
|
+
measure();
|
|
109
|
+
const observer = new ResizeObserver(measure);
|
|
110
|
+
if (main.parentElement)
|
|
111
|
+
observer.observe(main.parentElement);
|
|
112
|
+
window.addEventListener("resize", measure);
|
|
113
|
+
return () => { observer.disconnect(); window.removeEventListener("resize", measure); };
|
|
114
|
+
}, []);
|
|
115
|
+
const { data, isLoading, isFetching, isError, refetch, error } = useQuery({
|
|
116
|
+
queryKey: ["dashboard-planner-installatie-techniek", wekenTerug],
|
|
117
|
+
queryFn: async () => {
|
|
118
|
+
try {
|
|
119
|
+
const res = await fetchDashboard({ data: { wekenTerug } });
|
|
120
|
+
if (res && typeof res === "object" && "error" in res && res.error) {
|
|
121
|
+
throw new Error(`Dashboard laden mislukt (${String(res.error)})`);
|
|
122
|
+
}
|
|
123
|
+
return res;
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
if (err instanceof Response) {
|
|
127
|
+
const tekst = await err.text().catch(() => "");
|
|
128
|
+
throw new Error(`Dashboard laden mislukt (${err.status}) ${tekst}`.trim());
|
|
129
|
+
}
|
|
130
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
staleTime: 30000,
|
|
134
|
+
retry: false,
|
|
135
|
+
});
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
const formatter = new Intl.DateTimeFormat("en-CA", { timeZone: DASHBOARD_TIJDZONE, year: "numeric", month: "2-digit", day: "2-digit" });
|
|
138
|
+
let day = formatter.format(new Date());
|
|
139
|
+
const checkDay = () => {
|
|
140
|
+
const next = formatter.format(new Date());
|
|
141
|
+
if (next !== day) {
|
|
142
|
+
day = next;
|
|
143
|
+
void queryClient.invalidateQueries({ queryKey: ["dashboard-planner-installatie-techniek"] });
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const timer = window.setInterval(checkDay, 15000);
|
|
147
|
+
window.addEventListener("focus", checkDay);
|
|
148
|
+
document.addEventListener("visibilitychange", checkDay);
|
|
149
|
+
return () => {
|
|
150
|
+
window.clearInterval(timer);
|
|
151
|
+
window.removeEventListener("focus", checkDay);
|
|
152
|
+
document.removeEventListener("visibilitychange", checkDay);
|
|
153
|
+
};
|
|
154
|
+
}, [queryClient]);
|
|
155
|
+
const dashboard = normaliseerDashboard(data);
|
|
156
|
+
const handleVernieuwen = () => {
|
|
157
|
+
queryClient.invalidateQueries({ queryKey: ["dashboard-planner-installatie-techniek"] });
|
|
158
|
+
refetch();
|
|
159
|
+
toast.success("Dashboard ververst");
|
|
160
|
+
};
|
|
161
|
+
return (_jsxs("main", { ref: mainRef, className: "planner-dashboard px-4 pb-6 pt-2 space-y-4 lg:px-6", children: [_jsx("style", { children: dashboardViewportStyles }), _jsxs("div", { className: "dashboard-toolbar flex flex-wrap items-center justify-between gap-3", children: [_jsxs("p", { className: "min-w-0 text-sm text-muted-foreground", children: ["Welkom terug", profile?.display_name || user?.email ? `, ${profile?.display_name ?? user?.email}` : "", ". Kies een module."] }), _jsx(DashboardSnelkoppelingen, { modules: modules }), _jsxs(Button, { onClick: handleVernieuwen, variant: "outline", size: "sm", className: "h-8 shrink-0 gap-2", children: [_jsx(RefreshCw, { className: `w-4 h-4 ${isFetching ? "animate-spin" : ""}` }), "Vernieuwen"] })] }), isError && (_jsxs("div", { className: "flex items-center gap-3 rounded-md border border-destructive/30 bg-destructive/5 p-4 text-sm", children: [_jsx(AlertTriangle, { className: "h-4 w-4 text-destructive" }), _jsx("span", { className: "text-muted-foreground", children: error instanceof Error && error.message.includes('unauthorized')
|
|
162
|
+
? 'Je sessie is verlopen — log opnieuw in.'
|
|
163
|
+
: 'Kon de aantallen niet laden — probeer het opnieuw met Vernieuwen.' })] })), _jsx(DashboardModuleNavigatie, { kpis: dashboard.kpis, loading: isLoading }), _jsx(DashboardStatistieken, { stats: normaliseerStats(data?.stats), wekenTerug: wekenTerug, onWeek: setWekenTerug, loading: isLoading })] }));
|
|
164
|
+
}
|
|
165
|
+
export function DashboardModuleNavigatie({ kpis, loading }) {
|
|
166
|
+
return (_jsxs("section", { "aria-labelledby": "dashboard-modules", className: "space-y-2", children: [_jsx("h2", { id: "dashboard-modules", className: "text-base font-semibold leading-5", children: "Modules" }), _jsx("div", { className: "dashboard-module-grid grid grid-cols-1 gap-3 min-[380px]:grid-cols-2 md:grid-cols-3 xl:grid-cols-6", children: MODULE_TEGELS.map((tegel) => {
|
|
167
|
+
const Icon = ICONEN[tegel.key] ?? FileText;
|
|
168
|
+
const kleur = MODULE_KLEUREN[tegel.key] ?? STANDAARD_KLEUR;
|
|
169
|
+
const waarde = tegel.kpi ? kpis[tegel.kpi] : null;
|
|
170
|
+
return (_jsx(Link, { to: "/dashboard-planner-installatie-techniek/module/$moduleKey", params: { moduleKey: tegel.key }, "aria-label": `${tegel.titel} openen`, className: "group block min-w-0 rounded-2xl focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", children: _jsx(Card, { className: cn("relative flex h-full flex-col overflow-hidden rounded-2xl border-border bg-card shadow-sm transition-all duration-200 group-hover:-translate-y-0.5 group-hover:shadow-md", kleur.hoverBorder), children: _jsxs(CardContent, { className: "dashboard-module-content relative flex h-full min-h-56 flex-col px-4 py-4", children: [_jsx("div", { "aria-hidden": "true", className: cn("pointer-events-none absolute inset-0 bg-gradient-to-b to-transparent", kleur.tint) }), _jsx(ArrowUpRight, { "aria-hidden": "true", className: cn("absolute right-3 top-3 h-4 w-4 text-muted-foreground transition-colors", kleur.hoverText) }), _jsx("div", { className: cn("dashboard-module-icon relative flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl transition-transform duration-200 group-hover:scale-105", kleur.badge), children: _jsx(Icon, { "aria-hidden": "true", className: "h-7 w-7" }) }), _jsx("p", { className: "dashboard-module-title relative mt-4 min-h-6 text-center text-base font-semibold leading-6", children: tegel.titel }), _jsx("div", { className: "dashboard-module-count relative mt-auto pt-4", children: tegel.kpi &&
|
|
171
|
+
(loading ? (_jsx(Skeleton, { className: "h-10 w-28 rounded-xl" })) : (_jsxs("div", { className: cn("inline-flex flex-col rounded-xl px-3 py-1.5 leading-tight", kleur.chip), children: [_jsx("span", { className: "text-xl font-semibold tabular-nums", title: waarde === null ? "Dit cijfer kon niet worden geteld" : undefined, children: waarde === null ? "–" : waarde.toLocaleString("nl-NL") }), _jsx("span", { className: "mt-0.5 text-[11px] text-muted-foreground", children: waarde === null ? "Niet beschikbaar" : tegel.getalLabel })] }))) })] }) }) }, tegel.key));
|
|
172
|
+
}) })] }));
|
|
173
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type FlowModule } from "@flowselections/core";
|
|
2
|
+
export declare function DashboardSnelkoppelingen({ modules }: {
|
|
3
|
+
modules?: FlowModule[];
|
|
4
|
+
}): import("react").JSX.Element;
|
|
5
|
+
export declare function SnelkoppelingToevoegenDialog({ open, onOpenChange, modules, bezig, vol, onAanmaken, }: {
|
|
6
|
+
open: boolean;
|
|
7
|
+
onOpenChange: (o: boolean) => void;
|
|
8
|
+
modules?: FlowModule[];
|
|
9
|
+
bezig: boolean;
|
|
10
|
+
vol?: boolean;
|
|
11
|
+
onAanmaken: (v: {
|
|
12
|
+
titel: string;
|
|
13
|
+
pad: string;
|
|
14
|
+
}) => void;
|
|
15
|
+
}): import("react").JSX.Element;
|
|
16
|
+
//# sourceMappingURL=DashboardSnelkoppelingen.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DashboardSnelkoppelingen.d.ts","sourceRoot":"","sources":["../../src/components/DashboardSnelkoppelingen.tsx"],"names":[],"mappings":"AAKA,OAAO,EAiBL,KAAK,UAAU,EAChB,MAAM,sBAAsB,CAAC;AAiB9B,wBAAgB,wBAAwB,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,CAAC,EAAE,UAAU,EAAE,CAAA;CAAE,+BAmI/E;AAED,wBAAgB,4BAA4B,CAAC,EAC3C,IAAI,EACJ,YAAY,EACZ,OAAO,EACP,KAAK,EACL,GAAW,EACX,UAAU,GACX,EAAE;IACD,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,OAAO,CAAC,EAAE,UAAU,EAAE,CAAC;IACvB,KAAK,EAAE,OAAO,CAAC;IACf,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,UAAU,EAAE,CAAC,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CACzD,+BA6FA"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Rij met persoonlijke snelkoppelingen + venster om ze toe te voegen.
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
5
|
+
import { Link, useRouter } from "@tanstack/react-router";
|
|
6
|
+
import { Plus, X } from "lucide-react";
|
|
7
|
+
import { Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Input, Label, Tabs, TabsContent, TabsList, TabsTrigger, supabase, toast, useAuth, useModuleListTransform, } from "@flowselections/core";
|
|
8
|
+
import { isExterneLink, isGeldigeLink, kanToevoegen, MAX_SNELKOPPELINGEN, } from "../lib/dashboard-snelkoppelingen";
|
|
9
|
+
import { maakPlannerCatalogus, leesSnelkoppelingRechten } from "../lib/dashboard-snelkoppelingen.catalogus";
|
|
10
|
+
import { PlannerPaginaKiezer } from "./PlannerPaginaKiezer";
|
|
11
|
+
const KEY = ["dashboard-snelkoppelingen"];
|
|
12
|
+
// De tabel is nieuw; core's client kent hem nog niet in zijn typen.
|
|
13
|
+
const tabel = () => supabase.from("dashboard_snelkoppelingen");
|
|
14
|
+
export function DashboardSnelkoppelingen({ modules }) {
|
|
15
|
+
const qc = useQueryClient();
|
|
16
|
+
const [open, setOpen] = useState(false);
|
|
17
|
+
const [verwijderModus, setVerwijderModus] = useState(false);
|
|
18
|
+
const { data } = useQuery({
|
|
19
|
+
queryKey: KEY,
|
|
20
|
+
queryFn: async () => {
|
|
21
|
+
const { data, error } = await tabel()
|
|
22
|
+
.select("id, titel, pad, volgorde")
|
|
23
|
+
.order("volgorde", { ascending: true })
|
|
24
|
+
.order("created_at", { ascending: true });
|
|
25
|
+
if (error)
|
|
26
|
+
throw error;
|
|
27
|
+
return data;
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
const items = Array.isArray(data) ? data : [];
|
|
31
|
+
const vol = !kanToevoegen(items.length);
|
|
32
|
+
// Verwijdermodus sluit vanzelf als er niets meer te verwijderen valt.
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (verwijderModus && items.length === 0)
|
|
35
|
+
setVerwijderModus(false);
|
|
36
|
+
}, [verwijderModus, items.length]);
|
|
37
|
+
const toevoegen = useMutation({
|
|
38
|
+
mutationFn: async (v) => {
|
|
39
|
+
if (!kanToevoegen(items.length))
|
|
40
|
+
throw new Error(`Maximaal ${MAX_SNELKOPPELINGEN} snelkoppelingen`);
|
|
41
|
+
const volgorde = items.reduce((m, i) => Math.max(m, i.volgorde), 0) + 1;
|
|
42
|
+
const { error } = await tabel().insert({ titel: v.titel, pad: v.pad, volgorde });
|
|
43
|
+
if (error)
|
|
44
|
+
throw error;
|
|
45
|
+
},
|
|
46
|
+
onSuccess: () => {
|
|
47
|
+
void qc.invalidateQueries({ queryKey: KEY });
|
|
48
|
+
setOpen(false);
|
|
49
|
+
toast.success("Snelkoppeling aangemaakt");
|
|
50
|
+
},
|
|
51
|
+
onError: (e) => toast.error(e.message || "Aanmaken mislukt"),
|
|
52
|
+
});
|
|
53
|
+
const verwijderen = useMutation({
|
|
54
|
+
mutationFn: async (id) => {
|
|
55
|
+
// .select() zodat we zien of er echt een rij is verwijderd;
|
|
56
|
+
// zonder rij geeft de database anders geen fout en lijkt de knop dood.
|
|
57
|
+
const { data, error } = await tabel().delete().eq("id", id).select("id");
|
|
58
|
+
if (error)
|
|
59
|
+
throw error;
|
|
60
|
+
if (!Array.isArray(data) || data.length === 0) {
|
|
61
|
+
throw new Error("Kon niet worden verwijderd — ververs de pagina en probeer opnieuw");
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
onSuccess: () => {
|
|
65
|
+
void qc.invalidateQueries({ queryKey: KEY });
|
|
66
|
+
toast.success("Snelkoppeling verwijderd");
|
|
67
|
+
},
|
|
68
|
+
onError: (e) => toast.error(e.message || "Verwijderen mislukt"),
|
|
69
|
+
});
|
|
70
|
+
return (_jsxs("div", { className: "dashboard-shortcuts flex min-w-0 flex-1 items-start gap-2 pt-1 pb-6", children: [_jsxs("div", { className: "flex shrink-0 flex-col items-start gap-1", children: [_jsxs(Button, { size: "sm", variant: "outline", className: "h-8 gap-1.5", disabled: vol, title: vol ? `Maximaal ${MAX_SNELKOPPELINGEN} snelkoppelingen` : undefined, onClick: () => setOpen(true), children: ["Snelkoppeling toevoegen", _jsx(Plus, { className: "h-4 w-4" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { size: "sm", variant: verwijderModus ? "destructive" : "outline", className: "h-5 border px-2 text-[10px]", "aria-pressed": verwijderModus, onClick: () => setVerwijderModus((v) => !v), children: "Verwijderen" }), vol && (_jsxs("p", { className: "text-[10px] leading-none text-muted-foreground", children: ["Maximaal ", MAX_SNELKOPPELINGEN, " snelkoppelingen"] }))] })] }), _jsx("div", { className: "dashboard-shortcut-items flex min-w-0 flex-1 flex-wrap items-start gap-x-2 gap-y-1.5", children: items.map((s) => (_jsxs("div", { className: "group relative shrink-0", children: [_jsx(Button, { asChild: true, size: "sm", variant: "secondary", className: "h-8 pr-7", children: isExterneLink(s.pad) ? (_jsx("a", { href: s.pad, target: "_blank", rel: "noopener noreferrer", children: s.titel })) : (_jsx(Link, { to: s.pad, children: s.titel })) }), verwijderModus && (_jsx(Button, { variant: "ghost", size: "icon", "aria-label": `${s.titel} verwijderen`, onClick: (e) => {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
e.stopPropagation();
|
|
73
|
+
verwijderen.mutate(s.id);
|
|
74
|
+
}, className: "absolute right-1 top-1/2 z-10 h-5 w-5 -translate-y-1/2 rounded bg-background p-0.5 text-muted-foreground transition-colors hover:bg-destructive hover:text-destructive-foreground [&_svg]:size-3.5", children: _jsx(X, { className: "h-3.5 w-3.5" }) }))] }, s.id))) }), _jsx(SnelkoppelingToevoegenDialog, { open: open, onOpenChange: setOpen, modules: modules, bezig: toevoegen.isPending, vol: vol, onAanmaken: (v) => toevoegen.mutate(v) })] }));
|
|
75
|
+
}
|
|
76
|
+
export function SnelkoppelingToevoegenDialog({ open, onOpenChange, modules, bezig, vol = false, onAanmaken, }) {
|
|
77
|
+
const router = useRouter();
|
|
78
|
+
const { user } = useAuth();
|
|
79
|
+
const shellModules = useModuleListTransform(Array.isArray(modules) ? modules : []);
|
|
80
|
+
const rechten = useQuery({
|
|
81
|
+
queryKey: ["dashboard-snelkoppeling-rechten", user?.id],
|
|
82
|
+
enabled: open && Boolean(user),
|
|
83
|
+
retry: false,
|
|
84
|
+
staleTime: 30000,
|
|
85
|
+
queryFn: async () => {
|
|
86
|
+
const { data, error } = await supabase.rpc("my_module_permissions");
|
|
87
|
+
if (error)
|
|
88
|
+
throw error;
|
|
89
|
+
return leesSnelkoppelingRechten(data);
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
const urenToegang = useQuery({
|
|
93
|
+
queryKey: ["dashboard-snelkoppeling-uren-toegang", user?.id],
|
|
94
|
+
enabled: open && Boolean(user),
|
|
95
|
+
retry: false,
|
|
96
|
+
staleTime: 30000,
|
|
97
|
+
queryFn: async () => {
|
|
98
|
+
const { data, error } = await supabase.rpc("uren_heeft_pagina_toegang");
|
|
99
|
+
if (error)
|
|
100
|
+
throw error;
|
|
101
|
+
return data === true;
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
const kiesbaar = maakPlannerCatalogus({
|
|
105
|
+
modules: shellModules,
|
|
106
|
+
geregistreerdePaden: Object.keys(router.routesByPath),
|
|
107
|
+
rechten: Array.isArray(rechten.data) ? rechten.data : [],
|
|
108
|
+
rechtenBekend: rechten.isSuccess && !rechten.isFetching,
|
|
109
|
+
urenToegang: urenToegang.isSuccess ? urenToegang.data : undefined,
|
|
110
|
+
});
|
|
111
|
+
const [titel, setTitel] = useState("");
|
|
112
|
+
const [pad, setPad] = useState("");
|
|
113
|
+
// Een volledig geplakte link van dezelfde site wordt omgezet naar het pad.
|
|
114
|
+
const origin = typeof window !== "undefined" ? window.location.origin : "";
|
|
115
|
+
const schoonPad = origin && pad.trim().startsWith(origin) ? pad.trim().slice(origin.length) || "/" : pad.trim();
|
|
116
|
+
const padFout = schoonPad !== "" && !isGeldigeLink(schoonPad);
|
|
117
|
+
const kanAanmaken = titel.trim() !== "" && titel.trim().length <= 60 && isGeldigeLink(schoonPad) && !bezig;
|
|
118
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-h-[90dvh] overflow-y-auto sm:max-w-xl", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Snelkoppeling toevoegen" }), _jsx(DialogDescription, { children: "Plak een link uit het ERP of kies een pagina." })] }), vol && (_jsxs("p", { className: "rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-xs text-destructive", children: ["Je hebt maximaal ", MAX_SNELKOPPELINGEN, " snelkoppelingen. Verwijder er een om een nieuwe toe te voegen."] })), _jsxs(Tabs, { defaultValue: "pagina", children: [_jsxs(TabsList, { className: "grid w-full grid-cols-2", children: [_jsx(TabsTrigger, { value: "pagina", children: "Kies een pagina" }), _jsx(TabsTrigger, { value: "link", children: "Link invoegen" })] }), _jsx(TabsContent, { value: "pagina", className: "mt-4", children: _jsx(PlannerPaginaKiezer, { modules: kiesbaar, bezig: bezig || vol, onAanmaken: onAanmaken }) }), _jsxs(TabsContent, { value: "link", className: "mt-4 space-y-3", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "sk-titel", children: "Titel" }), _jsx(Input, { id: "sk-titel", value: titel, maxLength: 60, onChange: (e) => setTitel(e.target.value), placeholder: "Bijv. Uren te beoordelen" })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "sk-pad", children: "Link" }), _jsx(Input, { id: "sk-pad", value: pad, onChange: (e) => setPad(e.target.value), placeholder: "/urenregistratie/week of https://..." }), padFout && _jsx("p", { className: "text-xs text-destructive", children: "Gebruik een geldig pad (/...) of een http(s)-link." })] }), _jsx(Button, { disabled: !kanAanmaken || vol, onClick: () => {
|
|
119
|
+
onAanmaken({ titel: titel.trim(), pad: schoonPad });
|
|
120
|
+
setTitel("");
|
|
121
|
+
setPad("");
|
|
122
|
+
}, children: "Aanmaken" })] })] })] }) }));
|
|
123
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DashboardStats } from "../lib/dashboard-planner-installatie-techniek.types";
|
|
2
|
+
export declare function normaliseerStats(raw: unknown): DashboardStats;
|
|
3
|
+
export declare function DashboardStatistieken({ stats, loading, wekenTerug, onWeek }: {
|
|
4
|
+
stats: DashboardStats;
|
|
5
|
+
loading: boolean;
|
|
6
|
+
wekenTerug?: number;
|
|
7
|
+
onWeek?: (wekenTerug: number) => void;
|
|
8
|
+
}): import("react").JSX.Element;
|
|
9
|
+
//# sourceMappingURL=DashboardStatistieken.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DashboardStatistieken.d.ts","sourceRoot":"","sources":["../../src/components/DashboardStatistieken.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qDAAqD,CAAC;AAY1F,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,cAAc,CAgD7D;AAmCD,wBAAgB,qBAAqB,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,UAAc,EAAE,MAAM,EAAE,EAAE;IAChF,KAAK,EAAE,cAAc,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;CAC5D,+BA2EA"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
// Statistieken-blok onder de module-vakken: drie kaarten met kerngetallen
|
|
3
|
+
// kerngetallen en een klein ingebouwd grafiekje (geen extra pakket).
|
|
4
|
+
import { CalendarCheck, FileSignature, Wallet } from "lucide-react";
|
|
5
|
+
import { Card, CardContent, Skeleton } from "@flowselections/core";
|
|
6
|
+
import { WerkbonnenFunnel } from "./WerkbonnenFunnel";
|
|
7
|
+
import { WerkurenGrafiek } from "./WerkurenGrafiek";
|
|
8
|
+
const euro = (cents) => cents === null
|
|
9
|
+
? "–"
|
|
10
|
+
: new Intl.NumberFormat("nl-NL", { style: "currency", currency: "EUR", maximumFractionDigits: 0 }).format(cents / 100);
|
|
11
|
+
const num = (v, suffix = "") => (v === null ? "–" : `${v.toLocaleString("nl-NL")}${suffix}`);
|
|
12
|
+
function num0(v) {
|
|
13
|
+
return typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
14
|
+
}
|
|
15
|
+
export function normaliseerStats(raw) {
|
|
16
|
+
const s = (raw && typeof raw === "object" ? raw : {});
|
|
17
|
+
const o = s.omzetOpen ?? {};
|
|
18
|
+
const p = s.offertePijplijn ?? {};
|
|
19
|
+
const w = s.werkUren ?? {};
|
|
20
|
+
const ws = s.werkbonSoorten ?? {};
|
|
21
|
+
const wd = s.werkbonDuur ?? {};
|
|
22
|
+
const fb = s.facturenBetaald ?? {};
|
|
23
|
+
return {
|
|
24
|
+
facturenBetaald: { verzondenAantal: num0(fb.verzondenAantal), betaaldAantal: num0(fb.betaaldAantal) },
|
|
25
|
+
omzetOpen: {
|
|
26
|
+
bedragCents: num0(o.bedragCents),
|
|
27
|
+
aantal: num0(o.aantal),
|
|
28
|
+
vervallenCents: num0(o.vervallenCents),
|
|
29
|
+
vervallenAantal: num0(o.vervallenAantal),
|
|
30
|
+
},
|
|
31
|
+
offertePijplijn: {
|
|
32
|
+
verzondenCents: num0(p.verzondenCents),
|
|
33
|
+
verzondenAantal: num0(p.verzondenAantal),
|
|
34
|
+
conceptCents: num0(p.conceptCents),
|
|
35
|
+
acceptatiePct: num0(p.acceptatiePct),
|
|
36
|
+
trechter: Array.isArray(p.trechter)
|
|
37
|
+
? p.trechter.map((t) => ({ stap: String(t?.stap ?? ""), aantal: num0(t?.aantal) ?? 0 }))
|
|
38
|
+
: [],
|
|
39
|
+
},
|
|
40
|
+
werkUren: {
|
|
41
|
+
afgerondDezeMaand: num0(w.afgerondDezeMaand),
|
|
42
|
+
urenDezeWeek: num0(w.urenDezeWeek),
|
|
43
|
+
overurenDezeWeek: num0(w.overurenDezeWeek),
|
|
44
|
+
firstTimeFixPct: num0(w.firstTimeFixPct),
|
|
45
|
+
firstTimeFixBasis: num0(w.firstTimeFixBasis),
|
|
46
|
+
urenPerDag: Array.isArray(w.urenPerDag)
|
|
47
|
+
? w.urenPerDag.map((d) => ({ dag: String(d?.dag ?? ""), uren: num0(d?.uren) ?? 0 }))
|
|
48
|
+
: [],
|
|
49
|
+
},
|
|
50
|
+
werkbonSoorten: {
|
|
51
|
+
totaal: num0(ws.totaal),
|
|
52
|
+
soorten: Array.isArray(ws.soorten)
|
|
53
|
+
? ws.soorten.map((x) => ({ soort: String(x?.soort ?? ""), aantal: num0(x?.aantal) ?? 0 }))
|
|
54
|
+
: [],
|
|
55
|
+
},
|
|
56
|
+
werkbonDuur: {
|
|
57
|
+
basis: num0(wd.basis),
|
|
58
|
+
onder1: num0(wd.onder1) ?? 0,
|
|
59
|
+
tussen1en3: num0(wd.tussen1en3) ?? 0,
|
|
60
|
+
boven3: num0(wd.boven3) ?? 0,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function StatKaart({ titel, icon: Icon, children, regels, loading }) {
|
|
65
|
+
return (_jsx(Card, { className: "h-full", children: _jsxs(CardContent, { className: "dashboard-stat-content dashboard-hours-content p-4 space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("p", { className: "text-sm font-medium text-muted-foreground", children: titel }), _jsx("div", { className: "flex items-center justify-center w-8 h-8 rounded-full text-primary bg-primary/10", children: _jsx(Icon, { className: "w-4 h-4" }) })] }), loading ? (_jsx(Skeleton, { className: "h-24 w-full" })) : (_jsxs(_Fragment, { children: [children, _jsx("dl", { className: "grid grid-cols-2 gap-x-4 gap-y-2 border-t pt-3", children: regels.map((r) => (_jsxs("div", { children: [_jsx("dt", { className: "text-xs text-muted-foreground", children: r.k }), _jsx("dd", { className: "text-sm font-medium", children: r.v })] }, r.k))) })] }))] }) }));
|
|
66
|
+
}
|
|
67
|
+
export function DashboardStatistieken({ stats, loading, wekenTerug = 0, onWeek }) {
|
|
68
|
+
const { omzetOpen: o, offertePijplijn: p, werkUren: w } = stats;
|
|
69
|
+
const fb = stats.facturenBetaald;
|
|
70
|
+
const betaaldPct = fb.verzondenAantal !== null && fb.verzondenAantal > 0 && fb.betaaldAantal !== null
|
|
71
|
+
? Math.round((fb.betaaldAantal / fb.verzondenAantal) * 100) : null;
|
|
72
|
+
return (_jsxs("section", { className: "dashboard-stats space-y-3", children: [_jsx("h2", { className: "text-lg font-semibold tracking-tight", children: "Statistieken" }), _jsxs("div", { className: "dashboard-stats-grid grid items-stretch gap-4 xl:grid-cols-3", children: [_jsx(Card, { className: "h-full min-w-0", children: _jsxs(CardContent, { className: "dashboard-stat-content dashboard-financial space-y-3 p-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("p", { className: "text-sm font-medium text-muted-foreground", children: "Financieel" }), _jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-full bg-primary/10 text-primary", children: _jsx(Wallet, { className: "h-4 w-4" }) })] }), loading ? _jsx(Skeleton, { className: "h-56 w-full" }) : _jsxs(_Fragment, { children: [_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-medium", children: "Openstaande debiteuren" }), _jsx("p", { className: "dashboard-amount mt-1 text-3xl font-semibold", children: euro(o.bedragCents) }), _jsxs("p", { className: "text-xs text-muted-foreground", children: [num(o.aantal), " verzonden, nog niet betaalde facturen"] })] }), _jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-center justify-between gap-2 text-xs", children: [_jsx("span", { className: "text-muted-foreground", children: "Betaald \u00B7 afgelopen 4 weken" }), _jsx("span", { className: "text-sm font-semibold tabular-nums", children: num(betaaldPct, "%") })] }), _jsx("div", { className: "h-2 w-full overflow-hidden rounded-full bg-muted", role: "progressbar", "aria-label": "Betaalde facturen afgelopen vier weken", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": betaaldPct ?? undefined, children: _jsx("div", { className: "h-full bg-primary", style: { width: `${betaaldPct ?? 0}%` } }) }), _jsx("p", { className: "text-xs text-muted-foreground", children: fb.verzondenAantal === 0 ? "Geen facturen verzonden" : fb.verzondenAantal === null || fb.betaaldAantal === null ? "–" : `${num(fb.betaaldAantal)} van ${num(fb.verzondenAantal)} verzonden facturen betaald` })] }), _jsxs("div", { className: "flex flex-wrap justify-between gap-2 text-xs", children: [_jsxs("span", { className: "text-muted-foreground", children: ["Vervallen \u00B7 ", num(o.vervallenAantal), " facturen"] }), _jsx("span", { className: "font-medium", children: euro(o.vervallenCents) })] })] }), _jsxs("div", { className: "dashboard-offers space-y-3 border-t-2 border-border pt-4", children: [_jsxs("div", { children: [_jsxs("h3", { className: "flex items-center gap-2 text-sm font-medium", children: [_jsx(FileSignature, { className: "h-4 w-4 text-primary" }), "Offertes in afwachting"] }), _jsx("p", { className: "dashboard-amount mt-1 text-3xl font-semibold", children: euro(p.verzondenCents) }), _jsxs("p", { className: "text-xs text-muted-foreground", children: [num(p.verzondenAantal), "\u00A0wachten op reactie"] })] }), _jsxs("dl", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { children: [_jsx("dt", { className: "text-xs text-muted-foreground", children: "Acceptatiegraad" }), _jsx("dd", { className: "text-sm font-medium", children: num(p.acceptatiePct, "%") })] }), _jsxs("div", { children: [_jsx("dt", { className: "text-xs text-muted-foreground", children: "In concept" }), _jsx("dd", { className: "text-sm font-medium", children: euro(p.conceptCents) })] })] })] })] })] }) }), _jsx(WerkbonnenFunnel, {}), _jsx(StatKaart, { titel: "Werk & uren", icon: CalendarCheck, loading: loading, regels: [
|
|
73
|
+
{ k: wekenTerug === 0 ? "Uren deze week" : "Uren in deze week", v: num(w.urenDezeWeek, " u") },
|
|
74
|
+
{ k: wekenTerug === 0 ? "Overuren deze week" : "Overuren in deze week", v: num(w.overurenDezeWeek, " u") },
|
|
75
|
+
{
|
|
76
|
+
k: "In één keer opgelost",
|
|
77
|
+
v: w.firstTimeFixPct === null ? "–" : `${w.firstTimeFixPct}% (van ${w.firstTimeFixBasis})`,
|
|
78
|
+
},
|
|
79
|
+
], children: _jsx(WerkurenGrafiek, { dagen: w.urenPerDag, beschikbaar: w.urenDezeWeek !== null, wekenTerug: wekenTerug, onWeek: onWeek }) })] })] }));
|
|
80
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ModulePlaceholderPage.d.ts","sourceRoot":"","sources":["../../src/components/ModulePlaceholderPage.tsx"],"names":[],"mappings":"AAOA,wBAAgB,qBAAqB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,+BAqBzE"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Tijdelijke pagina voor een Installatie Techniek-module. Verdwijnt zodra alle
|
|
3
|
+
// modules in één shell draaien en de tegels direct naar de module wijzen.
|
|
4
|
+
import { Link } from "@tanstack/react-router";
|
|
5
|
+
import { ArrowLeft, Construction } from "lucide-react";
|
|
6
|
+
import { Button, Card, CardContent } from "@flowselections/core";
|
|
7
|
+
import { vindModuleTegel } from "../lib/dashboard-planner-installatie-techniek.modules";
|
|
8
|
+
export function ModulePlaceholderPage({ moduleKey }) {
|
|
9
|
+
const titel = vindModuleTegel(moduleKey)?.titel ?? "Module";
|
|
10
|
+
return (_jsx("main", { className: "p-6", children: _jsx(Card, { className: "max-w-xl", children: _jsxs(CardContent, { className: "p-8 space-y-4", children: [_jsx(Construction, { className: "w-10 h-10 text-primary" }), _jsx("h1", { className: "text-2xl font-semibold tracking-tight", children: titel }), _jsx("p", { className: "text-muted-foreground", children: "Deze module komt hier beschikbaar zodra alle Installatie Techniek-modules in de shell staan." }), _jsx(Button, { asChild: true, variant: "outline", className: "gap-2", children: _jsxs(Link, { to: "/dashboard-planner-installatie-techniek", children: [_jsx(ArrowLeft, { className: "w-4 h-4" }), "Terug naar dashboard"] }) })] }) }) }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
interface PlannerInstallatieTechniekLogoProps {
|
|
2
|
+
className?: string;
|
|
3
|
+
}
|
|
4
|
+
export declare function PlannerInstallatieTechniekLogo({ className }: PlannerInstallatieTechniekLogoProps): import("react").JSX.Element;
|
|
5
|
+
export {};
|
|
6
|
+
//# sourceMappingURL=PlannerInstallatieTechniekLogo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PlannerInstallatieTechniekLogo.d.ts","sourceRoot":"","sources":["../../src/components/PlannerInstallatieTechniekLogo.tsx"],"names":[],"mappings":"AAKA,UAAU,mCAAmC;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,8BAA8B,CAAC,EAAE,SAAS,EAAE,EAAE,mCAAmC,+BAsBhG"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export function PlannerInstallatieTechniekLogo({ className }) {
|
|
3
|
+
return (_jsxs("svg", { viewBox: "0 0 40 40", fill: "none", xmlns: "http://www.w3.org/2000/svg", className: className, "aria-hidden": "true", children: [_jsx("rect", { width: "40", height: "40", rx: "8", fill: "currentColor", opacity: "0.1" }), _jsx("path", { d: "M12 28 L20 20 M24 16 a4 4 0 1 0 -2.8 -1.2 L19 17 a1.4 1.4 0 0 0 0 2 L22 22 a1.4 1.4 0 0 0 2 0 l2.2 -2.2 A4 4 0 0 0 24 16 Z", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", fill: "currentColor", fillOpacity: "0.15" })] }));
|
|
4
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { KiesbareModule } from "../lib/dashboard-snelkoppelingen";
|
|
2
|
+
export declare function PlannerPaginaKiezer({ modules, bezig, onAanmaken }: {
|
|
3
|
+
modules: KiesbareModule[];
|
|
4
|
+
bezig: boolean;
|
|
5
|
+
onAanmaken: (waarde: {
|
|
6
|
+
titel: string;
|
|
7
|
+
pad: string;
|
|
8
|
+
}) => void;
|
|
9
|
+
}): import("react").JSX.Element;
|
|
10
|
+
//# sourceMappingURL=PlannerPaginaKiezer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PlannerPaginaKiezer.d.ts","sourceRoot":"","sources":["../../src/components/PlannerPaginaKiezer.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAEvE,wBAAgB,mBAAmB,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE;IAClE,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,EAAE,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CAC9D,+BA4CA"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Button, cn } from "@flowselections/core";
|
|
4
|
+
import { ChevronRight, LockKeyhole } from "lucide-react";
|
|
5
|
+
export function PlannerPaginaKiezer({ modules, bezig, onAanmaken }) {
|
|
6
|
+
const [actief, setActief] = useState(null);
|
|
7
|
+
const actieveModule = modules.find((m) => m.key === actief) ?? modules[0];
|
|
8
|
+
if (modules.length === 0)
|
|
9
|
+
return _jsx("p", { className: "text-sm text-muted-foreground", children: "Geen modules beschikbaar." });
|
|
10
|
+
return (_jsxs("div", { className: "grid min-w-0 grid-cols-2 gap-3", children: [_jsx("ul", { "aria-label": "Plannermodules", className: "max-h-80 min-w-0 space-y-1 overflow-y-auto border-r pr-3", children: modules.map((m) => (_jsx("li", { children: _jsxs(Button, { variant: "ghost", onMouseEnter: () => setActief(m.key), onFocus: () => setActief(m.key), onClick: () => setActief(m.key), "aria-pressed": actieveModule?.key === m.key, className: cn("h-auto min-h-9 w-full justify-between gap-1 whitespace-normal px-2 py-2 text-left text-sm", actieveModule?.key === m.key && "bg-muted font-medium"), children: [_jsx("span", { className: "min-w-0 break-words", children: m.titel }), _jsx(ChevronRight, { className: "h-4 w-4 shrink-0 text-muted-foreground" })] }) }, m.key))) }), _jsx("ul", { "aria-label": `Pagina’s van ${actieveModule?.titel ?? "module"}`, className: "max-h-80 min-w-0 space-y-1 overflow-y-auto", children: actieveModule?.paginas.map((p) => (_jsx("li", { children: _jsxs(Button, { variant: "ghost", disabled: bezig || p.beschikbaar === false, title: p.reden, onClick: () => {
|
|
11
|
+
if (!p.pad || p.beschikbaar === false)
|
|
12
|
+
return;
|
|
13
|
+
onAanmaken({ titel: `${actieveModule.titel.replace(/ \(planner\)$/, "")} · ${p.titel}`.slice(0, 60), pad: p.pad });
|
|
14
|
+
}, className: "h-auto min-h-9 w-full justify-start gap-2 whitespace-normal px-2 py-2 text-left text-sm", children: [p.beschikbaar === false && _jsx(LockKeyhole, { className: "h-3.5 w-3.5 shrink-0" }), _jsxs("span", { className: "min-w-0 break-words", children: [p.titel, p.reden && _jsx("span", { className: "mt-0.5 block text-xs font-normal", children: p.reden })] })] }) }, `${p.titel}:${p.pad}`))) })] }));
|
|
15
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type WerkbonFunnelData } from '../lib/dashboard-werkbon-funnel';
|
|
2
|
+
import { type FunnelIndeling } from '../lib/funnel-indeling';
|
|
3
|
+
export declare function WerkbonnenFunnelInhoud({ data, indeling }: {
|
|
4
|
+
data: WerkbonFunnelData;
|
|
5
|
+
indeling?: FunnelIndeling;
|
|
6
|
+
}): import("react").JSX.Element;
|
|
7
|
+
export declare function WerkbonnenFunnel(): import("react").JSX.Element;
|
|
8
|
+
//# sourceMappingURL=WerkbonnenFunnel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WerkbonnenFunnel.d.ts","sourceRoot":"","sources":["../../src/components/WerkbonnenFunnel.tsx"],"names":[],"mappings":"AAOA,OAAO,EAA6E,KAAK,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AAEpJ,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAkBjF,wBAAgB,sBAAsB,CAAC,EAAE,IAAI,EAAE,QAA6B,EAAE,EAAE;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,QAAQ,CAAC,EAAE,cAAc,CAAA;CAAE,+BAarI;AAED,wBAAgB,gBAAgB,gCA0C/B"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { DASHBOARD_TIJDZONE } from "../lib/dashboard-constanten";
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
5
|
+
import { useServerFn } from '@tanstack/react-start';
|
|
6
|
+
import { Clipboard, LogIn, RefreshCw } from 'lucide-react';
|
|
7
|
+
import { Button, Card, CardContent, Skeleton, useAuth } from '@flowselections/core';
|
|
8
|
+
import { getWerkbonFunnel } from '../lib/dashboard-planner-installatie-techniek.functions';
|
|
9
|
+
import { maakFunnelStromen, funnelFoutMelding, FUNNEL_HERKOMST } from '../lib/dashboard-werkbon-funnel';
|
|
10
|
+
import { SankeyChart } from './ui/sankey-chart';
|
|
11
|
+
import { STANDAARD_INDELING } from '../lib/funnel-indeling';
|
|
12
|
+
const aantal = (n) => n.toLocaleString('nl-NL');
|
|
13
|
+
const datum = (value) => new Intl.DateTimeFormat('nl-NL', { day: 'numeric', month: 'short', year: 'numeric', timeZone: 'UTC' }).format(new Date(`${value}T12:00:00Z`));
|
|
14
|
+
const LEGENDA = ['standby', 'mail', 'extra_werk', 'handmatig'].map(soort => ({ soort, ...FUNNEL_HERKOMST[soort] }));
|
|
15
|
+
// Rustige melding als de cijfers niet opgevraagd kunnen worden: de server-function
|
|
16
|
+
// geeft een status terug in plaats van te gooien, dus het scherm valt niet leeg.
|
|
17
|
+
// 'sessie' vraagt om opnieuw in te loggen, 'data' biedt de opvraging opnieuw aan.
|
|
18
|
+
function FunnelMelding({ tekst, actie, opNieuw, opInloggen }) {
|
|
19
|
+
return _jsxs("div", { role: "alert", className: "space-y-3 py-8 text-center text-sm text-muted-foreground", children: [_jsx("p", { children: tekst }), actie === 'inloggen'
|
|
20
|
+
? _jsxs(Button, { size: "sm", onClick: opInloggen, children: [_jsx(LogIn, { className: "mr-2 h-3.5 w-3.5" }), "Inloggen"] })
|
|
21
|
+
: _jsxs(Button, { variant: "outline", size: "sm", onClick: opNieuw, children: [_jsx(RefreshCw, { className: "mr-2 h-3.5 w-3.5" }), "Opnieuw proberen"] })] });
|
|
22
|
+
}
|
|
23
|
+
export function WerkbonnenFunnelInhoud({ data, indeling = STANDAARD_INDELING }) {
|
|
24
|
+
const [tip, setTip] = useState(null);
|
|
25
|
+
return _jsxs(_Fragment, { children: [_jsxs("p", { className: "text-xs text-muted-foreground", children: [datum(data.vanaf), " \u2013 ", datum(data.tot)] }), data.totaal > 0 ? _jsxs(_Fragment, { children: [_jsxs("div", { className: "grid grid-cols-4 gap-1 text-center text-[10px] text-muted-foreground", children: [_jsx("span", { children: "Herkomst" }), _jsx("span", { children: "Aangemaakt" }), _jsx("span", { children: "Afgerond" }), _jsx("span", { children: "Betaling / extra werk" })] }), _jsx(SankeyChart, { data: maakFunnelStromen(data), total: data.totaal, onTip: setTip, indeling: indeling }), _jsx("div", { role: "status", "aria-live": "polite", className: "dashboard-funnel-tip min-h-[3rem] rounded-md border border-border bg-popover px-2.5 py-1.5 text-xs text-popover-foreground", children: tip ? _jsxs(_Fragment, { children: [_jsx("div", { className: "font-medium", children: tip.titel }), _jsx("div", { className: "text-muted-foreground", children: tip.tekst })] }) : _jsx("div", { className: "flex flex-wrap gap-x-4 gap-y-1 text-muted-foreground", children: LEGENDA.map(s => _jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: "inline-block h-2.5 w-2.5 shrink-0 rounded-full", style: { backgroundColor: s.kleur }, "aria-hidden": "true" }), s.label] }, s.soort)) }) })] }) : _jsx("p", { className: "py-8 text-center text-sm text-muted-foreground", children: "Geen werkbonnen aangemaakt in deze periode." }), data.onbekend > 0 && _jsxs("p", { className: "text-xs text-muted-foreground", children: ["Van ", aantal(data.onbekend), " eerder getelde werkbonnen is het verdere verloop onbekend."] })] });
|
|
26
|
+
}
|
|
27
|
+
export function WerkbonnenFunnel() {
|
|
28
|
+
const [periode, setPeriode] = useState('jaar');
|
|
29
|
+
const { user, signOut } = useAuth();
|
|
30
|
+
const fetchFunnel = useServerFn(getWerkbonFunnel);
|
|
31
|
+
const queryClient = useQueryClient();
|
|
32
|
+
const query = useQuery({ queryKey: ['dashboard-planner-installatie-techniek', 'funnel', user?.id, periode], queryFn: () => fetchFunnel({ data: { periode } }), enabled: Boolean(user?.id), staleTime: 30000, retry: false });
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
let dag = new Intl.DateTimeFormat('en-CA', { timeZone: DASHBOARD_TIJDZONE }).format(new Date());
|
|
35
|
+
const check = () => {
|
|
36
|
+
const nieuw = new Intl.DateTimeFormat('en-CA', { timeZone: DASHBOARD_TIJDZONE }).format(new Date());
|
|
37
|
+
if (nieuw !== dag) {
|
|
38
|
+
dag = nieuw;
|
|
39
|
+
void queryClient.invalidateQueries({ queryKey: ['dashboard-planner-installatie-techniek', 'funnel'] });
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const timer = window.setInterval(check, 15000);
|
|
43
|
+
window.addEventListener('focus', check);
|
|
44
|
+
return () => { window.clearInterval(timer); window.removeEventListener('focus', check); };
|
|
45
|
+
}, [queryClient]);
|
|
46
|
+
// Eerst uitloggen en dan het inlogscherm laden. De app stuurt iemand die volgens
|
|
47
|
+
// de eigen inlogstatus nog ingelogd is direct terug naar het dashboard, dus een
|
|
48
|
+
// interne sprong kan je meteen weer terugslingeren; een nieuwe pagina-lading na
|
|
49
|
+
// het uitloggen doet dat niet.
|
|
50
|
+
const logIn = async () => {
|
|
51
|
+
try {
|
|
52
|
+
await signOut();
|
|
53
|
+
}
|
|
54
|
+
catch { /* een mislukte uitlog mag het inlogscherm niet blokkeren */ }
|
|
55
|
+
window.location.assign('/login');
|
|
56
|
+
};
|
|
57
|
+
const status = query.isError ? 'data' : query.data?.status;
|
|
58
|
+
const fout = status === 'sessie' || status === 'data' ? funnelFoutMelding(status) : null;
|
|
59
|
+
return _jsx(Card, { className: "h-full min-w-0", children: _jsxs(CardContent, { className: "dashboard-stat-content space-y-3 p-4", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [_jsxs("h3", { className: "flex items-center gap-2 text-sm font-medium", children: [_jsx(Clipboard, { className: "h-4 w-4 text-primary" }), "Werkbonnen"] }), _jsx("div", { role: "group", "aria-label": "Werkbonnenperiode", className: "flex rounded-md bg-muted p-0.5", children: ['week', 'maand', 'jaar'].map(p => _jsx(Button, { size: "sm", variant: p === periode ? 'secondary' : 'ghost', "aria-pressed": p === periode, onClick: () => setPeriode(p), className: "h-7 px-2 text-xs", children: p === 'week' ? 'Week' : p === 'maand' ? 'Maand' : 'Jaar' }, p)) })] }), !user?.id
|
|
60
|
+
? _jsx(FunnelMelding, { tekst: "Log in om werkbonstatistieken te bekijken.", actie: "inloggen", opNieuw: () => void query.refetch(), opInloggen: () => void logIn() })
|
|
61
|
+
: query.isPending
|
|
62
|
+
? _jsx(Skeleton, { className: "dashboard-funnel-loading h-80 w-full" })
|
|
63
|
+
: fout
|
|
64
|
+
? _jsx(FunnelMelding, { tekst: fout.tekst, actie: fout.actie, opNieuw: () => void query.refetch(), opInloggen: () => void logIn() })
|
|
65
|
+
: query.data && query.data.status === 'ok'
|
|
66
|
+
? _jsx(WerkbonnenFunnelInhoud, { data: query.data.funnel })
|
|
67
|
+
: null] }) });
|
|
68
|
+
}
|