@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attach-core-auth.d.ts","sourceRoot":"","sources":["../../src/lib/attach-core-auth.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,cAAc,mHASzB,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Stuurt de Bearer-token van de ingelogde gebruiker mee met elke server-function.
|
|
2
|
+
// Leest de sessie uit @flowselections/core (dezelfde sessie als useAuth).
|
|
3
|
+
import { createMiddleware } from "@tanstack/react-start";
|
|
4
|
+
import { supabase } from "@flowselections/core";
|
|
5
|
+
export const attachCoreAuth = createMiddleware({ type: "function" }).client(async ({ next }) => {
|
|
6
|
+
let token;
|
|
7
|
+
try {
|
|
8
|
+
const { data } = await supabase.auth.getSession();
|
|
9
|
+
token = data.session?.access_token;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
token = undefined;
|
|
13
|
+
}
|
|
14
|
+
return next({ headers: token ? { Authorization: `Bearer ${token}` } : {} });
|
|
15
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dashboard-constanten.d.ts","sourceRoot":"","sources":["../../src/lib/dashboard-constanten.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,kBAAkB,qBAAqB,CAAC;AAErD,yCAAyC;AACzC,wBAAgB,cAAc,CAAC,CAAC,GAAE,IAAiB,GAAG,MAAM,CAI3D"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Vaste, klant-onafhankelijke waarden van de dashboardmodule op één plek.
|
|
2
|
+
export const DASHBOARD_TIJDZONE = "Europe/Amsterdam";
|
|
3
|
+
/** YYYY-MM-DD in de dashboardtijdzone */
|
|
4
|
+
export function dashboardDatum(d = new Date()) {
|
|
5
|
+
return new Intl.DateTimeFormat("en-CA", {
|
|
6
|
+
timeZone: DASHBOARD_TIJDZONE, year: "numeric", month: "2-digit", day: "2-digit",
|
|
7
|
+
}).format(d);
|
|
8
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { DashboardData } from "./dashboard-planner-installatie-techniek.types";
|
|
2
|
+
import { type FunnelStatus } from './dashboard-werkbon-funnel';
|
|
3
|
+
type Resultaat = DashboardData & {
|
|
4
|
+
error?: "unauthorized" | "config";
|
|
5
|
+
};
|
|
6
|
+
export declare const getDashboardData: import("@tanstack/start-client-core").OptionalFetcher<undefined, (input: unknown) => {
|
|
7
|
+
wekenTerug: number;
|
|
8
|
+
}, Promise<Resultaat>>;
|
|
9
|
+
export type { DashboardData } from "./dashboard-planner-installatie-techniek.types";
|
|
10
|
+
export declare const getWerkbonFunnel: import("@tanstack/start-client-core").OptionalFetcher<undefined, (input: unknown) => {
|
|
11
|
+
periode: import("./dashboard-werkbon-funnel").FunnelPeriode;
|
|
12
|
+
}, Promise<FunnelStatus>>;
|
|
13
|
+
//# sourceMappingURL=dashboard-planner-installatie-techniek.functions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dashboard-planner-installatie-techniek.functions.d.ts","sourceRoot":"","sources":["../../src/lib/dashboard-planner-installatie-techniek.functions.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gDAAgD,CAAC;AACpF,OAAO,EAAwB,KAAK,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAGrF,KAAK,SAAS,GAAG,aAAa,GAAG;IAAE,KAAK,CAAC,EAAE,cAAc,GAAG,QAAQ,CAAA;CAAE,CAAC;AAoBvE,eAAO,MAAM,gBAAgB,2EACH,OAAO;;sBA0BhC,CAAC;AAEF,YAAY,EAAE,aAAa,EAAE,MAAM,gDAAgD,CAAC;AAEpF,eAAO,MAAM,gBAAgB,2EACH,OAAO;;yBAM7B,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// src/lib/dashboard-planner-installatie-techniek.functions.ts
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Server-function voor de dashboardtellingen. Controleert de inlog zelf en
|
|
5
|
+
// gooit GEEN Response bij een ontbrekende/ongeldige token (dat gaf een leeg
|
|
6
|
+
// scherm "Error: [object Response]"), maar geeft een lege uitkomst met een
|
|
7
|
+
// foutcode terug. Waarden komen uit de omgeving; ontbreken ze → fail closed.
|
|
8
|
+
// ============================================================================
|
|
9
|
+
import { createServerFn } from "@tanstack/react-start";
|
|
10
|
+
import { valideerFunnelInvoer } from './dashboard-werkbon-funnel';
|
|
11
|
+
import { haalWerkbonFunnel, leesBearerToken } from './dashboard-werkbon-funnel.server';
|
|
12
|
+
const LEEG = {
|
|
13
|
+
planningNietIngepland: null,
|
|
14
|
+
werkbonnenOpen: null,
|
|
15
|
+
urenTeBeoordelen: null,
|
|
16
|
+
mailsTeControleren: null,
|
|
17
|
+
offertesMeerwerk: null,
|
|
18
|
+
facturenTeFactureren: null,
|
|
19
|
+
};
|
|
20
|
+
/** Weekkeuze voor de werkuren-grafiek: 0 = deze week, max 6 weken terug. */
|
|
21
|
+
function valideerWekenTerug(input) {
|
|
22
|
+
const v = (input && typeof input === "object" ? input.wekenTerug : undefined) ?? 0;
|
|
23
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v < 0 || v > 6) {
|
|
24
|
+
throw new Error("Ongeldige weekkeuze (0 t/m 6 weken terug)");
|
|
25
|
+
}
|
|
26
|
+
return v;
|
|
27
|
+
}
|
|
28
|
+
export const getDashboardData = createServerFn({ method: "GET" })
|
|
29
|
+
.inputValidator((input) => ({ wekenTerug: valideerWekenTerug(input) }))
|
|
30
|
+
.handler(async ({ data: invoer }) => {
|
|
31
|
+
const url = process.env.SUPABASE_URL;
|
|
32
|
+
const key = process.env.SUPABASE_PUBLISHABLE_KEY;
|
|
33
|
+
if (!url || !key) {
|
|
34
|
+
console.error("[dashboard] SUPABASE_URL / SUPABASE_PUBLISHABLE_KEY ontbreken");
|
|
35
|
+
return { kpis: { ...LEEG }, error: "config" };
|
|
36
|
+
}
|
|
37
|
+
const { getRequest } = await import("@tanstack/react-start/server");
|
|
38
|
+
const token = leesBearerToken(getRequest()?.headers);
|
|
39
|
+
if (!token)
|
|
40
|
+
return { kpis: { ...LEEG }, error: "unauthorized" };
|
|
41
|
+
const { createClient } = await import("@supabase/supabase-js");
|
|
42
|
+
const supabase = createClient(url, key, {
|
|
43
|
+
global: { headers: { Authorization: `Bearer ${token}` } },
|
|
44
|
+
auth: { storage: undefined, persistSession: false, autoRefreshToken: false },
|
|
45
|
+
});
|
|
46
|
+
const { data, error } = await supabase.auth.getUser(token);
|
|
47
|
+
if (error || !data?.user?.id)
|
|
48
|
+
return { kpis: { ...LEEG }, error: "unauthorized" };
|
|
49
|
+
const { fetchDashboardData } = await import("./dashboard-planner-installatie-techniek.server");
|
|
50
|
+
return fetchDashboardData(supabase, data.user.id, invoer.wekenTerug);
|
|
51
|
+
});
|
|
52
|
+
export const getWerkbonFunnel = createServerFn({ method: 'GET' })
|
|
53
|
+
.inputValidator((input) => ({ periode: valideerFunnelInvoer(input) }))
|
|
54
|
+
.handler(async ({ data }) => {
|
|
55
|
+
// Mislukte inlog of opvraging geeft een status terug (zie dashboard-werkbon-funnel.server):
|
|
56
|
+
// een gooiende server-function blankeerde het hele dashboard.
|
|
57
|
+
const { getRequest } = await import('@tanstack/react-start/server');
|
|
58
|
+
return haalWerkbonFunnel({ periode: data.periode, token: leesBearerToken(getRequest()?.headers) });
|
|
59
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DashboardKpis } from "./dashboard-planner-installatie-techniek.types";
|
|
2
|
+
export interface ModuleTegel {
|
|
3
|
+
key: string;
|
|
4
|
+
titel: string;
|
|
5
|
+
/** Uitleg bij het getal; leeg = tegel zonder getal. */
|
|
6
|
+
getalLabel: string;
|
|
7
|
+
kpi: keyof DashboardKpis | null;
|
|
8
|
+
}
|
|
9
|
+
export declare const MODULE_TEGELS: readonly ModuleTegel[];
|
|
10
|
+
export declare function vindModuleTegel(key: string): ModuleTegel | undefined;
|
|
11
|
+
//# sourceMappingURL=dashboard-planner-installatie-techniek.modules.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dashboard-planner-installatie-techniek.modules.d.ts","sourceRoot":"","sources":["../../src/lib/dashboard-planner-installatie-techniek.modules.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gDAAgD,CAAC;AAEpF,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,aAAa,GAAG,IAAI,CAAC;CACjC;AAED,eAAO,MAAM,aAAa,EAAE,SAAS,WAAW,EAO/C,CAAC;AAEF,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAEpE"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const MODULE_TEGELS = [
|
|
2
|
+
{ key: "e-mail", titel: "E-mail", getalLabel: "te controleren", kpi: "mailsTeControleren" },
|
|
3
|
+
{ key: "werkbonnen", titel: "Werkbonnen", getalLabel: "niet gefactureerd", kpi: "werkbonnenOpen" },
|
|
4
|
+
{ key: "urenregistratie", titel: "Urenregistratie", getalLabel: "te beoordelen", kpi: "urenTeBeoordelen" },
|
|
5
|
+
{ key: "planning", titel: "Planning", getalLabel: "niet ingepland", kpi: "planningNietIngepland" },
|
|
6
|
+
{ key: "offertes", titel: "Offertes", getalLabel: "meerwerkmeldingen", kpi: "offertesMeerwerk" },
|
|
7
|
+
{ key: "facturen", titel: "Facturen", getalLabel: "te factureren", kpi: "facturenTeFactureren" },
|
|
8
|
+
];
|
|
9
|
+
export function vindModuleTegel(key) {
|
|
10
|
+
return MODULE_TEGELS.find((m) => m.key === key);
|
|
11
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
+
import type { DashboardData } from "./dashboard-planner-installatie-techniek.types";
|
|
3
|
+
type Supa = SupabaseClient<any>;
|
|
4
|
+
/**
|
|
5
|
+
* Facturatie — één bron voor beide tegels, op basis van invoices.source_ref.
|
|
6
|
+
* - werkbonnenOpen: werkbonnen waar nog geen factuur voor bestaat
|
|
7
|
+
* - facturenTeFactureren: klaargekomen werkbonnen zonder factuur (nog aan te
|
|
8
|
+
* maken) + facturen in concept (nog te verzenden) — precies wat de
|
|
9
|
+
* Facturen-module nog van actie vraagt.
|
|
10
|
+
*/
|
|
11
|
+
export declare function telFacturatieStatus(supabase: Supa): Promise<{
|
|
12
|
+
werkbonnenOpen: number | null;
|
|
13
|
+
facturenTeFactureren: number | null;
|
|
14
|
+
}>;
|
|
15
|
+
export declare function fetchDashboardData(supabase: Supa, userId: string, wekenTerug?: number): Promise<DashboardData>;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=dashboard-planner-installatie-techniek.server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dashboard-planner-installatie-techniek.server.d.ts","sourceRoot":"","sources":["../../src/lib/dashboard-planner-installatie-techniek.server.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gDAAgD,CAAC;AAMpF,KAAK,IAAI,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC;AAchC;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,IAAI,GACb,OAAO,CAAC;IAAE,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,CAuBjF;AA6JD,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,SAAI,GAAG,OAAO,CAAC,aAAa,CAAC,CAyC/G"}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// src/lib/dashboard-planner-installatie-techniek.server.ts
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Server-only: tellingen per Installatie Techniek-module. Alles onder RLS als
|
|
5
|
+
// de ingelogde gebruiker. Een mislukte telling wordt `null` (geen crash).
|
|
6
|
+
// ============================================================================
|
|
7
|
+
import { dashboardDatum } from "./dashboard-constanten";
|
|
8
|
+
import { leesDbTellingen } from "./dashboard-tellingen";
|
|
9
|
+
async function telling(q) {
|
|
10
|
+
try {
|
|
11
|
+
const { count, error } = await q;
|
|
12
|
+
if (error)
|
|
13
|
+
return null;
|
|
14
|
+
return count ?? 0;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Facturatie — één bron voor beide tegels, op basis van invoices.source_ref.
|
|
22
|
+
* - werkbonnenOpen: werkbonnen waar nog geen factuur voor bestaat
|
|
23
|
+
* - facturenTeFactureren: klaargekomen werkbonnen zonder factuur (nog aan te
|
|
24
|
+
* maken) + facturen in concept (nog te verzenden) — precies wat de
|
|
25
|
+
* Facturen-module nog van actie vraagt.
|
|
26
|
+
*/
|
|
27
|
+
export async function telFacturatieStatus(supabase) {
|
|
28
|
+
const leeg = { werkbonnenOpen: null, facturenTeFactureren: null };
|
|
29
|
+
try {
|
|
30
|
+
const [werkbonnen, facturen] = await Promise.all([
|
|
31
|
+
supabase.from("werkbonnen").select("id, status"),
|
|
32
|
+
supabase.from("invoices").select("source_ref, status").eq("source_type", "work_order"),
|
|
33
|
+
]);
|
|
34
|
+
if (werkbonnen.error || facturen.error)
|
|
35
|
+
return leeg;
|
|
36
|
+
const wb = Array.isArray(werkbonnen.data) ? werkbonnen.data : [];
|
|
37
|
+
const ff = Array.isArray(facturen.data) ? facturen.data : [];
|
|
38
|
+
const gefactureerd = new Set(ff.map((f) => String(f.source_ref ?? "")).filter(Boolean));
|
|
39
|
+
const open = wb.filter((w) => !gefactureerd.has(String(w.id))).length;
|
|
40
|
+
const klaarZonderFactuur = wb.filter((w) => ["completed", "invoicing"].includes(String(w.status)) && !gefactureerd.has(String(w.id))).length;
|
|
41
|
+
const nogTeVerzenden = ff.filter((f) => String(f.status) === "concept").length;
|
|
42
|
+
return { werkbonnenOpen: open, facturenTeFactureren: klaarZonderFactuur + nogTeVerzenden };
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return leeg;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Zelfde logica als de E-mail-module: hoort een mailbox bij het doeleinde? */
|
|
49
|
+
function matchesPurpose(assignments, purpose) {
|
|
50
|
+
const needle = purpose.trim().toLowerCase();
|
|
51
|
+
if (!needle)
|
|
52
|
+
return true;
|
|
53
|
+
if (!assignments)
|
|
54
|
+
return false;
|
|
55
|
+
if (Array.isArray(assignments)) {
|
|
56
|
+
return assignments.some((v) => v && typeof v === "object"
|
|
57
|
+
? String(v["purpose"] ?? "").toLowerCase() === needle
|
|
58
|
+
: String(v).toLowerCase() === needle);
|
|
59
|
+
}
|
|
60
|
+
if (typeof assignments === "object") {
|
|
61
|
+
return Object.entries(assignments).some(([key, val]) => (key.toLowerCase() === needle && Boolean(val)) || String(val).toLowerCase() === needle);
|
|
62
|
+
}
|
|
63
|
+
return String(assignments).toLowerCase() === needle;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* E-mail — "Te controleren" zoals in de E-mail-module: niet-gearchiveerde mails
|
|
67
|
+
* in de mailbox(en) van het gekozen doeleinde van de gebruiker, zonder
|
|
68
|
+
* afgehandeld werkbonvoorstel. Geen doeleinde gekozen = null ("–").
|
|
69
|
+
*/
|
|
70
|
+
async function telMailsTeControleren(supabase, userId) {
|
|
71
|
+
try {
|
|
72
|
+
const { data: inst, error: instErr } = await supabase
|
|
73
|
+
.from("werkbon_mail_user_settings")
|
|
74
|
+
.select("email_purpose")
|
|
75
|
+
.eq("user_id", userId)
|
|
76
|
+
.maybeSingle();
|
|
77
|
+
if (instErr)
|
|
78
|
+
return null;
|
|
79
|
+
const purpose = typeof inst?.email_purpose === "string" ? inst.email_purpose : "";
|
|
80
|
+
if (!purpose)
|
|
81
|
+
return null;
|
|
82
|
+
const { data: conns, error: connErr } = await supabase
|
|
83
|
+
.from("email_auth_connections")
|
|
84
|
+
.select("id, purpose_assignments");
|
|
85
|
+
if (connErr)
|
|
86
|
+
return null;
|
|
87
|
+
const connectionIds = (Array.isArray(conns) ? conns : [])
|
|
88
|
+
.filter((c) => matchesPurpose(c.purpose_assignments, purpose))
|
|
89
|
+
.map((c) => c.id);
|
|
90
|
+
if (connectionIds.length === 0)
|
|
91
|
+
return 0;
|
|
92
|
+
const [events, gearchiveerd, verwerkt] = await Promise.all([
|
|
93
|
+
supabase
|
|
94
|
+
.from("email_auth_events")
|
|
95
|
+
.select("id")
|
|
96
|
+
.in("connection_id", connectionIds)
|
|
97
|
+
.order("received_at", { ascending: false })
|
|
98
|
+
.limit(1000),
|
|
99
|
+
supabase.from("werkbon_email_state").select("event_id").eq("is_archived", true),
|
|
100
|
+
supabase.from("werkbon_proposals").select("event_id").in("status", ["approved", "rejected"]),
|
|
101
|
+
]);
|
|
102
|
+
if (events.error || gearchiveerd.error || verwerkt.error)
|
|
103
|
+
return null;
|
|
104
|
+
const uitsluiten = new Set([
|
|
105
|
+
...(gearchiveerd.data ?? []).map((r) => r.event_id),
|
|
106
|
+
...(verwerkt.data ?? []).map((r) => r.event_id),
|
|
107
|
+
]);
|
|
108
|
+
return (events.data ?? []).filter((e) => !uitsluiten.has(e.id)).length;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** Planning — zelfde regel als "Niet ingeplande klussen" in de Planning-module. */
|
|
115
|
+
async function telPlanningNietIngepland(supabase) {
|
|
116
|
+
try {
|
|
117
|
+
const { data: wb, error } = await supabase
|
|
118
|
+
.from("werkbonnen")
|
|
119
|
+
.select("id")
|
|
120
|
+
.not("status", "in", "(completed,in_progress,invoicing)");
|
|
121
|
+
if (error)
|
|
122
|
+
return null;
|
|
123
|
+
const ids = (Array.isArray(wb) ? wb : []).map((w) => w.id);
|
|
124
|
+
if (ids.length === 0)
|
|
125
|
+
return 0;
|
|
126
|
+
const { data: gepland, error: pErr } = await supabase
|
|
127
|
+
.from("planning_items")
|
|
128
|
+
.select("werkbon_id")
|
|
129
|
+
.neq("status", "geannuleerd");
|
|
130
|
+
if (pErr)
|
|
131
|
+
return null;
|
|
132
|
+
const set = new Set((Array.isArray(gepland) ? gepland : []).map((p) => p.werkbon_id));
|
|
133
|
+
return ids.filter((id) => !set.has(id)).length;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const AFGEHANDELD = ["approved", "goedgekeurd", "rejected", "afgewezen", "cancelled", "ingetrokken"];
|
|
140
|
+
/** Urenregistratie — zelfde regel als "Te beoordelen" in de Urenregistratie-module. */
|
|
141
|
+
async function telUrenTeBeoordelen(supabase) {
|
|
142
|
+
try {
|
|
143
|
+
const [verlof, overuren, standaardweek] = await Promise.all([
|
|
144
|
+
supabase.from("leave_requests").select("monteur_id, user_id, status, cancellation_status"),
|
|
145
|
+
supabase.from("uren_overuren_verklaringen").select("monteur_id, status, cancellation_status"),
|
|
146
|
+
supabase.from("uren_standaardweek_wijzigingen").select("monteur_id, status"),
|
|
147
|
+
]);
|
|
148
|
+
if (verlof.error || overuren.error || standaardweek.error)
|
|
149
|
+
return null;
|
|
150
|
+
const tel = (rows, metIntrek) => {
|
|
151
|
+
let n = 0;
|
|
152
|
+
for (const r of (Array.isArray(rows) ? rows : [])) {
|
|
153
|
+
if (!(r.monteur_id ?? r.user_id))
|
|
154
|
+
continue;
|
|
155
|
+
const st = String(r.status ?? "");
|
|
156
|
+
const afgehandeld = AFGEHANDELD.includes(st);
|
|
157
|
+
if (!afgehandeld)
|
|
158
|
+
n++;
|
|
159
|
+
const goedgekeurd = st === "approved" || st === "goedgekeurd";
|
|
160
|
+
if (metIntrek && goedgekeurd && r.cancellation_status === "pending")
|
|
161
|
+
n++;
|
|
162
|
+
}
|
|
163
|
+
return n;
|
|
164
|
+
};
|
|
165
|
+
return tel(verlof.data, true) + tel(overuren.data, true) + tel(standaardweek.data, false);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/** Mailboxen van het gekozen doeleinde; null = geen doeleinde of fout. */
|
|
172
|
+
async function mailConnecties(supabase, userId) {
|
|
173
|
+
try {
|
|
174
|
+
const { data: inst, error } = await supabase
|
|
175
|
+
.from("werkbon_mail_user_settings").select("email_purpose").eq("user_id", userId).maybeSingle();
|
|
176
|
+
if (error)
|
|
177
|
+
return null;
|
|
178
|
+
const purpose = typeof inst?.email_purpose === "string" ? inst.email_purpose : "";
|
|
179
|
+
if (!purpose)
|
|
180
|
+
return null;
|
|
181
|
+
const { data: conns, error: cErr } = await supabase.from("email_auth_connections").select("id, purpose_assignments");
|
|
182
|
+
if (cErr)
|
|
183
|
+
return null;
|
|
184
|
+
return (Array.isArray(conns) ? conns : [])
|
|
185
|
+
.filter((c) => matchesPurpose(c.purpose_assignments, purpose))
|
|
186
|
+
.map((c) => String(c.id));
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async function dbTellingen(supabase, userId) {
|
|
193
|
+
try {
|
|
194
|
+
const connecties = await mailConnecties(supabase, userId);
|
|
195
|
+
const { data, error } = await supabase.rpc("dashboard_pit_tellingen", {
|
|
196
|
+
_vandaag: dashboardDatum(),
|
|
197
|
+
_mail_connecties: connecties,
|
|
198
|
+
});
|
|
199
|
+
if (error) {
|
|
200
|
+
console.warn("[dashboard] databasetelling niet beschikbaar, val terug:", error.message);
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
return leesDbTellingen(data);
|
|
204
|
+
}
|
|
205
|
+
catch (e) {
|
|
206
|
+
console.warn("[dashboard] databasetelling mislukt, val terug:", e);
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
export async function fetchDashboardData(supabase, userId, wekenTerug = 0) {
|
|
211
|
+
const head = { count: "exact", head: true };
|
|
212
|
+
const db = await dbTellingen(supabase, userId);
|
|
213
|
+
const [planningNietIngepland, facturatie, urenTeBeoordelen, mailsTeControleren, offertesMeerwerk,] = await Promise.all([
|
|
214
|
+
db ? db.planningNietIngepland : telPlanningNietIngepland(supabase),
|
|
215
|
+
db ? { werkbonnenOpen: db.werkbonnenOpen, facturenTeFactureren: db.facturenTeFactureren } : telFacturatieStatus(supabase),
|
|
216
|
+
db ? db.urenTeBeoordelen : telUrenTeBeoordelen(supabase),
|
|
217
|
+
db ? db.mailsTeControleren : telMailsTeControleren(supabase, userId),
|
|
218
|
+
telling(supabase
|
|
219
|
+
.from("itk_offertes")
|
|
220
|
+
.select("id", head)
|
|
221
|
+
.eq("source_type", "work_order")
|
|
222
|
+
.eq("status", "concept")),
|
|
223
|
+
]);
|
|
224
|
+
const werkbonnenOpen = facturatie.werkbonnenOpen;
|
|
225
|
+
const facturenTeFactureren = facturatie.facturenTeFactureren;
|
|
226
|
+
const { fetchDashboardStats } = await import("./dashboard-planner-installatie-techniek.stats.server");
|
|
227
|
+
const stats = await fetchDashboardStats(supabase, wekenTerug, db?.omzetOpen ?? null);
|
|
228
|
+
return {
|
|
229
|
+
stats,
|
|
230
|
+
kpis: {
|
|
231
|
+
planningNietIngepland,
|
|
232
|
+
werkbonnenOpen,
|
|
233
|
+
urenTeBeoordelen,
|
|
234
|
+
mailsTeControleren,
|
|
235
|
+
offertesMeerwerk,
|
|
236
|
+
facturenTeFactureren,
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
+
import type { DashboardStats, OmzetOpenStat, WerkUrenStat } from "./dashboard-planner-installatie-techniek.types";
|
|
3
|
+
type Supa = SupabaseClient<any>;
|
|
4
|
+
export declare function telWerkUren(supabase: Supa, nu?: Date, wekenTerug?: number): Promise<WerkUrenStat>;
|
|
5
|
+
export declare function fetchDashboardStats(supabase: Supa, wekenTerug?: number, omzetUitDb?: OmzetOpenStat | null): Promise<DashboardStats>;
|
|
6
|
+
/** Vandaag en de 27 voorgaande Nederlandse kalenderdagen. */
|
|
7
|
+
export declare function betaalPeriode(nu?: Date): {
|
|
8
|
+
vanaf: string;
|
|
9
|
+
tot: string;
|
|
10
|
+
};
|
|
11
|
+
/** Maandag 00:00 Nederlandse tijd tot en met nu — de gekozen week (ma → zo).
|
|
12
|
+
* Bij een eerdere week loopt de periode tot de maandag erna (de week is al voorbij). */
|
|
13
|
+
export declare function weekPeriode(nu?: Date, wekenTerug?: number): {
|
|
14
|
+
vanaf: string;
|
|
15
|
+
tot: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function telFacturenBetaald(supabase: Supa, nu?: Date): Promise<DashboardStats["facturenBetaald"]>;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=dashboard-planner-installatie-techniek.stats.server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dashboard-planner-installatie-techniek.stats.server.d.ts","sourceRoot":"","sources":["../../src/lib/dashboard-planner-installatie-techniek.stats.server.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,KAAK,EACV,cAAc,EAEd,aAAa,EACb,YAAY,EAGb,MAAM,gDAAgD,CAAC;AAGxD,KAAK,IAAI,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC;AAoFhC,wBAAsB,WAAW,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,GAAE,IAAiB,EAAE,UAAU,SAAI,GAAG,OAAO,CAAC,YAAY,CAAC,CAuD9G;AAiBD,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,IAAI,EAAE,UAAU,SAAI,EAAE,UAAU,CAAC,EAAE,aAAa,GAAG,IAAI,GAAG,OAAO,CAAC,cAAc,CAAC,CASpI;AAiBD,6DAA6D;AAC7D,wBAAgB,aAAa,CAAC,EAAE,OAAa;;;EAI5C;AAED;yFACyF;AACzF,wBAAgB,WAAW,CAAC,EAAE,OAAa,EAAE,UAAU,SAAI;;;EAQ1D;AAED,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,OAAa,GAAG,OAAO,CAAC,cAAc,CAAC,iBAAiB,CAAC,CAAC,CASpH"}
|