feat: Add specific shift date field for multi-day events with date selector in EventEditor and date badge in ShiftMatrix
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0.7 on 2026-07-31 08:51
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('events', '0003_event_is_active'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='shift',
|
||||||
|
name='date',
|
||||||
|
field=models.DateField(blank=True, help_text='Spezifisches Datum für mehrtägige Veranstaltungen', null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
Binary file not shown.
@@ -39,13 +39,15 @@ class TaskArea(models.Model):
|
|||||||
class Shift(models.Model):
|
class Shift(models.Model):
|
||||||
task_area = models.ForeignKey(TaskArea, on_delete=models.CASCADE, related_name='shifts')
|
task_area = models.ForeignKey(TaskArea, on_delete=models.CASCADE, related_name='shifts')
|
||||||
title = models.CharField(max_length=150) # e.g., "Frühschicht", "14:00 - 18:00"
|
title = models.CharField(max_length=150) # e.g., "Frühschicht", "14:00 - 18:00"
|
||||||
|
date = models.DateField(null=True, blank=True, help_text="Spezifisches Datum für mehrtägige Veranstaltungen")
|
||||||
start_time = models.CharField(max_length=50) # e.g. "14:00" or ISO string
|
start_time = models.CharField(max_length=50) # e.g. "14:00" or ISO string
|
||||||
end_time = models.CharField(max_length=50) # e.g. "18:00" or ISO string
|
end_time = models.CharField(max_length=50) # e.g. "18:00" or ISO string
|
||||||
max_participants = models.PositiveIntegerField(default=1)
|
max_participants = models.PositiveIntegerField(default=1)
|
||||||
required_skills = models.ManyToManyField(Skill, blank=True, related_name='shifts')
|
required_skills = models.ManyToManyField(Skill, blank=True, related_name='shifts')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.task_area.name}: {self.title} ({self.start_time} - {self.end_time})"
|
date_str = f" ({self.date.strftime('%d.%m.%Y')})" if self.date else ""
|
||||||
|
return f"{self.task_area.name}: {self.title}{date_str} ({self.start_time} - {self.end_time})"
|
||||||
|
|
||||||
class ShiftSignup(models.Model):
|
class ShiftSignup(models.Model):
|
||||||
shift = models.ForeignKey(Shift, on_delete=models.CASCADE, related_name='signups')
|
shift = models.ForeignKey(Shift, on_delete=models.CASCADE, related_name='signups')
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class ShiftSerializer(serializers.ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Shift
|
model = Shift
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'task_area', 'title', 'start_time', 'end_time',
|
'id', 'task_area', 'title', 'date', 'start_time', 'end_time',
|
||||||
'max_participants', 'required_skills', 'required_skill_ids',
|
'max_participants', 'required_skills', 'required_skill_ids',
|
||||||
'signups', 'signups_count', 'is_full'
|
'signups', 'signups_count', 'is_full'
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -369,6 +369,7 @@ class EventTemplateViewSet(viewsets.ModelViewSet):
|
|||||||
shift = Shift.objects.create(
|
shift = Shift.objects.create(
|
||||||
task_area=ta,
|
task_area=ta,
|
||||||
title=shift_data.get('title', 'Schicht'),
|
title=shift_data.get('title', 'Schicht'),
|
||||||
|
date=shift_data.get('date') or start_date,
|
||||||
start_time=shift_data.get('start_time', '18:00'),
|
start_time=shift_data.get('start_time', '18:00'),
|
||||||
end_time=shift_data.get('end_time', '22:00'),
|
end_time=shift_data.get('end_time', '22:00'),
|
||||||
max_participants=shift_data.get('max_participants', 1)
|
max_participants=shift_data.get('max_participants', 1)
|
||||||
|
|||||||
+54
-54
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -11,7 +11,7 @@
|
|||||||
<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-9dCYFrQm.js"></script>
|
<script type="module" crossorigin src="/assets/index-BTg0Pu2I.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-DyS9UMYS.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DyS9UMYS.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">
|
||||||
|
|||||||
@@ -321,11 +321,13 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let sData of taData.shifts) {
|
for (let sData of taData.shifts) {
|
||||||
|
const shiftDate = sData.date || eventPayload.start_date;
|
||||||
if (sData.id) {
|
if (sData.id) {
|
||||||
await apiFetch(`/shifts/${sData.id}/`, {
|
await apiFetch(`/shifts/${sData.id}/`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
title: sData.title,
|
title: sData.title,
|
||||||
|
date: shiftDate,
|
||||||
start_time: sData.start_time,
|
start_time: sData.start_time,
|
||||||
end_time: sData.end_time,
|
end_time: sData.end_time,
|
||||||
max_participants: sData.max_participants,
|
max_participants: sData.max_participants,
|
||||||
@@ -338,6 +340,7 @@ export default function App() {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
task_area: taId,
|
task_area: taId,
|
||||||
title: sData.title,
|
title: sData.title,
|
||||||
|
date: shiftDate,
|
||||||
start_time: sData.start_time,
|
start_time: sData.start_time,
|
||||||
end_time: sData.end_time,
|
end_time: sData.end_time,
|
||||||
max_participants: sData.max_participants,
|
max_participants: sData.max_participants,
|
||||||
|
|||||||
@@ -143,7 +143,14 @@ export default function ShiftMatrixTable({
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<td className="py-3.5 px-4">
|
<td className="py-3.5 px-4">
|
||||||
<div className="font-semibold text-main">{shift.title}</div>
|
<div className="font-semibold text-main flex items-center gap-2 flex-wrap">
|
||||||
|
<span>{shift.title}</span>
|
||||||
|
{shift.date && (
|
||||||
|
<span className="text-[10px] font-mono font-bold text-indigo-400 bg-indigo-500/10 px-2 py-0.5 rounded-sm border border-indigo-500/20">
|
||||||
|
📅 {new Date(shift.date + 'T00:00:00').toLocaleDateString('de-DE', { weekday: 'short', day: '2-digit', month: '2-digit' })}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="text-[11px] font-mono text-muted flex items-center gap-1 mt-0.5">
|
<div className="text-[11px] font-mono text-muted flex items-center gap-1 mt-0.5">
|
||||||
<Clock className="w-3 h-3 text-muted shrink-0" />
|
<Clock className="w-3 h-3 text-muted shrink-0" />
|
||||||
<span>{shift.start_time} — {shift.end_time} Uhr</span>
|
<span>{shift.start_time} — {shift.end_time} Uhr</span>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export default function EventEditorPage({ eventToEdit, skills, onBack, onSubmit
|
|||||||
shifts: (ta.shifts || []).map(s => ({
|
shifts: (ta.shifts || []).map(s => ({
|
||||||
id: s.id,
|
id: s.id,
|
||||||
title: s.title,
|
title: s.title,
|
||||||
|
date: s.date || (eventToEdit ? eventToEdit.start_date : new Date().toISOString().split('T')[0]),
|
||||||
start_time: s.start_time,
|
start_time: s.start_time,
|
||||||
end_time: s.end_time,
|
end_time: s.end_time,
|
||||||
max_participants: s.max_participants || 1,
|
max_participants: s.max_participants || 1,
|
||||||
@@ -78,6 +79,7 @@ export default function EventEditorPage({ eventToEdit, skills, onBack, onSubmit
|
|||||||
const updated = [...taskAreas];
|
const updated = [...taskAreas];
|
||||||
updated[areaIndex].shifts.push({
|
updated[areaIndex].shifts.push({
|
||||||
title: `Schicht ${updated[areaIndex].shifts.length + 1}`,
|
title: `Schicht ${updated[areaIndex].shifts.length + 1}`,
|
||||||
|
date: startDate,
|
||||||
start_time: '14:00',
|
start_time: '14:00',
|
||||||
end_time: '18:00',
|
end_time: '18:00',
|
||||||
max_participants: 1,
|
max_participants: 1,
|
||||||
@@ -300,7 +302,16 @@ export default function EventEditorPage({ eventToEdit, skills, onBack, onSubmit
|
|||||||
placeholder="Titel"
|
placeholder="Titel"
|
||||||
value={shift.title}
|
value={shift.title}
|
||||||
onChange={(e) => updateShift(aIdx, sIdx, 'title', e.target.value)}
|
onChange={(e) => updateShift(aIdx, sIdx, 'title', e.target.value)}
|
||||||
className="sm:col-span-4 px-2.5 py-1 rounded-sm input-field text-xs"
|
className="sm:col-span-3 px-2.5 py-1 rounded-sm input-field text-xs"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
title="Schichtdatum"
|
||||||
|
min={startDate}
|
||||||
|
max={endDate}
|
||||||
|
value={shift.date || startDate}
|
||||||
|
onChange={(e) => updateShift(aIdx, sIdx, 'date', e.target.value)}
|
||||||
|
className="sm:col-span-3 px-2 py-1 rounded-sm input-field text-xs font-mono text-indigo-400 font-bold"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -316,14 +327,14 @@ export default function EventEditorPage({ eventToEdit, skills, onBack, onSubmit
|
|||||||
onChange={(e) => updateShift(aIdx, sIdx, 'end_time', e.target.value)}
|
onChange={(e) => updateShift(aIdx, sIdx, 'end_time', e.target.value)}
|
||||||
className="sm:col-span-2 px-2.5 py-1 rounded-sm input-field text-xs"
|
className="sm:col-span-2 px-2.5 py-1 rounded-sm input-field text-xs"
|
||||||
/>
|
/>
|
||||||
<div className="sm:col-span-3 flex items-center gap-1">
|
<div className="sm:col-span-1 flex items-center gap-1">
|
||||||
<span className="text-[11px] text-muted">Plätze:</span>
|
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
|
title="Max. Plätze"
|
||||||
value={shift.max_participants}
|
value={shift.max_participants}
|
||||||
onChange={(e) => updateShift(aIdx, sIdx, 'max_participants', parseInt(e.target.value) || 1)}
|
onChange={(e) => updateShift(aIdx, sIdx, 'max_participants', parseInt(e.target.value) || 1)}
|
||||||
className="w-full px-2 py-1 rounded-sm input-field text-xs font-bold"
|
className="w-full px-1.5 py-1 rounded-sm input-field text-xs font-bold text-center"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
|||||||
Reference in New Issue
Block a user