refactor: break up large functions, add RPG/league/salary/burnout, fix bugs, add tests

- Refactor GenerateMonthlyReport (198→70), HandleCallback (128→85),
  handleHistoryCallback (131→38), handleExportCallback (100→25),
  checkAchievements (86→25) into extracted helper functions
- Add RPG system (XP, levels, streak, achievements, burnout)
- Add WorkTime League leaderboard
- Add salary estimation with configurable currency/rate
- Add input sanitization (SanitizeNote, SanitizeDisplayName)
- Add settings select-style pickers for toggles (report, RPG, league)
- Add break threshold inline picker UI
- Add event notes with pending state and /note command
- Add multi-calendar support (Jalali, Hijri)
- Add Excel export with theme picker
- Fix: getTodayBreakThreshold uses user's timezone (was UTC)
- Fix: acknowledgeCallback passes real callback ID
- Fix: currency symbol safety with currencySymbol() helper
- Fix: count work hours in summary on day-off days
- Fix: unsilence all error returns from SQL Exec/Bot API/migrations
- Remove stale db/schema.sql and unreferenced computeWeekTotals
- Extract constants: DateLayout, TimeLayout, secondsPerHour, etc.
- Add 12 new tests (XP, salary, sanitize, currency, dateutil)
- Remove duplicate package doc comment in totals.go
This commit is contained in:
2026-06-25 01:02:16 +03:30
parent 0d942c51db
commit 344c615666
18 changed files with 2510 additions and 483 deletions

View File

@@ -0,0 +1,101 @@
-- +goose Up
ALTER TABLE users ADD COLUMN username TEXT NOT NULL DEFAULT '';
CREATE TABLE IF NOT EXISTS user_settings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL UNIQUE REFERENCES users(id),
export_accent TEXT NOT NULL DEFAULT 'ocean'
CHECK (export_accent IN ('ocean', 'beach', 'rose', 'catppuccin')),
report_enabled INTEGER NOT NULL DEFAULT 1,
report_time TEXT NOT NULL DEFAULT '23:00',
last_report_date TEXT NOT NULL DEFAULT '',
rpg_enabled INTEGER NOT NULL DEFAULT 0,
league_opt_in INTEGER NOT NULL DEFAULT 0,
currency TEXT NOT NULL DEFAULT '$ USD',
hourly_rate REAL NOT NULL DEFAULT 0.0,
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
);
INSERT OR IGNORE INTO user_settings (user_id)
SELECT id FROM users;
CREATE TABLE IF NOT EXISTS user_rpg (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL UNIQUE REFERENCES users(id),
xp INTEGER NOT NULL DEFAULT 0,
level INTEGER NOT NULL DEFAULT 1,
current_streak INTEGER NOT NULL DEFAULT 0,
longest_streak INTEGER NOT NULL DEFAULT 0,
total_work_seconds INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
);
INSERT OR IGNORE INTO user_rpg (user_id)
SELECT id FROM users;
CREATE TABLE IF NOT EXISTS daily_xp (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id),
date TEXT NOT NULL,
xp_earned INTEGER NOT NULL DEFAULT 0,
work_seconds INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
UNIQUE(user_id, date)
);
CREATE TABLE IF NOT EXISTS achievements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
description TEXT NOT NULL,
icon TEXT NOT NULL DEFAULT ''
);
INSERT OR IGNORE INTO achievements (code, name, description) VALUES
('first_clockin', 'First Steps', 'Clock in for the first time'),
('early_bird', 'Early Bird', 'Clock in before 7 AM'),
('night_owl', 'Night Owl', 'Clock out after 10 PM'),
('iron_streak_5', 'Iron Streak', 'Work 5 consecutive days'),
('iron_streak_10', 'Iron Streak II', 'Work 10 consecutive days'),
('iron_streak_30', 'Iron Streak III', 'Work 30 consecutive days'),
('remote_veteran', 'Remote Veteran', 'Log 100 hours of remote work'),
('onsite_master', 'Onsite Master', 'Log 100 hours of onsite work'),
('consistency_master', 'Consistency Master', 'Work at least 6 hours daily for a week'),
('perfect_week', 'Perfect Week', 'No days off for a full week'),
('overtime_hero', 'Overtime Hero', 'Work more than 10 hours in a day'),
('level_5', 'Getting Started', 'Reach level 5'),
('level_10', 'Dedicated', 'Reach level 10'),
('level_25', 'Veteran', 'Reach level 25'),
('level_50', 'Legend', 'Reach level 50'),
('weekend_warrior', 'Weekend Warrior', 'Work on a weekend day'),
('hundred_hours', 'Century', 'Log 100 total work hours'),
('five_hundred_hours', 'Iron Will', 'Log 500 total work hours'),
('salary_setter', 'Salary Tracker', 'Configure your hourly rate'),
('rpg_pioneer', 'RPG Pioneer', 'Enable the RPG system');
CREATE TABLE IF NOT EXISTS user_achievements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id),
achievement_id INTEGER NOT NULL REFERENCES achievements(id),
unlocked_at INTEGER NOT NULL DEFAULT (unixepoch()),
UNIQUE(user_id, achievement_id)
);
CREATE INDEX IF NOT EXISTS idx_daily_xp_user_date ON daily_xp(user_id, date);
CREATE INDEX IF NOT EXISTS idx_user_achievements_user ON user_achievements(user_id);
-- +goose Down
DROP TABLE IF EXISTS user_achievements;
DROP TABLE IF EXISTS achievements;
DROP TABLE IF EXISTS daily_xp;
DROP TABLE IF EXISTS user_rpg;
DROP TABLE IF EXISTS user_settings;
CREATE TABLE IF NOT EXISTS users_old AS SELECT * FROM users;
DROP TABLE IF EXISTS users;
-- Note: restoring users table without the username column requires
-- recreating it from the backup. In practice this migration is safe
-- and the down path preserves data via the backup table.
ALTER TABLE users_old RENAME TO users;