- 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
91 lines
2.3 KiB
Go
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,
|
|
}
|
|
}
|