2026-05-16 05:26:49 +00:00
|
|
|
|
package templates
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
style(planning): implement Month/Week/Day calendar views from sketch 005
- Replace 14-day agenda list with three-view calendar (month/week/day)
- Add PlanningCalendar, CalendarDay, CalendarTimeEvent, MiniCalDay data structs
- Add BuildMonthCalendar, BuildWeekCalendar, BuildDayCalendar builder helpers
- Add MondayOf, PlanningMonthURL, PlanningWeekURL, PlanningDayURL URL helpers
- Handler now parses ?view=month|week|day with matching date params
- Month view: 7-col grid with event chips using tablo color via color-mix()
- Week/Day view: split layout with mini-month panel and hour timeline (07-20)
- Timeline events positioned via TopPx/HeightPx from start_time microseconds
- Append all calendar CSS to app.css (view-toggle, cal-grid, tl-* classes)
2026-05-18 14:56:44 +00:00
|
|
|
|
"fmt"
|
2026-05-16 05:26:49 +00:00
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"backend/internal/db/sqlc"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
type PlanningAgenda struct {
|
|
|
|
|
|
Start time.Time
|
|
|
|
|
|
End time.Time
|
|
|
|
|
|
Today time.Time
|
|
|
|
|
|
RangeLabel string
|
|
|
|
|
|
PrevURL string
|
|
|
|
|
|
TodayURL string
|
|
|
|
|
|
NextURL string
|
|
|
|
|
|
ShowingToday bool
|
|
|
|
|
|
Events []PlanningEventRow
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type PlanningEventRow struct {
|
|
|
|
|
|
Title string
|
2026-05-16 06:39:10 +00:00
|
|
|
|
DateLabel string
|
2026-05-16 05:26:49 +00:00
|
|
|
|
TimeRange string
|
|
|
|
|
|
TabloTitle string
|
|
|
|
|
|
TabloColor string
|
|
|
|
|
|
HasColor bool
|
|
|
|
|
|
Location string
|
|
|
|
|
|
HasLocation bool
|
|
|
|
|
|
URL string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func NewPlanningAgenda(start time.Time, end time.Time, today time.Time, rows []sqlc.ListUserEventsRangeRow) PlanningAgenda {
|
|
|
|
|
|
events := make([]PlanningEventRow, 0, len(rows))
|
|
|
|
|
|
for _, row := range rows {
|
|
|
|
|
|
location := ""
|
|
|
|
|
|
if row.Location.Valid {
|
|
|
|
|
|
location = row.Location.String
|
|
|
|
|
|
}
|
|
|
|
|
|
color := ""
|
|
|
|
|
|
if row.TabloColor.Valid {
|
|
|
|
|
|
color = row.TabloColor.String
|
|
|
|
|
|
}
|
|
|
|
|
|
events = append(events, PlanningEventRow{
|
|
|
|
|
|
Title: row.Title,
|
2026-05-16 06:39:10 +00:00
|
|
|
|
DateLabel: PlanningEventDateLabel(row),
|
2026-05-16 05:26:49 +00:00
|
|
|
|
TimeRange: PlanningEventTimeRange(row),
|
|
|
|
|
|
TabloTitle: row.TabloTitle,
|
|
|
|
|
|
TabloColor: color,
|
|
|
|
|
|
HasColor: color != "",
|
|
|
|
|
|
Location: location,
|
|
|
|
|
|
HasLocation: location != "",
|
|
|
|
|
|
URL: PlanningEventURL(row),
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
return PlanningAgenda{
|
|
|
|
|
|
Start: start,
|
|
|
|
|
|
End: end,
|
|
|
|
|
|
Today: today,
|
|
|
|
|
|
RangeLabel: PlanningRangeLabel(start, end),
|
|
|
|
|
|
PrevURL: PlanningURL(start.AddDate(0, 0, -14)),
|
|
|
|
|
|
TodayURL: "/planning",
|
|
|
|
|
|
NextURL: PlanningURL(start.AddDate(0, 0, 14)),
|
|
|
|
|
|
ShowingToday: samePlanningDay(start, today),
|
|
|
|
|
|
Events: events,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func PlanningURL(start time.Time) string {
|
|
|
|
|
|
return "/planning?start=" + start.Format("2006-01-02")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func PlanningEventURL(row sqlc.ListUserEventsRangeRow) string {
|
|
|
|
|
|
return "/tablos/" + row.TabloID.String() + "/events?month=" + row.EventDate.Time.Format("2006-01")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-16 06:39:10 +00:00
|
|
|
|
func PlanningEventDateLabel(row sqlc.ListUserEventsRangeRow) string {
|
|
|
|
|
|
if !row.EventDate.Valid {
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
return row.EventDate.Time.Format("Jan 2, 2006")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-16 05:26:49 +00:00
|
|
|
|
func PlanningEventTimeRange(row sqlc.ListUserEventsRangeRow) string {
|
|
|
|
|
|
start := FormatEventTime(row.StartTime)
|
|
|
|
|
|
if start == "" {
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
if !row.EndTime.Valid {
|
|
|
|
|
|
return start
|
|
|
|
|
|
}
|
|
|
|
|
|
return start + "-" + FormatEventTime(row.EndTime)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func PlanningRangeLabel(start time.Time, end time.Time) string {
|
|
|
|
|
|
if start.Year() == end.Year() {
|
|
|
|
|
|
return start.Format("January 2") + " - " + end.Format("January 2, 2006")
|
|
|
|
|
|
}
|
|
|
|
|
|
return start.Format("January 2, 2006") + " - " + end.Format("January 2, 2006")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func samePlanningDay(a time.Time, b time.Time) bool {
|
|
|
|
|
|
return a.Year() == b.Year() && a.Month() == b.Month() && a.Day() == b.Day()
|
|
|
|
|
|
}
|
2026-05-17 11:03:30 +00:00
|
|
|
|
|
|
|
|
|
|
// PlanningShowDaySeparator returns true when the event at index i belongs to a
|
|
|
|
|
|
// different day than the previous event — i.e. a date-group header should be
|
|
|
|
|
|
// rendered before it.
|
|
|
|
|
|
func PlanningShowDaySeparator(events []PlanningEventRow, index int) bool {
|
|
|
|
|
|
if index == 0 {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
return events[index].DateLabel != events[index-1].DateLabel
|
|
|
|
|
|
}
|
style(planning): implement Month/Week/Day calendar views from sketch 005
- Replace 14-day agenda list with three-view calendar (month/week/day)
- Add PlanningCalendar, CalendarDay, CalendarTimeEvent, MiniCalDay data structs
- Add BuildMonthCalendar, BuildWeekCalendar, BuildDayCalendar builder helpers
- Add MondayOf, PlanningMonthURL, PlanningWeekURL, PlanningDayURL URL helpers
- Handler now parses ?view=month|week|day with matching date params
- Month view: 7-col grid with event chips using tablo color via color-mix()
- Week/Day view: split layout with mini-month panel and hour timeline (07-20)
- Timeline events positioned via TopPx/HeightPx from start_time microseconds
- Append all calendar CSS to app.css (view-toggle, cal-grid, tl-* classes)
2026-05-18 14:56:44 +00:00
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Calendar view data structures
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
// CalendarEvent is used in the month grid cells.
|
|
|
|
|
|
type CalendarEvent struct {
|
|
|
|
|
|
Title string
|
|
|
|
|
|
ColorClass string
|
|
|
|
|
|
Style string // inline style for background/color when tablo has a color
|
|
|
|
|
|
URL string
|
|
|
|
|
|
TimeLabel string // "10:00" or "" if all-day
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// CalendarDay is one cell in the month grid.
|
|
|
|
|
|
type CalendarDay struct {
|
|
|
|
|
|
Date time.Time
|
|
|
|
|
|
DayNum int
|
|
|
|
|
|
IsToday bool
|
|
|
|
|
|
OtherMonth bool
|
|
|
|
|
|
Events []CalendarEvent
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// CalendarTimeEvent is an event placed on the hour-timeline (week/day views).
|
|
|
|
|
|
type CalendarTimeEvent struct {
|
|
|
|
|
|
Title string
|
|
|
|
|
|
ColorClass string
|
|
|
|
|
|
Style string
|
|
|
|
|
|
URL string
|
|
|
|
|
|
TopPx int
|
|
|
|
|
|
HeightPx int
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// CalendarDayColumn is one day column in the week/day timeline.
|
|
|
|
|
|
type CalendarDayColumn struct {
|
|
|
|
|
|
Date time.Time
|
|
|
|
|
|
Label string // "Mon 18" (week) or "Monday 18 May" (day)
|
|
|
|
|
|
IsToday bool
|
|
|
|
|
|
Events []CalendarTimeEvent
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// MiniCalDay is one cell in the mini-month panel used by week/day views.
|
|
|
|
|
|
type MiniCalDay struct {
|
|
|
|
|
|
Date time.Time
|
|
|
|
|
|
DayNum int
|
|
|
|
|
|
IsToday bool
|
|
|
|
|
|
InWeek bool // highlighted because it is in the viewed week
|
|
|
|
|
|
URL string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// PlanningCalendar holds all data needed to render any of the three views.
|
|
|
|
|
|
type PlanningCalendar struct {
|
|
|
|
|
|
View string // "month", "week", "day"
|
|
|
|
|
|
Label string // period label shown in header
|
|
|
|
|
|
PrevURL string
|
|
|
|
|
|
TodayURL string
|
|
|
|
|
|
NextURL string
|
|
|
|
|
|
// Month view
|
|
|
|
|
|
Weeks [][]CalendarDay
|
|
|
|
|
|
// Week / Day views
|
|
|
|
|
|
Days []CalendarDayColumn
|
|
|
|
|
|
HourSlots []string // ["07:00", "08:00", ... "20:00"]
|
|
|
|
|
|
// Mini-month panel (week/day views)
|
|
|
|
|
|
MiniMonth [][]MiniCalDay // 5-6 rows × 7
|
|
|
|
|
|
MiniMonthLabel string // "May 2026"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// URL helpers
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
func PlanningMonthURL(t time.Time) string {
|
|
|
|
|
|
return "/planning?view=month&month=" + t.Format("2006-01")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func PlanningWeekURL(t time.Time) string {
|
|
|
|
|
|
monday := MondayOf(t)
|
|
|
|
|
|
return "/planning?view=week&start=" + monday.Format("2006-01-02")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func PlanningDayURL(t time.Time) string {
|
|
|
|
|
|
return "/planning?view=day&date=" + t.Format("2006-01-02")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// MondayOf returns the Monday of the week containing t (ISO week, Mon=first day).
|
|
|
|
|
|
func MondayOf(t time.Time) time.Time {
|
|
|
|
|
|
wd := int(t.Weekday()) // Sunday=0
|
|
|
|
|
|
if wd == 0 {
|
|
|
|
|
|
wd = 7
|
|
|
|
|
|
}
|
|
|
|
|
|
return time.Date(t.Year(), t.Month(), t.Day()-wd+1, 0, 0, 0, 0, t.Location())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Color helper
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
// calColorStyle returns an inline style string that tints the event chip using
|
|
|
|
|
|
// the tablo's hex color. When color is empty the ev-default class is used instead.
|
|
|
|
|
|
func calColorStyle(color string) string {
|
|
|
|
|
|
if color == "" {
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
return fmt.Sprintf(
|
|
|
|
|
|
"background:color-mix(in srgb,%s 15%%,transparent);color:%s",
|
|
|
|
|
|
color, color,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Calendar builder helpers
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
// calEventFromRow converts a raw DB row into a CalendarEvent (month view).
|
|
|
|
|
|
func calEventFromRow(row sqlc.ListUserEventsRangeRow) CalendarEvent {
|
|
|
|
|
|
color := ""
|
|
|
|
|
|
if row.TabloColor.Valid {
|
|
|
|
|
|
color = row.TabloColor.String
|
|
|
|
|
|
}
|
|
|
|
|
|
colorClass := "ev-default"
|
|
|
|
|
|
style := calColorStyle(color)
|
|
|
|
|
|
if style != "" {
|
|
|
|
|
|
colorClass = ""
|
|
|
|
|
|
}
|
|
|
|
|
|
timeLabel := FormatEventTime(row.StartTime)
|
|
|
|
|
|
return CalendarEvent{
|
|
|
|
|
|
Title: row.Title,
|
|
|
|
|
|
ColorClass: colorClass,
|
|
|
|
|
|
Style: style,
|
|
|
|
|
|
URL: PlanningEventURL(row),
|
|
|
|
|
|
TimeLabel: timeLabel,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// calTimeEventFromRow converts a DB row into a CalendarTimeEvent (week/day view).
|
|
|
|
|
|
func calTimeEventFromRow(row sqlc.ListUserEventsRangeRow) CalendarTimeEvent {
|
|
|
|
|
|
color := ""
|
|
|
|
|
|
if row.TabloColor.Valid {
|
|
|
|
|
|
color = row.TabloColor.String
|
|
|
|
|
|
}
|
|
|
|
|
|
colorClass := "ev-default"
|
|
|
|
|
|
style := calColorStyle(color)
|
|
|
|
|
|
if style != "" {
|
|
|
|
|
|
colorClass = ""
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Compute TopPx and HeightPx from start/end times.
|
|
|
|
|
|
// Timeline shows 07:00–20:00 (hour offset 0 = 07:00). 48px per hour.
|
|
|
|
|
|
topPx := 48 // default: 08:00
|
|
|
|
|
|
heightPx := 24 // default: 30 min
|
|
|
|
|
|
if row.StartTime.Valid {
|
|
|
|
|
|
totalMicros := row.StartTime.Microseconds
|
|
|
|
|
|
hours := int(totalMicros / 3_600_000_000)
|
|
|
|
|
|
mins := int((totalMicros % 3_600_000_000) / 60_000_000)
|
|
|
|
|
|
topPx = (hours-7)*48 + mins*48/60
|
|
|
|
|
|
if topPx < 0 {
|
|
|
|
|
|
topPx = 0
|
|
|
|
|
|
}
|
|
|
|
|
|
heightPx = 24 // default 30 min if no end time
|
|
|
|
|
|
if row.EndTime.Valid {
|
|
|
|
|
|
endMicros := row.EndTime.Microseconds
|
|
|
|
|
|
endHours := int(endMicros / 3_600_000_000)
|
|
|
|
|
|
endMins := int((endMicros % 3_600_000_000) / 60_000_000)
|
|
|
|
|
|
durationMins := (endHours*60 + endMins) - (hours*60 + mins)
|
|
|
|
|
|
if durationMins < 0 {
|
|
|
|
|
|
durationMins = 30
|
|
|
|
|
|
}
|
|
|
|
|
|
heightPx = durationMins * 48 / 60
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if heightPx < 20 {
|
|
|
|
|
|
heightPx = 20
|
|
|
|
|
|
}
|
|
|
|
|
|
return CalendarTimeEvent{
|
|
|
|
|
|
Title: row.Title,
|
|
|
|
|
|
ColorClass: colorClass,
|
|
|
|
|
|
Style: style,
|
|
|
|
|
|
URL: PlanningEventURL(row),
|
|
|
|
|
|
TopPx: topPx,
|
|
|
|
|
|
HeightPx: heightPx,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// BuildMonthCalendar constructs a PlanningCalendar for the month view.
|
|
|
|
|
|
func BuildMonthCalendar(month time.Time, today time.Time, rows []sqlc.ListUserEventsRangeRow) PlanningCalendar {
|
|
|
|
|
|
// Index events by date
|
|
|
|
|
|
byDate := map[string][]CalendarEvent{}
|
|
|
|
|
|
for _, row := range rows {
|
|
|
|
|
|
if !row.EventDate.Valid {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
key := row.EventDate.Time.Format("2006-01-02")
|
|
|
|
|
|
byDate[key] = append(byDate[key], calEventFromRow(row))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// First day of month, last day of month
|
|
|
|
|
|
first := time.Date(month.Year(), month.Month(), 1, 0, 0, 0, 0, month.Location())
|
|
|
|
|
|
last := first.AddDate(0, 1, -1)
|
|
|
|
|
|
|
|
|
|
|
|
// Start grid from the Monday on or before 1st of month
|
|
|
|
|
|
gridStart := MondayOf(first)
|
|
|
|
|
|
|
|
|
|
|
|
// Build 6 weeks (42 days) to ensure we always cover the month
|
|
|
|
|
|
var weeks [][]CalendarDay
|
|
|
|
|
|
cursor := gridStart
|
|
|
|
|
|
for w := 0; w < 6; w++ {
|
|
|
|
|
|
var week []CalendarDay
|
|
|
|
|
|
for d := 0; d < 7; d++ {
|
|
|
|
|
|
key := cursor.Format("2006-01-02")
|
|
|
|
|
|
week = append(week, CalendarDay{
|
|
|
|
|
|
Date: cursor,
|
|
|
|
|
|
DayNum: cursor.Day(),
|
|
|
|
|
|
IsToday: samePlanningDay(cursor, today),
|
|
|
|
|
|
OtherMonth: cursor.Month() != first.Month(),
|
|
|
|
|
|
Events: byDate[key],
|
|
|
|
|
|
})
|
|
|
|
|
|
cursor = cursor.AddDate(0, 0, 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
weeks = append(weeks, week)
|
|
|
|
|
|
// Stop after we've passed the last day of the month and completed the week
|
|
|
|
|
|
if cursor.After(last) && len(weeks) >= 4 {
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
prevMonth := first.AddDate(0, -1, 0)
|
|
|
|
|
|
nextMonth := first.AddDate(0, 1, 0)
|
|
|
|
|
|
|
|
|
|
|
|
return PlanningCalendar{
|
|
|
|
|
|
View: "month",
|
|
|
|
|
|
Label: first.Format("January 2006"),
|
|
|
|
|
|
PrevURL: PlanningMonthURL(prevMonth),
|
|
|
|
|
|
TodayURL: PlanningMonthURL(today),
|
|
|
|
|
|
NextURL: PlanningMonthURL(nextMonth),
|
|
|
|
|
|
Weeks: weeks,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// buildHourSlots returns a slice of hour label strings from 07:00 to 20:00.
|
|
|
|
|
|
func buildHourSlots() []string {
|
|
|
|
|
|
slots := make([]string, 14)
|
|
|
|
|
|
for i := range slots {
|
|
|
|
|
|
slots[i] = fmt.Sprintf("%02d:00", 7+i)
|
|
|
|
|
|
}
|
|
|
|
|
|
return slots
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// BuildWeekCalendar constructs a PlanningCalendar for the week view.
|
|
|
|
|
|
func BuildWeekCalendar(weekStart time.Time, today time.Time, rows []sqlc.ListUserEventsRangeRow) PlanningCalendar {
|
|
|
|
|
|
// Index events by date
|
|
|
|
|
|
byDate := map[string][]sqlc.ListUserEventsRangeRow{}
|
|
|
|
|
|
for _, row := range rows {
|
|
|
|
|
|
if !row.EventDate.Valid {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
key := row.EventDate.Time.Format("2006-01-02")
|
|
|
|
|
|
byDate[key] = append(byDate[key], row)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
days := make([]CalendarDayColumn, 7)
|
|
|
|
|
|
for i := 0; i < 7; i++ {
|
|
|
|
|
|
d := weekStart.AddDate(0, 0, i)
|
|
|
|
|
|
key := d.Format("2006-01-02")
|
|
|
|
|
|
events := make([]CalendarTimeEvent, 0, len(byDate[key]))
|
|
|
|
|
|
for _, row := range byDate[key] {
|
|
|
|
|
|
events = append(events, calTimeEventFromRow(row))
|
|
|
|
|
|
}
|
|
|
|
|
|
days[i] = CalendarDayColumn{
|
|
|
|
|
|
Date: d,
|
|
|
|
|
|
Label: d.Format("Mon 2"),
|
|
|
|
|
|
IsToday: samePlanningDay(d, today),
|
|
|
|
|
|
Events: events,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
weekEnd := weekStart.AddDate(0, 0, 6)
|
|
|
|
|
|
var label string
|
|
|
|
|
|
if weekStart.Month() == weekEnd.Month() {
|
|
|
|
|
|
label = fmt.Sprintf("%d–%d %s %d", weekStart.Day(), weekEnd.Day(), weekStart.Format("January"), weekStart.Year())
|
|
|
|
|
|
} else {
|
|
|
|
|
|
label = weekStart.Format("2 Jan") + " – " + weekEnd.Format("2 Jan 2006")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
prevWeek := weekStart.AddDate(0, 0, -7)
|
|
|
|
|
|
nextWeek := weekStart.AddDate(0, 0, 7)
|
|
|
|
|
|
|
|
|
|
|
|
mini, miniLabel := buildMiniMonth(weekStart, today, weekStart, weekEnd)
|
|
|
|
|
|
|
|
|
|
|
|
return PlanningCalendar{
|
|
|
|
|
|
View: "week",
|
|
|
|
|
|
Label: label,
|
|
|
|
|
|
PrevURL: PlanningWeekURL(prevWeek),
|
|
|
|
|
|
TodayURL: PlanningWeekURL(today),
|
|
|
|
|
|
NextURL: PlanningWeekURL(nextWeek),
|
|
|
|
|
|
Days: days,
|
|
|
|
|
|
HourSlots: buildHourSlots(),
|
|
|
|
|
|
MiniMonth: mini,
|
|
|
|
|
|
MiniMonthLabel: miniLabel,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// BuildDayCalendar constructs a PlanningCalendar for the day view.
|
|
|
|
|
|
func BuildDayCalendar(date time.Time, today time.Time, rows []sqlc.ListUserEventsRangeRow) PlanningCalendar {
|
|
|
|
|
|
events := make([]CalendarTimeEvent, 0, len(rows))
|
|
|
|
|
|
for _, row := range rows {
|
|
|
|
|
|
events = append(events, calTimeEventFromRow(row))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
days := []CalendarDayColumn{
|
|
|
|
|
|
{
|
|
|
|
|
|
Date: date,
|
|
|
|
|
|
Label: date.Format("Monday 2 January"),
|
|
|
|
|
|
IsToday: samePlanningDay(date, today),
|
|
|
|
|
|
Events: events,
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
prevDay := date.AddDate(0, 0, -1)
|
|
|
|
|
|
nextDay := date.AddDate(0, 0, 1)
|
|
|
|
|
|
|
|
|
|
|
|
mini, miniLabel := buildMiniMonth(date, today, date, date)
|
|
|
|
|
|
|
|
|
|
|
|
return PlanningCalendar{
|
|
|
|
|
|
View: "day",
|
|
|
|
|
|
Label: date.Format("Monday 2 January 2006"),
|
|
|
|
|
|
PrevURL: PlanningDayURL(prevDay),
|
|
|
|
|
|
TodayURL: PlanningDayURL(today),
|
|
|
|
|
|
NextURL: PlanningDayURL(nextDay),
|
|
|
|
|
|
Days: days,
|
|
|
|
|
|
HourSlots: buildHourSlots(),
|
|
|
|
|
|
MiniMonth: mini,
|
|
|
|
|
|
MiniMonthLabel: miniLabel,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// buildMiniMonth builds the 7-col mini-month grid for the week/day sidebar panel.
|
|
|
|
|
|
// highlightStart/highlightEnd define the range of days that get the in-week class.
|
|
|
|
|
|
func buildMiniMonth(anchor time.Time, today time.Time, highlightStart time.Time, highlightEnd time.Time) ([][]MiniCalDay, string) {
|
|
|
|
|
|
first := time.Date(anchor.Year(), anchor.Month(), 1, 0, 0, 0, 0, anchor.Location())
|
|
|
|
|
|
last := first.AddDate(0, 1, -1)
|
|
|
|
|
|
gridStart := MondayOf(first)
|
|
|
|
|
|
|
|
|
|
|
|
var weeks [][]MiniCalDay
|
|
|
|
|
|
cursor := gridStart
|
|
|
|
|
|
for w := 0; w < 6; w++ {
|
|
|
|
|
|
var week []MiniCalDay
|
|
|
|
|
|
for d := 0; d < 7; d++ {
|
|
|
|
|
|
inWeek := !cursor.Before(highlightStart) && !cursor.After(highlightEnd)
|
|
|
|
|
|
week = append(week, MiniCalDay{
|
|
|
|
|
|
Date: cursor,
|
|
|
|
|
|
DayNum: cursor.Day(),
|
|
|
|
|
|
IsToday: samePlanningDay(cursor, today),
|
|
|
|
|
|
InWeek: inWeek,
|
|
|
|
|
|
URL: PlanningDayURL(cursor),
|
|
|
|
|
|
})
|
|
|
|
|
|
cursor = cursor.AddDate(0, 0, 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
weeks = append(weeks, week)
|
|
|
|
|
|
if cursor.After(last) && len(weeks) >= 4 {
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return weeks, first.Format("January 2006")
|
|
|
|
|
|
}
|