- 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
543 lines
17 KiB
Go
543 lines
17 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-telegram/bot/models"
|
|
|
|
"worktimeBot/internal/domain"
|
|
)
|
|
|
|
// -- History command and callback --
|
|
|
|
func (h *Handler) handleHistory(ctx context.Context, msg *models.Message, user *domain.User) {
|
|
h.editCalendar(ctx, msg.Chat.ID, 0, 0, 0)
|
|
}
|
|
|
|
func (h *Handler) historyCallback(ctx context.Context, chatID int64, msgID int, cbID string, user *domain.User) {
|
|
defer h.answerCb(ctx, cbID)
|
|
h.editCalendar(ctx, chatID, msgID, 0, 0)
|
|
}
|
|
|
|
// -- Calendar rendering --
|
|
|
|
func (h *Handler) editCalendar(ctx context.Context, chatID int64, msgID int, year, month int) {
|
|
user, err := h.getOrCreateUser(chatID)
|
|
if err != nil {
|
|
return
|
|
}
|
|
loc := h.loadLocation(user.Timezone)
|
|
now := time.Now().In(loc)
|
|
|
|
if year == 0 || month == 0 {
|
|
switch user.Calendar {
|
|
case "jalali":
|
|
jy, jm, _ := domain.GregorianToJalali(now.Year(), int(now.Month()), now.Day())
|
|
year, month = jy, jm
|
|
case "hijri":
|
|
hy, hm, _ := domain.GregorianToHijri(now.Year(), int(now.Month()), now.Day())
|
|
year, month = hy, hm
|
|
default:
|
|
year, month = now.Year(), int(now.Month())
|
|
}
|
|
}
|
|
|
|
cal := user.Calendar
|
|
cm := domain.BuildCalendarMonth(cal, year, month)
|
|
text := cm.Title
|
|
todayKey := now.Format(domain.DateLayout)
|
|
|
|
hasEvent := make(map[string]bool)
|
|
startDate, endDate := domain.MonthGregorianRange(cal, year, month)
|
|
dates, err := h.Repo.GetDistinctEventDates(user.ID, startDate, endDate)
|
|
if err == nil {
|
|
for _, d := range dates {
|
|
hasEvent[d] = true
|
|
}
|
|
}
|
|
|
|
kbRows := [][]models.InlineKeyboardButton{
|
|
{
|
|
{Text: "<", CallbackData: fmt.Sprintf("cal_prev_year_%d_%d", year-1, month)},
|
|
{Text: fmt.Sprintf("%d", year), CallbackData: fmt.Sprintf("cal_show_years_%d_%d", year, month)},
|
|
{Text: ">", CallbackData: fmt.Sprintf("cal_next_year_%d_%d", year+1, month)},
|
|
},
|
|
}
|
|
|
|
headerRow := []models.InlineKeyboardButton{}
|
|
for _, wn := range cm.WeekDays {
|
|
headerRow = append(headerRow, models.InlineKeyboardButton{Text: wn, CallbackData: "noop"})
|
|
}
|
|
kbRows = append(kbRows, headerRow)
|
|
|
|
row := []models.InlineKeyboardButton{}
|
|
for i, d := range cm.Days {
|
|
if d.DayNum == 0 {
|
|
row = append(row, models.InlineKeyboardButton{Text: " ", CallbackData: "noop"})
|
|
} else {
|
|
label := fmt.Sprintf("%d", d.DayNum)
|
|
if d.Date == todayKey {
|
|
label = fmt.Sprintf("[%d]", d.DayNum)
|
|
} else if hasEvent[d.Date] {
|
|
label = fmt.Sprintf("%d*", d.DayNum)
|
|
}
|
|
row = append(row, models.InlineKeyboardButton{Text: label, CallbackData: "cal_day_" + d.Date})
|
|
}
|
|
if len(row) == 7 || i == len(cm.Days)-1 {
|
|
for len(row) < 7 {
|
|
row = append(row, models.InlineKeyboardButton{Text: " ", CallbackData: "noop"})
|
|
}
|
|
kbRows = append(kbRows, row)
|
|
row = nil
|
|
}
|
|
}
|
|
|
|
prevY, prevM, nextY, nextM := domain.NavMonth(year, month)
|
|
kbRows = append(kbRows, []models.InlineKeyboardButton{
|
|
{Text: "<", CallbackData: fmt.Sprintf("cal_prev_%d_%d", prevY, prevM)},
|
|
{Text: "Today", CallbackData: "history"},
|
|
{Text: ">", CallbackData: fmt.Sprintf("cal_next_%d_%d", nextY, nextM)},
|
|
})
|
|
kbRows = append(kbRows, []models.InlineKeyboardButton{
|
|
{Text: "Back to Menu", CallbackData: "back_menu"},
|
|
})
|
|
|
|
kb := models.InlineKeyboardMarkup{InlineKeyboard: kbRows}
|
|
h.sendOrEdit(ctx, chatID, msgID, text, &kb)
|
|
}
|
|
|
|
// -- Calendar callbacks routing --
|
|
|
|
func (h *Handler) routeHistoryCallback(ctx context.Context, chatID int64, msgID int, cbID string, user *domain.User, data string) {
|
|
loc := h.loadLocation(user.Timezone)
|
|
|
|
if data == "history" {
|
|
h.editCalendar(ctx, chatID, msgID, 0, 0)
|
|
return
|
|
}
|
|
|
|
var y, m int
|
|
if n, _ := fmt.Sscanf(data, "cal_prev_%d_%d", &y, &m); n == 2 {
|
|
h.editCalendar(ctx, chatID, msgID, y, m)
|
|
return
|
|
}
|
|
if n, _ := fmt.Sscanf(data, "cal_next_%d_%d", &y, &m); n == 2 {
|
|
h.editCalendar(ctx, chatID, msgID, y, m)
|
|
return
|
|
}
|
|
if n, _ := fmt.Sscanf(data, "cal_prev_year_%d_%d", &y, &m); n == 2 {
|
|
h.editCalendar(ctx, chatID, msgID, y, m)
|
|
return
|
|
}
|
|
if n, _ := fmt.Sscanf(data, "cal_next_year_%d_%d", &y, &m); n == 2 {
|
|
h.editCalendar(ctx, chatID, msgID, y, m)
|
|
return
|
|
}
|
|
|
|
if n, _ := fmt.Sscanf(data, "cal_show_years_%d_%d", &y, &m); n == 2 {
|
|
h.editCalendarYearPicker(ctx, chatID, msgID, y, m, y)
|
|
return
|
|
}
|
|
var startY, refY, refM int
|
|
if n, _ := fmt.Sscanf(data, "cal_years_%d_%d_%d", &startY, &refY, &refM); n == 3 {
|
|
h.editCalendarYearPicker(ctx, chatID, msgID, startY+6, refM, refY)
|
|
return
|
|
}
|
|
if n, _ := fmt.Sscanf(data, "cal_year_%d", &y); n == 1 {
|
|
h.editCalendar(ctx, chatID, msgID, y, 1)
|
|
return
|
|
}
|
|
if n, _ := fmt.Sscanf(data, "cal_years_back_%d_%d", &y, &m); n == 2 {
|
|
h.editCalendar(ctx, chatID, msgID, y, m)
|
|
return
|
|
}
|
|
|
|
var date string
|
|
if n, _ := fmt.Sscanf(data, "cal_day_%s", &date); n == 1 && len(date) == 10 {
|
|
h.editDayView(ctx, chatID, msgID, user, date, loc)
|
|
return
|
|
}
|
|
|
|
var eid int64
|
|
if n, _ := fmt.Sscanf(data, "back_day_%d", &eid); n == 1 {
|
|
h.backToDayView(ctx, chatID, msgID, user, eid, loc)
|
|
return
|
|
}
|
|
|
|
var wtid int64
|
|
if n, _ := fmt.Sscanf(data, "settype_%d_%d", &eid, &wtid); n == 2 {
|
|
h.setEventWorkType(ctx, chatID, msgID, user, eid, wtid, loc)
|
|
return
|
|
}
|
|
if n, _ := fmt.Sscanf(data, "edit_type_%d", &eid); n == 1 {
|
|
h.editEventType(ctx, chatID, msgID, user, eid, loc)
|
|
return
|
|
}
|
|
if n, _ := fmt.Sscanf(data, "delete_%d", &eid); n == 1 {
|
|
h.deleteEventPrompt(ctx, chatID, msgID, user, eid, loc)
|
|
return
|
|
}
|
|
if n, _ := fmt.Sscanf(data, "delconf_%d", &eid); n == 1 {
|
|
h.deleteEventConfirm(ctx, chatID, msgID, user, eid, loc)
|
|
return
|
|
}
|
|
if n, _ := fmt.Sscanf(data, "edit_time_%d", &eid); n == 1 {
|
|
h.editEventTime(ctx, chatID, msgID, user, eid, loc)
|
|
return
|
|
}
|
|
var hh, mm int
|
|
if n, _ := fmt.Sscanf(data, "edittm_%d_%d_%d", &eid, &hh, &mm); n == 3 {
|
|
h.editEventTimeSet(ctx, chatID, msgID, user, eid, hh, mm, loc, cbID)
|
|
return
|
|
}
|
|
if n, _ := fmt.Sscanf(data, "edittm_%d_%d", &eid, &hh); n == 2 {
|
|
h.editEventTimeMin(ctx, chatID, msgID, user, eid, hh, loc)
|
|
return
|
|
}
|
|
if n, _ := fmt.Sscanf(data, "note_%d", &eid); n == 1 {
|
|
h.promptNoteInput(ctx, chatID, msgID, user, eid, loc)
|
|
return
|
|
}
|
|
if n, _ := fmt.Sscanf(data, "note_cancel_%d", &eid); n == 1 {
|
|
h.backToDayView(ctx, chatID, msgID, user, eid, loc)
|
|
return
|
|
}
|
|
|
|
switch {
|
|
case strings.HasPrefix(data, "addin_"):
|
|
h.addEventTime(ctx, chatID, msgID, user, data[6:], "in", loc)
|
|
case strings.HasPrefix(data, "addout_"):
|
|
h.addEventTime(ctx, chatID, msgID, user, data[7:], "out", loc)
|
|
case strings.HasPrefix(data, "addtm_"):
|
|
h.handleAddEventTime(ctx, chatID, msgID, user, data[6:], loc)
|
|
}
|
|
}
|
|
|
|
// -- Calendar year picker --
|
|
|
|
func (h *Handler) editCalendarYearPicker(ctx context.Context, chatID int64, msgID int, centerYear, refM, refY int) {
|
|
backData := fmt.Sprintf("cal_years_back_%d_%d", refY, refM)
|
|
navSuffix := fmt.Sprintf("_%d_%d", refY, refM)
|
|
kb := buildYearPicker("cal_", backData, navSuffix, centerYear)
|
|
h.editText(ctx, chatID, msgID, "Select year:", &kb)
|
|
}
|
|
|
|
// -- Day view --
|
|
|
|
func (h *Handler) editDayView(ctx context.Context, chatID int64, msgID int, user *domain.User, date string, loc *time.Location) {
|
|
h.sendDayView(ctx, chatID, msgID, user, date, loc, false)
|
|
}
|
|
|
|
func (h *Handler) sendDayView(ctx context.Context, chatID int64, msgID int, user *domain.User, date string, loc *time.Location, isNewMsg bool) {
|
|
events, err := h.Repo.EventsForDayByDate(user.ID, date)
|
|
if err != nil {
|
|
if isNewMsg {
|
|
h.sendText(ctx, chatID, "Error loading events")
|
|
}
|
|
return
|
|
}
|
|
text := fmt.Sprintf("Date: %s", domain.FormatDateForCalendar(date, user.Calendar))
|
|
if len(events) == 0 {
|
|
text += "\nNo events for this day."
|
|
kb := models.InlineKeyboardMarkup{
|
|
InlineKeyboard: [][]models.InlineKeyboardButton{
|
|
{{Text: "Add IN", CallbackData: "addin_" + date}},
|
|
{{Text: "Add OUT", CallbackData: "addout_" + date}},
|
|
{{Text: "Back to Calendar", CallbackData: "history"}},
|
|
},
|
|
}
|
|
h.sendOrEdit(ctx, chatID, msgID, text, &kb)
|
|
return
|
|
}
|
|
|
|
text += "\n\nEvents:"
|
|
kbRows := [][]models.InlineKeyboardButton{}
|
|
for _, e := range events {
|
|
t := time.Unix(e.OccurredAt, 0).In(loc).Format(domain.TimeLayout)
|
|
label := "IN"
|
|
if e.EventType == "out" {
|
|
label = "OUT"
|
|
}
|
|
wt := ""
|
|
if e.WorkTypeID != nil {
|
|
if wtObj, err := h.Repo.GetWorkType(*e.WorkTypeID); err == nil {
|
|
wt = " [" + wtObj.Name + "]"
|
|
}
|
|
}
|
|
note := ""
|
|
if e.Note != "" {
|
|
note = " (" + e.Note + ")"
|
|
}
|
|
text += fmt.Sprintf("\n%s %s%s%s", label, t, wt, note)
|
|
kbRows = append(kbRows, []models.InlineKeyboardButton{
|
|
{Text: fmt.Sprintf("Time %s", t), CallbackData: fmt.Sprintf("edit_time_%d", e.ID)},
|
|
{Text: "Type", CallbackData: fmt.Sprintf("edit_type_%d", e.ID)},
|
|
{Text: "Note", CallbackData: fmt.Sprintf("note_%d", e.ID)},
|
|
{Text: "DEL", CallbackData: fmt.Sprintf("delete_%d", e.ID), Style: "danger"},
|
|
})
|
|
}
|
|
kbRows = append(kbRows, []models.InlineKeyboardButton{
|
|
{Text: "Add IN", CallbackData: "addin_" + date},
|
|
{Text: "Add OUT", CallbackData: "addout_" + date},
|
|
})
|
|
kbRows = append(kbRows, []models.InlineKeyboardButton{
|
|
{Text: "Back to Calendar", CallbackData: "history"},
|
|
})
|
|
kb := models.InlineKeyboardMarkup{InlineKeyboard: kbRows}
|
|
h.sendOrEdit(ctx, chatID, msgID, text, &kb)
|
|
}
|
|
|
|
// -- Event editing --
|
|
|
|
func (h *Handler) editEventType(ctx context.Context, chatID int64, msgID int, user *domain.User, eventID int64, loc *time.Location) {
|
|
wts, err := h.Repo.GetWorkTypes()
|
|
if err != nil {
|
|
return
|
|
}
|
|
text := "Select new work type:"
|
|
kbRows := [][]models.InlineKeyboardButton{}
|
|
for _, wt := range wts {
|
|
kbRows = append(kbRows, []models.InlineKeyboardButton{
|
|
{Text: wt.Name, CallbackData: fmt.Sprintf("settype_%d_%d", eventID, wt.ID)},
|
|
})
|
|
}
|
|
kbRows = append(kbRows, []models.InlineKeyboardButton{
|
|
{Text: "Back", CallbackData: fmt.Sprintf("back_day_%d", eventID)},
|
|
})
|
|
kb := models.InlineKeyboardMarkup{InlineKeyboard: kbRows}
|
|
h.editText(ctx, chatID, msgID, text, &kb)
|
|
}
|
|
|
|
func (h *Handler) setEventWorkType(ctx context.Context, chatID int64, msgID int, user *domain.User, eventID, workTypeID int64, loc *time.Location) {
|
|
if err := h.Repo.UpdateEventWorkType(eventID, workTypeID); err != nil {
|
|
slog.Error("failed to update event work type", "event_id", eventID, "error", err)
|
|
h.sendDayView(ctx, chatID, msgID, user, "", loc, false)
|
|
return
|
|
}
|
|
event, err := h.Repo.GetEventByID(eventID, user.ID)
|
|
if err != nil {
|
|
return
|
|
}
|
|
t := time.Unix(event.OccurredAt, 0).In(loc)
|
|
h.sendDayView(ctx, chatID, msgID, user, t.Format(domain.DateLayout), loc, false)
|
|
}
|
|
|
|
func (h *Handler) editEventTime(ctx context.Context, chatID int64, msgID int, user *domain.User, eventID int64, loc *time.Location) {
|
|
kb := buildHourPickerKeyboard(
|
|
func(hh int) string { return fmt.Sprintf("edittm_%d_%02d", eventID, hh) },
|
|
fmt.Sprintf("back_day_%d", eventID),
|
|
)
|
|
h.editText(ctx, chatID, msgID, "Select hour:", &kb)
|
|
}
|
|
|
|
func (h *Handler) editEventTimeMin(ctx context.Context, chatID int64, msgID int, user *domain.User, eventID int64, hh int, loc *time.Location) {
|
|
kb := buildMinutePickerKeyboard(
|
|
func(mm int) string { return fmt.Sprintf("edittm_%d_%02d_%02d", eventID, hh, mm) },
|
|
fmt.Sprintf("edit_time_%d", eventID),
|
|
)
|
|
h.editText(ctx, chatID, msgID, fmt.Sprintf("Select minute for hour %02d:", hh), &kb)
|
|
}
|
|
|
|
func (h *Handler) editEventTimeSet(ctx context.Context, chatID int64, msgID int, user *domain.User, eventID int64, hh, mm int, loc *time.Location, cbID string) {
|
|
event, err := h.Repo.GetEventByID(eventID, user.ID)
|
|
if err != nil {
|
|
return
|
|
}
|
|
t := time.Unix(event.OccurredAt, 0).In(loc)
|
|
newTime := time.Date(t.Year(), t.Month(), t.Day(), hh, mm, 0, 0, loc)
|
|
date := newTime.Format(domain.DateLayout)
|
|
|
|
events, err := h.Repo.EventsForDayByDate(user.ID, date)
|
|
if err == nil {
|
|
for _, e := range events {
|
|
if e.ID == eventID {
|
|
continue
|
|
}
|
|
eTime := time.Unix(e.OccurredAt, 0)
|
|
if newTime.Unix() == eTime.Unix() {
|
|
h.answerCb(ctx, cbID)
|
|
h.sendDayView(ctx, chatID, msgID, user, date, loc, false)
|
|
return
|
|
}
|
|
}
|
|
type eventPos struct {
|
|
id int64
|
|
typ string
|
|
t int64
|
|
}
|
|
sorted := []eventPos{}
|
|
for _, e := range events {
|
|
et := e.OccurredAt
|
|
if e.ID == eventID {
|
|
et = newTime.Unix()
|
|
}
|
|
sorted = append(sorted, eventPos{e.ID, e.EventType, et})
|
|
}
|
|
for i := 0; i < len(sorted); i++ {
|
|
for j := i + 1; j < len(sorted); j++ {
|
|
if sorted[j].t < sorted[i].t || (sorted[j].t == sorted[i].t && sorted[j].id < sorted[i].id) {
|
|
sorted[i], sorted[j] = sorted[j], sorted[i]
|
|
}
|
|
}
|
|
}
|
|
for i := 1; i < len(sorted); i++ {
|
|
if sorted[i].typ == sorted[i-1].typ {
|
|
h.editText(ctx, chatID, msgID, "Edit rejected: overlapping events.", nil)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
if err = h.Repo.UpdateEventTime(eventID, newTime.Unix()); err != nil {
|
|
slog.Error("failed to update event time", "event_id", eventID, "error", err)
|
|
h.sendDayView(ctx, chatID, msgID, user, date, loc, false)
|
|
return
|
|
}
|
|
h.recalcDayWorkSeconds(user.ID, date, loc)
|
|
h.recalcAggregates(user.ID, loc)
|
|
h.sendDayView(ctx, chatID, msgID, user, date, loc, false)
|
|
}
|
|
|
|
// -- Event deletion --
|
|
|
|
func (h *Handler) deleteEventPrompt(ctx context.Context, chatID int64, msgID int, user *domain.User, eventID int64, loc *time.Location) {
|
|
kb := models.InlineKeyboardMarkup{
|
|
InlineKeyboard: [][]models.InlineKeyboardButton{
|
|
{
|
|
{Text: "Yes, DEL", CallbackData: fmt.Sprintf("delconf_%d", eventID), Style: "danger"},
|
|
{Text: "Cancel", CallbackData: fmt.Sprintf("back_day_%d", eventID)},
|
|
},
|
|
},
|
|
}
|
|
h.editText(ctx, chatID, msgID, "Delete this event?", &kb)
|
|
}
|
|
|
|
func (h *Handler) deleteEventConfirm(ctx context.Context, chatID int64, msgID int, user *domain.User, eventID int64, loc *time.Location) {
|
|
event, err := h.Repo.GetEventByID(eventID, user.ID)
|
|
if err != nil {
|
|
return
|
|
}
|
|
t := time.Unix(event.OccurredAt, 0).In(loc)
|
|
date := t.Format(domain.DateLayout)
|
|
|
|
if err = h.Repo.DeleteEvent(eventID); err != nil {
|
|
slog.Error("failed to delete event", "event_id", eventID, "error", err)
|
|
h.sendDayView(ctx, chatID, msgID, user, date, loc, false)
|
|
return
|
|
}
|
|
|
|
events, err := h.Repo.EventsForDayByDate(user.ID, date)
|
|
if err == nil && len(events) > 1 {
|
|
hasIssue := false
|
|
for i := 1; i < len(events); i++ {
|
|
if events[i].EventType == events[i-1].EventType {
|
|
hasIssue = true
|
|
break
|
|
}
|
|
}
|
|
if hasIssue {
|
|
h.sendText(ctx, chatID, "Warning: Timeline has consecutive events of same type after deletion.")
|
|
}
|
|
}
|
|
|
|
h.recalcDayWorkSeconds(user.ID, date, loc)
|
|
h.recalcAggregates(user.ID, loc)
|
|
h.sendDayView(ctx, chatID, msgID, user, date, loc, false)
|
|
}
|
|
|
|
func (h *Handler) backToDayView(ctx context.Context, chatID int64, msgID int, user *domain.User, eventID int64, loc *time.Location) {
|
|
event, err := h.Repo.GetEventByID(eventID, user.ID)
|
|
if err != nil {
|
|
return
|
|
}
|
|
t := time.Unix(event.OccurredAt, 0).In(loc)
|
|
h.sendDayView(ctx, chatID, msgID, user, t.Format(domain.DateLayout), loc, false)
|
|
}
|
|
|
|
// -- Note via pending input --
|
|
|
|
func (h *Handler) promptNoteInput(ctx context.Context, chatID int64, msgID int, user *domain.User, eventID int64, loc *time.Location) {
|
|
event, err := h.Repo.GetEventByID(eventID, user.ID)
|
|
if err != nil {
|
|
return
|
|
}
|
|
t := time.Unix(event.OccurredAt, 0).In(loc).Format(domain.TimeLayout)
|
|
|
|
kb := models.InlineKeyboardMarkup{
|
|
InlineKeyboard: [][]models.InlineKeyboardButton{
|
|
{{Text: "Cancel", CallbackData: fmt.Sprintf("note_cancel_%d", eventID)}},
|
|
},
|
|
}
|
|
h.editText(ctx, chatID, msgID, fmt.Sprintf("Send the note for event at %s:", t), &kb)
|
|
h.setPending(ctx, chatID, msgID, user.ID, PendingNote, map[string]string{"event_id": fmt.Sprintf("%d", eventID)})
|
|
}
|
|
|
|
// -- Adding events --
|
|
|
|
func (h *Handler) addEventTime(ctx context.Context, chatID int64, msgID int, user *domain.User, date, eventType string, loc *time.Location) {
|
|
kb := buildHourPickerKeyboard(
|
|
func(hh int) string { return fmt.Sprintf("addtm_%s_%s_%02d", eventType, date, hh) },
|
|
"cal_day_"+date,
|
|
)
|
|
h.editText(ctx, chatID, msgID, fmt.Sprintf("Select hour for %s event:", eventType), &kb)
|
|
}
|
|
|
|
func (h *Handler) handleAddEventTime(ctx context.Context, chatID int64, msgID int, user *domain.User, payload string, loc *time.Location) {
|
|
parts := strings.SplitN(payload, "_", 4)
|
|
if len(parts) < 3 {
|
|
return
|
|
}
|
|
eventType := parts[0]
|
|
addDate := parts[1]
|
|
hh, err := strconv.Atoi(parts[2])
|
|
if err != nil {
|
|
return
|
|
}
|
|
if len(parts) == 3 {
|
|
h.addEventTimeMin(ctx, chatID, msgID, user, addDate, eventType, hh, loc)
|
|
return
|
|
}
|
|
mm, err := strconv.Atoi(parts[3])
|
|
if err != nil {
|
|
return
|
|
}
|
|
h.addEventDo(ctx, chatID, msgID, user, addDate, eventType, hh, mm, loc)
|
|
}
|
|
|
|
func (h *Handler) addEventTimeMin(ctx context.Context, chatID int64, msgID int, user *domain.User, date, eventType string, hh int, loc *time.Location) {
|
|
kb := buildMinutePickerKeyboard(
|
|
func(mm int) string { return fmt.Sprintf("addtm_%s_%s_%02d_%02d", eventType, date, hh, mm) },
|
|
"add"+eventType+"_"+date,
|
|
)
|
|
h.editText(ctx, chatID, msgID, fmt.Sprintf("Select minute for hour %02d:", hh), &kb)
|
|
}
|
|
|
|
func (h *Handler) addEventDo(ctx context.Context, chatID int64, msgID int, user *domain.User, date, eventType string, hh, mm int, loc *time.Location) {
|
|
t, err := time.ParseInLocation("2006-01-02 15:04", date+" "+fmt.Sprintf("%02d:%02d", hh, mm), loc)
|
|
if err != nil {
|
|
return
|
|
}
|
|
day, err := h.Repo.GetOrCreateDay(user.ID, date)
|
|
if err != nil {
|
|
return
|
|
}
|
|
var wt *int64
|
|
if eventType == "in" {
|
|
wt = &day.CurrentWorkTypeID
|
|
if *wt == 0 {
|
|
wt = nil
|
|
}
|
|
}
|
|
if err := h.Repo.CreateEvent(user.ID, day.ID, eventType, wt, t.Unix(), ""); err != nil {
|
|
return
|
|
}
|
|
h.recalcDayWorkSeconds(user.ID, date, loc)
|
|
h.recalcAggregates(user.ID, loc)
|
|
h.sendDayView(ctx, chatID, msgID, user, date, loc, false)
|
|
}
|