@dxv-systems/helpdesk 0.1.0 → 0.3.0

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/dist/react.js CHANGED
@@ -6,42 +6,86 @@ exports.HelpdeskInbox = HelpdeskInbox;
6
6
  const jsx_runtime_1 = require("react/jsx-runtime");
7
7
  const react_1 = require("react");
8
8
  const messages_1 = require("./inbox/messages");
9
+ const FOCUS = "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background";
10
+ const AVATAR = "flex h-9 w-9 flex-none items-center justify-center rounded-full text-sm font-semibold";
11
+ const BADGE = "whitespace-nowrap rounded-full px-2 py-0.5 text-xs font-semibold";
12
+ const BUBBLE = "flex min-w-0 flex-1 flex-col gap-2.5 rounded-xl border px-4 py-3.5";
9
13
  exports.DEFAULT_HELPDESK_INBOX_CLASSNAMES = {
10
- root: "flex w-full flex-col gap-4 text-foreground",
14
+ root: "flex w-full flex-col gap-4 text-sm leading-normal text-foreground",
11
15
  layout: "flex w-full flex-col gap-6 md:flex-row md:items-start",
12
- listPane: "flex min-w-0 flex-col gap-3 md:w-96 md:flex-none",
16
+ listPane: "flex min-w-0 flex-col gap-4 md:w-80 md:flex-none lg:w-[340px]",
13
17
  detailPane: "flex min-w-0 flex-1 flex-col gap-4",
14
18
  hiddenOnMobile: "max-md:hidden",
15
- title: "text-2xl font-bold leading-tight",
16
- filters: "grid grid-cols-1 gap-3 sm:grid-cols-3 md:grid-cols-1",
17
- field: "flex flex-col gap-1.5",
18
- label: "text-sm font-semibold",
19
- select: "h-11 w-full min-w-0 rounded-md border border-input bg-background px-3 text-base focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
20
- list: "flex flex-col gap-2",
21
- listItem: "flex min-h-11 w-full flex-col gap-1 rounded-md border border-border bg-card px-4 py-3 text-left text-card-foreground hover:bg-accent",
22
- listItemCurrent: "flex min-h-11 w-full flex-col gap-1 rounded-md border border-primary bg-accent px-4 py-3 text-left text-card-foreground",
23
- listItemTitle: "font-semibold break-words",
24
- listItemMeta: "flex flex-wrap items-center gap-2 text-sm text-muted-foreground",
25
- card: "flex flex-col gap-4 rounded-lg border border-border bg-card p-4 text-card-foreground sm:p-6",
26
- heading: "text-xl font-bold break-words",
19
+ title: "text-2xl font-bold leading-tight tracking-tight",
20
+ filters: "grid grid-cols-2 gap-2",
21
+ field: "flex min-w-0 flex-col gap-1",
22
+ fieldWide: "col-span-2 flex min-w-0 flex-col gap-1",
23
+ label: "text-xs font-medium text-muted-foreground",
24
+ selectWrap: "relative",
25
+ select: `h-10 w-full min-w-0 appearance-none rounded-md border border-input bg-background pl-3 pr-9 text-sm text-foreground ${FOCUS}`,
26
+ selectIcon: "pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground",
27
+ list: "flex flex-col gap-1.5",
28
+ listItem: `flex w-full flex-col gap-1 rounded-lg border border-border bg-card px-3.5 py-3 text-left text-card-foreground hover:bg-accent ${FOCUS}`,
29
+ listItemCurrent: `flex w-full flex-col gap-1 rounded-lg border border-ring bg-accent px-3.5 py-3 text-left text-card-foreground ${FOCUS}`,
30
+ listItemTop: "flex items-baseline justify-between gap-2",
31
+ listItemTitle: "min-w-0 font-semibold break-words",
32
+ listItemTime: "whitespace-nowrap text-xs text-muted-foreground",
33
+ listItemMeta: "flex flex-wrap items-center gap-x-2 gap-y-1 text-muted-foreground",
34
+ card: "flex flex-col gap-3 rounded-xl border border-border bg-card p-4 text-card-foreground",
35
+ header: "flex flex-col gap-3 border-b border-border pb-4",
36
+ headerTop: "flex flex-wrap items-start justify-between gap-3",
37
+ headerActions: "relative flex flex-wrap items-center gap-2",
38
+ heading: "text-2xl font-bold leading-tight tracking-tight break-words",
27
39
  subheading: "text-base font-semibold",
28
40
  text: "",
29
41
  muted: "text-sm text-muted-foreground",
30
- meta: "flex flex-col gap-1 text-sm text-muted-foreground break-words",
31
- badges: "flex flex-wrap items-center gap-2",
32
- badge: "whitespace-nowrap rounded-full bg-muted px-2.5 py-0.5 text-sm font-semibold",
33
- badgeAttention: "whitespace-nowrap rounded-full bg-amber-50 px-2.5 py-0.5 text-sm font-semibold text-amber-900 dark:bg-amber-950 dark:text-amber-100",
34
- badgeDone: "whitespace-nowrap rounded-full bg-emerald-50 px-2.5 py-0.5 text-sm font-semibold text-emerald-800 dark:bg-emerald-950 dark:text-emerald-200",
35
- messages: "flex flex-col gap-3",
36
- message: "flex flex-col gap-2 rounded-md border border-border p-4",
37
- messageHeader: "flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1 text-sm",
38
- messageBody: "whitespace-pre-wrap break-words text-sm leading-relaxed",
39
- textarea: "min-h-48 w-full rounded-md border border-input bg-background p-3 text-base leading-relaxed focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring read-only:bg-muted",
40
- actions: "flex flex-wrap items-center gap-3",
41
- primaryButton: "inline-flex min-h-11 items-center justify-center rounded-md bg-primary px-4 font-semibold text-primary-foreground hover:bg-primary/90 disabled:opacity-50",
42
- secondaryButton: "inline-flex min-h-11 items-center justify-center rounded-md border border-border bg-background px-4 font-semibold hover:bg-accent disabled:opacity-50",
43
- dangerButton: "inline-flex min-h-11 items-center justify-center rounded-md border border-destructive bg-background px-4 font-semibold text-destructive hover:bg-destructive/10 disabled:opacity-50",
44
- linkButton: "inline-flex min-h-11 items-center font-semibold text-primary underline-offset-4 hover:underline",
42
+ meta: "flex flex-wrap items-center gap-2 text-sm text-muted-foreground break-words",
43
+ badge: `${BADGE} bg-muted text-foreground`,
44
+ badgeAttention: `${BADGE} bg-amber-50 text-amber-900 dark:bg-amber-950 dark:text-amber-100`,
45
+ badgeDone: `${BADGE} bg-emerald-50 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-200`,
46
+ badgeError: `${BADGE} bg-destructive/10 text-destructive`,
47
+ reasonNote: "text-xs text-muted-foreground",
48
+ toolButton: `inline-flex h-9 items-center gap-2 rounded-md border border-border bg-card px-3 text-sm font-medium hover:bg-accent disabled:opacity-50 ${FOCUS}`,
49
+ assignedChip: "inline-flex h-9 items-center gap-2 rounded-md border border-border px-3 text-sm font-medium text-muted-foreground",
50
+ iconButton: `inline-flex h-9 w-9 items-center justify-center rounded-md border border-border bg-card hover:bg-accent disabled:opacity-50 ${FOCUS}`,
51
+ menu: "absolute right-0 top-full z-20 mt-1 flex min-w-56 flex-col rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md",
52
+ menuItem: "flex min-h-10 w-full items-center rounded-sm px-3 text-left text-sm hover:bg-accent focus-visible:bg-accent focus-visible:outline-none",
53
+ backLink: "inline-flex min-h-10 items-center self-start font-semibold text-primary underline-offset-4 hover:underline md:hidden",
54
+ conversation: "flex flex-col gap-3",
55
+ entry: "flex items-start gap-3",
56
+ entryOurs: "flex flex-row-reverse items-start gap-3",
57
+ avatar: `${AVATAR} bg-secondary text-secondary-foreground`,
58
+ avatarOurs: `${AVATAR} bg-primary text-primary-foreground`,
59
+ avatarEvent: `${AVATAR} bg-destructive/10 text-destructive`,
60
+ message: `${BUBBLE} border-border bg-card text-card-foreground`,
61
+ messageOurs: `${BUBBLE} border-primary/20 bg-primary/5 text-foreground sm:max-w-[85%]`,
62
+ messageEvent: `${BUBBLE} border-destructive/40 bg-destructive/5 text-foreground`,
63
+ messageHeader: "flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1",
64
+ messageBody: "whitespace-pre-wrap break-words leading-relaxed",
65
+ emptyBody: "italic text-muted-foreground",
66
+ eventTitle: "font-semibold text-destructive",
67
+ details: "text-sm text-muted-foreground [&>summary]:cursor-pointer",
68
+ detailsPre: "mt-2 whitespace-pre-wrap break-words rounded-md bg-background px-3 py-2.5 font-mono text-xs text-foreground",
69
+ fileChips: "flex flex-wrap gap-2",
70
+ fileChip: "inline-flex max-w-full items-center gap-2.5 rounded-md border border-border bg-muted/50 px-3 py-2 text-foreground no-underline hover:bg-accent",
71
+ fileChipName: "font-medium break-all",
72
+ fileChipMeta: "text-xs text-muted-foreground",
73
+ sentNote: "inline-flex items-center gap-1.5 text-xs text-muted-foreground",
74
+ composer: "flex flex-col gap-3 rounded-xl border border-ring/40 bg-card p-4 text-card-foreground",
75
+ composerHeader: "flex flex-wrap items-center justify-between gap-2",
76
+ aiChip: "inline-flex items-center gap-1.5 rounded-full bg-accent px-2.5 py-0.5 text-xs text-accent-foreground",
77
+ composerFooter: "flex flex-wrap items-center justify-between gap-3 border-t border-border pt-3",
78
+ replyChip: "inline-flex max-w-full items-center gap-2 rounded-md border border-border bg-muted/50 px-2.5 py-1.5",
79
+ chipRemove: `inline-flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-50 ${FOCUS}`,
80
+ dashedButton: "inline-flex h-8 cursor-pointer items-center gap-1.5 rounded-md border border-dashed border-border px-2.5 hover:bg-accent focus-within:ring-2 focus-within:ring-ring",
81
+ srOnly: "sr-only",
82
+ icon: "h-4 w-4 flex-none",
83
+ textarea: `min-h-48 w-full resize-y rounded-md border border-input bg-background px-3.5 py-3 text-base leading-relaxed ${FOCUS} read-only:bg-muted`,
84
+ actions: "flex flex-wrap items-center gap-2",
85
+ primaryButton: `inline-flex min-h-10 items-center justify-center gap-2 rounded-md bg-primary px-4 font-semibold text-primary-foreground hover:bg-primary/90 disabled:opacity-50 ${FOCUS}`,
86
+ secondaryButton: `inline-flex min-h-10 items-center justify-center gap-2 rounded-md border border-border bg-background px-3.5 font-medium hover:bg-accent disabled:opacity-50 ${FOCUS}`,
87
+ dangerButton: `inline-flex min-h-10 items-center justify-center rounded-md border border-destructive bg-background px-3.5 font-semibold text-destructive hover:bg-destructive/10 disabled:opacity-50 ${FOCUS}`,
88
+ linkButton: "inline-flex min-h-10 items-center font-semibold text-primary underline-offset-4 hover:underline",
45
89
  banner: "rounded-md border border-amber-300 bg-amber-50 px-4 py-3 text-sm font-semibold text-amber-900 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-100",
46
90
  success: "rounded-md bg-emerald-50 px-4 py-3 text-sm font-semibold text-emerald-800 dark:bg-emerald-950 dark:text-emerald-200",
47
91
  warning: "rounded-md bg-amber-50 px-4 py-3 text-sm text-amber-900 dark:bg-amber-950 dark:text-amber-100",
@@ -92,14 +136,62 @@ const ASSIGNEE_OPTIONS = [
92
136
  { value: "me", label: "Aan mij" },
93
137
  { value: "none", label: "Niet toegewezen" },
94
138
  ];
95
- const dateTime = new Intl.DateTimeFormat("nl-NL", { dateStyle: "medium", timeStyle: "short" });
96
- const formatDate = (iso) => dateTime.format(new Date(iso));
97
- function stateBadgeClass(cls, state) {
98
- if (state === "review")
99
- return cls.badgeAttention;
100
- if (state === "sent" || state === "waiting" || state === "closed")
101
- return cls.badgeDone;
102
- return cls.badge;
139
+ const timeOfDay = new Intl.DateTimeFormat("nl-NL", { hour: "2-digit", minute: "2-digit" });
140
+ const dayMonth = new Intl.DateTimeFormat("nl-NL", { day: "numeric", month: "short" });
141
+ const dayMonthTime = new Intl.DateTimeFormat("nl-NL", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" });
142
+ const formatDate = (iso) => dayMonthTime.format(new Date(iso));
143
+ function listTime(iso) {
144
+ const at = new Date(iso);
145
+ return at.toDateString() === new Date().toDateString() ? timeOfDay.format(at) : dayMonth.format(at);
146
+ }
147
+ function statusBadge(cls, state, reason) {
148
+ if (state === "review" && reason === "delivery_failed") {
149
+ return { label: messages_1.REVIEW_REASON_LABELS.delivery_failed, className: cls.badgeError, note: null };
150
+ }
151
+ if (state === "review") {
152
+ return { label: messages_1.STATE_LABELS.review, className: cls.badgeAttention, note: reason ? messages_1.REVIEW_REASON_LABELS[reason] : null };
153
+ }
154
+ const done = state === "sent" || state === "waiting" || state === "closed";
155
+ return { label: messages_1.STATE_LABELS[state], className: done ? cls.badgeDone : cls.badge, note: null };
156
+ }
157
+ function StatusBadge({ cls, state, reason, withNote }) {
158
+ const b = statusBadge(cls, state, reason);
159
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: b.className, children: b.label }), withNote && b.note && (0, jsx_runtime_1.jsx)("span", { className: cls.reasonNote, children: b.note })] }));
160
+ }
161
+ function initials(address) {
162
+ const local = address.split("@")[0] ?? "";
163
+ const parts = local.split(/[._+-]+/).filter((p) => /[a-z]/i.test(p));
164
+ const letters = parts.length > 1 ? `${parts[0][0]}${parts[1][0]}` : (parts[0] ?? local).replace(/[^a-z]/gi, "").slice(0, 2);
165
+ return (letters || "?").toUpperCase();
166
+ }
167
+ function formatSize(bytes) {
168
+ if (bytes < 1024)
169
+ return `${bytes} B`;
170
+ if (bytes < 1024 * 1024)
171
+ return `${Math.round(bytes / 1024)} kB`;
172
+ return `${(bytes / (1024 * 1024)).toFixed(1).replace(".", ",")} MB`;
173
+ }
174
+ function fileType(name) {
175
+ const ext = /\.([a-z0-9]{1,5})$/i.exec(name)?.[1];
176
+ return ext ? ext.toUpperCase() : "Bestand";
177
+ }
178
+ const ICON_PATHS = {
179
+ chevron: ["M6 9l6 6 6-6"],
180
+ user: ["M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8z", "M4 21c0-4 4-6 8-6s8 2 8 6"],
181
+ more: ["M5 12h.01", "M12 12h.01", "M19 12h.01"],
182
+ file: ["M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z", "M14 3v5h5"],
183
+ paperclip: ["M21 12.5l-8.5 8.5a5 5 0 0 1-7-7L14 5.5a3.5 3.5 0 0 1 5 5L10.5 19"],
184
+ close: ["M6 6l12 12", "M18 6L6 18"],
185
+ send: ["M22 2L11 13", "M22 2l-7 20-4-9-9-4z"],
186
+ check: ["M20 6L9 17l-5-5"],
187
+ warning: ["M12 9v4", "M12 17h.01", "M10.3 3.9L1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"],
188
+ sparkle: ["M12 3l1.8 4.7L18 9.5l-4.2 1.8L12 16l-1.8-4.7L6 9.5l4.2-1.8z"],
189
+ };
190
+ function Icon({ name, className }) {
191
+ return ((0, jsx_runtime_1.jsx)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: name === "more" ? 3 : 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", className: className, children: ICON_PATHS[name].map((d) => ((0, jsx_runtime_1.jsx)("path", { d: d }, d))) }));
192
+ }
193
+ function Select({ cls, id, label, value, onChange, wide = false, children, }) {
194
+ return ((0, jsx_runtime_1.jsxs)("div", { className: wide ? cls.fieldWide : cls.field, children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: id, className: cls.label, children: label }), (0, jsx_runtime_1.jsxs)("div", { className: cls.selectWrap, children: [(0, jsx_runtime_1.jsx)("select", { id: id, className: cls.select, value: value, onChange: (e) => onChange(e.target.value), children: children }), (0, jsx_runtime_1.jsx)(Icon, { name: "chevron", className: cls.selectIcon })] })] }));
103
195
  }
