feat: #45 meta de poupança configurável + conquistas genéricas

- Nova tabela user_settings com savings_goal_pct (default 40%)
- API GET /api/settings, PUT /api/settings
- DashboardService inclui savings_goal_pct no payload
- DashboardHandler dispara savings_checked com goal configurado
- GameService.NotifyAction: checa pct >= goal (sem hardcode de 40%)
- SettingsView: painel META DE POUPANÇA com input configurável
- HomeView: widget mostra meta dinâmica (META: X%) e usa savingsGoal
- stores/settings.ts: store com fetch e update

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
2026-05-28 22:32:08 -03:00
co-authored by Claude Sonnet 4.6
parent 9fada4997a
commit 1d59cb2a0e
15 changed files with 296 additions and 15 deletions
+13 -3
View File
@@ -8,11 +8,12 @@ import (
)
type DashboardHandler struct {
svc *service.DashboardService
svc *service.DashboardService
gameSvc *service.GameService
}
func NewDashboardHandler(svc *service.DashboardService) *DashboardHandler {
return &DashboardHandler{svc: svc}
func NewDashboardHandler(svc *service.DashboardService, gameSvc *service.GameService) *DashboardHandler {
return &DashboardHandler{svc: svc, gameSvc: gameSvc}
}
func (h *DashboardHandler) Get(w http.ResponseWriter, r *http.Request) {
@@ -26,5 +27,14 @@ func (h *DashboardHandler) Get(w http.ResponseWriter, r *http.Request) {
respondError(w, http.StatusInternalServerError, "failed to load dashboard")
return
}
// Notify game engine about savings percentage for the current month only
if month == time.Now().Format("2006-01") {
h.gameSvc.NotifyAction(r.Context(), "savings_checked", map[string]any{
"pct": data.SavingsPct,
"goal": data.SavingsGoalPct,
})
}
respondJSON(w, http.StatusOK, data)
}
+44
View File
@@ -0,0 +1,44 @@
package handler
import (
"encoding/json"
"net/http"
"financeiro-carvalho/internal/model"
"financeiro-carvalho/internal/service"
)
type SettingsHandler struct {
svc *service.SettingsService
}
func NewSettingsHandler(svc *service.SettingsService) *SettingsHandler {
return &SettingsHandler{svc: svc}
}
func (h *SettingsHandler) Get(w http.ResponseWriter, r *http.Request) {
s, err := h.svc.Get(r.Context())
if err != nil {
respondError(w, http.StatusInternalServerError, "failed to get settings")
return
}
respondJSON(w, http.StatusOK, s)
}
func (h *SettingsHandler) Update(w http.ResponseWriter, r *http.Request) {
var in model.UserSettings
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
respondError(w, http.StatusBadRequest, "invalid JSON")
return
}
out, err := h.svc.Update(r.Context(), in)
if err != nil {
if err == service.ErrInvalidSavingsGoal {
respondError(w, http.StatusUnprocessableEntity, err.Error())
return
}
respondError(w, http.StatusInternalServerError, "failed to update settings")
return
}
respondJSON(w, http.StatusOK, out)
}