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:
Binary file not shown.
@@ -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):
|
||||||
|
|||||||
+1
File diff suppressed because one or more lines are too long
+272
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
-272
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -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
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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,7 +434,18 @@ 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">
|
||||||
|
{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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
@@ -452,6 +463,7 @@ export default function EventEditorPage({ eventToEdit, skills, user, onBack, onS
|
|||||||
{submitting ? 'Speichern...' : 'Veranstaltung Speichern'}
|
{submitting ? 'Speichern...' : 'Veranstaltung Speichern'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user