package portfolio import ( "context" "fmt" "time" "github.com/google/uuid" ) // ============================================================================ // Metrics and Statistics // ============================================================================ // RecalculateMetrics recalculates aggregated metrics for a portfolio func (s *Store) RecalculateMetrics(ctx context.Context, portfolioID uuid.UUID) error { // Count by type var totalAssessments, totalRoadmaps, totalWorkshops int s.pool.QueryRow(ctx, "SELECT COUNT(*) FROM portfolio_items WHERE portfolio_id = $1 AND item_type = 'ASSESSMENT'", portfolioID).Scan(&totalAssessments) s.pool.QueryRow(ctx, "SELECT COUNT(*) FROM portfolio_items WHERE portfolio_id = $1 AND item_type = 'ROADMAP'", portfolioID).Scan(&totalRoadmaps) s.pool.QueryRow(ctx, "SELECT COUNT(*) FROM portfolio_items WHERE portfolio_id = $1 AND item_type = 'WORKSHOP'", portfolioID).Scan(&totalWorkshops) // Calculate risk metrics from assessments var avgRiskScore float64 var highRiskCount, conditionalCount, approvedCount int s.pool.QueryRow(ctx, ` SELECT COALESCE(AVG(risk_score), 0) FROM portfolio_items WHERE portfolio_id = $1 AND item_type = 'ASSESSMENT' `, portfolioID).Scan(&avgRiskScore) s.pool.QueryRow(ctx, ` SELECT COUNT(*) FROM portfolio_items WHERE portfolio_id = $1 AND item_type = 'ASSESSMENT' AND risk_level IN ('HIGH', 'UNACCEPTABLE') `, portfolioID).Scan(&highRiskCount) s.pool.QueryRow(ctx, ` SELECT COUNT(*) FROM portfolio_items WHERE portfolio_id = $1 AND item_type = 'ASSESSMENT' AND feasibility = 'CONDITIONAL' `, portfolioID).Scan(&conditionalCount) s.pool.QueryRow(ctx, ` SELECT COUNT(*) FROM portfolio_items WHERE portfolio_id = $1 AND item_type = 'ASSESSMENT' AND feasibility = 'YES' `, portfolioID).Scan(&approvedCount) // Calculate compliance score (simplified: % of approved items) var complianceScore float64 if totalAssessments > 0 { complianceScore = (float64(approvedCount) / float64(totalAssessments)) * 100 } // Update portfolio _, err := s.pool.Exec(ctx, ` UPDATE portfolios SET total_assessments = $2, total_roadmaps = $3, total_workshops = $4, avg_risk_score = $5, high_risk_count = $6, conditional_count = $7, approved_count = $8, compliance_score = $9, updated_at = NOW() WHERE id = $1 `, portfolioID, totalAssessments, totalRoadmaps, totalWorkshops, avgRiskScore, highRiskCount, conditionalCount, approvedCount, complianceScore, ) return err } // GetPortfolioStats returns detailed statistics for a portfolio func (s *Store) GetPortfolioStats(ctx context.Context, portfolioID uuid.UUID) (*PortfolioStats, error) { stats := &PortfolioStats{ ItemsByType: make(map[ItemType]int), } // Total items s.pool.QueryRow(ctx, "SELECT COUNT(*) FROM portfolio_items WHERE portfolio_id = $1", portfolioID).Scan(&stats.TotalItems) // Items by type rows, _ := s.pool.Query(ctx, ` SELECT item_type, COUNT(*) FROM portfolio_items WHERE portfolio_id = $1 GROUP BY item_type `, portfolioID) if rows != nil { defer rows.Close() for rows.Next() { var itemType string var count int rows.Scan(&itemType, &count) stats.ItemsByType[ItemType(itemType)] = count } } // Risk distribution s.pool.QueryRow(ctx, ` SELECT COUNT(*) FROM portfolio_items WHERE portfolio_id = $1 AND risk_level = 'MINIMAL' `, portfolioID).Scan(&stats.RiskDistribution.Minimal) s.pool.QueryRow(ctx, ` SELECT COUNT(*) FROM portfolio_items WHERE portfolio_id = $1 AND risk_level = 'LOW' `, portfolioID).Scan(&stats.RiskDistribution.Low) s.pool.QueryRow(ctx, ` SELECT COUNT(*) FROM portfolio_items WHERE portfolio_id = $1 AND risk_level = 'MEDIUM' `, portfolioID).Scan(&stats.RiskDistribution.Medium) s.pool.QueryRow(ctx, ` SELECT COUNT(*) FROM portfolio_items WHERE portfolio_id = $1 AND risk_level = 'HIGH' `, portfolioID).Scan(&stats.RiskDistribution.High) s.pool.QueryRow(ctx, ` SELECT COUNT(*) FROM portfolio_items WHERE portfolio_id = $1 AND risk_level = 'UNACCEPTABLE' `, portfolioID).Scan(&stats.RiskDistribution.Unacceptable) // Feasibility distribution s.pool.QueryRow(ctx, ` SELECT COUNT(*) FROM portfolio_items WHERE portfolio_id = $1 AND feasibility = 'YES' `, portfolioID).Scan(&stats.FeasibilityDist.Yes) s.pool.QueryRow(ctx, ` SELECT COUNT(*) FROM portfolio_items WHERE portfolio_id = $1 AND feasibility = 'CONDITIONAL' `, portfolioID).Scan(&stats.FeasibilityDist.Conditional) s.pool.QueryRow(ctx, ` SELECT COUNT(*) FROM portfolio_items WHERE portfolio_id = $1 AND feasibility = 'NO' `, portfolioID).Scan(&stats.FeasibilityDist.No) // Average risk score s.pool.QueryRow(ctx, ` SELECT COALESCE(AVG(risk_score), 0) FROM portfolio_items WHERE portfolio_id = $1 AND item_type = 'ASSESSMENT' `, portfolioID).Scan(&stats.AvgRiskScore) // Compliance score s.pool.QueryRow(ctx, "SELECT compliance_score FROM portfolios WHERE id = $1", portfolioID).Scan(&stats.ComplianceScore) // DSFA required count — estimate from high risk items stats.DSFARequired = stats.RiskDistribution.High + stats.RiskDistribution.Unacceptable // Controls required (items with CONDITIONAL feasibility) stats.ControlsRequired = stats.FeasibilityDist.Conditional stats.LastUpdated = time.Now().UTC() return stats, nil } // GetPortfolioSummary returns a complete portfolio summary func (s *Store) GetPortfolioSummary(ctx context.Context, portfolioID uuid.UUID) (*PortfolioSummary, error) { portfolio, err := s.GetPortfolio(ctx, portfolioID) if err != nil || portfolio == nil { return nil, err } items, err := s.ListItems(ctx, portfolioID, nil) if err != nil { return nil, err } stats, err := s.GetPortfolioStats(ctx, portfolioID) if err != nil { return nil, err } return &PortfolioSummary{ Portfolio: portfolio, Items: items, RiskDistribution: stats.RiskDistribution, FeasibilityDist: stats.FeasibilityDist, }, nil } // ============================================================================ // Bulk Operations // ============================================================================ // BulkAddItems adds multiple items to a portfolio func (s *Store) BulkAddItems(ctx context.Context, portfolioID uuid.UUID, items []PortfolioItem, userID uuid.UUID) (*BulkAddItemsResponse, error) { result := &BulkAddItemsResponse{ Errors: []string{}, } for _, item := range items { item.PortfolioID = portfolioID item.AddedBy = userID // Fetch item info from source table if not provided if item.Title == "" { s.populateItemInfo(ctx, &item) } if err := s.AddItem(ctx, &item); err != nil { result.Skipped++ result.Errors = append(result.Errors, fmt.Sprintf("%s: %v", item.ItemID, err)) } else { result.Added++ } } return result, nil } // populateItemInfo fetches item metadata from the source table func (s *Store) populateItemInfo(ctx context.Context, item *PortfolioItem) { switch item.ItemType { case ItemTypeAssessment: s.pool.QueryRow(ctx, ` SELECT title, feasibility, risk_level, risk_score, status FROM ucca_assessments WHERE id = $1 `, item.ItemID).Scan(&item.Title, &item.Feasibility, &item.RiskLevel, &item.RiskScore, &item.Status) case ItemTypeRoadmap: s.pool.QueryRow(ctx, ` SELECT name, status FROM roadmaps WHERE id = $1 `, item.ItemID).Scan(&item.Title, &item.Status) case ItemTypeWorkshop: s.pool.QueryRow(ctx, ` SELECT title, status FROM workshop_sessions WHERE id = $1 `, item.ItemID).Scan(&item.Title, &item.Status) } } // ============================================================================ // Activity Tracking // ============================================================================ // LogActivity logs an activity entry for a portfolio func (s *Store) LogActivity(ctx context.Context, portfolioID uuid.UUID, entry *ActivityEntry) error { _, err := s.pool.Exec(ctx, ` INSERT INTO portfolio_activity ( id, portfolio_id, timestamp, action, item_type, item_id, item_title, user_id ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8 ) `, uuid.New(), portfolioID, entry.Timestamp, entry.Action, string(entry.ItemType), entry.ItemID, entry.ItemTitle, entry.UserID, ) return err } // GetRecentActivity retrieves recent activity for a portfolio func (s *Store) GetRecentActivity(ctx context.Context, portfolioID uuid.UUID, limit int) ([]ActivityEntry, error) { if limit <= 0 { limit = 20 } rows, err := s.pool.Query(ctx, ` SELECT timestamp, action, item_type, item_id, item_title, user_id FROM portfolio_activity WHERE portfolio_id = $1 ORDER BY timestamp DESC LIMIT $2 `, portfolioID, limit) if err != nil { return nil, err } defer rows.Close() var activities []ActivityEntry for rows.Next() { var entry ActivityEntry var itemType string err := rows.Scan( &entry.Timestamp, &entry.Action, &itemType, &entry.ItemID, &entry.ItemTitle, &entry.UserID, ) if err != nil { return nil, err } entry.ItemType = ItemType(itemType) activities = append(activities, entry) } return activities, nil }