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): def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS: if request.method in permissions.SAFE_METHODS:
return True 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) return obj.is_user_manager_or_admin(request.user)
class IsTaskAreaOwnerOrAdmin(permissions.BasePermission): 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 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"> <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> <title>Schichtplaner — Veranstaltungsschichtpläne</title>
<script type="module" crossorigin src="/assets/index-KtQZvLw-.js"></script> <script type="module" crossorigin src="/assets/index-BnAMvh5e.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CGy_Z7Jy.css"> <link rel="stylesheet" crossorigin href="/assets/index-5idyA2ig.css">
</head> </head>
<body class="bg-paper text-main font-sans antialiased min-h-screen"> <body class="bg-paper text-main font-sans antialiased min-h-screen">
<div id="root"></div> <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 [eventToEdit, setEventToEdit] = useState(null);
const handleSaveEventSubmit = async (eventPayload) => { const handleSaveEventSubmit = async (eventPayload) => {
@@ -444,6 +467,7 @@ export default function App() {
setEventToEdit(evt); setEventToEdit(evt);
setActiveTab('event-editor'); setActiveTab('event-editor');
}} }}
onDeleteEvent={handleDeleteEvent}
branding={branding} branding={branding}
/> />
) : activeTab === 'calendar' ? ( ) : activeTab === 'calendar' ? (
@@ -467,8 +491,10 @@ export default function App() {
<EventEditorPage <EventEditorPage
eventToEdit={eventToEdit} eventToEdit={eventToEdit}
skills={skills} skills={skills}
user={user}
onBack={() => setActiveTab('schedule')} onBack={() => setActiveTab('schedule')}
onSubmit={handleSaveEventSubmit} onSubmit={handleSaveEventSubmit}
onDeleteEvent={handleDeleteEvent}
/> />
) : activeTab === 'templates' ? ( ) : activeTab === 'templates' ? (
<TemplatesPage <TemplatesPage
@@ -522,6 +548,11 @@ export default function App() {
user={user} user={user}
onSignupClick={handleShiftSignupClick} onSignupClick={handleShiftSignupClick}
onCancelClick={handleCancelSignup} onCancelClick={handleCancelSignup}
onEditEvent={(evt) => {
setEventToEdit(evt);
setActiveTab('event-editor');
}}
onDeleteEvent={handleDeleteEvent}
onRemoveUserFromShift={async (shift, signupId) => { onRemoveUserFromShift={async (shift, signupId) => {
try { try {
const res = await apiFetch(`/shifts/${shift.id}/signup/${signupId}/`, { const res = await apiFetch(`/shifts/${shift.id}/signup/${signupId}/`, {
@@ -539,10 +570,6 @@ export default function App() {
onGenerateClaimLink={handleGenerateClaimLink} onGenerateClaimLink={handleGenerateClaimLink}
onExportPdf={handleExportPdf} onExportPdf={handleExportPdf}
onPrintView={handlePrintView} onPrintView={handlePrintView}
onEditEvent={(evt) => {
setEventToEdit(evt);
setActiveTab('event-editor');
}}
/> />
</div> </div>
)} )}
+12 -2
View File
@@ -1,7 +1,7 @@
/* Hallmark · pre-emit critique: P5 H5 E5 S5 R5 V5 */ /* 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 */ /* Hallmark · component: shift-matrix-table · genre: editorial · theme: Atelier (Dark & Light) · studied-DNA: https://inihaus.de */
import React from 'react'; 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({ export default function ShiftMatrixTable({
event, event,
@@ -12,7 +12,8 @@ export default function ShiftMatrixTable({
onGenerateClaimLink, onGenerateClaimLink,
onExportPdf, onExportPdf,
onPrintView, onPrintView,
onEditEvent onEditEvent,
onDeleteEvent
}) { }) {
if (!event || !event.task_areas || event.task_areas.length === 0) { if (!event || !event.task_areas || event.task_areas.length === 0) {
return ( return (
@@ -71,6 +72,15 @@ export default function ShiftMatrixTable({
<Edit3 className="w-3.5 h-3.5" /> SCHICHTEN BEARBEITEN <Edit3 className="w-3.5 h-3.5" /> SCHICHTEN BEARBEITEN
</button> </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 <button
onClick={onPrintView} 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" 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'} {evt.is_active !== false ? 'AKTIV' : 'DEAKTIVIERT'}
</span> </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>
</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 { ArrowLeft, Calendar, Layers, Plus, Trash2, Edit3, CheckCircle2, AlertCircle, Users, UserCheck } from 'lucide-react';
import { apiFetch } from '../api/client'; 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 [title, setTitle] = useState(eventToEdit?.title || '');
const [description, setDescription] = useState(eventToEdit?.description || ''); const [description, setDescription] = useState(eventToEdit?.description || '');
const [location, setLocation] = useState(eventToEdit?.location || ''); const [location, setLocation] = useState(eventToEdit?.location || '');
@@ -434,23 +434,35 @@ export default function EventEditorPage({ eventToEdit, skills, user, onBack, onS
</div> </div>
{/* Submit Actions */} {/* Submit Actions */}
<div className="flex items-center justify-end gap-3 pt-2 font-mono"> <div className="flex items-center justify-between gap-3 pt-2 font-mono">
<button {eventToEdit && onDeleteEvent && user && (user.is_admin_user || user.is_staff || user.is_superuser || eventToEdit.created_by === user.id || eventToEdit.created_by?.id === user.id) ? (
type="button" <button
onClick={onBack} type="button"
className="px-5 py-2.5 rounded-sm text-xs font-semibold text-muted hover:bg-surface-hover transition border border-grid" 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"
Abbrechen >
</button> <Trash2 className="w-4 h-4" /> VERANSTALTUNG LÖSCHEN
<button </button>
type="submit" ) : <div />}
disabled={submitting}
style={{ backgroundColor: 'var(--brand-primary)' }} <div className="flex items-center gap-3">
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" <button
> type="button"
<CheckCircle2 className="w-4 h-4" /> onClick={onBack}
{submitting ? 'Speichern...' : 'Veranstaltung Speichern'} className="px-5 py-2.5 rounded-sm text-xs font-semibold text-muted hover:bg-surface-hover transition border border-grid"
</button> >
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> </div>
</form> </form>
</div> </div>
+12 -1
View File
@@ -1,7 +1,7 @@
/* Hallmark · pre-emit critique: P5 H5 E5 S5 R5 V5 */ /* Hallmark · pre-emit critique: P5 H5 E5 S5 R5 V5 */
/* Hallmark · redesign: HomePage · studied-DNA: https://inihaus.de · macrostructure: Newsstand Feed */ /* Hallmark · redesign: HomePage · studied-DNA: https://inihaus.de · macrostructure: Newsstand Feed */
import React, { useState } from 'react'; 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({ export default function HomePage({
events, events,
@@ -10,6 +10,7 @@ export default function HomePage({
onOpenCreateEvent, onOpenCreateEvent,
onToggleEventActive, onToggleEventActive,
onEditEvent, onEditEvent,
onDeleteEvent,
branding branding
}) { }) {
const [searchQuery, setSearchQuery] = useState(''); 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" />} {evt.is_active !== false ? <EyeOff className="w-3.5 h-3.5" /> : <CheckCircle2 className="w-3.5 h-3.5" />}
</button> </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> </div>
</div> </div>