xtablo-source/backend/templates/planning_forms.go

480 lines
13 KiB
Go
Raw Normal View History

package templates
import (
"fmt"
"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
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),
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")
}
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()
}
// 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
}
// ---------------------------------------------------------------------------
// 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:0020: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")
}