104
196
  const syncSince = new Intl.DateTimeFormat("nl-NL", { weekday: "long", hour: "2-digit", minute: "2-digit" });
105
197
  function SyncIssueBanner({ cls, issues }) {
@@ -107,7 +199,7 @@ function SyncIssueBanner({ cls, issues }) {
107
199
  return ((0, jsx_runtime_1.jsxs)("div", { role: "status", className: cls.warning, children: ["Microsoft 365 is niet bereikbaar sinds ", syncSince.format(new Date(since)), issues.length > 1 ? "" : ` (${issues[0].mailbox})`, ". Nieuwe mail komt binnen zodra het weer werkt; er gaat niets verloren."] }));
108
200
  }
109
201
  function PausedBanner({ cls }) {
110
- return ((0, jsx_runtime_1.jsx)("div", { role: "status", className: cls.banner, "data-helpdesk-paused": "", children: "De helpdesk staat op pauze. Er wordt geen mail opgehaald, geen concept geschreven en niets verstuurd. Je kunt tickets wel bekijken." }));
202
+ return ((0, jsx_runtime_1.jsx)("div", { role: "status", className: cls.banner, "data-helpdesk-paused": "", children: "De helpdesk staat op pauze. Er wordt geen mail opgehaald, geen voorstel geschreven en niets verstuurd. Je kunt tickets wel bekijken." }));
111
203
  }
112
204
  function HelpdeskInbox({ apiBasePath = messages_1.HELPDESK_BASE_PATH, classNames, pollIntervalMs = 30000, fetch: fetchImpl, renderCustomerContext, initialThreadId, onThreadChange, }) {
113
205
  const cls = (0, react_1.useMemo)(() => ({ ...exports.DEFAULT_HELPDESK_INBOX_CLASSNAMES, ...classNames }), [classNames]);
@@ -194,10 +286,11 @@ function HelpdeskInbox({ apiBasePath = messages_1.HELPDESK_BASE_PATH, classNames
194
286
  }, []);
195
287
  const paused = detailPaused ?? list?.pilot.paused ?? false;
196
288
  const mailboxes = list?.pilot.mailboxes ?? [];
197
- return ((0, jsx_runtime_1.jsxs)("div", { className: cls.root, "data-helpdesk-inbox": "", children: [paused && (0, jsx_runtime_1.jsx)(PausedBanner, { cls: cls }), (list?.syncIssues ?? []).length > 0 && (0, jsx_runtime_1.jsx)(SyncIssueBanner, { cls: cls, issues: list.syncIssues }), (0, jsx_runtime_1.jsxs)("div", { className: cls.layout, children: [(0, jsx_runtime_1.jsxs)("section", { "aria-labelledby": `${id}-list`, className: `${cls.listPane} ${selected ? cls.hiddenOnMobile : ""}`.trim(), children: [(0, jsx_runtime_1.jsx)("h1", { id: `${id}-list`, className: cls.title, children: "Helpdesk" }), (0, jsx_runtime_1.jsxs)("div", { className: cls.filters, children: [(0, jsx_runtime_1.jsxs)("div", { className: cls.field, children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: `${id}-view`, className: cls.label, children: "Tickets" }), (0, jsx_runtime_1.jsx)("select", { id: `${id}-view`, className: cls.select, value: view, onChange: (e) => setView(e.target.value), children: VIEWS.map((v) => ((0, jsx_runtime_1.jsx)("option", { value: v.value, children: v.label }, v.value))) })] }), (0, jsx_runtime_1.jsxs)("div", { className: cls.field, children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: `${id}-assignee`, className: cls.label, children: "Toegewezen aan" }), (0, jsx_runtime_1.jsx)("select", { id: `${id}-assignee`, className: cls.select, value: assignee, onChange: (e) => setAssignee(e.target.value), children: ASSIGNEE_OPTIONS.map((o) => ((0, jsx_runtime_1.jsx)("option", { value: o.value, children: o.label }, o.value))) })] }), mailboxes.length > 1 && ((0, jsx_runtime_1.jsxs)("div", { className: cls.field, children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: `${id}-mailbox`, className: cls.label, children: "Mailbox" }), (0, jsx_runtime_1.jsxs)("select", { id: `${id}-mailbox`, className: cls.select, value: mailbox, onChange: (e) => setMailbox(e.target.value), children: [(0, jsx_runtime_1.jsx)("option", { value: "", children: "Alle mailboxen" }), mailboxes.map((m) => ((0, jsx_runtime_1.jsx)("option", { value: m.address, children: m.address }, m.address)))] })] }))] }), listError && ((0, jsx_runtime_1.jsx)("p", { role: "alert", className: cls.error, children: listError })), !list && !listError && ((0, jsx_runtime_1.jsx)("p", { className: cls.muted, "aria-live": "polite", children: "Laden\u2026" })), list && list.threads.length === 0 && (0, jsx_runtime_1.jsx)("p", { className: cls.muted, children: "Geen tickets in deze weergave." }), list && list.threads.length > 0 && ((0, jsx_runtime_1.jsx)("ul", { "aria-label": "Tickets", className: cls.list, children: list.threads.map((t) => ((0, jsx_runtime_1.jsx)(ThreadListItem, { cls: cls, thread: t, current: t.id === selected, showMailbox: mailboxes.length > 1, onSelect: select }, t.id))) })), list?.nextCursor && ((0, jsx_runtime_1.jsx)("div", { className: cls.actions, children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.secondaryButton, disabled: loadingMore, onClick: () => void loadMore(), children: loadingMore ? "Laden…" : "Meer laden" }) }))] }), (0, jsx_runtime_1.jsx)("div", { className: `${cls.detailPane} ${selected ? "" : cls.hiddenOnMobile}`.trim(), children: selected ? ((0, jsx_runtime_1.jsx)(ThreadView, { cls: cls, api: api, fetchImpl: fetchImpl, threadId: selected, pollIntervalMs: interval, renderCustomerContext: renderCustomerContext, onBack: () => select(null), onUpdated: onThreadUpdated }, selected)) : ((0, jsx_runtime_1.jsx)("p", { className: cls.muted, children: "Kies een ticket om het te bekijken." })) })] })] }));
289
+ const viewCount = list ? ` (${list.threads.length}${list.nextCursor ? "+" : ""})` : "";
290
+ return ((0, jsx_runtime_1.jsxs)("div", { className: cls.root, "data-helpdesk-inbox": "", children: [paused && (0, jsx_runtime_1.jsx)(PausedBanner, { cls: cls }), (list?.syncIssues ?? []).length > 0 && (0, jsx_runtime_1.jsx)(SyncIssueBanner, { cls: cls, issues: list.syncIssues }), (0, jsx_runtime_1.jsxs)("div", { className: cls.layout, children: [(0, jsx_runtime_1.jsxs)("section", { "aria-labelledby": `${id}-list`, className: `${cls.listPane} ${selected ? cls.hiddenOnMobile : ""}`.trim(), children: [(0, jsx_runtime_1.jsx)("h1", { id: `${id}-list`, className: cls.title, children: "Helpdesk" }), (0, jsx_runtime_1.jsxs)("div", { className: cls.filters, children: [(0, jsx_runtime_1.jsx)(Select, { cls: cls, id: `${id}-view`, label: "Status", value: view, onChange: setView, children: VIEWS.map((v) => ((0, jsx_runtime_1.jsxs)("option", { value: v.value, children: [v.label, v.value === view ? viewCount : ""] }, v.value))) }), (0, jsx_runtime_1.jsx)(Select, { cls: cls, id: `${id}-assignee`, label: "Toegewezen", value: assignee, onChange: setAssignee, children: ASSIGNEE_OPTIONS.map((o) => ((0, jsx_runtime_1.jsx)("option", { value: o.value, children: o.label }, o.value))) }), mailboxes.length > 1 && ((0, jsx_runtime_1.jsxs)(Select, { cls: cls, id: `${id}-mailbox`, label: "Mailbox", value: mailbox, onChange: setMailbox, wide: true, children: [(0, jsx_runtime_1.jsx)("option", { value: "", children: "Alle mailboxen" }), mailboxes.map((m) => ((0, jsx_runtime_1.jsx)("option", { value: m.address, children: m.address }, m.address)))] }))] }), listError && ((0, jsx_runtime_1.jsx)("p", { role: "alert", className: cls.error, children: listError })), !list && !listError && ((0, jsx_runtime_1.jsx)("p", { className: cls.muted, "aria-live": "polite", children: "Laden\u2026" })), list && list.threads.length === 0 && (0, jsx_runtime_1.jsx)("p", { className: cls.muted, children: "Geen tickets in deze weergave." }), list && list.threads.length > 0 && ((0, jsx_runtime_1.jsx)("ul", { "aria-label": "Tickets", className: cls.list, children: list.threads.map((t) => ((0, jsx_runtime_1.jsx)(ThreadListItem, { cls: cls, thread: t, current: t.id === selected, showMailbox: mailboxes.length > 1, onSelect: select }, t.id))) })), list?.nextCursor && ((0, jsx_runtime_1.jsx)("div", { className: cls.actions, children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.secondaryButton, disabled: loadingMore, onClick: () => void loadMore(), children: loadingMore ? "Laden…" : "Meer laden" }) }))] }), (0, jsx_runtime_1.jsx)("div", { className: `${cls.detailPane} ${selected ? "" : cls.hiddenOnMobile}`.trim(), children: selected ? ((0, jsx_runtime_1.jsx)(ThreadView, { cls: cls, api: api, fetchImpl: fetchImpl, threadId: selected, pollIntervalMs: interval, renderCustomerContext: renderCustomerContext, onBack: () => select(null), onUpdated: onThreadUpdated }, selected)) : ((0, jsx_runtime_1.jsx)("p", { className: cls.muted, children: "Kies een ticket om het te bekijken." })) })] })] }));
198
291
  }
