fix: resolve go vet error and dayStart logic in ComputeDailyTotals
- Fix 'string(d)' int-to-rune conversion in rpg_test.go by using fmt.Sprintf - Fix ComputeDailyTotals dayStart logic: initial state should be StateWorking when dayStart > 0, correctly counting work from dayStart to first out event - Update README project structure to reflect new domain/handler/repo layout - Implement missing achievements: rpg_pioneer (on RPG enable) and salary_setter (on first rate set) - Add tryUnlockAchievement helper to Handler
This commit is contained in:
169
internal/handler/report.go
Normal file
169
internal/handler/report.go
Normal file
@@ -0,0 +1,169 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user