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:
531
internal/handler/export.go
Normal file
531
internal/handler/export.go
Normal file
@@ -0,0 +1,531 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tgbot "github.com/go-telegram/bot"
|
||||
"github.com/go-telegram/bot/models"
|
||||
"github.com/xuri/excelize/v2"
|
||||
|
||||
"worktimeBot/internal/domain"
|
||||
)
|
||||
|
||||
type themeColors struct {
|
||||
headerFill string
|
||||
border string
|
||||
totalFill string
|
||||
}
|
||||
|
||||
var themes = map[string]themeColors{
|
||||
"ocean": {headerFill: "4472C4", border: "D9D9D9", totalFill: "D9E2F3"},
|
||||
"beach": {headerFill: "C87D3C", border: "E8D5B7", totalFill: "FFF0E0"},
|
||||
"rose": {headerFill: "B86C80", border: "F0D0E0", totalFill: "FCE8F0"},
|
||||
"catppuccin": {headerFill: "B48DED", border: "D9D0E8", totalFill: "F0E8FC"},
|
||||
}
|
||||
|
||||
type reportStyles struct {
|
||||
title int
|
||||
header int
|
||||
data int
|
||||
total int
|
||||
dayOff int
|
||||
}
|
||||
|
||||
func newReportStyles(f *excelize.File, theme themeColors) reportStyles {
|
||||
return reportStyles{
|
||||
title: newStyle(f, &excelize.Style{
|
||||
Font: &excelize.Font{Bold: true, Size: 14, Color: "FFFFFF"},
|
||||
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{theme.headerFill}},
|
||||
Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center"},
|
||||
}),
|
||||
header: newStyle(f, &excelize.Style{
|
||||
Font: &excelize.Font{Bold: true, Color: "FFFFFF"},
|
||||
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{theme.headerFill}},
|
||||
Border: []excelize.Border{
|
||||
{Type: "left", Color: "FFFFFF", Style: 1},
|
||||
{Type: "right", Color: "FFFFFF", Style: 1},
|
||||
{Type: "top", Color: "FFFFFF", Style: 1},
|
||||
{Type: "bottom", Color: "FFFFFF", Style: 1},
|
||||
},
|
||||
Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center"},
|
||||
}),
|
||||
data: newStyle(f, &excelize.Style{
|
||||
Border: []excelize.Border{
|
||||
{Type: "left", Color: theme.border, Style: 1},
|
||||
{Type: "right", Color: theme.border, Style: 1},
|
||||
{Type: "top", Color: theme.border, Style: 1},
|
||||
{Type: "bottom", Color: theme.border, Style: 1},
|
||||
},
|
||||
Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center"},
|
||||
}),
|
||||
total: newStyle(f, &excelize.Style{
|
||||
Font: &excelize.Font{Bold: true, Size: 11},
|
||||
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{theme.totalFill}},
|
||||
Border: []excelize.Border{
|
||||
{Type: "left", Color: theme.border, Style: 1},
|
||||
{Type: "right", Color: theme.border, Style: 1},
|
||||
{Type: "top", Color: theme.border, Style: 1},
|
||||
{Type: "bottom", Color: theme.border, Style: 2},
|
||||
},
|
||||
Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center"},
|
||||
}),
|
||||
dayOff: newStyle(f, &excelize.Style{
|
||||
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{theme.totalFill}},
|
||||
Border: []excelize.Border{
|
||||
{Type: "left", Color: theme.border, Style: 1},
|
||||
{Type: "right", Color: theme.border, Style: 1},
|
||||
{Type: "top", Color: theme.border, Style: 1},
|
||||
{Type: "bottom", Color: theme.border, Style: 1},
|
||||
},
|
||||
Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center"},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func newStyle(f *excelize.File, s *excelize.Style) int {
|
||||
id, _ := f.NewStyle(s)
|
||||
return id
|
||||
}
|
||||
|
||||
func (h *Handler) generateMonthlyReport(user *domain.User, calYear, calMonth int, start, end time.Time) ([]byte, error) {
|
||||
st, err := h.Repo.GetOrCreateSettings(user.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accent := st.ExportAccent
|
||||
theme, ok := themes[accent]
|
||||
if !ok {
|
||||
theme = themes["ocean"]
|
||||
}
|
||||
|
||||
f := excelize.NewFile()
|
||||
defer f.Close()
|
||||
|
||||
sheet := "Report"
|
||||
index, err := f.NewSheet(sheet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.SetActiveSheet(index)
|
||||
f.DeleteSheet("Sheet1")
|
||||
|
||||
styles := newReportStyles(f, theme)
|
||||
monthName := domain.FormatMonthTitle(user.Calendar, calYear, calMonth)
|
||||
h.writeReportHeader(f, sheet, fmt.Sprintf("Work Time Report - %s", monthName), styles)
|
||||
|
||||
loc := h.loadLocation(user.Timezone)
|
||||
row := 3
|
||||
|
||||
userDays, err := h.Repo.GetUserDaysInRange(user.ID, start.Format(domain.DateLayout), end.Format(domain.DateLayout))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dayMap := make(map[string]*domain.Day)
|
||||
for i := range userDays {
|
||||
dayMap[userDays[i].Date] = &userDays[i]
|
||||
}
|
||||
|
||||
var totalWork, totalBreak int64
|
||||
layout := start.Format(domain.DateLayout)
|
||||
endLayout := end.Format(domain.DateLayout)
|
||||
|
||||
cur := start
|
||||
for !cur.After(end) {
|
||||
dateStr := cur.Format(domain.DateLayout)
|
||||
displayDate := domain.FormatDateForCalendar(dateStr, user.Calendar)
|
||||
|
||||
var (
|
||||
day *domain.Day
|
||||
events []domain.Event
|
||||
hasDayOff bool
|
||||
)
|
||||
if existing, ok := dayMap[dateStr]; ok {
|
||||
day = existing
|
||||
events, err = h.Repo.EventsForDayByDayID(day.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hasDayOff = day.IsDayOff
|
||||
}
|
||||
|
||||
var typeLabel string
|
||||
if hasDayOff {
|
||||
if day.DayOffReason != "" {
|
||||
typeLabel = "Day Off (" + day.DayOffReason + ")"
|
||||
} else {
|
||||
typeLabel = "Day Off"
|
||||
}
|
||||
} else if len(events) > 0 {
|
||||
typeLabel = h.workTypeLabel(day, events)
|
||||
}
|
||||
|
||||
ws, bs := h.writeReportDayRow(f, sheet, row, displayDate, typeLabel, events, day, loc, hasDayOff, styles)
|
||||
totalWork += ws
|
||||
totalBreak += bs
|
||||
row++
|
||||
cur = cur.AddDate(0, 0, 1)
|
||||
}
|
||||
|
||||
_ = layout
|
||||
_ = endLayout
|
||||
|
||||
h.writeReportTotalRow(f, sheet, row, totalWork, totalBreak, styles)
|
||||
|
||||
buf, err := f.WriteToBuffer()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (h *Handler) writeReportHeader(f *excelize.File, sheet, title string, s reportStyles) {
|
||||
f.SetCellValue(sheet, "A1", title)
|
||||
f.MergeCell(sheet, "A1", "F1")
|
||||
f.SetCellStyle(sheet, "A1", "F1", s.title)
|
||||
f.SetRowHeight(sheet, 1, 30)
|
||||
|
||||
headers := []string{"Date", "Clock In", "Clock Out", "Type", "Work", "Break"}
|
||||
for i, hdr := range headers {
|
||||
cell, _ := excelize.CoordinatesToCellName(i+1, 2)
|
||||
f.SetCellValue(sheet, cell, hdr)
|
||||
f.SetCellStyle(sheet, cell, cell, s.header)
|
||||
}
|
||||
f.SetRowHeight(sheet, 2, 22)
|
||||
|
||||
f.SetColWidth(sheet, "A", "A", 14)
|
||||
f.SetColWidth(sheet, "B", "B", 10)
|
||||
f.SetColWidth(sheet, "C", "C", 10)
|
||||
f.SetColWidth(sheet, "D", "D", 12)
|
||||
f.SetColWidth(sheet, "E", "E", 10)
|
||||
f.SetColWidth(sheet, "F", "F", 10)
|
||||
}
|
||||
|
||||
func (h *Handler) writeReportDayRow(f *excelize.File, sheet string, row int, displayDate, typeLabel string, events []domain.Event, day *domain.Day, loc *time.Location, hasDayOff bool, s reportStyles) (workSec, breakSec int64) {
|
||||
cellDate, _ := excelize.CoordinatesToCellName(1, row)
|
||||
f.SetCellValue(sheet, cellDate, displayDate)
|
||||
f.SetCellStyle(sheet, cellDate, cellDate, s.data)
|
||||
|
||||
if len(events) > 0 {
|
||||
cellIn, _ := excelize.CoordinatesToCellName(2, row)
|
||||
f.SetCellValue(sheet, cellIn, time.Unix(events[0].OccurredAt, 0).In(loc).Format(domain.TimeLayout))
|
||||
f.SetCellStyle(sheet, cellIn, cellIn, s.data)
|
||||
}
|
||||
if len(events) > 1 {
|
||||
lastEvent := events[len(events)-1]
|
||||
if lastEvent.EventType == "out" {
|
||||
cellOut, _ := excelize.CoordinatesToCellName(3, row)
|
||||
f.SetCellValue(sheet, cellOut, time.Unix(lastEvent.OccurredAt, 0).In(loc).Format(domain.TimeLayout))
|
||||
f.SetCellStyle(sheet, cellOut, cellOut, s.data)
|
||||
}
|
||||
}
|
||||
|
||||
cellType, _ := excelize.CoordinatesToCellName(4, row)
|
||||
f.SetCellValue(sheet, cellType, typeLabel)
|
||||
f.SetCellStyle(sheet, cellType, cellType, s.data)
|
||||
|
||||
if len(events) > 0 && day != nil {
|
||||
d, err := time.Parse(domain.DateLayout, day.Date)
|
||||
if err == nil {
|
||||
dayStart := time.Date(d.Year(), d.Month(), d.Day(), 0, 0, 0, 0, loc).Unix()
|
||||
dayEnd := time.Date(d.Year(), d.Month(), d.Day(), 23, 59, 59, 0, loc).Unix()
|
||||
totals := domain.ComputeDailyTotals(events, day.MinBreakThreshold, dayStart, dayEnd)
|
||||
workSec = totals.TotalSeconds
|
||||
breakSec = totals.BreakSeconds
|
||||
}
|
||||
}
|
||||
|
||||
cellWork, _ := excelize.CoordinatesToCellName(5, row)
|
||||
f.SetCellValue(sheet, cellWork, fmtHHMM(workSec))
|
||||
f.SetCellStyle(sheet, cellWork, cellWork, s.data)
|
||||
|
||||
cellBreak, _ := excelize.CoordinatesToCellName(6, row)
|
||||
f.SetCellValue(sheet, cellBreak, fmtHHMM(breakSec))
|
||||
f.SetCellStyle(sheet, cellBreak, cellBreak, s.data)
|
||||
|
||||
if hasDayOff {
|
||||
cellA, _ := excelize.CoordinatesToCellName(1, row)
|
||||
cellF, _ := excelize.CoordinatesToCellName(6, row)
|
||||
f.SetCellStyle(sheet, cellA, cellF, s.dayOff)
|
||||
}
|
||||
|
||||
return workSec, breakSec
|
||||
}
|
||||
|
||||
func (h *Handler) writeReportTotalRow(f *excelize.File, sheet string, row int, totalWork, totalBreak int64, s reportStyles) {
|
||||
row++
|
||||
f.SetCellValue(sheet, fmt.Sprintf("A%d", row), "Total")
|
||||
f.SetCellStyle(sheet, fmt.Sprintf("A%d", row), fmt.Sprintf("A%d", row), s.total)
|
||||
f.SetCellValue(sheet, fmt.Sprintf("E%d", row), fmtDDHHMM(totalWork))
|
||||
f.SetCellStyle(sheet, fmt.Sprintf("E%d", row), fmt.Sprintf("E%d", row), s.total)
|
||||
f.SetCellValue(sheet, fmt.Sprintf("F%d", row), fmtDDHHMM(totalBreak))
|
||||
f.SetCellStyle(sheet, fmt.Sprintf("F%d", row), fmt.Sprintf("F%d", row), s.total)
|
||||
}
|
||||
|
||||
func fmtHHMM(seconds int64) string {
|
||||
hours := seconds / domain.SecondsPerHour
|
||||
mins := (seconds % domain.SecondsPerHour) / 60
|
||||
return fmt.Sprintf("%d:%02d", hours, mins)
|
||||
}
|
||||
|
||||
func fmtDDHHMM(seconds int64) string {
|
||||
days := seconds / domain.SecondsPerDay
|
||||
rem := seconds % domain.SecondsPerDay
|
||||
hours := rem / domain.SecondsPerHour
|
||||
mins := (rem % domain.SecondsPerHour) / 60
|
||||
return fmt.Sprintf("%d:%02d:%02d", days, hours, mins)
|
||||
}
|
||||
|
||||
func (h *Handler) workTypeLabel(day *domain.Day, events []domain.Event) string {
|
||||
wtSeen := make(map[int64]bool)
|
||||
for _, e := range events {
|
||||
if e.WorkTypeID != nil {
|
||||
wtSeen[*e.WorkTypeID] = true
|
||||
}
|
||||
}
|
||||
if len(wtSeen) == 0 {
|
||||
wt, err := h.Repo.GetWorkType(day.CurrentWorkTypeID)
|
||||
if err == nil {
|
||||
return wt.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
if len(wtSeen) == 1 {
|
||||
for id := range wtSeen {
|
||||
wt, err := h.Repo.GetWorkType(id)
|
||||
if err == nil {
|
||||
return wt.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
return "mixed"
|
||||
}
|
||||
|
||||
// -- Commands --
|
||||
|
||||
func (h *Handler) handleExport(ctx context.Context, msg *models.Message, user *domain.User) {
|
||||
loc := h.loadLocation(user.Timezone)
|
||||
now := time.Now().In(loc)
|
||||
|
||||
calY, calM := now.Year(), int(now.Month())
|
||||
switch user.Calendar {
|
||||
case "jalali":
|
||||
calY, calM, _ = domain.GregorianToJalali(calY, calM, now.Day())
|
||||
case "hijri":
|
||||
calY, calM, _ = domain.GregorianToHijri(calY, calM, now.Day())
|
||||
}
|
||||
|
||||
args := strings.Fields(msg.Text)
|
||||
if len(args) >= 1 && args[0][0] == '/' {
|
||||
args = args[1:]
|
||||
}
|
||||
if len(args) >= 1 {
|
||||
if n, _ := fmt.Sscanf(args[0], "%d-%d", &calY, &calM); n == 2 {
|
||||
if calY < 0 || calM < 1 || calM > 12 {
|
||||
h.sendText(ctx, msg.Chat.ID, "Invalid date. Use: /export YYYY-MM")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h.sendExportDirect(ctx, msg.Chat.ID, user, calY, calM)
|
||||
}
|
||||
|
||||
func (h *Handler) sendExportDirect(ctx context.Context, chatID int64, user *domain.User, calYear, calMonth int) {
|
||||
last, err := h.Repo.GetLastEvent(user.ID)
|
||||
if err == nil && last != nil && last.EventType == "in" {
|
||||
h.sendText(ctx, chatID, "You are currently clocked in. Please clock out first, then try again.")
|
||||
return
|
||||
}
|
||||
|
||||
loc := h.loadLocation(user.Timezone)
|
||||
startStr, endStr := domain.MonthGregorianRange(user.Calendar, calYear, calMonth)
|
||||
start, err := time.Parse(domain.DateLayout, startStr)
|
||||
if err != nil {
|
||||
start = time.Date(calYear, time.Month(calMonth), 1, 0, 0, 0, 0, loc)
|
||||
}
|
||||
end, err := time.Parse(domain.DateLayout, endStr)
|
||||
if err != nil {
|
||||
startOfMonth := time.Date(calYear, time.Month(calMonth), 1, 0, 0, 0, 0, loc)
|
||||
end = startOfMonth.AddDate(0, 1, -1)
|
||||
}
|
||||
|
||||
slog.Info("export", "user_id", user.ID, "cal", user.Calendar, "cal_y", calYear, "cal_m", calMonth, "from", startStr, "to", endStr)
|
||||
data, err := h.generateMonthlyReport(user, calYear, calMonth, start, end)
|
||||
if err != nil {
|
||||
slog.Error("generate report", "error", err)
|
||||
h.sendText(ctx, chatID, "Error generating report")
|
||||
return
|
||||
}
|
||||
slog.Info("report generated", "bytes", len(data))
|
||||
if _, err := h.Bot.SendDocument(ctx, &tgbot.SendDocumentParams{
|
||||
ChatID: chatID,
|
||||
Document: &models.InputFileUpload{
|
||||
Filename: fmt.Sprintf("worktime_%s.xlsx", fmt.Sprintf("%04d_%02d", calYear, calMonth)),
|
||||
Data: bytes.NewReader(data),
|
||||
},
|
||||
}); err != nil {
|
||||
slog.Error("send document", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// -- Callbacks --
|
||||
|
||||
func (h *Handler) exportCallback(ctx context.Context, chatID int64, msgID int, cbID string, user *domain.User) {
|
||||
defer h.answerCb(ctx, cbID)
|
||||
loc := h.loadLocation(user.Timezone)
|
||||
now := time.Now().In(loc)
|
||||
cy, cm := now.Year(), int(now.Month())
|
||||
switch user.Calendar {
|
||||
case "jalali":
|
||||
cy, cm, _ = domain.GregorianToJalali(cy, cm, now.Day())
|
||||
case "hijri":
|
||||
cy, cm, _ = domain.GregorianToHijri(cy, cm, now.Day())
|
||||
}
|
||||
h.editExportMonthPicker(ctx, chatID, msgID, cy, cm, user)
|
||||
}
|
||||
|
||||
func (h *Handler) editExportMonthPicker(ctx context.Context, chatID int64, msgID int, year, _ int, user *domain.User) {
|
||||
months := domain.GregMonthNames
|
||||
switch user.Calendar {
|
||||
case "jalali":
|
||||
months = domain.JalaliMonthNames
|
||||
case "hijri":
|
||||
months = domain.HijriMonthNames
|
||||
}
|
||||
|
||||
kbRows := [][]models.InlineKeyboardButton{
|
||||
{
|
||||
{Text: "<", CallbackData: fmt.Sprintf("exp_year_prev_%d", year)},
|
||||
{Text: fmt.Sprintf("%d", year), CallbackData: fmt.Sprintf("exp_show_years_%d", year)},
|
||||
{Text: ">", CallbackData: fmt.Sprintf("exp_year_next_%d", year)},
|
||||
},
|
||||
}
|
||||
|
||||
for i := 0; i < 12; i += 4 {
|
||||
row := []models.InlineKeyboardButton{}
|
||||
for j := i; j < i+4 && j < 12; j++ {
|
||||
row = append(row, models.InlineKeyboardButton{
|
||||
Text: months[j], CallbackData: fmt.Sprintf("exp_do_%d_%d", year, j+1),
|
||||
})
|
||||
}
|
||||
kbRows = append(kbRows, row)
|
||||
}
|
||||
|
||||
kbRows = append(kbRows, []models.InlineKeyboardButton{
|
||||
{Text: "Back to Menu", CallbackData: "back_menu"},
|
||||
})
|
||||
|
||||
kb := models.InlineKeyboardMarkup{InlineKeyboard: kbRows}
|
||||
h.sendOrEdit(ctx, chatID, msgID, "Select month to export:", &kb)
|
||||
}
|
||||
|
||||
func (h *Handler) handleExportCallback(ctx context.Context, chatID int64, msgID int, cbID string, user *domain.User, data string) {
|
||||
defer h.answerCb(ctx, cbID)
|
||||
if h.handleExportNav(ctx, chatID, msgID, user, data) {
|
||||
return
|
||||
}
|
||||
h.handleExportDo(ctx, chatID, msgID, user, data)
|
||||
}
|
||||
|
||||
func (h *Handler) handleExportNav(ctx context.Context, chatID int64, msgID int, user *domain.User, data string) bool {
|
||||
var y int
|
||||
if n, _ := fmt.Sscanf(data, "exp_year_prev_%d", &y); n == 1 {
|
||||
h.editExportMonthPicker(ctx, chatID, msgID, y-1, 0, user)
|
||||
return true
|
||||
}
|
||||
if n, _ := fmt.Sscanf(data, "exp_year_next_%d", &y); n == 1 {
|
||||
h.editExportMonthPicker(ctx, chatID, msgID, y+1, 0, user)
|
||||
return true
|
||||
}
|
||||
if n, _ := fmt.Sscanf(data, "exp_show_years_%d", &y); n == 1 {
|
||||
h.editExportYearPicker(ctx, chatID, msgID, y, y)
|
||||
return true
|
||||
}
|
||||
var refY int
|
||||
if n, _ := fmt.Sscanf(data, "exp_years_%d_%d", &y, &refY); n == 2 {
|
||||
h.editExportYearPicker(ctx, chatID, msgID, y+6, refY)
|
||||
return true
|
||||
}
|
||||
if n, _ := fmt.Sscanf(data, "exp_year_%d", &y); n == 1 {
|
||||
h.editExportMonthPicker(ctx, chatID, msgID, y, 0, user)
|
||||
return true
|
||||
}
|
||||
if n, _ := fmt.Sscanf(data, "exp_years_back_%d", &y); n == 1 {
|
||||
h.editExportMonthPicker(ctx, chatID, msgID, y, 0, user)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *Handler) handleExportDo(ctx context.Context, chatID int64, msgID int, user *domain.User, data string) {
|
||||
var y, m int
|
||||
if n, _ := fmt.Sscanf(data, "exp_do_%d_%d", &y, &m); n != 2 {
|
||||
return
|
||||
}
|
||||
|
||||
last, err := h.Repo.GetLastEvent(user.ID)
|
||||
if err == nil && last != nil && last.EventType == "in" {
|
||||
backKB := models.InlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]models.InlineKeyboardButton{
|
||||
{{Text: "Back to Menu", CallbackData: "back_menu"}},
|
||||
},
|
||||
}
|
||||
h.editText(ctx, chatID, msgID, "You are currently clocked in. Please clock out first, then try again.", &backKB)
|
||||
return
|
||||
}
|
||||
|
||||
startStr, endStr := domain.MonthGregorianRange(user.Calendar, y, m)
|
||||
start, err := time.Parse(domain.DateLayout, startStr)
|
||||
if err != nil {
|
||||
h.sendText(ctx, chatID, "Error processing date")
|
||||
return
|
||||
}
|
||||
end, err := time.Parse(domain.DateLayout, endStr)
|
||||
if err != nil {
|
||||
h.sendText(ctx, chatID, "Error processing date")
|
||||
return
|
||||
}
|
||||
|
||||
slog.Info("export", "user_id", user.ID, "cal", user.Calendar, "cal_y", y, "cal_m", m, "from", startStr, "to", endStr)
|
||||
reportData, err := h.generateMonthlyReport(user, y, m, start, end)
|
||||
if err != nil {
|
||||
slog.Error("generate report", "error", err)
|
||||
backKB := models.InlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]models.InlineKeyboardButton{
|
||||
{{Text: "Back to Menu", CallbackData: "back_menu"}},
|
||||
},
|
||||
}
|
||||
h.editText(ctx, chatID, msgID, "Error generating report", &backKB)
|
||||
return
|
||||
}
|
||||
|
||||
slog.Info("report generated", "bytes", len(reportData))
|
||||
backKB := models.InlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]models.InlineKeyboardButton{
|
||||
{{Text: "Back to Menu", CallbackData: "back_menu"}},
|
||||
},
|
||||
}
|
||||
h.editText(ctx, chatID, msgID, "Report ready:", &backKB)
|
||||
|
||||
if _, err := h.Bot.SendDocument(ctx, &tgbot.SendDocumentParams{
|
||||
ChatID: chatID,
|
||||
Document: &models.InputFileUpload{
|
||||
Filename: fmt.Sprintf("worktime_%s.xlsx", fmt.Sprintf("%04d_%02d", y, m)),
|
||||
Data: bytes.NewReader(reportData),
|
||||
},
|
||||
}); err != nil {
|
||||
slog.Error("send document", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) editExportYearPicker(ctx context.Context, chatID int64, msgID int, centerYear, refY int) {
|
||||
backData := fmt.Sprintf("exp_years_back_%d", refY)
|
||||
navSuffix := fmt.Sprintf("_%d", refY)
|
||||
kb := buildYearPicker("exp_", backData, navSuffix, centerYear)
|
||||
h.editText(ctx, chatID, msgID, "Select year:", &kb)
|
||||
}
|
||||
Reference in New Issue
Block a user