feat: add delete account button in settings, implement all remaining achievements
All checks were successful
CI / build (push) Successful in 37s

- Add delete all data button with red danger style and confirmation prompt
- Add DeleteUserData repo method that wipes user from all tables in a transaction
- Add helper methods deleteAccountPrompt and deleteAccountConfirm
- Implement consistency_master, perfect_week, remote_veteran, onsite_master achievements
- Add GetDailyXPInRange and SumWorkSecondsByWorkType repo methods
- Update Dockerfile to alpine3.23, docker-compose to use env vars for proxy/paths
This commit is contained in:
2026-06-25 12:27:03 +03:30
parent 0926323d18
commit 5670b0455d
7 changed files with 185 additions and 8 deletions

View File

@@ -323,6 +323,10 @@ func (h *Handler) routePrefixedCallback(ctx context.Context, chatID int64, msgID
h.reportTimeCallback(ctx, chatID, msgID, cbID, user)
case data == "breakthreshold":
h.breakThresholdCallback(ctx, chatID, msgID, cbID, user)
case data == "deleteaccount":
h.deleteAccountPrompt(ctx, chatID, msgID, cbID, user)
case strings.HasPrefix(data, "delaccount_"):
h.deleteAccountConfirm(ctx, chatID, msgID, cbID, user)
case strings.HasPrefix(data, "league_"):
sortBy := data[7:]
h.leagueCallback(ctx, chatID, msgID, cbID, user, sortBy)

View File

@@ -241,6 +241,9 @@ func (h *Handler) checkAchievements(userID int64, stats *domain.RPGStats, workSe
h.checkHourAchievements(u, stats, totalHours)
h.checkSessionAchievements(u, workSeconds, isWeekend)
h.checkLevelAchievements(u, stats)
h.checkConsistencyAchievements(u, userID, date)
h.checkPerfectWeekAchievements(u, userID, date)
h.checkWorkTypeAchievements(u, userID)
return u.unlocked
}
@@ -334,3 +337,63 @@ func (h *Handler) checkLevelAchievements(u *achievementUnlocker, stats *domain.R
u.unlock("level_50")
}
}
func (h *Handler) checkConsistencyAchievements(u *achievementUnlocker, userID int64, today string) {
t, err := time.Parse(domain.DateLayout, today)
if err != nil {
return
}
sevenDaysAgo := t.AddDate(0, 0, -7).Format(domain.DateLayout)
yesterday := t.AddDate(0, 0, -1).Format(domain.DateLayout)
records, err := h.Repo.GetDailyXPInRange(userID, sevenDaysAgo, yesterday)
if err != nil {
return
}
if len(records) < 7 {
return
}
allHaveSixHours := true
for _, r := range records {
if r.WorkSeconds < 6*domain.SecondsPerHour {
allHaveSixHours = false
break
}
}
if allHaveSixHours {
u.unlock("consistency_master")
}
}
func (h *Handler) checkPerfectWeekAchievements(u *achievementUnlocker, userID int64, today string) {
t, err := time.Parse(domain.DateLayout, today)
if err != nil {
return
}
start := t.AddDate(0, 0, -6).Format(domain.DateLayout)
days, err := h.Repo.GetUserDaysInRange(userID, start, today)
if err != nil {
return
}
if len(days) < 7 {
return
}
for _, d := range days {
if d.IsDayOff {
return
}
}
u.unlock("perfect_week")
}
func (h *Handler) checkWorkTypeAchievements(u *achievementUnlocker, userID int64) {
rem, ons, err := h.Repo.SumWorkSecondsByWorkType(userID)
if err != nil {
return
}
if rem >= 100*domain.SecondsPerHour {
u.unlock("remote_veteran")
}
if ons >= 100*domain.SecondsPerHour {
u.unlock("onsite_master")
}
}

View File

@@ -112,6 +112,7 @@ func (h *Handler) buildSettingsKeyboard(user *domain.User, st *domain.UserSettin
{Text: fmt.Sprintf("Rate: %.2f", st.HourlyRate), CallbackData: "setrate"},
{Text: fmt.Sprintf("Currency: %s", st.Currency), CallbackData: "currency"},
},
{{Text: "Delete all data", CallbackData: "deleteaccount", Style: "danger"}},
{{Text: "Back to Menu", CallbackData: "back_menu"}},
}
return "Settings:", models.InlineKeyboardMarkup{InlineKeyboard: rows}
@@ -730,3 +731,26 @@ func (h *Handler) validateAndSaveReportTime(_ context.Context, _ int64, user *do
st.ReportTime = timeStr
return h.Repo.UpdateSettings(st)
}
func (h *Handler) deleteAccountPrompt(ctx context.Context, chatID int64, msgID int, cbID string, user *domain.User) {
defer h.answerCb(ctx, cbID)
kb := models.InlineKeyboardMarkup{
InlineKeyboard: [][]models.InlineKeyboardButton{
{
{Text: "Yes, delete everything", CallbackData: fmt.Sprintf("delaccount_%d", user.ID), Style: "danger"},
{Text: "Cancel", CallbackData: "back_settings"},
},
},
}
h.editText(ctx, chatID, msgID, "This will permanently delete ALL your data (events, days, RPG progress, settings, achievements).\nAre you sure?", &kb)
}
func (h *Handler) deleteAccountConfirm(ctx context.Context, chatID int64, msgID int, cbID string, user *domain.User) {
defer h.answerCb(ctx, cbID)
if err := h.Repo.DeleteUserData(user.ID); err != nil {
slog.Error("delete user data", "user_id", user.ID, "error", err)
h.editText(ctx, chatID, msgID, "Error deleting data. Please try again.", nil)
return
}
h.editText(ctx, chatID, msgID, "All your data has been deleted. You can start fresh by sending /start.", nil)
}