Manager overdue window: configurable via Admin > Settings

- Default 7 days: overdue tasks shown only within last N days
- New /api/admin/manager-settings endpoint (GET/PUT, AppSetting key)
- Workload API reads setting and applies to both summary count and per-user table
- Overdue card now shows 'past due within Nd' context label
- Admin > System Settings: new Manager Dashboard card with day input
This commit is contained in:
lorentz 2026-04-14 00:17:31 +00:00
parent 27247f77f0
commit 191698088b
5 changed files with 129 additions and 7 deletions

View file

@ -28,6 +28,7 @@ export default async function SystemSettingsPage() {
const settingsMap = Object.fromEntries(appSettings.map((s) => [s.key, s.value])) const settingsMap = Object.fromEntries(appSettings.map((s) => [s.key, s.value]))
const windowDays = settingsMap['renewal_group_window_days'] ?? '90' const windowDays = settingsMap['renewal_group_window_days'] ?? '90'
const dateRule = settingsMap['renewal_group_date_rule'] ?? 'nearest-to-year-start' const dateRule = settingsMap['renewal_group_date_rule'] ?? 'nearest-to-year-start'
const overdueWindowDays = parseInt(settingsMap['overdue_window_days'] ?? '7', 10)
const relatedPages = [ const relatedPages = [
{ {
@ -66,7 +67,7 @@ export default async function SystemSettingsPage() {
</div> </div>
{/* Sync config inline editor */} {/* Sync config inline editor */}
<SystemSettingsForm syncEnabled={syncEnabled === 'true'} syncSchedule={syncSchedule} /> <SystemSettingsForm syncEnabled={syncEnabled === 'true'} syncSchedule={syncSchedule} overdueWindowDays={overdueWindowDays} />
{/* Links to subsection settings pages */} {/* Links to subsection settings pages */}
<div> <div>

View file

@ -7,17 +7,20 @@ import { Label } from '@/components/ui/label'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Database, Save } from 'lucide-react' import { Database, Save, BarChart3 } from 'lucide-react'
interface SystemSettingsFormProps { interface SystemSettingsFormProps {
syncEnabled: boolean syncEnabled: boolean
syncSchedule: string syncSchedule: string
overdueWindowDays: number
} }
export function SystemSettingsForm({ syncEnabled: initialEnabled, syncSchedule: initialSchedule }: SystemSettingsFormProps) { export function SystemSettingsForm({ syncEnabled: initialEnabled, syncSchedule: initialSchedule, overdueWindowDays: initialOverdueWindow }: SystemSettingsFormProps) {
const [syncEnabled, setSyncEnabled] = useState(initialEnabled) const [syncEnabled, setSyncEnabled] = useState(initialEnabled)
const [syncSchedule, setSyncSchedule] = useState(initialSchedule) const [syncSchedule, setSyncSchedule] = useState(initialSchedule)
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const [overdueWindowDays, setOverdueWindowDays] = useState(initialOverdueWindow)
const [savingOverdue, setSavingOverdue] = useState(false)
async function handleSave() { async function handleSave() {
setSaving(true) setSaving(true)
@ -39,7 +42,28 @@ export function SystemSettingsForm({ syncEnabled: initialEnabled, syncSchedule:
} }
} }
async function handleSaveOverdue() {
setSavingOverdue(true)
try {
const res = await fetch('/api/admin/manager-settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ overdueWindowDays }),
})
if (!res.ok) {
const data = await res.json().catch(() => ({}))
throw new Error(data.error || 'Save failed')
}
toast.success('Manager settings saved')
} catch (err: any) {
toast.error(err.message || 'Save failed')
} finally {
setSavingOverdue(false)
}
}
return ( return (
<div className="space-y-6">
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-base"> <CardTitle className="flex items-center gap-2 text-base">
@ -82,5 +106,38 @@ export function SystemSettingsForm({ syncEnabled: initialEnabled, syncSchedule:
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<BarChart3 className="h-4 w-4" /> Manager Dashboard
</CardTitle>
</CardHeader>
<CardContent className="space-y-5">
<div className="space-y-1.5">
<Label htmlFor="overdue-window">Overdue task window (days)</Label>
<div className="flex items-center gap-3">
<Input
id="overdue-window"
type="number"
min={1}
max={365}
value={overdueWindowDays}
onChange={(e) => setOverdueWindowDays(Number(e.target.value))}
className="w-24 text-sm"
/>
<span className="text-sm text-muted-foreground">days back</span>
</div>
<p className="text-xs text-muted-foreground">
Only tasks with a due date within this many days in the past are shown as overdue on the Manager dashboard.
</p>
</div>
<Button onClick={handleSaveOverdue} disabled={savingOverdue} size="sm" className="gap-1.5">
<Save className="h-3.5 w-3.5" />
{savingOverdue ? 'Saving…' : 'Save'}
</Button>
</CardContent>
</Card>
</div>
) )
} }

View file

@ -0,0 +1,57 @@
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { prisma } from '@/lib/db'
export const OVERDUE_WINDOW_KEY = 'overdue_window_days'
export const OVERDUE_WINDOW_DEFAULT = 7
export async function GET() {
try {
const session = await getServerSession(authOptions)
if (!session?.user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
const setting = await prisma.appSetting.findUnique({ where: { key: OVERDUE_WINDOW_KEY } })
return NextResponse.json({
overdueWindowDays: parseInt(setting?.value ?? String(OVERDUE_WINDOW_DEFAULT), 10),
})
} catch (error) {
console.error('Manager settings GET error:', error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
export async function PUT(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')) return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
const { overdueWindowDays } = await request.json()
if (typeof overdueWindowDays !== 'number' || overdueWindowDays < 1 || overdueWindowDays > 365) {
return NextResponse.json({ error: 'overdueWindowDays must be between 1 and 365' }, { status: 400 })
}
await prisma.appSetting.upsert({
where: { key: OVERDUE_WINDOW_KEY },
update: { value: String(overdueWindowDays) },
create: { key: OVERDUE_WINDOW_KEY, value: String(overdueWindowDays) },
})
await prisma.auditLog.create({
data: {
userId: (session.user as any).id,
action: 'MANAGER_SETTINGS_UPDATED',
entityType: 'AppSetting',
newValues: { overdueWindowDays },
},
})
return NextResponse.json({ overdueWindowDays })
} catch (error) {
console.error('Manager settings PUT error:', error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

View file

@ -3,6 +3,7 @@ import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth' import { authOptions } from '@/lib/auth'
import { prisma } from '@/lib/db' import { prisma } from '@/lib/db'
import { DepartmentType } from '@prisma/client' import { DepartmentType } from '@prisma/client'
import { OVERDUE_WINDOW_KEY, OVERDUE_WINDOW_DEFAULT } from '@/app/api/admin/manager-settings/route'
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
@ -19,6 +20,10 @@ export async function GET(request: NextRequest) {
const weekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000) const weekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000)
const monthAgo = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000) const monthAgo = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000)
const overdueWindowSetting = await prisma.appSetting.findUnique({ where: { key: OVERDUE_WINDOW_KEY } })
const overdueWindowDays = parseInt(overdueWindowSetting?.value ?? String(OVERDUE_WINDOW_DEFAULT), 10)
const overdueFrom = new Date(today.getTime() - overdueWindowDays * 24 * 60 * 60 * 1000)
// Shape client IDs for scoping // Shape client IDs for scoping
const shapeClients = await prisma.client.findMany({ const shapeClients = await prisma.client.findMany({
where: { designation: { name: { in: ['Shape', 'Shape 2'] } } }, where: { designation: { name: { in: ['Shape', 'Shape 2'] } } },
@ -82,11 +87,11 @@ export async function GET(request: NextRequest) {
where: deptTaskWhere, where: deptTaskWhere,
_count: { id: true }, _count: { id: true },
}), }),
// Overdue tasks (past due, not completed) // Overdue tasks (past due within window, not completed)
prisma.task.count({ prisma.task.count({
where: { where: {
...deptTaskWhere, ...deptTaskWhere,
dueDate: { lt: today }, dueDate: { gte: overdueFrom, lt: today },
status: { notIn: ['COMPLETED', 'CANCELLED', 'NA'] }, status: { notIn: ['COMPLETED', 'CANCELLED', 'NA'] },
}, },
}), }),
@ -177,7 +182,7 @@ export async function GET(request: NextRequest) {
) )
const completedTasks = tasks.filter((t) => t.status === 'COMPLETED') const completedTasks = tasks.filter((t) => t.status === 'COMPLETED')
const overdue = activeTasks.filter( const overdue = activeTasks.filter(
(t) => new Date(t.dueDate) < today (t) => new Date(t.dueDate) >= overdueFrom && new Date(t.dueDate) < today
) )
const highPriority = activeTasks.filter( const highPriority = activeTasks.filter(
(t) => t.priority === 'HIGH' || t.priority === 'URGENT' (t) => t.priority === 'HIGH' || t.priority === 'URGENT'
@ -251,6 +256,7 @@ export async function GET(request: NextRequest) {
completedTasks: totalCompleted, completedTasks: totalCompleted,
completionRate, completionRate,
overdueTasks, overdueTasks,
overdueWindowDays,
dueTodayTasks, dueTodayTasks,
dueThisWeekTasks, dueThisWeekTasks,
activeUsers, activeUsers,

View file

@ -25,6 +25,7 @@ interface WorkloadData {
completedTasks: number completedTasks: number
completionRate: number completionRate: number
overdueTasks: number overdueTasks: number
overdueWindowDays: number
dueTodayTasks: number dueTodayTasks: number
dueThisWeekTasks: number dueThisWeekTasks: number
activeUsers: number activeUsers: number
@ -195,7 +196,7 @@ export function WorkloadKPIs({ department }: WorkloadKPIsProps = {}) {
{summary.overdueTasks} {summary.overdueTasks}
</div> </div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{summary.dueTodayTasks} due today past due within {summary.overdueWindowDays}d
</p> </p>
</CardContent> </CardContent>
</Card> </Card>