199
292
  function ThreadListItem({ cls, thread, current, showMailbox, onSelect, }) {
200
- return ((0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: current ? cls.listItemCurrent : cls.listItem, "aria-current": current ? "true" : undefined, onClick: () => onSelect(thread.id), children: [(0, jsx_runtime_1.jsx)("span", { className: cls.listItemTitle, children: thread.subject || "(geen onderwerp)" }), (0, jsx_runtime_1.jsxs)("span", { className: cls.listItemMeta, children: [(0, jsx_runtime_1.jsx)("span", { children: thread.customerAddress }), (0, jsx_runtime_1.jsx)("span", { "aria-hidden": "true", children: "\u00B7" }), (0, jsx_runtime_1.jsx)("span", { children: formatDate(thread.lastActivityAt) })] }), (0, jsx_runtime_1.jsxs)("span", { className: cls.listItemMeta, children: [(0, jsx_runtime_1.jsx)("span", { className: stateBadgeClass(cls, thread.state), children: messages_1.STATE_LABELS[thread.state] }), thread.state === "review" && thread.reviewReason && (0, jsx_runtime_1.jsx)("span", { children: messages_1.REVIEW_REASON_LABELS[thread.reviewReason] }), thread.assignee && (0, jsx_runtime_1.jsxs)("span", { children: ["Bij ", thread.assignee.name] }), showMailbox && (0, jsx_runtime_1.jsx)("span", { children: thread.mailbox })] })] }) }));
293
+ return ((0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: current ? cls.listItemCurrent : cls.listItem, "aria-current": current ? "true" : undefined, onClick: () => onSelect(thread.id), children: [(0, jsx_runtime_1.jsxs)("span", { className: cls.listItemTop, children: [(0, jsx_runtime_1.jsx)("span", { className: cls.listItemTitle, children: thread.subject || "(geen onderwerp)" }), (0, jsx_runtime_1.jsx)("time", { dateTime: thread.lastActivityAt, className: cls.listItemTime, children: listTime(thread.lastActivityAt) })] }), (0, jsx_runtime_1.jsx)("span", { className: cls.listItemMeta, children: thread.customerAddress }), (0, jsx_runtime_1.jsxs)("span", { className: cls.listItemMeta, children: [(0, jsx_runtime_1.jsx)(StatusBadge, { cls: cls, state: thread.state, reason: thread.reviewReason, withNote: true }), thread.assignee && (0, jsx_runtime_1.jsxs)("span", { className: cls.reasonNote, children: ["Bij ", thread.assignee.name] }), showMailbox && (0, jsx_runtime_1.jsx)("span", { className: cls.reasonNote, children: thread.mailbox })] })] }) }));
201
294
  }
