From 4aa19e6a21dfe1dc1e3e86f942171445a704cda1 Mon Sep 17 00:00:00 2001
From: lorentz
Date: Wed, 25 Mar 2026 14:57:21 +0000
Subject: [PATCH] Tasks: multi-note system, claims-first viewer dropdown,
TaskNote schema
---
.claude/settings.local.json | 8 +-
dev/Horizon_NewFeatures.md | 51 ++
.../migration.sql | 4 +
ondeck/prisma/schema.prisma | 16 +
.../src/app/(dashboard)/admin/users/page.tsx | 12 +-
ondeck/src/app/(dashboard)/manager/page.tsx | 176 +----
.../src/app/(dashboard)/tasks/page-client.tsx | 177 +++--
ondeck/src/app/(dashboard)/tasks/page.tsx | 7 +-
.../app/api/admin/users/entra-sync/route.ts | 118 +++
.../app/api/admin/users/website-sync/route.ts | 157 ++++
.../src/app/api/clients/[id]/tasks/route.ts | 34 +
.../src/app/api/dashboard/workload/route.ts | 28 +-
ondeck/src/app/api/tasks/[id]/notes/route.ts | 82 ++
ondeck/src/app/api/tasks/route.ts | 6 +
ondeck/src/components/admin/user-manager.tsx | 733 ++++++++++++++----
.../src/components/clients/client-detail.tsx | 18 +-
.../clients/policy-group-manager.tsx | 3 +
.../components/dashboard/workload-kpis.tsx | 13 +-
.../manager/manager-page-client.tsx | 215 +++++
.../manager/team-members-by-department.tsx | 8 +-
20 files changed, 1478 insertions(+), 388 deletions(-)
create mode 100644 dev/Horizon_NewFeatures.md
create mode 100644 ondeck/prisma/migrations/20260325112534_add_user_photo_title_office/migration.sql
create mode 100644 ondeck/src/app/api/admin/users/entra-sync/route.ts
create mode 100644 ondeck/src/app/api/admin/users/website-sync/route.ts
create mode 100644 ondeck/src/app/api/clients/[id]/tasks/route.ts
create mode 100644 ondeck/src/app/api/tasks/[id]/notes/route.ts
create mode 100644 ondeck/src/components/manager/manager-page-client.tsx
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index b17be45..0d37522 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -4,7 +4,13 @@
"Bash(ssh-keygen:*)",
"Bash(chmod:*)",
"Bash(git init:*)",
- "Bash(git add:*)"
+ "Bash(git add:*)",
+ "Bash(git rm:*)",
+ "Bash(git branch:*)",
+ "Bash(git remote add:*)",
+ "Bash(git commit:*)",
+ "Bash(git config:*)",
+ "Bash(git push:*)"
]
}
}
diff --git a/dev/Horizon_NewFeatures.md b/dev/Horizon_NewFeatures.md
new file mode 100644
index 0000000..a5ed975
--- /dev/null
+++ b/dev/Horizon_NewFeatures.md
@@ -0,0 +1,51 @@
+# Horizon
+
+This is a web application that syncs insurance policy data from an insurance brokerage's line of business system to a local database (postgres) and then allows for additional functionality:
+
+- Client Filtering
+
+ - Designation
+
+ - Claims Assignment
+
+ - AE
+
+- Policy Grouping
+
+ - Groups don't cross clients, they are meant to align work tasks in an efficient manner. For example pulling a clients credit report is a per client task not a per policy
+
+ -
+
+- Task Assignment
+
+ - Policies may need to retain individual tasks even if they are a member group.
+
+ - Tasks should be able to be assigned to a policy group or individual policy
+
+ - Tasks should be automatically assigned based on a date trigger: Group Date, or Policy Expiration Date
+
+ - Tasks should only be assigned automatically after a policy exists in the system for 20 days
+
+ - There should be a manual assignment in the ui that let's users assign tasks en masse using filters (policies for client A, new policies with no assignment, or just manual checkbox selection of multiple policies)
+
+- Logging
+
+ - Authentication
+
+ - Task Activities
+
+ - Assignment
+
+ - Changes
+
+ - Removal
+
+ - Completion
+
+ - User Activities
+
+ - Creation / Deletion
+
+ - Role Change (Admin/Manager/Advocate)
+
+
diff --git a/ondeck/prisma/migrations/20260325112534_add_user_photo_title_office/migration.sql b/ondeck/prisma/migrations/20260325112534_add_user_photo_title_office/migration.sql
new file mode 100644
index 0000000..e9dda49
--- /dev/null
+++ b/ondeck/prisma/migrations/20260325112534_add_user_photo_title_office/migration.sql
@@ -0,0 +1,4 @@
+-- AlterTable
+ALTER TABLE "users" ADD COLUMN "job_title" TEXT,
+ADD COLUMN "office" TEXT,
+ADD COLUMN "photo_url" TEXT;
diff --git a/ondeck/prisma/schema.prisma b/ondeck/prisma/schema.prisma
index 62b6658..89b8688 100644
--- a/ondeck/prisma/schema.prisma
+++ b/ondeck/prisma/schema.prisma
@@ -31,6 +31,7 @@ model User {
createdTasks Task[] @relation("TaskCreatedBy")
completedTasks Task[] @relation("TaskCompletedBy")
taskAssignments TaskAssignment[]
+ taskNotes TaskNote[] @relation("TaskNoteAuthor")
createdTemplates TaskTemplate[]
syncLogs SyncLog[]
auditLogs AuditLog[]
@@ -302,6 +303,7 @@ model Task {
creator User? @relation("TaskCreatedBy", fields: [createdBy], references: [id])
completer User? @relation("TaskCompletedBy", fields: [completedBy], references: [id])
assignments TaskAssignment[]
+ taskNotes TaskNote[]
@@index([clientId])
@@index([policyId])
@@ -325,6 +327,20 @@ model TaskAssignment {
@@map("task_assignments")
}
+model TaskNote {
+ id String @id @default(cuid())
+ taskId String @map("task_id")
+ userId String @map("user_id")
+ content String @db.Text
+ createdAt DateTime @default(now()) @map("created_at")
+
+ task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
+ user User @relation("TaskNoteAuthor", fields: [userId], references: [id])
+
+ @@index([taskId])
+ @@map("task_notes")
+}
+
// ============================================
// Sync & System Management
// ============================================
diff --git a/ondeck/src/app/(dashboard)/admin/users/page.tsx b/ondeck/src/app/(dashboard)/admin/users/page.tsx
index dae6acb..5f02944 100644
--- a/ondeck/src/app/(dashboard)/admin/users/page.tsx
+++ b/ondeck/src/app/(dashboard)/admin/users/page.tsx
@@ -19,7 +19,17 @@ export default async function UsersPage() {
const [users, roles] = await Promise.all([
prisma.user.findMany({
orderBy: { displayName: 'asc' },
- include: {
+ select: {
+ id: true,
+ email: true,
+ displayName: true,
+ department: true,
+ jobTitle: true,
+ photoUrl: true,
+ office: true,
+ isActive: true,
+ lastLoginAt: true,
+ createdAt: true,
userRoles: {
include: {
role: true,
diff --git a/ondeck/src/app/(dashboard)/manager/page.tsx b/ondeck/src/app/(dashboard)/manager/page.tsx
index b1286c6..fe9b9c2 100644
--- a/ondeck/src/app/(dashboard)/manager/page.tsx
+++ b/ondeck/src/app/(dashboard)/manager/page.tsx
@@ -2,11 +2,7 @@ import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { redirect } from 'next/navigation'
import { prisma } from '@/lib/db'
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
-import { Badge } from '@/components/ui/badge'
-import { Users, CheckSquare, TrendingUp, AlertCircle, Clock } from 'lucide-react'
-import { WorkloadKPIs } from '@/components/dashboard/workload-kpis'
-import { TeamMembersByDepartment } from '@/components/manager/team-members-by-department'
+import { ManagerPageClient } from '@/components/manager/manager-page-client'
export default async function ManagerPage() {
const session = await getServerSession(authOptions)
@@ -23,7 +19,7 @@ export default async function ManagerPage() {
}
// Fetch team statistics
- const [totalUsers, activeUsers, totalTasks, completedTasks, overdueTasks] = await Promise.all([
+ const [, activeUsers, totalTasks, completedTasks, overdueTasks] = await Promise.all([
prisma.user.count(),
prisma.user.count({ where: { isActive: true } }),
prisma.task.count(),
@@ -95,166 +91,14 @@ export default async function ManagerPage() {
}
})
- const completionRate = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0
-
return (
-
- {/* Header */}
-
-
- Team Management
-
-
- Monitor team performance and task assignments
-
-
-
- {/* Stats Grid */}
-
-
-
- Team Members
-
-
-
- {activeUsers}
-
- Active users
-
-
-
-
-
-
- Total Tasks
-
-
-
- {totalTasks}
-
- All team tasks
-
-
-
-
-
-
- Completed
-
-
-
- {completedTasks}
-
- {completionRate}% completion rate
-
-
-
-
-
-
- Overdue
-
-
-
- {overdueTasks}
-
- Require attention
-
-
-
-
-
-
- In Progress
-
-
-
- {totalTasks - completedTasks}
-
- Active tasks
-
-
-
-
-
- {/* Team Members & Recent Tasks */}
-
- {/* Team Members */}
-
-
-
-
- Team Members
-
-
-
-
-
-
-
- {/* Recent Tasks */}
-
-
-
-
- Recent Tasks
-
-
-
- {recentTasks.length === 0 ? (
-
- No tasks created yet
-
- ) : (
-
- {recentTasks.map((task) => {
- const getStatusColor = (status: string) => {
- switch (status) {
- case 'COMPLETED':
- return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300'
- case 'IN_PROGRESS':
- return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300'
- case 'BLOCKED':
- return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300'
- default:
- return 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300'
- }
- }
-
- return (
-
-
-
-
{task.title}
- {task.client && (
-
- {task.client.name}
-
- )}
-
-
- {task.status.replace('_', ' ')}
-
-
- {task.assignments.length > 0 && (
-
- Assigned to: {task.assignments.map(a => a.user.displayName).join(', ')}
-
- )}
-
- )
- })}
-
- )}
-
-
-
-
- {/* Workload KPIs Section */}
-
-
+
)
}
diff --git a/ondeck/src/app/(dashboard)/tasks/page-client.tsx b/ondeck/src/app/(dashboard)/tasks/page-client.tsx
index 3744d84..abf20ba 100644
--- a/ondeck/src/app/(dashboard)/tasks/page-client.tsx
+++ b/ondeck/src/app/(dashboard)/tasks/page-client.tsx
@@ -9,7 +9,9 @@ import { Textarea } from '@/components/ui/textarea'
import {
Select,
SelectContent,
+ SelectGroup,
SelectItem,
+ SelectLabel,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
@@ -18,6 +20,8 @@ import { formatDate } from '@/lib/utils'
interface TaskUser { displayName: string | null; email: string }
interface TaskAssignment { id: string; user: TaskUser }
+interface NoteUser { id: string; displayName: string | null; email: string }
+interface TaskNote { id: string; content: string; createdAt: string; user: NoteUser }
interface Task {
id: string
title: string
@@ -30,9 +34,10 @@ interface Task {
client: { id: string; name: string } | null
policy: { id: string; policyNumber: string | null; expirationDate: string | Date } | null
assignments: TaskAssignment[]
+ taskNotes: TaskNote[]
}
-interface SimpleUser { id: string; displayName: string | null; email: string }
+interface SimpleUser { id: string; displayName: string | null; email: string; department?: string | null }
interface TasksClientProps {
initialTasks: Task[]
@@ -72,8 +77,9 @@ function PriorityDot({ priority }: { priority: string }) {
function TaskCard({ task: initial }: { task: Task }) {
const [task, setTask] = useState(initial)
+ const [notes, setNotes] = useState(initial.taskNotes ?? [])
const [noteOpen, setNoteOpen] = useState(false)
- const [noteText, setNoteText] = useState(initial.notes ?? '')
+ const [newNote, setNewNote] = useState('')
const [noteStatus, setNoteStatus] = useState(initial.status)
const [saving, setSaving] = useState(false)
const [toggling, setToggling] = useState(false)
@@ -82,26 +88,18 @@ function TaskCard({ task: initial }: { task: Task }) {
const isOverdue = new Date(task.dueDate) < now && task.status !== 'COMPLETED'
const isCompleted = task.status === 'COMPLETED'
- const patch = async (body: object) => {
- const res = await fetch(`/api/tasks/${task.id}`, {
- method: 'PATCH',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(body),
- })
- if (!res.ok) {
- const data = await res.json()
- throw new Error(data.error || 'Failed to update task')
- }
- return res.json()
- }
-
const handleToggleStatus = async () => {
setToggling(true)
try {
const newStatus = isCompleted ? 'NOT_STARTED' : 'COMPLETED'
- const updated = await patch({ status: newStatus })
- setTask((t) => ({ ...t, status: updated.status, notes: updated.notes }))
- setNoteStatus(updated.status)
+ const res = await fetch(`/api/tasks/${task.id}`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ status: newStatus }),
+ })
+ if (!res.ok) throw new Error((await res.json()).error)
+ setTask((t) => ({ ...t, status: newStatus }))
+ setNoteStatus(newStatus)
toast.success(newStatus === 'COMPLETED' ? 'Task marked complete' : 'Task reopened')
} catch (err: any) {
toast.error(err.message)
@@ -110,15 +108,21 @@ function TaskCard({ task: initial }: { task: Task }) {
}
}
- const handleSaveNote = async () => {
+ const handleAddNote = async () => {
+ if (!newNote.trim()) return
setSaving(true)
try {
- const body: any = { notes: noteText }
- if (noteStatus !== task.status) body.status = noteStatus
- const updated = await patch(body)
- setTask((t) => ({ ...t, status: updated.status, notes: updated.notes }))
- setNoteOpen(false)
- toast.success('Note saved')
+ const res = await fetch(`/api/tasks/${task.id}/notes`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ content: newNote.trim(), status: noteStatus !== task.status ? noteStatus : undefined }),
+ })
+ const data = await res.json()
+ if (!res.ok) throw new Error(data.error)
+ setNotes((prev) => [...prev, data])
+ if (noteStatus !== task.status) setTask((t) => ({ ...t, status: noteStatus }))
+ setNewNote('')
+ toast.success('Note added')
} catch (err: any) {
toast.error(err.message)
} finally {
@@ -145,7 +149,11 @@ function TaskCard({ task: initial }: { task: Task }) {
- {task.notes && }
+ {notes.length > 0 && (
+
+ {notes.length}
+
+ )}
{task.description && (
@@ -180,15 +188,6 @@ function TaskCard({ task: initial }: { task: Task }) {
{task.assignments.map((a) => a.user.displayName || a.user.email).join(', ')}
)}
-
- {/* Note — always visible */}
-
- Note:
- {task.notes
- ? {task.notes}
- : No note — click Note to add one
- }
-
{/* Action buttons */}
@@ -207,42 +206,64 @@ function TaskCard({ task: initial }: { task: Task }) {
- {/* Note panel */}
+ {/* Notes panel */}
{noteOpen && (
-