Files
carvalho-finances/apps/api/internal/repository/dashboard.go
T
Mlcavalho1andClaude Sonnet 4.6 acbce4edc0 feat: multi-usuário com autenticação JWT
- Tabela `profiles` + coluna `profile_id` em todas as entidades
  (categories, transactions, recurring_expenses, accounts, player_profile,
   xp_events, player_quests, player_achievements, player_cosmetics)
- Dados existentes migrados para profile_id = 1 (Manoel)
- CLI `./api create-user --name <n> --password <p>` cria perfil com
  seed de categorias e player_profile; faz upsert de senha se já existir
- Auth substituída: cookie+APP_PASSWORD → JWT Bearer 24h (HS256)
- Middleware RequireAuth injeta profile_id no context de todas as rotas
- Todos os repositórios filtram por profile_id do context
- Endpoints: POST /api/auth/login, GET /api/auth/me,
  POST /api/auth/change-password, POST /api/logout
- Frontend: auth store usa localStorage (fc_token/fc_profile),
  api.ts envia Authorization header, LoginView usa campo name
- SettingsView reescrita com troca de senha e logout
- docker-compose.yml: remove APP_USERNAME/APP_PASSWORD, adiciona JWT_SECRET

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-27 20:04:44 -03:00

145 lines
4.0 KiB
Go

package repository
import (
"context"
"github.com/jackc/pgx/v5/pgxpool"
"financeiro-carvalho/internal/middleware"
"financeiro-carvalho/internal/model"
)
type DashboardRepository struct {
pool *pgxpool.Pool
}
func NewDashboardRepository(pool *pgxpool.Pool) *DashboardRepository {
return &DashboardRepository{pool: pool}
}
func (r *DashboardRepository) MonthlySummary(ctx context.Context, month string) (income, expenses float64, err error) {
pid := middleware.ProfileIDFromCtx(ctx)
row := r.pool.QueryRow(ctx, `
SELECT
COALESCE(SUM(CASE WHEN type = 'income' THEN amount ELSE 0 END), 0),
COALESCE(SUM(CASE WHEN type = 'expense' THEN amount ELSE 0 END), 0)
FROM transactions
WHERE to_char(date, 'YYYY-MM') = $1 AND profile_id = $2
`, month, pid)
err = row.Scan(&income, &expenses)
return
}
func (r *DashboardRepository) ByCategory(ctx context.Context, month string) ([]model.CategoryTotal, error) {
pid := middleware.ProfileIDFromCtx(ctx)
rows, err := r.pool.Query(ctx, `
SELECT
t.category_id,
COALESCE(c.name, 'Sem categoria') AS category_name,
COALESCE(c.color, '#6B7280') AS color,
SUM(t.amount) AS total
FROM transactions t
LEFT JOIN categories c ON c.id = t.category_id
WHERE to_char(t.date, 'YYYY-MM') = $1
AND t.type = 'expense'
AND t.profile_id = $2
GROUP BY t.category_id, c.name, c.color
ORDER BY total DESC
`, month, pid)
if err != nil {
return nil, err
}
defer rows.Close()
var out []model.CategoryTotal
for rows.Next() {
var ct model.CategoryTotal
if err := rows.Scan(&ct.CategoryID, &ct.CategoryName, &ct.Color, &ct.Total); err != nil {
return nil, err
}
out = append(out, ct)
}
return out, rows.Err()
}
func (r *DashboardRepository) MonthlyEvolution(ctx context.Context, month string) ([]model.MonthEvolution, error) {
pid := middleware.ProfileIDFromCtx(ctx)
rows, err := r.pool.Query(ctx, `
SELECT
to_char(m.ms, 'YYYY-MM') AS month,
COALESCE(SUM(CASE WHEN t.type = 'income' THEN t.amount ELSE 0 END), 0) AS income,
COALESCE(SUM(CASE WHEN t.type = 'expense' THEN t.amount ELSE 0 END), 0) AS expenses
FROM generate_series(
date_trunc('month', ($1 || '-01')::date) - INTERVAL '5 months',
date_trunc('month', ($1 || '-01')::date),
'1 month'::interval
) AS m(ms)
LEFT JOIN transactions t ON date_trunc('month', t.date) = m.ms AND t.profile_id = $2
GROUP BY m.ms
ORDER BY m.ms
`, month, pid)
if err != nil {
return nil, err
}
defer rows.Close()
var out []model.MonthEvolution
for rows.Next() {
var me model.MonthEvolution
if err := rows.Scan(&me.Month, &me.Income, &me.Expenses); err != nil {
return nil, err
}
me.Saved = me.Income - me.Expenses
out = append(out, me)
}
return out, rows.Err()
}
func (r *DashboardRepository) TotalTaxes(ctx context.Context, month string) (float64, error) {
pid := middleware.ProfileIDFromCtx(ctx)
var total float64
err := r.pool.QueryRow(ctx, `
SELECT COALESCE(SUM(t.amount), 0)
FROM transactions t
JOIN categories c ON c.id = t.category_id
WHERE to_char(t.date, 'YYYY-MM') = $1
AND t.type = 'expense'
AND c.is_tax = TRUE
AND t.profile_id = $2
`, month, pid).Scan(&total)
return total, err
}
func (r *DashboardRepository) RecentTransactions(ctx context.Context, month string) ([]model.RecentTransaction, error) {
pid := middleware.ProfileIDFromCtx(ctx)
rows, err := r.pool.Query(ctx, `
SELECT
t.id,
t.date::text,
t.description,
COALESCE(c.name, 'Sem categoria') AS category_name,
t.amount,
t.type
FROM transactions t
LEFT JOIN categories c ON c.id = t.category_id
WHERE to_char(t.date, 'YYYY-MM') = $1
AND t.profile_id = $2
ORDER BY t.date DESC, t.id DESC
LIMIT 10
`, month, pid)
if err != nil {
return nil, err
}
defer rows.Close()
var out []model.RecentTransaction
for rows.Next() {
var rt model.RecentTransaction
if err := rows.Scan(&rt.ID, &rt.Date, &rt.Description, &rt.CategoryName, &rt.Amount, &rt.Type); err != nil {
return nil, err
}
out = append(out, rt)
}
return out, rows.Err()
}