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:
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user