feat: Allow event owners to delete their own events and admins to delete any event with UI action buttons and confirmation dialog

This commit is contained in:
Richard
2026-07-31 11:19:49 +02:00
parent e2b311b634
commit 782d4d72a3
12 changed files with 382 additions and 300 deletions
+6
View File
@@ -29,6 +29,12 @@ class IsAdminOrEventCreator(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
if not request.user or not request.user.is_authenticated:
return False
if request.user.is_admin_user or request.user.is_staff or request.user.is_superuser:
return True
if request.method == 'DELETE':
return obj.created_by == request.user
return obj.is_user_manager_or_admin(request.user)
class IsTaskAreaOwnerOrAdmin(permissions.BasePermission):
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -11,8 +11,8 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=JetBrains+Mono:wght@400;500;600;700&family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<title>Schichtplaner — Veranstaltungsschichtpläne</title>
<script type="module" crossorigin src="/assets/index-KtQZvLw-.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CGy_Z7Jy.css">
<script type="module" crossorigin src="/assets/index-BnAMvh5e.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-5idyA2ig.css">
</head>
<body class="bg-paper text-main font-sans antialiased min-h-screen">
<div id="root"></div>
+31 -4
View File
@@ -264,6 +264,29 @@ export default function App() {
}
};
const handleDeleteEvent = async (eventObj) => {
if (!eventObj || !eventObj.id) return;
if (!window.confirm(`Möchtest du die Veranstaltung "${eventObj.title}" und alle zugehörigen Schichten wirklich unwiderruflich löschen?`)) {
return;
}
try {
await apiFetch(`/events/${eventObj.id}/`, {
method: 'DELETE'
});
showNotification(`Veranstaltung "${eventObj.title}" wurde erfolgreich gelöscht.`);
if (selectedEventId === eventObj.id) {
setSelectedEventId(null);
}
if (activeTab === 'event-editor') {
setActiveTab('home');
}
refreshEvents();
} catch (err) {
showNotification(`Fehler beim Löschen: ${err.message}`);
}
};
const [eventToEdit, setEventToEdit] = useState(null);
const handleSaveEventSubmit = async (eventPayload) => {
@@ -444,6 +467,7 @@ export default function App() {
setEventToEdit(evt);
setActiveTab('event-editor');
}}
onDeleteEvent={handleDeleteEvent}
branding={branding}
/>
) : activeTab === 'calendar' ? (
@@ -467,8 +491,10 @@ export default function App() {
<EventEditorPage
eventToEdit={eventToEdit}
skills={skills}
user={user}
onBack={() => setActiveTab('schedule')}
onSubmit={handleSaveEventSubmit}
onDeleteEvent={handleDeleteEvent}
/>
) : activeTab === 'templates' ? (
<TemplatesPage
@@ -522,6 +548,11 @@ export default function App() {
user={user}
onSignupClick={handleShiftSignupClick}
onCancelClick={handleCancelSignup}
onEditEvent={(evt) => {
setEventToEdit(evt);
setActiveTab('event-editor');
}}
onDeleteEvent={handleDeleteEvent}
onRemoveUserFromShift={async (shift, signupId) => {
try {
const res = await apiFetch(`/shifts/${shift.id}/signup/${signupId}/`, {
@@ -539,10 +570,6 @@ export default function App() {
onGenerateClaimLink={handleGenerateClaimLink}
onExportPdf={handleExportPdf}
onPrintView={handlePrintView}
onEditEvent={(evt) => {
setEventToEdit(evt);
setActiveTab('event-editor');
}}
/>
</div>
)}
+12 -2
View File
@@ -1,7 +1,7 @@
/* Hallmark · pre-emit critique: P5 H5 E5 S5 R5 V5 */
/* Hallmark · component: shift-matrix-table · genre: editorial · theme: Atelier (Dark & Light) · studied-DNA: https://inihaus.de */
import React from 'react';
import { Clock, UserCheck, Shield, Printer, Download, Award, CheckCircle2, UserX, UserPlus, MapPin, Edit3 } from 'lucide-react';
import { Clock, UserCheck, Shield, Printer, Download, Award, CheckCircle2, UserX, UserPlus, MapPin, Edit3, Trash2 } from 'lucide-react';
export default function ShiftMatrixTable({
event,
@@ -12,7 +12,8 @@ export default function ShiftMatrixTable({
onGenerateClaimLink,
onExportPdf,
onPrintView,
onEditEvent
onEditEvent,
onDeleteEvent
}) {
if (!event || !event.task_areas || event.task_areas.length === 0) {
return (
@@ -71,6 +72,15 @@ export default function ShiftMatrixTable({
<Edit3 className="w-3.5 h-3.5" /> SCHICHTEN BEARBEITEN
</button>
)}
{user && (user.is_admin_user || user.is_staff || user.is_superuser || event?.created_by === user.id || event?.created_by?.id === user.id) && onDeleteEvent && (
<button
onClick={() => onDeleteEvent(event)}
className="px-3.5 py-1.5 rounded-sm bg-red-500/10 hover:bg-red-500/20 text-red-400 border border-red-500/30 transition flex items-center gap-1.5 font-bold"
title="Diese Veranstaltung unwiderruflich löschen"
>
<Trash2 className="w-3.5 h-3.5" /> LÖSCHEN
</button>
)}
<button
onClick={onPrintView}
className="px-3.5 py-1.5 rounded-sm bg-surface hover:bg-surface-hover text-main border border-grid transition flex items-center gap-1.5 font-bold"
+16
View File
@@ -670,6 +670,22 @@ export default function AdminPage({ branding, onRefreshBranding, onRefreshEvents
}`}>
{evt.is_active !== false ? 'AKTIV' : 'DEAKTIVIERT'}
</span>
<button
onClick={async () => {
if (window.confirm(`Möchtest du "${evt.title}" und alle Schichten wirklich unwiderruflich löschen?`)) {
try {
await apiFetch(`/events/${evt.id}/`, { method: 'DELETE' });
onRefreshEvents();
} catch (e) {
alert(e.message);
}
}
}}
className="px-2.5 py-1 rounded-sm text-[10px] font-bold bg-red-500/10 hover:bg-red-500/20 text-red-400 border border-red-500/20 transition flex items-center gap-1"
title="Veranstaltung löschen"
>
<Trash2 className="w-3 h-3" /> LÖSCHEN
</button>
</div>
</div>
))}
+30 -18
View File
@@ -4,7 +4,7 @@ import React, { useState, useEffect } from 'react';
import { ArrowLeft, Calendar, Layers, Plus, Trash2, Edit3, CheckCircle2, AlertCircle, Users, UserCheck } from 'lucide-react';
import { apiFetch } from '../api/client';
export default function EventEditorPage({ eventToEdit, skills, user, onBack, onSubmit }) {
export default function EventEditorPage({ eventToEdit, skills, user, onBack, onSubmit, onDeleteEvent }) {
const [title, setTitle] = useState(eventToEdit?.title || '');
const [description, setDescription] = useState(eventToEdit?.description || '');
const [location, setLocation] = useState(eventToEdit?.location || '');
@@ -434,23 +434,35 @@ export default function EventEditorPage({ eventToEdit, skills, user, onBack, onS
</div>
{/* Submit Actions */}
<div className="flex items-center justify-end gap-3 pt-2 font-mono">
<button
type="button"
onClick={onBack}
className="px-5 py-2.5 rounded-sm text-xs font-semibold text-muted hover:bg-surface-hover transition border border-grid"
>
Abbrechen
</button>
<button
type="submit"
disabled={submitting}
style={{ backgroundColor: 'var(--brand-primary)' }}
className="px-6 py-2.5 rounded-sm text-xs font-bold text-white transition hover:brightness-110 shadow-sm flex items-center gap-2"
>
<CheckCircle2 className="w-4 h-4" />
{submitting ? 'Speichern...' : 'Veranstaltung Speichern'}
</button>
<div className="flex items-center justify-between gap-3 pt-2 font-mono">
{eventToEdit && onDeleteEvent && user && (user.is_admin_user || user.is_staff || user.is_superuser || eventToEdit.created_by === user.id || eventToEdit.created_by?.id === user.id) ? (
<button
type="button"
onClick={() => onDeleteEvent(eventToEdit)}
className="px-4 py-2.5 rounded-sm text-xs font-bold bg-red-500/10 hover:bg-red-500/20 text-red-400 border border-red-500/20 transition flex items-center gap-1.5"
>
<Trash2 className="w-4 h-4" /> VERANSTALTUNG LÖSCHEN
</button>
) : <div />}
<div className="flex items-center gap-3">
<button
type="button"
onClick={onBack}
className="px-5 py-2.5 rounded-sm text-xs font-semibold text-muted hover:bg-surface-hover transition border border-grid"
>
Abbrechen
</button>
<button
type="submit"
disabled={submitting}
style={{ backgroundColor: 'var(--brand-primary)' }}
className="px-6 py-2.5 rounded-sm text-xs font-bold text-white transition hover:brightness-110 shadow-sm flex items-center gap-2"
>
<CheckCircle2 className="w-4 h-4" />
{submitting ? 'Speichern...' : 'Veranstaltung Speichern'}
</button>
</div>
</div>
</form>
</div>
+12 -1
View File
@@ -1,7 +1,7 @@
/* Hallmark · pre-emit critique: P5 H5 E5 S5 R5 V5 */
/* Hallmark · redesign: HomePage · studied-DNA: https://inihaus.de · macrostructure: Newsstand Feed */
import React, { useState } from 'react';
import { Search, Calendar, MapPin, Clock, ArrowRight, Plus, EyeOff, CheckCircle2, Edit3, ShieldAlert } from 'lucide-react';
import { Search, Calendar, MapPin, Clock, ArrowRight, Plus, EyeOff, CheckCircle2, Edit3, ShieldAlert, Trash2 } from 'lucide-react';
export default function HomePage({
events,
@@ -10,6 +10,7 @@ export default function HomePage({
onOpenCreateEvent,
onToggleEventActive,
onEditEvent,
onDeleteEvent,
branding
}) {
const [searchQuery, setSearchQuery] = useState('');
@@ -212,6 +213,16 @@ export default function HomePage({
{evt.is_active !== false ? <EyeOff className="w-3.5 h-3.5" /> : <CheckCircle2 className="w-3.5 h-3.5" />}
</button>
)}
{(isUserAdmin || (user && (evt.created_by === user.id || evt.created_by?.id === user.id))) && onDeleteEvent && (
<button
onClick={() => onDeleteEvent(evt)}
className="py-1.5 px-2 rounded-sm text-[10px] font-mono font-bold bg-red-500/10 text-red-400 border border-red-500/20 hover:bg-red-500/20 transition flex items-center gap-1 shrink-0"
title="Veranstaltung unwiderruflich löschen"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
)}
</div>
</div>
</div>