Aktueller Stand
This commit is contained in:
@@ -7,6 +7,7 @@ export default function AdminSystemSettings() {
|
||||
const [provider, setProvider] = useState("osm");
|
||||
const [registrationEnabled, setRegistrationEnabled] = useState(true);
|
||||
const [publicAccessEnabled, setPublicAccessEnabled] = useState(true);
|
||||
const [emailVerificationRequired, setEmailVerificationRequired] = useState(true);
|
||||
const [appName, setAppName] = useState("Vereinskalender");
|
||||
const [logoFile, setLogoFile] = useState<File | null>(null);
|
||||
const [logoVersion, setLogoVersion] = useState(() => Date.now());
|
||||
@@ -28,6 +29,7 @@ export default function AdminSystemSettings() {
|
||||
setProvider(payload.provider || "osm");
|
||||
setRegistrationEnabled(payload.registrationEnabled !== false);
|
||||
setPublicAccessEnabled(payload.publicAccessEnabled !== false);
|
||||
setEmailVerificationRequired(payload.emailVerificationRequired !== false);
|
||||
if (appNameResponse.ok) {
|
||||
const appPayload = await appNameResponse.json();
|
||||
setAppName(appPayload.name || "Vereinskalender");
|
||||
@@ -67,7 +69,8 @@ export default function AdminSystemSettings() {
|
||||
apiKey,
|
||||
provider,
|
||||
registrationEnabled,
|
||||
publicAccessEnabled
|
||||
publicAccessEnabled,
|
||||
emailVerificationRequired
|
||||
})
|
||||
}),
|
||||
fetch("/api/settings/app-name", {
|
||||
@@ -252,6 +255,16 @@ export default function AdminSystemSettings() {
|
||||
/>
|
||||
Registrierung erlauben
|
||||
</label>
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={emailVerificationRequired}
|
||||
onChange={(event) =>
|
||||
setEmailVerificationRequired(event.target.checked)
|
||||
}
|
||||
/>
|
||||
E-Mail-Verifizierung erforderlich
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" className="btn-accent">
|
||||
|
||||
@@ -140,11 +140,20 @@ export default function AdminUserApprovals({ role }: AdminUserApprovalsProps) {
|
||||
setEditingUser(null);
|
||||
};
|
||||
|
||||
const removeUser = async (user: UserItem) => {
|
||||
const ok = window.confirm(`Benutzer ${user.email} deaktivieren?`);
|
||||
const removeUser = async (user: UserItem, mode: "disable" | "delete" = "disable") => {
|
||||
if (mode === "delete" && !isSuperAdmin) {
|
||||
setError("Nur Superadmins können Benutzer endgültig löschen.");
|
||||
return;
|
||||
}
|
||||
const ok = window.confirm(
|
||||
mode === "delete"
|
||||
? `Benutzer ${user.email} endgültig löschen? Alle Daten werden entfernt.`
|
||||
: `Benutzer ${user.email} deaktivieren?`
|
||||
);
|
||||
if (!ok) return;
|
||||
|
||||
const response = await fetch(`/api/users?id=${user.id}`, {
|
||||
const hardParam = mode === "delete" ? "&hard=true" : "";
|
||||
const response = await fetch(`/api/users?id=${user.id}${hardParam}`, {
|
||||
method: "DELETE"
|
||||
});
|
||||
|
||||
@@ -154,7 +163,7 @@ export default function AdminUserApprovals({ role }: AdminUserApprovalsProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus("Benutzer deaktiviert.");
|
||||
setStatus(mode === "delete" ? "Benutzer gelöscht." : "Benutzer deaktiviert.");
|
||||
loadPending();
|
||||
loadAll();
|
||||
};
|
||||
@@ -303,8 +312,8 @@ export default function AdminUserApprovals({ role }: AdminUserApprovalsProps) {
|
||||
</IconButton>
|
||||
)}
|
||||
<IconButton
|
||||
label="Deaktivieren"
|
||||
onClick={() => removeUser(user)}
|
||||
label={isSuperAdmin ? "Löschen" : "Deaktivieren"}
|
||||
onClick={() => removeUser(user, isSuperAdmin ? "delete" : "disable")}
|
||||
>
|
||||
<IconTrash />
|
||||
</IconButton>
|
||||
|
||||
@@ -1582,7 +1582,134 @@ export default function CalendarBoard() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<div className="md:hidden space-y-2">
|
||||
{displayedEvents.length === 0 ? (
|
||||
<p className="rounded-xl border border-slate-200 bg-white px-3 py-4 text-sm text-slate-600">
|
||||
Keine Termine für die aktuelle Auswahl.
|
||||
</p>
|
||||
) : (
|
||||
displayedEvents.map((event) => {
|
||||
const categoryName = event.category?.name || "Ohne Kategorie";
|
||||
const locationLabel = formatLocation(event.location);
|
||||
const dateLabel = new Date(event.startAt).toLocaleString("de-DE", {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short"
|
||||
});
|
||||
const bucket = getDateBucket(event.startAt);
|
||||
const bucketClass =
|
||||
bucket === "past"
|
||||
? "bg-slate-50 text-slate-500"
|
||||
: bucket === "today"
|
||||
? "bg-amber-50/60"
|
||||
: bucket === "tomorrow"
|
||||
? "bg-emerald-50/60"
|
||||
: "bg-sky-50/40";
|
||||
return (
|
||||
<div
|
||||
key={event.id}
|
||||
className={`rounded-xl border border-slate-200 p-2 ${bucketClass}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0 space-y-0.5">
|
||||
<p className="text-[11px] uppercase tracking-[0.2em] text-slate-500">
|
||||
{dateLabel}
|
||||
</p>
|
||||
<p className="text-sm font-semibold" title={event.title}>
|
||||
{event.title}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-2 text-[11px] text-slate-600">
|
||||
<span className="truncate" title={categoryName}>
|
||||
{categoryName}
|
||||
</span>
|
||||
<span>•</span>
|
||||
<span className="truncate" title={locationLabel}>
|
||||
{locationLabel}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{isAdmin && (
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label={`${event.title} auswählen`}
|
||||
checked={bulkSelection.has(event.id)}
|
||||
onChange={() => toggleBulkSelection(event.id)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-between gap-2">
|
||||
<div className="flex flex-nowrap items-center gap-1">
|
||||
{canManageView && (
|
||||
<ViewToggleButton
|
||||
isSelected={selectedEventIds.has(event.id)}
|
||||
onClick={() => toggleEvent(event.id)}
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full border border-slate-200 p-1.5 text-slate-600"
|
||||
onClick={() => {
|
||||
setEditStatus(null);
|
||||
setEditError(null);
|
||||
setEditEvent(event);
|
||||
setIsEditOpen(true);
|
||||
}}
|
||||
aria-label="Termin bearbeiten"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
className="h-3.5 w-3.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path
|
||||
d="M4 20h4l10-10-4-4L4 16v4z"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path d="M13 7l4 4" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full border border-slate-200 p-1.5 text-slate-600"
|
||||
onClick={() => setDetailsEvent(event)}
|
||||
aria-label="Termin Details"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
className="h-3.5 w-3.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<path d="M12 8h.01M12 12v4" strokeLinecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{event.locationLat && event.locationLng && (
|
||||
<a
|
||||
className="inline-flex items-center justify-center rounded-full border border-slate-200 p-1.5 text-slate-600"
|
||||
href={`https://maps.google.com/?q=${event.locationLat},${event.locationLng}&z=14`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title="Google Maps"
|
||||
aria-label="Google Maps"
|
||||
>
|
||||
<IconMapPin className="h-3.5 w-3.5" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
<div className="hidden overflow-x-auto md:block">
|
||||
<table className="list-table w-full table-fixed text-left text-sm">
|
||||
<thead className="text-xs uppercase tracking-[0.2em] text-slate-500">
|
||||
<tr>
|
||||
@@ -2551,9 +2678,9 @@ function SortIcon({
|
||||
);
|
||||
}
|
||||
|
||||
function IconMapPin() {
|
||||
function IconMapPin({ className = "h-4 w-4" }: { className?: string }) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<svg viewBox="0 0 24 24" className={className} fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M12 21s6-6.5 6-11a6 6 0 10-12 0c0 4.5 6 11 6 11z" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<circle cx="12" cy="10" r="2.5" />
|
||||
</svg>
|
||||
|
||||
Reference in New Issue
Block a user