@flowselections/offertes-installatie-techniek 1.0.3 → 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.
Files changed (105) hide show
  1. package/README.md +16 -1
  2. package/dist-lib/components/offertes/ArtikelKiezer.d.ts +16 -0
  3. package/dist-lib/components/offertes/ArtikelKiezer.d.ts.map +1 -0
  4. package/dist-lib/components/offertes/ArtikelKiezer.js +56 -0
  5. package/dist-lib/components/offertes/ExtraWerkMeldingenLijst.d.ts +4 -0
  6. package/dist-lib/components/offertes/ExtraWerkMeldingenLijst.d.ts.map +1 -0
  7. package/dist-lib/components/offertes/ExtraWerkMeldingenLijst.js +42 -0
  8. package/dist-lib/components/offertes/ExtraWerkOffertePaneel.d.ts +14 -0
  9. package/dist-lib/components/offertes/ExtraWerkOffertePaneel.d.ts.map +1 -0
  10. package/dist-lib/components/offertes/ExtraWerkOffertePaneel.js +91 -0
  11. package/dist-lib/components/offertes/MoederWerkbonPaneel.d.ts +5 -0
  12. package/dist-lib/components/offertes/MoederWerkbonPaneel.d.ts.map +1 -0
  13. package/dist-lib/components/offertes/MoederWerkbonPaneel.js +66 -0
  14. package/dist-lib/components/offertes/NieuweOfferteFormulier.d.ts +33 -0
  15. package/dist-lib/components/offertes/NieuweOfferteFormulier.d.ts.map +1 -0
  16. package/dist-lib/components/offertes/NieuweOfferteFormulier.js +523 -0
  17. package/dist-lib/components/offertes/OfferteA4.d.ts +28 -0
  18. package/dist-lib/components/offertes/OfferteA4.d.ts.map +1 -0
  19. package/dist-lib/components/offertes/OfferteA4.js +47 -0
  20. package/dist-lib/components/offertes/OfferteAkkoordPage.d.ts +4 -0
  21. package/dist-lib/components/offertes/OfferteAkkoordPage.d.ts.map +1 -0
  22. package/dist-lib/components/offertes/OfferteAkkoordPage.js +66 -0
  23. package/dist-lib/components/offertes/OfferteDetailPage.d.ts.map +1 -1
  24. package/dist-lib/components/offertes/OfferteDetailPage.js +247 -23
  25. package/dist-lib/components/offertes/OfferteRegelsEditor.js +1 -1
  26. package/dist-lib/components/offertes/OfferteStatusBadge.d.ts.map +1 -1
  27. package/dist-lib/components/offertes/OfferteStatusBadge.js +1 -0
  28. package/dist-lib/components/offertes/OffertesOverzichtPage.d.ts.map +1 -1
  29. package/dist-lib/components/offertes/OffertesOverzichtPage.js +25 -3
  30. package/dist-lib/components/offertes/VoorstelOffertePaneel.d.ts +14 -0
  31. package/dist-lib/components/offertes/VoorstelOffertePaneel.d.ts.map +1 -0
  32. package/dist-lib/components/offertes/VoorstelOffertePaneel.js +144 -0
  33. package/dist-lib/components/offertes/WerkbonAanmakenPaneel.d.ts +32 -0
  34. package/dist-lib/components/offertes/WerkbonAanmakenPaneel.d.ts.map +1 -0
  35. package/dist-lib/components/offertes/WerkbonAanmakenPaneel.js +26 -0
  36. package/dist-lib/components/offertes/WerkbonVoorstellenLijst.d.ts +4 -0
  37. package/dist-lib/components/offertes/WerkbonVoorstellenLijst.d.ts.map +1 -0
  38. package/dist-lib/components/offertes/WerkbonVoorstellenLijst.js +47 -0
  39. package/dist-lib/components/settings/OfferteEmailSettingsCard.d.ts +2 -0
  40. package/dist-lib/components/settings/OfferteEmailSettingsCard.d.ts.map +1 -0
  41. package/dist-lib/components/settings/OfferteEmailSettingsCard.js +38 -0
  42. package/dist-lib/components/settings/OfferteHuisstijlSettingsCard.d.ts +2 -0
  43. package/dist-lib/components/settings/OfferteHuisstijlSettingsCard.d.ts.map +1 -0
  44. package/dist-lib/components/settings/OfferteHuisstijlSettingsCard.js +66 -0
  45. package/dist-lib/components/settings/OfferteVoorwaardenSettingsCard.d.ts +2 -0
  46. package/dist-lib/components/settings/OfferteVoorwaardenSettingsCard.d.ts.map +1 -0
  47. package/dist-lib/components/settings/OfferteVoorwaardenSettingsCard.js +30 -0
  48. package/dist-lib/hooks/useExtraWerkMeldingen.d.ts +32 -0
  49. package/dist-lib/hooks/useExtraWerkMeldingen.d.ts.map +1 -0
  50. package/dist-lib/hooks/useExtraWerkMeldingen.js +103 -0
  51. package/dist-lib/hooks/useHuisstijl.d.ts +32 -0
  52. package/dist-lib/hooks/useHuisstijl.d.ts.map +1 -0
  53. package/dist-lib/hooks/useHuisstijl.js +60 -0
  54. package/dist-lib/hooks/useOfferte.d.ts +21 -0
  55. package/dist-lib/hooks/useOfferte.d.ts.map +1 -1
  56. package/dist-lib/hooks/useOfferte.js +105 -4
  57. package/dist-lib/hooks/useOffertes.d.ts.map +1 -1
  58. package/dist-lib/hooks/useOffertes.js +17 -7
  59. package/dist-lib/hooks/useWerkbonVoorstellen.d.ts +45 -0
  60. package/dist-lib/hooks/useWerkbonVoorstellen.d.ts.map +1 -0
  61. package/dist-lib/hooks/useWerkbonVoorstellen.js +74 -0
  62. package/dist-lib/index.d.ts +19 -2
  63. package/dist-lib/index.d.ts.map +1 -1
  64. package/dist-lib/index.js +26 -4
  65. package/dist-lib/integrations/supabase/auth-middleware.d.ts +4 -23403
  66. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  67. package/dist-lib/integrations/supabase/auth-middleware.js +1 -1
  68. package/dist-lib/integrations/supabase/client.d.ts +2 -23403
  69. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  70. package/dist-lib/integrations/supabase/client.server.d.ts +2 -23403
  71. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  72. package/dist-lib/integrations/supabase/previewAuthStorage.d.ts +1 -1
  73. package/dist-lib/integrations/supabase/previewAuthStorage.d.ts.map +1 -1
  74. package/dist-lib/integrations/supabase/previewAuthStorage.js +8 -1
  75. package/dist-lib/integrations/supabase/types.d.ts +23504 -11091
  76. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  77. package/dist-lib/integrations/supabase/types.js +2 -0
  78. package/dist-lib/lib/artikelenZoek.d.ts +17 -0
  79. package/dist-lib/lib/artikelenZoek.d.ts.map +1 -0
  80. package/dist-lib/lib/artikelenZoek.js +37 -0
  81. package/dist-lib/lib/offerteAcceptatie.functions.d.ts +7 -0
  82. package/dist-lib/lib/offerteAcceptatie.functions.d.ts.map +1 -0
  83. package/dist-lib/lib/offerteAcceptatie.functions.js +28 -0
  84. package/dist-lib/lib/offerteAdres.d.ts +7 -0
  85. package/dist-lib/lib/offerteAdres.d.ts.map +1 -0
  86. package/dist-lib/lib/offerteAdres.js +19 -0
  87. package/dist-lib/lib/offerteAi.functions.d.ts +34 -0
  88. package/dist-lib/lib/offerteAi.functions.d.ts.map +1 -0
  89. package/dist-lib/lib/offerteAi.functions.js +252 -0
  90. package/dist-lib/lib/offerteMail.d.ts +27 -0
  91. package/dist-lib/lib/offerteMail.d.ts.map +1 -0
  92. package/dist-lib/lib/offerteMail.js +141 -0
  93. package/dist-lib/lib/offerteRegels.d.ts +9 -0
  94. package/dist-lib/lib/offerteRegels.d.ts.map +1 -0
  95. package/dist-lib/lib/offerteRegels.js +20 -0
  96. package/dist-lib/lib/offerteRegels.test.d.ts +2 -0
  97. package/dist-lib/lib/offerteRegels.test.d.ts.map +1 -0
  98. package/dist-lib/lib/offerteRegels.test.js +17 -0
  99. package/dist-lib/lib/offerteReistijd.functions.d.ts +22 -0
  100. package/dist-lib/lib/offerteReistijd.functions.d.ts.map +1 -0
  101. package/dist-lib/lib/offerteReistijd.functions.js +173 -0
  102. package/dist-lib/lib/offertes.d.ts +8 -1
  103. package/dist-lib/lib/offertes.d.ts.map +1 -1
  104. package/dist-lib/lib/offertes.js +6 -4
  105. package/package.json +96 -5
