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.
83 lines
4.0 KiB
TypeScript
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>
|
|
)
|
|
}
|