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:
parent
851487a8dd
commit
56664068f8
1 changed files with 221 additions and 209 deletions
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue