63ef43ec-90ce-4fcf-82b4-abff91b2280a
This commit is contained in:
1 parent
8fa25577c0
commit
c71c7e3094
12 files changed
+578
-9
No files matched your search
@@ -22,6 +22,7 @@ export async function POST() {
|
||||
await db.apiToken.deleteMany()
|
||||
await db.portalApproval.deleteMany()
|
||||
await db.article.deleteMany()
|
||||
await db.taskComment.deleteMany()
|
||||
await db.estimateLine.deleteMany()
|
||||
await db.estimate.deleteMany()
|
||||
await db.ticketMessage.deleteMany()
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { db } from '@/lib/db'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// GET /api/aos/tasks/[id]/comments — list comments on a task
|
||||
export async function GET(
|
||||
_req: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await params
|
||||
const comments = await db.taskComment.findMany({
|
||||
where: { taskId: id },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
})
|
||||
return NextResponse.json({ comments })
|
||||
}
|
||||
|
||||
// POST /api/aos/tasks/[id]/comments — add a comment
|
||||
export async function POST(
|
||||
req: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await params
|
||||
const { body, author } = (await req.json()) as { body: string; author?: string }
|
||||
if (!body?.trim()) return NextResponse.json({ error: 'body required' }, { status: 400 })
|
||||
|
||||
const comment = await db.taskComment.create({
|
||||
data: { taskId: id, body: body.trim(), author: author ?? 'Max Brandt' },
|
||||
})
|
||||
|
||||
await db.auditLog.create({
|
||||
data: {
|
||||
actor: author ?? 'max@nordlicht.digital',
|
||||
action: 'task.comment:create',
|
||||
entity: 'TaskComment',
|
||||
entityId: comment.id,
|
||||
after: JSON.stringify({ taskId: id, length: body.length }),
|
||||
hash: 'sha256:' + Math.random().toString(36).slice(2),
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ comment })
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
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) {
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// PATCH /api/aos/timesheet — lock all entries in a week (PM approval)
|
||||
export async function PATCH(req: Request) {
|
||||
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) })
|
||||
}
|
||||
Reference in new issue
Block a user