Aktueller Stand
This commit is contained in:
@@ -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
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
143
app/api/ical/import/route.ts
Normal file
143
app/api/ical/import/route.ts
Normal 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
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user