feat(tickets): E-Mail-Benachrichtigung beim Antworten sichtbar/steuerbar
Beim Antworten auf ein Kunden-Ticket verschickte das System schon
automatisch (unsichtbar) eine Benachrichtigung an den Kunden. Jetzt
gibt es dafür eine Checkbox "Kunde per E-Mail benachrichtigen"
(Personal, Standard: an) im Antwortformular, und nach dem Absenden
eine Bestätigung ("Antwort gesendet, Kunde wurde per E-Mail
benachrichtigt."), statt dass der Versand unsichtbar im Hintergrund
passiert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
fe18bf0bce
commit
c0289317bf
2 changed files with 10 additions and 7 deletions
|
|
@ -101,7 +101,7 @@ export const ticketsModule: KcModule = {
|
|||
|
||||
app.post('/tickets/:id/messages', async (req) => {
|
||||
const a = requireAuth(req); const { id } = z.object({ id: z.string().uuid() }).parse(req.params);
|
||||
const b = z.object({ body: z.string().trim().min(1).max(10000), internalNote: z.boolean().default(false) }).parse(req.body);
|
||||
const b = z.object({ body: z.string().trim().min(1).max(10000), internalNote: z.boolean().default(false), notifyEmail: z.boolean().default(true) }).parse(req.body);
|
||||
const staff = can(a.principal, 'tickets.write');
|
||||
const t = await one('SELECT * FROM tickets WHERE id = ?', [id]); if (!t) throw notFound();
|
||||
if (!staff && !canInOrg(a.principal, t.org_id, 'tickets.create', 'tickets.write')) throw notFound();
|
||||
|
|
@ -115,11 +115,12 @@ export const ticketsModule: KcModule = {
|
|||
});
|
||||
await audit({ actorType: 'user', actorId: a.user.id, orgId: t.org_id, action: 'ticket.message', resourceType: 'ticket', resourceId: id, correlationId: req.correlationId, ip: clientIp(req), after: { internalNote: b.internalNote } });
|
||||
if (!b.internalNote && !staff) await notify('ticket.message', { number: t.number }, `ticket.message:${id}:${Date.now()}`, req.correlationId);
|
||||
if (!b.internalNote && staff) {
|
||||
let emailed = false;
|
||||
if (!b.internalNote && staff && b.notifyEmail) {
|
||||
const creator = await one('SELECT email, name FROM users WHERE id = ?', [t.created_by]);
|
||||
if (creator) await mailTo(creator.email, 'ticket_message', { name: creator.name, number: t.number, subject: t.subject }, `mail:ticket.message:${id}:${Date.now()}`, req.correlationId);
|
||||
if (creator) { await mailTo(creator.email, 'ticket_message', { name: creator.name, number: t.number, subject: t.subject }, `mail:ticket.message:${id}:${Date.now()}`, req.correlationId); emailed = true; }
|
||||
}
|
||||
return { ok: true, messageId };
|
||||
return { ok: true, messageId, emailed };
|
||||
});
|
||||
|
||||
// ---- Dateianhänge (Bilder, PDF) zu einer eigenen Nachricht --------------
|
||||
|
|
|
|||
|
|
@ -32,13 +32,14 @@ export default function TicketDetail() {
|
|||
useEffect(() => { if (write) api<StaffUser[]>('GET', '/admin/users').then((l) => setStaffUsers(l.filter((u) => !!u.staffRole))).catch(() => undefined); }, [write]);
|
||||
|
||||
async function send(e: FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault(); setErr(''); setBusy(true); const f = e.currentTarget; const fd = new FormData(f);
|
||||
const body = String(fd.get('body') ?? ''); const internal = fd.get('internal') === 'on';
|
||||
e.preventDefault(); setErr(''); setOk(''); setBusy(true); const f = e.currentTarget; const fd = new FormData(f);
|
||||
const body = String(fd.get('body') ?? ''); const internal = fd.get('internal') === 'on'; const notifyEmail = fd.get('notifyEmail') === 'on';
|
||||
const files = (fd.getAll('files') as File[]).filter((x) => x instanceof File && x.size > 0);
|
||||
try {
|
||||
const { messageId } = await api<{ messageId: string }>('POST', `/tickets/${id}/messages`, { body, internalNote: internal });
|
||||
const { messageId, emailed } = await api<{ messageId: string; emailed: boolean }>('POST', `/tickets/${id}/messages`, { body, internalNote: internal, notifyEmail });
|
||||
f.reset();
|
||||
if (files.length > 0) { try { await apiUpload(`/tickets/${id}/messages/${messageId}/attachments`, files); } catch (x) { setErr(`Nachricht gesendet, aber Anhang fehlgeschlagen: ${errMsg(x)}`); } }
|
||||
if (emailed) setOk('Antwort gesendet, Kunde wurde per E-Mail benachrichtigt.');
|
||||
void load();
|
||||
} catch (x) { setErr(errMsg(x)); } finally { setBusy(false); }
|
||||
}
|
||||
|
|
@ -67,6 +68,7 @@ export default function TicketDetail() {
|
|||
<Field id="body" label="Antwort"><textarea id="body" name="body" required rows={4} style={{ width: '100%' }} /></Field>
|
||||
<Field id="files" label="Anhänge (optional)" hint={`Bilder oder PDF, höchstens ${MAX_FILES} Dateien, je 15 MB.`}><input id="files" name="files" type="file" accept={ALLOWED} multiple /></Field>
|
||||
{staff && <label className="row small" style={{ marginBottom: 12 }}><input type="checkbox" name="internal" style={{ width: 20, minHeight: 20 }} /> Nur intern (für den Kunden nicht sichtbar)</label>}
|
||||
{staff && <label className="row small" style={{ marginBottom: 12 }}><input type="checkbox" name="notifyEmail" defaultChecked style={{ width: 20, minHeight: 20 }} /> Kunde per E-Mail benachrichtigen (bei internen Notizen ohne Wirkung)</label>}
|
||||
<div className="row"><button className="btn primary" type="submit" disabled={busy}>Absenden</button>{!staff && canClose && <button className="btn" type="button" onClick={close}>Als erledigt schließen</button>}</div>
|
||||
</form>}
|
||||
</>);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue