mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-18 18:01:23 +02:00
fix(calendar): integrate recurrence dialog and external calendars into UI
- Wire RecurrenceEditDialog into EventDetailModal and QuickEventOverlay so deleting recurring events shows "this/all/future" options - Add external calendars section to CalendarSidebar with visibility toggle and sync error indicator - Update COMPLEXITY_AUDIT.md to mark sync and recurrence as implemented Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
eb859c18bc
commit
5c9e16f634
4 changed files with 142 additions and 6 deletions
|
|
@ -1,14 +1,27 @@
|
|||
<script lang="ts">
|
||||
import { calendarsStore } from '$lib/stores/calendars.svelte';
|
||||
import { externalCalendarsStore } from '$lib/stores/external-calendars.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
function handleToggle(calendarId: string) {
|
||||
calendarsStore.toggleVisibility(calendarId);
|
||||
}
|
||||
|
||||
function handleExternalToggle(id: string, currentVisible: boolean) {
|
||||
externalCalendarsStore.update(id, { isVisible: !currentVisible });
|
||||
}
|
||||
|
||||
function handleAddCalendar() {
|
||||
goto('/settings');
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (authStore.isAuthenticated && externalCalendarsStore.calendars.length === 0) {
|
||||
externalCalendarsStore.fetchCalendars();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="calendar-sidebar-section">
|
||||
|
|
@ -41,6 +54,41 @@
|
|||
<p class="empty-message">Keine Kalender vorhanden</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if externalCalendarsStore.calendars.length > 0}
|
||||
<div class="section-header external-header">
|
||||
<h3 class="section-title">Externe Kalender</h3>
|
||||
<button class="add-btn" onclick={() => goto('/settings/sync')} aria-label="Sync verwalten">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="calendar-list" role="group" aria-label="Externe Kalender Sichtbarkeit">
|
||||
{#each externalCalendarsStore.calendars as cal}
|
||||
<label class="calendar-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={cal.isVisible}
|
||||
onchange={() => handleExternalToggle(cal.id, cal.isVisible)}
|
||||
style="accent-color: {cal.color}"
|
||||
aria-label="{cal.name} {cal.isVisible ? 'sichtbar' : 'ausgeblendet'}"
|
||||
/>
|
||||
<span class="color-dot" style="background-color: {cal.color}" aria-hidden="true"></span>
|
||||
<span class="calendar-name">{cal.name}</span>
|
||||
{#if cal.lastSyncError}
|
||||
<span class="sync-error-dot" title="Sync-Fehler"></span>
|
||||
{/if}
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
@ -121,4 +169,19 @@
|
|||
text-align: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.external-header {
|
||||
margin-top: 1rem;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid hsl(var(--color-border));
|
||||
}
|
||||
|
||||
.sync-error-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: hsl(0 84% 60%);
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@
|
|||
import { eventsStore } from '$lib/stores/events.svelte';
|
||||
import { calendarsStore } from '$lib/stores/calendars.svelte';
|
||||
import EventForm from './EventForm.svelte';
|
||||
import RecurrenceEditDialog from './RecurrenceEditDialog.svelte';
|
||||
import { TagBadge, toastStore as toast } from '@manacore/shared-ui';
|
||||
import type { CalendarEvent, UpdateEventInput } from '@calendar/shared';
|
||||
import { describeRecurrence, parseRRule } from '@calendar/shared';
|
||||
import * as api from '$lib/api/events';
|
||||
import { format } from 'date-fns';
|
||||
import { de } from 'date-fns/locale';
|
||||
|
|
@ -21,6 +23,8 @@
|
|||
let event = $state<CalendarEvent | null>(null);
|
||||
let loading = $state(true);
|
||||
let isEditing = $state(false);
|
||||
let showRecurrenceDialog = $state(false);
|
||||
let recurrenceDialogMode = $state<'edit' | 'delete'>('delete');
|
||||
|
||||
// Load event data
|
||||
$effect(() => {
|
||||
|
|
@ -60,6 +64,13 @@
|
|||
async function handleDelete() {
|
||||
if (!event) return;
|
||||
|
||||
// For recurring events, show the recurrence dialog
|
||||
if (event.recurrenceRule || eventsStore.isRecurrenceOccurrence(event.id)) {
|
||||
recurrenceDialogMode = 'delete';
|
||||
showRecurrenceDialog = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm('Möchten Sie diesen Termin wirklich löschen?')) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -75,6 +86,21 @@
|
|||
onClose();
|
||||
}
|
||||
|
||||
async function handleRecurrenceAction(action: 'this' | 'all' | 'this_and_future') {
|
||||
if (!event) return;
|
||||
showRecurrenceDialog = false;
|
||||
|
||||
if (recurrenceDialogMode === 'delete') {
|
||||
let result;
|
||||
if (action === 'this') {
|
||||
result = await eventsStore.deleteRecurrenceOccurrence(event.id);
|
||||
} else {
|
||||
result = await eventsStore.deleteRecurrenceSeries(event.id);
|
||||
}
|
||||
if (!result.error) onClose();
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
if (isEditing) {
|
||||
isEditing = false;
|
||||
|
|
@ -145,6 +171,13 @@
|
|||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<RecurrenceEditDialog
|
||||
visible={showRecurrenceDialog}
|
||||
mode={recurrenceDialogMode}
|
||||
onSelect={handleRecurrenceAction}
|
||||
onCancel={() => (showRecurrenceDialog = false)}
|
||||
/>
|
||||
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
||||
<div class="modal-backdrop" onclick={handleBackdropClick} role="presentation">
|
||||
<div class="modal-container" role="dialog" aria-modal="true" aria-labelledby="modal-title">
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { eventsStore } from '$lib/stores/events.svelte';
|
||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||
import { contactsStore } from '$lib/stores/contacts.svelte';
|
||||
import RecurrenceEditDialog from './RecurrenceEditDialog.svelte';
|
||||
import type {
|
||||
LocationDetails,
|
||||
CalendarEvent,
|
||||
|
|
@ -207,6 +208,7 @@
|
|||
let locationCity = $state('');
|
||||
let locationCountry = $state('');
|
||||
let submitting = $state(false);
|
||||
let showRecurrenceDialog = $state(false);
|
||||
|
||||
// People state
|
||||
let responsiblePerson = $state<ResponsiblePerson | null>(null);
|
||||
|
|
@ -605,6 +607,12 @@
|
|||
async function handleDelete() {
|
||||
if (!event) return;
|
||||
|
||||
// For recurring events, show recurrence dialog
|
||||
if (event.recurrenceRule || eventsStore.isRecurrenceOccurrence(event.id)) {
|
||||
showRecurrenceDialog = true;
|
||||
return;
|
||||
}
|
||||
|
||||
submitting = true;
|
||||
try {
|
||||
const result = await eventsStore.deleteEvent(event.id);
|
||||
|
|
@ -623,6 +631,26 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function handleRecurrenceDeleteAction(action: 'this' | 'all' | 'this_and_future') {
|
||||
if (!event) return;
|
||||
showRecurrenceDialog = false;
|
||||
submitting = true;
|
||||
try {
|
||||
let result;
|
||||
if (action === 'this') {
|
||||
result = await eventsStore.deleteRecurrenceOccurrence(event.id);
|
||||
} else {
|
||||
result = await eventsStore.deleteRecurrenceSeries(event.id);
|
||||
}
|
||||
if (!result.error) {
|
||||
onDeleted?.();
|
||||
onClose();
|
||||
}
|
||||
} finally {
|
||||
submitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') {
|
||||
onClose();
|
||||
|
|
@ -632,6 +660,13 @@
|
|||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<RecurrenceEditDialog
|
||||
visible={showRecurrenceDialog}
|
||||
mode="delete"
|
||||
onSelect={handleRecurrenceDeleteAction}
|
||||
onCancel={() => (showRecurrenceDialog = false)}
|
||||
/>
|
||||
|
||||
<!-- Overlay (no blocking backdrop - allows interaction with calendar) -->
|
||||
<!-- Portal to body to escape stacking contexts -->
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -80,15 +80,20 @@ Die Legacy-Composables (`useDragDrop`, `useResize`) sind von keiner Komponente i
|
|||
|
||||
## Teil 2: Unterentwickelte Bereiche
|
||||
|
||||
### 1. Keine Kalender-Synchronisation (CalDAV/iCal) — Priorität: Hoch
|
||||
### 1. Keine Kalender-Synchronisation (CalDAV/iCal) — ✅ Implementiert
|
||||
|
||||
Backend hat `external_calendars`-Tabelle und Sync-Endpunkte. Frontend hat null UI dafür.
|
||||
Für eine Kalender-App ist das das größte fehlende Feature.
|
||||
- API-Client (`lib/api/sync.ts`) für alle Sync-Endpunkte
|
||||
- External Calendars Store mit connect/disconnect/sync
|
||||
- Settings-Seite `/settings/sync` mit Provider-Auswahl, CalDAV-Discovery, Sync-Status
|
||||
- Sidebar zeigt externe Kalender mit Sichtbarkeits-Toggle
|
||||
|
||||
### 2. Keine wiederkehrenden Termine (Recurring Events) — Priorität: Hoch
|
||||
### 2. Keine wiederkehrenden Termine (Recurring Events) — ✅ Implementiert
|
||||
|
||||
Backend-Schema unterstützt RFC 5545 RRULE. Kein UI oder Store-Logik dafür.
|
||||
Essentiell für eine nutzbare Kalender-App.
|
||||
- RecurrenceSelector-Komponente mit Presets + benutzerdefinierter Konfiguration
|
||||
- Im EventForm integriert, sendet `recurrenceRule` + `recurrenceEndDate`
|
||||
- Events Store expandiert RRULE zu Einzelterminen per `generateOccurrences()`
|
||||
- RecurrenceEditDialog für "Diesen/Alle/Zukünftige" beim Löschen
|
||||
- In EventDetailModal und QuickEventOverlay integriert
|
||||
|
||||
### 3. Erinnerungen / Notifications nur rudimentär — Priorität: Mittel
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue