package domain // DayKind represents the kind of a closed day. type DayKind string const ( DayKindWork DayKind = "work" DayKindHoliday DayKind = "holiday" DayKindVacation DayKind = "vacation" DayKindSick DayKind = "sick" ) func (k DayKind) Valid() bool { switch k { case DayKindWork, DayKindHoliday, DayKindVacation, DayKindSick: return true } return false } // Entry is a tracked time range within a single day. type Entry struct { ID string `json:"id"` StartTime int64 `json:"start_time"` // unix ms UTC EndTime *int64 `json:"end_time"` // nil while running AutoStopped bool `json:"auto_stopped"` Note string `json:"note"` DayKey string `json:"day_key"` // YYYY-MM-DD in configured tz UpdatedAt int64 `json:"updated_at"` DeletedAt *int64 `json:"deleted_at,omitempty"` } // IsRunning returns true if the entry has no end time. func (e *Entry) IsRunning() bool { return e.EndTime == nil } // DurationMs returns the duration in milliseconds. Returns 0 for running entries. func (e *Entry) DurationMs() int64 { if e.EndTime == nil { return 0 } return *e.EndTime - e.StartTime } // ClosedDay is the merged result of closing a day. type ClosedDay struct { DayKey string `json:"day_key"` StartTime *int64 `json:"start_time"` // nil for non-work kinds EndTime *int64 `json:"end_time"` // nil for non-work kinds WorkedMs int64 `json:"worked_ms"` Kind DayKind `json:"kind"` ClosedAt int64 `json:"closed_at"` UpdatedAt int64 `json:"updated_at"` } // ClosedWeek is the overtime/undertime snapshot for a week. type ClosedWeek struct { WeekKey string `json:"week_key"` // YYYY-Www ISO week ExpectedMs int64 `json:"expected_ms"` WorkedMs int64 `json:"worked_ms"` DeltaMs int64 `json:"delta_ms"` // worked - expected (signed) ClosedAt int64 `json:"closed_at"` UpdatedAt int64 `json:"updated_at"` } // Settings holds the effective configuration for a period. type Settings struct { ID int64 `json:"id"` EffectiveFrom string `json:"effective_from"` // YYYY-MM-DD HoursPerWeek float64 `json:"hours_per_week"` WorkdaysMask int `json:"workdays_mask"` // bits Mon=1..Sun=64 Timezone string `json:"timezone"` CreatedAt int64 `json:"created_at"` } // DailyExpectedMs returns the expected milliseconds for a single workday. func (s *Settings) DailyExpectedMs() int64 { days := popcount(s.WorkdaysMask) if days == 0 { return 0 } totalMs := int64(s.HoursPerWeek * 3_600_000) return totalMs / int64(days) } // IsWorkday returns true if the given weekday (0=Sunday..6=Saturday, time.Weekday) is a workday. // We use Mon=bit0 .. Sun=bit6 internally. func (s *Settings) IsWorkday(wd int) bool { // time.Weekday: Sunday=0, Monday=1, ..., Saturday=6 // our mask: Monday=bit0(1), Tuesday=bit1(2), ... Sunday=bit6(64) var bit int switch wd { case 0: // Sunday bit = 64 case 1: // Monday bit = 1 case 2: bit = 2 case 3: bit = 4 case 4: bit = 8 case 5: bit = 16 case 6: // Saturday bit = 32 } return s.WorkdaysMask&bit != 0 } func popcount(n int) int { count := 0 for n != 0 { count += n & 1 n >>= 1 } return count }