Files
worktimeBot/internal/bot/report.go

188 lines
5.3 KiB
Go

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("\n %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)
}
}