feat: parent/child company badges in client header

- Parent companies show a violet badge with subsidiary count, clickable to open modal
- Modal lists each subsidiary with policy count, task count, and claims advocate, click to navigate
- Child companies show a blue badge with parent name, clickable to navigate to parent page
- Both badges update reactively with state
This commit is contained in:
lorentz 2026-04-09 12:52:56 +00:00
parent 170bb35921
commit 4774eb1932
2 changed files with 77 additions and 2 deletions

View file

@ -40,6 +40,7 @@ export default async function ClientDetailPage({
id: true,
name: true,
_count: { select: { tasks: true, policies: true } },
claimsAdvocate: { select: { id: true, displayName: true, email: true } },
},
orderBy: { name: 'asc' },
},

View file

@ -6,6 +6,12 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Select,
SelectContent,
@ -14,7 +20,7 @@ import {
SelectValue,
} from '@/components/ui/select'
import { Textarea } from '@/components/ui/textarea'
import { Building2, MapPin, Phone, Mail, FileText, CheckSquare, CalendarRange, Users, X, Plus, UserCheck, StickyNote, Pencil, Trash2, ChevronDown, ChevronUp, ArrowUp, ArrowDown, ArrowUpDown } from 'lucide-react'
import { Building2, MapPin, Phone, Mail, FileText, CheckSquare, CalendarRange, Users, X, Plus, UserCheck, StickyNote, Pencil, Trash2, ChevronDown, ChevronUp, ArrowUp, ArrowDown, ArrowUpDown, GitBranch, ChevronRight } from 'lucide-react'
import { Combobox } from '@/components/ui/combobox'
import { formatDate, formatRenewalDate } from '@/lib/utils'
import { PolicyGroupManager } from '@/components/clients/policy-group-manager'
@ -53,6 +59,7 @@ export function ClientDetail({ client, designations, policyGroups = [], allPolic
const [parentOptions, setParentOptions] = useState<{id:string;name:string}[]>([])
const [parentLinking, setParentLinking] = useState(false)
const [showArchivedTasks, setShowArchivedTasks] = useState(false)
const [subsidiariesModalOpen, setSubsidiariesModalOpen] = useState(false)
const [taskSort, setTaskSort] = useState<'date' | 'priority'>('date')
const [taskSortDir, setTaskSortDir] = useState<'asc' | 'desc'>('asc')
const [showCompleted, setShowCompleted] = useState(false)
@ -290,6 +297,9 @@ export function ClientDetail({ client, designations, policyGroups = [], allPolic
}
}
const isParent = subsidiaries.length > 0
const isChild = !!parentClient
return (
<div className="space-y-6">
{/* Header */}
@ -299,7 +309,7 @@ export function ClientDetail({ client, designations, policyGroups = [], allPolic
<Building2 className="h-8 w-8" />
{client.name}
</h1>
<div className="flex gap-4 mt-3 text-sm text-muted-foreground">
<div className="flex flex-wrap gap-4 mt-3 text-sm text-muted-foreground">
{client.city && client.state && (
<div className="flex items-center gap-1">
<MapPin className="h-4 w-4" />
@ -319,9 +329,73 @@ export function ClientDetail({ client, designations, policyGroups = [], allPolic
</div>
)}
</div>
{/* Parent / Child badges */}
<div className="flex flex-wrap gap-2 mt-3">
{isParent && (
<button
onClick={() => setSubsidiariesModalOpen(true)}
className="inline-flex items-center gap-1.5 rounded-full border border-violet-500/40 bg-violet-500/10 px-3 py-1 text-xs font-medium text-violet-700 dark:text-violet-400 hover:bg-violet-500/20 transition-colors"
>
<GitBranch className="h-3.5 w-3.5" />
Parent Company · {subsidiaries.length} {subsidiaries.length === 1 ? 'subsidiary' : 'subsidiaries'}
</button>
)}
{isChild && (
<Link
href={`/clients/${parentClient!.id}`}
className="inline-flex items-center gap-1.5 rounded-full border border-blue-500/40 bg-blue-500/10 px-3 py-1 text-xs font-medium text-blue-700 dark:text-blue-400 hover:bg-blue-500/20 transition-colors"
>
<ChevronRight className="h-3.5 w-3.5 rotate-180" />
Child of {parentClient!.name}
</Link>
)}
</div>
</div>
</div>
{/* Subsidiaries modal */}
<Dialog open={subsidiariesModalOpen} onOpenChange={setSubsidiariesModalOpen}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<GitBranch className="h-5 w-5" />
Subsidiaries of {client.name}
</DialogTitle>
</DialogHeader>
<div className="space-y-2 max-h-[60vh] overflow-y-auto pr-1">
{subsidiaries.map((sub: any) => (
<Link
key={sub.id}
href={`/clients/${sub.id}`}
onClick={() => setSubsidiariesModalOpen(false)}
className="flex items-center justify-between rounded-lg border border-border p-3 hover:bg-muted/50 transition-colors group"
>
<div className="flex-1 min-w-0">
<p className="font-medium text-sm group-hover:text-primary transition-colors truncate">{sub.name}</p>
{sub.claimsAdvocate && (
<p className="text-xs text-muted-foreground mt-0.5">
Advocate: {sub.claimsAdvocate.displayName || sub.claimsAdvocate.email}
</p>
)}
</div>
<div className="flex items-center gap-3 ml-4 shrink-0 text-xs text-muted-foreground">
<span className="flex items-center gap-1">
<FileText className="h-3.5 w-3.5" />
{sub._count?.policies ?? 0}
</span>
<span className="flex items-center gap-1">
<CheckSquare className="h-3.5 w-3.5" />
{sub._count?.tasks ?? 0}
</span>
<ChevronRight className="h-4 w-4 opacity-40 group-hover:opacity-100 transition-opacity" />
</div>
</Link>
))}
</div>
</DialogContent>
</Dialog>
{/* Client Notes — collapsible */}
<div className="rounded-lg border bg-card">
<button