fix(calendar): Termine anklickbar, Ganztags-Datum korrekt, Wochenraster mit Stunden
Drei gemeldete Fehler, alle nachgestellt: 1. Ein vorhandener Termin ließ sich nicht öffnen. Der Klick auf den Termin reichte an die Tageszelle darunter durch, die sofort 'Neuer Termin' öffnete und den Bearbeiten-Dialog überschrieb. Fehlendes stopPropagation. 2. Klick auf den 3. legte den Termin am 2. an. Ein ganztägiger Termin ist ein *Datum*, kein Zeitpunkt - die Oberfläche rechnete ihn über new Date(...).toISOString() trotzdem in UTC um, was östlich von UTC den Vortag ergibt. Nachgestellt und bestätigt: DTSTART landete als 20260902 statt 20260903. Ganztägige Termine laufen jetzt über dates.ts (allDayISO/eventDay), damit Dialog und Raster dieselbe Rechnung benutzen. Nebenbefund mitgefixt: DTEND war gleich DTSTART, was RFC 5545 verbietet (DTEND ist exklusiv) - ein eintägiger Termin am 3. schreibt jetzt DTEND 20260904, nach außen bleibt die Schnittstelle beim letzten belegten Tag. 3. 'Fühlt sich starr an': Wochenansicht ist jetzt ein echtes Stundenraster (Klick trifft die Stunde statt Mitternacht, ganztägige in eigener Zeile, überlappende Termine nebeneinander, Jetzt-Linie), mehrtägige Termine stehen in jeder Zelle statt nur am Starttag, neue Termine schlagen die nächste volle Stunde bzw. 9 Uhr vor, Kalenderauswahl im Termindialog, Escape schließt, Hover-Rückmeldung, Kalenderliste auf schmalen Schirmen einklappbar. Außerdem: ein neu angelegter Kalender blieb unsichtbar, weil er in keiner Sichtbar-Liste stand - die Liste merkt sich jetzt die ausgeblendeten statt der sichtbaren. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ba33706d2e
commit
105ce8de4a
5 files changed
+702
-240
No files matched your search
@@ -23,15 +23,19 @@ import (
|
||||
// eine RRULE über dieses vereinfachte Formular zu verändern, würde sie eher
|
||||
// beschädigen als korrekt anpassen.
|
||||
type EventDTO struct {
|
||||
UID string `json:"uid"`
|
||||
Summary string `json:"summary"`
|
||||
Description string `json:"description"`
|
||||
Location string `json:"location"`
|
||||
Start time.Time `json:"start"`
|
||||
End *time.Time `json:"end,omitempty"`
|
||||
AllDay bool `json:"all_day"`
|
||||
Recurring bool `json:"recurring"`
|
||||
ETag string `json:"etag"`
|
||||
UID string `json:"uid"`
|
||||
Summary string `json:"summary"`
|
||||
Description string `json:"description"`
|
||||
Location string `json:"location"`
|
||||
// Start/End bei AllDay: Mitternacht UTC des ersten bzw. des **letzten
|
||||
// belegten** Tages – in beide Richtungen gleich. Die exklusive
|
||||
// DTEND-Rechnung des .ics-Formats (letzter Tag + 1) bleibt vollständig
|
||||
// hier im Paket; wer diese Schnittstelle nutzt, rechnet nie mit ±1 Tag.
|
||||
Start time.Time `json:"start"`
|
||||
End *time.Time `json:"end,omitempty"`
|
||||
AllDay bool `json:"all_day"`
|
||||
Recurring bool `json:"recurring"`
|
||||
ETag string `json:"etag"`
|
||||
}
|
||||
|
||||
func eventFromObject(o *Object) (*EventDTO, error) {
|
||||
@@ -63,19 +67,42 @@ func eventFromObject(o *Object) (*EventDTO, error) {
|
||||
|
||||
startProp := ev.Props.Get(ical.PropDateTimeStart)
|
||||
if startProp != nil {
|
||||
dto.AllDay = startProp.ValueType() == ical.ValueDate
|
||||
dto.AllDay = isDateOnly(startProp)
|
||||
if t, err := startProp.DateTime(time.UTC); err == nil {
|
||||
dto.Start = t
|
||||
}
|
||||
}
|
||||
if endProp := ev.Props.Get(ical.PropDateTimeEnd); endProp != nil {
|
||||
if t, err := endProp.DateTime(time.UTC); err == nil {
|
||||
// Im .ics ist DTEND eines Ganztagstermins **exklusiv**: ein
|
||||
// eintägiger Termin am 3. endet dort auf dem 4. Nach außen gibt
|
||||
// diese Schnittstelle den letzten *belegten* Tag zurück – sonst
|
||||
// müsste jede Oberfläche dieselbe Rechnung noch einmal machen
|
||||
// und eine davon würde sie vergessen.
|
||||
if dto.AllDay {
|
||||
t = t.AddDate(0, 0, -1)
|
||||
if t.Before(dto.Start) {
|
||||
t = dto.Start
|
||||
}
|
||||
}
|
||||
dto.End = &t
|
||||
}
|
||||
}
|
||||
if dto.AllDay && dto.End == nil {
|
||||
// Ohne DTEND belegt ein Ganztagstermin genau seinen Starttag.
|
||||
end := dto.Start
|
||||
dto.End = &end
|
||||
}
|
||||
return dto, nil
|
||||
}
|
||||
|
||||
// isDateOnly erkennt einen Ganztagstermin. Nicht jeder Client schreibt
|
||||
// VALUE=DATE dazu, die achtstellige Form ("20260903") ist aber eindeutig –
|
||||
// eine Zeitangabe hat immer mindestens 15 Zeichen ("20260903T090000").
|
||||
func isDateOnly(prop *ical.Prop) bool {
|
||||
return prop.ValueType() == ical.ValueDate || len(prop.Value) == 8
|
||||
}
|
||||
|
||||
var errNoEventComponent = &icalError{"calendar: object has no VEVENT/VTODO component"}
|
||||
|
||||
type icalError struct{ msg string }
|
||||
@@ -137,10 +164,26 @@ func buildEventICal(uid string, body eventBody) *ical.Calendar {
|
||||
ev.Props.SetText(ical.PropLocation, body.Location)
|
||||
}
|
||||
if body.AllDay {
|
||||
ev.Props.SetDate(ical.PropDateTimeStart, body.Start)
|
||||
// Ganztägig heißt: ein Datum ohne Uhrzeit und ohne Zeitzone. Deshalb
|
||||
// hier ausdrücklich .UTC() – der Aufrufer schickt für einen
|
||||
// Ganztagstermin Mitternacht UTC, und ohne die Umrechnung würde eine
|
||||
// Zeitzone östlich von UTC den Tag um eins nach hinten schieben (der
|
||||
// 3. wurde zum 2.).
|
||||
start := body.Start.UTC()
|
||||
ev.Props.SetDate(ical.PropDateTimeStart, start)
|
||||
// DTEND ist im .ics **exklusiv**: der Tag *nach* dem letzten
|
||||
// belegten. Die Schnittstelle nimmt den letzten belegten Tag entgegen
|
||||
// (siehe eventFromObject), also hier +1. Ohne das verletzt ein
|
||||
// eintägiger Termin RFC 5545 (DTEND muss größer als DTSTART sein) und
|
||||
// Clients zeigen ihn unterschiedlich an.
|
||||
last := start
|
||||
if body.End != nil {
|
||||
ev.Props.SetDate(ical.PropDateTimeEnd, *body.End)
|
||||
last = body.End.UTC()
|
||||
if last.Before(start) {
|
||||
last = start
|
||||
}
|
||||
}
|
||||
ev.Props.SetDate(ical.PropDateTimeEnd, last.AddDate(0, 0, 1))
|
||||
} else {
|
||||
ev.Props.SetDateTime(ical.PropDateTimeStart, body.Start.UTC())
|
||||
if body.End != nil {
|
||||
|
||||
Reference in new issue
Block a user