|
|
import { useState } from 'react'
|
|
|
import { Link } from 'react-router-dom'
|
|
|
import { Badge, DataTable, EmptyState, ErrorState, FilterChips, Notice, PageHeader, Skeleton, Toolbar } from '@sims/ui'
|
|
|
import { getReminders, isManagerial, staffId, type ReminderStatus } from '../api'
|
|
|
import { QueueActions, RULE_LABEL, reminderTone } from '../components/ReminderQueue'
|
|
|
import { useData } from './Clients'
|
|
|
|
|
|
const PAGE_SIZE = 50
|
|
|
|
|
|
const STATUS_CHIPS: { key: ReminderStatus; label: string }[] = [
|
|
|
{ key: 'queued', label: 'Queued' }, { key: 'sent', label: 'Sent' },
|
|
|
{ key: 'failed', label: 'Failed' }, { key: 'dismissed', label: 'Dismissed' },
|
|
|
]
|
|
|
|
|
|
/**
|
|
|
* Reminders page (go-live cluster WS-C): the full queue the dashboard card is a
|
|
|
* slice of. Status chips map onto `?status=`; the Mine/All toggle (`?owner=`) only
|
|
|
* renders for owner/manager — staff are server-forced to their own rows anyway.
|
|
|
* Server-paginated with an honest total (rule 8).
|
|
|
*/
|
|
|
export function Reminders() {
|
|
|
const [status, setStatus] = useState<ReminderStatus>('queued')
|
|
|
const [scope, setScope] = useState<'all' | 'mine'>('all')
|
|
|
const [page, setPage] = useState(1)
|
|
|
const [err, setErr] = useState('')
|
|
|
const me = staffId()
|
|
|
// Mine narrows via ?owner=<my id>; hidden for pre-existing sessions that never stored the id.
|
|
|
const showScope = isManagerial() && me !== ''
|
|
|
|
|
|
const list = useData(
|
|
|
() => getReminders({
|
|
|
status,
|
|
|
...(showScope && scope === 'mine' ? { owner: me } : {}),
|
|
|
page, pageSize: PAGE_SIZE,
|
|
|
}),
|
|
|
[status, scope, page],
|
|
|
)
|
|
|
|
|
|
const data = list.data
|
|
|
const rows = data?.reminders
|
|
|
const totalPages = data !== undefined ? Math.max(1, Math.ceil(data.total / data.pageSize)) : 1
|
|
|
const from = data !== undefined && data.total > 0 ? (data.page - 1) * data.pageSize + 1 : 0
|
|
|
const to = data !== undefined ? Math.min(data.page * data.pageSize, data.total) : 0
|
|
|
|
|
|
return (
|
|
|
<div className="wf-page">
|
|
|
<PageHeader
|
|
|
title="Reminders"
|
|
|
desc="Every reminder the scanner has raised — send, preview or dismiss the open ones; sent and dismissed stay on record."
|
|
|
/>
|
|
|
<Toolbar>
|
|
|
<FilterChips
|
|
|
chips={STATUS_CHIPS}
|
|
|
active={status}
|
|
|
onChange={(k) => { setStatus(k as ReminderStatus); setPage(1) }}
|
|
|
/>
|
|
|
{showScope && (
|
|
|
<FilterChips
|
|
|
chips={[{ key: 'all', label: 'All' }, { key: 'mine', label: 'Mine' }]}
|
|
|
active={scope}
|
|
|
onChange={(k) => { setScope(k as 'all' | 'mine'); setPage(1) }}
|
|
|
/>
|
|
|
)}
|
|
|
<span style={{ flex: 1 }} />
|
|
|
{data !== undefined && <Badge>{data.total} total</Badge>}
|
|
|
</Toolbar>
|
|
|
{err !== '' && <Notice tone="err">{err}</Notice>}
|
|
|
{list.error !== undefined && <ErrorState message={list.error} onRetry={list.reload} />}
|
|
|
{list.error !== undefined ? null
|
|
|
: rows === undefined ? <Skeleton rows={6} />
|
|
|
: rows.length === 0 ? <EmptyState>No {status} reminders{scope === 'mine' ? ' of yours' : ''}.</EmptyState> : (
|
|
|
<>
|
|
|
<DataTable
|
|
|
columns={[
|
|
|
{ key: 'kind', label: 'Kind' },
|
|
|
{ key: 'client', label: 'Client' },
|
|
|
{ key: 'doc', label: 'Document', mono: true },
|
|
|
{ key: 'due', label: 'Due' },
|
|
|
{ key: 'status', label: 'Status' },
|
|
|
{ key: 'error', label: 'Error' },
|
|
|
{ key: 'act', label: '' },
|
|
|
]}
|
|
|
rowTone={(i) => (rows[i]!.status === 'failed' ? 'err' : undefined)}
|
|
|
rows={rows.map((rem) => ({
|
|
|
kind: rem.label !== '' ? rem.label : RULE_LABEL[rem.ruleKind] ?? rem.ruleKind,
|
|
|
client: <Link to={`/clients/${rem.clientId}`}>{rem.clientName}</Link>,
|
|
|
doc: rem.docId !== null
|
|
|
? <Link to={`/documents/${rem.docId}`}>{rem.docNo ?? '— draft —'}</Link>
|
|
|
: '—',
|
|
|
due: rem.duePeriod,
|
|
|
status: <Badge tone={reminderTone(rem.status)}>{rem.status}</Badge>,
|
|
|
error: rem.error ?? '—',
|
|
|
// Sent/dismissed rows are history — actions only while there is work to do.
|
|
|
act: rem.status === 'queued' || rem.status === 'failed'
|
|
|
? <QueueActions rem={rem} onDone={list.reload} onError={setErr} />
|
|
|
: null,
|
|
|
}))}
|
|
|
/>
|
|
|
<Toolbar>
|
|
|
<Badge>Showing {from}–{to} of {data!.total}</Badge>
|
|
|
<span style={{ flex: 1 }} />
|
|
|
<button
|
|
|
type="button" className="wf" disabled={data!.page <= 1}
|
|
|
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
|
|
>
|
|
|
‹ Prev
|
|
|
</button>
|
|
|
<span style={{ fontSize: 12, opacity: 0.7 }}>Page {data!.page} / {totalPages}</span>
|
|
|
<button
|
|
|
type="button" className="wf" disabled={data!.page >= totalPages}
|
|
|
onClick={() => setPage((p) => p + 1)}
|
|
|
>
|
|
|
Next ›
|
|
|
</button>
|
|
|
</Toolbar>
|
|
|
</>
|
|
|
)}
|
|
|
</div>
|
|
|
)
|
|
|
}
|