202
295
  function ThreadView({ cls, api, fetchImpl, threadId, pollIntervalMs, renderCustomerContext, onBack, onUpdated }) {
203
296
  const id = (0, react_1.useId)();
@@ -208,6 +301,7 @@ function ThreadView({ cls, api, fetchImpl, threadId, pollIntervalMs, renderCusto
208
301
  const [busy, setBusy] = (0, react_1.useState)(false);
209
302
  const [text, setText] = (0, react_1.useState)("");
210
303
  const [dirty, setDirty] = (0, react_1.useState)(false);
304
+ const [confirming, setConfirming] = (0, react_1.useState)(null);
211
305
  const bodies = (0, react_1.useRef)(new Map());
212
306
  const [, setBodiesVersion] = (0, react_1.useState)(0);
213
307
  const apply = (0, react_1.useCallback)((next, keepText) => {
@@ -256,7 +350,7 @@ function ThreadView({ cls, api, fetchImpl, threadId, pollIntervalMs, renderCusto
256
350
  continue;
257
351
  try {
258
352
  const { body } = await api(`threads/${encodeURIComponent(threadId)}/messages/${encodeURIComponent(messageId)}`);
259
- bodies.current.set(messageId, { text: body.text, attachments: body.attachments ?? [] });
353
+ bodies.current.set(messageId, { text: body.text, attachments: body.attachments ?? [], bounce: body.bounce ?? null });
260
354
  }
261
355
  catch (error) {
262
356
  bodies.current.set(messageId, { error: errorText(error) });
@@ -316,43 +410,112 @@ function ThreadView({ cls, api, fetchImpl, threadId, pollIntervalMs, renderCusto
316
410
  if (!detail) {
317
411
  return ((0, jsx_runtime_1.jsxs)("div", { className: cls.card, children: [(0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.linkButton, onClick: onBack, children: "Terug naar tickets" }) }), loadError ? ((0, jsx_runtime_1.jsx)("p", { role: "alert", className: cls.error, children: loadError })) : ((0, jsx_runtime_1.jsx)("p", { className: cls.muted, "aria-live": "polite", children: "Ticket laden\u2026" }))] }));
318
412
  }
319
- const { thread, pilot, actions } = detail;
320
- const can = (action) => actions.includes(action);
321
- const isMine = thread.assignee?.id === detail.viewer.id;
322
- return ((0, jsx_runtime_1.jsxs)("article", { "aria-labelledby": `${id}-subject`, className: cls.detailPane, "data-thread-state": thread.state, "data-write-mode": pilot.writeMode, children: [(0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.linkButton, onClick: onBack, children: "Terug naar tickets" }) }), (0, jsx_runtime_1.jsxs)("section", { className: cls.card, children: [(0, jsx_runtime_1.jsx)("h2", { id: `${id}-subject`, ref: headingRef, tabIndex: -1, className: cls.heading, children: thread.subject || "(geen onderwerp)" }), (0, jsx_runtime_1.jsxs)("div", { className: cls.badges, children: [(0, jsx_runtime_1.jsx)("span", { className: stateBadgeClass(cls, thread.state), children: messages_1.STATE_LABELS[thread.state] }), thread.state === "review" && thread.reviewReason && ((0, jsx_runtime_1.jsx)("span", { className: cls.badge, children: messages_1.REVIEW_REASON_LABELS[thread.reviewReason] })), (0, jsx_runtime_1.jsx)("span", { className: cls.badge, children: messages_1.WRITE_MODE_LABELS[pilot.writeMode] })] }), (0, jsx_runtime_1.jsxs)("div", { className: cls.meta, children: [(0, jsx_runtime_1.jsxs)("span", { children: ["Klant: ", thread.customerAddress] }), (0, jsx_runtime_1.jsxs)("span", { children: ["Mailbox: ", thread.mailbox] }), (0, jsx_runtime_1.jsxs)("span", { children: ["Laatste activiteit: ", formatDate(thread.lastActivityAt)] }), thread.relatedThreadId && (0, jsx_runtime_1.jsx)("span", { children: "Vervolg op een eerder ticket" }), thread.state === "ignored" && thread.dismissedBy && ((0, jsx_runtime_1.jsxs)("span", { children: ["Geen antwoord nodig, gemarkeerd door ", thread.dismissedBy === detail.viewer.id ? "jou" : "een collega"] }))] }), (0, jsx_runtime_1.jsxs)("div", { className: cls.actions, children: [(0, jsx_runtime_1.jsx)("span", { className: cls.muted, children: thread.assignee ? `Toegewezen aan ${isMine ? "jou" : thread.assignee.name}` : "Niet toegewezen" }), !isMine && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.secondaryButton, disabled: busy, onClick: () => run("assign"), children: "Aan mij toewijzen" })), thread.assignee && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.secondaryButton, disabled: busy, onClick: () => run("assign", { method: "DELETE" }), children: "Toewijzing weghalen" }))] })] }), (0, jsx_runtime_1.jsx)("div", { "aria-live": "polite", role: "status", children: notice && notice.kind !== "error" && (0, jsx_runtime_1.jsx)("p", { className: notice.kind === "success" ? cls.success : cls.warning, children: notice.text }) }), notice?.kind === "error" && ((0, jsx_runtime_1.jsx)("p", { role: "alert", className: cls.error, children: notice.text })), renderCustomerContext && (0, jsx_runtime_1.jsx)("section", { className: cls.context, children: renderCustomerContext(thread) }), (0, jsx_runtime_1.jsxs)("section", { "aria-labelledby": `${id}-messages`, className: cls.card, children: [(0, jsx_runtime_1.jsx)("h3", { id: `${id}-messages`, className: cls.subheading, children: "Berichten" }), (0, jsx_runtime_1.jsx)("ol", { className: cls.messages, children: detail.messages.map((m) => ((0, jsx_runtime_1.jsx)(MessageItem, { cls: cls, message: m, body: bodies.current.get(m.id) }, m.id))) })] }), (0, jsx_runtime_1.jsx)(DraftSection, { cls: cls, detail: detail, busy: busy, text: text, dirty: dirty, onText: (value) => {
413
+ const { thread, pilot, draft, send } = detail;
414
+ const can = (action) => detail.actions.includes(action);
415
+ const bounced = thread.state === "review" && thread.reviewReason === "delivery_failed";
416
+ const sentReply = send?.status === "sent" && draft?.text && !draft.stale ? { text: draft.text, at: send.at } : null;
417
+ const syncedCopy = send?.messageId ? detail.messages.some((m) => m.id === send.messageId && !m.removed) : false;
418
+ const showComposer = !bounced && !sentReply && (draft !== null || thread.state === "review");
419
+ const menuItems = [];
420
+ if (!bounced && can("dismiss"))
421
+ menuItems.push({ key: "dismiss", label: "Geen antwoord nodig", onSelect: () => setConfirming("dismiss") });
422
+ if (!bounced && can("replied_externally")) {
423
+ menuItems.push({ key: "external", label: "Al beantwoord in Outlook", onSelect: () => setConfirming("external") });
424
+ }
425
+ if (thread.assignee)
426
+ menuItems.push({ key: "unassign", label: "Toewijzing weghalen", onSelect: () => run("assign", { method: "DELETE" }) });
427
+ return ((0, jsx_runtime_1.jsxs)("article", { "aria-labelledby": `${id}-subject`, className: cls.detailPane, "data-thread-state": thread.state, "data-write-mode": pilot.writeMode, children: [(0, jsx_runtime_1.jsxs)("header", { className: cls.header, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.backLink, onClick: onBack, children: "Terug naar tickets" }), (0, jsx_runtime_1.jsxs)("div", { className: cls.headerTop, children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-col gap-1.5", children: [(0, jsx_runtime_1.jsx)("h2", { id: `${id}-subject`, ref: headingRef, tabIndex: -1, className: cls.heading, children: thread.subject || "(geen onderwerp)" }), (0, jsx_runtime_1.jsxs)("div", { className: cls.meta, children: [(0, jsx_runtime_1.jsx)(StatusBadge, { cls: cls, state: thread.state, reason: thread.reviewReason, withNote: false }), pilot.writeMode === "read_only" && (0, jsx_runtime_1.jsx)("span", { className: cls.badge, children: "Alleen lezen" }), (0, jsx_runtime_1.jsx)("span", { children: thread.customerAddress }), (0, jsx_runtime_1.jsx)("span", { "aria-hidden": "true", children: "\u00B7" }), (0, jsx_runtime_1.jsxs)("span", { children: ["naar ", thread.mailbox] })] }), (thread.relatedThreadId || (thread.state === "ignored" && thread.dismissedBy)) && ((0, jsx_runtime_1.jsxs)("div", { className: cls.meta, children: [thread.relatedThreadId && (0, jsx_runtime_1.jsx)("span", { children: "Vervolg op een eerder ticket" }), thread.state === "ignored" && thread.dismissedBy && ((0, jsx_runtime_1.jsxs)("span", { children: ["Geen antwoord nodig, gemarkeerd door ", thread.dismissedBy === detail.viewer.id ? "jou" : "een collega"] }))] }))] }), (0, jsx_runtime_1.jsxs)("div", { className: cls.headerActions, children: [(0, jsx_runtime_1.jsx)(Assignment, { cls: cls, detail: detail, busy: busy, run: run }), menuItems.length > 0 && (0, jsx_runtime_1.jsx)(MoreActions, { cls: cls, busy: busy, items: menuItems })] })] }), confirming && ((0, jsx_runtime_1.jsx)(ConfirmAction, { cls: cls, kind: confirming, busy: busy, onCancel: () => setConfirming(null), onConfirm: () => {
428
+ setConfirming(null);
429
+ run(confirming === "dismiss" ? "dismiss" : "replied-externally");
430
+ } }))] }), (0, jsx_runtime_1.jsx)("div", { "aria-live": "polite", role: "status", children: notice && notice.kind !== "error" && (0, jsx_runtime_1.jsx)("p", { className: notice.kind === "success" ? cls.success : cls.warning, children: notice.text }) }), notice?.kind === "error" && ((0, jsx_runtime_1.jsx)("p", { role: "alert", className: cls.error, children: notice.text })), renderCustomerContext && (0, jsx_runtime_1.jsx)("section", { className: cls.context, children: renderCustomerContext(thread) }), (0, jsx_runtime_1.jsx)(Conversation, { cls: cls, detail: detail, bodies: bodies.current, sentReply: syncedCopy ? null : sentReply }), bounced && (can("replied_externally") || can("dismiss")) && (0, jsx_runtime_1.jsx)(BouncedNextSteps, { cls: cls, busy: busy, can: can, run: run }), showComposer && ((0, jsx_runtime_1.jsx)(Composer, { cls: cls, detail: detail, busy: busy, text: text, dirty: dirty, onText: (value) => {
323
431
  setText(value);
324
432
  setDirty(true);
325
- }, act: act, run: run, can: can, uploadFile: uploadFile }), (0, jsx_runtime_1.jsx)(OtherActions, { cls: cls, busy: busy, can: can, run: run, draftInOutlook: pilot.writeMode === "drafts_in_outlook" && Boolean(detail.draft?.inOutlook) })] }));
433
+ }, act: act, run: run, can: can, uploadFile: uploadFile })), showComposer && thread.state === "review" && (can("dismiss") || can("replied_externally")) && ((0, jsx_runtime_1.jsxs)("p", { className: cls.muted, children: ["Geen antwoord nodig of al vanuit Outlook beantwoord? Kies het bij ", (0, jsx_runtime_1.jsx)("strong", { className: "font-medium text-foreground", children: "Meer acties" }), " ", "bovenaan."] })), (0, jsx_runtime_1.jsx)(OtherActions, { cls: cls, busy: busy, can: can, run: run })] }));
326
434
  }
