import { useEffect, useState } from 'react' import { fromRupees } from '@sims/domain' import { Button, Dialog, Field, FormField, FormGrid, Notice, Toolbar, useToast } from '@sims/ui' import { createAmc, createInteraction, createRecurringPlan, patchClientModule, recordPayment, updateAmc, updateInteraction, updateRecurringPlan, CLIENT_MODULE_STATUSES, KIND_LABEL, PAYMENT_MODES, type AmcContract, type ClientModule, type ClientModuleStatus, type Employee, type Interaction, type InteractionType, type Kind, type Module, type PaymentMode, type RecurringPlan, } from '../api' const today = () => new Date().toISOString().slice(0, 10) /** * Owner/manager only (server re-checks): pick the employee who owns this account, * so a lead with no quote is still routable to someone's queue. Writes client.owner_id * via the audited PATCH /clients/:id/owner. Inactive employees are hidden from the * picker, but a current owner who was since deactivated still renders. */ export function AccountOwnerSelect(props: { ownerId?: string; employees: Employee[]; onChange: (ownerId: string | null) => void }) { return ( Account owner props.onChange(e.target.value !== '' ? e.target.value : null)} > — unassigned — {props.employees .filter((emp) => emp.active || emp.id === props.ownerId) .map((emp) => {emp.displayName})} ) } /** Owner-only: create or edit a recurring plan on one of the client's modules. */ export function PlanDialog(props: { open: boolean; onClose: () => void; clientId: string options: { id: string; label: string }[]; initial?: RecurringPlan; onDone: () => void }) { const toast = useToast() const editing = props.initial !== undefined const [f, setF] = useState({ clientModuleId: '', cadence: 'monthly', amountRs: '', nextRun: today(), policy: 'manual' }) const [error, setError] = useState() const [saving, setSaving] = useState(false) useEffect(() => { if (!props.open) return const p = props.initial setError(undefined) setF({ clientModuleId: p?.clientModuleId ?? '', cadence: p?.cadence ?? 'monthly', amountRs: p?.amountPaise !== null && p?.amountPaise !== undefined ? String(p.amountPaise / 100) : '', nextRun: p?.nextRun ?? today(), policy: p?.policy ?? 'manual', }) }, [props.open, props.initial]) const set = (k: keyof typeof f) => (e: { target: { value: string } }) => setF((p) => ({ ...p, [k]: e.target.value })) const save = () => { if (saving) return if (!editing && f.clientModuleId === '') { setError('Pick a subscribed module'); return } const amount = f.amountRs === '' ? undefined : Number(f.amountRs) if (amount !== undefined && (!Number.isFinite(amount) || amount <= 0)) { setError('Amount must be a positive rupee value (or blank for the price book)'); return } setError(undefined) setSaving(true) const body = { cadence: f.cadence, nextRun: f.nextRun, policy: f.policy, // PATCH /recurring/:id cannot re-point a plan at another module, so edit mode // never sends clientModuleId (the select is disabled to match). ...(editing ? {} : { clientModuleId: f.clientModuleId }), // Edit mode: blank means "revert to price book" — send an explicit null so it clears. // Create mode: blank just omits the override, letting the price book resolve at generation time. ...(editing ? { amountPaise: amount !== undefined ? fromRupees(amount) : null } : amount !== undefined ? { amountPaise: fromRupees(amount) } : {}), } const call = editing ? updateRecurringPlan(props.initial!.id, body) : createRecurringPlan(props.clientId, body) call .then(() => { toast.ok('Plan saved'); props.onDone(); props.onClose() }) .catch((e: Error) => setError(e.message)) .finally(() => setSaving(false)) } return ( { if (!saving) props.onClose() }} title={editing ? 'Edit recurring plan' : 'New recurring plan'} footer={ <> { if (!saving) props.onClose() }}>Cancel {saving ? 'Saving…' : editing ? 'Save changes' : 'New recurring plan'} > } > Module… {props.options.map((o) => {o.label})} monthly yearly manual auto {error !== undefined && {error}} ) } /** Owner-only: create or edit an AMC contract — amount entered in rupees, stored in paise. */ export function AmcDialog(props: { open: boolean; onClose: () => void; clientId: string; initial?: AmcContract; onDone: () => void }) { const toast = useToast() const editing = props.initial !== undefined const [f, setF] = useState({ coverage: '', periodFrom: today(), periodTo: '', amountRs: '', reminderDays: '30' }) const [error, setError] = useState() const [saving, setSaving] = useState(false) useEffect(() => { if (!props.open) return const a = props.initial setError(undefined) setF({ coverage: a?.coverage ?? '', periodFrom: a?.periodFrom ?? today(), periodTo: a?.periodTo ?? '', amountRs: a !== undefined ? String(a.amountPaise / 100) : '', reminderDays: a !== undefined ? String(a.renewalReminderDays) : '30', }) }, [props.open, props.initial]) const set = (k: keyof typeof f) => (e: { target: { value: string } }) => setF((p) => ({ ...p, [k]: e.target.value })) const save = () => { if (saving) return const amount = Number(f.amountRs) if (!Number.isFinite(amount) || amount <= 0) { setError('Enter the contract amount in rupees'); return } if (f.periodTo === '') { setError('Pick the period end date'); return } const days = Number(f.reminderDays) if (!Number.isInteger(days) || days < 0) { setError('Reminder days must be a non-negative integer'); return } setError(undefined) setSaving(true) const body = { coverage: f.coverage, periodFrom: f.periodFrom, periodTo: f.periodTo, amountPaise: fromRupees(amount), renewalReminderDays: days, } const call = editing ? updateAmc(props.initial!.id, body) : createAmc(props.clientId, body) call .then(() => { toast.ok('AMC saved'); props.onDone(); props.onClose() }) .catch((e: Error) => setError(e.message)) .finally(() => setSaving(false)) } return ( { if (!saving) props.onClose() }} title={editing ? 'Edit AMC contract' : 'New AMC contract'} footer={ <> { if (!saving) props.onClose() }}>Cancel {saving ? 'Saving…' : editing ? 'Save changes' : 'New AMC'} > } > {error !== undefined && {error}} ) } /** All roles: log or edit a call / visit / training …; a follow-up date feeds the daily scan. */ export function InteractionDialog(props: { open: boolean; onClose: () => void; clientId: string types: InteractionType[]; initial?: Interaction; onDone: () => void }) { const toast = useToast() const editing = props.initial !== undefined const [f, setF] = useState({ typeCode: '', onDate: today(), notes: '', outcome: '', followUpOn: '' }) const [error, setError] = useState() const [saving, setSaving] = useState(false) useEffect(() => { if (!props.open) return const i = props.initial setError(undefined) setF({ typeCode: i?.typeCode ?? '', onDate: i?.onDate ?? today(), notes: i?.notes ?? '', outcome: i?.outcome ?? '', followUpOn: i?.followUpOn ?? '', }) }, [props.open, props.initial]) const set = (k: keyof typeof f) => (e: { target: { value: string } }) => setF((p) => ({ ...p, [k]: e.target.value })) const save = () => { if (saving) return if (f.typeCode === '') { setError('Pick an interaction type'); return } setError(undefined) setSaving(true) const body = { notes: f.notes, // PATCH /interactions/:id cannot change type or date, so edit mode never sends // typeCode/onDate (the fields are disabled to match) — a silently-ignored // "correction" with a success toast is worse than a locked field. ...(editing ? {} : { typeCode: f.typeCode, onDate: f.onDate }), // Edit mode: blank means "clear it" — send an explicit null so the server clears the column // (same semantics as ClientDetail's inline follow-up quick-edit). Create mode omits when blank. ...(editing ? { outcome: f.outcome !== '' ? f.outcome : null, followUpOn: f.followUpOn !== '' ? f.followUpOn : null } : { ...(f.outcome !== '' ? { outcome: f.outcome } : {}), ...(f.followUpOn !== '' ? { followUpOn: f.followUpOn } : {}), }), } const call = editing ? updateInteraction(props.initial!.id, body) : createInteraction(props.clientId, body) call .then(() => { toast.ok('Interaction saved'); props.onDone(); props.onClose() }) .catch((e: Error) => setError(e.message)) .finally(() => setSaving(false)) } return ( { if (!saving) props.onClose() }} title={editing ? 'Edit interaction' : 'Log interaction'} footer={ <> { if (!saving) props.onClose() }}>Cancel {saving ? (editing ? 'Saving…' : 'Logging…') : editing ? 'Save changes' : 'Log interaction'} > } > Type… {props.types.map((t) => {t.label})} — positive neutral negative {error !== undefined && {error}} ) } export function AssignModuleForm(props: { modules: Module[]; onAssign: (moduleId: string, kind: Kind) => void }) { const [moduleId, setModuleId] = useState('') const mod = props.modules.find((m) => m.id === moduleId) const [kind, setKind] = useState('') return ( { setModuleId(e.target.value); setKind('') }} > Assign module… {props.modules.filter((m) => m.active).map((m) => {m.name})} {mod !== undefined && ( setKind(e.target.value as Kind)}> Kind… {mod.allowedKinds.map((k) => {KIND_LABEL[k]})} )} { if (moduleId !== '' && kind !== '') { props.onAssign(moduleId, kind); setModuleId(''); setKind('') } }} > Assign ) } export function ClientModuleStatusSelect(props: { cm: ClientModule; onPatched: () => void; onError: (msg: string) => void }) { return ( { patchClientModule(props.cm.id, { status: e.target.value as ClientModuleStatus }) .then(props.onPatched).catch((err: Error) => props.onError(err.message)) }} > {CLIENT_MODULE_STATUSES.map((s) => {s})} ) } export function RowDate(props: { cm: ClientModule; field: 'installedOn' | 'completedOn' | 'trainedOn' onPatched: () => void; onError: (msg: string) => void }) { return ( { patchClientModule(props.cm.id, { [props.field]: e.target.value !== '' ? e.target.value : null }) .then(props.onPatched).catch((err: Error) => props.onError(err.message)) }} /> ) } /** * Record a payment against the client — amounts entered in rupees, stored in * integer paise via fromRupees. Shared with DocumentView's "Record payment". */ export function PaymentForm(props: { clientId: string; onDone: () => void }) { const [f, setF] = useState({ amountRs: '', tdsRs: '', mode: 'bank' as PaymentMode, reference: '', receivedOn: today() }) const [error, setError] = useState() const [saving, setSaving] = useState(false) const set = (k: keyof typeof f) => (e: { target: { value: string } }) => setF((p) => ({ ...p, [k]: e.target.value })) const save = () => { const amount = Number(f.amountRs) if (!Number.isFinite(amount) || amount <= 0) { setError('Enter a payment amount in rupees'); return } const tds = f.tdsRs === '' ? 0 : Number(f.tdsRs) if (!Number.isFinite(tds) || tds < 0) { setError('TDS must be a non-negative rupee amount'); return } setError(undefined) setSaving(true) recordPayment({ clientId: props.clientId, receivedOn: f.receivedOn, mode: f.mode, reference: f.reference, amountPaise: fromRupees(amount), tdsPaise: fromRupees(tds), }) .then(() => { setF({ amountRs: '', tdsRs: '', mode: 'bank', reference: '', receivedOn: today() }); props.onDone() }) .catch((e: Error) => setError(e.message)) .finally(() => setSaving(false)) } return ( {PAYMENT_MODES.map((m) => {m})} {saving ? 'Recording…' : 'Record payment'} {error !== undefined && {error}} ) }