- 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)
125 lines
3.8 KiB
Go
125 lines
3.8 KiB
Go
package web
|
|
|
|
import (
|
|
"log/slog"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"backend/internal/auth"
|
|
"backend/internal/db/sqlc"
|
|
"backend/templates"
|
|
|
|
"github.com/gorilla/csrf"
|
|
)
|
|
|
|
type PlanningDeps struct {
|
|
Queries *sqlc.Queries
|
|
Now func() time.Time
|
|
}
|
|
|
|
func parsePlanningStart(raw string, now time.Time) time.Time {
|
|
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return today
|
|
}
|
|
parsed, err := time.Parse("2006-01-02", raw)
|
|
if err != nil {
|
|
return today
|
|
}
|
|
return time.Date(parsed.Year(), parsed.Month(), parsed.Day(), 0, 0, 0, 0, time.Local)
|
|
}
|
|
|
|
func parsePlanningMonth(raw string, now time.Time) time.Time {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.Local)
|
|
}
|
|
parsed, err := time.Parse("2006-01", raw)
|
|
if err != nil {
|
|
return time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.Local)
|
|
}
|
|
return time.Date(parsed.Year(), parsed.Month(), 1, 0, 0, 0, 0, time.Local)
|
|
}
|
|
|
|
func PlanningPageHandler(deps PlanningDeps) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
_, user, _ := auth.Authed(r.Context())
|
|
now := time.Now
|
|
if deps.Now != nil {
|
|
now = deps.Now
|
|
}
|
|
today := parsePlanningStart("", now())
|
|
|
|
view := strings.TrimSpace(r.URL.Query().Get("view"))
|
|
if view == "" {
|
|
view = "month"
|
|
}
|
|
|
|
sidebarTablos, err := deps.Queries.ListTablosByUser(r.Context(), user.ID)
|
|
if err != nil {
|
|
slog.Default().Error("planning: ListTablosByUser failed", "user_id", user.ID, "err", err)
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if sidebarTablos == nil {
|
|
sidebarTablos = []sqlc.Tablo{}
|
|
}
|
|
|
|
var cal templates.PlanningCalendar
|
|
|
|
switch view {
|
|
case "week":
|
|
weekStart := templates.MondayOf(parsePlanningStart(r.URL.Query().Get("start"), now()))
|
|
weekEnd := weekStart.AddDate(0, 0, 6)
|
|
rows, err := deps.Queries.ListUserEventsRange(r.Context(), sqlc.ListUserEventsRangeParams{
|
|
UserID: user.ID,
|
|
StartDate: pgDateFromTime(weekStart),
|
|
EndDate: pgDateFromTime(weekEnd),
|
|
})
|
|
if err != nil {
|
|
slog.Default().Error("planning: ListUserEventsRange (week) failed", "user_id", user.ID, "err", err)
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
cal = templates.BuildWeekCalendar(weekStart, today, rows)
|
|
|
|
case "day":
|
|
date := parsePlanningStart(r.URL.Query().Get("date"), now())
|
|
rows, err := deps.Queries.ListUserEventsRange(r.Context(), sqlc.ListUserEventsRangeParams{
|
|
UserID: user.ID,
|
|
StartDate: pgDateFromTime(date),
|
|
EndDate: pgDateFromTime(date),
|
|
})
|
|
if err != nil {
|
|
slog.Default().Error("planning: ListUserEventsRange (day) failed", "user_id", user.ID, "err", err)
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
cal = templates.BuildDayCalendar(date, today, rows)
|
|
|
|
default: // "month"
|
|
month := parsePlanningMonth(r.URL.Query().Get("month"), now())
|
|
firstOfMonth := time.Date(month.Year(), month.Month(), 1, 0, 0, 0, 0, time.Local)
|
|
lastOfMonth := firstOfMonth.AddDate(0, 1, -1)
|
|
rows, err := deps.Queries.ListUserEventsRange(r.Context(), sqlc.ListUserEventsRangeParams{
|
|
UserID: user.ID,
|
|
StartDate: pgDateFromTime(firstOfMonth),
|
|
EndDate: pgDateFromTime(lastOfMonth),
|
|
})
|
|
if err != nil {
|
|
slog.Default().Error("planning: ListUserEventsRange (month) failed", "user_id", user.ID, "err", err)
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
cal = templates.BuildMonthCalendar(month, today, rows)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
_ = templates.PlanningCalendarPage(user, csrf.Token(r), "/planning", sidebarTablos, cal,
|
|
"Planning",
|
|
[]templates.BreadcrumbItem{{Label: "Planning", Href: ""}},
|
|
).Render(r.Context(), w)
|
|
}
|
|
}
|