327
- function formatSize(bytes) {
328
- if (bytes < 1024)
329
- return `${bytes} B`;
330
- if (bytes < 1024 * 1024)
331
- return `${Math.round(bytes / 1024)} kB`;
332
- return `${(bytes / (1024 * 1024)).toFixed(1).replace(".", ",")} MB`;
435
+ function Assignment({ cls, detail, busy, run }) {
436
+ const owner = detail.thread.assignee;
437
+ if (owner && owner.id === detail.viewer.id) {
438
+ return ((0, jsx_runtime_1.jsxs)("span", { className: cls.assignedChip, children: [(0, jsx_runtime_1.jsx)(Icon, { name: "user", className: cls.icon }), "Toegewezen aan jou"] }));
439
+ }
440
+ return ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: cls.toolButton, disabled: busy, onClick: () => run("assign"), children: [(0, jsx_runtime_1.jsx)(Icon, { name: "user", className: cls.icon }), owner ? `Toegewezen aan ${owner.name} · Aan mij` : "Niet toegewezen · Aan mij"] }));
441
+ }
442
+ function MoreActions({ cls, busy, items }) {
443
+ const id = (0, react_1.useId)();
444
+ const [open, setOpen] = (0, react_1.useState)(false);
445
+ const wrapRef = (0, react_1.useRef)(null);
446
+ const toggleRef = (0, react_1.useRef)(null);
447
+ const firstRef = (0, react_1.useRef)(null);
448
+ (0, react_1.useEffect)(() => {
449
+ if (!open)
450
+ return;
451
+ firstRef.current?.focus();
452
+ const onPointer = (e) => {
453
+ if (wrapRef.current && !wrapRef.current.contains(e.target))
454
+ setOpen(false);
455
+ };
456
+ document.addEventListener("mousedown", onPointer);
457
+ return () => document.removeEventListener("mousedown", onPointer);
458
+ }, [open]);
459
+ return ((0, jsx_runtime_1.jsxs)("div", { ref: wrapRef, className: "relative", onKeyDown: (e) => {
460
+ if (e.key === "Escape" && open) {
461
+ setOpen(false);
462
+ toggleRef.current?.focus();
463
+ }
464
+ }, children: [(0, jsx_runtime_1.jsx)("button", { ref: toggleRef, type: "button", className: cls.iconButton, "aria-label": "Meer acties", "aria-expanded": open, "aria-controls": `${id}-menu`, disabled: busy, onClick: () => setOpen((o) => !o), children: (0, jsx_runtime_1.jsx)(Icon, { name: "more", className: cls.icon }) }), open && ((0, jsx_runtime_1.jsx)("div", { id: `${id}-menu`, className: cls.menu, children: items.map((item, i) => ((0, jsx_runtime_1.jsx)("button", { ref: i === 0 ? firstRef : undefined, type: "button", className: cls.menuItem, onClick: () => {
465
+ setOpen(false);
466
+ item.onSelect();
467
+ }, children: item.label }, item.key))) }))] }));
468
+ }
469
+ function ConfirmAction({ cls, kind, busy, onConfirm, onCancel, }) {
470
+ const external = kind === "external";
471
+ return ((0, jsx_runtime_1.jsxs)("div", { role: "group", "aria-label": external ? "Beantwoord in Outlook bevestigen" : "Geen antwoord nodig bevestigen", className: cls.actions, children: [(0, jsx_runtime_1.jsx)("span", { className: cls.text, children: external
472
+ ? "Al beantwoord in Outlook? Het antwoord in de helpdesk wordt dan niet verstuurd."
473
+ : "Geen antwoord nodig? Er wordt niets verstuurd en het ticket gaat naar Genegeerd." }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.dangerButton, disabled: busy, onClick: onConfirm, children: external ? "Ja, al beantwoord" : "Ja, geen antwoord nodig" }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.secondaryButton, onClick: onCancel, autoFocus: true, children: "Annuleren" })] }));
333
474
  }
334
475
  const UNAVAILABLE_TEXT = {
335
476
  item: "bijgevoegde mail, open deze in Outlook",
336
477
  reference: "link naar een gedeeld bestand, open deze in Outlook",
337
478
  too_large: "te groot om hier te openen, open de mail in Outlook",
338
479
  };
339
- function AttachmentList({ cls, attachments }) {
480
+ function FileChips({ cls, attachments }) {
340
481
  if (attachments.length === 0)
341
482
  return null;
342
- return ((0, jsx_runtime_1.jsxs)("div", { className: cls.field, children: [(0, jsx_runtime_1.jsx)("span", { className: cls.label, children: attachments.length === 1 ? "Bijlage" : `Bijlagen (${attachments.length})` }), (0, jsx_runtime_1.jsx)("ul", { className: cls.list, children: attachments.map((a) => ((0, jsx_runtime_1.jsxs)("li", { children: [a.href ? ((0, jsx_runtime_1.jsx)("a", { href: a.href, download: a.name, className: cls.linkButton, children: a.name })) : ((0, jsx_runtime_1.jsx)("span", { children: a.name })), " ", (0, jsx_runtime_1.jsxs)("span", { className: cls.muted, children: ["(", formatSize(a.size), a.unavailable ? `, ${UNAVAILABLE_TEXT[a.unavailable]}` : "", ")"] })] }, a.id))) })] }));
483
+ return ((0, jsx_runtime_1.jsx)("ul", { "aria-label": attachments.length === 1 ? "Bijlage" : `Bijlagen (${attachments.length})`, className: cls.fileChips, children: attachments.map((a) => {
484
+ const type = a.kind === "item" ? "Mail" : a.kind === "reference" ? "Link" : fileType(a.name);
485
+ const content = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(Icon, { name: "file", className: cls.icon }), (0, jsx_runtime_1.jsxs)("span", { className: "flex min-w-0 flex-col leading-snug", children: [(0, jsx_runtime_1.jsx)("span", { className: cls.fileChipName, children: a.name }), (0, jsx_runtime_1.jsxs)("span", { className: cls.fileChipMeta, children: [type, " \u00B7 ", formatSize(a.size), a.unavailable ? `, ${UNAVAILABLE_TEXT[a.unavailable]}` : ""] })] })] }));
486
+ return ((0, jsx_runtime_1.jsx)("li", { className: "max-w-full", children: a.href ? ((0, jsx_runtime_1.jsx)("a", { href: a.href, download: a.name, className: cls.fileChip, children: content })) : ((0, jsx_runtime_1.jsx)("span", { className: cls.fileChip, children: content })) }, a.id));
487
+ }) }));
343
488
  }
