Workflow-App/Workflow/components/ResolutionWizard.tsx

874 lines
30 KiB
TypeScript
Raw Normal View History

// components/ResolutionWizard.tsx
// Phase 3 — Combined Resolution of Directors wizard.
//
// Multi-step modal for creating a new resolution. Triggered from
// ResolutionExplorer's "+ New resolution" button.
//
// Steps:
// 0 — Pick a company
// 1 — Choose path (Letters already received / Letter to be generated)
// 2 — Pick resigned directors (each with effective date)
// 3 — Pick appointed directors (each with effective date)
// 4 — Meeting date + notes
// 5 — Review & Create (auto-populated attendees table + submit)
//
// After successful create, the modal closes and the parent re-renders the
// table (router.refresh()) so the new draft row appears immediately.
//
// On dismiss/close: emits nothing, state resets.
"use client";
import { useEffect, useMemo, useState, useCallback } from "react";
import { useRouter } from "next/navigation";
// ─── Types ────────────────────────────────────────────────────────────────
type PathChoice = "letters_received" | "letter_generated";
interface Company {
id: string;
enterprise_name: string;
enterprise_no?: string;
}
interface Director {
id: string;
first_names: string;
surname: string;
email: string | null;
cell: string | null;
}
interface DirectorPick {
director_id: string;
effective_date: string;
}
interface WizardState {
step: number;
company: Company | null;
path: PathChoice | null;
resigned: DirectorPick[];
appointed: DirectorPick[];
meeting_date: string;
notes: string;
}
interface AttendeePreview {
director_id: string;
name: string;
surname: string;
email: string | null;
role: "existing" | "resigned" | "appointed";
must_sign: boolean;
attended: boolean;
effective_date: string;
}
// ─── Helpers ──────────────────────────────────────────────────────────────
const STEPS = [
"Pick company",
"Choose path",
"Resigning directors (optional)",
"Appointing directors (optional)",
"Meeting date",
"Review",
] as const;
function todayISO(): string {
return new Date().toISOString().split("T")[0];
}
function fullName(d: { first_names?: string; surname?: string }): string {
return [d.first_names, d.surname].filter(Boolean).join(" ").trim() || "—";
}
function classNames(...parts: Array<string | false | null>): string {
return parts.filter(Boolean).join(" ");
}
// ─── Component ────────────────────────────────────────────────────────────
interface Props {
open: boolean;
onClose: () => void;
}
export default function ResolutionWizard({ open, onClose }: Props) {
const router = useRouter();
const [state, setState] = useState<WizardState>({
step: 0,
company: null,
path: null,
resigned: [],
appointed: [],
meeting_date: todayISO(),
notes: "",
});
const [submitting, setSubmitting] = useState(false);
const [submitError, setSubmitError] = useState<string | null>(null);
// Reset on open/close
useEffect(() => {
if (!open) return;
setState({
step: 0,
company: null,
path: null,
resigned: [],
appointed: [],
meeting_date: todayISO(),
notes: "",
});
setSubmitError(null);
setSubmitting(false);
}, [open]);
// Close on Escape
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape" && !submitting) onClose();
};
document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey);
}, [open, submitting, onClose]);
const canAdvance = useMemo(() => {
switch (state.step) {
case 0: return !!state.company;
case 1: return !!state.path;
// Steps 2 (resigning) and 3 (appointing) are independently optional:
// you can pass a meeting with no resignations (only appointments),
// only resignations, or neither (just existing directors).
case 2: return true;
case 3: return true;
case 4: return !!state.meeting_date && /\d{4}-\d{2}-\d{2}/.test(state.meeting_date);
case 5: return true;
default: return false;
}
}, [state]);
const goNext = () => {
if (!canAdvance) return;
setState((s) => ({ ...s, step: Math.min(s.step + 1, STEPS.length - 1) }));
};
const goBack = () => {
setState((s) => ({ ...s, step: Math.max(s.step - 1, 0) }));
};
const submit = useCallback(async () => {
if (!state.company || !state.path) {
setSubmitError("Company and path are required.");
return;
}
setSubmitting(true);
setSubmitError(null);
try {
const res = await fetch("/api/resolutions/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
company_id: state.company.id,
path: state.path,
meeting_date: state.meeting_date,
resigned: state.resigned,
appointed: state.appointed,
notes: state.notes || undefined,
}),
});
const json = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(json.error || `HTTP ${res.status}`);
// success — close + refresh
onClose();
router.refresh();
} catch (err) {
setSubmitError(err instanceof Error ? err.message : "Failed to create resolution");
} finally {
setSubmitting(false);
}
}, [state, onClose, router]);
if (!open) return null;
return (
<div className="fixed inset-0 z-50 flex items-start justify-center bg-black/60 backdrop-blur-sm py-8">
<div className="bg-surface border border-border rounded-xl shadow-2xl w-full max-w-3xl mx-4 overflow-hidden flex flex-col max-h-[90vh]">
{/* Header */}
<div className="border-b border-border bg-surface-warm px-6 py-4 flex items-center justify-between">
<div>
<h2 className="text-fg font-bold text-lg">
New Resolution of Directors
</h2>
<p className="text-muted text-sm mt-0.5">
Step {state.step + 1} of {STEPS.length} — {STEPS[state.step]}
</p>
</div>
<button
onClick={onClose}
disabled={submitting}
aria-label="Close"
className="w-9 h-9 rounded-md text-muted hover:text-fg hover:bg-surface transition-colors text-xl disabled:opacity-40"
>
×
</button>
</div>
{/* Progress bar */}
<div className="px-6 py-3 border-b border-border bg-surface">
<div className="flex items-center gap-1.5">
{STEPS.map((label, i) => (
<div
key={label}
className={classNames(
"h-1.5 flex-1 rounded-full transition-colors",
i <= state.step ? "bg-accent" : "bg-surface-warm border border-border-soft"
)}
title={label}
/>
))}
</div>
</div>
{/* Body */}
<div className="flex-1 overflow-y-auto px-6 py-6">
{state.step === 0 && (
<CompanyStep
selected={state.company}
onPick={(c) => setState((s) => ({ ...s, company: c }))}
/>
)}
{state.step === 1 && (
<PathStep
value={state.path}
onChange={(p) => setState((s) => ({ ...s, path: p }))}
/>
)}
{state.step === 2 && state.company && (
<DirectorsStep
companyId={state.company.id}
role="resigned"
picked={state.resigned}
defaultDate={state.meeting_date}
onChange={(list) => setState((s) => ({ ...s, resigned: list }))}
/>
)}
{state.step === 3 && state.company && (
<DirectorsStep
companyId={state.company.id}
role="appointed"
picked={state.appointed}
defaultDate={state.meeting_date}
excludeIds={state.resigned.map((r) => r.director_id)}
onChange={(list) => setState((s) => ({ ...s, appointed: list }))}
/>
)}
{state.step === 4 && (
<MeetingDateStep
meetingDate={state.meeting_date}
notes={state.notes}
onChange={(d, n) =>
setState((s) => ({ ...s, meeting_date: d, notes: n }))
}
/>
)}
{state.step === 5 && state.company && (
<ReviewStep
state={state}
companyId={state.company.id}
/>
)}
{submitError && (
<div className="mt-4 border border-danger/40 bg-danger/5 rounded-lg p-4">
<p className="text-danger text-sm font-semibold">Failed to create resolution</p>
<p className="text-muted text-sm mt-1">{submitError}</p>
</div>
)}
</div>
{/* Footer */}
<div className="border-t border-border px-6 py-4 flex items-center justify-between bg-surface-warm">
<button
onClick={goBack}
disabled={state.step === 0 || submitting}
className="px-4 py-2 text-sm font-semibold text-muted hover:text-fg transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
>
← Back
</button>
<div className="flex items-center gap-3">
<button
onClick={onClose}
disabled={submitting}
className="px-4 py-2 text-sm font-semibold text-muted hover:text-fg transition-colors disabled:opacity-40"
>
Cancel
</button>
{state.step < STEPS.length - 1 ? (
<button
onClick={goNext}
disabled={!canAdvance}
className="px-5 py-2 bg-accent text-accent-on rounded font-semibold text-sm disabled:opacity-30 disabled:cursor-not-allowed hover:opacity-90 transition-opacity"
>
Next →
</button>
) : (
<button
onClick={submit}
disabled={submitting || !canAdvance}
className="px-5 py-2 bg-accent text-accent-on rounded font-semibold text-sm disabled:opacity-30 disabled:cursor-not-allowed hover:opacity-90 transition-opacity"
>
{submitting ? "Creating…" : "Create resolution"}
</button>
)}
</div>
</div>
</div>
</div>
);
}
// ─── Step 0: Company picker ───────────────────────────────────────────────
interface CompanyStepProps {
selected: Company | null;
onPick: (c: Company) => void;
}
function CompanyStep({ selected, onPick }: CompanyStepProps) {
const [search, setSearch] = useState("");
const [companies, setCompanies] = useState<Company[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch("/api/companies/list")
.then((r) => r.json())
.then((d) => { if (!cancelled) setCompanies(d.items || []); })
.catch(() => { if (!cancelled) setCompanies([]); })
.finally(() => { if (!cancelled) setLoading(false); });
return () => { cancelled = true; };
}, []);
const filtered = useMemo(() => {
if (!search.trim()) return companies;
const q = search.toLowerCase();
return companies.filter(
(c) =>
c.enterprise_name?.toLowerCase().includes(q) ||
c.enterprise_no?.toLowerCase().includes(q)
);
}, [companies, search]);
return (
<div className="space-y-3">
<p className="text-sm text-muted">
Choose the company this resolution applies to.
</p>
<input
autoFocus
placeholder="Search by name or enterprise number…"
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full bg-surface border border-border rounded px-3 py-2.5 text-sm text-fg placeholder-muted focus:outline-none focus:border-accent"
/>
<div className="border border-border rounded-lg overflow-hidden max-h-72 overflow-y-auto">
{loading ? (
<p className="p-4 text-sm text-muted">Loading companies…</p>
) : filtered.length === 0 ? (
<p className="p-4 text-sm text-muted italic">
{search.trim() ? "No companies match." : "No companies on file."}
</p>
) : (
filtered.map((c) => {
const isSelected = selected?.id === c.id;
return (
<button
key={c.id}
onClick={() => onPick(c)}
className={classNames(
"w-full text-left px-4 py-2.5 text-sm transition-colors border-b border-border last:border-b-0",
isSelected
? "bg-accent/10 text-accent font-semibold"
: "hover:bg-surface-warm text-fg"
)}
>
<span className="font-semibold">{c.enterprise_name}</span>
{c.enterprise_no && (
<span className="text-xs text-muted ml-2 font-mono">{c.enterprise_no}</span>
)}
</button>
);
})
)}
</div>
{selected && (
<div className="flex items-center gap-2 px-3 py-2 bg-accent/5 border border-accent/30 rounded-md text-sm">
<span className="text-muted">Selected:</span>
<span className="font-semibold text-fg">{selected.enterprise_name}</span>
{selected.enterprise_no && (
<span className="text-xs text-muted font-mono">({selected.enterprise_no})</span>
)}
</div>
)}
</div>
);
}
// ─── Step 1: Path picker ──────────────────────────────────────────────────
interface PathStepProps {
value: PathChoice | null;
onChange: (p: PathChoice) => void;
}
function PathStep({ value, onChange }: PathStepProps) {
return (
<div className="space-y-3">
<p className="text-sm text-muted">
How will the resolution letter be sourced?
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<PathCard
active={value === "letters_received"}
onClick={() => onChange("letters_received")}
title="Letters already received"
description="Resignation / appointment letters are already on file as PDFs. The minute of meeting will reference them."
/>
<PathCard
active={value === "letter_generated"}
onClick={() => onChange("letter_generated")}
title="Letter to be generated"
description="Render the combined minute from your master .docx template. Path B — Phase 6 will plug the generator in."
/>
</div>
</div>
);
}
interface PathCardProps {
active: boolean;
onClick: () => void;
title: string;
description: string;
}
function PathCard({ active, onClick, title, description }: PathCardProps) {
return (
<button
onClick={onClick}
className={classNames(
"text-left p-4 rounded-lg border-2 transition-all",
active
? "border-accent bg-accent/10"
: "border-border bg-surface hover:border-accent/50"
)}
>
<p className={classNames("font-semibold", active ? "text-accent" : "text-fg")}>{title}</p>
<p className="text-xs text-muted mt-1.5 leading-relaxed">{description}</p>
</button>
);
}
// ─── Steps 2/3: Director pickers (resigned / appointed) ───────────────────
interface DirectorsStepProps {
companyId: string;
role: "resigned" | "appointed";
picked: DirectorPick[];
defaultDate: string;
excludeIds?: string[];
onChange: (list: DirectorPick[]) => void;
}
function DirectorsStep({
companyId,
role,
picked,
defaultDate,
excludeIds = [],
onChange,
}: DirectorsStepProps) {
const [directors, setDirectors] = useState<Director[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState("");
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(`/api/resolutions/company-directors/${companyId}`)
.then((r) => r.json())
.then((d) => { if (!cancelled) setDirectors(d.items || []); })
.catch(() => { if (!cancelled) setDirectors([]); })
.finally(() => { if (!cancelled) setLoading(false); });
return () => { cancelled = true; };
}, [companyId]);
const pickedIds = new Set(picked.map((p) => p.director_id));
const available = useMemo(() => {
const lower = search.toLowerCase();
return directors
.filter((d) => !excludeIds.includes(d.id))
.filter((d) => {
if (!search.trim()) return true;
return (
d.first_names?.toLowerCase().includes(lower) ||
d.surname?.toLowerCase().includes(lower) ||
(d.email ?? "").toLowerCase().includes(lower)
);
});
}, [directors, search, excludeIds]);
const toggle = (d: Director) => {
if (pickedIds.has(d.id)) {
onChange(picked.filter((p) => p.director_id !== d.id));
} else {
onChange([...picked, { director_id: d.id, effective_date: defaultDate }]);
}
};
const setEffectiveDate = (directorId: string, date: string) => {
onChange(picked.map((p) => (p.director_id === directorId ? { ...p, effective_date: date } : p)));
};
const isResigned = role === "resigned";
const accentRing = isResigned ? "border-danger/30 bg-danger/5" : "border-success/30 bg-success/5";
return (
<div className="space-y-4">
<div className="flex items-baseline justify-between gap-3">
<p className="text-sm text-muted">
{isResigned
? "Pick any directors resigning on this resolution. Each gets an independent effective date."
: "Pick any directors being appointed on this resolution. Each gets an independent effective date."}
</p>
<span className="text-[10px] font-semibold uppercase tracking-wider text-muted bg-surface-warm border border-border rounded px-2 py-0.5">
Optional — leave empty to skip
</span>
</div>
{/* Skip-this-step affordance */}
{picked.length === 0 && (
<div className="text-xs text-muted border border-dashed border-border rounded-md px-3 py-2 bg-surface-warm">
No {isResigned ? "resigning" : "appointing"} directors selected. You can
advance to the next step — this is fine if there are simply no
{" "}{isResigned ? "resignations" : "appointments"} on this meeting.
</div>
)}
{/* Picked chips */}
{picked.length > 0 && (
<div className={classNames("border rounded-lg p-3 space-y-2", accentRing)}>
{picked.map((p) => {
const d = directors.find((x) => x.id === p.director_id);
return (
<div key={p.director_id} className="flex items-center gap-3">
<span className="flex-1 text-sm font-semibold text-fg">
{d ? fullName(d) : p.director_id}
</span>
<label className="text-xs text-muted">Effective date</label>
<input
type="date"
value={p.effective_date}
onChange={(e) => setEffectiveDate(p.director_id, e.target.value)}
className="bg-surface border border-border rounded px-2 py-1.5 text-xs font-mono text-fg focus:outline-none focus:border-accent"
/>
<button
onClick={() => toggle(d!)}
className="px-2 py-1 text-xs text-danger hover:bg-danger/10 rounded transition-colors"
title="Remove"
>
Remove
</button>
</div>
);
})}
</div>
)}
{/* Director search + list */}
<input
placeholder="Search directors by name or email…"
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full bg-surface border border-border rounded px-3 py-2 text-sm text-fg placeholder-muted focus:outline-none focus:border-accent"
/>
<div className="border border-border rounded-lg overflow-hidden max-h-72 overflow-y-auto">
{loading ? (
<p className="p-4 text-sm text-muted">Loading directors…</p>
) : available.length === 0 ? (
<p className="p-4 text-sm text-muted italic">
{search.trim() ? "No matching directors." : "No more directors available."}
</p>
) : (
available.map((d) => {
const isPicked = pickedIds.has(d.id);
return (
<button
key={d.id}
onClick={() => toggle(d)}
className={classNames(
"w-full text-left px-4 py-2.5 text-sm transition-colors border-b border-border last:border-b-0",
isPicked
? "bg-accent/10 text-accent font-semibold"
: "hover:bg-surface-warm text-fg"
)}
>
<div className="flex items-center justify-between gap-3">
<span>{fullName(d)}</span>
<span className="text-xs text-muted font-mono">
{d.email || "no email"}
</span>
</div>
</button>
);
})
)}
</div>
<p className="text-xs text-muted italic">
{picked.length} director{picked.length === 1 ? "" : "s"} selected.
{!isResigned && " Directors in the resigned list are hidden from this list."}
</p>
</div>
);
}
// ─── Step 4: Meeting date + notes ─────────────────────────────────────────
interface MeetingDateStepProps {
meetingDate: string;
notes: string;
onChange: (d: string, n: string) => void;
}
function MeetingDateStep({ meetingDate, notes, onChange }: MeetingDateStepProps) {
return (
<div className="space-y-4">
<p className="text-sm text-muted">
When did (or will) the meeting take place? This date appears on the
resolution minute.
</p>
<div>
<label className="block text-xs font-semibold text-muted uppercase mb-1.5">
Meeting date <span className="text-danger">*</span>
</label>
<input
type="date"
value={meetingDate}
onChange={(e) => onChange(e.target.value, notes)}
className="w-full bg-surface border border-border rounded px-3 py-2.5 text-sm text-fg font-mono focus:outline-none focus:border-accent"
/>
</div>
<div>
<label className="block text-xs font-semibold text-muted uppercase mb-1.5">
Internal notes (optional)
</label>
<textarea
rows={3}
value={notes}
onChange={(e) => onChange(meetingDate, e.target.value)}
placeholder="Anything SJ wants remembered about this resolution…"
className="w-full bg-surface border border-border rounded px-3 py-2 text-sm text-fg placeholder-muted focus:outline-none focus:border-accent resize-y"
/>
</div>
</div>
);
}
// ─── Step 5: Review ───────────────────────────────────────────────────────
interface ReviewStepProps {
state: WizardState;
companyId: string;
}
function ReviewStep({ state }: ReviewStepProps) {
const [existingDirectors, setActiveDirectors] = useState<Director[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(`/api/resolutions/company-directors/${state.company?.id}`)
.then((r) => r.json())
.then((d) => { if (!cancelled) setActiveDirectors(d.items || []); })
.catch(() => { if (!cancelled) setActiveDirectors([]); })
.finally(() => { if (!cancelled) setLoading(false); });
return () => { cancelled = true; };
}, [state.company?.id]);
// Build attendee preview list (same order the create endpoint will use):
// resigned → appointed → existing
const resignedMap = new Map(state.resigned.map((p) => [p.director_id, p]));
const appointedMap = new Map(state.appointed.map((p) => [p.director_id, p]));
const pickedIds = new Set([
...resignedMap.keys(),
...appointedMap.keys(),
]);
const existingAttendance: AttendeePreview[] = existingDirectors
.filter((d) => !pickedIds.has(d.id))
.map((d) => ({
director_id: d.id,
name: d.first_names,
surname: d.surname,
email: d.email,
role: "existing",
must_sign: true,
attended: true,
effective_date: state.meeting_date,
}));
const attendees: AttendeePreview[] = [
...[...resignedMap.values()].map((p) => {
const d = existingDirectors.find((x) => x.id === p.director_id);
return {
director_id: p.director_id,
name: d?.first_names ?? "",
surname: d?.surname ?? "",
email: d?.email ?? null,
role: "resigned" as const,
must_sign: true,
attended: true,
effective_date: p.effective_date,
};
}),
...[...appointedMap.values()].map((p) => {
const d = existingDirectors.find((x) => x.id === p.director_id);
return {
director_id: p.director_id,
name: d?.first_names ?? "",
surname: d?.surname ?? "",
email: d?.email ?? null,
role: "appointed" as const,
must_sign: true,
attended: true,
effective_date: p.effective_date,
};
}),
...existingAttendance,
];
return (
<div className="space-y-5">
<p className="text-sm text-muted">
Final check. The auto-populated attendees table below will be created
when you press <strong>Create resolution</strong>.
</p>
{/* Summary cards */}
<div className="grid grid-cols-2 gap-3">
<SummaryRow label="Company" value={state.company?.enterprise_name ?? "—"} />
<SummaryRow label="Path" value={state.path === "letters_received" ? "Letters received" : state.path === "letter_generated" ? "Letter generated" : "—"} />
<SummaryRow label="Meeting date" value={state.meeting_date} mono />
<SummaryRow label="Resigned" value={String(state.resigned.length)} />
<SummaryRow label="Appointed" value={String(state.appointed.length)} />
<SummaryRow
label="Total attendees"
value={String(attendees.length)}
accent
/>
</div>
{state.notes && (
<div className="border border-border rounded-lg p-3 bg-surface-warm">
<p className="text-xs font-semibold text-muted uppercase mb-1">Notes</p>
<p className="text-sm text-fg whitespace-pre-wrap">{state.notes}</p>
</div>
)}
{/* Attendees table */}
<div>
<p className="text-xs font-semibold text-muted uppercase mb-2">
Attendees (auto-populated, editable later)
</p>
{loading ? (
<p className="text-sm text-muted italic p-3">Loading attendees…</p>
) : attendees.length === 0 ? (
<p className="text-sm text-muted italic p-3">No attendees yet.</p>
) : (
<div className="border border-border rounded-lg overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-surface-warm border-b border-border">
<tr>
<th className="text-left px-3 py-2 text-xs font-semibold text-muted uppercase">Role</th>
<th className="text-left px-3 py-2 text-xs font-semibold text-muted uppercase">Name</th>
<th className="text-left px-3 py-2 text-xs font-semibold text-muted uppercase hidden sm:table-cell">Email</th>
<th className="text-left px-3 py-2 text-xs font-semibold text-muted uppercase hidden md:table-cell">Effective</th>
<th className="text-left px-3 py-2 text-xs font-semibold text-muted uppercase hidden md:table-cell">Sign?</th>
</tr>
</thead>
<tbody className="divide-y divide-border-soft">
{attendees.map((a) => (
<tr key={a.director_id}>
<td className="px-3 py-2">
<RolePill role={a.role} />
</td>
<td className="px-3 py-2 font-semibold text-fg">
{fullName(a)}
</td>
<td className="px-3 py-2 hidden sm:table-cell text-xs text-muted font-mono">
{a.email || "—"}
</td>
<td className="px-3 py-2 hidden md:table-cell text-xs font-mono text-muted">
{a.effective_date}
</td>
<td className="px-3 py-2 hidden md:table-cell text-xs">
{a.must_sign ? "Yes" : "No"}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
);
}
function SummaryRow({
label,
value,
mono,
accent,
}: {
label: string;
value: string;
mono?: boolean;
accent?: boolean;
}) {
return (
<div className="border border-border rounded-lg px-4 py-3 bg-surface">
<p className="text-xs text-muted uppercase tracking-wide">{label}</p>
<p
className={classNames(
"mt-1",
accent ? "text-accent font-bold" : "text-fg font-semibold",
Boolean(mono) && "font-mono text-sm"
)}
>
{value}
</p>
</div>
);
}
function RolePill({ role }: { role: "existing" | "resigned" | "appointed" }) {
const style =
role === "resigned"
? "bg-danger/10 text-danger"
: role === "appointed"
? "bg-success/10 text-success"
: "bg-surface-warm text-muted border border-border";
const label = role === "existing" ? "Existing" : role === "resigned" ? "Resigned" : "Appointed";
return (
<span className={classNames("inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold", style)}>
{label}
</span>
);
}