import { useEffect, useState } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useTrackedMutation } from '../hooks/useTrackedMutation'; import { getOwners, getTeams, deleteOwner, createOwner, updateOwner } from '../api/client'; import PageHeader from '../components/PageHeader'; import DataTable from '../components/DataTable'; import type { Column } from '../components/DataTable'; import ErrorState from '../components/ErrorState'; import { formatDateTime } from '../api/utils'; import type { Owner, Team } from '../api/types'; interface CreateOwnerModalProps { isOpen: boolean; onClose: () => void; onSuccess: () => void; isLoading: boolean; error: string | null; teamsData?: { data: Team[] }; } function CreateOwnerModal({ isOpen, onClose, onSuccess, isLoading, error, teamsData }: CreateOwnerModalProps) { const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [teamId, setTeamId] = useState(''); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!name.trim() || !email.trim()) return; await createOwner({ name: name.trim(), email: email.trim(), team_id: teamId || undefined, }); setName(''); setEmail(''); setTeamId(''); onSuccess(); }; if (!isOpen) return null; const teams = teamsData?.data || []; return (
e.stopPropagation()}>

Create Owner

{error &&
{error}
}
setName(e.target.value)} className="w-full bg-white border border-surface-border rounded px-3 py-2 text-sm text-ink focus:outline-none focus:border-brand-400" placeholder="e.g., Alice Smith" required />
setEmail(e.target.value)} className="w-full bg-white border border-surface-border rounded px-3 py-2 text-sm text-ink focus:outline-none focus:border-brand-400" placeholder="alice@example.com" required />
); } // EditOwnerModal — B-1 master closure (cat-b-31ceb6aaa9f1). Pre-B-1 the // only way to rename an owner was delete-and-recreate, which destroyed // audit history and broke every cert that referenced the old owner_id. // Mirrors CreateOwnerModal shape; pre-populates from the editing owner; // calls updateOwner(id, fields) instead of createOwner. interface EditOwnerModalProps { owner: Owner | null; onClose: () => void; onSuccess: () => void; isLoading: boolean; error: string | null; teamsData?: { data: Team[] }; } function EditOwnerModal({ owner, onClose, onSuccess, isLoading, error, teamsData }: EditOwnerModalProps) { const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [teamId, setTeamId] = useState(''); // Reset form fields whenever the editing target changes (modal opens). useEffect(() => { if (owner) { setName(owner.name); setEmail(owner.email); setTeamId(owner.team_id || ''); } }, [owner]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!owner || !name.trim() || !email.trim()) return; await updateOwner(owner.id, { name: name.trim(), email: email.trim(), team_id: teamId || undefined, }); onSuccess(); }; if (!owner) return null; const teams = teamsData?.data || []; return (
e.stopPropagation()}>

Edit Owner

{owner.id}

{error &&
{error}
}
setName(e.target.value)} className="w-full bg-white border border-surface-border rounded px-3 py-2 text-sm text-ink focus:outline-none focus:border-brand-400" required />
setEmail(e.target.value)} className="w-full bg-white border border-surface-border rounded px-3 py-2 text-sm text-ink focus:outline-none focus:border-brand-400" required />
); } export default function OwnersPage() { const queryClient = useQueryClient(); const [showCreate, setShowCreate] = useState(false); const [editingOwner, setEditingOwner] = useState(null); const { data, isLoading, error, refetch } = useQuery({ queryKey: ['owners'], queryFn: () => getOwners(), }); const { data: teamsData } = useQuery({ queryKey: ['teams'], queryFn: () => getTeams(), }); const deleteMutation = useTrackedMutation({ mutationFn: deleteOwner, invalidates: [['owners']], onError: (err: Error) => alert(`Delete failed: ${err.message}`), }); const createMutation = useTrackedMutation({ mutationFn: createOwner, invalidates: [['owners']], onSuccess: () => { setShowCreate(false); }, }); const updateMutation = useTrackedMutation({ mutationFn: ({ id, data }: { id: string; data: Partial }) => updateOwner(id, data), invalidates: [['owners']], onSuccess: () => { setEditingOwner(null); }, }); const teamMap = new Map(); (teamsData?.data || []).forEach((t) => teamMap.set(t.id, t)); const columns: Column[] = [ { key: 'name', label: 'Owner', render: (o) => (
{o.name}
{o.id}
), }, { key: 'email', label: 'Email', render: (o) => {o.email || '\u2014'}, }, { key: 'team', label: 'Team', render: (o) => { const team = teamMap.get(o.team_id); return team ? {team.name} : {o.team_id || '\u2014'}; }, }, { key: 'created', label: 'Created', render: (o) => {formatDateTime(o.created_at)}, }, { key: 'actions', label: '', render: (o) => (
), }, ]; return ( <> setShowCreate(true)} className="btn btn-primary"> + New Owner } />
{error ? ( refetch()} /> ) : ( )}
setShowCreate(false)} onSuccess={() => { queryClient.invalidateQueries({ queryKey: ['owners'] }); setShowCreate(false); }} isLoading={createMutation.isPending} error={createMutation.error ? (createMutation.error as Error).message : null} teamsData={teamsData} /> setEditingOwner(null)} onSuccess={() => { queryClient.invalidateQueries({ queryKey: ['owners'] }); setEditingOwner(null); }} isLoading={updateMutation.isPending} error={updateMutation.error ? (updateMutation.error as Error).message : null} teamsData={teamsData} /> ); }