@dxv-systems/helpdesk 0.1.0 → 0.2.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/README.md +273 -279
- package/dist/bounce.d.ts +6 -0
- package/dist/bounce.js +38 -0
- package/dist/controls.js +1 -6
- package/dist/draft-body.d.ts +0 -1
- package/dist/draft-body.js +0 -14
- package/dist/drafting.d.ts +1 -2
- package/dist/drafting.js +12 -80
- package/dist/errors.d.ts +0 -3
- package/dist/errors.js +1 -7
- package/dist/inbox/handlers.js +23 -35
- package/dist/inbox/messages.d.ts +4 -5
- package/dist/inbox/messages.js +23 -14
- package/dist/inbox/types.d.ts +10 -2
- package/dist/index.d.ts +2 -3
- package/dist/index.js +1 -6
- package/dist/react.d.ts +42 -2
- package/dist/react.js +239 -94
- package/dist/sending.d.ts +2 -1
- package/dist/sending.js +35 -58
- package/dist/states.d.ts +1 -1
- package/dist/states.js +1 -2
- package/dist/testing/index.d.ts +0 -2
- package/dist/testing/index.js +1 -18
- package/dist/types.d.ts +2 -8
- package/package.json +73 -73
- package/dist/autosend.d.ts +0 -43
- package/dist/autosend.js +0 -83
- package/dist/cleanup.d.ts +0 -7
- package/dist/cleanup.js +0 -39
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-
|
|
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-
|
|
17
|
-
field: "flex flex-col gap-1
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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,102 @@ 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,
|
|
320
|
-
const can = (action) => actions.includes(action);
|
|
321
|
-
const
|
|
322
|
-
|
|
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 showComposer = !bounced && !sentReply && (draft !== null || thread.state === "review");
|
|
418
|
+
const menuItems = [];
|
|
419
|
+
if (!bounced && can("dismiss"))
|
|
420
|
+
menuItems.push({ key: "dismiss", label: "Geen antwoord nodig", onSelect: () => setConfirming("dismiss") });
|
|
421
|
+
if (!bounced && can("replied_externally")) {
|
|
422
|
+
menuItems.push({ key: "external", label: "Al beantwoord in Outlook", onSelect: () => setConfirming("external") });
|
|
423
|
+
}
|
|
424
|
+
if (thread.assignee)
|
|
425
|
+
menuItems.push({ key: "unassign", label: "Toewijzing weghalen", onSelect: () => run("assign", { method: "DELETE" }) });
|
|
426
|
+
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: () => {
|
|
427
|
+
setConfirming(null);
|
|
428
|
+
run(confirming === "dismiss" ? "dismiss" : "replied-externally");
|
|
429
|
+
} }))] }), (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: 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
430
|
setText(value);
|
|
324
431
|
setDirty(true);
|
|
325
|
-
}, act: act, run: run, can: can, uploadFile: uploadFile }), (0, jsx_runtime_1.
|
|
432
|
+
}, 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
433
|
}
|
|
327
|
-
function
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
return
|
|
434
|
+
function Assignment({ cls, detail, busy, run }) {
|
|
435
|
+
const owner = detail.thread.assignee;
|
|
436
|
+
if (owner && owner.id === detail.viewer.id) {
|
|
437
|
+
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"] }));
|
|
438
|
+
}
|
|
439
|
+
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"] }));
|
|
440
|
+
}
|
|
441
|
+
function MoreActions({ cls, busy, items }) {
|
|
442
|
+
const id = (0, react_1.useId)();
|
|
443
|
+
const [open, setOpen] = (0, react_1.useState)(false);
|
|
444
|
+
const wrapRef = (0, react_1.useRef)(null);
|
|
445
|
+
const toggleRef = (0, react_1.useRef)(null);
|
|
446
|
+
const firstRef = (0, react_1.useRef)(null);
|
|
447
|
+
(0, react_1.useEffect)(() => {
|
|
448
|
+
if (!open)
|
|
449
|
+
return;
|
|
450
|
+
firstRef.current?.focus();
|
|
451
|
+
const onPointer = (e) => {
|
|
452
|
+
if (wrapRef.current && !wrapRef.current.contains(e.target))
|
|
453
|
+
setOpen(false);
|
|
454
|
+
};
|
|
455
|
+
document.addEventListener("mousedown", onPointer);
|
|
456
|
+
return () => document.removeEventListener("mousedown", onPointer);
|
|
457
|
+
}, [open]);
|
|
458
|
+
return ((0, jsx_runtime_1.jsxs)("div", { ref: wrapRef, className: "relative", onKeyDown: (e) => {
|
|
459
|
+
if (e.key === "Escape" && open) {
|
|
460
|
+
setOpen(false);
|
|
461
|
+
toggleRef.current?.focus();
|
|
462
|
+
}
|
|
463
|
+
}, 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: () => {
|
|
464
|
+
setOpen(false);
|
|
465
|
+
item.onSelect();
|
|
466
|
+
}, children: item.label }, item.key))) }))] }));
|
|
467
|
+
}
|
|
468
|
+
function ConfirmAction({ cls, kind, busy, onConfirm, onCancel, }) {
|
|
469
|
+
const external = kind === "external";
|
|
470
|
+
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
|
|
471
|
+
? "Al beantwoord in Outlook? Het antwoord in de helpdesk wordt dan niet verstuurd."
|
|
472
|
+
: "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
473
|
}
|
|
334
474
|
const UNAVAILABLE_TEXT = {
|
|
335
475
|
item: "bijgevoegde mail, open deze in Outlook",
|
|
336
476
|
reference: "link naar een gedeeld bestand, open deze in Outlook",
|
|
337
477
|
too_large: "te groot om hier te openen, open de mail in Outlook",
|
|
338
478
|
};
|
|
339
|
-
function
|
|
479
|
+
function FileChips({ cls, attachments }) {
|
|
340
480
|
if (attachments.length === 0)
|
|
341
481
|
return null;
|
|
342
|
-
return ((0, jsx_runtime_1.
|
|
482
|
+
return ((0, jsx_runtime_1.jsx)("ul", { "aria-label": attachments.length === 1 ? "Bijlage" : `Bijlagen (${attachments.length})`, className: cls.fileChips, children: attachments.map((a) => {
|
|
483
|
+
const type = a.kind === "item" ? "Mail" : a.kind === "reference" ? "Link" : fileType(a.name);
|
|
484
|
+
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]}` : ""] })] })] }));
|
|
485
|
+
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));
|
|
486
|
+
}) }));
|
|
343
487
|
}
|
|
344
|
-
function
|
|
345
|
-
|
|
488
|
+
function Conversation({ cls, detail, bodies, sentReply, }) {
|
|
489
|
+
const entries = detail.messages.map((m) => ({ kind: m.bounce ? "bounce" : "mail", at: m.receivedAt, message: m }));
|
|
490
|
+
if (sentReply)
|
|
491
|
+
entries.push({ kind: "reply", at: sentReply.at, text: sentReply.text });
|
|
492
|
+
entries.sort((a, b) => a.at.localeCompare(b.at));
|
|
493
|
+
const sentFiles = (detail.replyAttachments ?? []).filter((a) => a.sent);
|
|
494
|
+
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)) : ((0, jsx_runtime_1.jsx)(MessageItem, { cls: cls, message: e.message, body: bodies.get(e.message.id) }, e.message.id))) }));
|
|
346
495
|
}
|
|
347
|
-
function
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
}
|
|
496
|
+
function MessageItem({ cls, message, body }) {
|
|
497
|
+
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 &&
|
|
498
|
+
(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 })] })] }));
|
|
499
|
+
}
|
|
500
|
+
function BounceItem({ cls, message, body }) {
|
|
501
|
+
const report = body && "text" in body ? body.bounce : null;
|
|
502
|
+
const reason = report?.reason ?? "De ontvangende server weigerde het bericht.";
|
|
503
|
+
const diagnostic = report ? report.diagnostic : body && "text" in body ? body.text : "";
|
|
504
|
+
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 })] }))] })] }));
|
|
505
|
+
}
|
|
506
|
+
function BouncedNextSteps({ cls, busy, can, run }) {
|
|
507
|
+
const id = (0, react_1.useId)();
|
|
508
|
+
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
509
|
}
|
|
357
510
|
function ReplyAttachments({ cls, attachments, busy, editable, run, uploadFile, }) {
|
|
358
511
|
const id = (0, react_1.useId)();
|
|
@@ -366,36 +519,39 @@ function ReplyAttachments({ cls, attachments, busy, editable, run, uploadFile, }
|
|
|
366
519
|
await uploadFile(file);
|
|
367
520
|
setUploading([]);
|
|
368
521
|
};
|
|
369
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: cls.
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
522
|
+
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) => {
|
|
523
|
+
const input = e.currentTarget;
|
|
524
|
+
void choose(input.files).finally(() => {
|
|
525
|
+
input.value = "";
|
|
526
|
+
});
|
|
527
|
+
} })] }), (0, jsx_runtime_1.jsx)("span", { className: cls.reasonNote, children: "Max. 10 bestanden, samen 20 MB" })] }))] }));
|
|
375
528
|
}
|
|
376
529
|
function attachmentNote(detail) {
|
|
377
530
|
const n = (detail.replyAttachments ?? []).filter((a) => !a.sent).length;
|
|
378
531
|
return n === 0 ? "" : n === 1 ? ", met 1 bijlage" : `, met ${n} bijlagen`;
|
|
379
532
|
}
|
|
380
|
-
function
|
|
533
|
+
function footerNote(mode, mailbox) {
|
|
534
|
+
return mode === "read_only"
|
|
535
|
+
? "Deze mailbox staat op alleen lezen. Je kunt het antwoord lezen en aanpassen, maar er wordt niets verstuurd."
|
|
536
|
+
: `Wordt verstuurd vanaf ${mailbox}.`;
|
|
537
|
+
}
|
|
538
|
+
function Composer({ cls, detail, busy, text, dirty, onText, act, run, can, uploadFile }) {
|
|
381
539
|
const id = (0, react_1.useId)();
|
|
382
540
|
const [confirming, setConfirming] = (0, react_1.useState)(null);
|
|
383
541
|
const { thread, pilot, draft, send } = detail;
|
|
384
|
-
if (!draft && thread.state !== "review")
|
|
385
|
-
return null;
|
|
386
542
|
const writing = draft?.status === "requested" || draft?.status === "started";
|
|
387
543
|
const sendBlockedReason = pilot.paused
|
|
388
544
|
? "De helpdesk staat op pauze, daarom kan er nu niets worden verstuurd."
|
|
389
545
|
: pilot.writeMode === "read_only"
|
|
390
546
|
? "Versturen staat uit: deze mailbox staat op alleen lezen."
|
|
391
547
|
: !draft?.text
|
|
392
|
-
? "Er is nog geen
|
|
548
|
+
? "Er is nog geen antwoord om te versturen."
|
|
393
549
|
: send?.status === "unknown"
|
|
394
550
|
? "Geef eerst aan of het vorige antwoord is verstuurd."
|
|
395
551
|
: null;
|
|
396
|
-
const
|
|
397
|
-
const showSend = thread.state === "review" && !sentView;
|
|
552
|
+
const showSend = thread.state === "review";
|
|
398
553
|
const canSend = can("send") && text.trim() !== "";
|
|
554
|
+
const showText = (draft?.text !== null && draft?.text !== undefined) || can("edit");
|
|
399
555
|
const sendNow = async (acceptStale) => {
|
|
400
556
|
try {
|
|
401
557
|
if (dirty)
|
|
@@ -407,25 +563,14 @@ function DraftSection({ cls, detail, busy, text, dirty, onText, act, run, can, u
|
|
|
407
563
|
setConfirming(error instanceof ApiError && error.code === "stale_draft" ? "stale" : null);
|
|
408
564
|
}
|
|
409
565
|
};
|
|
410
|
-
return ((0, jsx_runtime_1.jsxs)("section", { "aria-labelledby": `${id}-h`, className: cls.
|
|
411
|
-
? "Het
|
|
412
|
-
: "Het
|
|
566
|
+
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")
|
|
567
|
+
? "Het voorstel kon niet worden gemaakt. Laat een nieuw voorstel schrijven, schrijf zelf een antwoord of antwoord vanuit Outlook."
|
|
568
|
+
: "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
569
|
? "Er is een nieuwere mail van de klant. Wil je dit antwoord toch versturen?"
|
|
414
570
|
: `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
571
|
}
|
|
416
|
-
function OtherActions({ cls, busy, can, run
|
|
417
|
-
|
|
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)
|
|
572
|
+
function OtherActions({ cls, busy, can, run }) {
|
|
573
|
+
if (!can("unignore") && !can("mark_waiting") && !can("close"))
|
|
421
574
|
return null;
|
|
422
|
-
return ((0, jsx_runtime_1.jsxs)("section", { "aria-label": "Overige acties", className: cls.actions, children: [can("
|
|
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" })))] }));
|
|
575
|
+
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
576
|
}
|
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;
|