This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/ai-compliance-sdk/internal/portfolio/store.go
Benjamin Admin 21a844cb8a fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

819 lines
23 KiB
Go

package portfolio
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
// Store handles portfolio data persistence
type Store struct {
pool *pgxpool.Pool
}
// NewStore creates a new portfolio store
func NewStore(pool *pgxpool.Pool) *Store {
return &Store{pool: pool}
}
// ============================================================================
// Portfolio CRUD Operations
// ============================================================================
// CreatePortfolio creates a new portfolio
func (s *Store) CreatePortfolio(ctx context.Context, p *Portfolio) error {
p.ID = uuid.New()
p.CreatedAt = time.Now().UTC()
p.UpdatedAt = p.CreatedAt
if p.Status == "" {
p.Status = PortfolioStatusDraft
}
settings, _ := json.Marshal(p.Settings)
_, err := s.pool.Exec(ctx, `
INSERT INTO portfolios (
id, tenant_id, namespace_id,
name, description, status,
department, business_unit, owner, owner_email,
total_assessments, total_roadmaps, total_workshops,
avg_risk_score, high_risk_count, conditional_count, approved_count,
compliance_score, settings,
created_at, updated_at, created_by, approved_at, approved_by
) VALUES (
$1, $2, $3,
$4, $5, $6,
$7, $8, $9, $10,
$11, $12, $13,
$14, $15, $16, $17,
$18, $19,
$20, $21, $22, $23, $24
)
`,
p.ID, p.TenantID, p.NamespaceID,
p.Name, p.Description, string(p.Status),
p.Department, p.BusinessUnit, p.Owner, p.OwnerEmail,
p.TotalAssessments, p.TotalRoadmaps, p.TotalWorkshops,
p.AvgRiskScore, p.HighRiskCount, p.ConditionalCount, p.ApprovedCount,
p.ComplianceScore, settings,
p.CreatedAt, p.UpdatedAt, p.CreatedBy, p.ApprovedAt, p.ApprovedBy,
)
return err
}
// GetPortfolio retrieves a portfolio by ID
func (s *Store) GetPortfolio(ctx context.Context, id uuid.UUID) (*Portfolio, error) {
var p Portfolio
var status string
var settings []byte
err := s.pool.QueryRow(ctx, `
SELECT
id, tenant_id, namespace_id,
name, description, status,
department, business_unit, owner, owner_email,
total_assessments, total_roadmaps, total_workshops,
avg_risk_score, high_risk_count, conditional_count, approved_count,
compliance_score, settings,
created_at, updated_at, created_by, approved_at, approved_by
FROM portfolios WHERE id = $1
`, id).Scan(
&p.ID, &p.TenantID, &p.NamespaceID,
&p.Name, &p.Description, &status,
&p.Department, &p.BusinessUnit, &p.Owner, &p.OwnerEmail,
&p.TotalAssessments, &p.TotalRoadmaps, &p.TotalWorkshops,
&p.AvgRiskScore, &p.HighRiskCount, &p.ConditionalCount, &p.ApprovedCount,
&p.ComplianceScore, &settings,
&p.CreatedAt, &p.UpdatedAt, &p.CreatedBy, &p.ApprovedAt, &p.ApprovedBy,
)
if err == pgx.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
p.Status = PortfolioStatus(status)
json.Unmarshal(settings, &p.Settings)
return &p, nil
}
// ListPortfolios lists portfolios for a tenant with optional filters
func (s *Store) ListPortfolios(ctx context.Context, tenantID uuid.UUID, filters *PortfolioFilters) ([]Portfolio, error) {
query := `
SELECT
id, tenant_id, namespace_id,
name, description, status,
department, business_unit, owner, owner_email,
total_assessments, total_roadmaps, total_workshops,
avg_risk_score, high_risk_count, conditional_count, approved_count,
compliance_score, settings,
created_at, updated_at, created_by, approved_at, approved_by
FROM portfolios 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, string(filters.Status))
argIdx++
}
if filters.Department != "" {
query += fmt.Sprintf(" AND department = $%d", argIdx)
args = append(args, filters.Department)
argIdx++
}
if filters.BusinessUnit != "" {
query += fmt.Sprintf(" AND business_unit = $%d", argIdx)
args = append(args, filters.BusinessUnit)
argIdx++
}
if filters.Owner != "" {
query += fmt.Sprintf(" AND owner ILIKE $%d", argIdx)
args = append(args, "%"+filters.Owner+"%")
argIdx++
}
if filters.MinRiskScore != nil {
query += fmt.Sprintf(" AND avg_risk_score >= $%d", argIdx)
args = append(args, *filters.MinRiskScore)
argIdx++
}
if filters.MaxRiskScore != nil {
query += fmt.Sprintf(" AND avg_risk_score <= $%d", argIdx)
args = append(args, *filters.MaxRiskScore)
argIdx++
}
}
query += " ORDER BY updated_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 portfolios []Portfolio
for rows.Next() {
var p Portfolio
var status string
var settings []byte
err := rows.Scan(
&p.ID, &p.TenantID, &p.NamespaceID,
&p.Name, &p.Description, &status,
&p.Department, &p.BusinessUnit, &p.Owner, &p.OwnerEmail,
&p.TotalAssessments, &p.TotalRoadmaps, &p.TotalWorkshops,
&p.AvgRiskScore, &p.HighRiskCount, &p.ConditionalCount, &p.ApprovedCount,
&p.ComplianceScore, &settings,
&p.CreatedAt, &p.UpdatedAt, &p.CreatedBy, &p.ApprovedAt, &p.ApprovedBy,
)
if err != nil {
return nil, err
}
p.Status = PortfolioStatus(status)
json.Unmarshal(settings, &p.Settings)
portfolios = append(portfolios, p)
}
return portfolios, nil
}
// UpdatePortfolio updates a portfolio
func (s *Store) UpdatePortfolio(ctx context.Context, p *Portfolio) error {
p.UpdatedAt = time.Now().UTC()
settings, _ := json.Marshal(p.Settings)
_, err := s.pool.Exec(ctx, `
UPDATE portfolios SET
name = $2, description = $3, status = $4,
department = $5, business_unit = $6, owner = $7, owner_email = $8,
settings = $9,
updated_at = $10, approved_at = $11, approved_by = $12
WHERE id = $1
`,
p.ID, p.Name, p.Description, string(p.Status),
p.Department, p.BusinessUnit, p.Owner, p.OwnerEmail,
settings,
p.UpdatedAt, p.ApprovedAt, p.ApprovedBy,
)
return err
}
// DeletePortfolio deletes a portfolio and its items
func (s *Store) DeletePortfolio(ctx context.Context, id uuid.UUID) error {
// Delete items first
_, err := s.pool.Exec(ctx, "DELETE FROM portfolio_items WHERE portfolio_id = $1", id)
if err != nil {
return err
}
// Delete portfolio
_, err = s.pool.Exec(ctx, "DELETE FROM portfolios WHERE id = $1", id)
return err
}
// ============================================================================
// Portfolio Item Operations
// ============================================================================
// AddItem adds an item to a portfolio
func (s *Store) AddItem(ctx context.Context, item *PortfolioItem) error {
item.ID = uuid.New()
item.AddedAt = time.Now().UTC()
tags, _ := json.Marshal(item.Tags)
// Check if item already exists in portfolio
var exists bool
s.pool.QueryRow(ctx,
"SELECT EXISTS(SELECT 1 FROM portfolio_items WHERE portfolio_id = $1 AND item_id = $2)",
item.PortfolioID, item.ItemID).Scan(&exists)
if exists {
return fmt.Errorf("item already exists in portfolio")
}
// Get max sort order
var maxSort int
s.pool.QueryRow(ctx,
"SELECT COALESCE(MAX(sort_order), 0) FROM portfolio_items WHERE portfolio_id = $1",
item.PortfolioID).Scan(&maxSort)
item.SortOrder = maxSort + 1
_, err := s.pool.Exec(ctx, `
INSERT INTO portfolio_items (
id, portfolio_id, item_type, item_id,
title, status, risk_level, risk_score, feasibility,
sort_order, tags, notes,
added_at, added_by
) VALUES (
$1, $2, $3, $4,
$5, $6, $7, $8, $9,
$10, $11, $12,
$13, $14
)
`,
item.ID, item.PortfolioID, string(item.ItemType), item.ItemID,
item.Title, item.Status, item.RiskLevel, item.RiskScore, item.Feasibility,
item.SortOrder, tags, item.Notes,
item.AddedAt, item.AddedBy,
)
if err != nil {
return err
}
// Update portfolio metrics
return s.RecalculateMetrics(ctx, item.PortfolioID)
}
// GetItem retrieves a portfolio item by ID
func (s *Store) GetItem(ctx context.Context, id uuid.UUID) (*PortfolioItem, error) {
var item PortfolioItem
var itemType string
var tags []byte
err := s.pool.QueryRow(ctx, `
SELECT
id, portfolio_id, item_type, item_id,
title, status, risk_level, risk_score, feasibility,
sort_order, tags, notes,
added_at, added_by
FROM portfolio_items WHERE id = $1
`, id).Scan(
&item.ID, &item.PortfolioID, &itemType, &item.ItemID,
&item.Title, &item.Status, &item.RiskLevel, &item.RiskScore, &item.Feasibility,
&item.SortOrder, &tags, &item.Notes,
&item.AddedAt, &item.AddedBy,
)
if err == pgx.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
item.ItemType = ItemType(itemType)
json.Unmarshal(tags, &item.Tags)
return &item, nil
}
// ListItems lists items in a portfolio
func (s *Store) ListItems(ctx context.Context, portfolioID uuid.UUID, itemType *ItemType) ([]PortfolioItem, error) {
query := `
SELECT
id, portfolio_id, item_type, item_id,
title, status, risk_level, risk_score, feasibility,
sort_order, tags, notes,
added_at, added_by
FROM portfolio_items WHERE portfolio_id = $1`
args := []interface{}{portfolioID}
if itemType != nil {
query += " AND item_type = $2"
args = append(args, string(*itemType))
}
query += " ORDER BY sort_order ASC"
rows, err := s.pool.Query(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var items []PortfolioItem
for rows.Next() {
var item PortfolioItem
var iType string
var tags []byte
err := rows.Scan(
&item.ID, &item.PortfolioID, &iType, &item.ItemID,
&item.Title, &item.Status, &item.RiskLevel, &item.RiskScore, &item.Feasibility,
&item.SortOrder, &tags, &item.Notes,
&item.AddedAt, &item.AddedBy,
)
if err != nil {
return nil, err
}
item.ItemType = ItemType(iType)
json.Unmarshal(tags, &item.Tags)
items = append(items, item)
}
return items, nil
}
// RemoveItem removes an item from a portfolio
func (s *Store) RemoveItem(ctx context.Context, id uuid.UUID) error {
// Get portfolio ID first
var portfolioID uuid.UUID
err := s.pool.QueryRow(ctx,
"SELECT portfolio_id FROM portfolio_items WHERE id = $1", id).Scan(&portfolioID)
if err != nil {
return err
}
_, err = s.pool.Exec(ctx, "DELETE FROM portfolio_items WHERE id = $1", id)
if err != nil {
return err
}
// Recalculate metrics
return s.RecalculateMetrics(ctx, portfolioID)
}
// UpdateItemOrder updates the sort order of items
func (s *Store) UpdateItemOrder(ctx context.Context, portfolioID uuid.UUID, itemIDs []uuid.UUID) error {
for i, id := range itemIDs {
_, err := s.pool.Exec(ctx,
"UPDATE portfolio_items SET sort_order = $2 WHERE id = $1 AND portfolio_id = $3",
id, i+1, portfolioID)
if err != nil {
return err
}
}
return nil
}
// ============================================================================
// Merge Operations
// ============================================================================
// MergePortfolios merges source portfolio into target
func (s *Store) MergePortfolios(ctx context.Context, req *MergeRequest, userID uuid.UUID) (*MergeResult, error) {
result := &MergeResult{
ConflictsResolved: []MergeConflict{},
}
// Get source items
sourceItems, err := s.ListItems(ctx, req.SourcePortfolioID, nil)
if err != nil {
return nil, fmt.Errorf("failed to get source items: %w", err)
}
// Get target items for conflict detection
targetItems, err := s.ListItems(ctx, req.TargetPortfolioID, nil)
if err != nil {
return nil, fmt.Errorf("failed to get target items: %w", err)
}
// Build map of existing items in target
targetItemMap := make(map[uuid.UUID]bool)
for _, item := range targetItems {
targetItemMap[item.ItemID] = true
}
// Filter items based on strategy and options
for _, item := range sourceItems {
// Skip if not including this type
if item.ItemType == ItemTypeRoadmap && !req.IncludeRoadmaps {
result.ItemsSkipped++
continue
}
if item.ItemType == ItemTypeWorkshop && !req.IncludeWorkshops {
result.ItemsSkipped++
continue
}
// Check for conflicts
if targetItemMap[item.ItemID] {
switch req.Strategy {
case MergeStrategyUnion:
// Skip duplicates
result.ItemsSkipped++
result.ConflictsResolved = append(result.ConflictsResolved, MergeConflict{
ItemID: item.ItemID,
ItemType: item.ItemType,
Reason: "duplicate",
Resolution: "kept_target",
})
case MergeStrategyReplace:
// Update existing item in target
// For now, just skip (could implement update logic)
result.ItemsUpdated++
result.ConflictsResolved = append(result.ConflictsResolved, MergeConflict{
ItemID: item.ItemID,
ItemType: item.ItemType,
Reason: "duplicate",
Resolution: "merged",
})
case MergeStrategyIntersect:
// Keep only items that exist in both
// Skip items not in target
result.ItemsSkipped++
}
continue
}
// Add item to target
newItem := &PortfolioItem{
PortfolioID: req.TargetPortfolioID,
ItemType: item.ItemType,
ItemID: item.ItemID,
Title: item.Title,
Status: item.Status,
RiskLevel: item.RiskLevel,
RiskScore: item.RiskScore,
Feasibility: item.Feasibility,
Tags: item.Tags,
Notes: item.Notes,
AddedBy: userID,
}
if err := s.AddItem(ctx, newItem); err != nil {
// Skip on error but continue
result.ItemsSkipped++
continue
}
result.ItemsAdded++
}
// Delete source if requested
if req.DeleteSource {
if err := s.DeletePortfolio(ctx, req.SourcePortfolioID); err != nil {
return nil, fmt.Errorf("failed to delete source portfolio: %w", err)
}
result.SourceDeleted = true
}
// Get updated target portfolio
result.TargetPortfolio, _ = s.GetPortfolio(ctx, req.TargetPortfolioID)
return result, nil
}
// ============================================================================
// 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
// This would need to join with ucca_assessments to get dsfa_recommended
// For now, 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
}