diff --git a/ondeck/src/app/(dashboard)/admin/settings/page.tsx b/ondeck/src/app/(dashboard)/admin/settings/page.tsx index f5ce371..4be4c64 100644 --- a/ondeck/src/app/(dashboard)/admin/settings/page.tsx +++ b/ondeck/src/app/(dashboard)/admin/settings/page.tsx @@ -28,6 +28,7 @@ export default async function SystemSettingsPage() { const settingsMap = Object.fromEntries(appSettings.map((s) => [s.key, s.value])) const windowDays = settingsMap['renewal_group_window_days'] ?? '90' const dateRule = settingsMap['renewal_group_date_rule'] ?? 'nearest-to-year-start' + const overdueWindowDays = parseInt(settingsMap['overdue_window_days'] ?? '7', 10) const relatedPages = [ { @@ -66,7 +67,7 @@ export default async function SystemSettingsPage() { {/* Sync config inline editor */} - + {/* Links to subsection settings pages */}
diff --git a/ondeck/src/app/(dashboard)/admin/settings/system-settings-form.tsx b/ondeck/src/app/(dashboard)/admin/settings/system-settings-form.tsx index b0810ff..f082db2 100644 --- a/ondeck/src/app/(dashboard)/admin/settings/system-settings-form.tsx +++ b/ondeck/src/app/(dashboard)/admin/settings/system-settings-form.tsx @@ -7,17 +7,20 @@ import { Label } from '@/components/ui/label' import { Input } from '@/components/ui/input' import { Switch } from '@/components/ui/switch' import { Button } from '@/components/ui/button' -import { Database, Save } from 'lucide-react' +import { Database, Save, BarChart3 } from 'lucide-react' interface SystemSettingsFormProps { syncEnabled: boolean 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 [syncSchedule, setSyncSchedule] = useState(initialSchedule) const [saving, setSaving] = useState(false) + const [overdueWindowDays, setOverdueWindowDays] = useState(initialOverdueWindow) + const [savingOverdue, setSavingOverdue] = useState(false) async function handleSave() { 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 ( +
@@ -82,5 +106,38 @@ export function SystemSettingsForm({ syncEnabled: initialEnabled, syncSchedule: + + + + + Manager Dashboard + + + +
+ +
+ setOverdueWindowDays(Number(e.target.value))} + className="w-24 text-sm" + /> + days back +
+

+ Only tasks with a due date within this many days in the past are shown as overdue on the Manager dashboard. +

+
+ +
+
+
) } diff --git a/ondeck/src/app/api/admin/manager-settings/route.ts b/ondeck/src/app/api/admin/manager-settings/route.ts new file mode 100644 index 0000000..7978f7c --- /dev/null +++ b/ondeck/src/app/api/admin/manager-settings/route.ts @@ -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 }) + } +} diff --git a/ondeck/src/app/api/dashboard/workload/route.ts b/ondeck/src/app/api/dashboard/workload/route.ts index df4cb19..13afaa4 100644 --- a/ondeck/src/app/api/dashboard/workload/route.ts +++ b/ondeck/src/app/api/dashboard/workload/route.ts @@ -3,6 +3,7 @@ import { getServerSession } from 'next-auth' import { authOptions } from '@/lib/auth' import { prisma } from '@/lib/db' 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) { try { @@ -19,6 +20,10 @@ export async function GET(request: NextRequest) { const weekAgo = new Date(today.getTime() - 7 * 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 const shapeClients = await prisma.client.findMany({ where: { designation: { name: { in: ['Shape', 'Shape 2'] } } }, @@ -82,11 +87,11 @@ export async function GET(request: NextRequest) { where: deptTaskWhere, _count: { id: true }, }), - // Overdue tasks (past due, not completed) + // Overdue tasks (past due within window, not completed) prisma.task.count({ where: { ...deptTaskWhere, - dueDate: { lt: today }, + dueDate: { gte: overdueFrom, lt: today }, status: { notIn: ['COMPLETED', 'CANCELLED', 'NA'] }, }, }), @@ -177,7 +182,7 @@ export async function GET(request: NextRequest) { ) const completedTasks = tasks.filter((t) => t.status === 'COMPLETED') 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( (t) => t.priority === 'HIGH' || t.priority === 'URGENT' @@ -251,6 +256,7 @@ export async function GET(request: NextRequest) { completedTasks: totalCompleted, completionRate, overdueTasks, + overdueWindowDays, dueTodayTasks, dueThisWeekTasks, activeUsers, diff --git a/ondeck/src/components/dashboard/workload-kpis.tsx b/ondeck/src/components/dashboard/workload-kpis.tsx index b2db6d5..411dae3 100644 --- a/ondeck/src/components/dashboard/workload-kpis.tsx +++ b/ondeck/src/components/dashboard/workload-kpis.tsx @@ -25,6 +25,7 @@ interface WorkloadData { completedTasks: number completionRate: number overdueTasks: number + overdueWindowDays: number dueTodayTasks: number dueThisWeekTasks: number activeUsers: number @@ -195,7 +196,7 @@ export function WorkloadKPIs({ department }: WorkloadKPIsProps = {}) { {summary.overdueTasks}

- {summary.dueTodayTasks} due today + past due within {summary.overdueWindowDays}d