fix: calendar day-of-week offset off by one, nav double-jump, race conditions

- Fix Jalali/Gregorian/Hijri day-of-week startOffset formulas
  (dow returns 0=Sat, not 0=Sun as the old comment claimed)
- Fix calendar and export month picker navigation jumping 2 months
  (navMonth buttons already have pre-computed values; handlers
  were decrementing/incrementing them again)
- Fix GetOrCreateUser/GetOrCreateDay race condition with INSERT OR IGNORE
- Add month/day bounds validation in handleEditMsg to prevent panic
This commit is contained in:
2026-06-24 11:07:53 +03:30
parent 577aee91fb
commit f89f5607aa
3 changed files with 83 additions and 52 deletions

View File

@@ -403,7 +403,7 @@ func (h *Handler) handleHelp(msg *tgbotapi.Message) {
/worktype - Select work type
/dayoff - Toggle day off
/report - Show today report
/export - Export monthly report (Excel)
/export [YYYY-MM] - Export monthly report (current month if omitted)
/history - View calendar history
/edit YYYY-MM-DD - Edit a specific day
/reporttoggle - Enable/disable auto report
@@ -411,6 +411,7 @@ func (h *Handler) handleHelp(msg *tgbotapi.Message) {
/setaccent - Select accent color
/settimezone <name> - Set your timezone (e.g. Asia/Tehran)
Dates use your calendar type (Gregorian/Jalali/Hijri).
Buttons on the main menu also work.`
h.sendText(msg.Chat.ID, text)
}
@@ -708,11 +709,16 @@ func (h *Handler) historyCallback(chatID int64, msgID int, callbackID string) {
func (h *Handler) handleEditMsg(msg *tgbotapi.Message) {
date := msg.CommandArguments()
if date == "" {
h.sendText(msg.Chat.ID, "Usage: /edit YYYY-MM-DD")
h.sendText(msg.Chat.ID, "Usage: /edit YYYY-MM-DD (your calendar type)")
return
}
if len(date) != 10 || date[4] != '-' || date[7] != '-' {
h.sendText(msg.Chat.ID, "Invalid date. Use: /edit YYYY-MM-DD")
h.sendText(msg.Chat.ID, "Invalid date. Use: /edit YYYY-MM-DD (your calendar type)")
return
}
_, m, d := parseGregorianDateKey(date)
if m < 1 || m > 12 || d < 1 || d > 31 {
h.sendText(msg.Chat.ID, "Invalid date.")
return
}
user, err := h.getOrCreateUser(msg.Chat.ID)
@@ -721,6 +727,7 @@ func (h *Handler) handleEditMsg(msg *tgbotapi.Message) {
return
}
loc, _ := time.LoadLocation(user.Timezone)
date = userDateToGregorian(date, user.Calendar)
h.sendDayView(msg.Chat.ID, 0, user, date, loc, true)
}
@@ -743,21 +750,11 @@ func (h *Handler) handleHistoryCallback(chatID int64, msgID int, callbackID, dat
// cal_prev_YYYY_M
if n, _ := fmt.Sscanf(data, "cal_prev_%d_%d", &y, &m); n == 2 {
m--
if m < 1 {
m = 12
y--
}
h.editCalendar(chatID, msgID, y, m)
return
}
// cal_next_YYYY_M
if n, _ := fmt.Sscanf(data, "cal_next_%d_%d", &y, &m); n == 2 {
m++
if m > 12 {
m = 1
y++
}
h.editCalendar(chatID, msgID, y, m)
return
}
@@ -1189,14 +1186,45 @@ func (h *Handler) handleExport(msg *tgbotapi.Message) {
}
loc, _ := time.LoadLocation(user.Timezone)
now := time.Now().In(loc)
cy, cm := now.Year(), int(now.Month())
switch user.Calendar {
case "jalali":
cy, cm, _ = gregorianToJalali(cy, cm, now.Day())
case "hijri":
cy, cm, _ = gregorianToHijri(cy, cm, now.Day())
// Parse optional YYYY-MM argument (in user's calendar)
gy, gm := now.Year(), int(now.Month())
if len(msg.Text) > 7 {
if n, _ := fmt.Sscanf(msg.Text[7:], "%d-%d", &gy, &gm); n == 2 {
if gy < 0 || gm < 1 || gm > 12 {
h.sendText(msg.Chat.ID, "Invalid date. Use: /export YYYY-MM")
return
}
gy, gm = userYearMonthToGregorian(gy, gm, user.Calendar)
}
}
h.sendExportDirect(msg.Chat.ID, user, gy, gm)
}
func (h *Handler) sendExportDirect(chatID int64, user *db.User, gy, gm int) {
// Check if clocked in
last, err := h.DB.GetLastEvent(user.ID)
if err == nil && last != nil && last.EventType == "in" {
h.sendText(chatID, "You are currently clocked in. Please clock out first, then try again.")
return
}
slog.Info("export", "user_id", user.ID, "year", gy, "month", gm)
data, err := GenerateMonthlyReport(h.DB, user.ID, user.Timezone, user.ExportAccent, user.Calendar, gy, time.Month(gm))
if err != nil {
slog.Error("generate report", "error", err)
h.sendText(chatID, "Error generating report")
return
}
slog.Info("report generated", "bytes", len(data))
doc := tgbotapi.NewDocument(chatID, tgbotapi.FileBytes{
Name: fmt.Sprintf("worktime_%s.xlsx", fmt.Sprintf("%04d_%02d", gy, gm)),
Bytes: data,
})
if _, err := h.Bot.Send(doc); err != nil {
slog.Error("send document", "error", err)
}
h.sendExportMonthPicker(msg.Chat.ID, 0, cy, cm, user)
}
func (h *Handler) exportCallback(chatID int64, msgID int, callbackID string) {
@@ -1262,20 +1290,10 @@ func (h *Handler) handleExportCallback(chatID int64, msgID int, callbackID, data
var y, m int
if n, _ := fmt.Sscanf(data, "exp_prev_%d_%d", &y, &m); n == 2 {
m--
if m < 1 {
m = 12
y--
}
h.editExportMonthPicker(chatID, msgID, y, m, user)
return
}
if n, _ := fmt.Sscanf(data, "exp_next_%d_%d", &y, &m); n == 2 {
m++
if m > 12 {
m = 1
y++
}
h.editExportMonthPicker(chatID, msgID, y, m, user)
return
}