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), } 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'], }) }