feat(calendar): Mehrfachauswahl + Bulk-Loeschen in der Listen-Ansicht

Checkbox-Spalte plus Header-Checkbox "Alle". Bulk-Aktion mit
Bestaetigung loescht ausgewaehlte Termine; Read-Only-Eintraege
werden uebersprungen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Hacker
2026-04-13 11:11:12 +02:00
parent e96c84b5f7
commit f6626da114
+78 -3
View File
@@ -45,10 +45,22 @@
optionLabel="label" optionValue="value" placeholder="Alle Kalender"
showClear style="min-width: 180px" />
</div>
<div class="list-meta">{{ filteredListEvents.length }} Termin(e)</div>
<div class="list-meta-row">
<div class="list-meta">{{ filteredListEvents.length }} Termin(e)</div>
<div v-if="selectedListIds.length" class="list-bulk">
<span>{{ selectedListIds.length }} ausgewaehlt</span>
<Button icon="pi pi-trash" :label="`${selectedListIds.length} loeschen`"
severity="danger" size="small" @click="deleteSelectedListEvents" />
<Button label="Auswahl aufheben" size="small" text @click="selectedListIds = []" />
</div>
</div>
<table class="list-table">
<thead>
<tr>
<th class="col-check">
<Checkbox v-model="allListSelected" :binary="true"
@change="toggleAllListSelected" title="Alle auswaehlen" />
</th>
<th @click="toggleListSort('dtstart')" class="sortable">
Datum <i v-if="listSort === 'dtstart'" :class="listSortDir === 'asc' ? 'pi pi-arrow-up' : 'pi pi-arrow-down'"></i>
</th>
@@ -61,7 +73,14 @@
</tr>
</thead>
<tbody>
<tr v-for="ev in filteredListEvents" :key="ev.id" class="list-row" @click="openEditEvent(ev)">
<tr v-for="ev in filteredListEvents" :key="ev.id" class="list-row"
:class="{ selected: selectedListIds.includes(ev.id) }"
@click="openEditEvent(ev)">
<td class="col-check" @click.stop>
<Checkbox :modelValue="selectedListIds.includes(ev.id)" :binary="true"
:disabled="ev._cal?.permission === 'read'"
@update:modelValue="toggleListSelect(ev.id, $event)" />
</td>
<td class="col-date">
<div>{{ formatListDate(ev) }}</div>
<div v-if="!ev.all_day" class="meta-time">{{ formatListTime(ev) }}</div>
@@ -80,7 +99,7 @@
</td>
</tr>
<tr v-if="!filteredListEvents.length">
<td colspan="5" class="empty-row">Keine Termine gefunden.</td>
<td colspan="6" class="empty-row">Keine Termine gefunden.</td>
</tr>
</tbody>
</table>
@@ -337,6 +356,7 @@ import InputText from 'primevue/inputtext'
import Textarea from 'primevue/textarea'
import Select from 'primevue/select'
import SelectButton from 'primevue/selectbutton'
import Checkbox from 'primevue/checkbox'
import FullCalendar from '@fullcalendar/vue3'
import dayGridPlugin from '@fullcalendar/daygrid'
import timeGridPlugin from '@fullcalendar/timegrid'
@@ -368,6 +388,57 @@ const listTo = ref('')
const listCalFilter = ref(null)
const listSort = ref('dtstart')
const listSortDir = ref('asc')
const selectedListIds = ref([])
const allListSelected = computed({
get: () => {
const writable = filteredListEvents.value.filter(e => e._cal?.permission !== 'read')
return writable.length > 0 && writable.every(e => selectedListIds.value.includes(e.id))
},
set: () => {},
})
function toggleAllListSelected() {
const writableIds = filteredListEvents.value
.filter(e => e._cal?.permission !== 'read').map(e => e.id)
const allSel = writableIds.length > 0 && writableIds.every(id => selectedListIds.value.includes(id))
if (allSel) {
selectedListIds.value = selectedListIds.value.filter(id => !writableIds.includes(id))
} else {
const set = new Set(selectedListIds.value)
writableIds.forEach(id => set.add(id))
selectedListIds.value = [...set]
}
}
function toggleListSelect(id, checked) {
if (checked) {
if (!selectedListIds.value.includes(id)) selectedListIds.value = [...selectedListIds.value, id]
} else {
selectedListIds.value = selectedListIds.value.filter(x => x !== id)
}
}
async function deleteSelectedListEvents() {
const ids = [...selectedListIds.value]
if (!ids.length) return
if (!confirm(`${ids.length} Termin(e) wirklich loeschen?`)) return
let ok = 0, fail = 0
for (const id of ids) {
try {
await apiClient.delete(`/events/${id}`)
ok++
} catch { fail++ }
}
selectedListIds.value = []
toast.add({
severity: fail ? 'warn' : 'success',
summary: `${ok} geloescht${fail ? `, ${fail} fehlgeschlagen` : ''}`,
life: 3000,
})
await loadListEvents()
refreshEvents()
}
const listCalOptions = computed(() => calendars.value.map(c => ({ label: c.name, value: c.id })))
@@ -1117,6 +1188,10 @@ onUnmounted(() => {
.list-view { display: flex; flex-direction: column; gap: 0.75rem; }
.list-filters { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
.list-meta { font-size: 0.8rem; color: var(--p-text-muted-color); }
.list-meta-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
.list-bulk { display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; }
.col-check { width: 36px; }
.list-row.selected { background: var(--p-primary-50); }
.list-table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
.list-table th { text-align: left; padding: 0.5rem; border-bottom: 2px solid var(--p-surface-200); font-weight: 600; user-select: none; }
.list-table th.sortable { cursor: pointer; }