feat(savings): Add savings module with categories, transactions, recurring plans

- Categories: regular, deposits, credits, recurring, multi-user, accounts
- Transactions: deposits and withdrawals with user tracking
- Recurring plans: monthly payment obligations per user
- Stats: overdues calculation with allocation algorithm
- Excludes is_account categories from total sums
- Documentation: docs/SAVINGS.md
This commit is contained in:
Cosmo
2026-02-16 06:48:09 +00:00
parent 9e90aa6d95
commit 2a50e50771
18 changed files with 2910 additions and 162 deletions

View File

@@ -61,6 +61,39 @@ func (b *Bot) handleCallback(callback *tgbotapi.CallbackQuery) {
}
action := parts[0]
// Handle checkhabit with optional date (checkhabit_<id> or checkhabit_<id>_yesterday)
if action == "checkhabit" {
id, _ := strconv.ParseInt(parts[1], 10, 64)
logDate := time.Now()
dateLabel := "сегодня"
if len(parts) >= 3 && parts[2] == "yesterday" {
logDate = time.Now().AddDate(0, 0, -1)
dateLabel = "вчера"
}
log := &model.HabitLog{
HabitID: id,
UserID: user.ID,
Date: logDate,
Count: 1,
}
err = b.habitRepo.CreateLog(log)
if err != nil {
if strings.Contains(err.Error(), "duplicate") || strings.Contains(err.Error(), "already") {
b.answerCallback(callback.ID, "⚠️ Уже отмечено за эту дату")
} else {
b.answerCallback(callback.ID, "❌ Ошибка")
}
return
}
b.answerCallback(callback.ID, fmt.Sprintf("✅ Отмечено за %s!", dateLabel))
b.refreshHabitsMessage(chatID, messageID, user.ID)
return
}
id, _ := strconv.ParseInt(parts[1], 10, 64)
switch action {
@@ -81,21 +114,6 @@ func (b *Bot) handleCallback(callback *tgbotapi.CallbackQuery) {
}
b.answerCallback(callback.ID, "🗑 Задача удалена")
b.refreshTasksMessage(chatID, messageID, user.ID)
case "checkhabit":
log := &model.HabitLog{
HabitID: id,
UserID: user.ID,
Date: time.Now(),
Count: 1,
}
err = b.habitRepo.CreateLog(log)
if err != nil {
b.answerCallback(callback.ID, "❌ Ошибка")
return
}
b.answerCallback(callback.ID, "✅ Привычка отмечена!")
b.refreshHabitsMessage(chatID, messageID, user.ID)
}
}
@@ -204,11 +222,15 @@ func (b *Bot) buildHabitsMessage(habits []model.Habit, userID int64) (string, *t
text := "🎯 <b>Привычки на сегодня:</b>\n\n"
var rows [][]tgbotapi.InlineKeyboardButton
yesterday := time.Now().AddDate(0, 0, -1).Truncate(24 * time.Hour)
for _, habit := range todayHabits {
completed, _ := b.habitRepo.IsHabitCompletedToday(habit.ID, userID)
completedToday, _ := b.habitRepo.IsHabitCompletedToday(habit.ID, userID)
completedYesterday, _ := b.habitRepo.IsHabitCompletedOnDate(habit.ID, userID, yesterday)
status := "⬜"
if completed {
if completedToday {
status = "✅"
}
@@ -218,15 +240,31 @@ func (b *Bot) buildHabitsMessage(habits []model.Habit, userID int64) (string, *t
}
text += "\n"
if !completed {
row := []tgbotapi.InlineKeyboardButton{
tgbotapi.NewInlineKeyboardButtonData(fmt.Sprintf("✅ %s", habit.Name), fmt.Sprintf("checkhabit_%d", habit.ID)),
}
rows = append(rows, row)
// Build buttons row
var btnRow []tgbotapi.InlineKeyboardButton
if !completedToday {
btnRow = append(btnRow, tgbotapi.NewInlineKeyboardButtonData(
fmt.Sprintf("✅ %s", habit.Name),
fmt.Sprintf("checkhabit_%d", habit.ID),
))
}
// Add "За вчера" button if not completed yesterday
if !completedYesterday {
btnRow = append(btnRow, tgbotapi.NewInlineKeyboardButtonData(
"📅 За вчера",
fmt.Sprintf("checkhabit_%d_yesterday", habit.ID),
))
}
if len(btnRow) > 0 {
rows = append(rows, btnRow)
}
}
if len(rows) == 0 {
text += "\n✨ Всё выполнено!"
return text, nil
}
@@ -235,28 +273,13 @@ func (b *Bot) buildHabitsMessage(habits []model.Habit, userID int64) (string, *t
}
func (b *Bot) handleStart(msg *tgbotapi.Message) {
text := fmt.Sprintf(`👋 Привет! Я бот Pulse.
Твой Chat ID: <code>%d</code>
Скопируй его и вставь в настройках Pulse для получения уведомлений.
Доступные команды:
/tasks — задачи на сегодня
/habits — привычки на сегодня
/help — справка`, msg.Chat.ID)
text := fmt.Sprintf("👋 Привет! Я бот Pulse.\n\nТвой Chat ID: <code>%d</code>\n\nСкопируй его и вставь в настройках Pulse для получения уведомлений.\n\nДоступные команды:\n/tasks — задачи на сегодня\n/habits — привычки на сегодня\n/help — справка", msg.Chat.ID)
b.SendMessage(msg.Chat.ID, text)
}
func (b *Bot) handleHelp(msg *tgbotapi.Message) {
text := `📚 <b>Справка по командам:</b>
/start — получить твой Chat ID
/tasks — список задач на сегодня
/habits — список привычек
💡 Чтобы получать уведомления, добавь свой Chat ID в настройках Pulse.`
text := "📚 <b>Справка по командам:</b>\n\n/start — получить твой Chat ID\n/tasks — список задач на сегодня\n/habits — список привычек\n\n💡 Чтобы получать уведомления, добавь свой Chat ID в настройках Pulse."
b.SendMessage(msg.Chat.ID, text)
}