refactor: split handlers.go into 5 files, redesign export month picker, pad calendar rows, improve comments
- Split 1571-line handlers.go into: handlers.go (core), clock.go, settings.go, calendar.go, report.go - Redesigned export month picker: year navigation + 12-month grid instead of prev/next month - Fixed calendar last row: pad remaining cells with empty buttons to ensure 7 columns - Added Go doc comments across all files (dateutil.go, totals.go, store.go, main.go, webhook.go)
This commit is contained in:
496
internal/bot/calendar.go
Normal file
496
internal/bot/calendar.go
Normal file
@@ -0,0 +1,496 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
|
||||
"worktimeBot/internal/db"
|
||||
)
|
||||
|
||||
// handleHistoryMsg shows the calendar view (new message).
|
||||
func (h *Handler) handleHistoryMsg(msg *tgbotapi.Message) {
|
||||
h.sendCalendar(msg.Chat.ID, 0, 0, 0)
|
||||
}
|
||||
|
||||
// historyCallback opens or refreshes the calendar view (inline).
|
||||
func (h *Handler) historyCallback(chatID int64, msgID int, callbackID string) {
|
||||
defer h.Bot.Request(tgbotapi.NewCallback(callbackID, ""))
|
||||
h.editCalendar(chatID, msgID, 0, 0)
|
||||
}
|
||||
|
||||
// handleEditMsg parses a /edit YYYY-MM-DD command and shows that day's events.
|
||||
func (h *Handler) handleEditMsg(msg *tgbotapi.Message) {
|
||||
date := msg.CommandArguments()
|
||||
if date == "" {
|
||||
h.sendText(msg.Chat.ID, "Usage: /edit YYYY-MM-DD (your calendar type)")
|
||||
return
|
||||
}
|
||||
if len(date) != 10 || date[4] != '-' || date[7] != '-' {
|
||||
h.sendText(msg.Chat.ID, "Invalid date. Use: /edit YYYY-MM-DD (your calendar type)")
|
||||
return
|
||||
}
|
||||
_, m, d := parseGregorianDateKey(date)
|
||||
if m < 1 || m > 12 || d < 1 || d > 31 {
|
||||
h.sendText(msg.Chat.ID, "Invalid date.")
|
||||
return
|
||||
}
|
||||
user, err := h.getOrCreateUser(msg.Chat.ID)
|
||||
if err != nil {
|
||||
h.sendText(msg.Chat.ID, "Error loading profile")
|
||||
return
|
||||
}
|
||||
loc := loadLocation(user.Timezone)
|
||||
date = userDateToGregorian(date, user.Calendar)
|
||||
h.sendDayView(msg.Chat.ID, 0, user, date, loc, true)
|
||||
}
|
||||
|
||||
// handleHistoryCallback routes calendar-related callback data to the right handler.
|
||||
func (h *Handler) handleHistoryCallback(chatID int64, msgID int, callbackID, data string) {
|
||||
defer h.Bot.Request(tgbotapi.NewCallback(callbackID, ""))
|
||||
user, err := h.getOrCreateUser(chatID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
loc := loadLocation(user.Timezone)
|
||||
|
||||
if data == "history" {
|
||||
h.editCalendar(chatID, msgID, 0, 0)
|
||||
return
|
||||
}
|
||||
|
||||
var y, m int
|
||||
var date string
|
||||
var eid int64
|
||||
|
||||
// Navigate to previous month
|
||||
if n, _ := fmt.Sscanf(data, "cal_prev_%d_%d", &y, &m); n == 2 {
|
||||
h.editCalendar(chatID, msgID, y, m)
|
||||
return
|
||||
}
|
||||
// Navigate to next month
|
||||
if n, _ := fmt.Sscanf(data, "cal_next_%d_%d", &y, &m); n == 2 {
|
||||
h.editCalendar(chatID, msgID, y, m)
|
||||
return
|
||||
}
|
||||
// Open a specific day
|
||||
if n, _ := fmt.Sscanf(data, "cal_day_%s", &date); n == 1 && len(date) == 10 {
|
||||
h.editDayView(chatID, msgID, user, date, loc)
|
||||
return
|
||||
}
|
||||
// Back to day view from event editing
|
||||
if n, _ := fmt.Sscanf(data, "back_day_%d", &eid); n == 1 {
|
||||
h.backToDayView(chatID, msgID, user, eid, loc)
|
||||
return
|
||||
}
|
||||
// Set event work type
|
||||
var wtid int64
|
||||
if n, _ := fmt.Sscanf(data, "settype_%d_%d", &eid, &wtid); n == 2 {
|
||||
h.setEventWorkType(chatID, msgID, user, eid, wtid, loc)
|
||||
return
|
||||
}
|
||||
// Show work type picker for an event
|
||||
if n, _ := fmt.Sscanf(data, "edit_type_%d", &eid); n == 1 {
|
||||
h.editEventType(chatID, msgID, user, eid, loc)
|
||||
return
|
||||
}
|
||||
// Delete event confirmation prompt
|
||||
if n, _ := fmt.Sscanf(data, "delete_%d", &eid); n == 1 {
|
||||
h.deleteEventPrompt(chatID, msgID, user, eid, loc)
|
||||
return
|
||||
}
|
||||
// Confirm event deletion
|
||||
if n, _ := fmt.Sscanf(data, "delconf_%d", &eid); n == 1 {
|
||||
h.deleteEventConfirm(chatID, msgID, user, eid, loc)
|
||||
return
|
||||
}
|
||||
// Show hour picker for event time
|
||||
if n, _ := fmt.Sscanf(data, "edit_time_%d", &eid); n == 1 {
|
||||
h.editEventTime(chatID, msgID, user, eid, loc)
|
||||
return
|
||||
}
|
||||
// Set event time (hour+minute)
|
||||
var hh, mm int
|
||||
if n, _ := fmt.Sscanf(data, "edittm_%d_%d_%d", &eid, &hh, &mm); n == 3 {
|
||||
h.editEventTimeSet(chatID, msgID, user, eid, hh, mm, loc)
|
||||
return
|
||||
}
|
||||
// Show minute picker (hour already chosen)
|
||||
if n, _ := fmt.Sscanf(data, "edittm_%d_%d", &eid, &hh); n == 2 {
|
||||
h.editEventTimeMin(chatID, msgID, user, eid, hh, loc)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// sendCalendar sends a calendar view as a new message.
|
||||
func (h *Handler) sendCalendar(chatID int64, msgID int, year, month int) {
|
||||
h.editCalendar(chatID, msgID, year, month)
|
||||
}
|
||||
|
||||
// editCalendar renders a monthly calendar grid with event indicators and navigation.
|
||||
func (h *Handler) editCalendar(chatID int64, msgID int, year, month int) {
|
||||
user, err := h.getOrCreateUser(chatID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
loc := loadLocation(user.Timezone)
|
||||
now := time.Now().In(loc)
|
||||
|
||||
if year == 0 || month == 0 {
|
||||
switch user.Calendar {
|
||||
case "jalali":
|
||||
jy, jm, _ := gregorianToJalali(now.Year(), int(now.Month()), now.Day())
|
||||
year, month = jy, jm
|
||||
case "hijri":
|
||||
hy, hm, _ := gregorianToHijri(now.Year(), int(now.Month()), now.Day())
|
||||
year, month = hy, hm
|
||||
default:
|
||||
year, month = now.Year(), int(now.Month())
|
||||
}
|
||||
}
|
||||
|
||||
cal := user.Calendar
|
||||
cm := buildCalendarMonth(cal, year, month)
|
||||
|
||||
text := cm.title
|
||||
todayKey := now.Format("2006-01-02")
|
||||
|
||||
hasEvent := make(map[string]bool)
|
||||
startDate, endDate := monthGregorianRange(cal, year, month)
|
||||
rows, err := h.DB.DB().Query(
|
||||
"SELECT DISTINCT date FROM days WHERE user_id=? AND date >= ? AND date <= ?",
|
||||
user.ID, startDate, endDate,
|
||||
)
|
||||
if err == nil {
|
||||
for rows.Next() {
|
||||
var d string
|
||||
rows.Scan(&d)
|
||||
hasEvent[d] = true
|
||||
}
|
||||
rows.Close()
|
||||
}
|
||||
|
||||
kbRows := [][]tgbotapi.InlineKeyboardButton{}
|
||||
|
||||
// Weekday header row
|
||||
headerRow := []tgbotapi.InlineKeyboardButton{}
|
||||
for _, wn := range cm.weekDays {
|
||||
headerRow = append(headerRow, tgbotapi.NewInlineKeyboardButtonData(wn, "noop"))
|
||||
}
|
||||
kbRows = append(kbRows, headerRow)
|
||||
|
||||
// Day cells
|
||||
row := []tgbotapi.InlineKeyboardButton{}
|
||||
for i, d := range cm.days {
|
||||
if d.dayNum == 0 {
|
||||
row = append(row, tgbotapi.NewInlineKeyboardButtonData(" ", "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, tgbotapi.NewInlineKeyboardButtonData(label, "cal_day_"+d.date))
|
||||
}
|
||||
if len(row) == 7 || i == len(cm.days)-1 {
|
||||
// Pad the last row to 7 columns so buttons render evenly
|
||||
for len(row) < 7 {
|
||||
row = append(row, tgbotapi.NewInlineKeyboardButtonData(" ", "noop"))
|
||||
}
|
||||
kbRows = append(kbRows, row)
|
||||
row = nil
|
||||
}
|
||||
}
|
||||
|
||||
prevY, prevM, nextY, nextM := navMonth(year, month)
|
||||
|
||||
// Navigation row
|
||||
kbRows = append(kbRows, tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("<", fmt.Sprintf("cal_prev_%d_%d", prevY, prevM)),
|
||||
tgbotapi.NewInlineKeyboardButtonData("Today", "history"),
|
||||
tgbotapi.NewInlineKeyboardButtonData(">", fmt.Sprintf("cal_next_%d_%d", nextY, nextM)),
|
||||
))
|
||||
|
||||
kbRows = append(kbRows, tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Back to Menu", "back_menu"),
|
||||
))
|
||||
|
||||
kb := tgbotapi.NewInlineKeyboardMarkup(kbRows...)
|
||||
h.sendOrEdit(chatID, msgID, text, &kb)
|
||||
}
|
||||
|
||||
// editDayView opens an existing message as a day view.
|
||||
func (h *Handler) editDayView(chatID int64, msgID int, user *db.User, date string, loc *time.Location) {
|
||||
h.sendDayView(chatID, msgID, user, date, loc, false)
|
||||
}
|
||||
|
||||
// sendDayView displays all events for a given date with inline edit/delete buttons.
|
||||
func (h *Handler) sendDayView(chatID int64, msgID int, user *db.User, date string, loc *time.Location, isNewMsg bool) {
|
||||
events, err := h.DB.EventsForDayByDate(user.ID, date)
|
||||
if err != nil {
|
||||
if isNewMsg {
|
||||
h.sendText(chatID, "Error loading events")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
text := fmt.Sprintf("Date: %s", formatDateForCalendar(date, user.Calendar))
|
||||
if len(events) == 0 {
|
||||
text += "\nNo events for this day."
|
||||
kb := tgbotapi.NewInlineKeyboardMarkup(
|
||||
tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Back to Calendar", "history"),
|
||||
),
|
||||
)
|
||||
h.sendOrEdit(chatID, msgID, text, &kb)
|
||||
return
|
||||
}
|
||||
|
||||
text += "\n\nEvents:"
|
||||
kbRows := [][]tgbotapi.InlineKeyboardButton{}
|
||||
for _, e := range events {
|
||||
t := time.Unix(e.OccurredAt, 0).In(loc).Format("15:04")
|
||||
label := "IN"
|
||||
if e.EventType == "out" {
|
||||
label = "OUT"
|
||||
}
|
||||
wt := ""
|
||||
if e.WorkTypeID != nil {
|
||||
if wtObj, err := h.DB.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, tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData(fmt.Sprintf("Time %s", t), fmt.Sprintf("edit_time_%d", e.ID)),
|
||||
tgbotapi.NewInlineKeyboardButtonData("Type", fmt.Sprintf("edit_type_%d", e.ID)),
|
||||
tgbotapi.NewInlineKeyboardButtonData("DEL", fmt.Sprintf("delete_%d", e.ID)),
|
||||
))
|
||||
}
|
||||
|
||||
kbRows = append(kbRows, tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Back to Calendar", "history"),
|
||||
))
|
||||
kb := tgbotapi.NewInlineKeyboardMarkup(kbRows...)
|
||||
h.sendOrEdit(chatID, msgID, text, &kb)
|
||||
}
|
||||
|
||||
// editEventType shows a work type picker for a specific event.
|
||||
func (h *Handler) editEventType(chatID int64, msgID int, user *db.User, eventID int64, loc *time.Location) {
|
||||
wts, err := h.DB.GetWorkTypes()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
text := "Select new work type:"
|
||||
kbRows := [][]tgbotapi.InlineKeyboardButton{}
|
||||
for _, wt := range wts {
|
||||
kbRows = append(kbRows, tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData(wt.Name, fmt.Sprintf("settype_%d_%d", eventID, wt.ID)),
|
||||
))
|
||||
}
|
||||
kbRows = append(kbRows, tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Back", fmt.Sprintf("back_day_%d", eventID)),
|
||||
))
|
||||
kb := tgbotapi.NewInlineKeyboardMarkup(kbRows...)
|
||||
edit := tgbotapi.NewEditMessageText(chatID, msgID, text)
|
||||
edit.ReplyMarkup = &kb
|
||||
h.Bot.Send(edit)
|
||||
}
|
||||
|
||||
// setEventWorkType updates an event's work type and returns to the day view.
|
||||
func (h *Handler) setEventWorkType(chatID int64, msgID int, user *db.User, eventID, workTypeID int64, loc *time.Location) {
|
||||
h.DB.DB().Exec("UPDATE events SET work_type_id=? WHERE id=?", workTypeID, eventID)
|
||||
event, err := h.getEventByID(user.ID, eventID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t := time.Unix(event.OccurredAt, 0).In(loc)
|
||||
h.sendDayView(chatID, msgID, user, t.Format("2006-01-02"), loc, false)
|
||||
}
|
||||
|
||||
// editEventTime shows an hour picker for changing an event's time.
|
||||
func (h *Handler) editEventTime(chatID int64, msgID int, user *db.User, eventID int64, loc *time.Location) {
|
||||
text := "Select hour:"
|
||||
kbRows := [][]tgbotapi.InlineKeyboardButton{}
|
||||
hourRow := []tgbotapi.InlineKeyboardButton{}
|
||||
for hh := 0; hh < 24; hh++ {
|
||||
hourRow = append(hourRow, tgbotapi.NewInlineKeyboardButtonData(
|
||||
fmt.Sprintf("%02d", hh),
|
||||
fmt.Sprintf("edittm_%d_%02d", eventID, hh),
|
||||
))
|
||||
if len(hourRow) == 6 || hh == 23 {
|
||||
kbRows = append(kbRows, hourRow)
|
||||
hourRow = nil
|
||||
}
|
||||
}
|
||||
kbRows = append(kbRows, tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Back", fmt.Sprintf("back_day_%d", eventID)),
|
||||
))
|
||||
kb := tgbotapi.NewInlineKeyboardMarkup(kbRows...)
|
||||
edit := tgbotapi.NewEditMessageText(chatID, msgID, text)
|
||||
edit.ReplyMarkup = &kb
|
||||
h.Bot.Send(edit)
|
||||
}
|
||||
|
||||
// editEventTimeMin shows a minute picker (15-min intervals) after hour selection.
|
||||
func (h *Handler) editEventTimeMin(chatID int64, msgID int, user *db.User, eventID int64, hh int, loc *time.Location) {
|
||||
text := fmt.Sprintf("Select minute for hour %02d:", hh)
|
||||
kbRows := [][]tgbotapi.InlineKeyboardButton{}
|
||||
minRow := []tgbotapi.InlineKeyboardButton{}
|
||||
for _, mm := range []int{0, 15, 30, 45} {
|
||||
minRow = append(minRow, tgbotapi.NewInlineKeyboardButtonData(
|
||||
fmt.Sprintf("%02d", mm),
|
||||
fmt.Sprintf("edittm_%d_%02d_%02d", eventID, hh, mm),
|
||||
))
|
||||
}
|
||||
kbRows = append(kbRows, minRow)
|
||||
kbRows = append(kbRows, tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Back", fmt.Sprintf("edit_time_%d", eventID)),
|
||||
))
|
||||
kb := tgbotapi.NewInlineKeyboardMarkup(kbRows...)
|
||||
edit := tgbotapi.NewEditMessageText(chatID, msgID, text)
|
||||
edit.ReplyMarkup = &kb
|
||||
h.Bot.Send(edit)
|
||||
}
|
||||
|
||||
// editEventTimeSet applies a new time to an event, checking for overlaps.
|
||||
func (h *Handler) editEventTimeSet(chatID int64, msgID int, user *db.User, eventID int64, hh, mm int, loc *time.Location) {
|
||||
event, err := h.getEventByID(user.ID, eventID)
|
||||
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 := t.Format("2006-01-02")
|
||||
|
||||
// Reject if the new time collides with another event's timestamp
|
||||
events, err := h.DB.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.acknowledgeCallback(chatID, msgID)
|
||||
h.sendDayView(chatID, msgID, user, date, loc, false)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Validate that in/out events still alternate after the change
|
||||
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})
|
||||
}
|
||||
// Bubble sort by timestamp, then by ID for determinism
|
||||
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 {
|
||||
edit := tgbotapi.NewEditMessageText(chatID, msgID, "Edit rejected: overlapping events. Two consecutive events must be different types (in/out).")
|
||||
h.Bot.Send(edit)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h.DB.DB().Exec("UPDATE events SET occurred_at=? WHERE id=?", newTime.Unix(), eventID)
|
||||
h.sendDayView(chatID, msgID, user, date, loc, false)
|
||||
}
|
||||
|
||||
// acknowledgeCallback sends an empty acknowledgement for a callback query.
|
||||
func (h *Handler) acknowledgeCallback(chatID int64, msgID int) {
|
||||
h.Bot.Request(tgbotapi.NewCallback(fmt.Sprintf("cb_%d_%d", chatID, msgID), ""))
|
||||
}
|
||||
|
||||
// deleteEventPrompt asks the user to confirm event deletion.
|
||||
func (h *Handler) deleteEventPrompt(chatID int64, msgID int, user *db.User, eventID int64, loc *time.Location) {
|
||||
edit := tgbotapi.NewEditMessageText(chatID, msgID, "Delete this event?")
|
||||
kb := tgbotapi.NewInlineKeyboardMarkup(
|
||||
tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Yes, DEL", fmt.Sprintf("delconf_%d", eventID)),
|
||||
tgbotapi.NewInlineKeyboardButtonData("Cancel", fmt.Sprintf("back_day_%d", eventID)),
|
||||
),
|
||||
)
|
||||
edit.ReplyMarkup = &kb
|
||||
h.Bot.Send(edit)
|
||||
}
|
||||
|
||||
// deleteEventConfirm deletes an event and warns if the timeline becomes invalid.
|
||||
func (h *Handler) deleteEventConfirm(chatID int64, msgID int, user *db.User, eventID int64, loc *time.Location) {
|
||||
event, err := h.getEventByID(user.ID, eventID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t := time.Unix(event.OccurredAt, 0).In(loc)
|
||||
date := t.Format("2006-01-02")
|
||||
|
||||
h.DB.DB().Exec("DELETE FROM events WHERE id=?", eventID)
|
||||
|
||||
// Warn if consecutive events now have the same type
|
||||
events, err := h.DB.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(chatID, "Warning: Timeline has consecutive events of same type after deletion. Manual repair needed.")
|
||||
}
|
||||
}
|
||||
|
||||
h.sendDayView(chatID, msgID, user, date, loc, false)
|
||||
}
|
||||
|
||||
// backToDayView returns from event editing to the day view.
|
||||
func (h *Handler) backToDayView(chatID int64, msgID int, user *db.User, eventID int64, loc *time.Location) {
|
||||
event, err := h.getEventByID(user.ID, eventID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t := time.Unix(event.OccurredAt, 0).In(loc)
|
||||
h.sendDayView(chatID, msgID, user, t.Format("2006-01-02"), loc, false)
|
||||
}
|
||||
|
||||
// getEventByID fetches a single event by ID, scoped to the user.
|
||||
func (h *Handler) getEventByID(userID, eventID int64) (*db.Event, error) {
|
||||
row := h.DB.DB().QueryRow(
|
||||
"SELECT id, user_id, day_id, event_type, work_type_id, occurred_at, note, created_at FROM events WHERE id=? AND user_id=?",
|
||||
eventID, userID,
|
||||
)
|
||||
var e db.Event
|
||||
var wt sql.NullInt64
|
||||
if err := row.Scan(&e.ID, &e.UserID, &e.DayID, &e.EventType, &wt, &e.OccurredAt, &e.Note, &e.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if wt.Valid {
|
||||
e.WorkTypeID = &wt.Int64
|
||||
}
|
||||
return &e, nil
|
||||
}
|
||||
Reference in New Issue
Block a user