diff --git a/ondeck/prisma/schema.prisma b/ondeck/prisma/schema.prisma index 89b8688..c48dc9d 100644 --- a/ondeck/prisma/schema.prisma +++ b/ondeck/prisma/schema.prisma @@ -291,6 +291,7 @@ model Task { completedAt DateTime? @map("completed_at") completedBy String? @map("completed_by") notes String? @db.Text + isAdHoc Boolean @default(false) @map("is_ad_hoc") naReason String? @map("na_reason") @db.Text cancelledReason String? @map("cancelled_reason") @db.Text createdAt DateTime @default(now()) @map("created_at") diff --git a/ondeck/src/app/(dashboard)/tasks/page-client.tsx b/ondeck/src/app/(dashboard)/tasks/page-client.tsx index 293b5d2..6998b93 100644 --- a/ondeck/src/app/(dashboard)/tasks/page-client.tsx +++ b/ondeck/src/app/(dashboard)/tasks/page-client.tsx @@ -22,9 +22,10 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select' -import { CheckSquare, Clock, AlertCircle, MessageSquare, CheckCircle2, RotateCcw, Eye, CalendarRange, ArrowRightLeft, Search, X, Building2 } from 'lucide-react' +import { CheckSquare, Clock, AlertCircle, MessageSquare, CheckCircle2, RotateCcw, Eye, CalendarRange, ArrowRightLeft, Search, X, Building2, Plus } from 'lucide-react' import { Input } from '@/components/ui/input' import { formatDate } from '@/lib/utils' +import { AdditionalServiceModal } from '@/components/tasks/additional-service-modal' interface TaskUser { displayName: string | null; email: string } interface TaskAssignment { id: string; user: TaskUser } @@ -290,6 +291,7 @@ export function TasksClient({ initialTasks, currentUserId, isPrivileged, users } const [transferOpen, setTransferOpen] = useState(false) const [transferTo, setTransferTo] = useState('') const [transferring, setTransferring] = useState(false) + const [additionalServiceOpen, setAdditionalServiceOpen] = useState(false) const [viewingUserId, setViewingUserId] = useState(currentUserId) const [loadingTasks, setLoadingTasks] = useState(false) const [clientFilter, setClientFilter] = useState('') @@ -420,8 +422,17 @@ export function TasksClient({ initialTasks, currentUserId, isPrivileged, users }

+
+ + {isPrivileged && ( -
+
Viewing as:
)} +
{/* Client filter */} @@ -729,6 +741,13 @@ export function TasksClient({ initialTasks, currentUserId, isPrivileged, users } + + fetchTasks(viewingUserId, clientFilter || undefined)} + /> ) } diff --git a/ondeck/src/app/api/tasks/adhoc-recent/route.ts b/ondeck/src/app/api/tasks/adhoc-recent/route.ts new file mode 100644 index 0000000..50bb840 --- /dev/null +++ b/ondeck/src/app/api/tasks/adhoc-recent/route.ts @@ -0,0 +1,26 @@ +import { NextRequest, NextResponse } from 'next/server' +import { getServerSession } from 'next-auth' +import { authOptions } from '@/lib/auth' +import { prisma } from '@/lib/db' + +export async function GET(request: NextRequest) { + try { + const session = await getServerSession(authOptions) + if (!session?.user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + + const userId = (session.user as any).id + + const recent = await prisma.task.findMany({ + where: { isAdHoc: true, createdBy: userId }, + select: { id: true, title: true, description: true, priority: true, department: true }, + orderBy: { createdAt: 'desc' }, + take: 10, + distinct: ['title'], + }) + + return NextResponse.json(recent) + } catch (error) { + console.error('adhoc-recent error:', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/ondeck/src/app/api/tasks/route.ts b/ondeck/src/app/api/tasks/route.ts index 7691a18..20d58a2 100644 --- a/ondeck/src/app/api/tasks/route.ts +++ b/ondeck/src/app/api/tasks/route.ts @@ -114,14 +114,16 @@ export async function POST(request: NextRequest) { } const body = await request.json() - const { clientId, policyId, templateId, assignedUserIds, ...taskData } = body + const { clientId, policyId, policyGroupId, templateId, isAdHoc, assignedUserIds, ...taskData } = body const task = await prisma.task.create({ data: { ...taskData, clientId, policyId, + policyGroupId, templateId, + isAdHoc: isAdHoc ?? false, createdBy: (session.user as any).id, assignments: assignedUserIds ? { diff --git a/ondeck/src/components/clients/client-card.tsx b/ondeck/src/components/clients/client-card.tsx index bb2f1e0..2e364f6 100644 --- a/ondeck/src/components/clients/client-card.tsx +++ b/ondeck/src/components/clients/client-card.tsx @@ -80,12 +80,12 @@ export function ClientCard({ client }: ClientCardProps) { )} - {/* Next Policy Expiration */} + {/* Renewal Date */} {nextPolicy && (
- Next expiration: + Renewal date: {formatDate(nextPolicy.expirationDate)} diff --git a/ondeck/src/components/clients/client-detail.tsx b/ondeck/src/components/clients/client-detail.tsx index e807608..9ae1947 100644 --- a/ondeck/src/components/clients/client-detail.tsx +++ b/ondeck/src/components/clients/client-detail.tsx @@ -17,6 +17,7 @@ import { Building2, MapPin, Phone, Mail, FileText, CheckSquare, CalendarRange, U import { Combobox } from '@/components/ui/combobox' import { formatDate } from '@/lib/utils' import { PolicyGroupManager } from '@/components/clients/policy-group-manager' +import { AdditionalServiceModal } from '@/components/tasks/additional-service-modal' import { toast } from 'sonner' interface SimpleUser { @@ -37,6 +38,15 @@ export function ClientDetail({ client, designations, policyGroups = [], canManag const [selectedDesignation2, setSelectedDesignation2] = useState(client.designation2Id || '') const [saving, setSaving] = useState(false) const [tasks, setTasks] = useState(client.tasks || []) + const [additionalServiceOpen, setAdditionalServiceOpen] = useState(false) + const [sessionUserId, setSessionUserId] = useState('') + + useEffect(() => { + fetch('/api/auth/session') + .then((r) => r.json()) + .then((s) => setSessionUserId(s?.user?.id ?? '')) + .catch(() => {}) + }, []) const refreshTasks = async () => { try { @@ -321,7 +331,7 @@ export function ClientDetail({ client, designations, policyGroups = [], canManag
- Expires {formatDate(policy.expirationDate)} + Renewal {formatDate(policy.expirationDate)}
@@ -395,6 +405,11 @@ export function ClientDetail({ client, designations, policyGroups = [], canManag +
+ +
{tasks.length === 0 ? ( @@ -423,6 +438,27 @@ export function ClientDetail({ client, designations, policyGroups = [], canManag )}
+ ({ + id: p.id, + policyNumber: p.policyNumber, + policyType: p.policyType, + })), + policyGroups: policyGroups.map((g: any) => ({ + id: g.id, + name: g.name, + renewalDate: g.renewalDate ? new Date(g.renewalDate).toLocaleDateString() : undefined, + })), + }} + onCreated={refreshTasks} + /> + Client Location Designation - Next Expiration + Renewal Date Policy Type Carrier Executive diff --git a/ondeck/src/components/clients/policy-group-manager.tsx b/ondeck/src/components/clients/policy-group-manager.tsx index 19d1249..a86c80d 100644 --- a/ondeck/src/components/clients/policy-group-manager.tsx +++ b/ondeck/src/components/clients/policy-group-manager.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState } from 'react' +import { useState, useEffect } from 'react' import { toast } from 'sonner' import { Plus, @@ -16,6 +16,7 @@ import { } from 'lucide-react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' +import { AdditionalServiceModal } from '@/components/tasks/additional-service-modal' import { Badge } from '@/components/ui/badge' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Label } from '@/components/ui/label' @@ -94,6 +95,15 @@ export function PolicyGroupManager({ const [deleteTarget, setDeleteTarget] = useState(null) const [cancelTasks, setCancelTasks] = useState(false) const [generatingFor, setGeneratingFor] = useState(null) + const [adhocGroupId, setAdhocGroupId] = useState(null) + const [sessionUserId, setSessionUserId] = useState('') + + useEffect(() => { + fetch('/api/auth/session') + .then((r) => r.json()) + .then((s) => setSessionUserId(s?.user?.id ?? '')) + .catch(() => {}) + }, []) const assignedPolicyIds = new Set( groups.flatMap((g) => g.policies.map((p) => p.id)) @@ -322,6 +332,14 @@ export function PolicyGroupManager({
{canManage && ( <> +
) } diff --git a/ondeck/src/components/tasks/additional-service-modal.tsx b/ondeck/src/components/tasks/additional-service-modal.tsx new file mode 100644 index 0000000..10d9e34 --- /dev/null +++ b/ondeck/src/components/tasks/additional-service-modal.tsx @@ -0,0 +1,394 @@ +'use client' + +import { useState, useEffect } from 'react' +import { toast } from 'sonner' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Textarea } from '@/components/ui/textarea' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from '@/components/ui/dialog' +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { Plus, Wand2 } from 'lucide-react' + +interface SimpleUser { + id: string + displayName: string | null + email: string + department?: string | null +} + +interface PolicyOption { + id: string + policyNumber: string | null + policyType: string | null +} + +interface PolicyGroupOption { + id: string + name?: string | null + renewalDate?: string | null +} + +interface RecentAdHoc { + id: string + title: string + description: string | null + priority: string + department: string +} + +interface AdditionalServiceModalProps { + open: boolean + onOpenChange: (open: boolean) => void + currentUserId: string + /** Pre-fill context — pass what's known from the current surface */ + context?: { + clientId?: string + clientName?: string + policyId?: string + policyGroupId?: string + policies?: PolicyOption[] + policyGroups?: PolicyGroupOption[] + } + onCreated?: () => void +} + +const PRIORITIES = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] +const DEPARTMENTS = ['Claims', 'Personal Lines', 'Commercial Lines', 'Benefits', 'Life', 'Other'] +const TIMINGS = ['BEFORE_RENEWAL', 'AT_RENEWAL', 'AFTER_RENEWAL', 'ONGOING'] + +export function AdditionalServiceModal({ + open, + onOpenChange, + currentUserId, + context, + onCreated, +}: AdditionalServiceModalProps) { + const [level, setLevel] = useState<'client' | 'policy' | 'group'>('client') + const [title, setTitle] = useState('') + const [description, setDescription] = useState('') + const [priority, setPriority] = useState('MEDIUM') + const [department, setDepartment] = useState(DEPARTMENTS[0]) + const [dueDate, setDueDate] = useState('') + const [assignTo, setAssignTo] = useState(currentUserId) + const [saving, setSaving] = useState(false) + + const [users, setUsers] = useState([]) + const [recentAdHoc, setRecentAdHoc] = useState([]) + + // Client search (when no clientId pre-filled) + const [clientSearch, setClientSearch] = useState(context?.clientName ?? '') + const [clientId, setClientId] = useState(context?.clientId ?? '') + const [clientOptions, setClientOptions] = useState<{ id: string; name: string }[]>([]) + const [clientDropOpen, setClientDropOpen] = useState(false) + + const [selectedPolicyId, setSelectedPolicyId] = useState(context?.policyId ?? '') + const [selectedGroupId, setSelectedGroupId] = useState(context?.policyGroupId ?? '') + + // Load users and recent ad hoc tasks when modal opens + useEffect(() => { + if (!open) return + // Reset form + setTitle('') + setDescription('') + setPriority('MEDIUM') + setDepartment(DEPARTMENTS[0]) + setDueDate('') + setAssignTo(currentUserId) + setLevel(context?.policyId ? 'policy' : context?.policyGroupId ? 'group' : 'client') + setClientId(context?.clientId ?? '') + setClientSearch(context?.clientName ?? '') + setSelectedPolicyId(context?.policyId ?? '') + setSelectedGroupId(context?.policyGroupId ?? '') + + fetch('/api/users?isActive=true&limit=200') + .then((r) => r.json()) + .then((d) => setUsers(d.users || [])) + .catch(() => {}) + + fetch('/api/tasks/adhoc-recent') + .then((r) => r.json()) + .then((d) => setRecentAdHoc(Array.isArray(d) ? d : [])) + .catch(() => {}) + }, [open]) + + // Client search debounce + useEffect(() => { + if (context?.clientId || !clientSearch.trim()) { setClientOptions([]); return } + const t = setTimeout(async () => { + try { + const res = await fetch(`/api/clients?search=${encodeURIComponent(clientSearch)}&limit=20`) + const data = await res.json() + setClientOptions((data.clients || []).map((c: any) => ({ id: c.id, name: c.name }))) + setClientDropOpen(true) + } catch {} + }, 250) + return () => clearTimeout(t) + }, [clientSearch, context?.clientId]) + + const applyRecent = (r: RecentAdHoc) => { + setTitle(r.title) + setDescription(r.description ?? '') + setPriority(r.priority) + setDepartment(r.department) + } + + const handleSubmit = async () => { + if (!title.trim()) { toast.error('Title is required'); return } + if (!dueDate) { toast.error('Due date is required'); return } + if (!clientId) { toast.error('Please select a client'); return } + + setSaving(true) + try { + const body: any = { + title: title.trim(), + description: description.trim() || null, + priority, + department, + dueDate: new Date(dueDate).toISOString(), + timing: 'ONGOING', + daysOffset: 0, + status: 'NOT_STARTED', + isAdHoc: true, + clientId, + assignedUserIds: [assignTo], + } + + if (level === 'policy' && selectedPolicyId) body.policyId = selectedPolicyId + if (level === 'group' && selectedGroupId) body.policyGroupId = selectedGroupId + + const res = await fetch('/api/tasks', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }) + const data = await res.json() + if (!res.ok) throw new Error(data.error) + + toast.success('Additional service task created') + onOpenChange(false) + onCreated?.() + } catch (err: any) { + toast.error(err.message || 'Failed to create task') + } finally { + setSaving(false) + } + } + + const claimsUsers = users.filter((u) => u.department?.toLowerCase().includes('claims')) + const otherUsers = users.filter((u) => !u.department?.toLowerCase().includes('claims')) + + return ( + + + + + + Additional Service + + + +
+ {/* Recent suggestions */} + {recentAdHoc.length > 0 && ( +
+

+ Recent — click to pre-fill +

+
+ {recentAdHoc.slice(0, 5).map((r) => ( + + ))} +
+
+ )} + + {/* Title */} +
+ + setTitle(e.target.value)} placeholder="e.g. Certificate of Insurance Request" /> +
+ + {/* Description */} +
+ +