feat: overall overtime balance on history page
Backend:
- ClosedWeekStore.SumDelta: single SQL aggregate returning total delta_ms and
row count across all closed_weeks
- WeekService.Balance: thin passthrough returning BalanceResult{TotalDeltaMs, ClosedWeekCount}
- GET /api/weeks/balance handler; route registered alongside /weeks list/close/reopen
- Tests: store-level SumDelta (empty + populated), service-level Balance (empty + 2 weeks)
Frontend:
- weeks.balance() added to API client
- History page: balance card at top, fetched in parallel with existing data
- Loading state shows '—'; once loaded shows formatDelta value in green/red/gray
- Shows 'across N closed weeks' count alongside the value
This commit is contained in:
@@ -42,6 +42,21 @@ func NewWeekService(
|
||||
}
|
||||
}
|
||||
|
||||
// BalanceResult holds the overall overtime/undertime balance across all closed weeks.
|
||||
type BalanceResult struct {
|
||||
TotalDeltaMs int64 `json:"total_delta_ms"`
|
||||
ClosedWeekCount int `json:"closed_week_count"`
|
||||
}
|
||||
|
||||
// Balance returns the sum of delta_ms across all closed weeks.
|
||||
func (s *WeekService) Balance(ctx context.Context) (BalanceResult, error) {
|
||||
total, count, err := s.closedWeeks.SumDelta(ctx)
|
||||
if err != nil {
|
||||
return BalanceResult{}, err
|
||||
}
|
||||
return BalanceResult{TotalDeltaMs: total, ClosedWeekCount: count}, nil
|
||||
}
|
||||
|
||||
// WeekDayKeysExported is exported for testing.
|
||||
var WeekDayKeysExported = weekDayKeys
|
||||
|
||||
|
||||
@@ -241,3 +241,48 @@ func TestWeekSnapshotUpdatesWhenDayReopened(t *testing.T) {
|
||||
t.Errorf("updated delta_ms: want %dms, got %dms", wantDelta, cw2.DeltaMs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWeekServiceBalance(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
_, daySvc, weekSvc, _ := newFullServices(t)
|
||||
|
||||
// Empty — no closed weeks yet.
|
||||
bal, err := weekSvc.Balance(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Balance (empty): %v", err)
|
||||
}
|
||||
if bal.TotalDeltaMs != 0 || bal.ClosedWeekCount != 0 {
|
||||
t.Errorf("empty: want {0,0}, got {%d,%d}", bal.TotalDeltaMs, bal.ClosedWeekCount)
|
||||
}
|
||||
|
||||
// Close two weeks as holiday (worked = expected → delta = 0 each).
|
||||
for _, spec := range []struct {
|
||||
weekKey string
|
||||
monday time.Time
|
||||
}{
|
||||
{"2024-W03", time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC)},
|
||||
{"2024-W04", time.Date(2024, 1, 22, 0, 0, 0, 0, time.UTC)},
|
||||
} {
|
||||
for i := 0; i < 5; i++ {
|
||||
dk := spec.monday.AddDate(0, 0, i).Format("2006-01-02")
|
||||
if _, err := daySvc.MarkDay(ctx, dk, domain.DayKindHoliday); err != nil {
|
||||
t.Fatalf("MarkDay %s: %v", dk, err)
|
||||
}
|
||||
}
|
||||
if _, err := weekSvc.CloseWeek(ctx, spec.weekKey); err != nil {
|
||||
t.Fatalf("CloseWeek %s: %v", spec.weekKey, err)
|
||||
}
|
||||
}
|
||||
|
||||
bal, err = weekSvc.Balance(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Balance (populated): %v", err)
|
||||
}
|
||||
if bal.ClosedWeekCount != 2 {
|
||||
t.Errorf("count: want 2, got %d", bal.ClosedWeekCount)
|
||||
}
|
||||
// Both weeks: worked == expected (holiday marks = full hours), delta = 0.
|
||||
if bal.TotalDeltaMs != 0 {
|
||||
t.Errorf("total_delta_ms: want 0, got %d", bal.TotalDeltaMs)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user