Initial commit: Lageplan v1.0 - Next.js 15.5, React 19
This commit is contained in:
173
src/app/api/projects/[id]/editing/route.ts
Normal file
173
src/app/api/projects/[id]/editing/route.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/db'
|
||||
import { getSession } from '@/lib/auth'
|
||||
|
||||
const HEARTBEAT_TIMEOUT_MS = 2 * 60 * 1000 // 2 minutes
|
||||
|
||||
function isEditingExpired(heartbeat: Date | null): boolean {
|
||||
if (!heartbeat) return true
|
||||
return Date.now() - new Date(heartbeat).getTime() > HEARTBEAT_TIMEOUT_MS
|
||||
}
|
||||
|
||||
// GET: Check editing status of a project
|
||||
export async function GET(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const user = await getSession()
|
||||
if (!user) return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 401 })
|
||||
|
||||
const sessionId = req.nextUrl.searchParams.get('sessionId') || ''
|
||||
|
||||
const project = await (prisma as any).project.findUnique({
|
||||
where: { id: params.id },
|
||||
select: {
|
||||
id: true,
|
||||
editingById: true,
|
||||
editingUserName: true,
|
||||
editingSessionId: true,
|
||||
editingStartedAt: true,
|
||||
editingHeartbeat: true,
|
||||
isLocked: true,
|
||||
},
|
||||
})
|
||||
|
||||
if (!project) {
|
||||
return NextResponse.json({ error: 'Projekt nicht gefunden' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Auto-release if heartbeat expired
|
||||
if (project.editingById && isEditingExpired(project.editingHeartbeat)) {
|
||||
await (prisma as any).project.update({
|
||||
where: { id: params.id },
|
||||
data: {
|
||||
editingById: null,
|
||||
editingUserName: null,
|
||||
editingSessionId: null,
|
||||
editingStartedAt: null,
|
||||
editingHeartbeat: null,
|
||||
},
|
||||
})
|
||||
return NextResponse.json({
|
||||
editing: false,
|
||||
editingBy: null,
|
||||
isMe: false,
|
||||
isLocked: project.isLocked,
|
||||
})
|
||||
}
|
||||
|
||||
// isMe is true only if sessionId matches (same tab/device)
|
||||
const isMe = !!sessionId && project.editingSessionId === sessionId
|
||||
|
||||
return NextResponse.json({
|
||||
editing: !!project.editingById,
|
||||
editingBy: project.editingById ? {
|
||||
id: project.editingById,
|
||||
name: project.editingUserName,
|
||||
since: project.editingStartedAt,
|
||||
} : null,
|
||||
isMe,
|
||||
isLocked: project.isLocked,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error checking editing status:', error)
|
||||
return NextResponse.json({ error: 'Serverfehler' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// POST: Start editing / heartbeat / stop editing
|
||||
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 body = await req.json()
|
||||
const { action, sessionId } = body // 'start', 'heartbeat', 'stop'
|
||||
|
||||
const project = await (prisma as any).project.findUnique({
|
||||
where: { id: params.id },
|
||||
select: {
|
||||
id: true,
|
||||
editingById: true,
|
||||
editingSessionId: true,
|
||||
editingHeartbeat: true,
|
||||
isLocked: true,
|
||||
tenantId: true,
|
||||
},
|
||||
})
|
||||
|
||||
if (!project) {
|
||||
return NextResponse.json({ error: 'Projekt nicht gefunden' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (action === 'start') {
|
||||
if (!sessionId) {
|
||||
return NextResponse.json({ error: 'sessionId fehlt' }, { status: 400 })
|
||||
}
|
||||
// Check if another session is editing (and heartbeat not expired)
|
||||
if (project.editingSessionId && project.editingSessionId !== sessionId && !isEditingExpired(project.editingHeartbeat)) {
|
||||
return NextResponse.json({
|
||||
error: 'Projekt wird gerade von jemand anderem bearbeitet',
|
||||
editingBy: project.editingById,
|
||||
}, { status: 409 })
|
||||
}
|
||||
|
||||
// Start editing
|
||||
await (prisma as any).project.update({
|
||||
where: { id: params.id },
|
||||
data: {
|
||||
editingById: user.id,
|
||||
editingUserName: user.name,
|
||||
editingSessionId: sessionId,
|
||||
editingStartedAt: new Date(),
|
||||
editingHeartbeat: new Date(),
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true, action: 'started' })
|
||||
}
|
||||
|
||||
if (action === 'heartbeat') {
|
||||
// Only the current session can send heartbeats
|
||||
if (project.editingSessionId !== sessionId) {
|
||||
return NextResponse.json({ error: 'Sie sind nicht der aktuelle Bearbeiter' }, { status: 403 })
|
||||
}
|
||||
|
||||
await (prisma as any).project.update({
|
||||
where: { id: params.id },
|
||||
data: { editingHeartbeat: new Date() },
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true, action: 'heartbeat' })
|
||||
}
|
||||
|
||||
if (action === 'stop') {
|
||||
// Only the current session or a SERVER_ADMIN can stop editing
|
||||
if (project.editingSessionId !== sessionId && user.role !== 'SERVER_ADMIN') {
|
||||
return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 })
|
||||
}
|
||||
|
||||
await (prisma as any).project.update({
|
||||
where: { id: params.id },
|
||||
data: {
|
||||
editingById: null,
|
||||
editingUserName: null,
|
||||
editingSessionId: null,
|
||||
editingStartedAt: null,
|
||||
editingHeartbeat: null,
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true, action: 'stopped' })
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Ungültige Aktion' }, { status: 400 })
|
||||
} catch (error) {
|
||||
console.error('Error managing editing lock:', error)
|
||||
return NextResponse.json({ error: 'Serverfehler' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
85
src/app/api/projects/[id]/export/route.ts
Normal file
85
src/app/api/projects/[id]/export/route.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/db'
|
||||
import { getProjectWithTenantCheck } from '@/lib/tenant'
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const user = await getSession()
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Tenant isolation check
|
||||
const projectCheck = await getProjectWithTenantCheck(params.id, user)
|
||||
if (!projectCheck) {
|
||||
return NextResponse.json({ error: 'Projekt nicht gefunden' }, { status: 404 })
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const format = searchParams.get('format') || 'json'
|
||||
|
||||
const project = await (prisma as any).project.findUnique({
|
||||
where: { id: params.id },
|
||||
include: {
|
||||
features: true,
|
||||
owner: {
|
||||
select: { name: true },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!project) {
|
||||
return NextResponse.json({ error: 'Projekt nicht gefunden' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (format === 'geojson') {
|
||||
const geojson = {
|
||||
type: 'FeatureCollection',
|
||||
properties: {
|
||||
title: project.title,
|
||||
location: project.location,
|
||||
description: project.description,
|
||||
createdAt: project.createdAt,
|
||||
updatedAt: project.updatedAt,
|
||||
owner: project.owner?.name ?? null,
|
||||
},
|
||||
features: project.features.map((f: any) => ({
|
||||
type: 'Feature',
|
||||
id: f.id,
|
||||
geometry: f.geometry,
|
||||
properties: {
|
||||
type: f.type,
|
||||
...f.properties as object,
|
||||
},
|
||||
})),
|
||||
}
|
||||
|
||||
return new NextResponse(JSON.stringify(geojson, null, 2), {
|
||||
headers: {
|
||||
'Content-Type': 'application/geo+json',
|
||||
'Content-Disposition': `attachment; filename="${project.title.replace(/[^a-z0-9]/gi, '_')}.geojson"`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Default: return project data for client-side PDF/PNG generation
|
||||
return NextResponse.json({
|
||||
project: {
|
||||
...project,
|
||||
features: project.features.map((f: any) => ({
|
||||
id: f.id,
|
||||
type: f.type,
|
||||
geometry: f.geometry,
|
||||
properties: f.properties,
|
||||
})),
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error exporting project:', error)
|
||||
return NextResponse.json({ error: 'Serverfehler' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
139
src/app/api/projects/[id]/features/route.ts
Normal file
139
src/app/api/projects/[id]/features/route.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/db'
|
||||
import { featureSchema } from '@/lib/validations'
|
||||
import { getProjectWithTenantCheck } from '@/lib/tenant'
|
||||
|
||||
export async function GET(
|
||||
request: 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 })
|
||||
}
|
||||
|
||||
const features = await (prisma as any).feature.findMany({
|
||||
where: { projectId: params.id },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
})
|
||||
|
||||
return NextResponse.json({ features })
|
||||
} catch (error) {
|
||||
console.error('Error fetching features:', error)
|
||||
return NextResponse.json({ error: 'Serverfehler' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const user = await getSession()
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 401 })
|
||||
}
|
||||
|
||||
if (user.role === 'VIEWER') {
|
||||
return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 })
|
||||
}
|
||||
|
||||
const project = await getProjectWithTenantCheck(params.id, user)
|
||||
if (!project) {
|
||||
return NextResponse.json({ error: 'Projekt nicht gefunden' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (project.isLocked && user.role !== 'SERVER_ADMIN') {
|
||||
return NextResponse.json({ error: 'Projekt ist gesperrt' }, { status: 403 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const validated = featureSchema.safeParse(body)
|
||||
|
||||
if (!validated.success) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Ungültige Eingabedaten', details: validated.error.flatten() },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const feature = await (prisma as any).feature.create({
|
||||
data: {
|
||||
projectId: params.id,
|
||||
type: validated.data.type,
|
||||
geometry: validated.data.geometry,
|
||||
properties: validated.data.properties || {},
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ feature }, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error('Error creating feature:', error)
|
||||
return NextResponse.json({ error: 'Serverfehler' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const user = await getSession()
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 401 })
|
||||
}
|
||||
|
||||
if (user.role === 'VIEWER') {
|
||||
return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 })
|
||||
}
|
||||
|
||||
const project = await getProjectWithTenantCheck(params.id, user)
|
||||
if (!project) {
|
||||
const exists = await (prisma as any).project.findUnique({ where: { id: params.id }, select: { id: true, tenantId: true, ownerId: true } })
|
||||
if (!exists) {
|
||||
console.warn(`[Features PUT] Project ${params.id} not in DB`)
|
||||
return NextResponse.json({ error: 'Projekt nicht gefunden' }, { status: 404 })
|
||||
}
|
||||
console.warn(`[Features PUT] Access denied: user=${user.id} tenant=${user.tenantId}, project owner=${exists.ownerId} tenant=${exists.tenantId}`)
|
||||
return NextResponse.json({ error: 'Keine Berechtigung für dieses Projekt' }, { status: 403 })
|
||||
}
|
||||
|
||||
if (project.isLocked && user.role !== 'SERVER_ADMIN') {
|
||||
return NextResponse.json({ error: 'Projekt ist gesperrt' }, { status: 403 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { features } = body as { features: Array<{ id?: string; type: string; geometry: object; properties?: object }> }
|
||||
|
||||
await (prisma as any).feature.deleteMany({
|
||||
where: { projectId: params.id },
|
||||
})
|
||||
|
||||
if (features && features.length > 0) {
|
||||
await (prisma as any).feature.createMany({
|
||||
data: features.map((f: any) => ({
|
||||
projectId: params.id,
|
||||
type: f.type,
|
||||
geometry: f.geometry,
|
||||
properties: f.properties || {},
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
const updatedFeatures = await (prisma as any).feature.findMany({
|
||||
where: { projectId: params.id },
|
||||
})
|
||||
|
||||
return NextResponse.json({ features: updatedFeatures })
|
||||
} catch (error) {
|
||||
console.error('Error updating features:', error)
|
||||
return NextResponse.json({ error: 'Serverfehler' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/db'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { getProjectWithTenantCheck } from '@/lib/tenant'
|
||||
|
||||
// PUT: Toggle confirmed/ok on a check item
|
||||
export async function PUT(req: NextRequest, { params }: { params: { id: string; itemId: string } }) {
|
||||
try {
|
||||
const user = await getSession()
|
||||
if (!user) return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 401 })
|
||||
if (user.role === 'VIEWER') return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 })
|
||||
|
||||
const project = await getProjectWithTenantCheck(params.id, user)
|
||||
if (!project) return NextResponse.json({ error: 'Projekt nicht gefunden' }, { status: 404 })
|
||||
|
||||
// Verify item belongs to this project
|
||||
const existing = await (prisma as any).journalCheckItem.findFirst({
|
||||
where: { id: params.itemId, projectId: params.id },
|
||||
})
|
||||
if (!existing) return NextResponse.json({ error: 'Element nicht gefunden' }, { status: 404 })
|
||||
|
||||
const body = await req.json()
|
||||
const data: any = {}
|
||||
if (body.label !== undefined) data.label = body.label
|
||||
if (body.confirmed !== undefined) {
|
||||
data.confirmed = body.confirmed
|
||||
data.confirmedAt = body.confirmed ? new Date() : null
|
||||
}
|
||||
if (body.ok !== undefined) {
|
||||
data.ok = body.ok
|
||||
data.okAt = body.ok ? new Date() : null
|
||||
}
|
||||
|
||||
const item = await (prisma as any).journalCheckItem.update({
|
||||
where: { id: params.itemId },
|
||||
data,
|
||||
})
|
||||
return NextResponse.json(item)
|
||||
} catch (error) {
|
||||
console.error('Error updating check item:', error)
|
||||
return NextResponse.json({ error: 'Failed to update check item' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE
|
||||
export async function DELETE(req: NextRequest, { params }: { params: { id: string; itemId: string } }) {
|
||||
try {
|
||||
const user = await getSession()
|
||||
if (!user) return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 401 })
|
||||
if (user.role === 'VIEWER') return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 })
|
||||
|
||||
const project = await getProjectWithTenantCheck(params.id, user)
|
||||
if (!project) return NextResponse.json({ error: 'Projekt nicht gefunden' }, { status: 404 })
|
||||
|
||||
// Verify item belongs to this project
|
||||
const existing = await (prisma as any).journalCheckItem.findFirst({
|
||||
where: { id: params.itemId, projectId: params.id },
|
||||
})
|
||||
if (!existing) return NextResponse.json({ error: 'Element nicht gefunden' }, { status: 404 })
|
||||
|
||||
await (prisma as any).journalCheckItem.delete({ where: { id: params.itemId } })
|
||||
return NextResponse.json({ ok: true })
|
||||
} catch (error) {
|
||||
console.error('Error deleting check item:', error)
|
||||
return NextResponse.json({ error: 'Failed to delete check item' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
57
src/app/api/projects/[id]/journal/check-items/route.ts
Normal file
57
src/app/api/projects/[id]/journal/check-items/route.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/db'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { getProjectWithTenantCheck } from '@/lib/tenant'
|
||||
|
||||
// POST: Add check item (or initialize from templates)
|
||||
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 })
|
||||
if (user.role === 'VIEWER') return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 })
|
||||
|
||||
const project = await getProjectWithTenantCheck(params.id, user)
|
||||
if (!project) return NextResponse.json({ error: 'Projekt nicht gefunden' }, { status: 404 })
|
||||
|
||||
const body = await req.json()
|
||||
|
||||
// If 'initFromTemplates' is true, create check items from templates (only if none exist)
|
||||
if (body.initFromTemplates) {
|
||||
const existing = await (prisma as any).journalCheckItem.findMany({
|
||||
where: { projectId: params.id },
|
||||
})
|
||||
if (existing.length > 0) {
|
||||
return NextResponse.json(existing)
|
||||
}
|
||||
const templates = await (prisma as any).journalCheckTemplate.findMany({
|
||||
where: { isActive: true },
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
})
|
||||
const items = await Promise.all(
|
||||
templates.map((tpl: any, i: number) =>
|
||||
(prisma as any).journalCheckItem.create({
|
||||
data: {
|
||||
projectId: params.id,
|
||||
label: tpl.label,
|
||||
sortOrder: i,
|
||||
},
|
||||
})
|
||||
)
|
||||
)
|
||||
return NextResponse.json(items)
|
||||
}
|
||||
|
||||
// Single item creation
|
||||
const item = await (prisma as any).journalCheckItem.create({
|
||||
data: {
|
||||
projectId: params.id,
|
||||
label: body.label || '',
|
||||
sortOrder: body.sortOrder || 0,
|
||||
},
|
||||
})
|
||||
return NextResponse.json(item)
|
||||
} catch (error) {
|
||||
console.error('Error creating check item:', error)
|
||||
return NextResponse.json({ error: 'Failed to create check item' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
101
src/app/api/projects/[id]/journal/entries/[entryId]/route.ts
Normal file
101
src/app/api/projects/[id]/journal/entries/[entryId]/route.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/db'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { getProjectWithTenantCheck } from '@/lib/tenant'
|
||||
|
||||
// PUT: Update a journal entry — only toggle done status allowed directly
|
||||
export async function PUT(req: NextRequest, { params }: { params: { id: string; entryId: string } }) {
|
||||
try {
|
||||
const user = await getSession()
|
||||
if (!user) return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 401 })
|
||||
if (user.role === 'VIEWER') return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 })
|
||||
|
||||
const project = await getProjectWithTenantCheck(params.id, user)
|
||||
if (!project) return NextResponse.json({ error: 'Projekt nicht gefunden' }, { status: 404 })
|
||||
|
||||
const existing = await (prisma as any).journalEntry.findFirst({
|
||||
where: { id: params.entryId, projectId: params.id },
|
||||
})
|
||||
if (!existing) return NextResponse.json({ error: 'Eintrag nicht gefunden' }, { status: 404 })
|
||||
|
||||
const body = await req.json()
|
||||
|
||||
// Only done toggle is allowed as direct edit
|
||||
if (body.done !== undefined) {
|
||||
const entry = await (prisma as any).journalEntry.update({
|
||||
where: { id: params.entryId },
|
||||
data: { done: body.done, doneAt: body.done ? new Date() : null },
|
||||
})
|
||||
return NextResponse.json(entry)
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Direkte Bearbeitung nicht erlaubt. Bitte Korrektur erstellen.' }, { status: 400 })
|
||||
} catch (error) {
|
||||
console.error('Error updating journal entry:', error)
|
||||
return NextResponse.json({ error: 'Failed to update entry' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// POST: Create a correction for a journal entry (replaces DELETE)
|
||||
// Marks the original as corrected (strikethrough) and creates a new correction entry below it
|
||||
export async function POST(req: NextRequest, { params }: { params: { id: string; entryId: string } }) {
|
||||
try {
|
||||
const user = await getSession()
|
||||
if (!user) return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 401 })
|
||||
if (user.role === 'VIEWER') return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 })
|
||||
|
||||
const project = await getProjectWithTenantCheck(params.id, user)
|
||||
if (!project) return NextResponse.json({ error: 'Projekt nicht gefunden' }, { status: 404 })
|
||||
|
||||
const existing = await (prisma as any).journalEntry.findFirst({
|
||||
where: { id: params.entryId, projectId: params.id },
|
||||
})
|
||||
if (!existing) return NextResponse.json({ error: 'Eintrag nicht gefunden' }, { status: 404 })
|
||||
|
||||
// Prevent double-correction or correcting a correction entry
|
||||
if (existing.isCorrected) {
|
||||
return NextResponse.json({ error: 'Dieser Eintrag wurde bereits korrigiert.' }, { status: 400 })
|
||||
}
|
||||
if (existing.correctionOfId) {
|
||||
return NextResponse.json({ error: 'Ein Korrektureintrag kann nicht nochmals korrigiert werden.' }, { status: 400 })
|
||||
}
|
||||
|
||||
const body = await req.json()
|
||||
const correctionText = body.what || ''
|
||||
|
||||
if (!correctionText.trim()) {
|
||||
return NextResponse.json({ error: 'Korrekturtext ist erforderlich' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Mark original as corrected
|
||||
await (prisma as any).journalEntry.update({
|
||||
where: { id: params.entryId },
|
||||
data: { isCorrected: true },
|
||||
})
|
||||
|
||||
// Create correction entry with same time, placed right after the original
|
||||
const correction = await (prisma as any).journalEntry.create({
|
||||
data: {
|
||||
time: existing.time,
|
||||
what: `[Korrektur] ${correctionText}`,
|
||||
who: body.who || existing.who || user.name,
|
||||
sortOrder: existing.sortOrder + 1,
|
||||
correctionOfId: existing.id,
|
||||
projectId: params.id,
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ original: existing, correction })
|
||||
} catch (error) {
|
||||
console.error('Error creating correction:', error)
|
||||
return NextResponse.json({ error: 'Failed to create correction' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE: Not allowed — entries cannot be deleted, only corrected
|
||||
export async function DELETE() {
|
||||
return NextResponse.json(
|
||||
{ error: 'Journal-Einträge können nicht gelöscht werden. Bitte erstellen Sie eine Korrektur.' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
31
src/app/api/projects/[id]/journal/entries/route.ts
Normal file
31
src/app/api/projects/[id]/journal/entries/route.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/db'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { getProjectWithTenantCheck } from '@/lib/tenant'
|
||||
|
||||
// POST: Add a new journal entry
|
||||
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 })
|
||||
if (user.role === 'VIEWER') return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 })
|
||||
|
||||
const project = await getProjectWithTenantCheck(params.id, user)
|
||||
if (!project) return NextResponse.json({ error: 'Projekt nicht gefunden' }, { status: 404 })
|
||||
|
||||
const body = await req.json()
|
||||
const entry = await (prisma as any).journalEntry.create({
|
||||
data: {
|
||||
projectId: params.id,
|
||||
time: body.time ? new Date(body.time) : new Date(),
|
||||
what: body.what || '',
|
||||
who: body.who || null,
|
||||
done: body.done || false,
|
||||
},
|
||||
})
|
||||
return NextResponse.json(entry)
|
||||
} catch (error) {
|
||||
console.error('Error creating journal entry:', error)
|
||||
return NextResponse.json({ error: 'Failed to create entry' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/db'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { getProjectWithTenantCheck } from '@/lib/tenant'
|
||||
|
||||
// PUT: Update a pendenz
|
||||
export async function PUT(req: NextRequest, { params }: { params: { id: string; pendenzId: string } }) {
|
||||
try {
|
||||
const user = await getSession()
|
||||
if (!user) return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 401 })
|
||||
if (user.role === 'VIEWER') return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 })
|
||||
|
||||
const project = await getProjectWithTenantCheck(params.id, user)
|
||||
if (!project) return NextResponse.json({ error: 'Projekt nicht gefunden' }, { status: 404 })
|
||||
|
||||
// Verify pendenz belongs to this project
|
||||
const existing = await (prisma as any).journalPendenz.findFirst({
|
||||
where: { id: params.pendenzId, projectId: params.id },
|
||||
})
|
||||
if (!existing) return NextResponse.json({ error: 'Pendenz nicht gefunden' }, { status: 404 })
|
||||
|
||||
const body = await req.json()
|
||||
const data: any = {}
|
||||
if (body.what !== undefined) data.what = body.what
|
||||
if (body.who !== undefined) data.who = body.who
|
||||
if (body.whenHow !== undefined) data.whenHow = body.whenHow
|
||||
if (body.done !== undefined) {
|
||||
data.done = body.done
|
||||
data.doneAt = body.done ? new Date() : null
|
||||
}
|
||||
|
||||
const item = await (prisma as any).journalPendenz.update({
|
||||
where: { id: params.pendenzId },
|
||||
data,
|
||||
})
|
||||
return NextResponse.json(item)
|
||||
} catch (error) {
|
||||
console.error('Error updating pendenz:', error)
|
||||
return NextResponse.json({ error: 'Failed to update pendenz' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE
|
||||
export async function DELETE(req: NextRequest, { params }: { params: { id: string; pendenzId: string } }) {
|
||||
try {
|
||||
const user = await getSession()
|
||||
if (!user) return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 401 })
|
||||
if (user.role === 'VIEWER') return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 })
|
||||
|
||||
const project = await getProjectWithTenantCheck(params.id, user)
|
||||
if (!project) return NextResponse.json({ error: 'Projekt nicht gefunden' }, { status: 404 })
|
||||
|
||||
// Verify pendenz belongs to this project
|
||||
const existing = await (prisma as any).journalPendenz.findFirst({
|
||||
where: { id: params.pendenzId, projectId: params.id },
|
||||
})
|
||||
if (!existing) return NextResponse.json({ error: 'Pendenz nicht gefunden' }, { status: 404 })
|
||||
|
||||
await (prisma as any).journalPendenz.delete({ where: { id: params.pendenzId } })
|
||||
return NextResponse.json({ ok: true })
|
||||
} catch (error) {
|
||||
console.error('Error deleting pendenz:', error)
|
||||
return NextResponse.json({ error: 'Failed to delete pendenz' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
30
src/app/api/projects/[id]/journal/pendenzen/route.ts
Normal file
30
src/app/api/projects/[id]/journal/pendenzen/route.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/db'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { getProjectWithTenantCheck } from '@/lib/tenant'
|
||||
|
||||
// POST: Add a new pendenz
|
||||
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 })
|
||||
if (user.role === 'VIEWER') return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 })
|
||||
|
||||
const project = await getProjectWithTenantCheck(params.id, user)
|
||||
if (!project) return NextResponse.json({ error: 'Projekt nicht gefunden' }, { status: 404 })
|
||||
|
||||
const body = await req.json()
|
||||
const item = await (prisma as any).journalPendenz.create({
|
||||
data: {
|
||||
projectId: params.id,
|
||||
what: body.what || '',
|
||||
who: body.who || null,
|
||||
whenHow: body.whenHow || null,
|
||||
},
|
||||
})
|
||||
return NextResponse.json(item)
|
||||
} catch (error) {
|
||||
console.error('Error creating pendenz:', error)
|
||||
return NextResponse.json({ error: 'Failed to create pendenz' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
35
src/app/api/projects/[id]/journal/route.ts
Normal file
35
src/app/api/projects/[id]/journal/route.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/db'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { getProjectWithTenantCheck } from '@/lib/tenant'
|
||||
|
||||
// GET all journal data for a project (entries, check items, pendenzen)
|
||||
export async function GET(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 })
|
||||
|
||||
const [entries, checkItems, pendenzen] = await Promise.all([
|
||||
(prisma as any).journalEntry.findMany({
|
||||
where: { projectId: params.id },
|
||||
orderBy: [{ time: 'asc' }, { sortOrder: 'asc' }, { createdAt: 'asc' }],
|
||||
}),
|
||||
(prisma as any).journalCheckItem.findMany({
|
||||
where: { projectId: params.id },
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
}),
|
||||
(prisma as any).journalPendenz.findMany({
|
||||
where: { projectId: params.id },
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
}),
|
||||
])
|
||||
|
||||
return NextResponse.json({ entries, checkItems, pendenzen })
|
||||
} catch (error) {
|
||||
console.error('Error fetching journal:', error)
|
||||
return NextResponse.json({ error: 'Failed to fetch journal' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
168
src/app/api/projects/[id]/journal/send-report/route.ts
Normal file
168
src/app/api/projects/[id]/journal/send-report/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
121
src/app/api/projects/[id]/plan-image/route.ts
Normal file
121
src/app/api/projects/[id]/plan-image/route.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/db'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { getProjectWithTenantCheck } from '@/lib/tenant'
|
||||
import { uploadFile, deleteFile, getFileUrl } from '@/lib/minio'
|
||||
|
||||
// POST: Upload a plan image for a project
|
||||
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 })
|
||||
if (user.role === 'VIEWER') return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 })
|
||||
|
||||
const project = await getProjectWithTenantCheck(params.id, user)
|
||||
if (!project) return NextResponse.json({ error: 'Projekt nicht gefunden' }, { status: 404 })
|
||||
|
||||
const formData = await req.formData()
|
||||
const file = formData.get('file') as File | null
|
||||
const boundsStr = formData.get('bounds') as string | null
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json({ error: 'Keine Datei angegeben' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Validate file type
|
||||
const allowedTypes = ['image/png', 'image/jpeg', 'image/webp', 'image/svg+xml']
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
return NextResponse.json({ error: 'Nur PNG, JPEG, WebP oder SVG erlaubt' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Delete old plan image if exists
|
||||
const p = project as any
|
||||
if (p.planImageKey) {
|
||||
try { await deleteFile(p.planImageKey) } catch {}
|
||||
}
|
||||
|
||||
// Upload to MinIO
|
||||
const buffer = Buffer.from(await file.arrayBuffer())
|
||||
const ext = file.name.split('.').pop() || 'png'
|
||||
const fileKey = `plans/${params.id}/${Date.now()}.${ext}`
|
||||
await uploadFile(fileKey, buffer, file.type)
|
||||
|
||||
// Parse bounds or use default (current map view)
|
||||
let bounds = null
|
||||
if (boundsStr) {
|
||||
try { bounds = JSON.parse(boundsStr) } catch {}
|
||||
}
|
||||
|
||||
// Update project
|
||||
await (prisma as any).project.update({
|
||||
where: { id: params.id },
|
||||
data: {
|
||||
planImageKey: fileKey,
|
||||
planBounds: bounds,
|
||||
},
|
||||
})
|
||||
|
||||
const url = await getFileUrl(fileKey)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
planImageUrl: url,
|
||||
planImageKey: fileKey,
|
||||
planBounds: bounds,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error uploading plan image:', error)
|
||||
return NextResponse.json({ error: 'Fehler beim Hochladen' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE: Remove the plan image
|
||||
export async function DELETE(req: NextRequest, { params }: { params: { id: string } }) {
|
||||
try {
|
||||
const user = await getSession()
|
||||
if (!user) return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 401 })
|
||||
if (user.role === 'VIEWER') return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 })
|
||||
|
||||
const project = await getProjectWithTenantCheck(params.id, user)
|
||||
if (!project) return NextResponse.json({ error: 'Projekt nicht gefunden' }, { status: 404 })
|
||||
|
||||
const p = project as any
|
||||
if (p.planImageKey) {
|
||||
try { await deleteFile(p.planImageKey) } catch {}
|
||||
}
|
||||
|
||||
await (prisma as any).project.update({
|
||||
where: { id: params.id },
|
||||
data: { planImageKey: null, planBounds: null },
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error('Error deleting plan image:', error)
|
||||
return NextResponse.json({ error: 'Fehler beim Löschen' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// PATCH: Update plan bounds (repositioning)
|
||||
export async function PATCH(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 })
|
||||
|
||||
const body = await req.json()
|
||||
if (!body.bounds) return NextResponse.json({ error: 'Bounds erforderlich' }, { status: 400 })
|
||||
|
||||
await (prisma as any).project.update({
|
||||
where: { id: params.id },
|
||||
data: { planBounds: body.bounds },
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error('Error updating plan bounds:', error)
|
||||
return NextResponse.json({ error: 'Fehler beim Aktualisieren' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
40
src/app/api/projects/[id]/plan-image/serve/route.ts
Normal file
40
src/app/api/projects/[id]/plan-image/serve/route.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/db'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { getFileStream } from '@/lib/minio'
|
||||
|
||||
// Serve plan image (authenticated users only)
|
||||
export async function GET(req: NextRequest, { params }: { params: { id: string } }) {
|
||||
try {
|
||||
const user = await getSession()
|
||||
if (!user) return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 401 })
|
||||
|
||||
const project = await (prisma as any).project.findUnique({
|
||||
where: { id: params.id },
|
||||
select: { planImageKey: true },
|
||||
})
|
||||
|
||||
if (!project?.planImageKey) {
|
||||
return NextResponse.json({ error: 'Kein Plan vorhanden' }, { status: 404 })
|
||||
}
|
||||
|
||||
const { stream, contentType } = await getFileStream(project.planImageKey)
|
||||
|
||||
// Collect stream into buffer
|
||||
const chunks: Buffer[] = []
|
||||
for await (const chunk of stream as AsyncIterable<Buffer>) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
const buffer = Buffer.concat(chunks)
|
||||
|
||||
return new NextResponse(buffer, {
|
||||
headers: {
|
||||
'Content-Type': contentType,
|
||||
'Cache-Control': 'public, max-age=3600',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error serving plan image:', error)
|
||||
return NextResponse.json({ error: 'Fehler beim Laden des Plans' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
110
src/app/api/projects/[id]/route.ts
Normal file
110
src/app/api/projects/[id]/route.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/db'
|
||||
import { projectSchema } from '@/lib/validations'
|
||||
import { getProjectWithTenantCheck } from '@/lib/tenant'
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const user = await getSession()
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 401 })
|
||||
}
|
||||
|
||||
const projectBase = await getProjectWithTenantCheck(params.id, user)
|
||||
if (!projectBase) {
|
||||
return NextResponse.json({ error: 'Projekt nicht gefunden' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Re-fetch with includes
|
||||
const project = await (prisma as any).project.findUnique({
|
||||
where: { id: params.id },
|
||||
include: {
|
||||
owner: {
|
||||
select: { id: true, name: true, email: true },
|
||||
},
|
||||
features: true,
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ project })
|
||||
} catch (error) {
|
||||
console.error('Error fetching project:', error)
|
||||
return NextResponse.json({ error: 'Serverfehler' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const user = await getSession()
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 401 })
|
||||
}
|
||||
|
||||
if (user.role === 'VIEWER') {
|
||||
return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 })
|
||||
}
|
||||
|
||||
const existingProject = await getProjectWithTenantCheck(params.id, user)
|
||||
if (!existingProject) {
|
||||
return NextResponse.json({ error: 'Projekt nicht gefunden' }, { status: 404 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const validated = projectSchema.partial().safeParse(body)
|
||||
|
||||
if (!validated.success) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Ungültige Eingabedaten', details: validated.error.flatten() },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const project = await (prisma as any).project.update({
|
||||
where: { id: params.id },
|
||||
data: validated.data,
|
||||
})
|
||||
|
||||
return NextResponse.json({ project })
|
||||
} catch (error) {
|
||||
console.error('Error updating project:', error)
|
||||
return NextResponse.json({ error: 'Serverfehler' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const user = await getSession()
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 401 })
|
||||
}
|
||||
|
||||
const existingProject = await getProjectWithTenantCheck(params.id, user)
|
||||
if (!existingProject) {
|
||||
return NextResponse.json({ error: 'Projekt nicht gefunden' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Only owner, tenant admin, or server admin can delete
|
||||
if (user.role !== 'SERVER_ADMIN' && user.role !== 'TENANT_ADMIN' && existingProject.ownerId !== user.id) {
|
||||
return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 })
|
||||
}
|
||||
|
||||
await (prisma as any).project.delete({
|
||||
where: { id: params.id },
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error('Error deleting project:', error)
|
||||
return NextResponse.json({ error: 'Serverfehler' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user