feat: add /summary, /setbreak, weekly totals, and fix command dispatch for arg-bearing commands
- Add /summary [YYYY-MM] command for monthly work/break/day-off totals - Add weekly totals row to main menu status - Add /setbreak <minutes> for configurable daily break threshold - Fix HandleMessage routing: extract command name instead of exact match (fixes /export YYYY-MM, /edit, /note, /summary, /setbreak) - Update README with full command table and configurable break docs
This commit is contained in:
139
internal/bot/summary.go
Normal file
139
internal/bot/summary.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-telegram/bot/models"
|
||||
|
||||
"worktimeBot/internal/db"
|
||||
)
|
||||
|
||||
// handleSummaryMsg parses /summary [YYYY-MM] and sends a monthly summary.
|
||||
func (h *Handler) handleSummaryMsg(ctx context.Context, msg *models.Message) {
|
||||
user, err := h.getOrCreateUser(msg.Chat.ID)
|
||||
if err != nil {
|
||||
h.sendText(ctx, msg.Chat.ID, "Error loading profile")
|
||||
return
|
||||
}
|
||||
loc := loadLocation(user.Timezone)
|
||||
now := time.Now().In(loc)
|
||||
|
||||
args := strings.Fields(msg.Text)
|
||||
if len(args) > 0 && args[0][0] == '/' {
|
||||
args = args[1:]
|
||||
}
|
||||
|
||||
var calY, calM int
|
||||
if len(args) >= 1 {
|
||||
if _, err := fmt.Sscanf(args[0], "%d-%d", &calY, &calM); err != nil || calM < 1 || calM > 12 {
|
||||
h.sendText(ctx, msg.Chat.ID, "Usage: /summary [YYYY-MM] (e.g. /summary 2026-06)")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
calY, calM = now.Year(), int(now.Month())
|
||||
switch user.Calendar {
|
||||
case "jalali":
|
||||
calY, calM, _ = gregorianToJalali(calY, calM, now.Day())
|
||||
case "hijri":
|
||||
calY, calM, _ = gregorianToHijri(calY, calM, now.Day())
|
||||
}
|
||||
}
|
||||
|
||||
startStr, endStr := monthGregorianRange(user.Calendar, calY, calM)
|
||||
summary := h.buildMonthlySummary(user, calY, calM, startStr, endStr, loc)
|
||||
h.sendText(ctx, msg.Chat.ID, summary)
|
||||
}
|
||||
|
||||
// buildMonthlySummary returns a formatted summary for a calendar month.
|
||||
func (h *Handler) buildMonthlySummary(user *db.User, calY, calM int, startStr, endStr string, loc *time.Location) string {
|
||||
days, err := h.DB.GetUserDaysInRange(user.ID, startStr, endStr)
|
||||
if err != nil {
|
||||
return "Error loading days"
|
||||
}
|
||||
|
||||
title := formatMonthTitle(user.Calendar, calY, calM)
|
||||
|
||||
totalWork := int64(0)
|
||||
totalBreak := int64(0)
|
||||
dayOffCount := 0
|
||||
workDayCount := 0
|
||||
dayDetails := []string{}
|
||||
|
||||
for _, day := range days {
|
||||
events, err := h.DB.EventsForDayByDayID(day.ID)
|
||||
if err != nil || len(events) == 0 {
|
||||
continue
|
||||
}
|
||||
y, m, d := parseGregorianDateKey(day.Date)
|
||||
dayStart := time.Date(y, time.Month(m), d, 0, 0, 0, 0, loc).Unix()
|
||||
dayEnd := time.Date(y, time.Month(m), d, 23, 59, 59, 0, loc).Unix()
|
||||
totals := ComputeDailyTotals(events, day.MinBreakThreshold, dayStart, dayEnd)
|
||||
|
||||
dayLabel := formatDateForCalendar(day.Date, user.Calendar)
|
||||
if day.IsDayOff {
|
||||
dayOffCount++
|
||||
dayDetails = append(dayDetails, fmt.Sprintf(" %s - Day Off", dayLabel))
|
||||
} else if totals.TotalSeconds > 0 {
|
||||
workDayCount++
|
||||
totalWork += totals.TotalSeconds
|
||||
totalBreak += totals.BreakSeconds
|
||||
dayDetails = append(dayDetails, fmt.Sprintf(" %s - %s worked", dayLabel, formatDuration(totals.TotalSeconds)))
|
||||
}
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString(fmt.Sprintf("Summary for %s\n", title))
|
||||
b.WriteString(fmt.Sprintf("Work days: %d\n", workDayCount))
|
||||
if dayOffCount > 0 {
|
||||
b.WriteString(fmt.Sprintf("Days off: %d\n", dayOffCount))
|
||||
}
|
||||
b.WriteString(fmt.Sprintf("Total: %s worked", formatDuration(totalWork)))
|
||||
if totalBreak > 0 {
|
||||
b.WriteString(fmt.Sprintf(", %s break", formatDuration(totalBreak)))
|
||||
}
|
||||
b.WriteString("\n\nDetails:\n")
|
||||
for _, d := range dayDetails {
|
||||
b.WriteString(d)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// computeWeekTotals returns the total work seconds for the current week (Mon-Sun containing today).
|
||||
func (h *Handler) computeWeekTotals(user *db.User, loc *time.Location) int64 {
|
||||
now := time.Now().In(loc)
|
||||
weekday := now.Weekday()
|
||||
if weekday == time.Sunday {
|
||||
weekday = 7
|
||||
}
|
||||
monday := now.AddDate(0, 0, -int(weekday-time.Monday))
|
||||
sunday := monday.AddDate(0, 0, 6)
|
||||
|
||||
startStr := monday.Format("2006-01-02")
|
||||
endStr := sunday.Format("2006-01-02")
|
||||
|
||||
days, err := h.DB.GetUserDaysInRange(user.ID, startStr, endStr)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
var total int64
|
||||
for _, day := range days {
|
||||
if day.IsDayOff {
|
||||
continue
|
||||
}
|
||||
events, err := h.DB.EventsForDayByDayID(day.ID)
|
||||
if err != nil || len(events) == 0 {
|
||||
continue
|
||||
}
|
||||
y, m, d := parseGregorianDateKey(day.Date)
|
||||
dayStart := time.Date(y, time.Month(m), d, 0, 0, 0, 0, loc).Unix()
|
||||
dayEnd := time.Date(y, time.Month(m), d, 23, 59, 59, 0, loc).Unix()
|
||||
totals := ComputeDailyTotals(events, day.MinBreakThreshold, dayStart, dayEnd)
|
||||
total += totals.TotalSeconds
|
||||
}
|
||||
return total
|
||||
}
|
||||
Reference in New Issue
Block a user