package bot import ( "context" "fmt" "log/slog" "time" bot "github.com/go-telegram/bot" "worktimeBot/internal/db" ) // buildStatusText returns the main menu status text for the current user. func (h *Handler) buildStatusText(chatID int64) string { user, day, err := h.getUserToday(chatID) if err != nil { return "Welcome. Choose an action:" } text := fmt.Sprintf("Today: %s", formatDateForCalendar(day.Date, user.Calendar)) if day.IsDayOff { text += "\nDay Off" } wt, _ := h.DB.GetWorkType(day.CurrentWorkTypeID) if wt != nil { text += fmt.Sprintf("\nWork type: %s", wt.Name) } last, err := h.DB.GetLastEvent(user.ID) if err == nil && last != nil && last.EventType == "in" { loc := loadLocation(user.Timezone) t := time.Unix(last.OccurredAt, 0).In(loc).Format(TimeLayout) text += fmt.Sprintf("\nStatus: working since %s", t) } else { text += "\nStatus: not working" } events, err := h.DB.EventsForDayByDayID(day.ID) if err == nil && len(events) > 0 { loc := loadLocation(user.Timezone) now := time.Now().In(loc) y, m, d := now.Date() dayStart := time.Date(y, m, d, 0, 0, 0, 0, loc).Unix() dayEnd := now.Unix() totals := ComputeDailyTotals(events, day.MinBreakThreshold, dayStart, dayEnd) text += fmt.Sprintf("\nToday: %s worked", formatDuration(totals.TotalSeconds)) } // Salary info if configured st, _ := h.DB.GetOrCreateSettings(user.ID) if st.HourlyRate > 0 { if events, err := h.DB.EventsForDayByDayID(day.ID); err == nil && len(events) > 0 { loc := loadLocation(user.Timezone) now := time.Now().In(loc) y, m, d := now.Date() dayStart := time.Date(y, m, d, 0, 0, 0, 0, loc).Unix() dayEnd := now.Unix() totals := ComputeDailyTotals(events, day.MinBreakThreshold, dayStart, dayEnd) salLine := h.buildSalaryStatus(user.ID, totals.TotalSeconds) if salLine != "" { text += salLine } } } // RPG info if enabled if st.RPGEnabled { stats, _ := h.DB.GetOrCreateRPGStats(user.ID) if stats != nil { text += fmt.Sprintf("\nLevel: %d | XP: %d", stats.Level, stats.XP) rankText := h.buildLeagueRankText(user.ID) if rankText != "" { text += fmt.Sprintf(" | %s", rankText) } } } text += "\n\nChoose an action:" return text } // backToMenu returns to the main menu from any sub-menu. func (h *Handler) backToMenu(ctx context.Context, chatID int64, msgID int, callbackID string) { defer h.answerCb(ctx, callbackID) kb := mainKeyboard() h.editText(ctx, chatID, msgID, h.buildStatusText(chatID), &kb) } // buildDailyReport builds a formatted daily report string for a given user/day. func (h *Handler) buildDailyReport(user *db.User, day *db.Day, events []db.Event) string { loc, err := time.LoadLocation(user.Timezone) if err != nil { loc = time.UTC } if len(events) == 0 { if day.IsDayOff { return fmt.Sprintf("%s - Day Off", formatDateForCalendar(day.Date, user.Calendar)) } return fmt.Sprintf("%s - No events recorded.", formatDateForCalendar(day.Date, user.Calendar)) } y, m, d := parseGregorianDateKey(day.Date) dayStart := time.Date(y, time.Month(m), d, 0, 0, 0, 0, loc).Unix() now := time.Now().In(loc) todayStr := now.Format(DateLayout) dayEnd := time.Date(y, time.Month(m), d, 23, 59, 59, 0, loc).Unix() if day.Date == todayStr { dayEnd = now.Unix() } totals := ComputeDailyTotals(events, day.MinBreakThreshold, dayStart, dayEnd) lines := fmt.Sprintf("Report for %s", formatDateForCalendar(day.Date, user.Calendar)) if day.IsDayOff { lines += "\nDay Off: Yes" } lines += "\n\n" for _, e := range events { t := time.Unix(e.OccurredAt, 0).In(loc).Format(TimeLayout) label := "IN" if e.EventType == "out" { label = "OUT" } suffix := "" if e.WorkTypeID != nil { if wtObj, err := h.DB.GetWorkType(*e.WorkTypeID); err == nil { suffix = " [" + wtObj.Name + "]" } } if e.Note != "" { suffix += " (" + e.Note + ")" } lines += fmt.Sprintf("%s %s%s\n", label, t, suffix) } lines += fmt.Sprintf("\nSummary:\n Work: %s\n Break: %s\n", formatDuration(totals.TotalSeconds), formatDuration(totals.BreakSeconds)) // Salary info if configured st, _ := h.DB.GetOrCreateSettings(user.ID) if st.HourlyRate > 0 { est := computeDailySalary(totals.TotalSeconds, st.HourlyRate) lines += fmt.Sprintf(" Salary: %s\n", SalaryEstimate{GrossEarning: est}.formatSalary(st.Currency)) } return lines } // SendDailyReport sends the automated daily report to the user. func (h *Handler) SendDailyReport(ctx context.Context, userID int64, chatID int64) { user, err := h.DB.GetUserByChatID(chatID) if err != nil { slog.Error("daily report: user not found", "user_id", userID) return } loc, err := time.LoadLocation(user.Timezone) if err != nil { loc = time.UTC } now := time.Now().In(loc) today := now.Format(DateLayout) day, err := h.DB.GetOrCreateDay(user.ID, today) if err != nil { slog.Error("daily report: get day", "error", err) return } events, err := h.DB.EventsForDayByDayID(day.ID) if err != nil { slog.Error("daily report: get events", "error", err) return } text := h.buildDailyReport(user, day, events) _, err = h.Bot.SendMessage(ctx, &bot.SendMessageParams{ChatID: chatID, Text: text}) if err != nil { slog.Error("daily report: send message failed", "chat_id", chatID, "error", err) return } if err := h.DB.MarkReportSent(user.ID, today); err != nil { slog.Error("daily report: mark sent failed", "chat_id", chatID, "error", err) } }