package roadmap import ( "context" "fmt" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) // Store handles roadmap data persistence type Store struct { pool *pgxpool.Pool } // NewStore creates a new roadmap store func NewStore(pool *pgxpool.Pool) *Store { return &Store{pool: pool} } // ============================================================================ // Roadmap CRUD Operations // ============================================================================ // CreateRoadmap creates a new roadmap func (s *Store) CreateRoadmap(ctx context.Context, r *Roadmap) error { r.ID = uuid.New() r.CreatedAt = time.Now().UTC() r.UpdatedAt = r.CreatedAt if r.Status == "" { r.Status = "draft" } if r.Version == "" { r.Version = "1.0" } _, err := s.pool.Exec(ctx, ` INSERT INTO roadmaps ( id, tenant_id, namespace_id, title, description, version, assessment_id, portfolio_id, status, total_items, completed_items, progress, start_date, target_date, created_at, updated_at, created_by ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17 ) `, r.ID, r.TenantID, r.NamespaceID, r.Title, r.Description, r.Version, r.AssessmentID, r.PortfolioID, r.Status, r.TotalItems, r.CompletedItems, r.Progress, r.StartDate, r.TargetDate, r.CreatedAt, r.UpdatedAt, r.CreatedBy, ) return err } // GetRoadmap retrieves a roadmap by ID func (s *Store) GetRoadmap(ctx context.Context, id uuid.UUID) (*Roadmap, error) { var r Roadmap err := s.pool.QueryRow(ctx, ` SELECT id, tenant_id, namespace_id, title, description, version, assessment_id, portfolio_id, status, total_items, completed_items, progress, start_date, target_date, created_at, updated_at, created_by FROM roadmaps WHERE id = $1 `, id).Scan( &r.ID, &r.TenantID, &r.NamespaceID, &r.Title, &r.Description, &r.Version, &r.AssessmentID, &r.PortfolioID, &r.Status, &r.TotalItems, &r.CompletedItems, &r.Progress, &r.StartDate, &r.TargetDate, &r.CreatedAt, &r.UpdatedAt, &r.CreatedBy, ) if err == pgx.ErrNoRows { return nil, nil } if err != nil { return nil, err } return &r, nil } // ListRoadmaps lists roadmaps for a tenant with optional filters func (s *Store) ListRoadmaps(ctx context.Context, tenantID uuid.UUID, filters *RoadmapFilters) ([]Roadmap, error) { query := ` SELECT id, tenant_id, namespace_id, title, description, version, assessment_id, portfolio_id, status, total_items, completed_items, progress, start_date, target_date, created_at, updated_at, created_by FROM roadmaps WHERE tenant_id = $1` args := []interface{}{tenantID} argIdx := 2 if filters != nil { if filters.Status != "" { query += fmt.Sprintf(" AND status = $%d", argIdx) args = append(args, filters.Status) argIdx++ } if filters.AssessmentID != nil { query += fmt.Sprintf(" AND assessment_id = $%d", argIdx) args = append(args, *filters.AssessmentID) argIdx++ } if filters.PortfolioID != nil { query += fmt.Sprintf(" AND portfolio_id = $%d", argIdx) args = append(args, *filters.PortfolioID) argIdx++ } } query += " ORDER BY created_at DESC" if filters != nil && filters.Limit > 0 { query += fmt.Sprintf(" LIMIT $%d", argIdx) args = append(args, filters.Limit) argIdx++ if filters.Offset > 0 { query += fmt.Sprintf(" OFFSET $%d", argIdx) args = append(args, filters.Offset) } } rows, err := s.pool.Query(ctx, query, args...) if err != nil { return nil, err } defer rows.Close() var roadmaps []Roadmap for rows.Next() { var r Roadmap err := rows.Scan( &r.ID, &r.TenantID, &r.NamespaceID, &r.Title, &r.Description, &r.Version, &r.AssessmentID, &r.PortfolioID, &r.Status, &r.TotalItems, &r.CompletedItems, &r.Progress, &r.StartDate, &r.TargetDate, &r.CreatedAt, &r.UpdatedAt, &r.CreatedBy, ) if err != nil { return nil, err } roadmaps = append(roadmaps, r) } return roadmaps, nil } // UpdateRoadmap updates a roadmap func (s *Store) UpdateRoadmap(ctx context.Context, r *Roadmap) error { r.UpdatedAt = time.Now().UTC() _, err := s.pool.Exec(ctx, ` UPDATE roadmaps SET title = $2, description = $3, version = $4, assessment_id = $5, portfolio_id = $6, status = $7, total_items = $8, completed_items = $9, progress = $10, start_date = $11, target_date = $12, updated_at = $13 WHERE id = $1 `, r.ID, r.Title, r.Description, r.Version, r.AssessmentID, r.PortfolioID, r.Status, r.TotalItems, r.CompletedItems, r.Progress, r.StartDate, r.TargetDate, r.UpdatedAt, ) return err } // DeleteRoadmap deletes a roadmap and its items func (s *Store) DeleteRoadmap(ctx context.Context, id uuid.UUID) error { // Delete items first _, err := s.pool.Exec(ctx, "DELETE FROM roadmap_items WHERE roadmap_id = $1", id) if err != nil { return err } // Delete roadmap _, err = s.pool.Exec(ctx, "DELETE FROM roadmaps WHERE id = $1", id) return err } // UpdateRoadmapProgress recalculates and updates roadmap progress func (s *Store) UpdateRoadmapProgress(ctx context.Context, roadmapID uuid.UUID) error { var total, completed int s.pool.QueryRow(ctx, "SELECT COUNT(*) FROM roadmap_items WHERE roadmap_id = $1", roadmapID).Scan(&total) s.pool.QueryRow(ctx, "SELECT COUNT(*) FROM roadmap_items WHERE roadmap_id = $1 AND status = 'COMPLETED'", roadmapID).Scan(&completed) progress := 0 if total > 0 { progress = (completed * 100) / total } _, err := s.pool.Exec(ctx, ` UPDATE roadmaps SET total_items = $2, completed_items = $3, progress = $4, updated_at = $5 WHERE id = $1 `, roadmapID, total, completed, progress, time.Now().UTC()) return err }