344
- function MessageItem({ cls, message, body, }) {
345
- return ((0, jsx_runtime_1.jsxs)("li", { className: cls.message, children: [(0, jsx_runtime_1.jsxs)("div", { className: cls.messageHeader, children: [(0, jsx_runtime_1.jsx)("span", { className: cls.subheading, children: message.fromAddress }), (0, jsx_runtime_1.jsx)("span", { className: cls.muted, children: formatDate(message.receivedAt) })] }), (message.noise || message.removed) && ((0, jsx_runtime_1.jsxs)("div", { className: cls.badges, children: [message.noise && (0, jsx_runtime_1.jsx)("span", { className: cls.badge, children: "Automatisch bericht" }), message.removed && (0, jsx_runtime_1.jsx)("span", { className: cls.badge, children: "Niet meer in de Inbox" })] })), body === undefined && (0, jsx_runtime_1.jsx)("p", { className: cls.muted, children: "Bericht laden\u2026" }), body && "error" in body && (0, jsx_runtime_1.jsx)("p", { className: cls.muted, children: body.error }), body && "text" in body && (0, jsx_runtime_1.jsx)("div", { className: cls.messageBody, children: body.text || "(leeg bericht)" }), body && "text" in body && (0, jsx_runtime_1.jsx)(AttachmentList, { cls: cls, attachments: body.attachments })] }));
489
+ function Conversation({ cls, detail, bodies, sentReply, }) {
490
+ const copyId = sentReply ? detail.send?.messageId : null;
491
+ const entries = detail.messages.filter((m) => !(copyId && m.id === copyId)).map((m) => ({
492
+ kind: m.outbound ? "sent" : m.bounce ? "bounce" : "mail",
493
+ at: m.receivedAt,
494
+ message: m,
495
+ }));
496
+ if (sentReply)
497
+ entries.push({ kind: "reply", at: sentReply.at, text: sentReply.text });
498
+ entries.sort((a, b) => a.at.localeCompare(b.at));
499
+ const sentFiles = (detail.replyAttachments ?? []).filter((a) => a.sent);
500
+ return ((0, jsx_runtime_1.jsx)("ol", { "aria-label": "Gesprek", className: cls.conversation, children: entries.map((e) => e.kind === "reply" ? ((0, jsx_runtime_1.jsxs)("li", { className: cls.entryOurs, children: [(0, jsx_runtime_1.jsx)("span", { className: cls.avatarOurs, children: (0, jsx_runtime_1.jsx)(Icon, { name: "send", className: cls.icon }) }), (0, jsx_runtime_1.jsxs)("article", { className: cls.messageOurs, children: [(0, jsx_runtime_1.jsxs)("div", { className: cls.messageHeader, children: [(0, jsx_runtime_1.jsxs)("span", { children: [(0, jsx_runtime_1.jsx)("strong", { className: "font-semibold", children: "Jouw antwoord" }), " ", (0, jsx_runtime_1.jsxs)("span", { className: "text-muted-foreground", children: ["\u00B7 vanaf ", detail.thread.mailbox] })] }), (0, jsx_runtime_1.jsx)("time", { dateTime: e.at, className: cls.muted, children: formatDate(e.at) })] }), (0, jsx_runtime_1.jsx)("div", { className: cls.messageBody, children: e.text }), sentFiles.length > 0 && ((0, jsx_runtime_1.jsx)("ul", { "aria-label": "Verstuurde bijlagen", className: cls.fileChips, children: sentFiles.map((a) => ((0, jsx_runtime_1.jsxs)("li", { className: cls.replyChip, children: [(0, jsx_runtime_1.jsx)("span", { className: "break-all", children: a.name }), " ", (0, jsx_runtime_1.jsx)("span", { className: cls.fileChipMeta, children: formatSize(a.size) })] }, a.id))) })), (0, jsx_runtime_1.jsxs)("span", { className: cls.sentNote, children: [(0, jsx_runtime_1.jsx)(Icon, { name: "check", className: cls.icon }), "Verstuurd, staat in Verzonden items"] })] })] }, "reply")) : e.kind === "bounce" ? ((0, jsx_runtime_1.jsx)(BounceItem, { cls: cls, message: e.message, body: bodies.get(e.message.id) }, e.message.id)) : e.kind === "sent" ? ((0, jsx_runtime_1.jsx)(SentItem, { cls: cls, message: e.message, mailbox: detail.thread.mailbox, body: bodies.get(e.message.id) }, e.message.id)) : ((0, jsx_runtime_1.jsx)(MessageItem, { cls: cls, message: e.message, body: bodies.get(e.message.id) }, e.message.id))) }));
346
501
  }
347
- function modeExplanation(mode, mailbox) {
348
- switch (mode) {
349
- case "read_only":
350
- return "Deze mailbox staat op alleen lezen. Je kunt het concept lezen en aanpassen, maar er gaat niets naar Outlook en er wordt niets verstuurd.";
351
- case "drafts_in_app":
352
- return `Dit concept staat alleen in de app. Het gaat pas naar Outlook als je het goedkeurt, en wordt dan meteen verstuurd vanaf ${mailbox}.`;
353
- case "drafts_in_outlook":
354
- return `Dit concept staat ook in Outlook bij Concepten van ${mailbox}. Versturen gebeurt pas als je het goedkeurt.`;
355
- }
502
+ function MessageItem({ cls, message, body }) {
503
+ return ((0, jsx_runtime_1.jsxs)("li", { className: cls.entry, children: [(0, jsx_runtime_1.jsx)("span", { className: cls.avatar, "aria-hidden": "true", children: initials(message.fromAddress) }), (0, jsx_runtime_1.jsxs)("article", { className: cls.message, children: [(0, jsx_runtime_1.jsxs)("div", { className: cls.messageHeader, children: [(0, jsx_runtime_1.jsxs)("span", { className: "min-w-0 break-words", children: [(0, jsx_runtime_1.jsx)("strong", { className: "font-semibold", children: message.fromAddress }), " ", (0, jsx_runtime_1.jsx)("span", { className: "text-muted-foreground", children: "\u00B7 klant" }), message.noise && (0, jsx_runtime_1.jsx)("span", { className: "text-muted-foreground", children: " \u00B7 automatisch bericht" }), message.removed && (0, jsx_runtime_1.jsx)("span", { className: "text-muted-foreground", children: " \u00B7 niet meer in de Inbox" })] }), (0, jsx_runtime_1.jsx)("time", { dateTime: message.receivedAt, className: cls.muted, children: formatDate(message.receivedAt) })] }), body === undefined && (0, jsx_runtime_1.jsx)("p", { className: cls.muted, children: "Bericht laden\u2026" }), body && "error" in body && (0, jsx_runtime_1.jsx)("p", { className: cls.muted, children: body.error }), body && "text" in body &&
504
+ (body.text ? ((0, jsx_runtime_1.jsx)("div", { className: cls.messageBody, children: body.text })) : ((0, jsx_runtime_1.jsx)("p", { className: cls.emptyBody, children: body.attachments.length > 0 ? "Geen tekst, alleen een bijlage." : "Geen tekst." }))), body && "text" in body && (0, jsx_runtime_1.jsx)(FileChips, { cls: cls, attachments: body.attachments })] })] }));
505
+ }
506
+ function SentItem({ cls, message, mailbox, body }) {
507
+ return ((0, jsx_runtime_1.jsxs)("li", { className: cls.entryOurs, "data-outbound": "", children: [(0, jsx_runtime_1.jsx)("span", { className: cls.avatarOurs, children: (0, jsx_runtime_1.jsx)(Icon, { name: "send", className: cls.icon }) }), (0, jsx_runtime_1.jsxs)("article", { className: cls.messageOurs, children: [(0, jsx_runtime_1.jsxs)("div", { className: cls.messageHeader, children: [(0, jsx_runtime_1.jsxs)("span", { className: "min-w-0 break-words", children: [(0, jsx_runtime_1.jsx)("strong", { className: "font-semibold", children: "Jouw antwoord" }), " ", (0, jsx_runtime_1.jsxs)("span", { className: "text-muted-foreground", children: ["\u00B7 vanaf ", message.fromAddress || mailbox] }), message.removed && (0, jsx_runtime_1.jsx)("span", { className: "text-muted-foreground", children: " \u00B7 niet meer in Verzonden items" })] }), (0, jsx_runtime_1.jsx)("time", { dateTime: message.receivedAt, className: cls.muted, children: formatDate(message.receivedAt) })] }), body === undefined && (0, jsx_runtime_1.jsx)("p", { className: cls.muted, children: "Bericht laden\u2026" }), body && "error" in body && (0, jsx_runtime_1.jsx)("p", { className: cls.muted, children: body.error }), body && "text" in body &&
508
+ (body.text ? ((0, jsx_runtime_1.jsx)("div", { className: cls.messageBody, children: body.text })) : ((0, jsx_runtime_1.jsx)("p", { className: cls.emptyBody, children: body.attachments.length > 0 ? "Geen tekst, alleen een bijlage." : "Geen tekst." }))), body && "text" in body && (0, jsx_runtime_1.jsx)(FileChips, { cls: cls, attachments: body.attachments }), !message.removed && ((0, jsx_runtime_1.jsxs)("span", { className: cls.sentNote, children: [(0, jsx_runtime_1.jsx)(Icon, { name: "check", className: cls.icon }), "Verstuurd, staat in Verzonden items"] }))] })] }));
509
+ }
510
+ function BounceItem({ cls, message, body }) {
511
+ const report = body && "text" in body ? body.bounce : null;
512
+ const reason = report?.reason ?? "De ontvangende server weigerde het bericht.";
513
+ const diagnostic = report ? report.diagnostic : body && "text" in body ? body.text : "";
514
+ return ((0, jsx_runtime_1.jsxs)("li", { className: cls.entry, "data-bounce": "", children: [(0, jsx_runtime_1.jsx)("span", { className: cls.avatarEvent, children: (0, jsx_runtime_1.jsx)(Icon, { name: "warning", className: cls.icon }) }), (0, jsx_runtime_1.jsxs)("div", { className: cls.messageEvent, children: [(0, jsx_runtime_1.jsxs)("div", { className: cls.messageHeader, children: [(0, jsx_runtime_1.jsx)("strong", { className: cls.eventTitle, children: "Jouw antwoord is niet afgeleverd" }), (0, jsx_runtime_1.jsx)("time", { dateTime: message.receivedAt, className: cls.muted, children: formatDate(message.receivedAt) })] }), body === undefined && (0, jsx_runtime_1.jsx)("p", { className: cls.muted, children: "Bericht laden\u2026" }), body && "error" in body && (0, jsx_runtime_1.jsx)("p", { className: cls.muted, children: body.error }), body && "text" in body && (0, jsx_runtime_1.jsx)("p", { children: reason }), diagnostic && ((0, jsx_runtime_1.jsxs)("details", { className: cls.details, children: [(0, jsx_runtime_1.jsx)("summary", { children: "Technische details van Microsoft" }), (0, jsx_runtime_1.jsx)("pre", { className: cls.detailsPre, children: diagnostic })] }))] })] }));
515
+ }
516
+ function BouncedNextSteps({ cls, busy, can, run }) {
517
+ const id = (0, react_1.useId)();
518
+ return ((0, jsx_runtime_1.jsxs)("section", { "aria-labelledby": `${id}-h`, className: cls.card, children: [(0, jsx_runtime_1.jsx)("h3", { id: `${id}-h`, className: cls.subheading, children: "Wat nu?" }), (0, jsx_runtime_1.jsx)("p", { className: cls.text, children: "De klant heeft je antwoord niet. Stuur het opnieuw vanuit Outlook, bijvoorbeeld naar een ander adres van de klant, of sluit het ticket als het niet meer nodig is." }), (0, jsx_runtime_1.jsxs)("div", { className: cls.actions, children: [can("replied_externally") && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.primaryButton, disabled: busy, onClick: () => run("replied-externally"), children: "Opnieuw verstuurd vanuit Outlook" })), can("dismiss") && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.secondaryButton, disabled: busy, onClick: () => run("dismiss"), children: "Geen antwoord meer nodig" }))] })] }));
356
519
  }
