refactor: professional code quality audit

- Fix all TypeScript compilation errors (0 errors in src/)
- Add try/catch error handling to ALL API routes (38 handlers)
- Fix empty catch blocks (api-auth.ts now logs warnings)
- Add aria-labels to all icon-only buttons (WCAG 2.1 AA)
- Fix Prisma type mismatches (OR filter, Person.companyId, Estimate.projectId, Ticket.createdAt)
- Fix webhook PATCH route (moved to [id]/route.ts)
- Fix mobile-bottom-nav type safety
- Fix KB route where-clause type
- Fix test-mail banner null vs undefined
- Fix task comment fetch: add cancellation + error toast
- ESLint: 0 errors, 0 warnings
- 175 files, 24.741 lines of code
This commit is contained in:
Z User committed 2026-10-27 12:23:32 +00:00
1 parent 969202764a
commit fa8dd46571
175 files changed
+24741 -2

No files matched your search

+210
View File
@@ -0,0 +1,210 @@
import { NextResponse } from 'next/server'
import { db } from '@/lib/db'
export const dynamic = 'force-dynamic'
// GET /api/aos/notifications — aggregated alerts from all modules.
// Real checks: overdue invoices, expiring licenses/assets, SLA-breach tickets,
// pending approvals, expiring contracts, pending VAT returns.
export async function GET() {
try {
const now = new Date()
const in30Days = new Date(now.getTime() + 30 * 86400000)
const in7Days = new Date(now.getTime() + 7 * 86400000)
const [invoices, licenses, assets, tickets, contracts, approvals, vatReturn, expenses, estimates] = await Promise.all([
db.invoice.findMany({ where: { status: { in: ['overdue', 'sent'] } }, include: { company: true } }),
db.license.findMany({ where: { status: 'active', validUntil: { not: null } }, include: { company: true } }),
db.asset.findMany({ where: { status: { in: ['active', 'expiring'] } } }),
db.ticket.findMany({ where: { status: { in: ['open', 'pending'] } }, include: { messages: { orderBy: { createdAt: 'asc' }, take: 1 } } }),
db.contract.findMany({ where: { status: 'active', endDate: { not: null } } }),
db.portalApproval.findMany({ where: { status: 'pending' }, include: { project: true } }),
db.vatReturn.findFirst({ where: { status: { in: ['computed', 'prepared'] } }, orderBy: { periodYear: 'desc' } }),
db.expense.findMany({ where: { status: 'recorded' } }),
db.estimate.findMany({ where: { status: 'sent' } }),
])
type Notification = {
id: string
type: 'urgent' | 'warning' | 'info'
module: string
title: string
detail: string
entity?: string
createdAt: string
}
const notifications: Notification[] = []
// Overdue invoices
for (const inv of invoices.filter((i) => i.status === 'overdue')) {
const daysOver = Math.round((now.getTime() - inv.dueDate.getTime()) / 86400000)
notifications.push({
id: `inv-overdue-${inv.id}`,
type: 'urgent',
module: 'finance',
title: `Rechnung ${inv.number} überfällig`,
detail: `${inv.company.name} · ${daysOver} Tage überfällig · Mahnwesen prüfen`,
entity: inv.id,
createdAt: inv.dueDate.toISOString(),
})
}
// Sent invoices (due soon)
for (const inv of invoices.filter((i) => i.status === 'sent' && i.dueDate < in7Days)) {
const days = Math.round((inv.dueDate.getTime() - now.getTime()) / 86400000)
notifications.push({
id: `inv-due-${inv.id}`,
type: 'warning',
module: 'finance',
title: `Rechnung ${inv.number} bald fällig`,
detail: `${inv.company.name} · fällig in ${days} Tagen`,
entity: inv.id,
createdAt: inv.issueDate.toISOString(),
})
}
// Expiring licenses
for (const lic of licenses.filter((l) => l.validUntil! < in30Days)) {
const days = Math.round((lic.validUntil!.getTime() - now.getTime()) / 86400000)
notifications.push({
id: `lic-exp-${lic.id}`,
type: days < 0 ? 'urgent' : 'warning',
module: 'licenses',
title: `Lizenz ${lic.key.slice(0, 16)}… ${days < 0 ? 'abgelaufen' : 'läuft ab'}`,
detail: `${lic.company.name} · ${lic.product} · ${days < 0 ? `${Math.abs(days)} d drüber` : `${days} d`}`,
entity: lic.id,
createdAt: now.toISOString(),
})
}
// Expiring assets
for (const a of assets.filter((a) => a.renewalDate < in30Days)) {
const days = Math.round((a.renewalDate.getTime() - now.getTime()) / 86400000)
notifications.push({
id: `asset-exp-${a.id}`,
type: days < 0 ? 'urgent' : 'warning',
module: 'licenses',
title: `Asset ${a.name} ${days < 0 ? 'abgelaufen' : 'läuft ab'}`,
detail: `${a.type} · ${a.provider ?? '—'} · ${days < 0 ? `${Math.abs(days)} d drüber` : `${days} d`}`,
entity: a.id,
createdAt: now.toISOString(),
})
}
// Urgent / SLA-breached tickets
for (const t of tickets) {
if (t.priority === 'urgent') {
notifications.push({
id: `tkt-urgent-${t.id}`,
type: 'urgent',
module: 'support',
title: `Dringendes Ticket ${t.number}`,
detail: t.subject,
entity: t.id,
createdAt: t.createdAt.toISOString(),
})
}
// SLA response breach
if (!t.slaRespondedAt) {
const ageHours = (now.getTime() - t.createdAt.getTime()) / 3600000
if (ageHours > t.slaResponseHours) {
notifications.push({
id: `tkt-sla-${t.id}`,
type: 'urgent',
module: 'support',
title: `SLA-Verletzung: ${t.number}`,
detail: `Antwort-SLA überschritten (${Math.round(ageHours)} h / ${t.slaResponseHours} h)`,
entity: t.id,
createdAt: t.createdAt.toISOString(),
})
}
}
}
// Expiring contracts
for (const c of contracts.filter((c) => c.endDate! < in30Days)) {
const days = Math.round((c.endDate!.getTime() - now.getTime()) / 86400000)
notifications.push({
id: `con-exp-${c.id}`,
type: 'warning',
module: 'contracts',
title: `Vertrag ${c.number} läuft ab`,
detail: `${c.title} · ${days < 0 ? `${Math.abs(days)} d drüber` : `${days} d`}`,
entity: c.id,
createdAt: now.toISOString(),
})
}
// Pending portal approvals
for (const a of approvals) {
notifications.push({
id: `appr-pending-${a.id}`,
type: 'info',
module: 'portal',
title: `Freigabe wartet: ${a.title}`,
detail: a.project?.name ?? '—',
entity: a.id,
createdAt: a.createdAt.toISOString(),
})
}
// Pending VAT return
if (vatReturn) {
notifications.push({
id: `vat-pending-${vatReturn.id}`,
type: 'warning',
module: 'accounting',
title: `USt-Voranmeldung ${vatReturn.periodMonth}/${vatReturn.periodYear} ausstehend`,
detail: `Zahllast: ${(vatReturn.payableCents / 100).toFixed(2)} € · UStVA-XML erstellen`,
entity: vatReturn.id,
createdAt: now.toISOString(),
})
}
// Expenses pending approval
if (expenses.length > 0) {
notifications.push({
id: 'exp-pending',
type: 'info',
module: 'expenses',
title: `${expenses.length} Ausgaben warten auf Freigabe`,
detail: `Gesamt: ${(expenses.reduce((s, e) => s + e.grossCents, 0) / 100).toFixed(2)} € brutto`,
createdAt: now.toISOString(),
})
}
// Estimates expiring
for (const e of estimates.filter((e) => e.validUntil && e.validUntil < in7Days)) {
const days = Math.round((e.validUntil!.getTime() - now.getTime()) / 86400000)
notifications.push({
id: `est-exp-${e.id}`,
type: 'warning',
module: 'estimates',
title: `Angebot ${e.number} läuft bald ab`,
detail: `${e.subject} · ${days < 0 ? `${Math.abs(days)} d drüber` : `${days} d`}`,
entity: e.id,
createdAt: e.issueDate.toISOString(),
})
}
// sort by priority (urgent > warning > info) then date
const typeOrder = { urgent: 0, warning: 1, info: 2 }
notifications.sort((a, b) => {
if (typeOrder[a.type] !== typeOrder[b.type]) return typeOrder[a.type] - typeOrder[b.type]
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
})
const stats = {
total: notifications.length,
urgent: notifications.filter((n) => n.type === 'urgent').length,
warning: notifications.filter((n) => n.type === 'warning').length,
info: notifications.filter((n) => n.type === 'info').length,
byModule: notifications.reduce((acc, n) => { acc[n.module] = (acc[n.module] ?? 0) + 1; return acc }, {} as Record<string, number>),
}
return NextResponse.json({ notifications, stats })
} catch (err) {
const msg = err instanceof Error ? err.message : 'internal server error'
return NextResponse.json({ error: msg }, { status: 500 })
}
}