232 lines
8.7 KiB
TypeScript
232 lines
8.7 KiB
TypeScript
import { NextResponse } from 'next/server'
|
|
import { db } from '@/lib/db'
|
|
|
|
export const dynamic = 'force-dynamic'
|
|
|
|
// GET /api/aos/calendar?month=YYYY-MM
|
|
// Aggregates ALL deadlines across modules into a unified calendar:
|
|
// - VAT returns (submission due date = 10th of following month)
|
|
// - Contract expiry/renewal dates
|
|
// - License expiry dates
|
|
// - Asset renewal dates
|
|
// - Project due dates
|
|
// - Invoice due dates
|
|
// - Ticket SLA deadlines
|
|
export async function GET(req: Request) {
|
|
const { searchParams } = new URL(req.url)
|
|
const monthParam = searchParams.get('month') // YYYY-MM
|
|
const now = new Date()
|
|
|
|
// Determine the calendar range: current month ± 1 month for context
|
|
let year = now.getFullYear()
|
|
let month = now.getMonth() // 0-indexed
|
|
if (monthParam) {
|
|
const [y, m] = monthParam.split('-').map(Number)
|
|
if (y && m) { year = y; month = m - 1 }
|
|
}
|
|
|
|
const monthStart = new Date(year, month, 1)
|
|
const monthEnd = new Date(year, month + 1, 0, 23, 59, 59)
|
|
// Expand range to include ±7 days for calendar grid overlap
|
|
const rangeStart = new Date(monthStart)
|
|
rangeStart.setDate(rangeStart.getDate() - 7)
|
|
const rangeEnd = new Date(monthEnd)
|
|
rangeEnd.setDate(rangeEnd.getDate() + 7)
|
|
|
|
type Deadline = {
|
|
id: string
|
|
date: string
|
|
module: string
|
|
type: string
|
|
title: string
|
|
detail: string
|
|
urgency: 'past' | 'today' | 'soon' | 'future'
|
|
daysUntil: number
|
|
}
|
|
const deadlines: Deadline[] = []
|
|
|
|
// 1. VAT returns — due on the 10th of the following month
|
|
const vatReturns = await db.vatReturn.findMany({ where: { status: { in: ['computed', 'prepared', 'submitted'] } } })
|
|
for (const v of vatReturns) {
|
|
// Due date: 10th of the month AFTER the period
|
|
const dueDate = new Date(v.periodYear, v.periodMonth, 10) // periodMonth is 1-indexed, so month=periodMonth means next month
|
|
const days = Math.round((dueDate.getTime() - now.getTime()) / 86400000)
|
|
deadlines.push({
|
|
id: `vat-${v.id}`,
|
|
date: dueDate.toISOString(),
|
|
module: 'accounting',
|
|
type: 'USt-Voranmeldung',
|
|
title: `UStVA ${String(v.periodMonth).padStart(2, '0')}/${v.periodYear}`,
|
|
detail: `Zahllast: ${(v.payableCents / 100).toFixed(2)} € · Status: ${v.status}`,
|
|
urgency: days < 0 ? 'past' : days === 0 ? 'today' : days <= 7 ? 'soon' : 'future',
|
|
daysUntil: days,
|
|
})
|
|
}
|
|
|
|
// 2. Contract expiry
|
|
const contracts = await db.contract.findMany({ where: { status: 'active', endDate: { not: null } } })
|
|
for (const c of contracts) {
|
|
const days = Math.round((c.endDate!.getTime() - now.getTime()) / 86400000)
|
|
deadlines.push({
|
|
id: `con-${c.id}`,
|
|
date: c.endDate!.toISOString(),
|
|
module: 'contracts',
|
|
type: 'Vertrag-Ablauf',
|
|
title: `${c.number} läuft ab`,
|
|
detail: c.title,
|
|
urgency: days < 0 ? 'past' : days === 0 ? 'today' : days <= 14 ? 'soon' : 'future',
|
|
daysUntil: days,
|
|
})
|
|
}
|
|
|
|
// 3. License expiry
|
|
const licenses = await db.license.findMany({ where: { status: 'active', validUntil: { not: null } }, include: { company: true } })
|
|
for (const l of licenses) {
|
|
const days = Math.round((l.validUntil!.getTime() - now.getTime()) / 86400000)
|
|
deadlines.push({
|
|
id: `lic-${l.id}`,
|
|
date: l.validUntil!.toISOString(),
|
|
module: 'licenses',
|
|
type: 'Lizenz-Ablauf',
|
|
title: `${l.product} (${l.company.name})`,
|
|
detail: `Key: ${l.key.slice(0, 20)}…`,
|
|
urgency: days < 0 ? 'past' : days === 0 ? 'today' : days <= 14 ? 'soon' : 'future',
|
|
daysUntil: days,
|
|
})
|
|
}
|
|
|
|
// 4. Asset renewals
|
|
const assets = await db.asset.findMany({ where: { status: { in: ['active', 'expiring'] } } })
|
|
for (const a of assets) {
|
|
const days = Math.round((a.renewalDate.getTime() - now.getTime()) / 86400000)
|
|
deadlines.push({
|
|
id: `asset-${a.id}`,
|
|
date: a.renewalDate.toISOString(),
|
|
module: 'licenses',
|
|
type: 'Asset-Erneuerung',
|
|
title: a.name,
|
|
detail: `${a.type} · ${a.provider ?? '—'}`,
|
|
urgency: days < 0 ? 'past' : days === 0 ? 'today' : days <= 14 ? 'soon' : 'future',
|
|
daysUntil: days,
|
|
})
|
|
}
|
|
|
|
// 5. Project due dates
|
|
const projects = await db.project.findMany({ where: { dueDate: { not: null }, status: { in: ['planning', 'active', 'review'] } } })
|
|
for (const p of projects) {
|
|
const days = Math.round((p.dueDate!.getTime() - now.getTime()) / 86400000)
|
|
deadlines.push({
|
|
id: `proj-${p.id}`,
|
|
date: p.dueDate!.toISOString(),
|
|
module: 'projects',
|
|
type: 'Projekt-Deadline',
|
|
title: p.name,
|
|
detail: `${p.progress}% fertig · ${p.status}`,
|
|
urgency: days < 0 ? 'past' : days === 0 ? 'today' : days <= 7 ? 'soon' : 'future',
|
|
daysUntil: days,
|
|
})
|
|
}
|
|
|
|
// 6. Invoice due dates
|
|
const invoices = await db.invoice.findMany({ where: { status: { in: ['sent', 'overdue'] } }, include: { company: true } })
|
|
for (const i of invoices) {
|
|
const days = Math.round((i.dueDate.getTime() - now.getTime()) / 86400000)
|
|
deadlines.push({
|
|
id: `inv-${i.id}`,
|
|
date: i.dueDate.toISOString(),
|
|
module: 'finance',
|
|
type: 'Rechnung-Fällig',
|
|
title: `${i.number} (${i.company.name})`,
|
|
detail: `${(i.grossCents / 100).toFixed(2)} €`,
|
|
urgency: days < 0 ? 'past' : days === 0 ? 'today' : days <= 7 ? 'soon' : 'future',
|
|
daysUntil: days,
|
|
})
|
|
}
|
|
|
|
// 7. Ticket SLA deadlines (resolve-by)
|
|
const tickets = await db.ticket.findMany({ where: { status: { in: ['open', 'pending'] } } })
|
|
for (const t of tickets) {
|
|
const slaDeadline = new Date(t.createdAt.getTime() + t.slaResolveHours * 3600000)
|
|
const days = Math.round((slaDeadline.getTime() - now.getTime()) / 86400000)
|
|
deadlines.push({
|
|
id: `tkt-${t.id}`,
|
|
date: slaDeadline.toISOString(),
|
|
module: 'support',
|
|
type: 'Ticket-SLA',
|
|
title: `${t.number}: ${t.subject}`,
|
|
detail: `SLA-Lösung in ${t.slaResolveHours} h (Priorität: ${t.priority})`,
|
|
urgency: days < 0 ? 'past' : days === 0 ? 'today' : days <= 1 ? 'soon' : 'future',
|
|
daysUntil: days,
|
|
})
|
|
}
|
|
|
|
// 8. Portal approvals (pending — no hard deadline, but track)
|
|
const approvals = await db.portalApproval.findMany({ where: { status: 'pending' } })
|
|
for (const a of approvals) {
|
|
deadlines.push({
|
|
id: `appr-${a.id}`,
|
|
date: a.createdAt.toISOString(),
|
|
module: 'portal',
|
|
type: 'Freigabe-wartend',
|
|
title: a.title,
|
|
detail: `Eingereicht ${new Date(a.createdAt).toLocaleDateString('de-DE')}`,
|
|
urgency: 'future',
|
|
daysUntil: Math.round((now.getTime() - a.createdAt.getTime()) / 86400000),
|
|
})
|
|
}
|
|
|
|
// Sort by date
|
|
deadlines.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
|
|
|
|
// Build calendar grid: weeks starting Monday
|
|
const firstDay = new Date(year, month, 1)
|
|
const lastDay = new Date(year, month + 1, 0)
|
|
// Monday = 0 in our grid (JS Sunday = 0)
|
|
const startOffset = (firstDay.getDay() + 6) % 7 // days from Monday
|
|
const gridStart = new Date(firstDay)
|
|
gridStart.setDate(gridStart.getDate() - startOffset)
|
|
|
|
const weeks: { date: Date; isCurrentMonth: boolean; deadlines: Deadline[] }[][] = []
|
|
const gridDate = new Date(gridStart)
|
|
for (let w = 0; w < 6; w++) {
|
|
const week: { date: Date; isCurrentMonth: boolean; deadlines: Deadline[] }[] = []
|
|
for (let d = 0; d < 7; d++) {
|
|
const dayDeadlines = deadlines.filter((dl) => {
|
|
const dlDate = new Date(dl.date)
|
|
return dlDate.getDate() === gridDate.getDate() && dlDate.getMonth() === gridDate.getMonth() && dlDate.getFullYear() === gridDate.getFullYear()
|
|
})
|
|
week.push({
|
|
date: new Date(gridDate),
|
|
isCurrentMonth: gridDate.getMonth() === month,
|
|
deadlines: dayDeadlines,
|
|
})
|
|
gridDate.setDate(gridDate.getDate() + 1)
|
|
}
|
|
weeks.push(week)
|
|
// stop if we've passed the last day and the week is complete
|
|
if (gridDate > lastDay && gridDate.getMonth() !== month) break
|
|
}
|
|
|
|
// Upcoming (next 14 days, excluding past)
|
|
const upcoming = deadlines.filter((d) => d.daysUntil >= 0 && d.daysUntil <= 14)
|
|
const overdue = deadlines.filter((d) => d.daysUntil < 0)
|
|
|
|
const stats = {
|
|
total: deadlines.length,
|
|
overdue: overdue.length,
|
|
today: deadlines.filter((d) => d.daysUntil === 0).length,
|
|
thisWeek: deadlines.filter((d) => d.daysUntil > 0 && d.daysUntil <= 7).length,
|
|
byModule: deadlines.reduce((acc, d) => { acc[d.module] = (acc[d.module] ?? 0) + 1; return acc }, {} as Record<string, number>),
|
|
}
|
|
|
|
return NextResponse.json({
|
|
month: `${year}-${String(month + 1).padStart(2, '0')}`,
|
|
monthLabel: new Date(year, month, 1).toLocaleDateString('de-DE', { month: 'long', year: 'numeric' }),
|
|
weeks,
|
|
upcoming,
|
|
overdue,
|
|
stats,
|
|
modules: ['accounting', 'contracts', 'licenses', 'projects', 'finance', 'support', 'portal'],
|
|
})
|
|
}
|