package repository import ( "context" "errors" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/yuxingu/digital-psychology/apps/api/internal/model" ) // GrowthRepo persists growth plans and check-ins. type GrowthRepo struct { Pool *pgxpool.Pool } // CreatePlan inserts a plan. func (r *GrowthRepo) CreatePlan(ctx context.Context, userID uuid.UUID, title, focus string) (*model.GrowthPlan, error) { p := &model.GrowthPlan{} err := r.Pool.QueryRow(ctx, ` INSERT INTO growth_plans(user_id, title, focus) VALUES ($1,$2,$3) RETURNING id, user_id, title, focus, status, created_at`, userID, title, focus, ).Scan(&p.ID, &p.UserID, &p.Title, &p.Focus, &p.Status, &p.CreatedAt) return p, err } // ListPlans returns active plans for user. func (r *GrowthRepo) ListPlans(ctx context.Context, userID uuid.UUID) ([]model.GrowthPlan, error) { rows, err := r.Pool.Query(ctx, ` SELECT id, user_id, title, focus, status, created_at FROM growth_plans WHERE user_id=$1 AND deleted_at IS NULL AND status='active' ORDER BY created_at DESC LIMIT 20`, userID) if err != nil { return nil, err } defer rows.Close() var out []model.GrowthPlan for rows.Next() { var p model.GrowthPlan if err := rows.Scan(&p.ID, &p.UserID, &p.Title, &p.Focus, &p.Status, &p.CreatedAt); err != nil { return nil, err } out = append(out, p) } return out, rows.Err() } // Checkin upserts today's check-in. func (r *GrowthRepo) Checkin(ctx context.Context, userID, planID uuid.UUID, day time.Time, note *string) (*model.GrowthCheckin, error) { var owner uuid.UUID err := r.Pool.QueryRow(ctx, ` SELECT user_id FROM growth_plans WHERE id=$1 AND deleted_at IS NULL`, planID, ).Scan(&owner) if err != nil { return nil, errors.New("plan not found") } if owner != userID { return nil, errors.New("plan not found") } c := &model.GrowthCheckin{} var dayOut time.Time err = r.Pool.QueryRow(ctx, ` INSERT INTO growth_checkins(plan_id, user_id, day, note) VALUES ($1,$2,$3::date,$4) ON CONFLICT (plan_id, day) DO UPDATE SET note=EXCLUDED.note RETURNING id, plan_id, user_id, day, note, created_at`, planID, userID, day.Format("2006-01-02"), note, ).Scan(&c.ID, &c.PlanID, &c.UserID, &dayOut, &c.Note, &c.CreatedAt) if err != nil { return nil, err } c.Day = dayOut.Format("2006-01-02") return c, nil } // ListCheckinsRecent returns check-ins for a plan in last N days. func (r *GrowthRepo) ListCheckinsRecent(ctx context.Context, userID, planID uuid.UUID, days int) ([]model.GrowthCheckin, error) { rows, err := r.Pool.Query(ctx, ` SELECT id, plan_id, user_id, day, note, created_at FROM growth_checkins WHERE user_id=$1 AND plan_id=$2 AND day >= (CURRENT_DATE - $3::int) ORDER BY day DESC`, userID, planID, days) if err != nil { return nil, err } defer rows.Close() var out []model.GrowthCheckin for rows.Next() { var c model.GrowthCheckin var dayOut time.Time if err := rows.Scan(&c.ID, &c.PlanID, &c.UserID, &dayOut, &c.Note, &c.CreatedAt); err != nil { return nil, err } c.Day = dayOut.Format("2006-01-02") out = append(out, c) } return out, rows.Err() } // ListMoodsRecent for companion trail (reuse moods table). func (r *GrowthRepo) ListMoodsRecent(ctx context.Context, userID uuid.UUID, days int) ([]model.Mood, error) { rows, err := r.Pool.Query(ctx, ` SELECT id, user_id, day, score, note, created_at FROM moods WHERE user_id=$1 AND deleted_at IS NULL AND day >= (CURRENT_DATE - $2::int) ORDER BY day DESC`, userID, days) if err != nil { return nil, err } defer rows.Close() var out []model.Mood for rows.Next() { var m model.Mood var dayOut time.Time if err := rows.Scan(&m.ID, &m.UserID, &dayOut, &m.Score, &m.Note, &m.CreatedAt); err != nil { return nil, err } m.Day = dayOut.Format("2006-01-02") out = append(out, m) } return out, rows.Err() } // ErrNoRows re-export helper. var ErrNoRows = pgx.ErrNoRows