Aktueller Stand

This commit is contained in:
2026-01-15 16:24:09 +01:00
parent 5d2630a02f
commit 46eae2a2a9
70 changed files with 7866 additions and 447 deletions

View File

@@ -8,7 +8,12 @@ export async function GET(
) {
const view = await prisma.userView.findUnique({
where: { token: context.params.token },
include: { items: { include: { event: true } }, user: true }
include: {
items: { include: { event: true } },
categories: true,
exclusions: true,
user: true
}
});
if (!view) {
@@ -20,17 +25,36 @@ export async function GET(
timezone: "Europe/Berlin"
});
view.items
const excludedIds = new Set(view.exclusions.map((item) => item.eventId));
const explicitEvents = view.items
.map((item) => item.event)
.filter((event) => event.status === "APPROVED")
.forEach((event) => {
.filter((event) => event.status === "APPROVED");
const categoryIds = view.categories.map((item) => item.categoryId);
const categoryEvents =
categoryIds.length > 0
? await prisma.event.findMany({
where: { categoryId: { in: categoryIds }, status: "APPROVED" }
})
: [];
const combined = [...explicitEvents, ...categoryEvents].filter(
(event, index, all) =>
all.findIndex((item) => item.id === event.id) === index &&
!excludedIds.has(event.id)
);
combined.forEach((event) => {
const start = event.startAt;
const end =
event.endAt || new Date(event.startAt.getTime() + 3 * 60 * 60 * 1000);
calendar.createEvent({
id: event.id,
summary: event.title,
description: event.description || undefined,
location: event.location || undefined,
start: event.startAt,
end: event.endAt
start,
end
});
});

View File

@@ -0,0 +1,143 @@
import { NextResponse } from "next/server";
import { parseICS } from "node-ical";
import { isAdminSession, requireSession } from "../../../../lib/auth-helpers";
import { prisma } from "../../../../lib/prisma";
const MAX_FILE_SIZE = 5 * 1024 * 1024;
const asText = (value: unknown) => {
if (value === null || value === undefined) return "";
if (typeof value === "string") return value.trim();
return String(value).trim();
};
export async function POST(request: Request) {
const { session } = await requireSession();
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
if (!isAdminSession(session)) {
return NextResponse.json({ error: "Nur für Admins." }, { status: 403 });
}
const formData = await request.formData();
const file = formData.get("file");
const categoryId = asText(formData.get("categoryId"));
if (!(file instanceof File)) {
return NextResponse.json(
{ error: "Bitte eine iCal-Datei hochladen." },
{ status: 400 }
);
}
if (!categoryId) {
return NextResponse.json(
{ error: "Bitte eine Kategorie auswählen." },
{ status: 400 }
);
}
const category = await prisma.category.findUnique({
where: { id: categoryId }
});
if (!category) {
return NextResponse.json(
{ error: "Kategorie nicht gefunden." },
{ status: 404 }
);
}
if (file.size > MAX_FILE_SIZE) {
return NextResponse.json(
{ error: "Datei ist zu groß (max. 5 MB)." },
{ status: 400 }
);
}
let parsed: Record<string, any>;
try {
const raw = await file.text();
parsed = parseICS(raw);
} catch (err) {
return NextResponse.json(
{ error: "iCal-Datei konnte nicht gelesen werden." },
{ status: 400 }
);
}
const entries = Object.values(parsed).filter(
(entry) => entry && entry.type === "VEVENT"
);
if (entries.length === 0) {
return NextResponse.json(
{ error: "Keine Termine in der iCal-Datei gefunden." },
{ status: 400 }
);
}
let created = 0;
let duplicates = 0;
let skipped = 0;
let recurringSkipped = 0;
const creatorEmail = session.user?.email || "";
for (const entry of entries) {
if (entry.rrule) {
recurringSkipped += 1;
continue;
}
const title = asText(entry.summary);
const start = entry.start instanceof Date ? entry.start : null;
if (!title || !start || Number.isNaN(start.getTime())) {
skipped += 1;
continue;
}
const end =
entry.end instanceof Date && !Number.isNaN(entry.end.getTime())
? entry.end
: new Date(start.getTime() + 3 * 60 * 60 * 1000);
const location = asText(entry.location) || null;
const description = asText(entry.description) || null;
const existing = await prisma.event.findFirst({
where: {
title,
startAt: start,
location,
categoryId
}
});
if (existing) {
duplicates += 1;
continue;
}
await prisma.event.create({
data: {
title,
description,
location,
startAt: start,
endAt: end,
status: "APPROVED",
createdBy: { connect: { email: creatorEmail } },
category: { connect: { id: categoryId } }
}
});
created += 1;
}
return NextResponse.json({
created,
duplicates,
skipped,
recurringSkipped
});
}