You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
sims-hq/apps/hq-web/src/components/ReminderQueue.tsx

83 lines
4.0 KiB
TypeScript

import { useState } from 'react'
import { Button, ConfirmDialog, useToast } from '@sims/ui'
import { dismissReminder, getReminderPreview, sendReminder, type QueueItem } from '../api'
/** Fallback kind labels for queue rows whose server `label` is empty. */
export const RULE_LABEL: Record<string, string> = {
invoice_overdue: 'Overdue invoice', renewal_due: 'Renewal due', amc_expiring: 'AMC expiring',
follow_up: 'Follow-up', recurring_generated: 'Recurring invoice', email_bounced: 'Email bounced',
quote_followup: 'Quote follow-up',
}
/** Rule kinds with a client-facing email; the rest are internal work items (dismiss only). */
export const SENDABLE = new Set(['invoice_overdue', 'renewal_due', 'amc_expiring', 'recurring_generated', 'quote_followup'])
/** Badge tone for a reminder status: failed red, sent green, dismissed neutral, queued amber. */
export const reminderTone = (status: string): 'ok' | 'warn' | 'err' | undefined =>
status === 'failed' ? 'err' : status === 'sent' ? 'ok' : status === 'dismissed' ? undefined : 'warn'
/**
* Send / Preview / Dismiss for one queue row — shared by the Dashboard queue card and
* the Reminders page (WS-C: one set of actions, two views). Reloads via `onDone` even
* on failure: the server has already parked the reminder as 'failed' with its error,
* and the row must flip live.
*/
export function QueueActions(props: { rem: QueueItem; onDone: () => void; onError: (m: string) => void }) {
const toast = useToast()
const [busy, setBusy] = useState(false)
const [mail, setMail] = useState<{ subject: string; body: string } | undefined>()
const [loading, setLoading] = useState(false)
const [confirmSend, setConfirmSend] = useState(false)
// Toast on success only — onDone still fires on failure too (the server has
// already parked the reminder as 'failed' and the row must flip live).
const run = (start: () => Promise<unknown>, okMsg: string) => {
if (busy) return // Button has no disabled prop — guard re-entry here
setBusy(true); props.onError('')
start()
.then(() => toast.ok(okMsg))
.catch((e: Error) => props.onError(e.message))
.then(props.onDone)
.finally(() => setBusy(false))
}
const preview = () => {
if (mail !== undefined) { setMail(undefined); return } // toggle closed
if (loading) return
setLoading(true); props.onError('')
getReminderPreview(props.rem.id)
.then(setMail)
.catch((e: Error) => props.onError(e.message))
.finally(() => setLoading(false))
}
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<div style={{ display: 'flex', gap: 6 }}>
{SENDABLE.has(props.rem.ruleKind) && (
<>
<Button onClick={preview}>{loading ? '…' : mail !== undefined ? 'Hide' : 'Preview'}</Button>
<Button tone="primary" disabled={busy} onClick={() => setConfirmSend(true)}>{busy ? '…' : 'Send'}</Button>
</>
)}
<Button onClick={() => run(() => dismissReminder(props.rem.id), 'Dismissed')}>Dismiss</Button>
</div>
{confirmSend && (
<ConfirmDialog
open
onClose={() => setConfirmSend(false)}
onConfirm={() => run(() => sendReminder(props.rem.id), 'Reminder sent')}
title="Send reminder"
body={`Email this reminder to ${props.rem.clientName}${props.rem.docNo !== null ? ` for ${props.rem.docNo}` : ''} now?`}
confirmLabel="Send"
/>
)}
{mail !== undefined && (
<div style={{ border: '1px solid var(--border)', borderRadius: 6, padding: '6px 8px', background: 'var(--bg-raised)', maxWidth: 420 }}>
<div style={{ fontSize: 11, textTransform: 'uppercase', color: 'var(--text-muted, #666)' }}>Subject</div>
<div style={{ fontWeight: 600, marginBottom: 4 }}>{mail.subject}</div>
<div style={{ fontSize: 11, textTransform: 'uppercase', color: 'var(--text-muted, #666)' }}>Body</div>
<pre style={{ whiteSpace: 'pre-wrap', margin: 0, fontFamily: 'inherit', fontSize: 13 }}>{mail.body}</pre>
</div>
)}
</div>
)
}