Initial commit: Lageplan v1.0 - Next.js 15.5, React 19

This commit is contained in:
Pepe Ziberi
2026-02-21 11:57:44 +01:00
commit adf3dc8c1d
167 changed files with 34265 additions and 0 deletions

View File

@@ -0,0 +1,185 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/db'
import { getSession, isServerAdmin } from '@/lib/auth'
import { sendEmail, getSmtpConfig } from '@/lib/email'
import { z } from 'zod'
const processSchema = z.object({
action: z.enum(['approve', 'reject']),
adminNote: z.string().max(500).optional(),
// Only for approve: override limits
maxUsers: z.number().min(1).max(1000).optional(),
maxProjects: z.number().min(1).max(10000).optional(),
})
// PATCH: Approve or reject an upgrade request (SERVER_ADMIN only)
export async function PATCH(
req: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const user = await getSession()
if (!user || !isServerAdmin(user.role)) {
return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 403 })
}
const body = await req.json()
const validated = processSchema.safeParse(body)
if (!validated.success) {
return NextResponse.json({ error: 'Ungültige Eingabe', details: validated.error.flatten() }, { status: 400 })
}
// Get the request
const upgradeReq = await (prisma as any).upgradeRequest.findUnique({
where: { id: params.id },
include: {
tenant: { select: { id: true, name: true, plan: true, contactEmail: true } },
requestedBy: { select: { name: true, email: true } },
},
})
if (!upgradeReq) {
return NextResponse.json({ error: 'Anfrage nicht gefunden' }, { status: 404 })
}
if (upgradeReq.status !== 'PENDING') {
return NextResponse.json({ error: 'Anfrage wurde bereits bearbeitet' }, { status: 400 })
}
const planLabels: Record<string, string> = {
FREE: 'Free', PRO: 'Pro',
}
const planLimits: Record<string, { maxUsers: number; maxProjects: number }> = {
FREE: { maxUsers: 5, maxProjects: 10 },
PRO: { maxUsers: 999, maxProjects: 9999 },
}
if (validated.data.action === 'approve') {
const limits = planLimits[upgradeReq.requestedPlan] || planLimits.PRO
// Update tenant plan
await (prisma as any).tenant.update({
where: { id: upgradeReq.tenantId },
data: {
plan: upgradeReq.requestedPlan,
subscriptionStatus: 'ACTIVE',
trialEndsAt: null,
maxUsers: validated.data.maxUsers || limits.maxUsers,
maxProjects: validated.data.maxProjects || limits.maxProjects,
},
})
// Update request status
await (prisma as any).upgradeRequest.update({
where: { id: params.id },
data: {
status: 'APPROVED',
adminNote: validated.data.adminNote || null,
processedAt: new Date(),
processedById: user.id,
},
})
// Send approval email to requester
const smtpConfig = await getSmtpConfig()
if (smtpConfig) {
try {
await sendEmail(
upgradeReq.requestedBy.email,
`Upgrade bestätigt — ${planLabels[upgradeReq.requestedPlan]} Plan aktiviert`,
`
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 520px; margin: 0 auto;">
<div style="background: #16a34a; color: white; padding: 16px 24px; border-radius: 8px 8px 0 0;">
<h2 style="margin: 0; font-size: 18px;">✓ Upgrade bestätigt</h2>
</div>
<div style="border: 1px solid #e5e7eb; border-top: none; padding: 24px; border-radius: 0 0 8px 8px;">
<p style="margin: 0 0 16px; line-height: 1.6; color: #374151;">
Ihr Upgrade für <strong>${upgradeReq.tenant.name}</strong> wurde bestätigt und ist ab sofort aktiv.
</p>
<table style="width: 100%; border-collapse: collapse; margin: 16px 0;">
<tr>
<td style="padding: 8px 0; color: #6b7280; font-size: 14px;">Neuer Plan</td>
<td style="padding: 8px 0; font-weight: 600; text-align: right; color: #16a34a;">${planLabels[upgradeReq.requestedPlan]}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #6b7280; font-size: 14px;">Max. Benutzer</td>
<td style="padding: 8px 0; font-weight: 600; text-align: right;">${validated.data.maxUsers || limits.maxUsers}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #6b7280; font-size: 14px;">Max. Projekte</td>
<td style="padding: 8px 0; font-weight: 600; text-align: right;">${validated.data.maxProjects || limits.maxProjects}</td>
</tr>
</table>
${validated.data.adminNote ? `<p style="margin: 16px 0 0; padding: 12px; background: #f0fdf4; border-radius: 6px; font-size: 14px; color: #374151;"><strong>Hinweis:</strong><br/>${validated.data.adminNote}</p>` : ''}
<p style="margin: 16px 0 0; font-size: 13px; color: #6b7280;">
Vielen Dank für Ihr Vertrauen. Bei Fragen stehen wir Ihnen gerne zur Verfügung.
</p>
<hr style="border: none; border-top: 1px solid #e5e7eb; margin: 20px 0;" />
<p style="margin: 0; font-size: 12px; color: #9ca3af;">Lageplan — Digitale Lagepläne für die Feuerwehr</p>
</div>
</div>
`
)
} catch (e) {
console.error('Failed to send approval email:', e)
}
}
} else {
// Reject
await (prisma as any).upgradeRequest.update({
where: { id: params.id },
data: {
status: 'REJECTED',
adminNote: validated.data.adminNote || null,
processedAt: new Date(),
processedById: user.id,
},
})
// Send rejection email
const smtpConfig = await getSmtpConfig()
if (smtpConfig) {
try {
await sendEmail(
upgradeReq.requestedBy.email,
`Upgrade-Anfrage — Rückmeldung`,
`
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 520px; margin: 0 auto;">
<div style="background: #6b7280; color: white; padding: 16px 24px; border-radius: 8px 8px 0 0;">
<h2 style="margin: 0; font-size: 18px;">Upgrade-Anfrage</h2>
</div>
<div style="border: 1px solid #e5e7eb; border-top: none; padding: 24px; border-radius: 0 0 8px 8px;">
<p style="margin: 0 0 16px; line-height: 1.6; color: #374151;">
Ihre Upgrade-Anfrage für <strong>${upgradeReq.tenant.name}</strong> auf den <strong>${planLabels[upgradeReq.requestedPlan]}</strong>-Plan konnte leider nicht bestätigt werden.
</p>
${validated.data.adminNote ? `<p style="margin: 0 0 16px; padding: 12px; background: #f9fafb; border-radius: 6px; font-size: 14px; color: #374151;"><strong>Begründung:</strong><br/>${validated.data.adminNote}</p>` : ''}
<p style="margin: 0; font-size: 13px; color: #6b7280;">
Bei Fragen kontaktieren Sie uns bitte unter app@lageplan.ch. Sie können jederzeit eine neue Anfrage stellen.
</p>
<hr style="border: none; border-top: 1px solid #e5e7eb; margin: 20px 0;" />
<p style="margin: 0; font-size: 12px; color: #9ca3af;">Lageplan — Digitale Lagepläne für die Feuerwehr</p>
</div>
</div>
`
)
} catch (e) {
console.error('Failed to send rejection email:', e)
}
}
}
// Return updated request
const updated = await (prisma as any).upgradeRequest.findUnique({
where: { id: params.id },
include: {
tenant: { select: { name: true, slug: true, plan: true, subscriptionStatus: true } },
requestedBy: { select: { name: true, email: true } },
},
})
return NextResponse.json({ request: updated })
} catch (error) {
console.error('Error processing upgrade request:', error)
return NextResponse.json({ error: 'Serverfehler' }, { status: 500 })
}
}