From 970ffe28645fd22808bad47e0a4356d74719039a Mon Sep 17 00:00:00 2001 From: lorentz Date: Tue, 19 May 2026 03:49:39 +0000 Subject: [PATCH] Feature: task origin/business logic visibility for managers & admins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Task cards show an info (ⓘ) icon in the Level row for privileged users Tooltip: e.g. '90 days before renewal · Group level · Template: Request Loss Runs' - Edit modal shows a read-only Task Origin callout at top for privileged users - Added template include to all task queries (tasks page, client page, client tasks API) - Also applied the open-task cutoff fix to /api/clients/[id]/tasks/route.ts (same bug as Luke's) --- .../src/app/(dashboard)/clients/[id]/page.tsx | 3 ++ .../src/app/(dashboard)/tasks/page-client.tsx | 41 +++++++++++++++++-- ondeck/src/app/(dashboard)/tasks/page.tsx | 1 + .../src/app/api/clients/[id]/tasks/route.ts | 14 +++++-- .../src/components/clients/client-detail.tsx | 1 + ondeck/src/components/tasks/task-card.tsx | 38 +++++++++++++++-- .../src/components/tasks/task-edit-modal.tsx | 31 +++++++++++++- 7 files changed, 117 insertions(+), 12 deletions(-) diff --git a/ondeck/src/app/(dashboard)/clients/[id]/page.tsx b/ondeck/src/app/(dashboard)/clients/[id]/page.tsx index 96e9e13..a1570a9 100644 --- a/ondeck/src/app/(dashboard)/clients/[id]/page.tsx +++ b/ondeck/src/app/(dashboard)/clients/[id]/page.tsx @@ -97,6 +97,9 @@ export default async function ClientDetailPage({ policyGroup: { select: { id: true, name: true, renewalDate: true }, }, + template: { + select: { id: true, name: true, level: true }, + }, assignments: { include: { user: { diff --git a/ondeck/src/app/(dashboard)/tasks/page-client.tsx b/ondeck/src/app/(dashboard)/tasks/page-client.tsx index c027a88..fc3bdcb 100644 --- a/ondeck/src/app/(dashboard)/tasks/page-client.tsx +++ b/ondeck/src/app/(dashboard)/tasks/page-client.tsx @@ -21,7 +21,7 @@ import { SelectValue, } from '@/components/ui/select' import { UserSelectContent } from '@/components/ui/user-select-content' -import { CheckSquare, Clock, AlertCircle, MessageSquare, CheckCircle2, RotateCcw, Eye, CalendarRange, ArrowRightLeft, Search, X, Building2, Plus, Ban, ArrowUpDown, ArrowUp, ArrowDown, Filter, Pencil, ChevronDown } from 'lucide-react' +import { CheckSquare, Clock, AlertCircle, MessageSquare, CheckCircle2, RotateCcw, Eye, CalendarRange, ArrowRightLeft, Search, X, Building2, Plus, Ban, ArrowUpDown, ArrowUp, ArrowDown, Filter, Pencil, ChevronDown, Info } from 'lucide-react' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { Input } from '@/components/ui/input' import { formatDate, formatRenewalDate } from '@/lib/utils' @@ -47,6 +47,11 @@ interface Task { policyGroup: { id: string; name: string | null; renewalDate: string | Date | null } | null assignments: TaskAssignment[] taskNotes: TaskNote[] + daysOffset?: number | null + timing?: string | null + templateId?: string | null + isAdHoc?: boolean | null + template?: { id?: string; name: string; level?: string } | null } interface SimpleUser { id: string; displayName: string | null; email: string; department?: string | null } @@ -87,7 +92,20 @@ function PriorityDot({ priority }: { priority: string }) { ) } -function TaskCard({ task: initial }: { task: Task }) { +function buildOriginLabel(task: Task): string { + if (task.isAdHoc) return 'Ad hoc task, added manually' + if (!task.templateId) return 'Template-generated task (template details unavailable)' + const days = task.daysOffset ?? 0 + const absDays = Math.abs(days) + const timing = task.timing === 'POST_RENEWAL' + ? `${absDays} day${absDays !== 1 ? 's' : ''} after renewal` + : `${absDays} day${absDays !== 1 ? 's' : ''} before renewal` + const level = task.policyGroup ? 'Group level' : task.policy ? 'Policy level' : 'Client level' + const templateName = task.template?.name ?? task.title + return `${timing} · ${level} · Template: ${templateName}` +} + +function TaskCard({ task: initial, isPrivileged = false }: { task: Task; isPrivileged?: boolean }) { const [task, setTask] = useState(initial) const [notes, setNotes] = useState(initial.taskNotes ?? []) const [noteOpen, setNoteOpen] = useState(false) @@ -403,8 +421,22 @@ function TaskCard({ task: initial }: { task: Task }) {
)} {/* Row 3: Level | Edit */} -
+
{levelLabel} + {isPrivileged && ( + + + + + + + + + {buildOriginLabel(task)} + + + + )}
@@ -516,6 +548,7 @@ function TaskCard({ task: initial }: { task: Task }) { open={editOpen} onOpenChange={setEditOpen} task={task as unknown as EditableTask} + isPrivileged={isPrivileged} onUpdated={(updated) => { setTask((prev) => ({ ...prev, ...updated })) }} @@ -1046,7 +1079,7 @@ export function TasksClient({ initialTasks, currentUserId, isPrivileged, users } ) : (
{visibleTasks.map((task) => ( - + ))}
)} diff --git a/ondeck/src/app/(dashboard)/tasks/page.tsx b/ondeck/src/app/(dashboard)/tasks/page.tsx index 5b3066d..7881fa3 100644 --- a/ondeck/src/app/(dashboard)/tasks/page.tsx +++ b/ondeck/src/app/(dashboard)/tasks/page.tsx @@ -58,6 +58,7 @@ export default async function TasksPage() { client: { select: { id: true, name: true } }, policy: { select: { id: true, policyNumber: true, policyType: true, expirationDate: true, carrierName: true, writingCompanyName: true } }, policyGroup: { select: { id: true, name: true, renewalDate: true } }, + template: { select: { id: true, name: true, level: true } }, assignments: { include: { user: { select: { displayName: true, email: true } } }, }, diff --git a/ondeck/src/app/api/clients/[id]/tasks/route.ts b/ondeck/src/app/api/clients/[id]/tasks/route.ts index 53697db..cbfc3c5 100644 --- a/ondeck/src/app/api/clients/[id]/tasks/route.ts +++ b/ondeck/src/app/api/clients/[id]/tasks/route.ts @@ -35,12 +35,17 @@ export async function GET( { policyGroupId: null }, { policyGroup: { renewalDate: { gte: cutoff } } }, ] - // Only show recent or terminal tasks + // Keep ALL open tasks regardless of age; only hide ancient terminal tasks where.AND = [ { OR: [ - { dueDate: { gte: cutoff } }, - { status: { in: ['COMPLETED', 'CANCELLED', 'NA'] } }, + { status: { notIn: ['COMPLETED', 'CANCELLED', 'NA'] } }, + { + AND: [ + { status: { in: ['COMPLETED', 'CANCELLED', 'NA'] } }, + { dueDate: { gte: cutoff } }, + ], + }, ], }, ] @@ -67,6 +72,9 @@ export async function GET( policyGroup: { select: { id: true, name: true, renewalDate: true }, }, + template: { + select: { id: true, name: true, level: true }, + }, taskNotes: { include: { user: { select: { id: true, displayName: true, email: true } } }, orderBy: { createdAt: 'asc' }, diff --git a/ondeck/src/components/clients/client-detail.tsx b/ondeck/src/components/clients/client-detail.tsx index 13753eb..052ca99 100644 --- a/ondeck/src/components/clients/client-detail.tsx +++ b/ondeck/src/components/clients/client-detail.tsx @@ -671,6 +671,7 @@ export function ClientDetail({ client, designations, policyGroups = [], allPolic key={task.id} task={task} onUpdated={() => refreshTasks()} + isPrivileged={canManageGroups} /> )) ) diff --git a/ondeck/src/components/tasks/task-card.tsx b/ondeck/src/components/tasks/task-card.tsx index b997be8..7a81a17 100644 --- a/ondeck/src/components/tasks/task-card.tsx +++ b/ondeck/src/components/tasks/task-card.tsx @@ -20,7 +20,7 @@ import { SelectValue, } from '@/components/ui/select' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' -import { MessageSquare, CheckCircle2, RotateCcw, Ban, Pencil, ChevronDown, X } from 'lucide-react' +import { MessageSquare, CheckCircle2, RotateCcw, Ban, Pencil, ChevronDown, X, Info } from 'lucide-react' import { formatDate, formatRenewalDate } from '@/lib/utils' import Link from 'next/link' import { TaskEditModal, type EditableTask } from '@/components/tasks/task-edit-modal' @@ -42,6 +42,10 @@ export interface TaskCardTask { isAdHoc?: boolean taskGroup?: string | null anchorGroup?: { id: string; name: string; renewalDate: string | Date } | null + daysOffset?: number | null + timing?: string | null + templateId?: string | null + template?: { id?: string; name: string; level?: string } | null } function StatusBadge({ status }: { status: string }) { @@ -73,13 +77,27 @@ function PriorityDot({ priority }: { priority: string }) { ) } +function buildOriginLabel(task: TaskCardTask): string { + if (task.isAdHoc) return 'Ad hoc task, added manually' + if (!task.templateId) return 'Template-generated task (template details unavailable)' + const days = task.daysOffset ?? 0 + const absDays = Math.abs(days) + const timing = task.timing === 'POST_RENEWAL' + ? `${absDays} day${absDays !== 1 ? 's' : ''} after renewal` + : `${absDays} day${absDays !== 1 ? 's' : ''} before renewal` + const level = task.policyGroup ? 'Group level' : task.policy ? 'Policy level' : 'Client level' + const templateName = task.template?.name ?? task.title + return `${timing} · ${level} · Template: ${templateName}` +} + interface TaskCardProps { task: TaskCardTask onUpdated?: (updated: Partial) => void showClient?: boolean + isPrivileged?: boolean } -export function TaskCard({ task: initial, onUpdated, showClient = false }: TaskCardProps) { +export function TaskCard({ task: initial, onUpdated, showClient = false, isPrivileged = false }: TaskCardProps) { const [task, setTask] = useState(initial) const [notes, setNotes] = useState(initial.taskNotes ?? []) const [noteOpen, setNoteOpen] = useState(false) @@ -406,8 +424,22 @@ export function TaskCard({ task: initial, onUpdated, showClient = false }: TaskC
)} {/* Row 3: Level | Edit */} -
+
{levelLabel} + {isPrivileged && ( + + + + + + + + + {buildOriginLabel(task)} + + + + )}
diff --git a/ondeck/src/components/tasks/task-edit-modal.tsx b/ondeck/src/components/tasks/task-edit-modal.tsx index 88778ec..7d3c230 100644 --- a/ondeck/src/components/tasks/task-edit-modal.tsx +++ b/ondeck/src/components/tasks/task-edit-modal.tsx @@ -20,7 +20,7 @@ import { SelectValue, } from '@/components/ui/select' import { UserSelectContent } from '@/components/ui/user-select-content' -import { Pencil } from 'lucide-react' +import { Pencil, Info } from 'lucide-react' interface SimpleUser { id: string @@ -56,6 +56,24 @@ export interface EditableTask { policy?: { id: string; policyNumber: string | null; policyType: string | null; expirationDate?: string | Date | null } | null policyGroup?: { id: string; name: string | null; renewalDate?: string | Date | null } | null assignments: Array<{ id?: string; user: { id?: string; displayName: string | null; email: string }; userId?: string }> + daysOffset?: number | null + timing?: string | null + templateId?: string | null + isAdHoc?: boolean | null + template?: { id?: string; name: string; level?: string } | null +} + +function buildOriginLabel(task: EditableTask): string { + if (task.isAdHoc) return 'Ad hoc task, added manually' + if (!task.templateId) return 'Template-generated task (template details unavailable)' + const days = task.daysOffset ?? 0 + const absDays = Math.abs(days) + const timing = task.timing === 'POST_RENEWAL' + ? `${absDays} day${absDays !== 1 ? 's' : ''} after renewal` + : `${absDays} day${absDays !== 1 ? 's' : ''} before renewal` + const level = task.policyGroupId ? 'Group level' : task.policyId ? 'Policy level' : 'Client level' + const templateName = task.template?.name ?? task.title + return `${timing} · ${level} · Template: ${templateName}` } interface TaskEditModalProps { @@ -63,6 +81,7 @@ interface TaskEditModalProps { onOpenChange: (open: boolean) => void task: EditableTask onUpdated: (updated: any) => void + isPrivileged?: boolean } const PRIORITIES = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] @@ -74,7 +93,7 @@ const DEPARTMENTS = [ { value: 'OTHER', label: 'Other' }, ] -export function TaskEditModal({ open, onOpenChange, task, onUpdated }: TaskEditModalProps) { +export function TaskEditModal({ open, onOpenChange, task, onUpdated, isPrivileged = false }: TaskEditModalProps) { const [title, setTitle] = useState('') const [description, setDescription] = useState('') const [priority, setPriority] = useState('MEDIUM') @@ -236,6 +255,14 @@ export function TaskEditModal({ open, onOpenChange, task, onUpdated }: TaskEditM
+ {/* Task Origin — managers/admins only */} + {isPrivileged && ( +
+ + {buildOriginLabel(task)} +
+ )} + {/* Title */}