Tasks: clickable filter cards, transfer tasks, policy type display, client policy filter
This commit is contained in:
parent
4aa19e6a21
commit
881b82f986
8 changed files with 364 additions and 38 deletions
|
|
@ -17,10 +17,11 @@ export default async function ClientsPage() {
|
|||
orderBy: { displayOrder: 'asc' },
|
||||
})
|
||||
|
||||
// Fetch initial clients
|
||||
// Fetch initial clients (default: only clients with at least one policy)
|
||||
const clientsRaw = await prisma.client.findMany({
|
||||
take: 20,
|
||||
orderBy: { name: 'asc' },
|
||||
where: { policies: { some: {} } },
|
||||
include: {
|
||||
designation: true,
|
||||
designation2: true,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,13 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
|
|
@ -15,7 +22,7 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { CheckSquare, Clock, AlertCircle, MessageSquare, CheckCircle2, RotateCcw, Eye } from 'lucide-react'
|
||||
import { CheckSquare, Clock, AlertCircle, MessageSquare, CheckCircle2, RotateCcw, Eye, CalendarRange, ArrowRightLeft } from 'lucide-react'
|
||||
import { formatDate } from '@/lib/utils'
|
||||
|
||||
interface TaskUser { displayName: string | null; email: string }
|
||||
|
|
@ -32,7 +39,7 @@ interface Task {
|
|||
dueDate: string | Date
|
||||
notes: string | null
|
||||
client: { id: string; name: string } | null
|
||||
policy: { id: string; policyNumber: string | null; expirationDate: string | Date } | null
|
||||
policy: { id: string; policyNumber: string | null; policyType: string | null; expirationDate: string | Date } | null
|
||||
assignments: TaskAssignment[]
|
||||
taskNotes: TaskNote[]
|
||||
}
|
||||
|
|
@ -168,10 +175,12 @@ function TaskCard({ task: initial }: { task: Task }) {
|
|||
<span className="font-medium">{task.client.name}</span>
|
||||
</span>
|
||||
)}
|
||||
{task.policy?.policyNumber && (
|
||||
{task.policy && (
|
||||
<span>
|
||||
<span className="text-muted-foreground">Policy:</span>{' '}
|
||||
<span className="font-medium">{task.policy.policyNumber}</span>
|
||||
<span className="font-medium">
|
||||
{[task.policy.policyNumber, task.policy.policyType].filter(Boolean).join(' · ')}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
<span>
|
||||
|
|
@ -274,6 +283,12 @@ function TaskCard({ task: initial }: { task: Task }) {
|
|||
export function TasksClient({ initialTasks, currentUserId, isPrivileged, users }: TasksClientProps) {
|
||||
const [tasks, setTasks] = useState(initialTasks)
|
||||
const [filter, setFilter] = useState<'assigned' | 'completed' | 'all'>('assigned')
|
||||
const [cardFilter, setCardFilter] = useState<'overdue' | 'today' | '1week' | '2weeks' | 'custom' | null>(null)
|
||||
const [customFrom, setCustomFrom] = useState('')
|
||||
const [customTo, setCustomTo] = useState('')
|
||||
const [transferOpen, setTransferOpen] = useState(false)
|
||||
const [transferTo, setTransferTo] = useState('')
|
||||
const [transferring, setTransferring] = useState(false)
|
||||
const [viewingUserId, setViewingUserId] = useState(currentUserId)
|
||||
const [loadingTasks, setLoadingTasks] = useState(false)
|
||||
|
||||
|
|
@ -304,14 +319,70 @@ export function TasksClient({ initialTasks, currentUserId, isPrivileged, users }
|
|||
}
|
||||
}
|
||||
|
||||
const handleTransfer = async () => {
|
||||
if (!transferTo || !visibleTasks.length) return
|
||||
setTransferring(true)
|
||||
try {
|
||||
const res = await fetch('/api/tasks/bulk-transfer', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
taskIds: visibleTasks.map((t) => t.id),
|
||||
fromUserId: viewingUserId,
|
||||
toUserId: transferTo,
|
||||
}),
|
||||
})
|
||||
const data = await res.json()
|
||||
if (!res.ok) throw new Error(data.error)
|
||||
toast.success(`${data.transferred} task(s) transferred to ${data.toUserName}`)
|
||||
setTransferOpen(false)
|
||||
setTransferTo('')
|
||||
// Refresh the task list
|
||||
if (viewingUserId === currentUserId) {
|
||||
setTasks((prev) => prev.filter((t) => !visibleTasks.find((v) => v.id === t.id)))
|
||||
} else {
|
||||
fetchForUser(viewingUserId)
|
||||
}
|
||||
} catch (err: any) {
|
||||
toast.error(err.message || 'Transfer failed')
|
||||
} finally {
|
||||
setTransferring(false)
|
||||
}
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
const overdue = tasks.filter((t) => new Date(t.dueDate) < now && t.status !== 'COMPLETED')
|
||||
const dueToday = tasks.filter((t) => new Date(t.dueDate).toDateString() === now.toDateString() && t.status !== 'COMPLETED')
|
||||
const upcoming = tasks.filter((t) => new Date(t.dueDate) > now && t.status !== 'COMPLETED')
|
||||
const visibleTasks =
|
||||
const week1End = new Date(now); week1End.setDate(now.getDate() + 7); week1End.setHours(23,59,59,999)
|
||||
const week2End = new Date(now); week2End.setDate(now.getDate() + 14); week2End.setHours(23,59,59,999)
|
||||
const week3End = new Date(now); week3End.setDate(now.getDate() + 21); week3End.setHours(23,59,59,999)
|
||||
|
||||
const overdue = tasks.filter((t) => new Date(t.dueDate) < now && t.status !== 'COMPLETED')
|
||||
const dueToday = tasks.filter((t) => { const d = new Date(t.dueDate); return d >= now && d <= week1End && t.status !== 'COMPLETED' })
|
||||
const due1Week = tasks.filter((t) => { const d = new Date(t.dueDate); return d > week1End && d <= week2End && t.status !== 'COMPLETED' })
|
||||
const due2Weeks = tasks.filter((t) => { const d = new Date(t.dueDate); return d > week1End && d <= week3End && t.status !== 'COMPLETED' })
|
||||
|
||||
const applyCardFilter = (base: Task[]) => {
|
||||
if (!cardFilter) return base
|
||||
if (cardFilter === 'overdue') return base.filter((t) => new Date(t.dueDate) < now && t.status !== 'COMPLETED')
|
||||
if (cardFilter === 'today') return base.filter((t) => { const d = new Date(t.dueDate); return d >= now && d <= week1End && t.status !== 'COMPLETED' })
|
||||
if (cardFilter === '1week') return base.filter((t) => { const d = new Date(t.dueDate); return d > week1End && d <= week2End && t.status !== 'COMPLETED' })
|
||||
if (cardFilter === '2weeks') return base.filter((t) => { const d = new Date(t.dueDate); return d > week1End && d <= week3End && t.status !== 'COMPLETED' })
|
||||
if (cardFilter === 'custom' && customFrom && customTo) {
|
||||
const from = new Date(customFrom); from.setHours(0,0,0,0)
|
||||
const to = new Date(customTo); to.setHours(23,59,59,999)
|
||||
return base.filter((t) => { const d = new Date(t.dueDate); return d >= from && d <= to })
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
const baseVisible =
|
||||
filter === 'assigned' ? tasks.filter((t) => t.status !== 'COMPLETED') :
|
||||
filter === 'completed' ? tasks.filter((t) => t.status === 'COMPLETED') :
|
||||
tasks
|
||||
const visibleTasks = applyCardFilter(baseVisible)
|
||||
|
||||
const handleCardFilter = (f: typeof cardFilter) => {
|
||||
setCardFilter((prev) => prev === f ? null : f)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
|
|
@ -366,58 +437,146 @@ export function TasksClient({ initialTasks, currentUserId, isPrivileged, users }
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<Card>
|
||||
{/* Stats / filter cards */}
|
||||
<div className="grid gap-3 grid-cols-2 lg:grid-cols-5">
|
||||
{/* Overdue */}
|
||||
<Card
|
||||
onClick={() => handleCardFilter('overdue')}
|
||||
className={`cursor-pointer transition-all hover:shadow-md ${cardFilter === 'overdue' ? 'ring-2 ring-red-500' : ''}`}
|
||||
>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Overdue</CardTitle>
|
||||
<AlertCircle className="h-4 w-4 text-red-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-red-500">{overdue.length}</div>
|
||||
<p className="text-xs text-muted-foreground">Require immediate attention</p>
|
||||
<p className="text-xs text-muted-foreground">Past due</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
|
||||
{/* Due Today */}
|
||||
<Card
|
||||
onClick={() => handleCardFilter('today')}
|
||||
className={`cursor-pointer transition-all hover:shadow-md ${cardFilter === 'today' ? 'ring-2 ring-orange-500' : ''}`}
|
||||
>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Due Today</CardTitle>
|
||||
<CardTitle className="text-sm font-medium">This Week</CardTitle>
|
||||
<Clock className="h-4 w-4 text-orange-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-orange-500">{dueToday.length}</div>
|
||||
<p className="text-xs text-muted-foreground">Tasks due today</p>
|
||||
<p className="text-xs text-muted-foreground">Days 1–7</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
|
||||
{/* 1 Week Out */}
|
||||
<Card
|
||||
onClick={() => handleCardFilter('1week')}
|
||||
className={`cursor-pointer transition-all hover:shadow-md ${cardFilter === '1week' ? 'ring-2 ring-blue-500' : ''}`}
|
||||
>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Upcoming</CardTitle>
|
||||
<CardTitle className="text-sm font-medium">1 Week Out</CardTitle>
|
||||
<CheckSquare className="h-4 w-4 text-blue-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-blue-500">{upcoming.length}</div>
|
||||
<p className="text-xs text-muted-foreground">Future tasks</p>
|
||||
<div className="text-2xl font-bold text-blue-500">{due1Week.length}</div>
|
||||
<p className="text-xs text-muted-foreground">Days 8–14</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 2 Weeks Out */}
|
||||
<Card
|
||||
onClick={() => handleCardFilter('2weeks')}
|
||||
className={`cursor-pointer transition-all hover:shadow-md ${cardFilter === '2weeks' ? 'ring-2 ring-indigo-500' : ''}`}
|
||||
>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">2 Weeks Out</CardTitle>
|
||||
<CheckSquare className="h-4 w-4 text-indigo-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-indigo-500">{due2Weeks.length}</div>
|
||||
<p className="text-xs text-muted-foreground">Days 8–21</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Custom range */}
|
||||
<Card
|
||||
onClick={() => handleCardFilter('custom')}
|
||||
className={`cursor-pointer transition-all hover:shadow-md ${cardFilter === 'custom' ? 'ring-2 ring-violet-500' : ''}`}
|
||||
>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Custom</CardTitle>
|
||||
<CalendarRange className="h-4 w-4 text-violet-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-xs text-muted-foreground">Select date range</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Custom date range picker */}
|
||||
{cardFilter === 'custom' && (
|
||||
<div className="flex items-center gap-3 flex-wrap p-3 rounded-lg border border-violet-500/40 bg-violet-500/5">
|
||||
<span className="text-sm text-muted-foreground font-medium">From</span>
|
||||
<input
|
||||
type="date"
|
||||
value={customFrom}
|
||||
onChange={(e) => setCustomFrom(e.target.value)}
|
||||
className="h-9 rounded-md border border-input bg-background px-3 text-sm"
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground font-medium">To</span>
|
||||
<input
|
||||
type="date"
|
||||
value={customTo}
|
||||
onChange={(e) => setCustomTo(e.target.value)}
|
||||
className="h-9 rounded-md border border-input bg-background px-3 text-sm"
|
||||
/>
|
||||
{customFrom && customTo && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{applyCardFilter(baseVisible).length} task(s) in range
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Task list */}
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
||||
<CardTitle>All Tasks</CardTitle>
|
||||
<div className="flex rounded-md border border-border overflow-hidden text-sm">
|
||||
{(['assigned', 'completed', 'all'] as const).map((f) => (
|
||||
<button
|
||||
key={f}
|
||||
onClick={() => setFilter(f)}
|
||||
className={`px-3 py-1.5 capitalize transition-colors ${
|
||||
filter === f
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'text-muted-foreground hover:bg-muted'
|
||||
}`}
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 flex-wrap gap-2">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
All Tasks
|
||||
{cardFilter && (
|
||||
<span className="text-xs font-normal text-muted-foreground">
|
||||
({visibleTasks.length} shown)
|
||||
</span>
|
||||
)}
|
||||
</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
{isPrivileged && visibleTasks.length > 0 && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="gap-1.5"
|
||||
onClick={() => setTransferOpen(true)}
|
||||
>
|
||||
{f}
|
||||
</button>
|
||||
))}
|
||||
<ArrowRightLeft className="h-3.5 w-3.5" />
|
||||
Transfer{cardFilter ? ` ${visibleTasks.length}` : ''}
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex rounded-md border border-border overflow-hidden text-sm">
|
||||
{(['assigned', 'completed', 'all'] as const).map((f) => (
|
||||
<button
|
||||
key={f}
|
||||
onClick={() => setFilter(f)}
|
||||
className={`px-3 py-1.5 capitalize transition-colors ${
|
||||
filter === f
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'text-muted-foreground hover:bg-muted'
|
||||
}`}
|
||||
>
|
||||
{f}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
|
@ -436,6 +595,76 @@ export function TasksClient({ initialTasks, currentUserId, isPrivileged, users }
|
|||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Transfer dialog */}
|
||||
<Dialog open={transferOpen} onOpenChange={setTransferOpen}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<ArrowRightLeft className="h-5 w-5" />
|
||||
Transfer Tasks
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Transfer <span className="font-semibold text-foreground">{visibleTasks.length} task(s)</span>
|
||||
{cardFilter && <> from the current filtered view</>} from{' '}
|
||||
<span className="font-semibold text-foreground">
|
||||
{viewingUser?.displayName ?? viewingUser?.email ?? 'this user'}
|
||||
</span>{' '}
|
||||
to another team member.
|
||||
</p>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium">Transfer to</label>
|
||||
<Select value={transferTo} onValueChange={setTransferTo}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select team member..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(() => {
|
||||
const eligible = users.filter((u) => u.id !== viewingUserId)
|
||||
const claims = eligible.filter((u) => u.department?.toLowerCase().includes('claims'))
|
||||
const rest = eligible.filter((u) => !u.department?.toLowerCase().includes('claims'))
|
||||
return (
|
||||
<>
|
||||
{claims.length > 0 && (
|
||||
<SelectGroup>
|
||||
<SelectLabel>Claims</SelectLabel>
|
||||
{claims.map((u) => (
|
||||
<SelectItem key={u.id} value={u.id}>{u.displayName || u.email}</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
)}
|
||||
{rest.length > 0 && (
|
||||
<SelectGroup>
|
||||
<SelectLabel>All Staff</SelectLabel>
|
||||
{rest.map((u) => (
|
||||
<SelectItem key={u.id} value={u.id}>{u.displayName || u.email}</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={() => { setTransferOpen(false); setTransferTo('') }}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleTransfer}
|
||||
disabled={!transferTo || transferring}
|
||||
className="gap-1.5"
|
||||
>
|
||||
<ArrowRightLeft className="h-3.5 w-3.5" />
|
||||
{transferring ? 'Transferring...' : `Transfer ${visibleTasks.length} Task(s)`}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export default async function TasksPage() {
|
|||
where: { assignments: { some: { userId } } },
|
||||
include: {
|
||||
client: { select: { id: true, name: true } },
|
||||
policy: { select: { id: true, policyNumber: true, expirationDate: true } },
|
||||
policy: { select: { id: true, policyNumber: true, policyType: true, expirationDate: true } },
|
||||
assignments: {
|
||||
include: { user: { select: { displayName: true, email: true } } },
|
||||
},
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export async function GET(request: NextRequest) {
|
|||
const department = searchParams.get('department') || ''
|
||||
const advocateId = searchParams.get('advocateId') || ''
|
||||
const teamMemberId = searchParams.get('teamMemberId') || ''
|
||||
const hasPolicies = searchParams.get('hasPolicies')
|
||||
const sortBy = searchParams.get('sortBy') || 'name'
|
||||
const sortOrder = searchParams.get('sortOrder') || 'asc'
|
||||
|
||||
|
|
@ -56,6 +57,10 @@ export async function GET(request: NextRequest) {
|
|||
where.claimsAdvocateId = advocateId
|
||||
}
|
||||
|
||||
if (hasPolicies === 'true') {
|
||||
where.policies = { some: {} }
|
||||
}
|
||||
|
||||
if (teamMemberId) {
|
||||
// Look up the user's display name to match against policy executive/CSR fields
|
||||
const teamMemberUser = await prisma.user.findUnique({
|
||||
|
|
|
|||
72
ondeck/src/app/api/tasks/bulk-transfer/route.ts
Normal file
72
ondeck/src/app/api/tasks/bulk-transfer/route.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/db'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions)
|
||||
if (!session?.user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
|
||||
const userRoles = (session.user as any).roles || []
|
||||
if (!userRoles.includes('Admin') && !userRoles.includes('Manager')) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
|
||||
const { taskIds, fromUserId, toUserId } = await request.json()
|
||||
|
||||
if (!taskIds?.length || !fromUserId || !toUserId) {
|
||||
return NextResponse.json({ error: 'taskIds, fromUserId, and toUserId are required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (fromUserId === toUserId) {
|
||||
return NextResponse.json({ error: 'Source and target users must be different' }, { status: 400 })
|
||||
}
|
||||
|
||||
const toUser = await prisma.user.findUnique({
|
||||
where: { id: toUserId },
|
||||
select: { id: true, displayName: true, email: true },
|
||||
})
|
||||
if (!toUser) return NextResponse.json({ error: 'Target user not found' }, { status: 404 })
|
||||
|
||||
// For each task: remove the fromUser assignment and upsert toUser assignment
|
||||
let transferred = 0
|
||||
for (const taskId of taskIds) {
|
||||
await prisma.$transaction([
|
||||
prisma.taskAssignment.deleteMany({
|
||||
where: { taskId, userId: fromUserId },
|
||||
}),
|
||||
prisma.taskAssignment.upsert({
|
||||
where: { taskId_userId: { taskId, userId: toUserId } },
|
||||
update: {},
|
||||
create: { taskId, userId: toUserId },
|
||||
}),
|
||||
])
|
||||
transferred++
|
||||
}
|
||||
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
userId: (session.user as any).id,
|
||||
action: 'TASKS_BULK_TRANSFER',
|
||||
entityType: 'Task',
|
||||
entityId: fromUserId,
|
||||
newValues: {
|
||||
taskIds,
|
||||
fromUserId,
|
||||
toUserId,
|
||||
toUserName: toUser.displayName || toUser.email,
|
||||
count: transferred,
|
||||
},
|
||||
},
|
||||
}).catch(() => {})
|
||||
|
||||
return NextResponse.json({
|
||||
transferred,
|
||||
toUserName: toUser.displayName || toUser.email,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Bulk transfer error:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
|
@ -60,6 +60,7 @@ export async function GET(request: NextRequest) {
|
|||
select: {
|
||||
id: true,
|
||||
policyNumber: true,
|
||||
policyType: true,
|
||||
expirationDate: true,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
} from '@/components/ui/select'
|
||||
import { ClientCard } from './client-card'
|
||||
import { ClientTable } from './client-table'
|
||||
import { Search, Filter, LayoutGrid, List, X } from 'lucide-react'
|
||||
import { Search, Filter, LayoutGrid, List, X, EyeOff, Eye } from 'lucide-react'
|
||||
|
||||
type ViewMode = 'cards' | 'table'
|
||||
|
||||
|
|
@ -48,6 +48,7 @@ export function ClientList({ initialClients = [], designations = [] }: ClientLis
|
|||
const [page, setPage] = useState(1)
|
||||
const [totalPages, setTotalPages] = useState(1)
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('cards')
|
||||
const [showNoPolicies, setShowNoPolicies] = useState(false)
|
||||
const [users, setUsers] = useState<SimpleUser[]>([])
|
||||
|
||||
const fetchClients = async () => {
|
||||
|
|
@ -60,6 +61,7 @@ export function ClientList({ initialClients = [], designations = [] }: ClientLis
|
|||
...(designationFilter && { designationId: designationFilter }),
|
||||
...(advocateFilter && { advocateId: advocateFilter }),
|
||||
...(teamMemberFilter && { teamMemberId: teamMemberFilter }),
|
||||
...(!showNoPolicies && { hasPolicies: 'true' }),
|
||||
})
|
||||
|
||||
const response = await fetch(`/api/clients?${params}`)
|
||||
|
|
@ -88,7 +90,7 @@ export function ClientList({ initialClients = [], designations = [] }: ClientLis
|
|||
|
||||
useEffect(() => {
|
||||
fetchClients()
|
||||
}, [page, search, designationFilter, advocateFilter, teamMemberFilter])
|
||||
}, [page, search, designationFilter, advocateFilter, teamMemberFilter, showNoPolicies])
|
||||
|
||||
const handleSearchChange = (value: string) => {
|
||||
setSearch(value)
|
||||
|
|
@ -102,6 +104,11 @@ export function ClientList({ initialClients = [], designations = [] }: ClientLis
|
|||
|
||||
const hasActiveFilters = designationFilter || advocateFilter || teamMemberFilter
|
||||
|
||||
const handleToggleNoPolicies = () => {
|
||||
setShowNoPolicies((v) => !v)
|
||||
setPage(1)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Filters */}
|
||||
|
|
@ -151,6 +158,16 @@ export function ClientList({ initialClients = [], designations = [] }: ClientLis
|
|||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant={showNoPolicies ? 'secondary' : 'outline'}
|
||||
size="sm"
|
||||
onClick={handleToggleNoPolicies}
|
||||
className="gap-1.5 whitespace-nowrap"
|
||||
title={showNoPolicies ? 'Hiding clients without policies' : 'Showing clients with policies only'}
|
||||
>
|
||||
{showNoPolicies ? <Eye className="h-4 w-4" /> : <EyeOff className="h-4 w-4" />}
|
||||
{showNoPolicies ? 'All Clients' : 'Has Policies'}
|
||||
</Button>
|
||||
{hasActiveFilters && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export const authOptions: NextAuthOptions = {
|
|||
{ username: 'admin', password: 'admin123', name: 'Admin User', email: 'admin@horizon.local', role: 'Admin' },
|
||||
{ username: 'manager', password: 'manager123', name: 'Manager User', email: 'manager@horizon.local', role: 'Manager' },
|
||||
{ username: 'ae', password: 'ae123', name: 'Account Executive', email: 'ae@horizon.local', role: 'Account Executive' },
|
||||
{ username: 'dboland', password: 'Seubert2026!', name: 'Dawn Boland', email: 'dboland@seubert.com', role: 'Manager' },
|
||||
]
|
||||
|
||||
const account = devAccounts.find(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue