Aktueller Stand

This commit is contained in:
2026-01-16 23:02:36 +01:00
parent dcf45bac3d
commit b2b23268b2
14 changed files with 768 additions and 226 deletions

View File

@@ -14,23 +14,28 @@ type EventItem = {
locationLat?: number | null;
locationLng?: number | null;
description?: string | null;
category?: { id: string; name: string } | null;
publicOverride?: boolean | null;
category?: { id: string; name: string; isPublic?: boolean } | null;
createdBy?: { name?: string | null; email?: string | null } | null;
};
type CategoryItem = {
id: string;
name: string;
isPublic: boolean;
};
export default function AdminPanel() {
const [events, setEvents] = useState<EventItem[]>([]);
const [allEvents, setAllEvents] = useState<EventItem[]>([]);
const [error, setError] = useState<string | null>(null);
const [categories, setCategories] = useState<{ id: string; name: string }[]>(
[]
);
const [categories, setCategories] = useState<CategoryItem[]>([]);
const [categoryError, setCategoryError] = useState<string | null>(null);
const [categoryStatus, setCategoryStatus] = useState<string | null>(null);
const [categoryModalOpen, setCategoryModalOpen] = useState(false);
const [categoryModalError, setCategoryModalError] = useState<string | null>(null);
const [categoryModalStatus, setCategoryModalStatus] = useState<string | null>(null);
const [editingCategory, setEditingCategory] = useState<{ id: string; name: string } | null>(null);
const [editingCategory, setEditingCategory] = useState<CategoryItem | null>(null);
const [editEvent, setEditEvent] = useState<EventItem | null>(null);
const [editStatus, setEditStatus] = useState<string | null>(null);
const [editError, setEditError] = useState<string | null>(null);
@@ -45,6 +50,7 @@ export default function AdminPanel() {
const [importCategoryId, setImportCategoryId] = useState("");
const [importStatus, setImportStatus] = useState<string | null>(null);
const [importError, setImportError] = useState<string | null>(null);
const [publicAccessEnabled, setPublicAccessEnabled] = useState<boolean | null>(null);
const load = async () => {
try {
@@ -82,10 +88,22 @@ export default function AdminPanel() {
}
};
const loadSystemSettings = async () => {
try {
const response = await fetch("/api/settings/system");
if (!response.ok) return;
const payload = await response.json();
setPublicAccessEnabled(payload.publicAccessEnabled !== false);
} catch {
// ignore
}
};
useEffect(() => {
load();
loadCategories();
loadAllEvents();
loadSystemSettings();
}, []);
useEffect(() => {
@@ -97,6 +115,7 @@ export default function AdminPanel() {
}, [pageSize]);
const totalPages = Math.max(1, Math.ceil(allEvents.length / pageSize));
const showPublicControls = publicAccessEnabled === true;
const sortedEvents = [...allEvents].sort((a, b) => {
const dir = sortDir === "asc" ? 1 : -1;
@@ -160,6 +179,14 @@ export default function AdminPanel() {
return date.toISOString();
};
const parsePublicOverride = (value: FormDataEntryValue | null) => {
if (!value) return null;
const raw = String(value);
if (raw === "public") return true;
if (raw === "private") return false;
return null;
};
const updateEvent = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setEditStatus(null);
@@ -167,7 +194,7 @@ export default function AdminPanel() {
if (!editEvent) return;
const formData = new FormData(event.currentTarget);
const payload = {
const payload: Record<string, unknown> = {
title: formData.get("title"),
description: formData.get("description"),
location: formData.get("location"),
@@ -178,6 +205,11 @@ export default function AdminPanel() {
endAt: toIsoString(formData.get("endAt")),
categoryId: formData.get("categoryId")
};
if (showPublicControls) {
payload.publicOverride = parsePublicOverride(
formData.get("publicOverride")
);
}
const response = await fetch(`/api/events/${editEvent.id}`, {
method: "PATCH",
@@ -205,15 +237,21 @@ export default function AdminPanel() {
setCategoryStatus(null);
const formData = new FormData(event.currentTarget);
const rawName = String(formData.get("name") || "").trim();
const isPublic = formData.get("isPublic") === "on";
if (!rawName) {
setCategoryError("Name erforderlich.");
return;
}
const payload: Record<string, unknown> = { name: rawName };
if (showPublicControls) {
payload.isPublic = isPublic;
}
const response = await fetch("/api/categories", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: rawName })
body: JSON.stringify(payload)
});
if (!response.ok) {
@@ -231,7 +269,7 @@ export default function AdminPanel() {
setCategoryStatus("Kategorie angelegt.");
};
const openCategoryModal = (category: { id: string; name: string }) => {
const openCategoryModal = (category: CategoryItem) => {
setEditingCategory(category);
setCategoryModalError(null);
setCategoryModalStatus(null);
@@ -251,15 +289,24 @@ export default function AdminPanel() {
const formData = new FormData(event.currentTarget);
const name = String(formData.get("name") || "").trim();
const isPublic = formData.get("isPublic") === "on";
if (!name) {
setCategoryModalError("Name erforderlich.");
return;
}
const payload: Record<string, unknown> = {
id: editingCategory.id,
name
};
if (showPublicControls) {
payload.isPublic = isPublic;
}
const response = await fetch("/api/categories", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id: editingCategory.id, name })
body: JSON.stringify(payload)
});
if (!response.ok) {
@@ -411,6 +458,12 @@ export default function AdminPanel() {
placeholder="z.B. Training"
className="flex-1 rounded-xl border border-slate-300 px-3 py-2"
/>
{showPublicControls && (
<label className="flex items-center gap-2 text-sm text-slate-700">
<input type="checkbox" name="isPublic" />
Öffentlich
</label>
)}
<button className="btn-accent" type="submit">
Anlegen
</button>
@@ -434,6 +487,11 @@ export default function AdminPanel() {
className="category-pill flex items-center gap-2 rounded-full border border-slate-200 bg-slate-50 px-3 py-1 text-sm text-slate-700"
>
<span className="font-medium">{category.name}</span>
{showPublicControls && category.isPublic && (
<span className="rounded-full bg-emerald-100 px-2 py-0.5 text-xs text-emerald-700">
Öffentlich
</span>
)}
<button
type="button"
className="rounded-full border border-slate-200 p-1 text-slate-600"
@@ -500,6 +558,16 @@ export default function AdminPanel() {
required
className="w-full rounded-xl border border-slate-300 px-3 py-2"
/>
{showPublicControls && (
<label className="flex items-center gap-2 text-sm text-slate-700">
<input
type="checkbox"
name="isPublic"
defaultChecked={editingCategory.isPublic}
/>
Öffentlich
</label>
)}
<button type="submit" className="btn-accent w-full">
Speichern
</button>
@@ -656,6 +724,45 @@ export default function AdminPanel() {
</option>
))}
</select>
{showPublicControls && (
<div className="rounded-xl border border-slate-200 bg-slate-50/60 p-3 text-sm text-slate-700 md:col-span-2">
<p className="text-xs uppercase tracking-[0.2em] text-slate-500">
Öffentlich
</p>
<div className="mt-2 flex flex-wrap gap-3">
<label className="flex items-center gap-2">
<input
type="radio"
name="publicOverride"
value="inherit"
defaultChecked={
editEvent.publicOverride !== true &&
editEvent.publicOverride !== false
}
/>
Kategorie übernehmen
</label>
<label className="flex items-center gap-2">
<input
type="radio"
name="publicOverride"
value="public"
defaultChecked={editEvent.publicOverride === true}
/>
Öffentlich
</label>
<label className="flex items-center gap-2">
<input
type="radio"
name="publicOverride"
value="private"
defaultChecked={editEvent.publicOverride === false}
/>
Nicht öffentlich
</label>
</div>
</div>
)}
</div>
<textarea
name="description"