GET /api/dashboard returns aggregated data: - deadline_summary: overdue, due this/next week, ok counts - case_summary: active, new this month, closed counts - upcoming_deadlines: next 7 days with case info - upcoming_appointments: next 7 days - recent_activity: last 10 case events Uses efficient CTE query for summaries. Also fixes duplicate writeJSON/writeError declarations in appointments handler.
152 lines
5.4 KiB
Go
152 lines
5.4 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jmoiron/sqlx"
|
|
)
|
|
|
|
type DashboardService struct {
|
|
db *sqlx.DB
|
|
}
|
|
|
|
func NewDashboardService(db *sqlx.DB) *DashboardService {
|
|
return &DashboardService{db: db}
|
|
}
|
|
|
|
type DashboardData struct {
|
|
DeadlineSummary DeadlineSummary `json:"deadline_summary"`
|
|
CaseSummary CaseSummary `json:"case_summary"`
|
|
UpcomingDeadlines []UpcomingDeadline `json:"upcoming_deadlines"`
|
|
UpcomingAppointments []UpcomingAppointment `json:"upcoming_appointments"`
|
|
RecentActivity []RecentActivity `json:"recent_activity"`
|
|
}
|
|
|
|
type DeadlineSummary struct {
|
|
OverdueCount int `json:"overdue_count" db:"overdue_count"`
|
|
DueThisWeek int `json:"due_this_week" db:"due_this_week"`
|
|
DueNextWeek int `json:"due_next_week" db:"due_next_week"`
|
|
OKCount int `json:"ok_count" db:"ok_count"`
|
|
}
|
|
|
|
type CaseSummary struct {
|
|
ActiveCount int `json:"active_count" db:"active_count"`
|
|
NewThisMonth int `json:"new_this_month" db:"new_this_month"`
|
|
ClosedCount int `json:"closed_count" db:"closed_count"`
|
|
}
|
|
|
|
type UpcomingDeadline struct {
|
|
ID uuid.UUID `json:"id" db:"id"`
|
|
Title string `json:"title" db:"title"`
|
|
DueDate string `json:"due_date" db:"due_date"`
|
|
CaseNumber string `json:"case_number" db:"case_number"`
|
|
CaseTitle string `json:"case_title" db:"case_title"`
|
|
Status string `json:"status" db:"status"`
|
|
}
|
|
|
|
type UpcomingAppointment struct {
|
|
ID uuid.UUID `json:"id" db:"id"`
|
|
Title string `json:"title" db:"title"`
|
|
StartAt time.Time `json:"start_at" db:"start_at"`
|
|
CaseNumber *string `json:"case_number" db:"case_number"`
|
|
Location *string `json:"location" db:"location"`
|
|
}
|
|
|
|
type RecentActivity struct {
|
|
EventType *string `json:"event_type" db:"event_type"`
|
|
Title string `json:"title" db:"title"`
|
|
CaseNumber string `json:"case_number" db:"case_number"`
|
|
EventDate *time.Time `json:"event_date" db:"event_date"`
|
|
}
|
|
|
|
func (s *DashboardService) Get(ctx context.Context, tenantID uuid.UUID) (*DashboardData, error) {
|
|
now := time.Now()
|
|
today := now.Format("2006-01-02")
|
|
endOfWeek := now.AddDate(0, 0, 7-int(now.Weekday())).Format("2006-01-02")
|
|
endOfNextWeek := now.AddDate(0, 0, 14-int(now.Weekday())).Format("2006-01-02")
|
|
in7Days := now.AddDate(0, 0, 7).Format("2006-01-02")
|
|
startOfMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()).Format("2006-01-02")
|
|
|
|
data := &DashboardData{}
|
|
|
|
// Single query with CTEs for deadline + case summaries
|
|
summaryQuery := `
|
|
WITH deadline_stats AS (
|
|
SELECT
|
|
COUNT(*) FILTER (WHERE due_date < $2 AND status = 'pending') AS overdue_count,
|
|
COUNT(*) FILTER (WHERE due_date >= $2 AND due_date <= $3 AND status = 'pending') AS due_this_week,
|
|
COUNT(*) FILTER (WHERE due_date > $3 AND due_date <= $4 AND status = 'pending') AS due_next_week,
|
|
COUNT(*) FILTER (WHERE due_date > $4 AND status = 'pending') AS ok_count
|
|
FROM deadlines
|
|
WHERE tenant_id = $1
|
|
),
|
|
case_stats AS (
|
|
SELECT
|
|
COUNT(*) FILTER (WHERE status = 'active') AS active_count,
|
|
COUNT(*) FILTER (WHERE created_at >= $5::date AND status != 'archived') AS new_this_month,
|
|
COUNT(*) FILTER (WHERE status IN ('closed', 'archived')) AS closed_count
|
|
FROM cases
|
|
WHERE tenant_id = $1
|
|
)
|
|
SELECT
|
|
ds.overdue_count, ds.due_this_week, ds.due_next_week, ds.ok_count,
|
|
cs.active_count, cs.new_this_month, cs.closed_count
|
|
FROM deadline_stats ds, case_stats cs`
|
|
|
|
var summaryRow struct {
|
|
DeadlineSummary
|
|
CaseSummary
|
|
}
|
|
err := s.db.GetContext(ctx, &summaryRow, summaryQuery, tenantID, today, endOfWeek, endOfNextWeek, startOfMonth)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("dashboard summary: %w", err)
|
|
}
|
|
data.DeadlineSummary = summaryRow.DeadlineSummary
|
|
data.CaseSummary = summaryRow.CaseSummary
|
|
|
|
// Upcoming deadlines (next 7 days)
|
|
deadlineQuery := `
|
|
SELECT d.id, d.title, d.due_date, c.case_number, c.title AS case_title, d.status
|
|
FROM deadlines d
|
|
JOIN cases c ON c.id = d.case_id AND c.tenant_id = d.tenant_id
|
|
WHERE d.tenant_id = $1 AND d.status = 'pending' AND d.due_date >= $2 AND d.due_date <= $3
|
|
ORDER BY d.due_date ASC`
|
|
|
|
data.UpcomingDeadlines = []UpcomingDeadline{}
|
|
if err := s.db.SelectContext(ctx, &data.UpcomingDeadlines, deadlineQuery, tenantID, today, in7Days); err != nil {
|
|
return nil, fmt.Errorf("dashboard upcoming deadlines: %w", err)
|
|
}
|
|
|
|
// Upcoming appointments (next 7 days)
|
|
appointmentQuery := `
|
|
SELECT a.id, a.title, a.start_at, c.case_number, a.location
|
|
FROM appointments a
|
|
LEFT JOIN cases c ON c.id = a.case_id AND c.tenant_id = a.tenant_id
|
|
WHERE a.tenant_id = $1 AND a.start_at >= $2::timestamp AND a.start_at < ($2::date + interval '7 days')
|
|
ORDER BY a.start_at ASC`
|
|
|
|
data.UpcomingAppointments = []UpcomingAppointment{}
|
|
if err := s.db.SelectContext(ctx, &data.UpcomingAppointments, appointmentQuery, tenantID, now); err != nil {
|
|
return nil, fmt.Errorf("dashboard upcoming appointments: %w", err)
|
|
}
|
|
|
|
// Recent activity (last 10 case events)
|
|
activityQuery := `
|
|
SELECT ce.event_type, ce.title, c.case_number, ce.event_date
|
|
FROM case_events ce
|
|
JOIN cases c ON c.id = ce.case_id AND c.tenant_id = ce.tenant_id
|
|
WHERE ce.tenant_id = $1
|
|
ORDER BY COALESCE(ce.event_date, ce.created_at) DESC
|
|
LIMIT 10`
|
|
|
|
data.RecentActivity = []RecentActivity{}
|
|
if err := s.db.SelectContext(ctx, &data.RecentActivity, activityQuery, tenantID); err != nil {
|
|
return nil, fmt.Errorf("dashboard recent activity: %w", err)
|
|
}
|
|
|
|
return data, nil
|
|
}
|