Files
worktimeBot/internal/bot/totals.go
db123 9d123316e6 feat: Hijri calendar, virtual midnight crossover, overlap rejection, WAL mode
- Hijri (قمری) calendar added as 3rd calendar option with tabular conversion
- Calendar type select menu (like accent) with gregorian/jalali/hijri
- Calendar type applied to exports and reports
- Midnight crossover: virtual splits at computation time (no synthetic DB events)
  - Handles both UTC and local timezone boundaries
  - Timezone changes handled naturally (recomputed on-the-fly)
- Rate limiting per user ID instead of chat ID
- WAL mode enabled for SQLite
- Smart reply keyboard updates after every state change
- Overlapping time edits rejected (must preserve in/out alternation)
- Single event deletion warns if timeline needs repair
- Day off no longer prevents clock in/out
- Accent colors shown with descriptive names in select menu
- Delete button label changed to DEL for visual distinction
2026-06-24 09:39:15 +03:30

87 lines
1.8 KiB
Go

package bot
import (
"sort"
"worktimeBot/internal/db"
)
type State int
const (
StateIdle State = iota
StateWorking
StateOnBreak
)
type DailyTotals struct {
TotalSeconds int64
BreakSeconds int64
PairCount int
}
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
pairs := 0
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
pairs++
breakStart = e.OccurredAt
state = StateOnBreak
case StateIdle:
breakStart = e.OccurredAt
state = StateOnBreak
case StateOnBreak:
breakStart = e.OccurredAt
}
}
}
return DailyTotals{
TotalSeconds: workTotal,
BreakSeconds: breakTotal,
PairCount: pairs,
}
}