feat: Liste/Kalender/Adressbuch beim Anlegen waehlbar (nur Schreibrecht)
- ContactsView: Adressbuch-Auswahl im Kontakt-Dialog (versteckt bei nur einem beschreibbaren Buch). Neuer-Kontakt-Button disabled wenn keiner. - TasksView: gleiches fuer Aufgabenlisten. - CalendarView: writableCalendars (eigene + Schreibfreigaben) ersetzt ownCalendars in Event-Dialog und Import-Auswahl. Auswahlfeld nur ab 2. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4d67819cac
commit
2ef186e262
|
|
@ -9,7 +9,8 @@
|
|||
<input ref="calImportInput" type="file" accept=".ics,.ical,.csv" hidden @change="onCalImportFile" />
|
||||
<Button icon="pi pi-download" label="Export" size="small" outlined
|
||||
:disabled="!exportableCalendars.length" @click="showCalExportDialog = true" />
|
||||
<Button icon="pi pi-plus" label="Neuer Termin" size="small" @click="openNewEvent()" />
|
||||
<Button icon="pi pi-plus" label="Neuer Termin" size="small"
|
||||
:disabled="!writableCalendars.length" @click="openNewEvent()" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -118,7 +119,7 @@
|
|||
<Dialog v-model:visible="showCalImportDialog" header="In welchen Kalender importieren?" modal :style="{ width: '420px' }">
|
||||
<div class="field">
|
||||
<label>Kalender</label>
|
||||
<Select v-model="importTargetCalId" :options="ownCalendars" optionLabel="name" optionValue="id" fluid />
|
||||
<Select v-model="importTargetCalId" :options="writableCalendars" optionLabel="name" optionValue="id" fluid />
|
||||
</div>
|
||||
<p class="hint" style="font-size:0.85rem;color:var(--p-text-muted-color)">Datei: {{ pendingImportFile?.name }}</p>
|
||||
<template #footer>
|
||||
|
|
@ -168,9 +169,9 @@
|
|||
<label>Titel</label>
|
||||
<InputText v-model="eventForm.summary" fluid autofocus />
|
||||
</div>
|
||||
<div class="field">
|
||||
<div v-if="writableCalendars.length > 1" class="field">
|
||||
<label>Kalender</label>
|
||||
<Select v-model="eventForm.calendar_id" :options="ownCalendars" optionLabel="name" optionValue="id" fluid />
|
||||
<Select v-model="eventForm.calendar_id" :options="writableCalendars" optionLabel="name" optionValue="id" fluid />
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<div class="field">
|
||||
|
|
@ -616,6 +617,10 @@ const currentEditScope = ref(null)
|
|||
const permOptions = [{ label: 'Lesen', value: 'read' }, { label: 'Lesen+Schreiben', value: 'readwrite' }]
|
||||
|
||||
const ownCalendars = computed(() => calendars.value.filter(c => c.permission === 'owner'))
|
||||
// Beschreibbar = eigener Kalender ODER Freigabe mit Schreibrecht.
|
||||
const writableCalendars = computed(() =>
|
||||
calendars.value.filter(c => c.permission === 'owner' || c.permission === 'readwrite')
|
||||
)
|
||||
const exportableCalendars = computed(() => calendars.value)
|
||||
|
||||
// --- Calendar Import / Export ---
|
||||
|
|
@ -638,8 +643,8 @@ watch(showCalExportDialog, (v) => {
|
|||
})
|
||||
|
||||
function triggerCalImport() {
|
||||
if (!ownCalendars.value.length) {
|
||||
toast.add({ severity: 'warn', summary: 'Kein eigener Kalender', life: 3000 })
|
||||
if (!writableCalendars.value.length) {
|
||||
toast.add({ severity: 'warn', summary: 'Kein beschreibbarer Kalender', life: 3000 })
|
||||
return
|
||||
}
|
||||
calImportInput.value?.click()
|
||||
|
|
@ -650,7 +655,7 @@ function onCalImportFile(ev) {
|
|||
ev.target.value = ''
|
||||
if (!file) return
|
||||
pendingImportFile.value = file
|
||||
importTargetCalId.value = ownCalendars.value[0]?.id
|
||||
importTargetCalId.value = writableCalendars.value[0]?.id
|
||||
showCalImportDialog.value = true
|
||||
}
|
||||
|
||||
|
|
@ -969,6 +974,10 @@ async function createCalendar() {
|
|||
}
|
||||
|
||||
function openNewEvent(start, end, allDay = false) {
|
||||
if (!writableCalendars.value.length) {
|
||||
toast.add({ severity: 'warn', summary: 'Kein beschreibbarer Kalender vorhanden', life: 3000 })
|
||||
return
|
||||
}
|
||||
editingEvent.value = null
|
||||
const now = start || new Date()
|
||||
const later = end || new Date(now.getTime() + 3600000)
|
||||
|
|
@ -976,7 +985,7 @@ function openNewEvent(start, end, allDay = false) {
|
|||
summary: '',
|
||||
description: '',
|
||||
location: '',
|
||||
calendar_id: ownCalendars.value[0]?.id,
|
||||
calendar_id: writableCalendars.value[0].id,
|
||||
dtstart: toLocalISO(now, allDay),
|
||||
dtend: toLocalISO(later, allDay),
|
||||
all_day: allDay,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@
|
|||
<input ref="importInput" type="file" accept=".vcf,.vcard,.csv" hidden @change="onImportFile" />
|
||||
<Button icon="pi pi-download" label="Export" size="small" outlined
|
||||
:disabled="!selectedBookId" @click="showExportDialog = true" />
|
||||
<Button icon="pi pi-user-plus" label="Neuer Kontakt" size="small" @click="openNewContact" />
|
||||
<Button icon="pi pi-user-plus" label="Neuer Kontakt" size="small"
|
||||
:disabled="!writableBooks.length" @click="openNewContact" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -179,6 +180,11 @@
|
|||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel value="general">
|
||||
<div v-if="writableBooks.length > 1" class="field" style="max-width:400px">
|
||||
<label>Adressbuch</label>
|
||||
<Select v-model="contactTargetBookId" :options="writableBooks"
|
||||
optionLabel="name" optionValue="id" fluid />
|
||||
</div>
|
||||
<div class="photo-row">
|
||||
<div class="avatar large" :style="{ background: avatarColor(contactForm) }">
|
||||
<img v-if="contactForm.photo" :src="contactForm.photo" />
|
||||
|
|
@ -391,6 +397,10 @@ const auth = useAuthStore()
|
|||
const addressBooks = ref([])
|
||||
const contacts = ref([])
|
||||
const selectedBookId = ref(null)
|
||||
const contactTargetBookId = ref(null)
|
||||
const writableBooks = computed(() =>
|
||||
addressBooks.value.filter(b => b.permission === 'owner' || b.permission === 'readwrite')
|
||||
)
|
||||
const loading = ref(false)
|
||||
const searchQuery = ref('')
|
||||
let searchTimer = null
|
||||
|
|
@ -615,10 +625,17 @@ function copyText(t) {
|
|||
}
|
||||
|
||||
function openNewContact() {
|
||||
if (!writableBooks.value.length) {
|
||||
toast.add({ severity: 'warn', summary: 'Kein beschreibbares Adressbuch vorhanden', life: 3000 })
|
||||
return
|
||||
}
|
||||
editingContactId.value = null
|
||||
Object.assign(contactForm, emptyContact())
|
||||
categoriesString.value = ''
|
||||
activeTab.value = 'general'
|
||||
// Default: aktuell markiertes Buch, falls beschreibbar, sonst erstes beschreibbares
|
||||
const selectedBook = writableBooks.value.find(b => b.id === selectedBookId.value)
|
||||
contactTargetBookId.value = selectedBook ? selectedBook.id : writableBooks.value[0].id
|
||||
showContactDialog.value = true
|
||||
}
|
||||
|
||||
|
|
@ -685,7 +702,12 @@ async function saveContact() {
|
|||
if (editingContactId.value) {
|
||||
await apiClient.put(`/contacts/${editingContactId.value}`, payload)
|
||||
} else {
|
||||
await apiClient.post(`/addressbooks/${selectedBookId.value}/contacts`, payload)
|
||||
const target = contactTargetBookId.value || selectedBookId.value
|
||||
if (!target) {
|
||||
toast.add({ severity: 'error', summary: 'Bitte Adressbuch waehlen', life: 3000 })
|
||||
return
|
||||
}
|
||||
await apiClient.post(`/addressbooks/${target}/contacts`, payload)
|
||||
}
|
||||
showContactDialog.value = false
|
||||
await loadBooks()
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<Button icon="pi pi-download" label="Export" size="small" outlined
|
||||
:disabled="!selectedListId" @click="showExportDialog = true" />
|
||||
<Button icon="pi pi-plus" label="Neue Aufgabe" size="small"
|
||||
:disabled="!selectedListId" @click="openNewTask" />
|
||||
:disabled="!writableLists.length" @click="openNewTask" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -166,6 +166,11 @@
|
|||
<!-- Task Dialog -->
|
||||
<Dialog v-model:visible="showTaskDialog" :header="editingTaskId ? 'Aufgabe bearbeiten' : 'Neue Aufgabe'"
|
||||
modal :style="{ width: '560px' }">
|
||||
<div v-if="writableLists.length > 1" class="field">
|
||||
<label>Liste</label>
|
||||
<Select v-model="taskTargetListId" :options="writableLists"
|
||||
optionLabel="name" optionValue="id" fluid />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Titel</label>
|
||||
<InputText v-model="taskForm.summary" fluid autofocus />
|
||||
|
|
@ -247,6 +252,10 @@ const username = computed(() => auth.user?.username || '')
|
|||
|
||||
const lists = ref([])
|
||||
const selectedListId = ref(null)
|
||||
const taskTargetListId = ref(null)
|
||||
const writableLists = computed(() =>
|
||||
lists.value.filter(l => l.permission === 'owner' || l.permission === 'readwrite')
|
||||
)
|
||||
const tasks = ref([])
|
||||
const search = ref('')
|
||||
const hideDone = ref(false)
|
||||
|
|
@ -464,12 +473,19 @@ async function deleteList() {
|
|||
}
|
||||
|
||||
function openNewTask() {
|
||||
if (!writableLists.value.length) {
|
||||
toast.add({ severity: 'warn', summary: 'Keine beschreibbare Liste', life: 3000 })
|
||||
return
|
||||
}
|
||||
editingTaskId.value = null
|
||||
Object.assign(taskForm, {
|
||||
summary: '', description: '', due: '',
|
||||
status: 'NEEDS-ACTION', priority: null, percent_complete: null,
|
||||
categories: '',
|
||||
})
|
||||
// Default-Liste: aktuell markierte falls beschreibbar, sonst erste beschreibbare
|
||||
const sel = writableLists.value.find(l => l.id === selectedListId.value)
|
||||
taskTargetListId.value = sel ? sel.id : writableLists.value[0].id
|
||||
showTaskDialog.value = true
|
||||
}
|
||||
|
||||
|
|
@ -502,7 +518,12 @@ async function saveTask() {
|
|||
if (editingTaskId.value) {
|
||||
await apiClient.put(`/tasks/${editingTaskId.value}`, payload)
|
||||
} else {
|
||||
await apiClient.post(`/tasklists/${selectedListId.value}/tasks`, payload)
|
||||
const target = taskTargetListId.value || selectedListId.value
|
||||
if (!target) {
|
||||
toast.add({ severity: 'error', summary: 'Bitte Liste waehlen', life: 3000 })
|
||||
return
|
||||
}
|
||||
await apiClient.post(`/tasklists/${target}/tasks`, payload)
|
||||
}
|
||||
showTaskDialog.value = false
|
||||
await loadLists()
|
||||
|
|
|
|||
Loading…
Reference in New Issue