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:
parent
27247f77f0
commit
191698088b
5 changed files with 129 additions and 7 deletions
|
|
@ -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() {
|
|||
</div>
|
||||
|
||||
{/* Sync config inline editor */}
|
||||
<SystemSettingsForm syncEnabled={syncEnabled === 'true'} syncSchedule={syncSchedule} />
|
||||
<SystemSettingsForm syncEnabled={syncEnabled === 'true'} syncSchedule={syncSchedule} overdueWindowDays={overdueWindowDays} />
|
||||
|
||||
{/* Links to subsection settings pages */}
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
|
|
@ -82,5 +106,38 @@ export function SystemSettingsForm({ syncEnabled: initialEnabled, syncSchedule:
|
|||
</Button>
|
||||
</CardContent>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
57
ondeck/src/app/api/admin/manager-settings/route.ts
Normal file
57
ondeck/src/app/api/admin/manager-settings/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{summary.dueTodayTasks} due today
|
||||
past due within {summary.overdueWindowDays}d
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue