@flowselections/offertes-installatie-techniek 1.0.2 → 1.21.1
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 +16 -1
- package/dist-lib/components/offertes/ArtikelKiezer.d.ts +16 -0
- package/dist-lib/components/offertes/ArtikelKiezer.d.ts.map +1 -0
- package/dist-lib/components/offertes/ArtikelKiezer.js +56 -0
- package/dist-lib/components/offertes/ExtraWerkMeldingenLijst.d.ts +4 -0
- package/dist-lib/components/offertes/ExtraWerkMeldingenLijst.d.ts.map +1 -0
- package/dist-lib/components/offertes/ExtraWerkMeldingenLijst.js +42 -0
- package/dist-lib/components/offertes/ExtraWerkOffertePaneel.d.ts +14 -0
- package/dist-lib/components/offertes/ExtraWerkOffertePaneel.d.ts.map +1 -0
- package/dist-lib/components/offertes/ExtraWerkOffertePaneel.js +91 -0
- package/dist-lib/components/offertes/MoederWerkbonPaneel.d.ts +5 -0
- package/dist-lib/components/offertes/MoederWerkbonPaneel.d.ts.map +1 -0
- package/dist-lib/components/offertes/MoederWerkbonPaneel.js +66 -0
- package/dist-lib/components/offertes/NieuweOfferteFormulier.d.ts +33 -0
- package/dist-lib/components/offertes/NieuweOfferteFormulier.d.ts.map +1 -0
- package/dist-lib/components/offertes/NieuweOfferteFormulier.js +523 -0
- package/dist-lib/components/offertes/OfferteA4.d.ts +28 -0
- package/dist-lib/components/offertes/OfferteA4.d.ts.map +1 -0
- package/dist-lib/components/offertes/OfferteA4.js +47 -0
- package/dist-lib/components/offertes/OfferteAkkoordPage.d.ts +4 -0
- package/dist-lib/components/offertes/OfferteAkkoordPage.d.ts.map +1 -0
- package/dist-lib/components/offertes/OfferteAkkoordPage.js +66 -0
- package/dist-lib/components/offertes/OfferteDetailPage.d.ts.map +1 -1
- package/dist-lib/components/offertes/OfferteDetailPage.js +247 -23
- package/dist-lib/components/offertes/OfferteRegelsEditor.js +1 -1
- package/dist-lib/components/offertes/OfferteStatusBadge.d.ts.map +1 -1
- package/dist-lib/components/offertes/OfferteStatusBadge.js +1 -0
- package/dist-lib/components/offertes/OffertesOverzichtPage.d.ts.map +1 -1
- package/dist-lib/components/offertes/OffertesOverzichtPage.js +25 -3
- package/dist-lib/components/offertes/VoorstelOffertePaneel.d.ts +14 -0
- package/dist-lib/components/offertes/VoorstelOffertePaneel.d.ts.map +1 -0
- package/dist-lib/components/offertes/VoorstelOffertePaneel.js +144 -0
- package/dist-lib/components/offertes/WerkbonAanmakenPaneel.d.ts +32 -0
- package/dist-lib/components/offertes/WerkbonAanmakenPaneel.d.ts.map +1 -0
- package/dist-lib/components/offertes/WerkbonAanmakenPaneel.js +26 -0
- package/dist-lib/components/offertes/WerkbonVoorstellenLijst.d.ts +4 -0
- package/dist-lib/components/offertes/WerkbonVoorstellenLijst.d.ts.map +1 -0
- package/dist-lib/components/offertes/WerkbonVoorstellenLijst.js +47 -0
- package/dist-lib/components/settings/OfferteEmailSettingsCard.d.ts +2 -0
- package/dist-lib/components/settings/OfferteEmailSettingsCard.d.ts.map +1 -0
- package/dist-lib/components/settings/OfferteEmailSettingsCard.js +38 -0
- package/dist-lib/components/settings/OfferteHuisstijlSettingsCard.d.ts +2 -0
- package/dist-lib/components/settings/OfferteHuisstijlSettingsCard.d.ts.map +1 -0
- package/dist-lib/components/settings/OfferteHuisstijlSettingsCard.js +66 -0
- package/dist-lib/components/settings/OfferteVoorwaardenSettingsCard.d.ts +2 -0
- package/dist-lib/components/settings/OfferteVoorwaardenSettingsCard.d.ts.map +1 -0
- package/dist-lib/components/settings/OfferteVoorwaardenSettingsCard.js +30 -0
- package/dist-lib/hooks/useExtraWerkMeldingen.d.ts +32 -0
- package/dist-lib/hooks/useExtraWerkMeldingen.d.ts.map +1 -0
- package/dist-lib/hooks/useExtraWerkMeldingen.js +103 -0
- package/dist-lib/hooks/useHuisstijl.d.ts +32 -0
- package/dist-lib/hooks/useHuisstijl.d.ts.map +1 -0
- package/dist-lib/hooks/useHuisstijl.js +60 -0
- package/dist-lib/hooks/useOfferte.d.ts +21 -0
- package/dist-lib/hooks/useOfferte.d.ts.map +1 -1
- package/dist-lib/hooks/useOfferte.js +105 -4
- package/dist-lib/hooks/useOffertes.d.ts.map +1 -1
- package/dist-lib/hooks/useOffertes.js +17 -7
- package/dist-lib/hooks/useWerkbonVoorstellen.d.ts +45 -0
- package/dist-lib/hooks/useWerkbonVoorstellen.d.ts.map +1 -0
- package/dist-lib/hooks/useWerkbonVoorstellen.js +74 -0
- package/dist-lib/index.d.ts +19 -2
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +26 -4
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +4 -23403
- 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 -23403
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +2 -23403
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/previewAuthStorage.d.ts +1 -1
- package/dist-lib/integrations/supabase/previewAuthStorage.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/previewAuthStorage.js +8 -1
- package/dist-lib/integrations/supabase/types.d.ts +23504 -11091
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.js +2 -0
- package/dist-lib/lib/artikelenZoek.d.ts +17 -0
- package/dist-lib/lib/artikelenZoek.d.ts.map +1 -0
- package/dist-lib/lib/artikelenZoek.js +37 -0
- package/dist-lib/lib/offerteAcceptatie.functions.d.ts +7 -0
- package/dist-lib/lib/offerteAcceptatie.functions.d.ts.map +1 -0
- package/dist-lib/lib/offerteAcceptatie.functions.js +28 -0
- package/dist-lib/lib/offerteAdres.d.ts +7 -0
- package/dist-lib/lib/offerteAdres.d.ts.map +1 -0
- package/dist-lib/lib/offerteAdres.js +19 -0
- package/dist-lib/lib/offerteAi.functions.d.ts +34 -0
- package/dist-lib/lib/offerteAi.functions.d.ts.map +1 -0
- package/dist-lib/lib/offerteAi.functions.js +252 -0
- package/dist-lib/lib/offerteMail.d.ts +27 -0
- package/dist-lib/lib/offerteMail.d.ts.map +1 -0
- package/dist-lib/lib/offerteMail.js +141 -0
- package/dist-lib/lib/offerteRegels.d.ts +9 -0
- package/dist-lib/lib/offerteRegels.d.ts.map +1 -0
- package/dist-lib/lib/offerteRegels.js +20 -0
- package/dist-lib/lib/offerteRegels.test.d.ts +2 -0
- package/dist-lib/lib/offerteRegels.test.d.ts.map +1 -0
- package/dist-lib/lib/offerteRegels.test.js +17 -0
- package/dist-lib/lib/offerteReistijd.functions.d.ts +22 -0
- package/dist-lib/lib/offerteReistijd.functions.d.ts.map +1 -0
- package/dist-lib/lib/offerteReistijd.functions.js +173 -0
- package/dist-lib/lib/offertes.d.ts +8 -1
- package/dist-lib/lib/offertes.d.ts.map +1 -1
- package/dist-lib/lib/offertes.js +6 -4
- package/package.json +96 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"offerteRegels.test.d.ts","sourceRoot":"","sources":["../../src/lib/offerteRegels.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// @ts-ignore -- bun test-runner types are not installed in this project
|
|
2
|
+
import { describe, expect, test } from "bun:test";
|
|
3
|
+
import { offerteNummer, reisPakketAantal, verkoopPrijsCent } from "./offerteRegels";
|
|
4
|
+
describe("reistijd heen en terug per kwartier", () => {
|
|
5
|
+
test("19 min enkele rit = 38 min = 3 kwartieren", () => expect(reisPakketAantal(19, 15)).toBe(3));
|
|
6
|
+
test("restant onder 6 min valt weg (35 min = 2)", () => expect(reisPakketAantal(17.5, 15)).toBe(2));
|
|
7
|
+
test("restant van 6 min telt mee (36 min = 3)", () => expect(reisPakketAantal(18, 15)).toBe(3));
|
|
8
|
+
test("4 min totaal = geen pakket", () => expect(reisPakketAantal(2, 15)).toBe(0));
|
|
9
|
+
test("ongeldige pakketduur = 0", () => expect(reisPakketAantal(30, 0)).toBe(0));
|
|
10
|
+
});
|
|
11
|
+
describe("prijs met marge", () => {
|
|
12
|
+
test("eigen marge gaat voor", () => expect(verkoopPrijsCent(1000, 2000, 50, 20)).toBe(1500));
|
|
13
|
+
test("zonder netto: bruto met algemene marge", () => expect(verkoopPrijsCent(0, 1000, null, 20)).toBe(1200));
|
|
14
|
+
});
|
|
15
|
+
describe("offertenummer", () => {
|
|
16
|
+
test("eerste offerte 2026", () => expect(offerteNummer(2026, 1)).toBe("OFF2600001"));
|
|
17
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
type ReistijdStatus = "ok" | "wacht" | "onbekend" | "niet_ingesteld" | "geen_pakket" | "kaarten_fout";
|
|
2
|
+
export interface OfferteReistijdResultaat {
|
|
3
|
+
status: ReistijdStatus;
|
|
4
|
+
/** Totale reistijd (heen + terug) in minuten. */
|
|
5
|
+
minuten: number | null;
|
|
6
|
+
/** Reistijd van één rit (zaak naar klant) in minuten. */
|
|
7
|
+
enkeleRitMinuten?: number;
|
|
8
|
+
pakket: {
|
|
9
|
+
id: string;
|
|
10
|
+
naam: string;
|
|
11
|
+
minuten: number;
|
|
12
|
+
aantal: number;
|
|
13
|
+
} | null;
|
|
14
|
+
melding: string;
|
|
15
|
+
}
|
|
16
|
+
declare function inputValideren(input: unknown): {
|
|
17
|
+
accessToken: string;
|
|
18
|
+
destination: string;
|
|
19
|
+
};
|
|
20
|
+
export declare const berekenOfferteReistijd: import("@tanstack/start-client-core").OptionalFetcher<undefined, typeof inputValideren, Promise<OfferteReistijdResultaat>>;
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=offerteReistijd.functions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"offerteReistijd.functions.d.ts","sourceRoot":"","sources":["../../src/lib/offerteReistijd.functions.ts"],"names":[],"mappings":"AAIA,KAAK,cAAc,GAAG,IAAI,GAAG,OAAO,GAAG,UAAU,GAAG,gBAAgB,GAAG,aAAa,GAAG,cAAc,CAAC;AAEtG,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,cAAc,CAAC;IACvB,iDAAiD;IACjD,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,yDAAyD;IACzD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC7E,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,iBAAS,cAAc,CAAC,KAAK,EAAE,OAAO;;;EAKrC;AA4CD,eAAO,MAAM,sBAAsB,4HAmI/B,CAAC"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { createServerFn } from "@tanstack/react-start";
|
|
2
|
+
import { createClient } from "@supabase/supabase-js";
|
|
3
|
+
import { adresLijktCompleet } from "./offerteAdres";
|
|
4
|
+
function inputValideren(input) {
|
|
5
|
+
const value = input;
|
|
6
|
+
if (typeof value?.accessToken !== "string" || value.accessToken.length < 10)
|
|
7
|
+
throw new Error("Niet ingelogd");
|
|
8
|
+
if (typeof value?.destination !== "string")
|
|
9
|
+
throw new Error("Werkadres ontbreekt");
|
|
10
|
+
return { accessToken: value.accessToken, destination: value.destination.trim().slice(0, 500) };
|
|
11
|
+
}
|
|
12
|
+
async function adresHash(adres) {
|
|
13
|
+
const buffer = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(adres.trim().toLowerCase()));
|
|
14
|
+
return Array.from(new Uint8Array(buffer)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
15
|
+
}
|
|
16
|
+
/** Bedrijfsregel: een restant van minstens zoveel minuten telt als een extra reistijdpakket. */
|
|
17
|
+
import { reisPakketAantal } from "./offerteRegels";
|
|
18
|
+
function mislukt() {
|
|
19
|
+
return { status: "onbekend", minuten: null, pakket: null, melding: "Reistijd kon niet worden aangevraagd. Probeer het opnieuw." };
|
|
20
|
+
}
|
|
21
|
+
function kaartenFout(melding) {
|
|
22
|
+
return { status: "kaarten_fout", minuten: null, pakket: null, melding };
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Geeft de Google Maps-module een seintje om de wachtrij te verwerken en
|
|
26
|
+
* wacht op het antwoord. De URL komt uitsluitend uit de omgeving.
|
|
27
|
+
*/
|
|
28
|
+
async function seintjeKaarten(url) {
|
|
29
|
+
try {
|
|
30
|
+
const res = await fetch(url, {
|
|
31
|
+
method: "POST",
|
|
32
|
+
headers: { "Content-Type": "application/json" },
|
|
33
|
+
body: "{}",
|
|
34
|
+
signal: AbortSignal.timeout(30000),
|
|
35
|
+
});
|
|
36
|
+
if (!res.ok)
|
|
37
|
+
return `Kaarten-module niet bereikbaar (antwoord ${res.status}). Reistijd kan niet berekend worden.`;
|
|
38
|
+
const body = (await res.json().catch(() => null));
|
|
39
|
+
if (typeof body?.error === "string" && body.error)
|
|
40
|
+
return `Kaarten-module: ${body.error}`;
|
|
41
|
+
const skipped = typeof body?.skipped === "string" ? body.skipped : "";
|
|
42
|
+
if (skipped && skipped !== "Verwerking is al bezig")
|
|
43
|
+
return `Kaarten-module: ${skipped}`;
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return "Kaarten-module reageert niet. Reistijd kan niet berekend worden.";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export const berekenOfferteReistijd = createServerFn({ method: "POST" })
|
|
51
|
+
.inputValidator(inputValideren)
|
|
52
|
+
.handler(async ({ data }) => {
|
|
53
|
+
const url = process.env["SUPABASE_URL"];
|
|
54
|
+
const publishableKey = process.env["SUPABASE_PUBLISHABLE_KEY"];
|
|
55
|
+
if (!url || !publishableKey)
|
|
56
|
+
throw new Error("Databaseconfiguratie ontbreekt");
|
|
57
|
+
const authClient = createClient(url, publishableKey, { auth: { persistSession: false, autoRefreshToken: false } });
|
|
58
|
+
const { data: gebruiker, error: authFout } = await authClient.auth.getUser(data.accessToken);
|
|
59
|
+
if (authFout || !gebruiker.user)
|
|
60
|
+
throw new Error("Niet ingelogd");
|
|
61
|
+
const gebruikerId = gebruiker.user.id;
|
|
62
|
+
if (!adresLijktCompleet(data.destination)) {
|
|
63
|
+
return { status: "onbekend", minuten: null, pakket: null, melding: "Vul een volledig werkadres in (straat, huisnummer, plaats)." };
|
|
64
|
+
}
|
|
65
|
+
// Fail closed: zonder seintje-adres wordt de wachtrij nooit verwerkt.
|
|
66
|
+
const cronUrl = process.env["MAPS_CRON_URL"]?.trim();
|
|
67
|
+
if (!cronUrl) {
|
|
68
|
+
return kaartenFout("Kaarten-module niet ingesteld (MAPS_CRON_URL ontbreekt). Reistijd kan niet berekend worden.");
|
|
69
|
+
}
|
|
70
|
+
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
71
|
+
const admin = supabaseAdmin;
|
|
72
|
+
const [{ data: instelling }, { data: pakketRijen }] = await Promise.all([
|
|
73
|
+
admin.from("planning_settings").select("company_address").eq("singleton", true).maybeSingle(),
|
|
74
|
+
admin.from("werkbon_packages")
|
|
75
|
+
.select("id, name, travel_minutes, labor_minutes, duration_minutes, sort_order, code")
|
|
76
|
+
.eq("active", true)
|
|
77
|
+
.gt("travel_minutes", 0)
|
|
78
|
+
.eq("labor_minutes", 0)
|
|
79
|
+
.order("travel_minutes", { ascending: true })
|
|
80
|
+
.order("sort_order", { ascending: true })
|
|
81
|
+
.order("code", { ascending: true }),
|
|
82
|
+
]);
|
|
83
|
+
const vertrek = typeof instelling?.company_address === "string" ? instelling.company_address.trim() : "";
|
|
84
|
+
if (!vertrek) {
|
|
85
|
+
return { status: "niet_ingesteld", minuten: null, pakket: null, melding: "Vul eerst het adres van de zaak in bij Planning." };
|
|
86
|
+
}
|
|
87
|
+
const veiligePakketten = Array.isArray(pakketRijen) ? pakketRijen : [];
|
|
88
|
+
const reisPakket = veiligePakketten.find((pakket) => Number(pakket.travel_minutes) > 0 &&
|
|
89
|
+
Number(pakket.labor_minutes ?? 0) === 0 &&
|
|
90
|
+
Number(pakket.duration_minutes ?? pakket.travel_minutes) === Number(pakket.travel_minutes));
|
|
91
|
+
if (!reisPakket) {
|
|
92
|
+
return { status: "geen_pakket", minuten: null, pakket: null, melding: "Er is geen actief pakket dat uitsluitend reistijd bevat." };
|
|
93
|
+
}
|
|
94
|
+
if (vertrek.toLowerCase() === data.destination.toLowerCase()) {
|
|
95
|
+
return { status: "ok", minuten: 0, pakket: null, melding: "Geen reistijdpakket nodig." };
|
|
96
|
+
}
|
|
97
|
+
const [vertrekHash, bestemmingHash] = await Promise.all([adresHash(vertrek), adresHash(data.destination)]);
|
|
98
|
+
/** Leest de cache; zet ontbrekend werk in de wachtrij. null = nog wachten. */
|
|
99
|
+
const stap = async () => {
|
|
100
|
+
const { data: adresRijen } = await admin
|
|
101
|
+
.from("maps_addresses")
|
|
102
|
+
.select("id, address_hash, status")
|
|
103
|
+
.in("address_hash", [vertrekHash, bestemmingHash]);
|
|
104
|
+
const veiligeAdressen = Array.isArray(adresRijen) ? adresRijen : [];
|
|
105
|
+
const perHash = new Map(veiligeAdressen.map((rij) => [String(rij.address_hash), rij]));
|
|
106
|
+
if (perHash.get(vertrekHash)?.status === "failed" || perHash.get(bestemmingHash)?.status === "failed") {
|
|
107
|
+
return { status: "onbekend", minuten: null, pakket: null, melding: "Het vertrek- of werkadres kon niet worden gevonden. Controleer het adres." };
|
|
108
|
+
}
|
|
109
|
+
const ontbrekend = [
|
|
110
|
+
{ hash: vertrekHash, adres: vertrek },
|
|
111
|
+
{ hash: bestemmingHash, adres: data.destination },
|
|
112
|
+
].filter((item) => !perHash.has(item.hash));
|
|
113
|
+
if (ontbrekend.length > 0) {
|
|
114
|
+
const { error: adresFout } = await admin.from("maps_addresses").upsert(ontbrekend.map((item) => ({ address_hash: item.hash, raw_address: item.adres, status: "pending" })), { onConflict: "address_hash", ignoreDuplicates: true });
|
|
115
|
+
if (adresFout)
|
|
116
|
+
return mislukt();
|
|
117
|
+
const { error: geoFout } = await admin.from("maps_request_queue").upsert(ontbrekend.map((item) => ({
|
|
118
|
+
kind: "geocode", origin_hash: item.hash, destination_hash: "", raw_origin: item.adres,
|
|
119
|
+
raw_destination: "", mode: "drive", status: "pending", attempts: 0, last_error: null, processed_at: null, requested_by: gebruikerId,
|
|
120
|
+
})), { onConflict: "kind,origin_hash,destination_hash,mode" });
|
|
121
|
+
if (geoFout)
|
|
122
|
+
return mislukt();
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
const oorsprong = perHash.get(vertrekHash);
|
|
126
|
+
const bestemming = perHash.get(bestemmingHash);
|
|
127
|
+
if (oorsprong?.status !== "resolved" || bestemming?.status !== "resolved")
|
|
128
|
+
return null;
|
|
129
|
+
const { data: rit } = await admin.from("maps_travel_times")
|
|
130
|
+
.select("duration_sec")
|
|
131
|
+
.eq("origin_id", oorsprong.id)
|
|
132
|
+
.eq("destination_id", bestemming.id)
|
|
133
|
+
.eq("mode", "drive")
|
|
134
|
+
.gt("expires_at", new Date().toISOString())
|
|
135
|
+
.maybeSingle();
|
|
136
|
+
if (Number(rit?.duration_sec) > 0) {
|
|
137
|
+
// Heen en terug: de rit telt twee keer mee.
|
|
138
|
+
const enkeleRit = Math.max(1, Math.round(Number(rit.duration_sec) / 60));
|
|
139
|
+
const totaal = enkeleRit * 2;
|
|
140
|
+
const pakketMinuten = Number(reisPakket.travel_minutes);
|
|
141
|
+
const aantal = reisPakketAantal(enkeleRit, pakketMinuten);
|
|
142
|
+
return {
|
|
143
|
+
status: "ok",
|
|
144
|
+
minuten: totaal,
|
|
145
|
+
enkeleRitMinuten: enkeleRit,
|
|
146
|
+
pakket: aantal > 0
|
|
147
|
+
? { id: String(reisPakket.id), naam: String(reisPakket.name), minuten: pakketMinuten, aantal }
|
|
148
|
+
: null,
|
|
149
|
+
melding: `${enkeleRit} min heen + ${enkeleRit} min terug = ${totaal} min${aantal > 0 ? "" : " · geen reistijdpakket nodig"}.`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const { error: ritFout } = await admin.from("maps_request_queue").upsert({
|
|
153
|
+
kind: "matrix", origin_hash: vertrekHash, destination_hash: bestemmingHash,
|
|
154
|
+
raw_origin: vertrek, raw_destination: data.destination, mode: "drive", status: "pending",
|
|
155
|
+
attempts: 0, last_error: null, processed_at: null, requested_by: gebruikerId,
|
|
156
|
+
}, { onConflict: "kind,origin_hash,destination_hash,mode" });
|
|
157
|
+
if (ritFout)
|
|
158
|
+
return mislukt();
|
|
159
|
+
return null;
|
|
160
|
+
};
|
|
161
|
+
// Maximaal twee verwerkingsrondes: eerst adressen opzoeken, dan de route.
|
|
162
|
+
for (let ronde = 0; ronde < 3; ronde += 1) {
|
|
163
|
+
const resultaat = await stap();
|
|
164
|
+
if (resultaat)
|
|
165
|
+
return resultaat;
|
|
166
|
+
if (ronde === 2)
|
|
167
|
+
break;
|
|
168
|
+
const fout = await seintjeKaarten(cronUrl);
|
|
169
|
+
if (fout)
|
|
170
|
+
return kaartenFout(fout);
|
|
171
|
+
}
|
|
172
|
+
return { status: "wacht", minuten: null, pakket: null, melding: "Reistijd wordt exact berekend. Even wachten…" };
|
|
173
|
+
});
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
export type OfferteStatus = "concept" | "mandaat" | "verzonden" | "bekeken" | "goedgekeurd" | "omgezet" | "afgewezen" | "verlopen";
|
|
1
|
+
export type OfferteStatus = "concept" | "mandaat" | "verzonden" | "bekeken" | "goedgekeurd" | "wacht_op_werkbon" | "omgezet" | "afgewezen" | "verlopen";
|
|
2
2
|
export interface OfferteRegel {
|
|
3
3
|
id: string;
|
|
4
4
|
offerte_id: string;
|
|
5
5
|
package_id: string | null;
|
|
6
6
|
source: string;
|
|
7
7
|
omschrijving: string;
|
|
8
|
+
unit: string | null;
|
|
8
9
|
quantity: number;
|
|
9
10
|
unit_price_cents: number;
|
|
10
11
|
line_total_cents: number;
|
|
@@ -23,7 +24,10 @@ export interface Offerte {
|
|
|
23
24
|
contact_id: string | null;
|
|
24
25
|
work_location_id: string | null;
|
|
25
26
|
status: string;
|
|
27
|
+
onderwerp: string | null;
|
|
26
28
|
omschrijving: string;
|
|
29
|
+
aandachtspunten: string | null;
|
|
30
|
+
voorwaarden: string | null;
|
|
27
31
|
notities: string | null;
|
|
28
32
|
subtotal_cents: number;
|
|
29
33
|
vat_rate: number;
|
|
@@ -69,6 +73,9 @@ export interface Pakket {
|
|
|
69
73
|
unit_price_cents: number | null;
|
|
70
74
|
default_quantity: number | null;
|
|
71
75
|
active: boolean;
|
|
76
|
+
duration_minutes: number | null;
|
|
77
|
+
labor_minutes: number | null;
|
|
78
|
+
travel_minutes: number | null;
|
|
72
79
|
}
|
|
73
80
|
export declare const OFFERTE_STATUS_LABELS: Record<string, string>;
|
|
74
81
|
/** Tabbladen in het overzicht — key `alle` toont alles. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"offertes.d.ts","sourceRoot":"","sources":["../../src/lib/offertes.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"offertes.d.ts","sourceRoot":"","sources":["../../src/lib/offertes.ts"],"names":[],"mappings":"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"}
|
package/dist-lib/lib/offertes.js
CHANGED
|
@@ -7,6 +7,7 @@ export const OFFERTE_STATUS_LABELS = {
|
|
|
7
7
|
verzonden: "Verzonden",
|
|
8
8
|
bekeken: "Bekeken",
|
|
9
9
|
goedgekeurd: "Goedgekeurd",
|
|
10
|
+
wacht_op_werkbon: "Wacht op werkbon",
|
|
10
11
|
omgezet: "Omgezet",
|
|
11
12
|
afgewezen: "Afgewezen",
|
|
12
13
|
verlopen: "Verlopen",
|
|
@@ -16,10 +17,11 @@ export const OFFERTE_TABS = [
|
|
|
16
17
|
{ key: "alle", label: "Alle" },
|
|
17
18
|
{ key: "concept", label: "Concept", statuses: ["concept"] },
|
|
18
19
|
{ key: "verzonden", label: "Verzonden", statuses: ["verzonden", "bekeken"] },
|
|
19
|
-
{
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
{
|
|
21
|
+
key: "goedgekeurd",
|
|
22
|
+
label: "Goedgekeurd",
|
|
23
|
+
statuses: ["goedgekeurd", "wacht_op_werkbon"],
|
|
24
|
+
},
|
|
23
25
|
];
|
|
24
26
|
export const OPEN_STATUSES = ["concept", "mandaat", "verzonden", "bekeken"];
|
|
25
27
|
export function formatEuro(cents) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowselections/offertes-installatie-techniek",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.1",
|
|
4
4
|
"flowselections": {
|
|
5
5
|
"moduleId": "offertes-installatietechniek",
|
|
6
6
|
"pages": [
|
|
@@ -13,8 +13,96 @@
|
|
|
13
13
|
"export": "OfferteDetailRoute",
|
|
14
14
|
"route": "offertes/$offerteId",
|
|
15
15
|
"title": "Offerte detail"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"export": "OfferteAkkoordPage",
|
|
19
|
+
"route": "offerte-akkoord/$token",
|
|
20
|
+
"title": "Offerte akkoord",
|
|
21
|
+
"public": true
|
|
16
22
|
}
|
|
17
|
-
]
|
|
23
|
+
],
|
|
24
|
+
"handover": {
|
|
25
|
+
"titel": "Offertes Installatie Techniek",
|
|
26
|
+
"samenvatting": "Maakt van een afgeronde werkbon een offerte, controleert het mandaat, en maakt vanuit een goedgekeurde offerte direct een nieuwe vervolgwerkbon. Toont extra-werkmeldingen van monteurs (boven mandaat) en laat die met AI uitwerken tot offerte. Het extra-werkpaneel start met 35% melding en 65% offerte, met een centrale [<>]-sleepgreep; op smalle schermen staan de delen onder elkaar. Versie 1.20.6 gebruikt react-resizable-panels rechtstreeks omdat core de schuifonderdelen nog niet openbaar exporteert.",
|
|
27
|
+
"eigenTabellen": [
|
|
28
|
+
"itk_offertes",
|
|
29
|
+
"itk_offerte_regels",
|
|
30
|
+
"itk_offerte_events",
|
|
31
|
+
"itk_offerte_acceptaties",
|
|
32
|
+
"itk_offerte_settings",
|
|
33
|
+
"itk_offerte_werkbon_queue",
|
|
34
|
+
"itk_offerte_voorstel_status"
|
|
35
|
+
],
|
|
36
|
+
"leestTabellen": [
|
|
37
|
+
"werkbonnen",
|
|
38
|
+
"werkbon_package_lines",
|
|
39
|
+
"werkbon_materials",
|
|
40
|
+
"werkbon_packages",
|
|
41
|
+
"itk_customers",
|
|
42
|
+
"werkbon_proposals",
|
|
43
|
+
"werkbon_proposal_lines",
|
|
44
|
+
"invoice_branding_settings",
|
|
45
|
+
"planning_settings",
|
|
46
|
+
"maps_addresses",
|
|
47
|
+
"maps_travel_times"
|
|
48
|
+
],
|
|
49
|
+
"schrijftTabellen": [
|
|
50
|
+
"itk_offertes",
|
|
51
|
+
"itk_offerte_regels",
|
|
52
|
+
"itk_offerte_events",
|
|
53
|
+
"itk_offerte_acceptaties",
|
|
54
|
+
"itk_offerte_settings",
|
|
55
|
+
"itk_offerte_werkbon_queue",
|
|
56
|
+
"itk_offerte_voorstel_status",
|
|
57
|
+
"email_auth_outbox",
|
|
58
|
+
"itk_customers",
|
|
59
|
+
"werkbonnen",
|
|
60
|
+
"werkbon_package_lines",
|
|
61
|
+
"maps_addresses",
|
|
62
|
+
"maps_request_queue"
|
|
63
|
+
],
|
|
64
|
+
"biedtFuncties": [
|
|
65
|
+
"quote_create_from_source(_source_type text, _source_id uuid, _payload jsonb)",
|
|
66
|
+
"quote_get_summary(_quotation_id uuid)",
|
|
67
|
+
"quote_get_billable_lines(_quotation_id uuid)",
|
|
68
|
+
"quote_list_sources(_source_type text, _limit integer)",
|
|
69
|
+
"quote_list_packages()",
|
|
70
|
+
"quote_queue_follow_up(_offerte_id uuid)",
|
|
71
|
+
"quote_list_pending_work_orders(_limit integer)",
|
|
72
|
+
"quote_ack_work_order(_queue_id uuid, _werkbon_id uuid, _werkbon_nummer text)",
|
|
73
|
+
"quote_fail_work_order(_queue_id uuid, _foutmelding text)",
|
|
74
|
+
"itk_offerte_naar_werkbon(_offerte_id uuid)",
|
|
75
|
+
"quote_assign_number",
|
|
76
|
+
"quote_create_follow_up_now(_offerte_id uuid, _payload jsonb)",
|
|
77
|
+
"quote_create_from_scratch(_payload jsonb)",
|
|
78
|
+
"quote_sync_work_order(_offerte_id uuid, _monteur_instructies text)",
|
|
79
|
+
"quote_public_accept(_token uuid, _naam text, _email text, _akkoord boolean, _ip_address text, _user_agent text)"
|
|
80
|
+
],
|
|
81
|
+
"gebruiktFuncties": [
|
|
82
|
+
"wo_create_from_source(_source_type text, _source_id uuid, _payload jsonb)",
|
|
83
|
+
"invoice_save_branding_settings",
|
|
84
|
+
"email_auth_for_purpose"
|
|
85
|
+
],
|
|
86
|
+
"eigenBuckets": [],
|
|
87
|
+
"gebruiktBuckets": [
|
|
88
|
+
"layout-logos"
|
|
89
|
+
],
|
|
90
|
+
"bekendeRisicos": [
|
|
91
|
+
"Leeskant (afspraak 11 sep 2026): Offertes leest werkbongegevens rechtstreeks in quote_create_from_source (werkbonnen, werkbon_package_lines, werkbon_packages, werkbon_materials) en in quote_list_packages (werkbon_packages). Er zijn geen wo_*-aanroepen meer. Kolomwijzigingen bij Werkbonnen kunnen deze functies breken.",
|
|
92
|
+
"Schrijfkant (afspraak 28 sep 2026): Offertes schrijft niet rechtstreeks in werkbonnen/werkbon_*. quote_create_follow_up_now gebruikt de officiële Werkbonnen-functie wo_create_from_source transactioneel en koppelt het resultaat direct; de oude wachtrij blijft alleen voor bestaande aanvragen beschikbaar.",
|
|
93
|
+
"itk_offertes.werkbon_id is een backwards-compatibility-kolom; leidend is het drieveldenmodel source_type/source_ref/source_number.",
|
|
94
|
+
"Alle bedragen zijn hele centen (integer); nummering OFF-<jaar>-<volgnummer> gaat uitsluitend via de trigger assign_itk_offerte_number.",
|
|
95
|
+
"quote_get_billable_lines levert het gezamenlijke ketenformaat (Engelse veldnamen, kind verplicht, bedragen in hele centen).",
|
|
96
|
+
"Goedgekeurde werkbonvoorstellen (E-mailmodule) worden direct gelezen uit werkbon_proposals/werkbon_proposal_lines; gezien/omgezet staat in eigen tabel itk_offerte_voorstel_status. Offertes vanuit een voorstel krijgen source_type 'werkbon_voorstel'. AI-concept via Lovable AI (LOVABLE_API_KEY).",
|
|
97
|
+
"Extra-werkmeldingen (Werkbonnen-monteur) worden herkend als itk_offertes met source_type 'work_order', status 'concept' en notities beginnend met 'Extra werk gemeld'. Herkenning via notitietekst is kwetsbaar; voorstel aan beheerder: vaste markering vanuit de monteur-app (bijv. source_type 'extra_werk'). Gezien/uitgewerkt/AI-concept worden vastgelegd als itk_offerte_events (melding_bekeken, melding_uitgewerkt, melding_ai_concept).",
|
|
98
|
+
"Offertehuisstijl is gedeeld met en eigendom van de Facturen-module (invoice_branding_settings, invoice_save_branding_settings, bucket layout-logos). Offertes volgt ook payment_term_days en show_company_name uit hetzelfde contract; zonder Facturen toont de offerte een foutmelding (fail closed).",
|
|
99
|
+
"Offertes worden als PDF-bijlage verstuurd via de e-mailmodule (email_auth_outbox, doeleinde in itk_offerte_settings.email_purpose); zonder gekozen doeleinde of gekoppelde mailbox wordt niets verstuurd (fail closed).",
|
|
100
|
+
"Offertenummer (OFF + jj + 5 cijfers, bv. OFF2600001) wordt pas bij verzenden toegekend via quote_assign_number; concepten hebben geen nummer.",
|
|
101
|
+
"Nieuwe offertes berekenen de enkele exacte autorit vanaf planning_settings.company_address via de gedeelde maps-cache. Ontbrekende routes worden aangemeld en blokkeren verzending; er wordt nooit geschat. Het kleinste actieve zuivere reistijdpakket wordt automatisch toegevoegd aan offerte en werkbonconcept."
|
|
102
|
+
],
|
|
103
|
+
"herzieningstermijnDagen": 90,
|
|
104
|
+
"laatstBevestigdOp": "2026-10-01"
|
|
105
|
+
}
|
|
18
106
|
},
|
|
19
107
|
"private": false,
|
|
20
108
|
"sideEffects": false,
|
|
@@ -55,13 +143,16 @@
|
|
|
55
143
|
"sonner": "^2.0.0"
|
|
56
144
|
},
|
|
57
145
|
"dependencies": {
|
|
58
|
-
"@supabase/supabase-js": "^2.112.3"
|
|
146
|
+
"@supabase/supabase-js": "^2.112.3",
|
|
147
|
+
"html2canvas-pro": "^2.4.5",
|
|
148
|
+
"jspdf": "^4.2.1",
|
|
149
|
+
"react-resizable-panels": "^4.6.5"
|
|
59
150
|
},
|
|
60
151
|
"devDependencies": {
|
|
61
152
|
"@cloudflare/vite-plugin": "^1.25.5",
|
|
62
153
|
"@eslint/js": "^9.32.0",
|
|
63
|
-
"@flowselections/core": "1.0.
|
|
64
|
-
"@lovable.dev/vite-tanstack-config": "2.
|
|
154
|
+
"@flowselections/core": "1.0.29",
|
|
155
|
+
"@lovable.dev/vite-tanstack-config": "2.26.5",
|
|
65
156
|
"@rsbuild/core": "^1.0.0",
|
|
66
157
|
"@tailwindcss/vite": "^4.2.1",
|
|
67
158
|
"@tanstack/react-query": "^5.83.0",
|