feat(calendar): Wiederholungen, Tagesansicht, Terminfarben, Farbpunkt als Schalter

Fünf Wünsche aus dem Durchtesten:

1. Wiederholungen einstellbar (täglich/wöchentlich/monatlich/jährlich,
   alle N, Ende nie/nach Anzahl/an einem Datum). Der eigentliche Aufwand
   lag nicht im Formular, sondern im Aufklappen: eine wöchentliche Reihe
   erschien sonst als *ein* Eintrag. Der Server klappt sie jetzt im
   angefragten Zeitfenster auf (?from=&to=, rrule-go über go-icals
   RecurrenceSet), die Ansichten fragen ihren sichtbaren Ausschnitt an.
   Geprüft: 5 Termine wöchentlich, nach Umstellung auf 14-tägig korrekt
   neu gerechnet, Fenster einer Woche liefert genau einen.

   Regeln, die das Formular nicht abbildet (etwa jeder zweite Dienstag
   im Monat), werden weiterhin korrekt *angezeigt* -- geprüft, die vier
   Termine stimmen -- aber nicht zum Bearbeiten angeboten (409). Ein
   halb verstandenes Speichern würde die Regel sonst still beschneiden.

2. Tag wird erst geöffnet, statt sofort einen Termin anzulegen: Klick
   auf einen Tag zeigt dessen Termine als Liste, dort wählt man einen
   aus oder legt bewusst einen neuen an.

3. Eigene Farbe je Termin (COLOR nach RFC 7986), leer = Kalenderfarbe.

4. Kalenderliste ohne Kästchen: der Farbpunkt selbst ist der Schalter --
   gefüllt heißt sichtbar, leerer Ring ausgeblendet.

5. Ganztägig als Schiebeschalter statt Kästchen -- es stellt das
   Formular um, das ist keine Nebenangabe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-09-04 00:56:28 +02:00
1 parent 105ce8de4a
commit 79a7cc6a3d
8 files changed
+795 -218

No files matched your search

@@ -0,0 +1,119 @@
import { useEffect } from 'react'
import { useOverlayDismiss } from '../../hooks/useOverlayDismiss'
import type { CalendarEvent } from '../../api/client'
const weekdayLong = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag']
const monthLabels = [
'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember',
]
export interface DayEvent extends CalendarEvent {
calendarId: string
/** Endgültige Farbe: eigene Farbe des Termins, sonst die des Kalenders. */
color: string
calendarName: string
startsAt: Date
endsAt: Date | null
}
/**
* Ein angeklickter Tag wird erst **geöffnet**, statt sofort einen neuen
* Termin anzulegen.
*
* Vorher legte jeder Klick auf eine Tageszelle direkt einen neuen Termin an –
* auch der Klick, mit dem man eigentlich nachsehen wollte, was an dem Tag
* ansteht. Hier steht zuerst, was da ist; einen neuen Termin gibt es auf
* Ansage.
*/
export default function DayDialog({ day, events, onPick, onCreate, onClose }: {
day: Date
events: DayEvent[]
onPick: (ev: DayEvent) => void
onCreate: () => void
onClose: () => void
}) {
const overlay = useOverlayDismiss(onClose)
useEffect(() => {
function onKey(e: KeyboardEvent) { if (e.key === 'Escape') onClose() }
window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey)
}, [onClose])
const timeLabel = (ev: DayEvent) => {
if (ev.all_day) return 'ganztägig'
const from = ev.startsAt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
if (!ev.endsAt) return from
const to = ev.endsAt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
return `${from} – ${to}`
}
return (
<div style={{
position: 'fixed', inset: 0, zIndex: 190, display: 'flex',
alignItems: 'center', justifyContent: 'center', background: 'rgba(0,0,0,0.6)', padding: 16,
}} {...overlay}>
<div style={{
background: 'var(--color-surface)', border: '1px solid var(--color-border)',
borderRadius: 14, width: 420, maxWidth: '100%', maxHeight: '80vh',
display: 'flex', flexDirection: 'column', overflow: 'hidden',
}}>
<div style={{ padding: '18px 22px 12px', borderBottom: '1px solid var(--color-border)' }}>
<div style={{ fontSize: 12, color: 'var(--color-text-muted)' }}>{weekdayLong[day.getDay()]}</div>
<div style={{ fontSize: 17, fontWeight: 600, color: 'var(--color-text)' }}>
{day.getDate()}. {monthLabels[day.getMonth()]} {day.getFullYear()}
</div>
</div>
<div style={{ padding: 12, overflowY: 'auto', flex: 1 }}>
{events.length === 0 && (
<p style={{ margin: '12px 10px', fontSize: 13, color: 'var(--color-text-muted)' }}>
Nichts an diesem Tag.
</p>
)}
{events.map(ev => (
<button
key={ev.calendarId + ev.uid + ev.start}
onClick={() => onPick(ev)}
style={{
display: 'flex', alignItems: 'center', gap: 10, width: '100%',
padding: '9px 10px', marginBottom: 4, borderRadius: 8, textAlign: 'left',
background: 'transparent', border: '1px solid var(--color-border)', cursor: 'pointer',
}}
className="aether-cal-cell"
>
<span style={{ width: 4, alignSelf: 'stretch', borderRadius: 2, background: ev.color, flexShrink: 0 }} />
<span style={{ flex: 1, minWidth: 0 }}>
<span style={{ display: 'block', fontSize: 13.5, color: 'var(--color-text)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{ev.summary || '(ohne Titel)'}
{ev.recurring && <span style={{ marginLeft: 6, fontSize: 11, color: 'var(--color-text-muted)' }}>↻</span>}
</span>
<span style={{ display: 'block', fontSize: 11.5, color: 'var(--color-text-muted)' }}>
{timeLabel(ev)}
{ev.location ? ` · ${ev.location}` : ''}
{` · ${ev.calendarName}`}
</span>
</span>
</button>
))}
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 8, padding: '12px 16px', borderTop: '1px solid var(--color-border)' }}>
<button onClick={onCreate} style={{
padding: '8px 14px', borderRadius: 8, border: 'none',
background: 'var(--color-primary)', color: '#fff', fontSize: 13, fontWeight: 600, cursor: 'pointer',
}}>
+ Termin an diesem Tag
</button>
<button onClick={onClose} style={{
padding: '8px 16px', borderRadius: 8, border: '1px solid var(--color-border)',
background: 'transparent', color: 'var(--color-text-muted)', fontSize: 13, cursor: 'pointer',
}}>
Schließen
</button>
</div>
</div>
</div>
)
}