Task card: right sidebar with Due/Status/Level info + 2x2 action grid

This commit is contained in:
lorentz 2026-04-10 14:35:51 +00:00
parent bc240c79c1
commit f2192cb70b

View file

@ -198,6 +198,13 @@ function TaskCard({ task: initial }: { task: Task }) {
}
}
const levelLabel = task.policyGroup ? 'Group' : task.policy ? 'Policy' : 'Client'
const levelColor = task.policyGroup
? 'bg-purple-500/10 text-purple-700 dark:text-purple-400'
: task.policy
? 'bg-blue-500/10 text-blue-700 dark:text-blue-400'
: 'bg-muted text-muted-foreground'
return (
<div
className={`rounded-lg border transition-shadow hover:shadow-md ${
@ -208,71 +215,88 @@ function TaskCard({ task: initial }: { task: Task }) {
: 'border-border'
}`}
>
{/* Header: title + badges + actions */}
<div className="flex items-center gap-3 px-4 pt-3 pb-2">
<div className="flex-1 min-w-0 flex flex-wrap items-center gap-2">
<h3 className={`font-semibold truncate ${isCompleted ? 'line-through text-muted-foreground' : ''}`}>
{task.title}
</h3>
<StatusBadge status={task.status} />
<PriorityDot priority={task.priority} />
{task.policyGroup ? (
<span className="inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium bg-purple-500/10 text-purple-700 dark:text-purple-400">Group</span>
) : task.policy ? (
<span className="inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium bg-blue-500/10 text-blue-700 dark:text-blue-400">Policy</span>
) : (
<span className="inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium bg-muted text-muted-foreground">Client</span>
)}
</div>
<div className="flex items-center gap-1 shrink-0">
<Button size="icon" variant="ghost" onClick={() => setEditOpen(true)} className="h-7 w-7 text-muted-foreground hover:text-foreground" title="Edit task">
<Pencil className="h-3.5 w-3.5" />
</Button>
<Button
size="sm"
variant={isCompleted ? 'outline' : 'default'}
onClick={handleToggleStatus}
disabled={toggling}
className="h-7 gap-1 text-xs px-2.5"
>
{isCompleted
? <><RotateCcw className="h-3 w-3" /> Reopen</>
: <><CheckCircle2 className="h-3 w-3" /> Complete</>
}
</Button>
{task.status !== 'COMPLETED' && task.status !== 'NA' && task.status !== 'CANCELLED' && (
<Button size="sm" variant="ghost" onClick={() => setNaDialogOpen(true)} className="h-7 gap-1 text-xs px-2 text-muted-foreground hover:text-foreground" title="Mark as N/A">
<Ban className="h-3 w-3" /> N/A
</Button>
)}
<Button
size="sm"
variant={noteOpen ? 'secondary' : 'ghost'}
onClick={() => setNoteOpen((o) => !o)}
className="h-7 gap-1 text-xs px-2"
>
<MessageSquare className="h-3 w-3" />
{notes.length > 0 ? notes.length : 'Notes'}
</Button>
</div>
</div>
{/* Meta: structured two-row grid */}
<div className="px-4 pb-3 text-[13px] text-muted-foreground space-y-1">
<div className="flex items-center gap-x-6 gap-y-0.5 flex-wrap">
{task.client && (
<span><span className="text-muted-foreground/70">Client</span> <span className="text-foreground font-medium">{task.client.name}</span></span>
)}
<span><span className="text-muted-foreground/70">Policy #</span> <span className="text-foreground font-medium">{task.policy?.policyNumber || '—'}</span></span>
<span><span className="text-muted-foreground/70">Type</span> <span className="text-foreground font-medium">{task.policy?.policyType || '—'}</span></span>
<span><span className="text-muted-foreground/70">Renewal</span> <span className="text-foreground font-medium" suppressHydrationWarning>{task.policy?.expirationDate ? formatRenewalDate(task.policy.expirationDate) : '—'}</span></span>
<span><span className="text-muted-foreground/70">Due</span> <span className={`font-medium ${isOverdue ? 'text-red-500' : 'text-foreground'}`} suppressHydrationWarning>{formatDate(task.dueDate)}</span></span>
</div>
{task.assignments.length > 0 && (
<div className="text-[12px]">
Assigned to {task.assignments.map((a) => a.user.displayName || a.user.email).join(', ')}
<div className="flex">
{/* Left: main content */}
<div className="flex-1 min-w-0 px-4 py-3 space-y-1.5">
<div className="flex items-center gap-2">
<PriorityDot priority={task.priority} />
<h3 className={`font-semibold truncate ${isCompleted ? 'line-through text-muted-foreground' : ''}`}>
{task.title}
</h3>
</div>
)}
<div className="flex flex-wrap gap-x-5 gap-y-0.5 text-[13px]">
{task.client && (
<span><span className="text-muted-foreground">Client</span> <span className="text-foreground font-medium">{task.client.name}</span></span>
)}
<span><span className="text-muted-foreground">Policy #</span> <span className="text-foreground font-medium">{task.policy?.policyNumber || '—'}</span></span>
<span><span className="text-muted-foreground">Type</span> <span className="text-foreground font-medium">{task.policy?.policyType || '—'}</span></span>
<span><span className="text-muted-foreground">Renewal</span> <span className="text-foreground font-medium" suppressHydrationWarning>{task.policy?.expirationDate ? formatRenewalDate(task.policy.expirationDate) : '—'}</span></span>
</div>
{task.assignments.length > 0 && (
<p className="text-[12px] text-muted-foreground">
Assigned to {task.assignments.map((a) => a.user.displayName || a.user.email).join(', ')}
</p>
)}
</div>
{/* Right sidebar */}
<div className="shrink-0 w-[160px] border-l border-border flex flex-col">
{/* Info: Due / Status / Level */}
<div className="px-3 py-2.5 space-y-1 text-[12px]">
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Due</span>
<span className={`font-medium ${isOverdue ? 'text-red-500' : 'text-foreground'}`} suppressHydrationWarning>{formatDate(task.dueDate)}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Status</span>
<StatusBadge status={task.status} />
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Level</span>
<span className={`inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px] font-medium ${levelColor}`}>{levelLabel}</span>
</div>
</div>
{/* Actions: 2×2 grid */}
<div className="border-t border-border grid grid-cols-2 divide-x divide-border">
<button
onClick={handleToggleStatus}
disabled={toggling}
className="flex items-center justify-center gap-1 py-2 text-[11px] font-medium hover:bg-muted transition-colors disabled:opacity-50"
>
{isCompleted
? <><RotateCcw className="h-3 w-3" /> Reopen</>
: <><CheckCircle2 className="h-3 w-3" /> Complete</>
}
</button>
<button
onClick={() => setEditOpen(true)}
className="flex items-center justify-center gap-1 py-2 text-[11px] font-medium text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
>
<Pencil className="h-3 w-3" /> Edit
</button>
</div>
<div className="border-t border-border grid grid-cols-2 divide-x divide-border">
{task.status !== 'COMPLETED' && task.status !== 'NA' && task.status !== 'CANCELLED' ? (
<button
onClick={() => setNaDialogOpen(true)}
className="flex items-center justify-center gap-1 py-2 text-[11px] font-medium text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
>
<Ban className="h-3 w-3" /> N/A
</button>
) : (
<span />
)}
<button
onClick={() => setNoteOpen((o) => !o)}
className={`flex items-center justify-center gap-1 py-2 text-[11px] font-medium transition-colors ${
noteOpen ? 'bg-secondary text-foreground' : 'text-muted-foreground hover:text-foreground hover:bg-muted'
}`}
>
<MessageSquare className="h-3 w-3" />
{notes.length > 0 ? `Notes (${notes.length})` : 'Notes'}
</button>
</div>
</div>
</div>
{/* Completion Dialog */}