diff --git a/ondeck/prisma/schema.prisma b/ondeck/prisma/schema.prisma index c48dc9d..92d327c 100644 --- a/ondeck/prisma/schema.prisma +++ b/ondeck/prisma/schema.prisma @@ -122,6 +122,7 @@ model Client { designationId String? @map("designation_id") designation2Id String? @map("designation2_id") claimsAdvocateId String? @map("claims_advocate_id") + parentClientId String? @map("parent_client_id") notes String? @db.Text customFields Json @default("{}") @map("custom_fields") lastSyncedAt DateTime? @map("last_synced_at") @@ -131,18 +132,39 @@ model Client { designation Designation? @relation("ClientDesignation1", fields: [designationId], references: [id]) designation2 Designation? @relation("ClientDesignation2", fields: [designation2Id], references: [id]) claimsAdvocate User? @relation("ClientAdvocate", fields: [claimsAdvocateId], references: [id]) + parentClient Client? @relation("ClientParent", fields: [parentClientId], references: [id]) + subsidiaries Client[] @relation("ClientParent") policies Policy[] tasks Task[] policyGroups PolicyGroup[] members ClientMember[] + contacts ClientContact[] @@index([name]) @@index([designationId]) @@index([designation2Id]) @@index([claimsAdvocateId]) + @@index([parentClientId]) @@map("clients") } +model ClientContact { + id String @id @default(cuid()) + clientId String @map("client_id") + label String + name String + phone String? + email String? + notes String? @db.Text + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + client Client @relation(fields: [clientId], references: [id], onDelete: Cascade) + + @@index([clientId]) + @@map("client_contacts") +} + model ClientMember { id String @id @default(cuid()) clientId String @map("client_id") @@ -258,6 +280,7 @@ model TaskTemplate { timing TaskTiming daysOffset Int @map("days_offset") defaultPriority TaskPriority @map("default_priority") + taskGroup String? @map("task_group") isActive Boolean @default(true) @map("is_active") displayOrder Int? @map("display_order") designationId String? @map("designation_id") @@ -294,6 +317,9 @@ model Task { isAdHoc Boolean @default(false) @map("is_ad_hoc") naReason String? @map("na_reason") @db.Text cancelledReason String? @map("cancelled_reason") @db.Text + taskGroup String? @map("task_group") + imageRightFiled Boolean? @map("image_right_filed") + reminderDate DateTime? @map("reminder_date") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") diff --git a/ondeck/src/app/(dashboard)/clients/[id]/page.tsx b/ondeck/src/app/(dashboard)/clients/[id]/page.tsx index ab61e99..d480581 100644 --- a/ondeck/src/app/(dashboard)/clients/[id]/page.tsx +++ b/ondeck/src/app/(dashboard)/clients/[id]/page.tsx @@ -23,7 +23,7 @@ export default async function ClientDetailPage({ const userRoles = (session.user as any).roles || [] const canManageGroups = userRoles.includes('Admin') || userRoles.includes('Manager') - const [client, designations, policyGroups] = await Promise.all([ + const [client, designations, policyGroups, allPolicies] = await Promise.all([ prisma.client.findUnique({ where: { id }, include: { @@ -32,6 +32,17 @@ export default async function ClientDetailPage({ claimsAdvocate: { select: { id: true, displayName: true, email: true }, }, + parentClient: { + select: { id: true, name: true }, + }, + subsidiaries: { + select: { + id: true, + name: true, + _count: { select: { tasks: true, policies: true } }, + }, + orderBy: { name: 'asc' }, + }, members: { include: { user: { select: { id: true, displayName: true, email: true } }, @@ -39,9 +50,24 @@ export default async function ClientDetailPage({ orderBy: { createdAt: 'asc' as const }, }, policies: { + where: { status: 'Active' }, orderBy: { expirationDate: 'desc' }, }, tasks: { + where: (() => { + const cutoff = new Date() + cutoff.setDate(cutoff.getDate() - 7) + return { + NOT: [ + { policy: { status: { in: ['Cancelled', 'Expired', 'Non-Renewed', 'Rewritten', 'Not taken'] } } }, + { policyGroup: { renewalDate: { lt: cutoff } } }, + ], + OR: [ + { dueDate: { gte: cutoff } }, + { status: { in: ['COMPLETED', 'CANCELLED', 'NA'] } }, + ], + } + })(), include: { assignments: { include: { @@ -83,6 +109,21 @@ export default async function ClientDetailPage({ }, orderBy: { renewalDate: 'asc' }, }), + prisma.policy.findMany({ + where: { clientId: id }, + select: { + id: true, + policyNumber: true, + policyType: true, + expirationDate: true, + department: true, + carrierName: true, + writingCompanyName: true, + policyGroupId: true, + premiumAmount: true, + }, + orderBy: { expirationDate: 'desc' }, + }), ]) if (!client) { @@ -98,12 +139,18 @@ export default async function ClientDetailPage({ })), } + const allPoliciesData = allPolicies.map(policy => ({ + ...policy, + premiumAmount: policy.premiumAmount ? Number(policy.premiumAmount) : null, + })) + return (
diff --git a/ondeck/src/app/(dashboard)/tasks/page-client.tsx b/ondeck/src/app/(dashboard)/tasks/page-client.tsx index e524175..98abce7 100644 --- a/ondeck/src/app/(dashboard)/tasks/page-client.tsx +++ b/ondeck/src/app/(dashboard)/tasks/page-client.tsx @@ -22,7 +22,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select' -import { CheckSquare, Clock, AlertCircle, MessageSquare, CheckCircle2, RotateCcw, Eye, CalendarRange, ArrowRightLeft, Search, X, Building2, Plus } from 'lucide-react' +import { CheckSquare, Clock, AlertCircle, MessageSquare, CheckCircle2, RotateCcw, Eye, CalendarRange, ArrowRightLeft, Search, X, Building2, Plus, Ban } from 'lucide-react' import { Input } from '@/components/ui/input' import { formatDate } from '@/lib/utils' import { AdditionalServiceModal } from '@/components/tasks/additional-service-modal' @@ -93,24 +93,36 @@ function TaskCard({ task: initial }: { task: Task }) { const [noteStatus, setNoteStatus] = useState(initial.status) const [saving, setSaving] = useState(false) const [toggling, setToggling] = useState(false) + const [naDialogOpen, setNaDialogOpen] = useState(false) + const [naReason, setNaReason] = useState('') + const [naSubmitting, setNaSubmitting] = useState(false) + const [completeDialogOpen, setCompleteDialogOpen] = useState(false) + const [imageRightFiled, setImageRightFiled] = useState(null) + const [reminderDate, setReminderDate] = useState('') + const [completeSubmitting, setCompleteSubmitting] = useState(false) const now = new Date() const isOverdue = new Date(task.dueDate) < now && task.status !== 'COMPLETED' const isCompleted = task.status === 'COMPLETED' const handleToggleStatus = async () => { + if (!isCompleted) { + setImageRightFiled(null) + setReminderDate('') + setCompleteDialogOpen(true) + return + } setToggling(true) try { - const newStatus = isCompleted ? 'NOT_STARTED' : 'COMPLETED' const res = await fetch(`/api/tasks/${task.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ status: newStatus }), + body: JSON.stringify({ status: 'NOT_STARTED' }), }) 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') + setTask((t) => ({ ...t, status: 'NOT_STARTED' })) + setNoteStatus('NOT_STARTED') + toast.success('Task reopened') } catch (err: any) { toast.error(err.message) } finally { @@ -118,6 +130,51 @@ function TaskCard({ task: initial }: { task: Task }) { } } + const handleConfirmComplete = async () => { + setCompleteSubmitting(true) + try { + const body: any = { status: 'COMPLETED' } + if (imageRightFiled !== null) body.imageRightFiled = imageRightFiled + if (reminderDate) body.reminderDate = reminderDate + const res = await fetch(`/api/tasks/${task.id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }) + if (!res.ok) throw new Error((await res.json()).error) + setTask((t) => ({ ...t, status: 'COMPLETED' })) + setNoteStatus('COMPLETED') + setCompleteDialogOpen(false) + toast.success('Task marked complete') + } catch (err: any) { + toast.error(err.message) + } finally { + setCompleteSubmitting(false) + } + } + + const handleMarkNA = async () => { + if (!naReason.trim()) return + setNaSubmitting(true) + try { + const res = await fetch(`/api/tasks/${task.id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status: 'NA', naReason: naReason.trim() }), + }) + if (!res.ok) throw new Error((await res.json()).error) + setTask((t) => ({ ...t, status: 'NA' })) + setNoteStatus('NA') + setNaDialogOpen(false) + setNaReason('') + toast.success('Task marked N/A') + } catch (err: any) { + toast.error(err.message) + } finally { + setNaSubmitting(false) + } + } + const handleAddNote = async () => { if (!newNote.trim()) return setSaving(true) @@ -231,6 +288,17 @@ function TaskCard({ task: initial }: { task: Task }) { : <> Complete } + {task.status !== 'COMPLETED' && task.status !== 'NA' && task.status !== 'CANCELLED' && ( + + )} + + + +
+

Set a reminder? (optional)

+ setReminderDate(e.target.value)} + min={new Date().toISOString().split('T')[0]} + className="w-48" + /> +
+ + + + + + + + + {/* N/A Dialog */} + + + + Mark as N/A + +

Please provide a reason why this task is not applicable.

+