refactor: client detail page layout - notes collapsible near top, assignment tab

- Notes moved to collapsible panel just below header
  - Auto-expanded if client has notes, collapsed if empty
  - Shows preview snippet when collapsed
- Claims Advocate, Designations, Team Members, Related Companies
  consolidated into new 'Assignment' tab
- Removed 4 standalone cards from main scroll area
This commit is contained in:
lorentz 2026-04-08 10:10:39 +00:00
parent 851487a8dd
commit 56664068f8

View file

@ -14,7 +14,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 } from 'lucide-react'
import { Building2, MapPin, Phone, Mail, FileText, CheckSquare, CalendarRange, Users, X, Plus, UserCheck, StickyNote, Pencil, Trash2, ChevronDown, ChevronUp } 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 +53,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 [notesExpanded, setNotesExpanded] = useState(!!(client.notes))
const [additionalServiceOpen, setAdditionalServiceOpen] = useState(false)
const [sessionUserId, setSessionUserId] = useState('')
@ -318,220 +319,43 @@ export function ClientDetail({ client, designations, policyGroups = [], allPolic
</div>
</div>
{/* Claims Assignments */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<UserCheck className="h-5 w-5" />
Claims Assignment
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Advocate */}
<div>
<label className="text-sm font-medium mb-2 block">Claims Advocate</label>
<div className="flex gap-2">
<Combobox
className="flex-1"
options={claimsUsers.map((u) => ({ value: u.id, label: u.displayName || u.email }))}
value={advocateId}
onChange={setAdvocateId}
placeholder="Select advocate (Claims dept)"
emptyText="No Claims staff found"
/>
<Button onClick={handleAdvocateSave} disabled={advocateSaving}>
{advocateSaving ? 'Saving...' : 'Save'}
</Button>
</div>
</div>
{/* Team Members */}
<div>
<label className="text-sm font-medium mb-2 block">Team Members</label>
<div className="space-y-2">
{members.length === 0 && (
<p className="text-xs text-muted-foreground">No members assigned</p>
)}
{members.map((m: any) => (
<div key={m.id} className="flex items-center justify-between rounded-md border px-3 py-2">
<div className="flex items-center gap-2">
<Users className="h-4 w-4 text-muted-foreground" />
<span className="text-sm">{m.user?.displayName || m.user?.email}</span>
</div>
<Button
variant="ghost"
size="icon"
onClick={() => handleRemoveMember(m.userId)}
>
<X className="h-4 w-4" />
</Button>
</div>
))}
<div className="flex gap-2 pt-1">
<Combobox
className="flex-1"
options={allUsers
.filter((u) => !members.some((m: any) => m.userId === u.id))
.map((u) => ({ value: u.id, label: u.displayName || u.email }))}
value={addMemberId}
onChange={setAddMemberId}
placeholder="Add member..."
/>
<Button variant="outline" onClick={handleAddMember} disabled={!addMemberId}>
<Plus className="h-4 w-4 mr-1" />
Add
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Related Companies */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Building2 className="h-5 w-5" />
Related Companies
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Parent Company */}
<div>
<p className="text-sm font-medium mb-2">Parent Company</p>
{parentClient ? (
<div className="flex items-center justify-between rounded-md border px-3 py-2">
<a href={`/clients/${parentClient.id}`} className="text-sm font-medium hover:underline">
{parentClient.name}
</a>
<Button
size="sm"
variant="ghost"
className="text-destructive hover:text-destructive h-7 px-2"
onClick={handleUnlinkParent}
disabled={parentLinking}
>
<X className="h-3.5 w-3.5" />
</Button>
</div>
) : (
<div className="space-y-2">
<div className="relative">
<input
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm"
placeholder="Search for parent company..."
value={parentSearch}
onChange={(e) => setParentSearch(e.target.value)}
/>
</div>
{parentOptions.length > 0 && (
<div className="rounded-md border bg-popover shadow-md">
{parentOptions.map((opt) => (
<button
key={opt.id}
className="w-full text-left px-3 py-2 text-sm hover:bg-accent"
onClick={() => handleLinkParent(opt.id, opt.name)}
disabled={parentLinking}
>
{opt.name}
</button>
))}
</div>
)}
</div>
{/* Client Notes — collapsible */}
<div className="rounded-lg border bg-card">
<button
className="w-full flex items-center justify-between px-4 py-3 text-sm font-medium hover:bg-accent/50 transition-colors rounded-lg"
onClick={() => setNotesExpanded((v) => !v)}
>
<div className="flex items-center gap-2 text-muted-foreground">
<StickyNote className="h-4 w-4" />
<span>Client Notes</span>
{notes && !notesExpanded && (
<span className="text-xs text-foreground font-normal truncate max-w-[400px] ml-1 italic">{notes.slice(0, 80)}{notes.length > 80 ? '…' : ''}</span>
)}
{!notes && !notesExpanded && (
<span className="text-xs font-normal text-muted-foreground/60">No notes</span>
)}
</div>
{/* Subsidiaries */}
{subsidiaries.length > 0 && (
<div>
<p className="text-sm font-medium mb-2">Subsidiaries ({subsidiaries.length})</p>
<div className="space-y-1">
{subsidiaries.map((sub: any) => (
<div key={sub.id} className="flex items-center justify-between rounded-md border px-3 py-2">
<a href={`/clients/${sub.id}`} className="text-sm font-medium hover:underline">
{sub.name}
</a>
<div className="flex gap-3 text-xs text-muted-foreground">
<span>{sub._count?.policies ?? 0} policies</span>
<span>{sub._count?.tasks ?? 0} tasks</span>
</div>
</div>
))}
</div>
</div>
)}
</CardContent>
</Card>
{/* Client Notes */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<StickyNote className="h-5 w-5" />
Client Notes
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<Textarea
placeholder="Add notes about this client (e.g. reporting instructions, special handling)..."
value={notes}
onChange={(e) => setNotes(e.target.value)}
rows={4}
/>
<Button onClick={handleNotesSave} disabled={notesSaving} size="sm">
{notesSaving ? 'Saving...' : 'Save Notes'}
</Button>
</CardContent>
</Card>
{/* Designation Assignment */}
<Card>
<CardHeader>
<CardTitle>Designations</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<div>
<label className="text-sm font-medium mb-2 block">Primary Designation</label>
<Select value={selectedDesignation || undefined} onValueChange={setSelectedDesignation}>
<SelectTrigger>
<SelectValue placeholder="Select designation" />
</SelectTrigger>
<SelectContent>
{designations.map((designation) => (
<SelectItem key={designation.id} value={designation.id}>
{designation.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<label className="text-sm font-medium mb-2 block">Secondary Designation</label>
<Select value={selectedDesignation2 || undefined} onValueChange={setSelectedDesignation2}>
<SelectTrigger>
<SelectValue placeholder="Select designation" />
</SelectTrigger>
<SelectContent>
{designations.map((designation) => (
<SelectItem key={designation.id} value={designation.id}>
{designation.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{notesExpanded ? <ChevronUp className="h-4 w-4 text-muted-foreground" /> : <ChevronDown className="h-4 w-4 text-muted-foreground" />}
</button>
{notesExpanded && (
<div className="px-4 pb-4 space-y-3 border-t">
<Textarea
className="mt-3"
placeholder="Add notes about this client (e.g. reporting instructions, special handling)..."
value={notes}
onChange={(e) => setNotes(e.target.value)}
rows={4}
/>
<Button onClick={handleNotesSave} disabled={notesSaving} size="sm">
{notesSaving ? 'Saving...' : 'Save Notes'}
</Button>
</div>
<Button onClick={handleDesignationUpdate} disabled={saving}>
{saving ? 'Saving...' : 'Update Designations'}
</Button>
</CardContent>
</Card>
)}
</div>
{/* Tabs */}
<Tabs defaultValue="policies">
<TabsList>
<TabsList className="flex-wrap h-auto">
<TabsTrigger value="policies">
<FileText className="h-4 w-4 mr-2" />
Active Policies ({client.policies.length})
@ -548,6 +372,10 @@ export function ClientDetail({ client, designations, policyGroups = [], allPolic
<Users className="h-4 w-4 mr-2" />
Contacts ({contacts.length})
</TabsTrigger>
<TabsTrigger value="assignment">
<UserCheck className="h-4 w-4 mr-2" />
Assignment
</TabsTrigger>
</TabsList>
<TabsContent value="policies" className="space-y-4">
@ -859,6 +687,190 @@ export function ClientDetail({ client, designations, policyGroups = [], allPolic
</div>
)}
</TabsContent>
<TabsContent value="assignment" className="space-y-6">
{/* Claims Advocate */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<UserCheck className="h-4 w-4" />
Claims Advocate
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex gap-2">
<Combobox
className="flex-1"
options={claimsUsers.map((u) => ({ value: u.id, label: u.displayName || u.email }))}
value={advocateId}
onChange={setAdvocateId}
placeholder="Select advocate (Claims dept)"
emptyText="No Claims staff found"
/>
<Button onClick={handleAdvocateSave} disabled={advocateSaving}>
{advocateSaving ? 'Saving...' : 'Save'}
</Button>
</div>
</CardContent>
</Card>
{/* Designations */}
<Card>
<CardHeader>
<CardTitle className="text-base">Designations</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<div>
<label className="text-sm font-medium mb-2 block">Primary Designation</label>
<Select value={selectedDesignation || undefined} onValueChange={setSelectedDesignation}>
<SelectTrigger>
<SelectValue placeholder="Select designation" />
</SelectTrigger>
<SelectContent>
{designations.map((designation) => (
<SelectItem key={designation.id} value={designation.id}>
{designation.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<label className="text-sm font-medium mb-2 block">Secondary Designation</label>
<Select value={selectedDesignation2 || undefined} onValueChange={setSelectedDesignation2}>
<SelectTrigger>
<SelectValue placeholder="Select designation" />
</SelectTrigger>
<SelectContent>
{designations.map((designation) => (
<SelectItem key={designation.id} value={designation.id}>
{designation.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<Button onClick={handleDesignationUpdate} disabled={saving}>
{saving ? 'Saving...' : 'Update Designations'}
</Button>
</CardContent>
</Card>
{/* Team Members */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<Users className="h-4 w-4" />
Team Members
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{members.length === 0 && (
<p className="text-xs text-muted-foreground">No members assigned</p>
)}
{members.map((m: any) => (
<div key={m.id} className="flex items-center justify-between rounded-md border px-3 py-2">
<div className="flex items-center gap-2">
<Users className="h-4 w-4 text-muted-foreground" />
<span className="text-sm">{m.user?.displayName || m.user?.email}</span>
</div>
<Button variant="ghost" size="icon" onClick={() => handleRemoveMember(m.userId)}>
<X className="h-4 w-4" />
</Button>
</div>
))}
<div className="flex gap-2 pt-1">
<Combobox
className="flex-1"
options={allUsers
.filter((u) => !members.some((m: any) => m.userId === u.id))
.map((u) => ({ value: u.id, label: u.displayName || u.email }))}
value={addMemberId}
onChange={setAddMemberId}
placeholder="Add member..."
/>
<Button variant="outline" onClick={handleAddMember} disabled={!addMemberId}>
<Plus className="h-4 w-4 mr-1" />
Add
</Button>
</div>
</div>
</CardContent>
</Card>
{/* Related Companies */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<Building2 className="h-4 w-4" />
Related Companies
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<p className="text-sm font-medium mb-2">Parent Company</p>
{parentClient ? (
<div className="flex items-center justify-between rounded-md border px-3 py-2">
<a href={`/clients/${parentClient.id}`} className="text-sm font-medium hover:underline">
{parentClient.name}
</a>
<Button
size="sm" variant="ghost"
className="text-destructive hover:text-destructive h-7 px-2"
onClick={handleUnlinkParent} disabled={parentLinking}
>
<X className="h-3.5 w-3.5" />
</Button>
</div>
) : (
<div className="space-y-2">
<input
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm"
placeholder="Search for parent company..."
value={parentSearch}
onChange={(e) => setParentSearch(e.target.value)}
/>
{parentOptions.length > 0 && (
<div className="rounded-md border bg-popover shadow-md">
{parentOptions.map((opt) => (
<button
key={opt.id}
className="w-full text-left px-3 py-2 text-sm hover:bg-accent"
onClick={() => handleLinkParent(opt.id, opt.name)}
disabled={parentLinking}
>
{opt.name}
</button>
))}
</div>
)}
</div>
)}
</div>
{subsidiaries.length > 0 && (
<div>
<p className="text-sm font-medium mb-2">Subsidiaries ({subsidiaries.length})</p>
<div className="space-y-1">
{subsidiaries.map((sub: any) => (
<div key={sub.id} className="flex items-center justify-between rounded-md border px-3 py-2">
<a href={`/clients/${sub.id}`} className="text-sm font-medium hover:underline">
{sub.name}
</a>
<div className="flex gap-3 text-xs text-muted-foreground">
<span>{sub._count?.policies ?? 0} policies</span>
<span>{sub._count?.tasks ?? 0} tasks</span>
</div>
</div>
))}
</div>
</div>
)}
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
)