Files
worktimeBot/internal/bot/totals.go
db123 344c615666 refactor: break up large functions, add RPG/league/salary/burnout, fix bugs, add tests
- Refactor GenerateMonthlyReport (198→70), HandleCallback (128→85),
  handleHistoryCallback (131→38), handleExportCallback (100→25),
  checkAchievements (86→25) into extracted helper functions
- Add RPG system (XP, levels, streak, achievements, burnout)
- Add WorkTime League leaderboard
- Add salary estimation with configurable currency/rate
- Add input sanitization (SanitizeNote, SanitizeDisplayName)
- Add settings select-style pickers for toggles (report, RPG, league)
- Add break threshold inline picker UI
- Add event notes with pending state and /note command
- Add multi-calendar support (Jalali, Hijri)
- Add Excel export with theme picker
- Fix: getTodayBreakThreshold uses user's timezone (was UTC)
- Fix: acknowledgeCallback passes real callback ID
- Fix: currency symbol safety with currencySymbol() helper
- Fix: count work hours in summary on day-off days
- Fix: unsilence all error returns from SQL Exec/Bot API/migrations
- Remove stale db/schema.sql and unreferenced computeWeekTotals
- Extract constants: DateLayout, TimeLayout, secondsPerHour, etc.
- Add 12 new tests (XP, salary, sanitize, currency, dateutil)
- Remove duplicate package doc comment in totals.go
2026-06-25 01:02:16 +03:30

91 lines
2.3 KiB
Go

package bot
import (
"sort"
"worktimeBot/internal/db"
)
// State represents the current tracking state during daily totals computation.
type State int
const (
// StateIdle indicates no active work session.
StateIdle State = iota
// StateWorking indicates an active work session.
StateWorking
// StateOnBreak indicates an active break session.
StateOnBreak
)
// DailyTotals holds the computed total work and break seconds for a single day.
type DailyTotals struct {
TotalSeconds int64
BreakSeconds int64
}
// ComputeDailyTotals calculates the total work and break time from a list of events.
// It sorts events chronologically, inserts synthetic start/end events when needed,
// and tracks state transitions between idle, working, and on-break.
func ComputeDailyTotals(events []db.Event, minBreakThreshold int64, dayStart, dayEnd int64) DailyTotals {
if len(events) == 0 {
return DailyTotals{}
}
sorted := make([]db.Event, len(events))
copy(sorted, events)
sort.Slice(sorted, func(i, j int) bool {
return sorted[i].OccurredAt < sorted[j].OccurredAt
})
if dayStart > 0 && len(sorted) > 0 && sorted[0].EventType == "out" {
first := db.Event{EventType: "in", OccurredAt: dayStart}
sorted = append([]db.Event{first}, sorted...)
}
if dayEnd > 0 && len(sorted) > 0 && sorted[len(sorted)-1].EventType == "in" {
last := db.Event{EventType: "out", OccurredAt: dayEnd}
sorted = append(sorted, last)
}
var workTotal, breakTotal int64
var workStart, breakStart int64
state := StateIdle
for _, e := range sorted {
switch e.EventType {
case "in":
switch state {
case StateIdle:
workStart = e.OccurredAt
state = StateWorking
case StateOnBreak:
breakDuration := e.OccurredAt - breakStart
if minBreakThreshold > 0 && breakDuration <= minBreakThreshold {
workStart = breakStart
} else {
breakTotal += breakDuration
workStart = e.OccurredAt
}
state = StateWorking
}
case "out":
switch state {
case StateWorking:
workTotal += e.OccurredAt - workStart
workStart = 0
breakStart = e.OccurredAt
state = StateOnBreak
case StateIdle:
breakStart = e.OccurredAt
state = StateOnBreak
case StateOnBreak:
breakStart = e.OccurredAt
}
}
}
return DailyTotals{
TotalSeconds: workTotal,
BreakSeconds: breakTotal,
}
}