Aktueller Stand
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user