357
520
  function ReplyAttachments({ cls, attachments, busy, editable, run, uploadFile, }) {
358
521
  const id = (0, react_1.useId)();
@@ -366,36 +529,39 @@ function ReplyAttachments({ cls, attachments, busy, editable, run, uploadFile, }
366
529
  await uploadFile(file);
367
530
  setUploading([]);
368
531
  };
369
- return ((0, jsx_runtime_1.jsxs)("div", { className: cls.field, children: [(0, jsx_runtime_1.jsx)("span", { className: cls.label, children: "Bijlagen" }), attachments.length > 0 && ((0, jsx_runtime_1.jsx)("ul", { className: cls.list, children: attachments.map((a) => ((0, jsx_runtime_1.jsxs)("li", { children: [(0, jsx_runtime_1.jsx)("span", { children: a.name }), " ", (0, jsx_runtime_1.jsxs)("span", { className: cls.muted, children: ["(", formatSize(a.size), ")"] }), " ", editable && !a.sent && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.linkButton, disabled: busy || uploading.length > 0, "aria-label": `${a.name} verwijderen`, onClick: () => run(`attachments/${encodeURIComponent(a.id)}`, { method: "DELETE" }), children: "Verwijderen" }))] }, a.id))) })), uploading.length > 0 && ((0, jsx_runtime_1.jsxs)("p", { className: cls.muted, "aria-live": "polite", children: ["Uploaden: ", uploading.join(", "), "\u2026"] })), editable && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: `${id}-file`, className: cls.secondaryButton, children: "Bijlage toevoegen" }), (0, jsx_runtime_1.jsx)("input", { id: `${id}-file`, type: "file", multiple: true, className: "sr-only", disabled: busy || uploading.length > 0, onChange: (e) => {
370
- const input = e.currentTarget;
371
- void choose(input.files).finally(() => {
372
- input.value = "";
373
- });
374
- } }), (0, jsx_runtime_1.jsx)("p", { className: cls.muted, children: "Samen hoogstens 20 MB en 10 bestanden. Ze gaan mee als je het antwoord goedkeurt." })] }))] }));
532
+ return ((0, jsx_runtime_1.jsxs)("div", { className: cls.actions, children: [attachments.length > 0 && ((0, jsx_runtime_1.jsx)("ul", { "aria-label": "Bijlagen bij je antwoord", className: cls.fileChips, children: attachments.map((a) => ((0, jsx_runtime_1.jsxs)("li", { className: cls.replyChip, children: [(0, jsx_runtime_1.jsx)("span", { className: "break-all", children: a.name }), " ", (0, jsx_runtime_1.jsx)("span", { className: cls.fileChipMeta, children: formatSize(a.size) }), editable && !a.sent && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.chipRemove, disabled: busy || uploading.length > 0, "aria-label": `${a.name} verwijderen`, onClick: () => run(`attachments/${encodeURIComponent(a.id)}`, { method: "DELETE" }), children: (0, jsx_runtime_1.jsx)(Icon, { name: "close", className: "h-3.5 w-3.5" }) }))] }, a.id))) })), uploading.length > 0 && ((0, jsx_runtime_1.jsxs)("p", { className: cls.muted, "aria-live": "polite", children: ["Uploaden: ", uploading.join(", "), "\u2026"] })), editable && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("label", { htmlFor: `${id}-file`, className: cls.dashedButton, children: [(0, jsx_runtime_1.jsx)(Icon, { name: "paperclip", className: "h-3.5 w-3.5 flex-none" }), "Bijlage toevoegen", (0, jsx_runtime_1.jsx)("input", { id: `${id}-file`, type: "file", multiple: true, className: cls.srOnly, disabled: busy || uploading.length > 0, onChange: (e) => {
533
+ const input = e.currentTarget;
534
+ void choose(input.files).finally(() => {
535
+ input.value = "";
536
+ });
537
+ } })] }), (0, jsx_runtime_1.jsx)("span", { className: cls.reasonNote, children: "Max. 10 bestanden, samen 20 MB" })] }))] }));
375
538
  }
376
539
  function attachmentNote(detail) {
377
540
  const n = (detail.replyAttachments ?? []).filter((a) => !a.sent).length;
378
541
  return n === 0 ? "" : n === 1 ? ", met 1 bijlage" : `, met ${n} bijlagen`;
379
542
  }
380
- function DraftSection({ cls, detail, busy, text, dirty, onText, act, run, can, uploadFile }) {
543
+ function footerNote(mode, mailbox) {
544
+ return mode === "read_only"
545
+ ? "Deze mailbox staat op alleen lezen. Je kunt het antwoord lezen en aanpassen, maar er wordt niets verstuurd."
546
+ : `Wordt verstuurd vanaf ${mailbox}.`;
547
+ }
548
+ function Composer({ cls, detail, busy, text, dirty, onText, act, run, can, uploadFile }) {
381
549
  const id = (0, react_1.useId)();
382
550
  const [confirming, setConfirming] = (0, react_1.useState)(null);
383
551
  const { thread, pilot, draft, send } = detail;
384
- if (!draft && thread.state !== "review")
385
- return null;
386
552
  const writing = draft?.status === "requested" || draft?.status === "started";
387
553
  const sendBlockedReason = pilot.paused
388
554
  ? "De helpdesk staat op pauze, daarom kan er nu niets worden verstuurd."
389
555
  : pilot.writeMode === "read_only"
390
556
  ? "Versturen staat uit: deze mailbox staat op alleen lezen."
391
557
  : !draft?.text
392
- ? "Er is nog geen concept om te versturen."
558
+ ? "Er is nog geen antwoord om te versturen."
393
559
  : send?.status === "unknown"
394
560
  ? "Geef eerst aan of het vorige antwoord is verstuurd."
395
561
  : null;
396
- const sentView = send?.status === "sent" && !draft?.stale;
397
- const showSend = thread.state === "review" && !sentView;
562
+ const showSend = thread.state === "review";
398
563
  const canSend = can("send") && text.trim() !== "";
564
+ const showText = (draft?.text !== null && draft?.text !== undefined) || can("edit");
399
565
  const sendNow = async (acceptStale) => {
400
566
  try {
401
567
  if (dirty)
@@ -407,25 +573,14 @@ function DraftSection({ cls, detail, busy, text, dirty, onText, act, run, can, u
407
573
  setConfirming(error instanceof ApiError && error.code === "stale_draft" ? "stale" : null);
408
574
  }
409
575
  };
410
- return ((0, jsx_runtime_1.jsxs)("section", { "aria-labelledby": `${id}-h`, className: cls.card, children: [(0, jsx_runtime_1.jsx)("h3", { id: `${id}-h`, className: cls.subheading, children: "Antwoord" }), writing && ((0, jsx_runtime_1.jsx)("p", { className: cls.info, "aria-live": "polite", children: "Er wordt een concept geschreven\u2026" })), draft?.status === "failed" && ((0, jsx_runtime_1.jsx)("p", { className: cls.warning, children: can("regenerate")
411
- ? "Het concept kon niet worden gemaakt. Laat een nieuw concept schrijven, schrijf zelf een antwoord of antwoord vanuit Outlook."
412
- : "Het concept kon niet worden gemaakt. Schrijf zelf een antwoord of antwoord vanuit Outlook." })), thread.reviewReason === "delivery_failed" && thread.state === "review" && ((0, jsx_runtime_1.jsx)("p", { className: cls.warning, children: "Dit antwoord is niet afgeleverd: het kwam terug als onbestelbaar. De reden staat bij Berichten. Stuur het opnieuw vanuit Outlook, bijvoorbeeld naar een ander adres, en kies daarna Al beantwoord in Outlook. Of kies Geen antwoord nodig." })), !draft && can("edit") && (0, jsx_runtime_1.jsx)("p", { className: cls.info, children: "Er is geen concept. Schrijf hieronder zelf het antwoord." }), draft?.stale && (0, jsx_runtime_1.jsx)("p", { className: cls.warning, children: "Na dit concept heeft de klant opnieuw gemaild. Lees de nieuwste mail voor je verstuurt." }), sentView ? ((0, jsx_runtime_1.jsxs)("div", { className: cls.field, children: [(0, jsx_runtime_1.jsx)("h4", { className: cls.label, children: "Verstuurd antwoord" }), (0, jsx_runtime_1.jsx)("div", { className: cls.messageBody, children: draft?.text || text || "(leeg)" }), (0, jsx_runtime_1.jsxs)("p", { className: cls.muted, children: ["Verstuurd op ", formatDate(send.at), " vanaf ", thread.mailbox, "."] }), (0, jsx_runtime_1.jsx)(ReplyAttachments, { cls: cls, attachments: (detail.replyAttachments ?? []).filter((a) => a.sent), busy: busy, editable: false, run: run, uploadFile: uploadFile })] })) : ((draft?.text !== null && draft?.text !== undefined) || can("edit")) && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: cls.field, children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: `${id}-text`, className: cls.label, children: draft?.text ? "Concept-antwoord" : "Jouw antwoord" }), (0, jsx_runtime_1.jsx)("textarea", { id: `${id}-text`, className: cls.textarea, value: text, readOnly: !can("edit"), "aria-describedby": `${id}-mode`, onChange: (e) => onText(e.target.value) })] }), (0, jsx_runtime_1.jsx)("p", { id: `${id}-mode`, className: cls.muted, children: modeExplanation(pilot.writeMode, thread.mailbox) }), can("edit") && ((0, jsx_runtime_1.jsx)("div", { className: cls.actions, children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.secondaryButton, disabled: busy || !dirty || text.trim() === "", onClick: () => run("draft", { method: "PUT", body: { text } }), children: "Concept opslaan" }) }))] })), !sentView && ((0, jsx_runtime_1.jsx)(ReplyAttachments, { cls: cls, attachments: (detail.replyAttachments ?? []).filter((a) => !a.sent), busy: busy, editable: can("attach"), run: run, uploadFile: uploadFile })), send?.status === "unknown" && ((0, jsx_runtime_1.jsxs)("div", { role: "group", "aria-label": "Verzending controleren", className: cls.warning, children: [(0, jsx_runtime_1.jsxs)("p", { children: ["Het is niet zeker of het vorige antwoord is verstuurd. Kijk in Outlook bij Verzonden items van ", thread.mailbox, ". Heeft Microsoft een storing? Dan controleert de helpdesk vanzelf of het concept nog klaarstaat zodra Microsoft weer werkt."] }), (0, jsx_runtime_1.jsxs)("div", { className: cls.actions, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.secondaryButton, disabled: busy || !can("resolve_send"), onClick: () => run("resolve-send", { body: { outcome: "sent" } }), children: "Het staat in Verzonden items" }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.secondaryButton, disabled: busy || !can("resolve_send"), onClick: () => run("resolve-send", { body: { outcome: "not_sent" } }), children: "Het is niet verstuurd" })] })] })), showSend && confirming === null && ((0, jsx_runtime_1.jsxs)("div", { className: cls.actions, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.primaryButton, disabled: busy || !canSend, "aria-describedby": sendBlockedReason ? `${id}-blocked` : undefined, onClick: () => setConfirming("send"), children: "Goedkeuren en versturen" }), sendBlockedReason && !can("send") && ((0, jsx_runtime_1.jsx)("p", { id: `${id}-blocked`, className: cls.muted, children: sendBlockedReason }))] })), showSend && confirming !== null && ((0, jsx_runtime_1.jsxs)("div", { role: "group", "aria-label": "Versturen bevestigen", className: cls.field, children: [(0, jsx_runtime_1.jsxs)("p", { className: cls.text, children: [confirming === "stale"
576
+ return ((0, jsx_runtime_1.jsxs)("section", { "aria-labelledby": `${id}-h`, className: cls.composer, children: [(0, jsx_runtime_1.jsxs)("div", { className: cls.composerHeader, children: [(0, jsx_runtime_1.jsx)("h3", { id: `${id}-h`, className: cls.subheading, children: "Jouw antwoord" }), draft?.fromDrafter && ((0, jsx_runtime_1.jsxs)("span", { className: cls.aiChip, children: [(0, jsx_runtime_1.jsx)(Icon, { name: "sparkle", className: "h-3.5 w-3.5 flex-none" }), "Voorstel van de AI \u00B7 pas aan waar nodig"] }))] }), writing && ((0, jsx_runtime_1.jsx)("p", { className: cls.info, "aria-live": "polite", children: "Er wordt een voorstel geschreven\u2026" })), draft?.status === "failed" && ((0, jsx_runtime_1.jsx)("p", { className: cls.warning, children: can("regenerate")
577
+ ? "Het voorstel kon niet worden gemaakt. Laat een nieuw voorstel schrijven, schrijf zelf een antwoord of antwoord vanuit Outlook."
578
+ : "Het voorstel kon niet worden gemaakt. Schrijf zelf een antwoord of antwoord vanuit Outlook." })), !draft && can("edit") && (0, jsx_runtime_1.jsx)("p", { className: cls.info, children: "Er is geen voorstel. Schrijf hieronder zelf het antwoord." }), draft?.stale && (0, jsx_runtime_1.jsx)("p", { className: cls.warning, children: "Na dit voorstel heeft de klant opnieuw gemaild. Lees de nieuwste mail voor je verstuurt." }), showText && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("label", { htmlFor: `${id}-text`, className: cls.srOnly, children: ["Antwoord aan ", thread.customerAddress] }), (0, jsx_runtime_1.jsx)("textarea", { id: `${id}-text`, className: cls.textarea, rows: 7, value: text, readOnly: !can("edit"), "aria-describedby": `${id}-mode`, onChange: (e) => onText(e.target.value) })] })), (0, jsx_runtime_1.jsx)(ReplyAttachments, { cls: cls, attachments: (detail.replyAttachments ?? []).filter((a) => !a.sent), busy: busy, editable: can("attach"), run: run, uploadFile: uploadFile }), send?.status === "unknown" && ((0, jsx_runtime_1.jsxs)("div", { role: "group", "aria-label": "Verzending controleren", className: cls.warning, children: [(0, jsx_runtime_1.jsxs)("p", { children: ["Het is niet zeker of het vorige antwoord is verstuurd. Kijk in Outlook bij Verzonden items van ", thread.mailbox, ". Heeft Microsoft een storing? Dan controleert de helpdesk vanzelf of het antwoord nog klaarstaat zodra Microsoft weer werkt."] }), (0, jsx_runtime_1.jsxs)("div", { className: cls.actions, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.secondaryButton, disabled: busy || !can("resolve_send"), onClick: () => run("resolve-send", { body: { outcome: "sent" } }), children: "Het staat in Verzonden items" }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.secondaryButton, disabled: busy || !can("resolve_send"), onClick: () => run("resolve-send", { body: { outcome: "not_sent" } }), children: "Het is niet verstuurd" })] })] })), (showText || showSend) && ((0, jsx_runtime_1.jsxs)("div", { className: cls.composerFooter, children: [(0, jsx_runtime_1.jsx)("p", { id: `${id}-mode`, className: cls.muted, children: footerNote(pilot.writeMode, thread.mailbox) }), confirming === null && ((0, jsx_runtime_1.jsxs)("div", { className: cls.actions, children: [can("regenerate") && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.secondaryButton, disabled: busy, onClick: () => run("regenerate"), children: "Nieuw voorstel" })), can("edit") && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.secondaryButton, disabled: busy || !dirty || text.trim() === "", onClick: () => run("draft", { method: "PUT", body: { text } }), children: "Opslaan" })), showSend && ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: cls.primaryButton, disabled: busy || !canSend, "aria-describedby": sendBlockedReason && !can("send") ? `${id}-blocked` : undefined, onClick: () => setConfirming("send"), children: [(0, jsx_runtime_1.jsx)(Icon, { name: "send", className: cls.icon }), "Goedkeuren en versturen"] }))] }))] })), showSend && confirming === null && sendBlockedReason && !can("send") && ((0, jsx_runtime_1.jsx)("p", { id: `${id}-blocked`, className: cls.muted, children: sendBlockedReason })), showSend && confirming !== null && ((0, jsx_runtime_1.jsxs)("div", { role: "group", "aria-label": "Versturen bevestigen", className: "flex flex-col gap-3", children: [(0, jsx_runtime_1.jsxs)("p", { className: cls.text, children: [confirming === "stale"
413
579
  ? "Er is een nieuwere mail van de klant. Wil je dit antwoord toch versturen?"
414
580
  : `Dit antwoord gaat nu naar ${thread.customerAddress}, vanaf ${thread.mailbox}${attachmentNote(detail)}.`, dirty ? " Je wijzigingen worden eerst opgeslagen." : ""] }), (0, jsx_runtime_1.jsxs)("div", { className: cls.actions, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.primaryButton, disabled: busy, onClick: () => void sendNow(confirming === "stale"), children: confirming === "stale" ? "Toch versturen" : "Ja, versturen" }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.secondaryButton, disabled: busy, onClick: () => setConfirming(null), children: "Annuleren" })] })] }))] }));
