@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
|
@@ -19,6 +19,7 @@ export const Constants = {
|
|
|
19
19
|
"sales",
|
|
20
20
|
"developer",
|
|
21
21
|
"receptie",
|
|
22
|
+
"superadmin",
|
|
22
23
|
],
|
|
23
24
|
doc_layout_type: [
|
|
24
25
|
"pakbon",
|
|
@@ -38,6 +39,7 @@ export const Constants = {
|
|
|
38
39
|
"veilingbon",
|
|
39
40
|
"afleverbrief",
|
|
40
41
|
"veilingbrief",
|
|
42
|
+
"traysticker",
|
|
41
43
|
],
|
|
42
44
|
doc_orientation: ["portrait", "landscape"],
|
|
43
45
|
exact_env: ["staging", "live"],
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tijdelijke adapter met dezelfde vorm als `zoekArtikelen` uit
|
|
3
|
+
* @flowselections/artikelen-installatie-techniek-planner.
|
|
4
|
+
* Dat pakket is nog niet gepubliceerd; zodra het beschikbaar is,
|
|
5
|
+
* vervang je de body van `zoekArtikelenContract` door een aanroep van `zoekArtikelen`.
|
|
6
|
+
*/
|
|
7
|
+
export interface ContractArtikel {
|
|
8
|
+
id: string;
|
|
9
|
+
artikelcode: string | null;
|
|
10
|
+
naam: string | null;
|
|
11
|
+
gtin: string | null;
|
|
12
|
+
eenheid: string | null;
|
|
13
|
+
verkoop_prijs_cent: number;
|
|
14
|
+
foto_url: string | null;
|
|
15
|
+
}
|
|
16
|
+
export declare function zoekArtikelenContract(query: string, limit?: number): Promise<ContractArtikel[]>;
|
|
17
|
+
//# sourceMappingURL=artikelenZoek.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"artikelenZoek.d.ts","sourceRoot":"","sources":["../../src/lib/artikelenZoek.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAKD,wBAAsB,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CA8BjG"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { supabase } from "@flowselections/core";
|
|
2
|
+
const db = supabase;
|
|
3
|
+
const FOTO_BUCKET = "artikel-fotos";
|
|
4
|
+
export async function zoekArtikelenContract(query, limit = 50) {
|
|
5
|
+
const q = query.trim().slice(0, 100).replace(/[%,()]/g, " ");
|
|
6
|
+
let req = db
|
|
7
|
+
.from("artikelen_cache")
|
|
8
|
+
.select("id,artikelcode,naam,omschrijving,gtin,eenheid,netto_prijs_cent,bruto_prijs_cent,marge_procent,foto_pad")
|
|
9
|
+
.order("naam")
|
|
10
|
+
.limit(limit);
|
|
11
|
+
if (q)
|
|
12
|
+
req = req.or(`artikelcode.ilike.%${q}%,naam.ilike.%${q}%,gtin.ilike.%${q}%`);
|
|
13
|
+
const [a, i] = await Promise.all([req, db.from("artikelen_instellingen").select("marge_procent").limit(1).maybeSingle()]);
|
|
14
|
+
const rijen = Array.isArray(a?.data) ? a.data : [];
|
|
15
|
+
const algemeen = Number(i?.data?.marge_procent ?? 0) || 0;
|
|
16
|
+
const paden = rijen.map((r) => r.foto_pad).filter((p) => typeof p === "string" && p.length > 0);
|
|
17
|
+
const urls = new Map();
|
|
18
|
+
if (paden.length) {
|
|
19
|
+
const { data } = await db.storage.from(FOTO_BUCKET).createSignedUrls(paden, 60 * 60);
|
|
20
|
+
for (const d of Array.isArray(data) ? data : [])
|
|
21
|
+
if (d?.path && d?.signedUrl)
|
|
22
|
+
urls.set(d.path, d.signedUrl);
|
|
23
|
+
}
|
|
24
|
+
return rijen.map((r) => {
|
|
25
|
+
const inkoop = r.netto_prijs_cent > 0 ? r.netto_prijs_cent : (r.bruto_prijs_cent ?? 0);
|
|
26
|
+
const marge = r.marge_procent != null ? Number(r.marge_procent) : algemeen;
|
|
27
|
+
return {
|
|
28
|
+
id: r.id,
|
|
29
|
+
artikelcode: r.artikelcode,
|
|
30
|
+
naam: r.naam ?? r.omschrijving,
|
|
31
|
+
gtin: r.gtin,
|
|
32
|
+
eenheid: r.eenheid,
|
|
33
|
+
verkoop_prijs_cent: Math.round(inkoop * (1 + marge / 100)),
|
|
34
|
+
foto_url: r.foto_pad ? (urls.get(r.foto_pad) ?? null) : null,
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const accepteerPubliekeOfferte: import("@tanstack/start-client-core").OptionalFetcher<undefined, (input: unknown) => {
|
|
2
|
+
token: string;
|
|
3
|
+
naam: string;
|
|
4
|
+
email: string;
|
|
5
|
+
akkoord: true;
|
|
6
|
+
}, Promise<import("../integrations/supabase/types").Json>>;
|
|
7
|
+
//# sourceMappingURL=offerteAcceptatie.functions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"offerteAcceptatie.functions.d.ts","sourceRoot":"","sources":["../../src/lib/offerteAcceptatie.functions.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,wBAAwB;;;;;0DAiBjC,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createServerFn } from "@tanstack/react-start";
|
|
2
|
+
import { getRequestHeader } from "@tanstack/react-start/server";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
const acceptatieSchema = z.object({
|
|
5
|
+
token: z.string().uuid(),
|
|
6
|
+
naam: z.string().trim().min(2).max(200),
|
|
7
|
+
email: z.string().trim().email().max(254),
|
|
8
|
+
akkoord: z.literal(true),
|
|
9
|
+
});
|
|
10
|
+
export const accepteerPubliekeOfferte = createServerFn({ method: "POST" })
|
|
11
|
+
.inputValidator((input) => acceptatieSchema.parse(input))
|
|
12
|
+
.handler(async ({ data }) => {
|
|
13
|
+
const forwardedFor = getRequestHeader("x-forwarded-for")?.split(",")[0]?.trim();
|
|
14
|
+
const ipAddress = getRequestHeader("cf-connecting-ip") ?? forwardedFor;
|
|
15
|
+
const userAgent = getRequestHeader("user-agent")?.slice(0, 1000);
|
|
16
|
+
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
17
|
+
const { data: result, error } = await supabaseAdmin.rpc("quote_public_accept", {
|
|
18
|
+
_token: data.token,
|
|
19
|
+
_naam: data.naam,
|
|
20
|
+
_email: data.email,
|
|
21
|
+
_akkoord: data.akkoord,
|
|
22
|
+
_ip_address: ipAddress,
|
|
23
|
+
_user_agent: userAgent,
|
|
24
|
+
});
|
|
25
|
+
if (error)
|
|
26
|
+
throw new Error(error.message || "Offerte accepteren is mislukt.");
|
|
27
|
+
return result;
|
|
28
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Controleert of een werkadres compleet genoeg is om een route te laten
|
|
3
|
+
* berekenen: een Nederlandse postcode, of een deel met huisnummer gevolgd
|
|
4
|
+
* door een deel met een plaatsnaam ("Straat 12, Plaats").
|
|
5
|
+
*/
|
|
6
|
+
export declare function adresLijktCompleet(adres: string): boolean;
|
|
7
|
+
//# sourceMappingURL=offerteAdres.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"offerteAdres.d.ts","sourceRoot":"","sources":["../../src/lib/offerteAdres.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CASzD"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Controleert of een werkadres compleet genoeg is om een route te laten
|
|
3
|
+
* berekenen: een Nederlandse postcode, of een deel met huisnummer gevolgd
|
|
4
|
+
* door een deel met een plaatsnaam ("Straat 12, Plaats").
|
|
5
|
+
*/
|
|
6
|
+
export function adresLijktCompleet(adres) {
|
|
7
|
+
const tekst = (adres ?? "").trim();
|
|
8
|
+
if (tekst.length < 6)
|
|
9
|
+
return false;
|
|
10
|
+
if (!/\d/.test(tekst))
|
|
11
|
+
return false;
|
|
12
|
+
if (/\b\d{4}\s?[a-z]{2}\b/i.test(tekst))
|
|
13
|
+
return true;
|
|
14
|
+
const delen = tekst.split(",").map((deel) => deel.trim());
|
|
15
|
+
const metNummer = delen.findIndex((deel) => /\d/.test(deel) && /[a-zà-ÿ]{2,}/i.test(deel));
|
|
16
|
+
if (metNummer < 0)
|
|
17
|
+
return false;
|
|
18
|
+
return delen.slice(metNummer + 1).some((deel) => /[a-zà-ÿ]{3,}/i.test(deel));
|
|
19
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
interface Input {
|
|
2
|
+
accessToken: string;
|
|
3
|
+
voorstel: Record<string, unknown>;
|
|
4
|
+
regels: Record<string, unknown>[];
|
|
5
|
+
pakketten: Record<string, unknown>[];
|
|
6
|
+
}
|
|
7
|
+
export interface AiOfferteConcept {
|
|
8
|
+
omschrijving: string;
|
|
9
|
+
regels: {
|
|
10
|
+
description: string;
|
|
11
|
+
quantity: number;
|
|
12
|
+
unit_price_cents: number;
|
|
13
|
+
}[];
|
|
14
|
+
vat_rate: number;
|
|
15
|
+
geldig_dagen: number;
|
|
16
|
+
}
|
|
17
|
+
export declare const genereerOfferteConcept: import("@tanstack/start-client-core").OptionalFetcher<undefined, (d: unknown) => Input, Promise<{
|
|
18
|
+
concept: AiOfferteConcept | null;
|
|
19
|
+
error: string | null;
|
|
20
|
+
}>>;
|
|
21
|
+
interface TekstInput {
|
|
22
|
+
accessToken: string;
|
|
23
|
+
formulier: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
export interface AiOfferteTeksten {
|
|
26
|
+
klant_tekst: string;
|
|
27
|
+
monteur_instructies: string;
|
|
28
|
+
}
|
|
29
|
+
export declare const genereerOfferteTeksten: import("@tanstack/start-client-core").OptionalFetcher<undefined, (d: unknown) => TekstInput, Promise<{
|
|
30
|
+
teksten: AiOfferteTeksten | null;
|
|
31
|
+
error: string | null;
|
|
32
|
+
}>>;
|
|
33
|
+
export {};
|
|
34
|
+
//# sourceMappingURL=offerteAi.functions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"offerteAi.functions.d.ts","sourceRoot":"","sources":["../../src/lib/offerteAi.functions.ts"],"names":[],"mappings":"AAYA,UAAU,KAAK;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAClC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;CACtC;AAUD,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC9E,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;CACtB;AA0BD,eAAO,MAAM,sBAAsB,uEACb,OAAO;aACmB,gBAAgB,GAAG,IAAI;WAAS,MAAM,GAAG,IAAI;GAiGzF,CAAC;AAOL,UAAU,UAAU;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,mBAAmB,EAAE,MAAM,CAAC;CAC7B;AAYD,eAAO,MAAM,sBAAsB,uEACb,OAAO,KAAG,UAAU;aAMM,gBAAgB,GAAG,IAAI;WAAS,MAAM,GAAG,IAAI;GA4EzF,CAAC"}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// src/lib/offerteAi.functions.ts — AI stelt een offerteconcept op uit een
|
|
3
|
+
// werkbonvoorstel. Draait server-side; LOVABLE_API_KEY blijft op de server.
|
|
4
|
+
// ============================================================================
|
|
5
|
+
import { createServerFn } from "@tanstack/react-start";
|
|
6
|
+
import { createClient } from "@supabase/supabase-js";
|
|
7
|
+
// Eén plek voor AI-adres en model (platformwaarden, niet per klant).
|
|
8
|
+
const AI_GATEWAY_URL = "https://ai.gateway.lovable.dev/v1/responses";
|
|
9
|
+
const AI_MODEL = "openai/gpt-6-astra";
|
|
10
|
+
function parseInput(d) {
|
|
11
|
+
const o = (d ?? {});
|
|
12
|
+
if (typeof o.accessToken !== "string" || o.accessToken.length < 10)
|
|
13
|
+
throw new Error("Ongeldig token");
|
|
14
|
+
if (!o.voorstel || typeof o.voorstel !== "object")
|
|
15
|
+
throw new Error("Voorstel ontbreekt");
|
|
16
|
+
const regels = Array.isArray(o.regels) ? o.regels.slice(0, 200) : [];
|
|
17
|
+
const pakketten = Array.isArray(o.pakketten) ? o.pakketten.slice(0, 500) : [];
|
|
18
|
+
return { accessToken: o.accessToken, voorstel: o.voorstel, regels, pakketten };
|
|
19
|
+
}
|
|
20
|
+
const SCHEMA = {
|
|
21
|
+
type: "object",
|
|
22
|
+
additionalProperties: false,
|
|
23
|
+
required: ["omschrijving", "regels", "vat_rate", "geldig_dagen"],
|
|
24
|
+
properties: {
|
|
25
|
+
omschrijving: { type: "string" },
|
|
26
|
+
regels: {
|
|
27
|
+
type: "array",
|
|
28
|
+
items: {
|
|
29
|
+
type: "object",
|
|
30
|
+
additionalProperties: false,
|
|
31
|
+
required: ["description", "quantity", "unit_price_cents"],
|
|
32
|
+
properties: {
|
|
33
|
+
description: { type: "string" },
|
|
34
|
+
quantity: { type: "number" },
|
|
35
|
+
unit_price_cents: { type: "integer" },
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
vat_rate: { type: "number" },
|
|
40
|
+
geldig_dagen: { type: "integer" },
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
export const genereerOfferteConcept = createServerFn({ method: "POST" })
|
|
44
|
+
.inputValidator((d) => parseInput(d))
|
|
45
|
+
.handler(async ({ data }) => {
|
|
46
|
+
const url = process.env["SUPABASE_URL"];
|
|
47
|
+
const anon = process.env["SUPABASE_PUBLISHABLE_KEY"];
|
|
48
|
+
const key = process.env["LOVABLE_API_KEY"];
|
|
49
|
+
if (!url || !anon)
|
|
50
|
+
return { concept: null, error: "Databaseconfiguratie ontbreekt." };
|
|
51
|
+
if (!key)
|
|
52
|
+
return { concept: null, error: "AI is niet geconfigureerd (LOVABLE_API_KEY ontbreekt)." };
|
|
53
|
+
const sb = createClient(url, anon, { auth: { persistSession: false } });
|
|
54
|
+
const { data: userRes, error: authErr } = await sb.auth.getUser(data.accessToken);
|
|
55
|
+
if (authErr || !userRes?.user)
|
|
56
|
+
return { concept: null, error: "Niet ingelogd." };
|
|
57
|
+
const prompt = [
|
|
58
|
+
"Je bent calculator bij een installatietechniekbedrijf. Maak van het onderstaande werkbonvoorstel of de extra-werkmelding van een monteur een nette, zakelijke Nederlandse offerte.",
|
|
59
|
+
"- Splits het werk in heldere offerteregels (arbeid, voorrijden, materiaal, pakketten).",
|
|
60
|
+
"- Gebruik waar passend prijzen uit de pakketcatalogus; bedragen in hele eurocenten, exclusief btw.",
|
|
61
|
+
"- 'omschrijving' is een korte klantvriendelijke toelichting (max 4 zinnen).",
|
|
62
|
+
"- vat_rate standaard 21, geldig_dagen standaard 30.",
|
|
63
|
+
"",
|
|
64
|
+
"WERKBONVOORSTEL:",
|
|
65
|
+
JSON.stringify(data.voorstel),
|
|
66
|
+
"VOORSTELREGELS:",
|
|
67
|
+
JSON.stringify(data.regels),
|
|
68
|
+
"PAKKETCATALOGUS:",
|
|
69
|
+
JSON.stringify(data.pakketten),
|
|
70
|
+
].join("\n");
|
|
71
|
+
const res = await fetch(AI_GATEWAY_URL, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: {
|
|
74
|
+
"Content-Type": "application/json",
|
|
75
|
+
"Lovable-API-Key": key,
|
|
76
|
+
"X-Lovable-AIG-SDK": "fetch",
|
|
77
|
+
},
|
|
78
|
+
body: JSON.stringify({
|
|
79
|
+
model: AI_MODEL,
|
|
80
|
+
input: prompt,
|
|
81
|
+
stream: true,
|
|
82
|
+
store: false,
|
|
83
|
+
reasoning: { effort: "low" },
|
|
84
|
+
text: { format: { type: "json_schema", name: "offerte", strict: true, schema: SCHEMA } },
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
if (!res.ok || !res.body) {
|
|
88
|
+
const body = await res.text().catch(() => "");
|
|
89
|
+
console.error(`AI gateway [${res.status}]: ${body}`);
|
|
90
|
+
if (res.status === 402)
|
|
91
|
+
return { concept: null, error: "AI-tegoed is op. Voeg tegoed toe in de werkruimte-instellingen." };
|
|
92
|
+
if (res.status === 429)
|
|
93
|
+
return { concept: null, error: "Te veel AI-verzoeken, probeer het zo opnieuw." };
|
|
94
|
+
return { concept: null, error: `AI-verzoek mislukt (${res.status}).` };
|
|
95
|
+
}
|
|
96
|
+
const reader = res.body.getReader();
|
|
97
|
+
const dec = new TextDecoder();
|
|
98
|
+
let buf = "";
|
|
99
|
+
let text = "";
|
|
100
|
+
let refusal = false;
|
|
101
|
+
for (;;) {
|
|
102
|
+
const { done, value } = await reader.read();
|
|
103
|
+
if (done)
|
|
104
|
+
break;
|
|
105
|
+
buf += dec.decode(value, { stream: true });
|
|
106
|
+
let idx;
|
|
107
|
+
while ((idx = buf.indexOf("\n")) >= 0) {
|
|
108
|
+
const line = buf.slice(0, idx).trim();
|
|
109
|
+
buf = buf.slice(idx + 1);
|
|
110
|
+
if (!line.startsWith("data:"))
|
|
111
|
+
continue;
|
|
112
|
+
const payload = line.slice(5).trim();
|
|
113
|
+
if (!payload || payload === "[DONE]")
|
|
114
|
+
continue;
|
|
115
|
+
try {
|
|
116
|
+
const ev = JSON.parse(payload);
|
|
117
|
+
if (ev.type === "response.output_text.delta")
|
|
118
|
+
text += ev.delta ?? "";
|
|
119
|
+
if (ev.type === "response.refusal.delta")
|
|
120
|
+
refusal = true;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
/* onvolledig frame */
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (refusal || !text)
|
|
128
|
+
return { concept: null, error: "AI kon geen offerte opstellen." };
|
|
129
|
+
try {
|
|
130
|
+
const c = JSON.parse(text);
|
|
131
|
+
const regels = Array.isArray(c.regels) ? c.regels : [];
|
|
132
|
+
return {
|
|
133
|
+
concept: {
|
|
134
|
+
omschrijving: String(c.omschrijving ?? ""),
|
|
135
|
+
regels: regels.map((r) => ({
|
|
136
|
+
description: String(r.description ?? ""),
|
|
137
|
+
quantity: Number(r.quantity) || 1,
|
|
138
|
+
unit_price_cents: Math.round(Number(r.unit_price_cents) || 0),
|
|
139
|
+
})),
|
|
140
|
+
vat_rate: Number(c.vat_rate) || 21,
|
|
141
|
+
geldig_dagen: Math.max(1, Math.round(Number(c.geldig_dagen) || 30)),
|
|
142
|
+
},
|
|
143
|
+
error: null,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return { concept: null, error: "AI-antwoord kon niet worden gelezen." };
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
const TEKST_SCHEMA = {
|
|
151
|
+
type: "object",
|
|
152
|
+
additionalProperties: false,
|
|
153
|
+
required: ["klant_tekst", "monteur_instructies"],
|
|
154
|
+
properties: {
|
|
155
|
+
klant_tekst: { type: "string" },
|
|
156
|
+
monteur_instructies: { type: "string" },
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
export const genereerOfferteTeksten = createServerFn({ method: "POST" })
|
|
160
|
+
.inputValidator((d) => {
|
|
161
|
+
const o = (d ?? {});
|
|
162
|
+
if (typeof o.accessToken !== "string" || o.accessToken.length < 10)
|
|
163
|
+
throw new Error("Ongeldig token");
|
|
164
|
+
if (!o.formulier || typeof o.formulier !== "object")
|
|
165
|
+
throw new Error("Formulier ontbreekt");
|
|
166
|
+
return { accessToken: o.accessToken, formulier: o.formulier };
|
|
167
|
+
})
|
|
168
|
+
.handler(async ({ data }) => {
|
|
169
|
+
const url = process.env["SUPABASE_URL"];
|
|
170
|
+
const anon = process.env["SUPABASE_PUBLISHABLE_KEY"];
|
|
171
|
+
const key = process.env["LOVABLE_API_KEY"];
|
|
172
|
+
if (!url || !anon)
|
|
173
|
+
return { teksten: null, error: "Databaseconfiguratie ontbreekt." };
|
|
174
|
+
if (!key)
|
|
175
|
+
return { teksten: null, error: "AI is niet geconfigureerd (LOVABLE_API_KEY ontbreekt)." };
|
|
176
|
+
const sb = createClient(url, anon, { auth: { persistSession: false } });
|
|
177
|
+
const { data: userRes, error: authErr } = await sb.auth.getUser(data.accessToken);
|
|
178
|
+
if (authErr || !userRes?.user)
|
|
179
|
+
return { teksten: null, error: "Niet ingelogd." };
|
|
180
|
+
const prompt = [
|
|
181
|
+
"Je werkt bij een installatietechniekbedrijf. De eigenaar heeft een vragenlijst ingevuld voor een nieuwe klus. Maak twee Nederlandse teksten:",
|
|
182
|
+
"1. klant_tekst: voor in de offerte aan de klant. Uitleggend, vriendelijk en zakelijk: wat er aan de hand is, wat we gaan doen en waarom. 4 tot 8 zinnen, geen prijzen noemen, geen gegevens verzinnen.",
|
|
183
|
+
"2. monteur_instructies: voor de monteur op de werkbon. Formeel en kort: vertel in enkele zinnen wat er moet gebeuren (wat, eventuele bijzonderheden zoals veiligheid of bereikbaarheid). GEEN stap-voor-stap handleiding en geen genummerde werkwijze. NOEM GEEN LOCATIE: het werkadres, het kamernummer en klantgegevens staan al in de gegevens van de werkbon en mogen in de monteurinstructies niet worden herhaald — schrijf dus geen adres, kamer/ruimtenummer of klantnaam uit.",
|
|
184
|
+
"",
|
|
185
|
+
"VRAGENLIJST:",
|
|
186
|
+
JSON.stringify(data.formulier),
|
|
187
|
+
].join("\n");
|
|
188
|
+
const res = await fetch(AI_GATEWAY_URL, {
|
|
189
|
+
method: "POST",
|
|
190
|
+
headers: { "Content-Type": "application/json", "Lovable-API-Key": key, "X-Lovable-AIG-SDK": "fetch" },
|
|
191
|
+
body: JSON.stringify({
|
|
192
|
+
model: AI_MODEL,
|
|
193
|
+
input: prompt,
|
|
194
|
+
stream: true,
|
|
195
|
+
store: false,
|
|
196
|
+
reasoning: { effort: "low" },
|
|
197
|
+
text: { format: { type: "json_schema", name: "teksten", strict: true, schema: TEKST_SCHEMA } },
|
|
198
|
+
}),
|
|
199
|
+
});
|
|
200
|
+
if (!res.ok || !res.body) {
|
|
201
|
+
const body = await res.text().catch(() => "");
|
|
202
|
+
console.error(`AI gateway [${res.status}]: ${body}`);
|
|
203
|
+
if (res.status === 402)
|
|
204
|
+
return { teksten: null, error: "AI-tegoed is op. Voeg tegoed toe in de werkruimte-instellingen." };
|
|
205
|
+
if (res.status === 429)
|
|
206
|
+
return { teksten: null, error: "Te veel AI-verzoeken, probeer het zo opnieuw." };
|
|
207
|
+
return { teksten: null, error: `AI-verzoek mislukt (${res.status}).` };
|
|
208
|
+
}
|
|
209
|
+
const reader = res.body.getReader();
|
|
210
|
+
const dec = new TextDecoder();
|
|
211
|
+
let buf = "";
|
|
212
|
+
let text = "";
|
|
213
|
+
let refusal = false;
|
|
214
|
+
for (;;) {
|
|
215
|
+
const { done, value } = await reader.read();
|
|
216
|
+
if (done)
|
|
217
|
+
break;
|
|
218
|
+
buf += dec.decode(value, { stream: true });
|
|
219
|
+
let idx;
|
|
220
|
+
while ((idx = buf.indexOf("\n")) >= 0) {
|
|
221
|
+
const line = buf.slice(0, idx).trim();
|
|
222
|
+
buf = buf.slice(idx + 1);
|
|
223
|
+
if (!line.startsWith("data:"))
|
|
224
|
+
continue;
|
|
225
|
+
const payload = line.slice(5).trim();
|
|
226
|
+
if (!payload || payload === "[DONE]")
|
|
227
|
+
continue;
|
|
228
|
+
try {
|
|
229
|
+
const ev = JSON.parse(payload);
|
|
230
|
+
if (ev.type === "response.output_text.delta")
|
|
231
|
+
text += ev.delta ?? "";
|
|
232
|
+
if (ev.type === "response.refusal.delta")
|
|
233
|
+
refusal = true;
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
/* onvolledig frame */
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (refusal || !text)
|
|
241
|
+
return { teksten: null, error: "AI kon geen teksten opstellen." };
|
|
242
|
+
try {
|
|
243
|
+
const t = JSON.parse(text);
|
|
244
|
+
return {
|
|
245
|
+
teksten: { klant_tekst: String(t.klant_tekst ?? ""), monteur_instructies: String(t.monteur_instructies ?? "") },
|
|
246
|
+
error: null,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return { teksten: null, error: "AI-antwoord kon niet worden gelezen." };
|
|
251
|
+
}
|
|
252
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export declare const OFFERTE_EMAIL_PURPOSES: Array<{
|
|
2
|
+
key: string;
|
|
3
|
+
label: string;
|
|
4
|
+
}>;
|
|
5
|
+
export interface OfferteMailbox {
|
|
6
|
+
id: string;
|
|
7
|
+
provider: string | null;
|
|
8
|
+
account_email: string | null;
|
|
9
|
+
display_name: string | null;
|
|
10
|
+
status: string | null;
|
|
11
|
+
}
|
|
12
|
+
export declare function haalOfferteEmailPurpose(): Promise<string | null>;
|
|
13
|
+
export declare function bewaarOfferteEmailPurpose(purpose: string): Promise<void>;
|
|
14
|
+
export declare function haalOfferteVoorwaarden(): Promise<string>;
|
|
15
|
+
export declare function bewaarOfferteVoorwaarden(tekst: string): Promise<void>;
|
|
16
|
+
export declare function haalMailboxVoorPurpose(purpose: string | null): Promise<OfferteMailbox | null>;
|
|
17
|
+
/** Zet elk A4-element om naar een eigen PDF-pagina en geeft base64 terug. */
|
|
18
|
+
export declare function a4NaarPdfBase64(paginas: HTMLElement[]): Promise<string>;
|
|
19
|
+
export declare function verstuurOfferteEmail(p: {
|
|
20
|
+
offerteId: string;
|
|
21
|
+
to: string;
|
|
22
|
+
subject: string;
|
|
23
|
+
message: string;
|
|
24
|
+
pdfBase64: string;
|
|
25
|
+
filename: string;
|
|
26
|
+
}): Promise<void>;
|
|
27
|
+
//# sourceMappingURL=offerteMail.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"offerteMail.d.ts","sourceRoot":"","sources":["../../src/lib/offerteMail.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,sBAAsB,EAAE,KAAK,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAQxE,CAAC;AAEF,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAKtE;AAED,wBAAsB,yBAAyB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAU9E;AAGD,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,MAAM,CAAC,CAK9D;AAED,wBAAsB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAW3E;AAED,wBAAsB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAMnG;AAED,6EAA6E;AAC7E,wBAAsB,eAAe,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAc7E;AAED,wBAAsB,oBAAoB,CAAC,CAAC,EAAE;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC,IAAI,CAAC,CA8ChB"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Verzenden van offertes via de e-mailmodule (E-mail - Authenticatie):
|
|
2
|
+
// doeleinde → gekoppelde mailbox (email_auth_for_purpose) → verzendwachtrij (email_auth_outbox).
|
|
3
|
+
import { supabase } from "@flowselections/core";
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5
|
+
const db = supabase;
|
|
6
|
+
// Doeleinden zoals de e-mailmodule die kent (gelijk aan de Facturen-module).
|
|
7
|
+
export const OFFERTE_EMAIL_PURPOSES = [
|
|
8
|
+
{ key: "orders", label: "Orders" },
|
|
9
|
+
{ key: "support", label: "Support" },
|
|
10
|
+
{ key: "internal", label: "Intern" },
|
|
11
|
+
{ key: "system", label: "Systeem" },
|
|
12
|
+
{ key: "marketing", label: "Marketing" },
|
|
13
|
+
{ key: "inkoop", label: "Inkoop" },
|
|
14
|
+
{ key: "transport", label: "Transport" },
|
|
15
|
+
];
|
|
16
|
+
export async function haalOfferteEmailPurpose() {
|
|
17
|
+
const { data, error } = await db.from("itk_offerte_settings").select("email_purpose").eq("singleton", true).maybeSingle();
|
|
18
|
+
if (error)
|
|
19
|
+
throw error;
|
|
20
|
+
const v = data?.email_purpose;
|
|
21
|
+
return typeof v === "string" && v ? v : null;
|
|
22
|
+
}
|
|
23
|
+
export async function bewaarOfferteEmailPurpose(purpose) {
|
|
24
|
+
const { data, error } = await db.from("itk_offerte_settings").select("id").eq("singleton", true).maybeSingle();
|
|
25
|
+
if (error)
|
|
26
|
+
throw error;
|
|
27
|
+
if (data?.id) {
|
|
28
|
+
const { error: e } = await db.from("itk_offerte_settings").update({ email_purpose: purpose }).eq("id", data.id);
|
|
29
|
+
if (e)
|
|
30
|
+
throw e;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
const { error: e } = await db.from("itk_offerte_settings").insert({ singleton: true, email_purpose: purpose });
|
|
34
|
+
if (e)
|
|
35
|
+
throw e;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Standaardtekst voor "Voorwaarden en toelichting" op nieuwe offertes.
|
|
39
|
+
export async function haalOfferteVoorwaarden() {
|
|
40
|
+
const { data, error } = await db.from("itk_offerte_settings").select("voorwaarden").eq("singleton", true).maybeSingle();
|
|
41
|
+
if (error)
|
|
42
|
+
throw error;
|
|
43
|
+
const v = data?.voorwaarden;
|
|
44
|
+
return typeof v === "string" ? v : "";
|
|
45
|
+
}
|
|
46
|
+
export async function bewaarOfferteVoorwaarden(tekst) {
|
|
47
|
+
const { data, error } = await db.from("itk_offerte_settings").select("id").eq("singleton", true).maybeSingle();
|
|
48
|
+
if (error)
|
|
49
|
+
throw error;
|
|
50
|
+
const waarde = tekst.trim() ? tekst : null;
|
|
51
|
+
if (data?.id) {
|
|
52
|
+
const { error: e } = await db.from("itk_offerte_settings").update({ voorwaarden: waarde }).eq("id", data.id);
|
|
53
|
+
if (e)
|
|
54
|
+
throw e;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const { error: e } = await db.from("itk_offerte_settings").insert({ singleton: true, voorwaarden: waarde });
|
|
58
|
+
if (e)
|
|
59
|
+
throw e;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export async function haalMailboxVoorPurpose(purpose) {
|
|
63
|
+
if (!purpose)
|
|
64
|
+
return null;
|
|
65
|
+
const { data, error } = await db.rpc("email_auth_for_purpose", { _purpose: purpose, _direction: "send" });
|
|
66
|
+
if (error)
|
|
67
|
+
throw error;
|
|
68
|
+
const lijst = Array.isArray(data) ? data : [];
|
|
69
|
+
return lijst[0] ?? null;
|
|
70
|
+
}
|
|
71
|
+
/** Zet elk A4-element om naar een eigen PDF-pagina en geeft base64 terug. */
|
|
72
|
+
export async function a4NaarPdfBase64(paginas) {
|
|
73
|
+
if (!paginas.length)
|
|
74
|
+
throw new Error("Geen offertepagina's om te verzenden.");
|
|
75
|
+
const [{ default: html2canvas }, { jsPDF }] = await Promise.all([import("html2canvas-pro"), import("jspdf")]);
|
|
76
|
+
const pdf = new jsPDF({ unit: "mm", format: "a4" });
|
|
77
|
+
for (let i = 0; i < paginas.length; i++) {
|
|
78
|
+
const canvas = await html2canvas(paginas[i], { scale: 2, useCORS: true, backgroundColor: "#ffffff" });
|
|
79
|
+
if (i > 0)
|
|
80
|
+
pdf.addPage();
|
|
81
|
+
const w = 210;
|
|
82
|
+
const h = Math.min((canvas.height * w) / canvas.width, 297);
|
|
83
|
+
pdf.addImage(canvas.toDataURL("image/jpeg", 0.92), "JPEG", 0, 0, w, h);
|
|
84
|
+
}
|
|
85
|
+
const base64 = pdf.output("datauristring").split(",")[1] ?? "";
|
|
86
|
+
if (base64.length < 1000)
|
|
87
|
+
throw new Error("De offerte-PDF is leeg of onvolledig.");
|
|
88
|
+
return base64;
|
|
89
|
+
}
|
|
90
|
+
export async function verstuurOfferteEmail(p) {
|
|
91
|
+
const purpose = await haalOfferteEmailPurpose();
|
|
92
|
+
if (!purpose)
|
|
93
|
+
throw new Error("Kies eerst een e-maildoeleinde bij Instellingen → Offerte e-mail.");
|
|
94
|
+
const mailbox = await haalMailboxVoorPurpose(purpose);
|
|
95
|
+
if (!mailbox?.id)
|
|
96
|
+
throw new Error(`Er is geen mailbox gekoppeld aan het doeleinde '${purpose}'. Koppel er een in de e-mailmodule.`);
|
|
97
|
+
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
98
|
+
const { data: u } = await supabase.auth.getUser();
|
|
99
|
+
// Akkoordlink: altijd op het domein van de eigen omgeving (geen vaste URL).
|
|
100
|
+
const { data: tok, error: tokErr } = await db.from("itk_offertes").select("sign_token").eq("id", p.offerteId).single();
|
|
101
|
+
if (tokErr || !tok?.sign_token)
|
|
102
|
+
throw new Error("Akkoordlink voor deze offerte ontbreekt.");
|
|
103
|
+
if (typeof window === "undefined" || !window.location?.origin)
|
|
104
|
+
throw new Error("Adres van de omgeving onbekend.");
|
|
105
|
+
const link = `${window.location.origin}/offerte-akkoord/${tok.sign_token}`;
|
|
106
|
+
const knop = `<p style="margin:24px 0"><a href="${link}" style="background:#15803d;color:#ffffff;padding:12px 22px;border-radius:6px;text-decoration:none;font-weight:600;display:inline-block">Offerte bekijken en online akkoord geven</a></p>` +
|
|
107
|
+
`<p style="font-size:12px;color:#6b7280">Werkt de knop niet? Open dan deze link: <a href="${link}">${link}</a></p>`;
|
|
108
|
+
const { data: rij, error } = await db
|
|
109
|
+
.from("email_auth_outbox")
|
|
110
|
+
.insert({
|
|
111
|
+
purpose,
|
|
112
|
+
connection_id: mailbox.id,
|
|
113
|
+
to_addresses: [p.to],
|
|
114
|
+
subject: p.subject,
|
|
115
|
+
text_body: `${p.message}\n\nOnline akkoord geven: ${link}`,
|
|
116
|
+
html_body: `<p>${esc(p.message).replace(/\n/g, "<br>")}</p>${knop}`,
|
|
117
|
+
attachments: [{ filename: p.filename, content_type: "application/pdf", encoding: "base64", content_base64: p.pdfBase64 }],
|
|
118
|
+
created_by: u?.user?.id ?? null,
|
|
119
|
+
})
|
|
120
|
+
.select("id, attachments")
|
|
121
|
+
.single();
|
|
122
|
+
if (error)
|
|
123
|
+
throw error;
|
|
124
|
+
if (!Array.isArray(rij?.attachments) || rij.attachments.length !== 1) {
|
|
125
|
+
throw new Error("De PDF-bijlage is niet opgeslagen; de offerte is niet verzonden.");
|
|
126
|
+
}
|
|
127
|
+
const nu = new Date().toISOString();
|
|
128
|
+
const { error: uErr } = await db
|
|
129
|
+
.from("itk_offertes")
|
|
130
|
+
.update({ status: "verzonden", sent_at: nu, verzonden_naar: p.to })
|
|
131
|
+
.eq("id", p.offerteId);
|
|
132
|
+
if (uErr)
|
|
133
|
+
throw uErr;
|
|
134
|
+
await db.from("itk_offerte_events").insert({
|
|
135
|
+
offerte_id: p.offerteId,
|
|
136
|
+
event_type: "verzonden",
|
|
137
|
+
omschrijving: `Offerte per e-mail verzonden naar ${p.to}`,
|
|
138
|
+
metadata: { outbox_id: rij.id, purpose },
|
|
139
|
+
user_id: u?.user?.id ?? null,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Restant van minder dan dit aantal minuten valt weg; vanaf dit aantal telt een extra pakket. */
|
|
2
|
+
export declare const RESTGRENS_MINUTEN = 6;
|
|
3
|
+
/** Aantal reispakketten voor een enkele rit: heen + terug, afgerond per pakketduur. */
|
|
4
|
+
export declare function reisPakketAantal(enkeleRitMinuten: number, pakketMinuten: number): number;
|
|
5
|
+
/** Verkoopprijs in centen: inkoop (netto, anders bruto) plus eigen of algemene marge. */
|
|
6
|
+
export declare function verkoopPrijsCent(nettoCent: number | null | undefined, brutoCent: number | null | undefined, margeArtikel: number | null | undefined, margeAlgemeen: number): number;
|
|
7
|
+
/** Offertenummerformaat zoals de database het toekent: OFF + jaar (2 cijfers) + 5 cijfers. */
|
|
8
|
+
export declare function offerteNummer(jaar: number, volgnummer: number): string;
|
|
9
|
+
//# sourceMappingURL=offerteRegels.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"offerteRegels.d.ts","sourceRoot":"","sources":["../../src/lib/offerteRegels.ts"],"names":[],"mappings":"AAEA,kGAAkG;AAClG,eAAO,MAAM,iBAAiB,IAAI,CAAC;AAEnC,uFAAuF;AACvF,wBAAgB,gBAAgB,CAAC,gBAAgB,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAIxF;AAED,yFAAyF;AACzF,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAInL;AAED,8FAA8F;AAC9F,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAEtE"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Vaste rekenregels van de offertemodule (los van UI en database, zodat ze getest kunnen worden).
|
|
2
|
+
/** Restant van minder dan dit aantal minuten valt weg; vanaf dit aantal telt een extra pakket. */
|
|
3
|
+
export const RESTGRENS_MINUTEN = 6;
|
|
4
|
+
/** Aantal reispakketten voor een enkele rit: heen + terug, afgerond per pakketduur. */
|
|
5
|
+
export function reisPakketAantal(enkeleRitMinuten, pakketMinuten) {
|
|
6
|
+
if (!(pakketMinuten > 0) || !(enkeleRitMinuten > 0))
|
|
7
|
+
return 0;
|
|
8
|
+
const totaal = enkeleRitMinuten * 2;
|
|
9
|
+
return Math.floor(totaal / pakketMinuten) + (totaal % pakketMinuten >= RESTGRENS_MINUTEN ? 1 : 0);
|
|
10
|
+
}
|
|
11
|
+
/** Verkoopprijs in centen: inkoop (netto, anders bruto) plus eigen of algemene marge. */
|
|
12
|
+
export function verkoopPrijsCent(nettoCent, brutoCent, margeArtikel, margeAlgemeen) {
|
|
13
|
+
const inkoop = (nettoCent ?? 0) > 0 ? Number(nettoCent) : Number(brutoCent ?? 0);
|
|
14
|
+
const marge = margeArtikel != null ? Number(margeArtikel) : margeAlgemeen;
|
|
15
|
+
return Math.round(inkoop * (1 + marge / 100));
|
|
16
|
+
}
|
|
17
|
+
/** Offertenummerformaat zoals de database het toekent: OFF + jaar (2 cijfers) + 5 cijfers. */
|
|
18
|
+
export function offerteNummer(jaar, volgnummer) {
|
|
19
|
+
return `OFF${String(jaar % 100).padStart(2, "0")}${String(volgnummer).padStart(5, "0")}`;
|
|
20
|
+
}
|