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
This commit is contained in:
@@ -65,123 +65,138 @@ func (h *Handler) handleHistoryCallback(ctx context.Context, chatID int64, msgID
|
||||
return
|
||||
}
|
||||
|
||||
var y, m int
|
||||
var date string
|
||||
var eid int64
|
||||
if h.handleHistoryCalendarNav(ctx, chatID, msgID, data) {
|
||||
return
|
||||
}
|
||||
if h.handleHistoryYearPicker(ctx, chatID, msgID, data) {
|
||||
return
|
||||
}
|
||||
if h.handleHistoryDayView(ctx, chatID, msgID, user, data, loc) {
|
||||
return
|
||||
}
|
||||
if h.handleHistoryEventEdit(ctx, chatID, msgID, user, data, loc, callbackID) {
|
||||
return
|
||||
}
|
||||
if h.handleHistoryEventAdd(ctx, chatID, msgID, user, data, loc) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Navigate to previous month
|
||||
// handleHistoryCalendarNav handles calendar navigation callbacks.
|
||||
func (h *Handler) handleHistoryCalendarNav(ctx context.Context, chatID int64, msgID int, data string) bool {
|
||||
var y, m int
|
||||
if n, _ := fmt.Sscanf(data, "cal_prev_%d_%d", &y, &m); n == 2 {
|
||||
h.editCalendar(ctx, chatID, msgID, y, m)
|
||||
return
|
||||
return true
|
||||
}
|
||||
// Navigate to next month
|
||||
if n, _ := fmt.Sscanf(data, "cal_next_%d_%d", &y, &m); n == 2 {
|
||||
h.editCalendar(ctx, chatID, msgID, y, m)
|
||||
return
|
||||
return true
|
||||
}
|
||||
// Navigate to previous year
|
||||
if n, _ := fmt.Sscanf(data, "cal_prev_year_%d_%d", &y, &m); n == 2 {
|
||||
h.editCalendar(ctx, chatID, msgID, y, m)
|
||||
return
|
||||
return true
|
||||
}
|
||||
// Navigate to next year
|
||||
if n, _ := fmt.Sscanf(data, "cal_next_year_%d_%d", &y, &m); n == 2 {
|
||||
h.editCalendar(ctx, chatID, msgID, y, m)
|
||||
return
|
||||
return true
|
||||
}
|
||||
// Show year picker from calendar
|
||||
return false
|
||||
}
|
||||
|
||||
// handleHistoryYearPicker handles year picker callbacks.
|
||||
func (h *Handler) handleHistoryYearPicker(ctx context.Context, chatID int64, msgID int, data string) bool {
|
||||
var y, m, startY, refY, refM int
|
||||
if n, _ := fmt.Sscanf(data, "cal_show_years_%d_%d", &y, &m); n == 2 {
|
||||
h.editCalendarYearPicker(ctx, chatID, msgID, y, m, y)
|
||||
return
|
||||
return true
|
||||
}
|
||||
// Navigate year block in picker
|
||||
var startY, refY, refM int
|
||||
if n, _ := fmt.Sscanf(data, "cal_years_%d_%d_%d", &startY, &refY, &refM); n == 3 {
|
||||
h.editCalendarYearPicker(ctx, chatID, msgID, startY+6, refM, refY)
|
||||
return
|
||||
return true
|
||||
}
|
||||
// Select year from picker
|
||||
if n, _ := fmt.Sscanf(data, "cal_year_%d", &y); n == 1 {
|
||||
h.editCalendar(ctx, chatID, msgID, y, 1)
|
||||
return
|
||||
return true
|
||||
}
|
||||
// Back from year picker to calendar
|
||||
if n, _ := fmt.Sscanf(data, "cal_years_back_%d_%d", &y, &m); n == 2 {
|
||||
h.editCalendar(ctx, chatID, msgID, y, m)
|
||||
return
|
||||
return true
|
||||
}
|
||||
// Open a specific day
|
||||
return false
|
||||
}
|
||||
|
||||
// handleHistoryDayView handles day-view-related callbacks.
|
||||
func (h *Handler) handleHistoryDayView(ctx context.Context, chatID int64, msgID int, user *db.User, data string, loc *time.Location) bool {
|
||||
var date string
|
||||
if n, _ := fmt.Sscanf(data, "cal_day_%s", &date); n == 1 && len(date) == 10 {
|
||||
h.editDayView(ctx, chatID, msgID, user, date, loc)
|
||||
return
|
||||
return true
|
||||
}
|
||||
// Back to day view from event editing
|
||||
var eid int64
|
||||
if n, _ := fmt.Sscanf(data, "back_day_%d", &eid); n == 1 {
|
||||
h.backToDayView(ctx, chatID, msgID, user, eid, loc)
|
||||
return
|
||||
return true
|
||||
}
|
||||
// Set event work type
|
||||
var wtid int64
|
||||
return false
|
||||
}
|
||||
|
||||
// handleHistoryEventEdit handles event editing callbacks.
|
||||
func (h *Handler) handleHistoryEventEdit(ctx context.Context, chatID int64, msgID int, user *db.User, data string, loc *time.Location, callbackID string) bool {
|
||||
var eid, wtid int64
|
||||
if n, _ := fmt.Sscanf(data, "settype_%d_%d", &eid, &wtid); n == 2 {
|
||||
h.setEventWorkType(ctx, chatID, msgID, user, eid, wtid, loc)
|
||||
return
|
||||
return true
|
||||
}
|
||||
// Show work type picker for an event
|
||||
if n, _ := fmt.Sscanf(data, "edit_type_%d", &eid); n == 1 {
|
||||
h.editEventType(ctx, chatID, msgID, user, eid, loc)
|
||||
return
|
||||
return true
|
||||
}
|
||||
// Delete event confirmation prompt
|
||||
if n, _ := fmt.Sscanf(data, "delete_%d", &eid); n == 1 {
|
||||
h.deleteEventPrompt(ctx, chatID, msgID, user, eid, loc)
|
||||
return
|
||||
return true
|
||||
}
|
||||
// Confirm event deletion
|
||||
if n, _ := fmt.Sscanf(data, "delconf_%d", &eid); n == 1 {
|
||||
h.deleteEventConfirm(ctx, chatID, msgID, user, eid, loc)
|
||||
return
|
||||
return true
|
||||
}
|
||||
// Show hour picker for event time
|
||||
if n, _ := fmt.Sscanf(data, "edit_time_%d", &eid); n == 1 {
|
||||
h.editEventTime(ctx, chatID, msgID, user, eid, loc)
|
||||
return
|
||||
return true
|
||||
}
|
||||
// Set event time (hour+minute)
|
||||
var hh, mm int
|
||||
if n, _ := fmt.Sscanf(data, "edittm_%d_%d_%d", &eid, &hh, &mm); n == 3 {
|
||||
h.editEventTimeSet(ctx, chatID, msgID, user, eid, hh, mm, loc)
|
||||
return
|
||||
h.editEventTimeSet(ctx, chatID, msgID, user, eid, hh, mm, loc, callbackID)
|
||||
return true
|
||||
}
|
||||
// Show minute picker (hour already chosen)
|
||||
if n, _ := fmt.Sscanf(data, "edittm_%d_%d", &eid, &hh); n == 2 {
|
||||
h.editEventTimeMin(ctx, chatID, msgID, user, eid, hh, loc)
|
||||
return
|
||||
return true
|
||||
}
|
||||
// Set note for an event — prompt user for text
|
||||
if n, _ := fmt.Sscanf(data, "note_%d", &eid); n == 1 {
|
||||
h.promptNote(ctx, chatID, msgID, user, eid, loc)
|
||||
return
|
||||
return true
|
||||
}
|
||||
// Cancel note prompt — return to day view
|
||||
if n, _ := fmt.Sscanf(data, "note_cancel_%d", &eid); n == 1 {
|
||||
h.backToDayView(ctx, chatID, msgID, user, eid, loc)
|
||||
return
|
||||
return true
|
||||
}
|
||||
// Add IN event — show hour picker
|
||||
if strings.HasPrefix(data, "addin_") {
|
||||
return false
|
||||
}
|
||||
|
||||
// handleHistoryEventAdd handles event-adding callbacks.
|
||||
func (h *Handler) handleHistoryEventAdd(ctx context.Context, chatID int64, msgID int, user *db.User, data string, loc *time.Location) bool {
|
||||
switch {
|
||||
case strings.HasPrefix(data, "addin_"):
|
||||
h.addEventTime(ctx, chatID, msgID, user, data[6:], "in", loc)
|
||||
return
|
||||
}
|
||||
// Add OUT event — show hour picker
|
||||
if strings.HasPrefix(data, "addout_") {
|
||||
case strings.HasPrefix(data, "addout_"):
|
||||
h.addEventTime(ctx, chatID, msgID, user, data[7:], "out", loc)
|
||||
return
|
||||
}
|
||||
// Add event hour/minute callbacks
|
||||
if strings.HasPrefix(data, "addtm_") {
|
||||
case strings.HasPrefix(data, "addtm_"):
|
||||
h.handleAddEventTime(ctx, chatID, msgID, user, data[6:], loc)
|
||||
return
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// sendCalendar sends a calendar view as a new message.
|
||||
@@ -245,7 +260,7 @@ func (h *Handler) editCalendar(ctx context.Context, chatID int64, msgID int, yea
|
||||
cm := buildCalendarMonth(cal, year, month)
|
||||
|
||||
text := cm.title
|
||||
todayKey := now.Format("2006-01-02")
|
||||
todayKey := now.Format(DateLayout)
|
||||
|
||||
hasEvent := make(map[string]bool)
|
||||
startDate, endDate := monthGregorianRange(cal, year, month)
|
||||
@@ -365,7 +380,7 @@ func (h *Handler) sendDayView(ctx context.Context, chatID int64, msgID int, user
|
||||
text += "\n\nEvents:"
|
||||
kbRows := [][]models.InlineKeyboardButton{}
|
||||
for _, e := range events {
|
||||
t := time.Unix(e.OccurredAt, 0).In(loc).Format("15:04")
|
||||
t := time.Unix(e.OccurredAt, 0).In(loc).Format(TimeLayout)
|
||||
label := "IN"
|
||||
if e.EventType == "out" {
|
||||
label = "OUT"
|
||||
@@ -435,7 +450,7 @@ func (h *Handler) setEventWorkType(ctx context.Context, chatID int64, msgID int,
|
||||
return
|
||||
}
|
||||
t := time.Unix(event.OccurredAt, 0).In(loc)
|
||||
h.sendDayView(ctx, chatID, msgID, user, t.Format("2006-01-02"), loc, false)
|
||||
h.sendDayView(ctx, chatID, msgID, user, t.Format(DateLayout), loc, false)
|
||||
}
|
||||
|
||||
// buildHourPickerKeyboard creates the 24-hour grid for time selection.
|
||||
@@ -488,14 +503,14 @@ func (h *Handler) editEventTimeMin(ctx context.Context, chatID int64, msgID int,
|
||||
}
|
||||
|
||||
// editEventTimeSet applies a new time to an event, checking for overlaps.
|
||||
func (h *Handler) editEventTimeSet(ctx context.Context, chatID int64, msgID int, user *db.User, eventID int64, hh, mm int, loc *time.Location) {
|
||||
func (h *Handler) editEventTimeSet(ctx context.Context, chatID int64, msgID int, user *db.User, eventID int64, hh, mm int, loc *time.Location, callbackID string) {
|
||||
event, err := h.getEventByID(user.ID, eventID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t := time.Unix(event.OccurredAt, 0).In(loc)
|
||||
newTime := time.Date(t.Year(), t.Month(), t.Day(), hh, mm, 0, 0, loc)
|
||||
date := t.Format("2006-01-02")
|
||||
date := t.Format(DateLayout)
|
||||
|
||||
// Reject if the new time collides with another event's timestamp
|
||||
events, err := h.DB.EventsForDayByDate(user.ID, date)
|
||||
@@ -506,7 +521,7 @@ func (h *Handler) editEventTimeSet(ctx context.Context, chatID int64, msgID int,
|
||||
}
|
||||
eTime := time.Unix(e.OccurredAt, 0)
|
||||
if newTime.Unix() == eTime.Unix() {
|
||||
h.acknowledgeCallback(ctx, chatID, msgID)
|
||||
h.acknowledgeCallback(ctx, callbackID)
|
||||
h.sendDayView(ctx, chatID, msgID, user, date, loc, false)
|
||||
return
|
||||
}
|
||||
@@ -552,8 +567,8 @@ func (h *Handler) editEventTimeSet(ctx context.Context, chatID int64, msgID int,
|
||||
}
|
||||
|
||||
// acknowledgeCallback sends an empty acknowledgement for a callback query.
|
||||
func (h *Handler) acknowledgeCallback(ctx context.Context, chatID int64, msgID int) {
|
||||
h.answerCb(ctx, fmt.Sprintf("cb_%d_%d", chatID, msgID))
|
||||
func (h *Handler) acknowledgeCallback(ctx context.Context, callbackID string) {
|
||||
h.answerCb(ctx, callbackID)
|
||||
}
|
||||
|
||||
// promptNote asks the user to type a note for the event, then re-renders the day view.
|
||||
@@ -562,7 +577,7 @@ func (h *Handler) promptNote(ctx context.Context, chatID int64, msgID int, user
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t := time.Unix(event.OccurredAt, 0).In(loc).Format("15:04")
|
||||
t := time.Unix(event.OccurredAt, 0).In(loc).Format(TimeLayout)
|
||||
|
||||
h.mu.Lock()
|
||||
h.pendingNotes[chatID] = &pendingNote{eventID: eventID, createdAt: time.Now()}
|
||||
@@ -596,7 +611,7 @@ func (h *Handler) deleteEventConfirm(ctx context.Context, chatID int64, msgID in
|
||||
return
|
||||
}
|
||||
t := time.Unix(event.OccurredAt, 0).In(loc)
|
||||
date := t.Format("2006-01-02")
|
||||
date := t.Format(DateLayout)
|
||||
|
||||
_, err = h.DB.DB().Exec("DELETE FROM events WHERE id=?", eventID)
|
||||
if err != nil {
|
||||
@@ -630,7 +645,7 @@ func (h *Handler) backToDayView(ctx context.Context, chatID int64, msgID int, us
|
||||
return
|
||||
}
|
||||
t := time.Unix(event.OccurredAt, 0).In(loc)
|
||||
h.sendDayView(ctx, chatID, msgID, user, t.Format("2006-01-02"), loc, false)
|
||||
h.sendDayView(ctx, chatID, msgID, user, t.Format(DateLayout), loc, false)
|
||||
}
|
||||
|
||||
// getEventByID fetches a single event by ID, scoped to the user.
|
||||
|
||||
Reference in New Issue
Block a user