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,168 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/db'
import { getSession } from '@/lib/auth'
import { getProjectWithTenantCheck } from '@/lib/tenant'
import { sendEmail } from '@/lib/email'
export async function POST(req: NextRequest, { params }: { params: { id: string } }) {
try {
const user = await getSession()
if (!user) return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 401 })
const project = await getProjectWithTenantCheck(params.id, user)
if (!project) return NextResponse.json({ error: 'Projekt nicht gefunden' }, { status: 404 })
// Load tenant logo
let tenantLogoUrl = ''
let tenantName = ''
if ((project as any).tenantId) {
const tenant = await (prisma as any).tenant.findUnique({
where: { id: (project as any).tenantId },
select: { logoUrl: true, name: true },
})
if (tenant?.logoUrl) tenantLogoUrl = tenant.logoUrl
if (tenant?.name) tenantName = tenant.name
}
const body = await req.json()
const { recipientEmail } = body
if (!recipientEmail) {
return NextResponse.json({ error: 'Empfänger-E-Mail erforderlich' }, { status: 400 })
}
// Load journal data
const entries = await (prisma as any).journalEntry.findMany({
where: { projectId: params.id },
orderBy: [{ time: 'asc' }, { sortOrder: 'asc' }],
})
const checkItems = await (prisma as any).journalCheckItem.findMany({
where: { projectId: params.id },
orderBy: { sortOrder: 'asc' },
})
const pendenzen = await (prisma as any).journalPendenz.findMany({
where: { projectId: params.id },
orderBy: { sortOrder: 'asc' },
})
// Build HTML report
const formatTime = (d: Date) => new Date(d).toLocaleTimeString('de-CH', { hour: '2-digit', minute: '2-digit' })
const formatDate = (d: Date) => new Date(d).toLocaleDateString('de-CH', { day: '2-digit', month: '2-digit', year: 'numeric' })
const p = project as any
let entriesHtml = ''
for (const e of entries) {
const correctedStyle = e.isCorrected ? 'text-decoration:line-through;opacity:0.5;' : ''
const correctionStyle = e.correctionOfId ? 'color:#b45309;font-style:italic;' : ''
const doneIcon = e.done ? '✅' : ''
const doneAtStr = e.done && e.doneAt ? ` <span style="color:#16a34a;font-size:10px;">(erledigt ${formatTime(e.doneAt)})</span>` : ''
entriesHtml += `
<tr style="${correctedStyle}${correctionStyle}">
<td style="padding:4px 8px;border-bottom:1px solid #e5e7eb;font-family:monospace;font-size:12px;white-space:nowrap;">${formatTime(e.time)}</td>
<td style="padding:4px 8px;border-bottom:1px solid #e5e7eb;font-size:13px;">${e.what}${e.isCorrected ? ' <span style="color:#ef4444;font-size:11px;">(korrigiert)</span>' : ''}${doneAtStr}</td>
<td style="padding:4px 8px;border-bottom:1px solid #e5e7eb;font-size:12px;color:#666;">${e.who || ''}</td>
<td style="padding:4px 8px;border-bottom:1px solid #e5e7eb;text-align:center;">${doneIcon}</td>
</tr>`
}
let checkHtml = ''
for (const c of checkItems) {
const confirmedTime = c.confirmed && c.confirmedAt ? ` <span style="font-size:10px;color:#666;">${formatTime(c.confirmedAt)}</span>` : ''
checkHtml += `
<tr>
<td style="padding:3px 8px;border-bottom:1px solid #e5e7eb;font-size:13px;">${c.label}${confirmedTime}</td>
<td style="padding:3px 8px;border-bottom:1px solid #e5e7eb;text-align:center;">${c.confirmed ? '✅' : ''}</td>
<td style="padding:3px 8px;border-bottom:1px solid #e5e7eb;text-align:center;">${c.ok ? '✅' : ''}</td>
</tr>`
}
let pendHtml = ''
for (const p of pendenzen) {
const pendDoneAt = p.done && p.doneAt ? ` <span style="color:#16a34a;font-size:10px;">(${formatTime(p.doneAt)})</span>` : ''
pendHtml += `
<tr>
<td style="padding:3px 8px;border-bottom:1px solid #e5e7eb;font-size:13px;">${p.what}${pendDoneAt}</td>
<td style="padding:3px 8px;border-bottom:1px solid #e5e7eb;font-size:12px;color:#666;">${p.who || ''}</td>
<td style="padding:3px 8px;border-bottom:1px solid #e5e7eb;font-size:12px;color:#666;">${p.whenHow || ''}</td>
<td style="padding:3px 8px;border-bottom:1px solid #e5e7eb;text-align:center;">${p.done ? '✅' : ''}</td>
</tr>`
}
const logoHtml = tenantLogoUrl
? `<img src="${tenantLogoUrl}" alt="${tenantName}" style="height:40px;max-width:120px;object-fit:contain;margin-right:16px;border-radius:4px;" />`
: ''
const html = `
<div style="font-family:sans-serif;max-width:800px;margin:0 auto;">
<div style="background:#dc2626;color:white;padding:20px 24px;border-radius:12px 12px 0 0;display:flex;align-items:center;">
${logoHtml}
<div>
<h1 style="margin:0;font-size:22px;">Einsatzrapport</h1>
<p style="margin:4px 0 0;opacity:0.9;">${p.title || 'Ohne Titel'}${tenantName ? `${tenantName}` : ''}</p>
</div>
</div>
<div style="border:1px solid #e5e7eb;border-top:none;padding:24px;border-radius:0 0 12px 12px;">
<div style="display:flex;gap:24px;margin-bottom:20px;flex-wrap:wrap;">
<div><strong>Standort:</strong> ${p.location || ''}</div>
<div><strong>Datum:</strong> ${p.createdAt ? formatDate(p.createdAt) : ''}</div>
</div>
<h2 style="font-size:16px;border-bottom:2px solid #dc2626;padding-bottom:4px;margin:20px 0 8px;">Journal-Einträge</h2>
<table style="width:100%;border-collapse:collapse;">
<thead>
<tr style="background:#f5f5f4;">
<th style="padding:6px 8px;text-align:left;font-size:11px;color:#666;border-bottom:2px solid #dc2626;">Zeit</th>
<th style="padding:6px 8px;text-align:left;font-size:11px;color:#666;border-bottom:2px solid #dc2626;">Was</th>
<th style="padding:6px 8px;text-align:left;font-size:11px;color:#666;border-bottom:2px solid #dc2626;">Wer</th>
<th style="padding:6px 8px;text-align:center;font-size:11px;color:#666;border-bottom:2px solid #dc2626;">Ok</th>
</tr>
</thead>
<tbody>${entriesHtml || '<tr><td colspan="4" style="padding:12px;text-align:center;color:#999;">Keine Einträge</td></tr>'}</tbody>
</table>
${checkItems.length > 0 ? `
<h2 style="font-size:16px;border-bottom:2px solid #dc2626;padding-bottom:4px;margin:20px 0 8px;">SOMA Checkliste</h2>
<table style="width:100%;border-collapse:collapse;">
<thead>
<tr style="background:#f5f5f4;">
<th style="padding:6px 8px;text-align:left;font-size:11px;color:#666;">Punkt</th>
<th style="padding:6px 8px;text-align:center;font-size:11px;color:#666;">Bestätigt</th>
<th style="padding:6px 8px;text-align:center;font-size:11px;color:#666;">Ok</th>
</tr>
</thead>
<tbody>${checkHtml}</tbody>
</table>` : ''}
${pendenzen.length > 0 ? `
<h2 style="font-size:16px;border-bottom:2px solid #dc2626;padding-bottom:4px;margin:20px 0 8px;">Pendenzen</h2>
<table style="width:100%;border-collapse:collapse;">
<thead>
<tr style="background:#f5f5f4;">
<th style="padding:6px 8px;text-align:left;font-size:11px;color:#666;">Was</th>
<th style="padding:6px 8px;text-align:left;font-size:11px;color:#666;">Wer</th>
<th style="padding:6px 8px;text-align:left;font-size:11px;color:#666;">Wann/Wie</th>
<th style="padding:6px 8px;text-align:center;font-size:11px;color:#666;">Erledigt</th>
</tr>
</thead>
<tbody>${pendHtml}</tbody>
</table>` : ''}
<hr style="margin:20px 0;border:none;border-top:1px solid #e5e7eb;" />
<p style="color:#999;font-size:11px;">Gesendet von Lageplan am ${new Date().toLocaleString('de-CH')} durch ${user.name || user.email}</p>
</div>
</div>`
await sendEmail(
recipientEmail,
`Einsatzrapport — ${p.title || 'Ohne Titel'}`,
html
)
return NextResponse.json({ success: true, message: `Rapport an ${recipientEmail} gesendet` })
} catch (error) {
console.error('Error sending report:', error)
return NextResponse.json({ error: 'Fehler beim Senden des Rapports' }, { status: 500 })
}
}