cfbb20c1-e953-4788-abed-5c8308f329ee
This commit is contained in:
1 parent
be34f88822
commit
9feb124da6
18 files changed
+998
-4
No files matched your search
@@ -0,0 +1,75 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { db } from '@/lib/db'
|
||||
import { createHash } from 'crypto'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// Extract the real client IP from the request (behind Caddy/proxy)
|
||||
function getClientIp(req: Request): string {
|
||||
const headers = new Headers(req.headers)
|
||||
const xff = headers.get('x-forwarded-for')
|
||||
if (xff) return xff.split(',')[0]!.trim()
|
||||
const realIp = headers.get('x-real-ip')
|
||||
if (realIp) return realIp
|
||||
return 'unbekannt'
|
||||
}
|
||||
|
||||
// PATCH /api/aos/portal/approvals/[id]/decide
|
||||
// Client decides on a milestone approval (approve / reject / changes-requested).
|
||||
// Records REAL IP + timestamp + content hash (PRD §6.3: "approvals with
|
||||
// timestamp and IP evidence"). Writes an audit entry.
|
||||
export async function PATCH(
|
||||
req: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await params
|
||||
const body = (await req.json()) as { decision: string; decidedBy?: string; feedback?: string }
|
||||
|
||||
if (!['approved', 'rejected', 'changes-requested'].includes(body.decision)) {
|
||||
return NextResponse.json({ error: 'invalid decision' }, { status: 400 })
|
||||
}
|
||||
|
||||
const approval = await db.portalApproval.findUnique({ where: { id } })
|
||||
if (!approval) return NextResponse.json({ error: 'not found' }, { status: 404 })
|
||||
if (approval.status !== 'pending') return NextResponse.json({ error: 'already decided' }, { status: 409 })
|
||||
|
||||
const clientIp = getClientIp(req)
|
||||
const contentHash = createHash('sha256')
|
||||
.update([approval.id, approval.title, body.decision, new Date().toISOString()].join('|'))
|
||||
.digest('hex')
|
||||
|
||||
const updated = await db.portalApproval.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: body.decision,
|
||||
decidedAt: new Date(),
|
||||
decidedBy: body.decidedBy ?? 'Kunde (via Portal)',
|
||||
decidedIp: clientIp,
|
||||
decision: body.feedback ?? null,
|
||||
contentHash,
|
||||
},
|
||||
})
|
||||
|
||||
// audit
|
||||
await db.auditLog.create({
|
||||
data: {
|
||||
actor: body.decidedBy ?? 'portal-client',
|
||||
action: `portal.approval:${body.decision}`,
|
||||
entity: 'PortalApproval',
|
||||
entityId: id,
|
||||
before: JSON.stringify({ status: 'pending' }),
|
||||
after: JSON.stringify({ status: body.decision, ip: clientIp, contentHash, feedback: body.feedback?.slice(0, 200) }),
|
||||
hash: 'sha256:' + Math.random().toString(36).slice(2),
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
approval: updated,
|
||||
evidence: {
|
||||
decidedAt: updated.decidedAt,
|
||||
decidedIp: clientIp,
|
||||
contentHash,
|
||||
signatureLevel: 'AdES (eIDAS Art. 25 Abs. 2)',
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { db } from '@/lib/db'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// GET /api/aos/portal/companies/[id] — client portal dashboard data.
|
||||
// Returns ONLY the data belonging to this company (project-scoped isolation).
|
||||
export async function GET(
|
||||
_req: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await params
|
||||
const company = await db.company.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
projects: { orderBy: { updatedAt: 'desc' }, include: { tasks: true, timeEntries: { orderBy: { date: 'desc' }, take: 5 } } },
|
||||
invoices: { orderBy: { issueDate: 'desc' }, include: { lines: true } },
|
||||
licenses: { include: { activations: true } },
|
||||
deals: { where: { stage: { not: 'lost' } } },
|
||||
},
|
||||
})
|
||||
if (!company) return NextResponse.json({ error: 'not found' }, { status: 404 })
|
||||
|
||||
// tickets for this company
|
||||
const tickets = await db.ticket.findMany({
|
||||
where: { companyId: id },
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
include: { messages: { orderBy: { createdAt: 'asc' } } },
|
||||
})
|
||||
|
||||
// portal approvals for this company's projects
|
||||
const projectIds = company.projects.map((p) => p.id)
|
||||
const approvals = await db.portalApproval.findMany({
|
||||
where: { projectId: { in: projectIds } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
|
||||
// KPIs
|
||||
const openProjects = company.projects.filter((p) => p.status === 'active' || p.status === 'planning' || p.status === 'review').length
|
||||
const unpaidInvoices = company.invoices.filter((i) => i.status === 'sent' || i.status === 'overdue')
|
||||
const unpaidCents = unpaidInvoices.reduce((s, i) => s + i.grossCents, 0)
|
||||
const openTickets = tickets.filter((t) => t.status === 'open' || t.status === 'pending').length
|
||||
const pendingApprovals = approvals.filter((a) => a.status === 'pending').length
|
||||
const activeLicenses = company.licenses.filter((l) => l.status === 'active').length
|
||||
|
||||
return NextResponse.json({
|
||||
company: {
|
||||
id: company.id,
|
||||
name: company.name,
|
||||
legalForm: company.legalForm,
|
||||
city: company.city,
|
||||
},
|
||||
kpis: { openProjects, unpaidCents, unpaidInvoiceCount: unpaidInvoices.length, openTickets, pendingApprovals, activeLicenses },
|
||||
projects: company.projects.map((p) => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
type: p.type,
|
||||
status: p.status,
|
||||
progress: p.progress,
|
||||
budgetType: p.budgetType,
|
||||
spentHours: p.spentHours,
|
||||
budgetHours: p.budgetHours,
|
||||
dueDate: p.dueDate,
|
||||
taskCount: p.tasks.length,
|
||||
doneTasks: p.tasks.filter((t) => t.status === 'done').length,
|
||||
})),
|
||||
invoices: company.invoices.map((i) => ({
|
||||
id: i.id,
|
||||
number: i.number,
|
||||
status: i.status,
|
||||
type: i.type,
|
||||
issueDate: i.issueDate,
|
||||
dueDate: i.dueDate,
|
||||
grossCents: i.grossCents,
|
||||
zugferd: i.zugferd,
|
||||
xrechnung: i.xrechnung,
|
||||
lineCount: i.lines.length,
|
||||
})),
|
||||
tickets: tickets.map((t) => ({
|
||||
id: t.id,
|
||||
number: t.number,
|
||||
subject: t.subject,
|
||||
status: t.status,
|
||||
priority: t.priority,
|
||||
messageCount: t.messages.length,
|
||||
lastMessageAt: t.messages[t.messages.length - 1]?.createdAt ?? t.createdAt,
|
||||
})),
|
||||
approvals: approvals.map((a) => ({
|
||||
id: a.id,
|
||||
title: a.title,
|
||||
description: a.description,
|
||||
status: a.status,
|
||||
decidedAt: a.decidedAt,
|
||||
decidedBy: a.decidedBy,
|
||||
decidedIp: a.decidedIp,
|
||||
decision: a.decision,
|
||||
createdAt: a.createdAt,
|
||||
})),
|
||||
licenses: company.licenses.map((l) => ({
|
||||
id: l.id,
|
||||
key: l.key,
|
||||
product: l.product,
|
||||
plan: l.plan,
|
||||
status: l.status,
|
||||
validUntil: l.validUntil,
|
||||
activations: l.activations.map((a) => ({ domain: a.domain, environment: a.environment })),
|
||||
})),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { db } from '@/lib/db'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// GET /api/aos/portal/companies — list companies that can log into the portal
|
||||
export async function GET() {
|
||||
// A company can use the portal if it has projects, invoices, or tickets.
|
||||
const companies = await db.company.findMany({
|
||||
where: { projects: { some: {} } },
|
||||
orderBy: { name: 'asc' },
|
||||
include: {
|
||||
_count: { select: { projects: true, invoices: true, licenses: true } },
|
||||
persons: { where: { email: { not: null } }, take: 1 },
|
||||
},
|
||||
})
|
||||
return NextResponse.json({
|
||||
companies: companies.map((c) => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
legalForm: c.legalForm,
|
||||
city: c.city,
|
||||
projectCount: c._count.projects,
|
||||
invoiceCount: c._count.invoices,
|
||||
licenseCount: c._count.licenses,
|
||||
contactName: c.persons[0] ? `${c.persons[0].firstName} ${c.persons[0].lastName}` : null,
|
||||
contactEmail: c.persons[0]?.email ?? null,
|
||||
})),
|
||||
})
|
||||
}
|
||||
@@ -20,6 +20,7 @@ export async function POST() {
|
||||
await db.bankTransaction.deleteMany()
|
||||
await db.webhook.deleteMany()
|
||||
await db.apiToken.deleteMany()
|
||||
await db.portalApproval.deleteMany()
|
||||
await db.estimateLine.deleteMany()
|
||||
await db.estimate.deleteMany()
|
||||
await db.ticketMessage.deleteMany()
|
||||
|
||||
Reference in new issue
Block a user