import { useMemo, useState } from 'react' import { Link, useNavigate } from 'react-router-dom' import { Badge, Button, DataTable, EmptyState, ErrorState, FilterChips, Notice, PageHeader, Skeleton, Toolbar, useToast, } from '@sims/ui' import { getSmsBalances, generateModuleRenewalQuote, refreshSmsBalances, isManagerial, type SmsBalanceRow, } from '../api' import { useData } from './Clients' type Scope = 'low' | 'all' | 'unknown' /** "3h ago" / "2d ago" from an ISO timestamp — compact freshness for the Checked column. */ function ago(iso: string | null): string { if (iso === null) return 'never' const ms = Date.now() - Date.parse(iso) if (Number.isNaN(ms)) return '—' const mins = Math.floor(ms / 60000) if (mins < 1) return 'just now' if (mins < 60) return `${mins}m ago` const hrs = Math.floor(mins / 60) if (hrs < 24) return `${hrs}h ago` return `${Math.floor(hrs / 24)}d ago` } /** * SMS credit review (D28): every SMS client with its balance, lowest first, so the * ones about to run dry are at the top. "Low" (below the threshold) is the default * view — that's the upsell list. One click raises a top-up proforma; the client name * links to the Modules tab where the balance is recorded. Balances are entered by the * team (there is no SMS-gateway sync — deliberately kept simple). */ export function SmsBalances() { const nav = useNavigate() const toast = useToast() const [scope, setScope] = useState('low') const [busy, setBusy] = useState() const [refreshing, setRefreshing] = useState(false) const [showCreds, setShowCreds] = useState(true) // managerial: show username/password columns const list = useData(getSmsBalances, []) const managerial = isManagerial() /** Download the SMS logins as a CSV (managerial): client, code, username, password, provider, balance. */ const downloadCreds = () => { const rows = data?.rows ?? [] const esc = (v: string | number | null) => { const s = v === null ? '' : String(v) return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s } const header = ['Code', 'Client', 'Username', 'Password', 'Provider', 'Balance', 'Installation date', 'Phone'] const lines = [header.join(',')] for (const r of rows) { lines.push([esc(r.clientCode), esc(r.clientName), esc(r.username), esc(r.password), esc(r.provider), esc(r.balance), esc(r.installationDate), esc(r.phone)].join(',')) } const blob = new Blob([lines.join('\n')], { type: 'text/csv;charset=utf-8' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url; a.download = 'sms-credentials.csv'; a.click() URL.revokeObjectURL(url) } const refreshAll = () => { if (refreshing) return setRefreshing(true) refreshSmsBalances() .then((s) => { toast.ok(`Checked ${s.attempted}: ${s.ok} ok, ${s.changed} updated${s.failed > 0 ? `, ${s.failed} failed` : ''}${s.skipped > 0 ? `, ${s.skipped} no login` : ''}`) list.reload() }) .catch((e: Error) => toast.err(e.message)) .finally(() => setRefreshing(false)) } const raiseTopup = (r: SmsBalanceRow) => { if (busy !== undefined) return setBusy(r.cmId) generateModuleRenewalQuote(r.cmId) .then((doc) => { toast.ok('Top-up proforma created'); nav(`/documents/${doc.id}`) }) .catch((e: Error) => { toast.err(e.message); setBusy(undefined) }) } const data = list.data const shown = useMemo(() => { const all = data?.rows ?? [] if (scope === 'low') return all.filter((r) => r.low) if (scope === 'unknown') return all.filter((r) => r.balance === null) return all }, [data, scope]) return (
) : undefined} /> {data !== undefined && data.lowCount > 0 && scope !== 'low' && ( {data.lowCount} client{data.lowCount === 1 ? '' : 's'} below {data.threshold.toLocaleString('en-IN')} SMS — needs a top-up. )} setScope(k as Scope)} /> {data !== undefined && threshold {data.threshold.toLocaleString('en-IN')}} {list.error !== undefined ? : data === undefined ? : shown.length === 0 ? ( {scope === 'low' ? 'No SMS client is below the low-balance threshold — all topped up.' : scope === 'unknown' ? 'Every SMS client has a balance recorded.' : 'No SMS clients yet.'} ) : ( (shown[i]!.low ? 'err' : undefined)} rows={shown.map((r) => ({ client: {r.clientCode} · {r.clientName}, balance: r.balance === null ? not recorded : ( {r.balance.toLocaleString('en-IN')} ), username: r.username ?? , password: r.password !== null && r.password !== '' ? r.password : {r.hasCreds ? '(set)' : '—'}, provider: r.provider ?? '—', checked: r.checkStatus === 'error' ? ⚠ {ago(r.checkedAt)} : !r.hasCreds ? no login : {ago(r.checkedAt)}, actions: ( e.stopPropagation()}> ), }))} /> )}
) }