package/README.md CHANGED
@@ -6,7 +6,6 @@ This project was built with [Lovable](https://lovable.dev).
6
6
 
7
7
  ## Build with Lovable
8
8
 
9
- Continue developing this project in the [Lovable editor](https://lovable.dev/projects/a26e8dec-7784-4e34-b97e-8cc0cd056ea5).
10
9
 
11
10
  - **Ship faster**: describe what you want to build and Lovable handles the code.
12
11
  - **Stay in sync**: every change made in Lovable is committed straight to this repository.
@@ -22,3 +21,19 @@ cd <repository-name>
22
21
  npm i
23
22
  npm run dev
24
23
  ```
24
+
25
+ ## Benodigde instellingen per omgeving
26
+
27
+ De module bevat geen vaste adressen, sleutels of klantnamen. Per shell/database nodig:
28
+
29
+ | Instelling | Waar | Gevolg als het ontbreekt |
30
+ | --- | --- | --- |
31
+ | `SUPABASE_URL`, `SUPABASE_PUBLISHABLE_KEY` | Shell (server) | AI- en reistijdfuncties geven een duidelijke fout |
32
+ | `SERVICE_ROLE_KEY` / service-role-binding | Shell (server) | Publieke offerte-acceptatie en reistijd werken niet |
33
+ | `LOVABLE_API_KEY` | Shell (server) | "AI is niet geconfigureerd" |
34
+ | `MAPS_CRON_URL` | Shell (server), adres van de eigen Google Maps-module | Reistijd toont "Kaarten-module niet ingesteld" |
35
+ | Mailbox-doel voor offertes | Instellingen → Offerte e-mail | Verzenden wordt geweigerd |
36
+ | Opslagmap `artikel-fotos` | Artikelen-module | Artikelen tonen een grijs vak |
37
+
38
+ Afhankelijke modules (zelfde database): Werkbonnen (`wo_create_from_source`), Planning (bedrijfsadres), Artikelen, Klanten (`itk_customers`), e-mail en Facturen-huisstijl.
39
+ De akkoordlink in mails gebruikt het domein van de shell zelf.
@@ -0,0 +1,16 @@
1
+ export interface GekozenArtikel {
2
+ article_number: string | null;
3
+ description: string;
4
+ unit: string;
5
+ unit_price_cents: number;
6
+ foto_url: string | null;
7
+ }
8
+ /** Vierkante foto; zonder foto of bij laadfout een effen grijs vak van dezelfde maat. */
9
+ export declare function ArtikelFoto({ url, className }: {
10
+ url: string | null | undefined;
11
+ className?: string;
12
+ }): import("react").JSX.Element;
13
+ export declare function ArtikelKiezer({ onKies }: {
14
+ onKies: (a: GekozenArtikel) => void;
15
+ }): import("react").JSX.Element;
16
+ //# sourceMappingURL=ArtikelKiezer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ArtikelKiezer.d.ts","sourceRoot":"","sources":["../../../src/components/offertes/ArtikelKiezer.tsx"],"names":[],"mappings":"AAMA,MAAM,WAAW,cAAc;IAC7B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,yFAAyF;AACzF,wBAAgB,WAAW,CAAC,EAAE,GAAG,EAAE,SAAc,EAAE,EAAE;IAAE,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,+BAK1G;AAED,wBAAgB,aAAa,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,CAAC,CAAC,EAAE,cAAc,KAAK,IAAI,CAAA;CAAE,+BAmFhF"}
@@ -0,0 +1,56 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { Plus } from "lucide-react";
4
+ import { Button, Input, Popover, PopoverContent, PopoverTrigger } from "@flowselections/core";
5
+ import { formatEuro } from "../../lib/offertes";
6
+ import { zoekArtikelenContract } from "../../lib/artikelenZoek";
7
+ /** Vierkante foto; zonder foto of bij laadfout een effen grijs vak van dezelfde maat. */
8
+ export function ArtikelFoto({ url, className = "" }) {
9
+ const [fout, setFout] = useState(false);
10
+ useEffect(() => setFout(false), [url]);
11
+ if (!url || fout)
12
+ return _jsx("div", { className: `aspect-square shrink-0 rounded-sm bg-muted ${className}`, "aria-hidden": true });
13
+ return _jsx("img", { src: url, alt: "", loading: "lazy", onError: () => setFout(true), className: `aspect-square shrink-0 rounded-sm object-cover ${className}` });
14
+ }
15
+ export function ArtikelKiezer({ onKies }) {
16
+ const [open, setOpen] = useState(false);
17
+ const [zoek, setZoek] = useState("");
18
+ const [artikelen, setArtikelen] = useState([]);
19
+ const [laden, setLaden] = useState(false);
20
+ useEffect(() => {
21
+ if (!open)
22
+ return;
23
+ let actief = true;
24
+ setLaden(true);
25
+ const t = setTimeout(async () => {
26
+ try {
27
+ const res = await zoekArtikelenContract(zoek, 50);
28
+ if (actief)
29
+ setArtikelen(Array.isArray(res) ? res : []);
30
+ }
31
+ catch {
32
+ if (actief)
33
+ setArtikelen([]);
34
+ }
35
+ finally {
36
+ if (actief)
37
+ setLaden(false);
38
+ }
39
+ }, 250);
40
+ return () => {
41
+ actief = false;
42
+ clearTimeout(t);
43
+ };
44
+ }, [open, zoek]);
45
+ const kies = (g) => {
46
+ onKies(g);
47
+ setOpen(false);
48
+ setZoek("");
49
+ };
50
+ return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", className: "w-full justify-between font-normal", children: ["Materiaal toevoegen", _jsx(Plus, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })] }) }), _jsxs(PopoverContent, { className: "min-w-72 max-w-[calc(100vw-1rem)] p-0 [--m-breedte:min(var(--radix-popover-trigger-width),26rem)]", style: { width: "var(--m-breedte)" }, align: "start", children: [_jsx("div", { className: "border-b p-2", children: _jsx(Input, { autoFocus: true, value: zoek, onChange: (e) => setZoek(e.target.value), placeholder: "Zoek op naam, code of EAN\u2026" }) }), _jsxs("div", { className: "overflow-y-auto overscroll-contain p-2 [--kaart:calc((var(--m-breedte)-1.5rem)/2+4.5rem)]", style: { maxHeight: "calc(var(--kaart) * 1.5 + 1.25rem)" },
51
+ // Het menu staat buiten het offertevenster; dat venster blokkeert anders wiel/veeg-scroll.
52
+ onWheel: (e) => e.stopPropagation(), onTouchMove: (e) => e.stopPropagation(), children: [laden && artikelen.length === 0 && _jsx("p", { className: "py-6 text-center text-sm text-muted-foreground", children: "Artikelen laden\u2026" }), !laden && artikelen.length === 0 && _jsx("p", { className: "py-6 text-center text-sm text-muted-foreground", children: "Geen materiaal gevonden." }), _jsx("div", { className: "grid grid-cols-2 gap-2", children: artikelen.map((a) => {
53
+ const p = Number(a.verkoop_prijs_cent) || 0;
54
+ return (_jsxs("button", { type: "button", className: "flex min-w-0 flex-col gap-1 rounded-md border border-border bg-card p-1.5 text-left text-sm transition-colors hover:bg-accent hover:text-accent-foreground", onClick: () => kies({ article_number: a.artikelcode, description: a.naam || a.artikelcode || "Materiaal", unit: a.eenheid || "st", unit_price_cents: p, foto_url: a.foto_url ?? null }), children: [_jsx(ArtikelFoto, { url: a.foto_url, className: "w-full" }), _jsx("span", { className: "line-clamp-2 font-medium leading-tight", children: a.naam || a.artikelcode }), _jsxs("span", { className: "flex justify-between gap-1 text-xs text-muted-foreground", children: [_jsx("span", { className: "truncate", children: a.gtin || "geen EAN" }), _jsx("span", { className: "shrink-0", children: p > 0 ? formatEuro(p) : "geen prijs" })] })] }, a.id));
55
+ }) })] }), _jsx("div", { className: "border-t p-1", children: _jsx("button", { type: "button", className: "w-full rounded-sm px-2 py-1.5 text-left text-sm hover:bg-accent", onClick: () => kies({ article_number: null, description: "", unit: "st", unit_price_cents: 0, foto_url: null }), children: "Eigen materiaal invoeren" }) })] })] }));
56
+ }
@@ -0,0 +1,4 @@
1
+ export declare function ExtraWerkMeldingenLijst({ onOfferteGemaakt }: {
2
+ onOfferteGemaakt?: () => void;
3
+ }): import("react").JSX.Element;
4
+ //# sourceMappingURL=ExtraWerkMeldingenLijst.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExtraWerkMeldingenLijst.d.ts","sourceRoot":"","sources":["../../../src/components/offertes/ExtraWerkMeldingenLijst.tsx"],"names":[],"mappings":"AAOA,wBAAgB,uBAAuB,CAAC,EAAE,gBAAgB,EAAE,EAAE;IAAE,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAA;CAAE,+BAmF9F"}
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRef, useState } from "react";
3
+ import { Badge, Card, CardContent, CardHeader, CardTitle, cn } from "@flowselections/core";
4
+ import { Wrench } from "lucide-react";
5
+ import { useExtraWerkMeldingen } from "../../hooks/useExtraWerkMeldingen";
6
+ import { ExtraWerkOffertePaneel } from "./ExtraWerkOffertePaneel";
7
+ import { formatDatum, formatEuro } from "../../lib/offertes";
8
+ export function ExtraWerkMeldingenLijst({ onOfferteGemaakt }) {
9
+ const { meldingen, loading, reload, logEvent } = useExtraWerkMeldingen();
10
+ const [actief, setActief] = useState(null);
11
+ const [navigatieLijst, setNavigatieLijst] = useState([]);
12
+ const actiefRef = useRef(null);
13
+ const open = Array.isArray(meldingen) ? meldingen : [];
14
+ const openen = async (m) => {
15
+ if (!actiefRef.current)
16
+ setNavigatieLijst(open);
17
+ actiefRef.current = m;
18
+ setActief(m);
19
+ if (!m.seen_at) {
20
+ await logEvent(m.offerte_id, "melding_bekeken", "Extra-werkmelding bekeken op kantoor");
21
+ reload();
22
+ }
23
+ };
24
+ const sluit = () => {
25
+ actiefRef.current = null;
26
+ setActief(null);
27
+ };
28
+ const basis = actief ? navigatieLijst : open;
29
+ const index = actief ? Math.max(basis.findIndex((m) => m.offerte_id === actief.offerte_id), 0) : 0;
30
+ return (_jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-2", children: _jsxs(CardTitle, { className: "flex items-center gap-2 text-base", children: [_jsx(Wrench, { className: "h-4 w-4 text-primary" }), " Extra werk gemeld door monteurs", _jsxs("span", { className: "text-sm font-normal text-muted-foreground", children: ["(", open.length, ")"] })] }) }), _jsxs(CardContent, { className: "space-y-1", children: [loading && _jsx("p", { className: "text-sm text-muted-foreground", children: "Laden\u2026" }), !loading && open.length === 0 && (_jsx("p", { className: "text-sm text-muted-foreground", children: "Geen openstaande meldingen." })), open.map((m) => (_jsxs("button", { type: "button", onClick: () => openen(m), className: cn("flex w-full items-center gap-3 rounded-md border px-3 py-2 text-left hover:bg-muted", !m.seen_at && "border-primary/40 bg-primary/5"), children: [!m.seen_at && _jsx(Badge, { className: "shrink-0", children: "Nieuw" }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate font-medium", children: m.omschrijving || "Extra werk" }), _jsxs("span", { className: "block truncate text-xs text-muted-foreground", children: [m.werkbon?.project_number ?? "—", " \u00B7 ", m.klant_naam ?? "Onbekende klant", " \u00B7 ", formatDatum(m.created_at)] })] }), _jsxs("span", { className: "text-xs text-muted-foreground", children: ["Geschat ", formatEuro(m.geschat_cents)] })] }, m.offerte_id)))] }), actief && (_jsx(ExtraWerkOffertePaneel, { melding: actief, index: index, aantal: basis.length, onNavigate: (i) => {
31
+ const doel = basis[i];
32
+ if (!doel)
33
+ return;
34
+ const vers = open.find((o) => o.offerte_id === doel.offerte_id) ?? doel;
35
+ openen(vers);
36
+ }, onClose: sluit, onSaveConcept: (c) => logEvent(actief.offerte_id, "melding_ai_concept", "AI-concept opgesteld", { concept: c }), onSaved: async () => {
37
+ await logEvent(actief.offerte_id, "melding_uitgewerkt", "Extra-werkmelding uitgewerkt tot offerte");
38
+ sluit();
39
+ await reload();
40
+ onOfferteGemaakt?.();
41
+ } }))] }));
42
+ }
@@ -0,0 +1,14 @@
1
+ import { type ExtraWerkMelding } from "../../hooks/useExtraWerkMeldingen";
2
+ import type { AiOfferteConcept } from "../../lib/offerteAi.functions";
3
+ interface Props {
4
+ melding: ExtraWerkMelding;
5
+ index: number;
6
+ aantal: number;
7
+ onNavigate: (index: number) => void;
8
+ onClose: () => void;
9
+ onSaved: () => void | Promise<void>;
10
+ onSaveConcept?: (c: AiOfferteConcept) => void;
11
+ }
12
+ export declare function ExtraWerkOffertePaneel({ melding, index, aantal, onNavigate, onClose, onSaved }: Props): import("react").JSX.Element;
13
+ export {};
14
+ //# sourceMappingURL=ExtraWerkOffertePaneel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExtraWerkOffertePaneel.d.ts","sourceRoot":"","sources":["../../../src/components/offertes/ExtraWerkOffertePaneel.tsx"],"names":[],"mappings":"AASA,OAAO,EAAyC,KAAK,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AACjH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAOtE,UAAU,KAAK;IACb,OAAO,EAAE,gBAAgB,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,aAAa,CAAC,EAAE,CAAC,CAAC,EAAE,gBAAgB,KAAK,IAAI,CAAC;CAC/C;AAqBD,wBAAgB,sBAAsB,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,KAAK,+BAqLrG"}
@@ -0,0 +1,91 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Groot paneel: links de extra-werkmelding + bronwerkbon, rechts het vragenformulier
3
+ // (zelfde als "Nieuwe offerte"), vooringevuld uit melding en bronwerkbon. Na
4
+ // Verzenden: controlevenster met A4 + mail, daarna wordt de bestaande
5
+ // conceptofferte bijgewerkt, genummerd en verstuurd.
6
+ import { useEffect, useMemo, useState } from "react";
7
+ import { useNavigate } from "@tanstack/react-router";
8
+ import { Button, Card, CardContent, CardHeader, CardTitle, supabase } from "@flowselections/core";
9
+ import { Group, Panel, Separator } from "react-resizable-panels";
10
+ import { ChevronLeft, ChevronRight, Send, X } from "lucide-react";
11
+ import { laadBronwerkbonRegels } from "../../hooks/useExtraWerkMeldingen";
12
+ import { formatDatum, formatEuro } from "../../lib/offertes";
13
+ import { NieuweOfferteFormulier } from "./NieuweOfferteFormulier";
14
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
+ const db = supabase;
16
+ function Veld({ label, value }) {
17
+ if (value === null || value === undefined || value === "")
18
+ return null;
19
+ return (_jsxs("div", { className: "space-y-0.5", children: [_jsx("p", { className: "text-xs text-muted-foreground", children: label }), _jsx("div", { className: "whitespace-pre-wrap text-sm", children: value })] }));
20
+ }
21
+ function alsPunten(...teksten) {
22
+ return teksten
23
+ .flatMap((t) => (typeof t === "string" ? t.split("\n") : []))
24
+ .map((l) => l.replace(/^[\s•\-*]+/, "").trim())
25
+ .filter(Boolean)
26
+ .map((l) => `• ${l}`)
27
+ .join("\n");
28
+ }
29
+ export function ExtraWerkOffertePaneel({ melding, index, aantal, onNavigate, onClose, onSaved }) {
30
+ const navigate = useNavigate();
31
+ const wb = melding.werkbon ?? {};
32
+ const [bronRegels, setBronRegels] = useState([]);
33
+ const [initial, setInitial] = useState(null);
34
+ const [naastElkaar, setNaastElkaar] = useState(false);
35
+ useEffect(() => {
36
+ const media = window.matchMedia("(min-width: 1024px)");
37
+ const update = () => setNaastElkaar(media.matches);
38
+ update();
39
+ media.addEventListener("change", update);
40
+ return () => media.removeEventListener("change", update);
41
+ }, []);
42
+ useEffect(() => {
43
+ let actief = true;
44
+ setInitial(null);
45
+ (async () => {
46
+ const [lijnen, pl, ml] = await Promise.all([
47
+ laadBronwerkbonRegels(wb.id),
48
+ wb.id
49
+ ? db.from("werkbon_materials").select("description, quantity, unit, unit_price_cents").eq("werkbon_id", wb.id)
50
+ : Promise.resolve({ data: [] }),
51
+ wb.id
52
+ ? db.from("werkbon_package_lines").select("package_id, quantity").eq("werkbon_id", wb.id)
53
+ : Promise.resolve({ data: [] }),
54
+ ]);
55
+ if (!actief)
56
+ return;
57
+ setBronRegels(Array.isArray(lijnen) ? lijnen : []);
58
+ const pakRows = Array.isArray(pl?.data) ? pl.data : [];
59
+ const packages = pakRows
60
+ .filter((r) => r.package_id)
61
+ .map((r) => ({ package_id: r.package_id, quantity: Number(r.quantity) || 1 }));
62
+ setInitial({
63
+ customerId: melding.customer_id ?? wb.customer_id ?? null,
64
+ werkadres: wb.work_address ?? "",
65
+ kamer: wb.room_number ?? "",
66
+ onderwerp: wb.technician_task_description?.split("\n")[0] ?? wb.complaint_description ?? "Extra werkzaamheden",
67
+ toelichting: alsPunten(melding.omschrijving, wb.complaint_description),
68
+ bijzonderheden: (wb.technician_notes ?? "").replace(/\s+/g, " ").trim(),
69
+ packages,
70
+ materials: (Array.isArray(ml?.data) ? ml.data : []).map((m) => ({
71
+ description: m.description ?? "",
72
+ unit: m.unit ?? "st",
73
+ quantity: Number(m.quantity) || 1,
74
+ unit_price_cents: Number(m.unit_price_cents) || 0,
75
+ })),
76
+ });
77
+ })();
78
+ return () => {
79
+ actief = false;
80
+ };
81
+ // eslint-disable-next-line react-hooks/exhaustive-deps
82
+ }, [melding.offerte_id]);
83
+ const mandaat = melding.mandaat_restant_cents ?? 0;
84
+ const werkadres = useMemo(() => [wb.work_address, wb.room_number].filter(Boolean).join(" · "), [wb.work_address, wb.room_number]);
85
+ return (_jsxs("div", { className: "fixed inset-y-0 left-0 right-0 z-50 flex flex-col bg-background shadow-2xl lg:left-[var(--flow-sidebar-width,16rem)]", children: [_jsxs("div", { className: "flex items-center justify-between border-b px-6 py-3", children: [_jsxs("div", { children: [_jsx("h2", { className: "text-lg font-semibold", children: "Offerte opstellen uit extra-werkmelding" }), _jsx("p", { className: "text-xs text-muted-foreground", children: [wb.project_number, melding.klant_naam].filter(Boolean).join(" · ") })] }), _jsxs("div", { className: "flex items-center gap-2", children: [aantal > 1 && (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Button, { variant: "outline", size: "icon", disabled: index <= 0, onClick: () => onNavigate(index - 1), "aria-label": "Vorige melding", children: _jsx(ChevronLeft, { className: "h-4 w-4" }) }), _jsxs("span", { className: "min-w-20 text-center text-sm text-muted-foreground", children: ["Melding ", index + 1, " van ", aantal] }), _jsx(Button, { variant: "outline", size: "icon", disabled: index >= aantal - 1, onClick: () => onNavigate(index + 1), "aria-label": "Volgende melding", children: _jsx(ChevronRight, { className: "h-4 w-4" }) })] })), _jsx(Button, { variant: "ghost", size: "icon", onClick: onClose, "aria-label": "Sluiten", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsxs(Group, { orientation: naastElkaar ? "horizontal" : "vertical", className: "min-h-0 flex-1 overflow-hidden", children: [_jsx(Panel, { id: "melding", defaultSize: "35%", minSize: naastElkaar ? "300px" : "25%", maxSize: naastElkaar ? "60%" : "50%", children: _jsxs("div", { className: "h-full min-w-0 space-y-4 overflow-y-auto border-r bg-muted/30 p-6", children: [_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Melding van de monteur" }) }), _jsxs(CardContent, { className: "space-y-3", children: [_jsx(Veld, { label: "Wat is er nog nodig?", value: melding.omschrijving }), _jsx(Veld, { label: "Geschatte kosten (monteur)", value: formatEuro(melding.geschat_cents) }), _jsx(Veld, { label: "Resterend mandaat", value: formatEuro(mandaat) }), _jsx(Veld, { label: "Gemeld op", value: formatDatum(melding.created_at) })] })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsxs(CardTitle, { className: "text-base", children: ["Bronwerkbon ", wb.project_number ?? ""] }) }), _jsxs(CardContent, { className: "space-y-3", children: [_jsx(Veld, { label: "Klant", value: melding.klant_naam }), _jsx(Veld, { label: "Werkadres", value: werkadres }), _jsx(Veld, { label: "Klacht", value: wb.complaint_description }), _jsx(Veld, { label: "Taak monteur", value: wb.technician_task_description }), _jsx(Veld, { label: "Monteurnotities", value: wb.technician_notes })] })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Pakketten en materialen op de werkbon" }) }), _jsxs(CardContent, { className: "space-y-2", children: [bronRegels.length === 0 && _jsx("p", { className: "text-sm text-muted-foreground", children: "Geen regels." }), bronRegels.map((r) => (_jsxs("div", { className: "flex justify-between gap-2 text-sm", children: [_jsxs("span", { children: [r.description, _jsx("span", { className: "block text-xs text-muted-foreground", children: r.soort })] }), _jsxs("span", { className: "text-muted-foreground", children: [r.quantity ?? "", " ", r.unit ?? ""] })] }, r.id)))] })] })] }) }), _jsx(Separator, { disabled: !naastElkaar, "aria-label": "Breedte van melding en offerte aanpassen", title: "Breedte van melding en offerte aanpassen", className: naastElkaar
86
+ ? "relative z-10 flex w-2 cursor-col-resize items-center justify-center bg-border outline-none hover:bg-primary/40 focus-visible:ring-2 focus-visible:ring-ring"
87
+ : "hidden", children: naastElkaar && _jsx("span", { "aria-hidden": "true", className: "pointer-events-none absolute flex h-8 w-10 select-none items-center justify-center rounded-sm border border-border bg-background text-sm font-medium text-foreground shadow-sm", children: "[<>]" }) }), _jsx(Panel, { id: "offerte", defaultSize: "65%", minSize: naastElkaar ? "480px" : "50%", children: _jsxs("div", { className: "flex h-full min-w-0 flex-col overflow-hidden border-l-2 border-primary/40", children: [_jsxs("div", { className: "flex shrink-0 items-center gap-2 border-b bg-primary/5 px-6 py-2.5", children: [_jsx(Send, { className: "h-4 w-4 text-primary" }), _jsx("p", { className: "text-sm font-medium text-primary", children: "Dit is de offerte die je verstuurt" })] }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-6", children: !initial ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Formulier vooraf invullen\u2026" })) : (_jsx(NieuweOfferteFormulier, { open: true, inline: true, initial: initial, bestaandeOfferteId: melding.offerte_id, bronWerkbonNummer: wb.project_number ?? null, meldingKort: melding.omschrijving, onCreated: async (id) => {
88
+ await onSaved();
89
+ navigate({ to: "/offertes/$offerteId", params: { offerteId: id } });
90
+ } })) })] }) })] })] }));
91
+ }
@@ -0,0 +1,5 @@
1
+ export declare function MoederWerkbonPaneel({ werkbonId, klantNaam }: {
2
+ werkbonId: string | null;
3
+ klantNaam: string;
4
+ }): import("react").JSX.Element;
5
+ //# sourceMappingURL=MoederWerkbonPaneel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MoederWerkbonPaneel.d.ts","sourceRoot":"","sources":["../../../src/components/offertes/MoederWerkbonPaneel.tsx"],"names":[],"mappings":"AAoCA,wBAAgB,mBAAmB,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,+BAmG5G"}
@@ -0,0 +1,66 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { Card, CardContent, CardHeader, CardTitle, supabase } from "@flowselections/core";
4
+ import { FileText } from "lucide-react";
5
+ const db = supabase;
6
+ const euro = (cents) => new Intl.NumberFormat("nl-NL", { style: "currency", currency: "EUR" }).format((cents || 0) / 100);
7
+ function Veld({ label, waarde }) {
8
+ return (_jsxs("div", { className: "space-y-0.5", children: [_jsx("p", { className: "text-xs text-muted-foreground", children: label }), _jsx("p", { className: "whitespace-pre-wrap text-sm", children: waarde && waarde.trim() ? waarde : "—" })] }));
9
+ }
10
+ export function MoederWerkbonPaneel({ werkbonId, klantNaam }) {
11
+ const [werkbon, setWerkbon] = useState(null);
12
+ const [regels, setRegels] = useState([]);
13
+ const [laden, setLaden] = useState(true);
14
+ useEffect(() => {
15
+ let actief = true;
16
+ const load = async () => {
17
+ setLaden(true);
18
+ if (!werkbonId) {
19
+ setWerkbon(null);
20
+ setRegels([]);
21
+ setLaden(false);
22
+ return;
23
+ }
24
+ const [wb, pk, mat] = await Promise.all([
25
+ db.from("werkbonnen")
26
+ .select("project_number,status,work_address,room_number,externe_referentie,complaint_description,technician_task_description,budget_mandate_cents")
27
+ .eq("id", werkbonId)
28
+ .maybeSingle(),
29
+ db.from("werkbon_package_lines").select("package_id,quantity,unit_price_cents_snapshot").eq("werkbon_id", werkbonId),
30
+ db.from("werkbon_materials").select("description,quantity,unit,unit_price_cents").eq("werkbon_id", werkbonId),
31
+ ]);
32
+ const pakketRegels = Array.isArray(pk.data) ? pk.data : [];
33
+ const ids = pakketRegels.map((r) => r.package_id).filter(Boolean);
34
+ let namen = {};
35
+ if (ids.length) {
36
+ const { data } = await db.from("werkbon_packages").select("id,name").in("id", ids);
37
+ const lijst = Array.isArray(data) ? data : [];
38
+ namen = Object.fromEntries(lijst.map((p) => [p.id, p.name ?? ""]));
39
+ }
40
+ const materialen = Array.isArray(mat.data) ? mat.data : [];
41
+ if (!actief)
42
+ return;
43
+ setWerkbon(wb.data ?? null);
44
+ setRegels([
45
+ ...pakketRegels.map((r) => ({
46
+ omschrijving: namen[r.package_id] ?? "Pakket",
47
+ eenheid: "pakket",
48
+ aantal: Number(r.quantity ?? 0),
49
+ prijs_cents: Number(r.unit_price_cents_snapshot ?? 0),
50
+ })),
51
+ ...materialen.map((m) => ({
52
+ omschrijving: m.description ?? "",
53
+ eenheid: m.unit ?? "st",
54
+ aantal: Number(m.quantity ?? 0),
55
+ prijs_cents: Number(m.unit_price_cents ?? 0),
56
+ })),
57
+ ]);
58
+ setLaden(false);
59
+ };
60
+ void load();
61
+ return () => {
62
+ actief = false;
63
+ };
64
+ }, [werkbonId]);
65
+ return (_jsxs(Card, { className: "h-fit min-w-0", children: [_jsxs(CardHeader, { className: "border-b", children: [_jsxs(CardTitle, { className: "flex items-center gap-2 text-lg", children: [_jsx(FileText, { className: "h-5 w-5" }), " Moederwerkbon"] }), _jsx("p", { className: "text-sm text-muted-foreground", children: werkbon?.project_number ?? "—" })] }), _jsx(CardContent, { className: "space-y-3 pt-5", children: laden ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Laden\u2026" })) : !werkbon ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Geen moederwerkbon gevonden." })) : (_jsxs(_Fragment, { children: [_jsx(Veld, { label: "Klant", waarde: klantNaam }), _jsx(Veld, { label: "Status", waarde: werkbon.status }), _jsx(Veld, { label: "Werkadres", waarde: werkbon.work_address }), _jsx(Veld, { label: "Kamer / ruimte", waarde: werkbon.room_number }), _jsx(Veld, { label: "Referentie", waarde: werkbon.externe_referentie }), _jsx(Veld, { label: "Klacht / aanleiding", waarde: werkbon.complaint_description }), _jsx(Veld, { label: "Opdracht voor monteur", waarde: werkbon.technician_task_description }), _jsx(Veld, { label: "Mandaat", waarde: euro(Number(werkbon.budget_mandate_cents ?? 0)) }), _jsxs("div", { className: "space-y-2 border-t pt-3", children: [_jsx("p", { className: "text-sm font-semibold", children: "Werkbonregels" }), regels.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "\u2014" })) : (regels.map((r, i) => (_jsxs("div", { className: "rounded-md border p-2 text-sm", children: [_jsx("p", { className: "whitespace-pre-wrap", children: r.omschrijving || "—" }), _jsxs("p", { className: "text-xs text-muted-foreground", children: [r.aantal, " ", r.eenheid, " \u00D7 ", euro(r.prijs_cents)] })] }, i))))] })] })) })] }));
66
+ }
@@ -0,0 +1,33 @@
1
+ export interface OfferteFormulierInitieel {
2
+ customerId?: string | null;
3
+ werkadres?: string | null;
4
+ kamer?: string | null;
5
+ onderwerp?: string | null;
6
+ toelichting?: string | null;
7
+ bijzonderheden?: string | null;
8
+ packages?: {
9
+ package_id: string;
10
+ quantity: number;
11
+ }[];
12
+ materials?: {
13
+ description: string;
14
+ unit: string;
15
+ quantity: number;
16
+ unit_price_cents: number;
17
+ }[];
18
+ }
19
+ interface Props {
20
+ open: boolean;
21
+ onOpenChange?: (open: boolean) => void;
22
+ onCreated: (offerteId: string) => void;
23
+ /** Toon het formulier zonder eigen pop-up (bijv. in het extra-werkpaneel). */
24
+ inline?: boolean;
25
+ initial?: OfferteFormulierInitieel;
26
+ /** Bestaande conceptofferte bijwerken in plaats van een nieuwe aanmaken. */
27
+ bestaandeOfferteId?: string;
28
+ bronWerkbonNummer?: string | null;
29
+ meldingKort?: string | null;
30
+ }
31
+ export declare function NieuweOfferteFormulier({ open, onOpenChange: onOpenChangeProp, onCreated, inline, initial, bestaandeOfferteId, bronWerkbonNummer, meldingKort }: Props): import("react").JSX.Element;
32
+ export {};
33
+ //# sourceMappingURL=NieuweOfferteFormulier.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NieuweOfferteFormulier.d.ts","sourceRoot":"","sources":["../../../src/components/offertes/NieuweOfferteFormulier.tsx"],"names":[],"mappings":"AAmCA,MAAM,WAAW,wBAAwB;IACvC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACtD,SAAS,CAAC,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACjG;AAED,UAAU,KAAK;IACb,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACvC,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,8EAA8E;IAC9E,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,wBAAwB,CAAC;IACnC,4EAA4E;IAC5E,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAsED,wBAAgB,sBAAsB,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,WAAW,EAAE,EAAE,KAAK,+BA8tBrK"}