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 && ( -
-