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:
2026-06-24 11:33:17 +03:30
parent d8599657f4
commit a8b849f8fd
11 changed files with 1366 additions and 1176 deletions

View File

@@ -1,3 +1,4 @@
// Package db provides the SQLite-backed data store for users, work types, days, and events.
package db
import (
@@ -20,10 +21,12 @@ func init() {
}
}
// Store wraps a SQLite database connection and provides methods for all data access.
type Store struct {
db *sql.DB
}
// User represents a bot user with timezone, reporting preferences, and calendar type.
type User struct {
ID int64
ChatID int64
@@ -38,12 +41,14 @@ type User struct {
UpdatedAt int64
}
// WorkType represents a type of work (e.g. "office", "remote") that can be assigned to a day.
type WorkType struct {
ID int64
Name string
CreatedAt int64
}
// Day represents a single day entry for a user, including day-off status and work type.
type Day struct {
ID int64
UserID int64
@@ -56,6 +61,7 @@ type Day struct {
UpdatedAt int64
}
// Event represents a single timestamped event ("in" or "out") for a user on a given day.
type Event struct {
ID int64
UserID int64
@@ -67,6 +73,7 @@ type Event struct {
CreatedAt int64
}
// NewStore opens or creates the SQLite database at path, runs migrations, and returns a Store.
func NewStore(path string) (*Store, error) {
db, err := sql.Open(driverName, path)
if err != nil {
@@ -85,10 +92,12 @@ func NewStore(path string) (*Store, error) {
return &Store{db: db}, nil
}
// Close closes the underlying SQLite database connection.
func (s *Store) Close() error {
return s.db.Close()
}
// DB returns the underlying *sql.DB for advanced use.
func (s *Store) DB() *sql.DB {
return s.db
}
@@ -295,6 +304,7 @@ func migrateSettings(db *sql.DB) {
}
}
// GetOrCreateUser retrieves a user by chat ID, creating a new record if none exists.
func (s *Store) GetOrCreateUser(chatID int64) (*User, error) {
_, err := s.db.Exec(
"INSERT OR IGNORE INTO users (chat_id) VALUES (?)",
@@ -306,6 +316,7 @@ func (s *Store) GetOrCreateUser(chatID int64) (*User, error) {
return s.GetUserByChatID(chatID)
}
// GetUserByChatID retrieves a single user by their Telegram chat ID.
func (s *Store) GetUserByChatID(chatID int64) (*User, error) {
row := s.db.QueryRow(`
SELECT id, chat_id, timezone, is_active, report_enabled, report_time,
@@ -327,6 +338,7 @@ func (s *Store) GetUserByChatID(chatID int64) (*User, error) {
return &u, nil
}
// UpdateUser persists all fields of the given user record.
func (s *Store) UpdateUser(user *User) error {
_, err := s.db.Exec(
"UPDATE users SET timezone=?, is_active=?, report_enabled=?, report_time=?, last_report_date=?, export_accent=?, calendar=?, updated_at=unixepoch() WHERE id=?",
@@ -335,6 +347,7 @@ func (s *Store) UpdateUser(user *User) error {
return err
}
// MarkReportSent records the last report date for a user to prevent duplicate daily reports.
func (s *Store) MarkReportSent(userID int64, date string) error {
_, err := s.db.Exec(
"UPDATE users SET last_report_date=?, updated_at=unixepoch() WHERE id=?",
@@ -343,6 +356,7 @@ func (s *Store) MarkReportSent(userID int64, date string) error {
return err
}
// GetAllUsers returns all users in the database.
func (s *Store) GetAllUsers() ([]User, error) {
rows, err := s.db.Query(`
SELECT id, chat_id, timezone, is_active, report_enabled, report_time,
@@ -372,6 +386,7 @@ func (s *Store) GetAllUsers() ([]User, error) {
return users, rows.Err()
}
// GetWorkTypes returns all work types ordered by ID.
func (s *Store) GetWorkTypes() ([]WorkType, error) {
rows, err := s.db.Query("SELECT id, name, created_at FROM work_types ORDER BY id ASC")
if err != nil {
@@ -389,6 +404,7 @@ func (s *Store) GetWorkTypes() ([]WorkType, error) {
return wts, rows.Err()
}
// GetWorkType returns a single work type by its ID.
func (s *Store) GetWorkType(id int64) (*WorkType, error) {
var wt WorkType
err := s.db.QueryRow("SELECT id, name, created_at FROM work_types WHERE id=?", id).Scan(&wt.ID, &wt.Name, &wt.CreatedAt)
@@ -398,6 +414,7 @@ func (s *Store) GetWorkType(id int64) (*WorkType, error) {
return &wt, nil
}
// GetOrCreateDay retrieves a day record for a user/date, creating one if it does not exist.
func (s *Store) GetOrCreateDay(userID int64, date string) (*Day, error) {
_, err := s.db.Exec(
"INSERT OR IGNORE INTO days (user_id, date) VALUES (?, ?)",
@@ -409,6 +426,7 @@ func (s *Store) GetOrCreateDay(userID int64, date string) (*Day, error) {
return s.GetDay(userID, date)
}
// GetDay returns a single day record for the given user and date.
func (s *Store) GetDay(userID int64, date string) (*Day, error) {
row := s.db.QueryRow(`
SELECT id, user_id, date, is_day_off, day_off_reason,
@@ -426,6 +444,7 @@ func (s *Store) GetDay(userID int64, date string) (*Day, error) {
return &d, nil
}
// UpdateDay persists the given day record's editable fields.
func (s *Store) UpdateDay(day *Day) error {
_, err := s.db.Exec(
"UPDATE days SET is_day_off=?, day_off_reason=?, current_work_type_id=?, min_break_threshold=?, updated_at=unixepoch() WHERE id=?",
@@ -434,6 +453,7 @@ func (s *Store) UpdateDay(day *Day) error {
return err
}
// SetDayOff marks a user's day as a day off with an optional reason.
func (s *Store) SetDayOff(userID int64, date string, reason string) error {
_, err := s.GetOrCreateDay(userID, date)
if err != nil {
@@ -446,6 +466,7 @@ func (s *Store) SetDayOff(userID int64, date string, reason string) error {
return err
}
// RemoveDayOff clears the day-off flag for a user on the given date.
func (s *Store) RemoveDayOff(userID int64, date string) error {
_, err := s.db.Exec(
"UPDATE days SET is_day_off=0, day_off_reason='', updated_at=unixepoch() WHERE user_id=? AND date=?",
@@ -454,6 +475,7 @@ func (s *Store) RemoveDayOff(userID int64, date string) error {
return err
}
// IsDayOff checks whether the given date is a day off for the user.
func (s *Store) IsDayOff(userID int64, date string) (bool, error) {
var isOff bool
err := s.db.QueryRow(
@@ -469,6 +491,7 @@ func (s *Store) IsDayOff(userID int64, date string) (bool, error) {
return isOff, nil
}
// GetUserDaysInRange returns all day records for a user within a date range (inclusive).
func (s *Store) GetUserDaysInRange(userID int64, startDate, endDate string) ([]Day, error) {
rows, err := s.db.Query(`
SELECT id, user_id, date, is_day_off, day_off_reason,
@@ -495,6 +518,7 @@ func (s *Store) GetUserDaysInRange(userID int64, startDate, endDate string) ([]D
return days, rows.Err()
}
// CreateEvent inserts a new event record (typically "in" or "out").
func (s *Store) CreateEvent(userID int64, dayID int64, eventType string, workTypeID *int64, occurredAt int64, note string) error {
_, err := s.db.Exec(
"INSERT INTO events (user_id, day_id, event_type, work_type_id, occurred_at, note) VALUES (?, ?, ?, ?, ?, ?)",
@@ -503,6 +527,7 @@ func (s *Store) CreateEvent(userID int64, dayID int64, eventType string, workTyp
return err
}
// GetLastEvent returns the most recent event for the user, ordered by occurrence time.
func (s *Store) GetLastEvent(userID int64) (*Event, error) {
row, err := s.db.Query(`
SELECT id, user_id, day_id, event_type, work_type_id, occurred_at, note, created_at
@@ -529,6 +554,7 @@ func (s *Store) GetLastEvent(userID int64) (*Event, error) {
return &e, nil
}
// EventsForDayByDate returns all events for a user on a specific date, ordered by occurrence.
func (s *Store) EventsForDayByDate(userID int64, date string) ([]Event, error) {
rows, err := s.db.Query(`
SELECT e.id, e.user_id, e.day_id, e.event_type, e.work_type_id, e.occurred_at, e.note, e.created_at
@@ -556,6 +582,7 @@ func (s *Store) EventsForDayByDate(userID int64, date string) ([]Event, error) {
return events, rows.Err()
}
// EventsForDayByDayID returns all events for a day record, ordered by occurrence.
func (s *Store) EventsForDayByDayID(dayID int64) ([]Event, error) {
rows, err := s.db.Query(`
SELECT id, user_id, day_id, event_type, work_type_id, occurred_at, note, created_at