415
581
  }
416
- function OtherActions({ cls, busy, can, run, draftInOutlook, }) {
417
- const [confirmExternal, setConfirmExternal] = (0, react_1.useState)(false);
418
- const [confirmDismiss, setConfirmDismiss] = (0, react_1.useState)(false);
419
- const any = ["regenerate", "retry_placement", "replied_externally", "mark_waiting", "close", "unignore", "dismiss"].some((a) => can(a));
420
- if (!any)
582
+ function OtherActions({ cls, busy, can, run }) {
583
+ if (!can("unignore") && !can("mark_waiting") && !can("close"))
421
584
  return null;
422
- return ((0, jsx_runtime_1.jsxs)("section", { "aria-label": "Overige acties", className: cls.actions, children: [can("regenerate") && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.secondaryButton, disabled: busy, onClick: () => run("regenerate"), children: "Nieuw concept laten schrijven" })), can("retry_placement") && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.secondaryButton, disabled: busy, onClick: () => run("retry-placement"), children: "Opnieuw in Outlook zetten" })), can("unignore") && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.secondaryButton, disabled: busy, onClick: () => run("unignore"), children: "Toch behandelen" })), can("mark_waiting") && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.secondaryButton, disabled: busy, onClick: () => run("waiting"), children: "Wacht op klant" })), can("close") && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.secondaryButton, disabled: busy, onClick: () => run("close"), children: "Ticket sluiten" })), can("replied_externally") &&
423
- (confirmExternal ? ((0, jsx_runtime_1.jsxs)("span", { role: "group", "aria-label": "Beantwoord in Outlook bevestigen", className: cls.actions, children: [(0, jsx_runtime_1.jsx)("span", { className: cls.text, children: "Al beantwoord in Outlook? Het concept wordt dan niet verstuurd." }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.dangerButton, disabled: busy, onClick: () => {
424
- setConfirmExternal(false);
425
- run("replied-externally");
426
- }, children: "Ja, al beantwoord" }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.secondaryButton, onClick: () => setConfirmExternal(false), children: "Annuleren" })] })) : ((0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.secondaryButton, disabled: busy, onClick: () => setConfirmExternal(true), children: "Al beantwoord in Outlook" }))), can("dismiss") &&
427
- (confirmDismiss ? ((0, jsx_runtime_1.jsxs)("span", { role: "group", "aria-label": "Geen antwoord nodig bevestigen", className: cls.actions, children: [(0, jsx_runtime_1.jsxs)("span", { className: cls.text, children: ["Geen antwoord nodig? Er wordt niets verstuurd en het ticket gaat naar Genegeerd.", draftInOutlook ? " Het concept in Outlook wordt verwijderd." : ""] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.dangerButton, disabled: busy, onClick: () => {
428
- setConfirmDismiss(false);
429
- run("dismiss");
430
- }, children: "Ja, geen antwoord nodig" }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.secondaryButton, onClick: () => setConfirmDismiss(false), children: "Annuleren" })] })) : ((0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.secondaryButton, disabled: busy, onClick: () => setConfirmDismiss(true), children: "Geen antwoord nodig" })))] }));
585
+ return ((0, jsx_runtime_1.jsxs)("section", { "aria-label": "Overige acties", className: cls.actions, children: [can("unignore") && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.secondaryButton, disabled: busy, onClick: () => run("unignore"), children: "Toch behandelen" })), can("mark_waiting") && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.secondaryButton, disabled: busy, onClick: () => run("waiting"), children: "Wacht op klant" })), can("close") && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: cls.secondaryButton, disabled: busy, onClick: () => run("close"), children: "Ticket sluiten" }))] }));
431
586
  }
package/dist/sending.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import type { HelpdeskDeps, Paused, SendIntent, Thread, WriteBlocked } from "./types";
1
+ import type { DraftRun, HelpdeskDeps, Paused, SendIntent, Thread, WriteBlocked } from "./types";
2
2
  export declare function classifySendError(err: unknown): "not_sent" | "unknown";
3
+ export declare function sendIntentId(run: DraftRun): string;
3
4
  export type SendOutcome = {
4
5
  status: "sent";
5
6
  thread: Thread;