diff --git a/frontend/src/client/dashboard.ts b/frontend/src/client/dashboard.ts index 8fb298e..b7fb4ec 100644 --- a/frontend/src/client/dashboard.ts +++ b/frontend/src/client/dashboard.ts @@ -14,7 +14,13 @@ interface DeadlineSummary { today: number; this_week: number; next_week: number; - completed_this_week: number; + later: number; +} + +interface AppointmentSummary { + today: number; + this_week: number; + later: number; } interface MatterSummary { @@ -60,6 +66,7 @@ interface ActivityEntry { interface DashboardData { user: DashboardUser | null; deadline_summary: DeadlineSummary; + appointment_summary: AppointmentSummary; matter_summary: MatterSummary; upcoming_deadlines: UpcomingDeadline[]; upcoming_appointments: UpcomingAppointment[]; @@ -98,6 +105,7 @@ function render(): void { if (!data) return; renderGreeting(data.user); renderSummary(data.deadline_summary); + renderAppointmentSummary(data.appointment_summary); renderMatters(data.matter_summary); renderDeadlines(data.upcoming_deadlines); renderAppointments(data.upcoming_appointments); @@ -133,17 +141,23 @@ function renderSummary(s: DeadlineSummary): void { setCount("dashboard-count-today", s.today); setCount("dashboard-count-this-week", s.this_week); setCount("dashboard-count-next-week", s.next_week); - setCount("dashboard-count-completed", s.completed_this_week); + setCount("dashboard-count-later", s.later); // Überfällig is an emergency category — hide the card entirely on a clean // slate (the .dashboard-summary-grid uses auto-fit so the row re-flows to // 4 cards) and trip the alarm styling when there's anything overdue. See - // t-paliad-105 / t-paliad-106. + // t-paliad-105 / t-paliad-106 / t-paliad-110. const overdueCard = document.getElementById("dashboard-card-overdue")!; overdueCard.classList.toggle("dashboard-card-overdue-hidden", s.overdue === 0); overdueCard.classList.toggle("dashboard-card-alarm", s.overdue > 0); } +function renderAppointmentSummary(s: AppointmentSummary): void { + setCount("dashboard-count-appt-today", s.today); + setCount("dashboard-count-appt-this-week", s.this_week); + setCount("dashboard-count-appt-later", s.later); +} + function renderMatters(s: MatterSummary): void { setCount("dashboard-matter-active", s.active); setCount("dashboard-matter-archived", s.archived); diff --git a/frontend/src/client/i18n.ts b/frontend/src/client/i18n.ts index b158bcb..6caaaec 100644 --- a/frontend/src/client/i18n.ts +++ b/frontend/src/client/i18n.ts @@ -667,6 +667,8 @@ const translations: Record> = { "dashboard.summary.this_week": "Diese Woche", "dashboard.summary.next_week": "N\u00e4chste Woche", "dashboard.summary.completed": "Erledigt", + "dashboard.summary.later": "Später", + "dashboard.appointment_summary.heading": "Termine auf einen Blick", "dashboard.matters.heading": "Meine Akten", "dashboard.matters.active": "Aktiv", "dashboard.matters.archived": "Archiviert", @@ -2165,6 +2167,8 @@ const translations: Record> = { "dashboard.summary.this_week": "This week", "dashboard.summary.next_week": "Next week", "dashboard.summary.completed": "Done", + "dashboard.summary.later": "Later", + "dashboard.appointment_summary.heading": "Appointments at a glance", "dashboard.matters.heading": "My matters", "dashboard.matters.active": "Active", "dashboard.matters.archived": "Archived", diff --git a/frontend/src/dashboard.tsx b/frontend/src/dashboard.tsx index ddf736d..5ebd8c1 100644 --- a/frontend/src/dashboard.tsx +++ b/frontend/src/dashboard.tsx @@ -57,7 +57,7 @@ export function renderDashboard(): string {

- {/* Traffic-light deadline summary */} + {/* Traffic-light deadline summary (4+1: Überfällig conditional + 4 universal — t-paliad-110) */}

Fristen auf einen Blick @@ -79,9 +79,30 @@ export function renderDashboard(): string {
0
Nächste Woche
- -
0
-
Erledigt
+
+
0
+
Später
+
+ +

+ + {/* Termine summary rail — 3 cards: Heute · Diese Woche · Später (t-paliad-110) */} +
+

+ Termine auf einen Blick +

+
+ +
0
+
Heute
+
+ +
0
+
Diese Woche
+
+ +
0
+
Später
diff --git a/frontend/src/i18n-keys.ts b/frontend/src/i18n-keys.ts index 34cbac8..553dcc1 100644 --- a/frontend/src/i18n-keys.ts +++ b/frontend/src/i18n-keys.ts @@ -505,6 +505,7 @@ export type I18nKey = | "dashboard.activity.event" | "dashboard.activity.heading" | "dashboard.activity.system" + | "dashboard.appointment_summary.heading" | "dashboard.appointments.empty" | "dashboard.appointments.heading" | "dashboard.deadlines.empty" @@ -517,6 +518,7 @@ export type I18nKey = | "dashboard.onboarding" | "dashboard.summary.completed" | "dashboard.summary.heading" + | "dashboard.summary.later" | "dashboard.summary.next_week" | "dashboard.summary.overdue" | "dashboard.summary.this_week" diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index 9ec60fd..dad090d 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -5702,6 +5702,17 @@ input[type="range"]::-moz-range-thumb { .dashboard-card-green { border-left: 3px solid var(--bucket-next-week); } .dashboard-card-done .dashboard-card-count { color: var(--bucket-done); } .dashboard-card-done { border-left: 3px solid var(--bucket-done); } +/* Später bucket on the Fristen rail — muted blue (t-paliad-110). */ +.dashboard-card-later .dashboard-card-count { color: var(--bucket-later); } +.dashboard-card-later { border-left: 3px solid var(--bucket-later); } +/* Termine rail uses the same urgency token family — keep the palette + consistent across rails so users read both at the same glance. */ +.dashboard-card-appt-today .dashboard-card-count { color: var(--bucket-today); } +.dashboard-card-appt-today { border-left: 3px solid var(--bucket-today); } +.dashboard-card-appt-week .dashboard-card-count { color: var(--bucket-week); } +.dashboard-card-appt-week { border-left: 3px solid var(--bucket-week); } +.dashboard-card-appt-later .dashboard-card-count { color: var(--bucket-later); } +.dashboard-card-appt-later { border-left: 3px solid var(--bucket-later); } /* Überfällig alarm — see t-paliad-105 / t-paliad-106. When overdue > 0 the card flips from a calm border-left accent to a diff --git a/internal/services/dashboard_service.go b/internal/services/dashboard_service.go index baf0918..4f023dd 100644 --- a/internal/services/dashboard_service.go +++ b/internal/services/dashboard_service.go @@ -33,6 +33,7 @@ func NewDashboardService(db *sqlx.DB, users *UserService) *DashboardService { type DashboardData struct { User *DashboardUser `json:"user"` DeadlineSummary DeadlineSummary `json:"deadline_summary"` + AppointmentSummary AppointmentSummary `json:"appointment_summary"` MatterSummary MatterSummary `json:"matter_summary"` UpcomingDeadlines []UpcomingDeadline `json:"upcoming_deadlines"` UpcomingAppointments []UpcomingAppointment `json:"upcoming_appointments"` @@ -52,15 +53,26 @@ type DashboardUser struct { // // Bucket math is identical to DeadlineService.SummaryCounts (single source // of truth via computeDeadlineBucketBounds) so the Dashboard and the -// /deadlines summary cards always show the same numbers (t-paliad-106). -// CompletedThisWeek keeps the 7-day window the Dashboard has always used — -// the /deadlines surface still shows the all-time completed count. +// /deadlines summary cards always show the same numbers (t-paliad-106 → +// t-paliad-110). The Erledigt card is gone; status=completed stays +// reachable via the dropdown filter on the EventsPage but is no longer +// rendered as a card on the dashboard rail. type DeadlineSummary struct { - Overdue int `json:"overdue" db:"overdue"` - Today int `json:"today" db:"today"` - ThisWeek int `json:"this_week" db:"this_week"` - NextWeek int `json:"next_week" db:"next_week"` - CompletedThisWeek int `json:"completed_this_week" db:"completed_this_week"` + Overdue int `json:"overdue" db:"overdue"` + Today int `json:"today" db:"today"` + ThisWeek int `json:"this_week" db:"this_week"` + NextWeek int `json:"next_week" db:"next_week"` + Later int `json:"later" db:"later"` +} + +// AppointmentSummary feeds the Dashboard "Termine auf einen Blick" rail +// (t-paliad-110). Three cards: Heute / Diese Woche / Später. No Überfällig +// (past appointments aren't urgent — they happened) and no Nächste Woche +// (low-value distinction for appointments per the design doc). +type AppointmentSummary struct { + Today int `json:"today" db:"today"` + ThisWeek int `json:"this_week" db:"this_week"` + Later int `json:"later" db:"later"` } // MatterSummary counts visible Projects by status. Field names kept as @@ -135,10 +147,9 @@ func (s *DashboardService) Get(ctx context.Context, userID uuid.UUID) (*Dashboar now := time.Now() today := now.Format("2006-01-02") endOfWindow := now.AddDate(0, 0, 7).Format("2006-01-02") - sevenDaysAgo := now.AddDate(0, 0, -7).UTC() bounds := computeDeadlineBucketBounds(now.UTC()) - if err := s.loadSummary(ctx, data, user, bounds, sevenDaysAgo); err != nil { + if err := s.loadSummary(ctx, data, user, bounds); err != nil { return nil, err } if err := s.loadUpcomingDeadlines(ctx, data, user, today, endOfWindow); err != nil { @@ -155,18 +166,23 @@ func (s *DashboardService) Get(ctx context.Context, userID uuid.UUID) (*Dashboar return data, nil } -// loadSummary fills DeadlineSummary + MatterSummary. +// loadSummary fills DeadlineSummary + AppointmentSummary + MatterSummary. // // Bucket math comes from computeDeadlineBucketBounds (deadline_service.go) so -// the four pending buckets stay in lockstep with /api/deadlines/summary; the -// "completed" column on the Dashboard remains the 7-day rolling window -// (sevenDaysAgo) — the /deadlines surface intentionally shows all-time -// completed instead. +// the buckets stay in lockstep with /api/deadlines/summary and the unified +// /api/events/summary used by the EventsPage (t-paliad-110). +// +// Bucket model on the Dashboard: +// - Fristen rail: Überfällig (conditional alarm) · Heute · Diese Woche · +// Nächste Woche · Später. Erledigt is no longer rendered as a card — +// status=completed stays reachable via the EventsPage filter dropdown. +// - Termine rail: Heute · Diese Woche · Später. No Überfällig, no +// Nächste Woche (low-value distinction for appointments). // // Visibility predicate: see internal/services/visibility.go — global_admin // shortcut OR any ancestor-or-direct team membership. Applied once via a CTE; // downstream queries reuse the same helper. -func (s *DashboardService) loadSummary(ctx context.Context, data *DashboardData, user *models.User, bounds deadlineBucketBounds, sevenDaysAgo time.Time) error { +func (s *DashboardService) loadSummary(ctx context.Context, data *DashboardData, user *models.User, bounds deadlineBucketBounds) error { query := ` WITH visible_projekte AS ( SELECT p.id, p.status @@ -179,10 +195,20 @@ deadline_stats AS ( COUNT(*) FILTER (WHERE f.status = 'pending' AND f.due_date = $2::date) AS today, COUNT(*) FILTER (WHERE f.status = 'pending' AND f.due_date >= $3::date AND f.due_date < $4::date) AS this_week, COUNT(*) FILTER (WHERE f.status = 'pending' AND f.due_date >= $4::date AND f.due_date < $5::date) AS next_week, - COUNT(*) FILTER (WHERE f.status = 'completed' AND f.completed_at >= $6) AS completed_this_week + COUNT(*) FILTER (WHERE f.status = 'pending' AND f.due_date >= $5::date) AS later FROM paliad.deadlines f JOIN visible_projekte v ON v.id = f.project_id ), +appointment_stats AS ( + SELECT + COUNT(*) FILTER (WHERE t.start_at >= $2 AND t.start_at < $3) AS today, + COUNT(*) FILTER (WHERE t.start_at >= $3 AND t.start_at < $4) AS this_week, + COUNT(*) FILTER (WHERE t.start_at >= $5) AS later + FROM paliad.appointments t + LEFT JOIN visible_projekte v ON v.id = t.project_id + WHERE (t.project_id IS NULL AND t.created_by = $1) + OR (t.project_id IS NOT NULL AND v.id IS NOT NULL) +), matter_stats AS ( SELECT COUNT(*) FILTER (WHERE status = 'active') AS active, @@ -190,16 +216,20 @@ matter_stats AS ( COUNT(*) AS total FROM visible_projekte ) -SELECT ds.overdue, ds.today, ds.this_week, ds.next_week, ds.completed_this_week, +SELECT ds.overdue, ds.today, ds.this_week, ds.next_week, ds.later, + aps.today AS appt_today, aps.this_week AS appt_this_week, aps.later AS appt_later, ms.active, ms.archived, ms.total - FROM deadline_stats ds, matter_stats ms` + FROM deadline_stats ds, appointment_stats aps, matter_stats ms` var row struct { DeadlineSummary + ApptToday int `db:"appt_today"` + ApptThisWeek int `db:"appt_this_week"` + ApptLater int `db:"appt_later"` MatterSummary } err := s.db.GetContext(ctx, &row, query, - user.ID, bounds.today, bounds.tomorrow, bounds.nextMonday, bounds.weekAfter, sevenDaysAgo) + user.ID, bounds.today, bounds.tomorrow, bounds.nextMonday, bounds.weekAfter) if errors.Is(err, sql.ErrNoRows) { return nil } @@ -207,6 +237,11 @@ SELECT ds.overdue, ds.today, ds.this_week, ds.next_week, ds.completed_this_week, return fmt.Errorf("dashboard summary: %w", err) } data.DeadlineSummary = row.DeadlineSummary + data.AppointmentSummary = AppointmentSummary{ + Today: row.ApptToday, + ThisWeek: row.ApptThisWeek, + Later: row.ApptLater, + } data.MatterSummary = row.MatterSummary return nil }