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

+150
View File
@@ -0,0 +1,150 @@
import { NextResponse } from 'next/server'
import { db } from '@/lib/db'
export const dynamic = 'force-dynamic'
// GET /api/aos/timesheet?week=YYYY-MM-DD
// Returns a weekly timesheet grid: projects × days (Mon-Sun)
// with time entries per cell + totals + lock status.
export async function GET(req: Request) {
try {
const { searchParams } = new URL(req.url)
const weekParam = searchParams.get('week') // a date within the target week (YYYY-MM-DD)
const now = new Date()
let refDate = now
if (weekParam) {
const d = new Date(weekParam + 'T00:00:00Z')
if (!isNaN(d.getTime())) refDate = d
}
// Calculate the Monday of the target week (ISO week, Monday start)
const day = refDate.getDay()
const offset = day === 0 ? -6 : 1 - day // Sunday=0 → -6; Monday=1 → 0; etc.
const monday = new Date(refDate)
monday.setDate(refDate.getDate() + offset)
monday.setHours(0, 0, 0, 0)
const days: Date[] = []
for (let i = 0; i < 7; i++) {
const d = new Date(monday)
d.setDate(monday.getDate() + i)
days.push(d)
}
const sunday = new Date(days[6]!)
sunday.setHours(23, 59, 59, 999)
// Fetch all time entries in this week
const entries = await db.timeEntry.findMany({
where: { date: { gte: monday, lte: sunday } },
include: { project: { select: { id: true, name: true } } },
orderBy: { date: 'asc' },
})
// Group by project
const projectMap = new Map<string, { id: string; name: string; entries: typeof entries }>()
for (const e of entries) {
if (!projectMap.has(e.project.id)) {
projectMap.set(e.project.id, { id: e.project.id, name: e.project.name, entries: [] })
}
projectMap.get(e.project.id)!.entries.push(e)
}
// Build grid: per project, per day, sum minutes
const grid = Array.from(projectMap.values()).map((p) => {
const dayData = days.map((d) => {
const dayEntries = p.entries.filter((e) => {
const ed = new Date(e.date)
return ed.getDate() === d.getDate() && ed.getMonth() === d.getMonth() && ed.getFullYear() === d.getFullYear()
})
const minutes = dayEntries.reduce((s, e) => s + e.minutes, 0)
const locked = dayEntries.every((e) => e.locked)
return {
date: d.toISOString(),
minutes,
locked,
entries: dayEntries.map((e) => ({ id: e.id, description: e.description, minutes: e.minutes, billable: e.billable, locked: e.locked })),
}
})
const totalMinutes = dayData.reduce((s, d) => s + d.minutes, 0)
return {
project: { id: p.id, name: p.name },
days: dayData,
totalMinutes,
}
})
// Day totals
const dayTotals = days.map((d, i) => ({
date: d.toISOString(),
label: d.toLocaleDateString('de-DE', { weekday: 'short' }),
dayNum: d.getDate(),
minutes: grid.reduce((s, p) => s + p.days[i]!.minutes, 0),
}))
const grandTotalMinutes = grid.reduce((s, p) => s + p.totalMinutes, 0)
const billableMinutes = entries.filter((e) => e.billable).reduce((s, e) => s + e.minutes, 0)
const lockedCount = entries.filter((e) => e.locked).length
const allLocked = entries.length > 0 && entries.every((e) => e.locked)
const weekLabel = `${monday.toLocaleDateString('de-DE', { day: '2-digit', month: 'short' })} – ${sunday.toLocaleDateString('de-DE', { day: '2-digit', month: 'short', year: 'numeric' })}`
return NextResponse.json({
week: { start: monday.toISOString(), end: sunday.toISOString(), label: weekLabel },
grid,
dayTotals,
stats: {
grandTotalMinutes,
billableMinutes,
lockedCount,
totalEntries: entries.length,
allLocked,
projectCount: grid.length,
},
})
} catch (err) {
const msg = err instanceof Error ? err.message : 'internal server error'
return NextResponse.json({ error: msg }, { status: 500 })
}
}
// PATCH /api/aos/timesheet — lock all entries in a week (PM approval)
export async function PATCH(req: Request) {
try {
const { week } = (await req.json()) as { week?: string }
if (!week) return NextResponse.json({ error: 'week required (YYYY-MM-DD)' }, { status: 400 })
const refDate = new Date(week + 'T00:00:00Z')
if (isNaN(refDate.getTime())) return NextResponse.json({ error: 'invalid date' }, { status: 400 })
const day = refDate.getDay()
const offset = day === 0 ? -6 : 1 - day
const monday = new Date(refDate)
monday.setDate(refDate.getDate() + offset)
monday.setHours(0, 0, 0, 0)
const sunday = new Date(monday)
sunday.setDate(monday.getDate() + 6)
sunday.setHours(23, 59, 59, 999)
const result = await db.timeEntry.updateMany({
where: { date: { gte: monday, lte: sunday }, locked: false },
data: { locked: true },
})
await db.auditLog.create({
data: {
actor: 'aylin@nordlicht.digital',
action: 'timesheet.lock:week',
entity: 'TimeEntry',
entityId: 'batch',
after: JSON.stringify({ week: monday.toISOString().slice(0, 10), locked: result.count }),
hash: 'sha256:' + Math.random().toString(36).slice(2),
},
})
return NextResponse.json({ locked: result.count, week: monday.toISOString().slice(0, 10) })
} catch (err) {
const msg = err instanceof Error ? err.message : 'internal server error'
return NextResponse.json({ error: msg }, { status: 500 })
}
}