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:
@@ -9,7 +9,8 @@
|
|||||||
<input ref="calImportInput" type="file" accept=".ics,.ical,.csv" hidden @change="onCalImportFile" />
|
<input ref="calImportInput" type="file" accept=".ics,.ical,.csv" hidden @change="onCalImportFile" />
|
||||||
<Button icon="pi pi-download" label="Export" size="small" outlined
|
<Button icon="pi pi-download" label="Export" size="small" outlined
|
||||||
:disabled="!exportableCalendars.length" @click="showCalExportDialog = true" />
|
: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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -118,7 +119,7 @@
|
|||||||
<Dialog v-model:visible="showCalImportDialog" header="In welchen Kalender importieren?" modal :style="{ width: '420px' }">
|
<Dialog v-model:visible="showCalImportDialog" header="In welchen Kalender importieren?" modal :style="{ width: '420px' }">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Kalender</label>
|
<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>
|
</div>
|
||||||
<p class="hint" style="font-size:0.85rem;color:var(--p-text-muted-color)">Datei: {{ pendingImportFile?.name }}</p>
|
<p class="hint" style="font-size:0.85rem;color:var(--p-text-muted-color)">Datei: {{ pendingImportFile?.name }}</p>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -168,9 +169,9 @@
|
|||||||
<label>Titel</label>
|
<label>Titel</label>
|
||||||
<InputText v-model="eventForm.summary" fluid autofocus />
|
<InputText v-model="eventForm.summary" fluid autofocus />
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div v-if="writableCalendars.length > 1" class="field">
|
||||||
<label>Kalender</label>
|
<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>
|
||||||
<div class="field-row">
|
<div class="field-row">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@@ -616,6 +617,10 @@ const currentEditScope = ref(null)
|
|||||||
const permOptions = [{ label: 'Lesen', value: 'read' }, { label: 'Lesen+Schreiben', value: 'readwrite' }]
|
const permOptions = [{ label: 'Lesen', value: 'read' }, { label: 'Lesen+Schreiben', value: 'readwrite' }]
|
||||||
|
|
||||||
const ownCalendars = computed(() => calendars.value.filter(c => c.permission === 'owner'))
|
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)
|
const exportableCalendars = computed(() => calendars.value)
|
||||||
|
|
||||||
// --- Calendar Import / Export ---
|
// --- Calendar Import / Export ---
|
||||||
@@ -638,8 +643,8 @@ watch(showCalExportDialog, (v) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function triggerCalImport() {
|
function triggerCalImport() {
|
||||||
if (!ownCalendars.value.length) {
|
if (!writableCalendars.value.length) {
|
||||||
toast.add({ severity: 'warn', summary: 'Kein eigener Kalender', life: 3000 })
|
toast.add({ severity: 'warn', summary: 'Kein beschreibbarer Kalender', life: 3000 })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
calImportInput.value?.click()
|
calImportInput.value?.click()
|
||||||
@@ -650,7 +655,7 @@ function onCalImportFile(ev) {
|
|||||||
ev.target.value = ''
|
ev.target.value = ''
|
||||||
if (!file) return
|
if (!file) return
|
||||||
pendingImportFile.value = file
|
pendingImportFile.value = file
|
||||||
importTargetCalId.value = ownCalendars.value[0]?.id
|
importTargetCalId.value = writableCalendars.value[0]?.id
|
||||||
showCalImportDialog.value = true
|
showCalImportDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -969,6 +974,10 @@ async function createCalendar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openNewEvent(start, end, allDay = false) {
|
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
|
editingEvent.value = null
|
||||||
const now = start || new Date()
|
const now = start || new Date()
|
||||||
const later = end || new Date(now.getTime() + 3600000)
|
const later = end || new Date(now.getTime() + 3600000)
|
||||||
@@ -976,7 +985,7 @@ function openNewEvent(start, end, allDay = false) {
|
|||||||
summary: '',
|
summary: '',
|
||||||
description: '',
|
description: '',
|
||||||
location: '',
|
location: '',
|
||||||
calendar_id: ownCalendars.value[0]?.id,
|
calendar_id: writableCalendars.value[0].id,
|
||||||
dtstart: toLocalISO(now, allDay),
|
dtstart: toLocalISO(now, allDay),
|
||||||
dtend: toLocalISO(later, allDay),
|
dtend: toLocalISO(later, allDay),
|
||||||
all_day: allDay,
|
all_day: allDay,
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
<input ref="importInput" type="file" accept=".vcf,.vcard,.csv" hidden @change="onImportFile" />
|
<input ref="importInput" type="file" accept=".vcf,.vcard,.csv" hidden @change="onImportFile" />
|
||||||
<Button icon="pi pi-download" label="Export" size="small" outlined
|
<Button icon="pi pi-download" label="Export" size="small" outlined
|
||||||
:disabled="!selectedBookId" @click="showExportDialog = true" />
|
: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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -179,6 +180,11 @@
|
|||||||
</TabList>
|
</TabList>
|
||||||
<TabPanels>
|
<TabPanels>
|
||||||
<TabPanel value="general">
|
<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="photo-row">
|
||||||
<div class="avatar large" :style="{ background: avatarColor(contactForm) }">
|
<div class="avatar large" :style="{ background: avatarColor(contactForm) }">
|
||||||
<img v-if="contactForm.photo" :src="contactForm.photo" />
|
<img v-if="contactForm.photo" :src="contactForm.photo" />
|
||||||
@@ -391,6 +397,10 @@ const auth = useAuthStore()
|
|||||||
const addressBooks = ref([])
|
const addressBooks = ref([])
|
||||||
const contacts = ref([])
|
const contacts = ref([])
|
||||||
const selectedBookId = ref(null)
|
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 loading = ref(false)
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
let searchTimer = null
|
let searchTimer = null
|
||||||
@@ -615,10 +625,17 @@ function copyText(t) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openNewContact() {
|
function openNewContact() {
|
||||||
|
if (!writableBooks.value.length) {
|
||||||
|
toast.add({ severity: 'warn', summary: 'Kein beschreibbares Adressbuch vorhanden', life: 3000 })
|
||||||
|
return
|
||||||
|
}
|
||||||
editingContactId.value = null
|
editingContactId.value = null
|
||||||
Object.assign(contactForm, emptyContact())
|
Object.assign(contactForm, emptyContact())
|
||||||
categoriesString.value = ''
|
categoriesString.value = ''
|
||||||
activeTab.value = 'general'
|
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
|
showContactDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -685,7 +702,12 @@ async function saveContact() {
|
|||||||
if (editingContactId.value) {
|
if (editingContactId.value) {
|
||||||
await apiClient.put(`/contacts/${editingContactId.value}`, payload)
|
await apiClient.put(`/contacts/${editingContactId.value}`, payload)
|
||||||
} else {
|
} 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
|
showContactDialog.value = false
|
||||||
await loadBooks()
|
await loadBooks()
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<Button icon="pi pi-download" label="Export" size="small" outlined
|
<Button icon="pi pi-download" label="Export" size="small" outlined
|
||||||
:disabled="!selectedListId" @click="showExportDialog = true" />
|
:disabled="!selectedListId" @click="showExportDialog = true" />
|
||||||
<Button icon="pi pi-plus" label="Neue Aufgabe" size="small"
|
<Button icon="pi pi-plus" label="Neue Aufgabe" size="small"
|
||||||
:disabled="!selectedListId" @click="openNewTask" />
|
:disabled="!writableLists.length" @click="openNewTask" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -166,6 +166,11 @@
|
|||||||
<!-- Task Dialog -->
|
<!-- Task Dialog -->
|
||||||
<Dialog v-model:visible="showTaskDialog" :header="editingTaskId ? 'Aufgabe bearbeiten' : 'Neue Aufgabe'"
|
<Dialog v-model:visible="showTaskDialog" :header="editingTaskId ? 'Aufgabe bearbeiten' : 'Neue Aufgabe'"
|
||||||
modal :style="{ width: '560px' }">
|
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">
|
<div class="field">
|
||||||
<label>Titel</label>
|
<label>Titel</label>
|
||||||
<InputText v-model="taskForm.summary" fluid autofocus />
|
<InputText v-model="taskForm.summary" fluid autofocus />
|
||||||
@@ -247,6 +252,10 @@ const username = computed(() => auth.user?.username || '')
|
|||||||
|
|
||||||
const lists = ref([])
|
const lists = ref([])
|
||||||
const selectedListId = ref(null)
|
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 tasks = ref([])
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const hideDone = ref(false)
|
const hideDone = ref(false)
|
||||||
@@ -464,12 +473,19 @@ async function deleteList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openNewTask() {
|
function openNewTask() {
|
||||||
|
if (!writableLists.value.length) {
|
||||||
|
toast.add({ severity: 'warn', summary: 'Keine beschreibbare Liste', life: 3000 })
|
||||||
|
return
|
||||||
|
}
|
||||||
editingTaskId.value = null
|
editingTaskId.value = null
|
||||||
Object.assign(taskForm, {
|
Object.assign(taskForm, {
|
||||||
summary: '', description: '', due: '',
|
summary: '', description: '', due: '',
|
||||||
status: 'NEEDS-ACTION', priority: null, percent_complete: null,
|
status: 'NEEDS-ACTION', priority: null, percent_complete: null,
|
||||||
categories: '',
|
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
|
showTaskDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,7 +518,12 @@ async function saveTask() {
|
|||||||
if (editingTaskId.value) {
|
if (editingTaskId.value) {
|
||||||
await apiClient.put(`/tasks/${editingTaskId.value}`, payload)
|
await apiClient.put(`/tasks/${editingTaskId.value}`, payload)
|
||||||
} else {
|
} 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
|
showTaskDialog.value = false
|
||||||
await loadLists()
|
await loadLists()
|
||||||
|
|||||||
Reference in New Issue
Block a user