package main import ( "bytes" "html/template" "log" "sort" "time" ) // calendarsURL is the page URL of calendarsDir ("/Calendars/"). var calendarsURL string // maintenanceHorizon is how many days after today the header widget lists. const maintenanceHorizon = 7 type maintItem struct { When string // "03:00" today; "Sa 27.09." or "Sa 27.09. 03:00" later Summary string } type maintWidgetData struct { Count int // occurrences today (the badge) Today []maintItem Upcoming []maintItem PageURL string } var maintWidgetTmpl = template.Must(template.New("widget.html"). Funcs(template.FuncMap{"icon": readIcon}). ParseFS(assets, "assets/calendar/widget.html")) // maintenanceWidget renders the header chip and its dropdown. It is a // template func so layout.html can call it without every page's data struct // carrying it. Empty when no maintenance calendar is configured. func maintenanceWidget() template.HTML { if calendarsDir == "" { return "" } names := readCalendarSettings(calendarsDir).Maintenance if len(names) == 0 { return "" } now := time.Now() today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local) tomorrow := today.AddDate(0, 0, 1) data := maintWidgetData{PageURL: calendarsURL} for _, ev := range calendarOccurrences(calendarsDir, names, today, today.AddDate(0, 0, maintenanceHorizon+1)) { // Anything that started before tomorrow is due today, including // multi-day events still running. if ev.Start.Before(tomorrow) { item := maintItem{Summary: ev.Summary} if !ev.AllDay && !ev.Start.Before(today) { item.When = ev.Start.Format("15:04") } data.Today = append(data.Today, item) continue } when := calDayLabel(ev.Start) if !ev.AllDay { when += " " + ev.Start.Format("15:04") } data.Upcoming = append(data.Upcoming, maintItem{When: when, Summary: ev.Summary}) } data.Count = len(data.Today) var buf bytes.Buffer if err := maintWidgetTmpl.Execute(&buf, data); err != nil { log.Printf("maintenance widget template: %v", err) return "" } return template.HTML(buf.String()) } // maintSeriesRow is one task in the calendars page's maintenance table. type maintSeriesRow struct { Summary string Rule string Last string // most recent occurrence before today Next string // first occurrence today or later next time.Time } // maintenanceWindow bounds the table's occurrence search, in days each way // from today. A year covers yearly tasks. const maintenanceWindow = 366 // maintenanceSeries lists every task of the named calendars in dir: one row // per recurring series with its last and next date, plus upcoming single // events. Sorted by next date; series without a next date come last. func maintenanceSeries(dir string, names []string, now time.Time) []maintSeriesRow { today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local) evs := calendarOccurrences(dir, names, today.AddDate(0, 0, -maintenanceWindow), today.AddDate(0, 0, maintenanceWindow)) type key struct{ calendar, uid string } rows := map[key]*maintSeriesRow{} var order []*maintSeriesRow for _, ev := range evs { if ev.Series == nil { if ev.Start.Before(today) { continue } order = append(order, &maintSeriesRow{ Summary: ev.Summary, Rule: "once", Next: calDayLabel(ev.Start), next: ev.Start, }) continue } k := key{ev.Calendar, ev.Series.UID} row := rows[k] if row == nil { row = &maintSeriesRow{Summary: ev.Series.Summary, Rule: ev.Series.Rule} rows[k] = row order = append(order, row) } // Occurrences arrive sorted by start: keep overwriting Last, take // the first one from today on as Next. if ev.Start.Before(today) { row.Last = calDayLabel(ev.Start) } else if row.next.IsZero() { row.next = ev.Start row.Next = calDayLabel(ev.Start) } } out := make([]maintSeriesRow, 0, len(order)) for _, r := range order { out = append(out, *r) } sort.SliceStable(out, func(i, j int) bool { a, b := out[i].next, out[j].next if a.IsZero() != b.IsZero() { return b.IsZero() } if !a.Equal(b) { return a.Before(b) } return out[i].Summary < out[j].Summary }) return out }