Merge: t-paliad-110 PR-4 — Dashboard Termine rail + Fristen rail refactor
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -667,6 +667,8 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"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<Lang, Record<string, string>> = {
|
||||
"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",
|
||||
|
||||
@@ -57,7 +57,7 @@ export function renderDashboard(): string {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Traffic-light deadline summary */}
|
||||
{/* Traffic-light deadline summary (4+1: Überfällig conditional + 4 universal — t-paliad-110) */}
|
||||
<section className="dashboard-summary" aria-labelledby="dashboard-summary-heading">
|
||||
<h2 id="dashboard-summary-heading" className="dashboard-section-heading" data-i18n="dashboard.summary.heading">
|
||||
Fristen auf einen Blick
|
||||
@@ -79,9 +79,30 @@ export function renderDashboard(): string {
|
||||
<div className="dashboard-card-count" id="dashboard-count-next-week">0</div>
|
||||
<div className="dashboard-card-label" data-i18n="dashboard.summary.next_week">Nächste Woche</div>
|
||||
</a>
|
||||
<a href="/deadlines?status=completed" className="dashboard-card dashboard-card-done" id="dashboard-card-completed">
|
||||
<div className="dashboard-card-count" id="dashboard-count-completed">0</div>
|
||||
<div className="dashboard-card-label" data-i18n="dashboard.summary.completed">Erledigt</div>
|
||||
<a href="/deadlines?status=later" className="dashboard-card dashboard-card-later" id="dashboard-card-later">
|
||||
<div className="dashboard-card-count" id="dashboard-count-later">0</div>
|
||||
<div className="dashboard-card-label" data-i18n="dashboard.summary.later">Später</div>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Termine summary rail — 3 cards: Heute · Diese Woche · Später (t-paliad-110) */}
|
||||
<section className="dashboard-summary" aria-labelledby="dashboard-appointment-summary-heading">
|
||||
<h2 id="dashboard-appointment-summary-heading" className="dashboard-section-heading" data-i18n="dashboard.appointment_summary.heading">
|
||||
Termine auf einen Blick
|
||||
</h2>
|
||||
<div className="dashboard-summary-grid">
|
||||
<a href="/appointments?status=today" className="dashboard-card dashboard-card-appt-today" id="dashboard-card-appt-today">
|
||||
<div className="dashboard-card-count" id="dashboard-count-appt-today">0</div>
|
||||
<div className="dashboard-card-label" data-i18n="dashboard.summary.today">Heute</div>
|
||||
</a>
|
||||
<a href="/appointments?status=this_week" className="dashboard-card dashboard-card-appt-week" id="dashboard-card-appt-thisweek">
|
||||
<div className="dashboard-card-count" id="dashboard-count-appt-this-week">0</div>
|
||||
<div className="dashboard-card-label" data-i18n="dashboard.summary.this_week">Diese Woche</div>
|
||||
</a>
|
||||
<a href="/appointments?status=later" className="dashboard-card dashboard-card-appt-later" id="dashboard-card-appt-later">
|
||||
<div className="dashboard-card-count" id="dashboard-count-appt-later">0</div>
|
||||
<div className="dashboard-card-label" data-i18n="dashboard.summary.later">Später</div>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user