t-paliad-030. Adds `/agenda` — a single page that merges every visible deadline and appointment into a day-grouped timeline, the third overview surface alongside Dashboard and the per-resource lists. - AgendaService: merges paliad.deadlines + paliad.appointments, gated by the same team-membership predicate used everywhere else; personal appointments stay creator-only. Items are sorted by date and tagged with urgency (overdue / today / tomorrow / this_week / later) so the client can apply the traffic-light colours without re-deriving buckets. - GET /api/agenda?from&to&types and GET /agenda with the same server-side hydration pattern as /dashboard (JSON payload spliced into the shell so the timeline paints on first frame). - Frontend: agenda.tsx + client/agenda.ts render a day-grouped timeline with type/range chips; filters round-trip through the URL. - Sidebar entry under "Übersicht"; DE+EN i18n across all new keys.
59 lines
1.7 KiB
Go
59 lines
1.7 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bytes"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
)
|
|
|
|
// Same server-side hydration trick as the dashboard: the agenda page shell
|
|
// is pre-rendered by bun (`renderAgenda()` → dist/agenda.html) with a
|
|
// placeholder token; the handler splices in the JSON payload at request time
|
|
// so the client paints without a second round-trip.
|
|
const agendaDataPlaceholder = "/*__PALIAD_AGENDA_DATA__*/"
|
|
|
|
var (
|
|
agendaShellOnce sync.Once
|
|
agendaShellBytes []byte
|
|
agendaShellErr error
|
|
)
|
|
|
|
func loadAgendaShell() ([]byte, error) {
|
|
agendaShellOnce.Do(func() {
|
|
path := filepath.Join("dist", "agenda.html")
|
|
agendaShellBytes, agendaShellErr = os.ReadFile(path)
|
|
if agendaShellErr != nil {
|
|
return
|
|
}
|
|
if !bytes.Contains(agendaShellBytes, []byte(agendaDataPlaceholder)) {
|
|
log.Printf("warning: agenda.html is missing the data placeholder — client will fall back to /api/agenda")
|
|
}
|
|
})
|
|
return agendaShellBytes, agendaShellErr
|
|
}
|
|
|
|
func serveAgendaShell(w http.ResponseWriter, _ *http.Request, payload []byte) {
|
|
shell, err := loadAgendaShell()
|
|
if err != nil {
|
|
http.Error(w, "agenda shell unavailable", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
var body []byte
|
|
if len(payload) > 0 {
|
|
inline := append([]byte("window.__PALIAD_AGENDA__="), escapeForScript(payload)...)
|
|
inline = append(inline, ';')
|
|
body = bytes.Replace(shell, []byte(agendaDataPlaceholder), inline, 1)
|
|
} else {
|
|
body = bytes.Replace(shell, []byte(agendaDataPlaceholder),
|
|
[]byte("window.__PALIAD_AGENDA__=null;"), 1)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
w.Header().Set("Cache-Control", "no-store")
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write(body)
|
|
}
|