package handler import ( "context" "fmt" "log/slog" "time" tgbot "github.com/go-telegram/bot" "worktimeBot/internal/domain" ) func (h *Handler) buildStatusText(user *domain.User) string { loc := h.loadLocation(user.Timezone) now := time.Now().In(loc) today := now.Format(domain.DateLayout) day, err := h.Repo.GetOrCreateDay(user.ID, today) if err != nil { return "Welcome. Choose an action:" } text := fmt.Sprintf("Today: %s", domain.FormatDateForCalendar(day.Date, user.Calendar)) if day.IsDayOff { text += "\nDay Off" } wt, _ := h.Repo.GetWorkType(day.CurrentWorkTypeID) if wt != nil { text += fmt.Sprintf("\nWork type: %s", wt.Name) } last, err := h.Repo.GetLastEvent(user.ID) if err == nil && last != nil && last.EventType == "in" { t := time.Unix(last.OccurredAt, 0).In(loc).Format(domain.TimeLayout) text += fmt.Sprintf("\nStatus: working since %s", t) } else { text += "\nStatus: not working" } events, err := h.Repo.EventsForDayByDayID(day.ID) if err == nil && len(events) > 0 { y, m, d := now.Date() dayStart := time.Date(y, m, d, 0, 0, 0, 0, loc).Unix() totals := domain.ComputeDailyTotals(events, day.MinBreakThreshold, dayStart, now.Unix()) text += fmt.Sprintf("\nToday: %s worked", formatDuration(totals.TotalSeconds)) } st, _ := h.Repo.GetOrCreateSettings(user.ID) if st != nil && st.HourlyRate > 0 { if events, err := h.Repo.EventsForDayByDayID(day.ID); err == nil && len(events) > 0 { y, m, d := now.Date() dayStart := time.Date(y, m, d, 0, 0, 0, 0, loc).Unix() totals := domain.ComputeDailyTotals(events, day.MinBreakThreshold, dayStart, now.Unix()) sal := h.buildSalaryStatus(user, totals.TotalSeconds) if sal != "" { text += sal } } } if st != nil && st.RPGEnabled { stats, _ := h.Repo.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 += "\n " + rankText } } } text += "\n\nChoose an action:" return text } func (h *Handler) buildDailyReport(user *domain.User, day *domain.Day, events []domain.Event) string { loc := h.loadLocation(user.Timezone) if len(events) == 0 { if day.IsDayOff { return fmt.Sprintf("%s - Day Off", domain.FormatDateForCalendar(day.Date, user.Calendar)) } return fmt.Sprintf("%s - No events recorded.", domain.FormatDateForCalendar(day.Date, user.Calendar)) } y, m, d := domain.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(domain.DateLayout) dayEnd := time.Date(y, time.Month(m), d, 23, 59, 59, 0, loc).Unix() if day.Date == todayStr { dayEnd = now.Unix() } totals := domain.ComputeDailyTotals(events, day.MinBreakThreshold, dayStart, dayEnd) lines := fmt.Sprintf("Report for %s", domain.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(domain.TimeLayout) label := "IN" if e.EventType == "out" { label = "OUT" } suffix := "" if e.WorkTypeID != nil { if wtObj, err := h.Repo.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)) st, _ := h.Repo.GetOrCreateSettings(user.ID) if st != nil && st.HourlyRate > 0 { est := domain.ComputeDailySalary(totals.TotalSeconds, st.HourlyRate) s := domain.SalaryEstimate{WorkSeconds: totals.TotalSeconds, GrossEarning: est} lines += fmt.Sprintf(" Salary: %s\n", s.FormatSalaryShort(st.Currency)) } return lines } func (h *Handler) buildSalaryStatus(user *domain.User, workSeconds int64) string { st, err := h.Repo.GetOrCreateSettings(user.ID) if err != nil || st.HourlyRate <= 0 { return "" } est := domain.ComputeDailySalary(workSeconds, st.HourlyRate) s := domain.SalaryEstimate{WorkSeconds: workSeconds, GrossEarning: est, HourlyRate: st.HourlyRate} return fmt.Sprintf("\nSalary: %s (%.2f/hr)", s.FormatSalaryShort(st.Currency), st.HourlyRate) } func (h *Handler) backToMenu(ctx context.Context, chatID int64, msgID int, cbID string) { defer h.answerCb(ctx, cbID) user, err := h.getOrCreateUser(chatID) if err != nil { h.editText(ctx, chatID, msgID, "Error loading profile", nil) return } kb := mainKeyboard() h.editText(ctx, chatID, msgID, h.buildStatusText(user), &kb) } // SendDailyReport sends a daily report to a specific user (called by scheduler). func (h *Handler) SendDailyReport(ctx context.Context, userID int64, chatID int64) { user, err := h.Repo.GetOrCreateUser(chatID) if err != nil { slog.Error("SendDailyReport: get user", "chat_id", chatID, "error", err) return } loc := h.loadLocation(user.Timezone) today := time.Now().In(loc).Format(domain.DateLayout) day, err := h.Repo.GetOrCreateDay(user.ID, today) if err != nil { slog.Error("SendDailyReport: get day", "chat_id", chatID, "error", err) return } events, err := h.Repo.EventsForDayByDayID(day.ID) if err != nil { slog.Error("SendDailyReport: events", "chat_id", chatID, "error", err) return } text := h.buildDailyReport(user, day, events) if _, err := h.Bot.SendMessage(ctx, &tgbot.SendMessageParams{ChatID: chatID, Text: text}); err != nil { slog.Warn("SendDailyReport: send", "chat_id", chatID, "error", err) } }