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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user