portfolio/store.go (818 LOC) → store_portfolio.go, store_items.go, store_metrics.go workshop/store.go (793 LOC) → store_sessions.go, store_participants.go, store_responses.go training/models.go (757 LOC) → models_enums.go, models_core.go, models_api.go, models_blocks.go roadmap/store.go (757 LOC) → store_roadmap.go, store_items.go, store_import.go All files under 350 LOC. Zero behavior changes, same package declarations. go vet passes on all five packages. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
214 lines
5.7 KiB
Go
214 lines
5.7 KiB
Go
package roadmap
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
// ============================================================================
|
|
// Import Job Operations
|
|
// ============================================================================
|
|
|
|
// CreateImportJob creates a new import job
|
|
func (s *Store) CreateImportJob(ctx context.Context, job *ImportJob) error {
|
|
job.ID = uuid.New()
|
|
job.CreatedAt = time.Now().UTC()
|
|
job.UpdatedAt = job.CreatedAt
|
|
if job.Status == "" {
|
|
job.Status = "pending"
|
|
}
|
|
|
|
parsedItems, _ := json.Marshal(job.ParsedItems)
|
|
|
|
_, err := s.pool.Exec(ctx, `
|
|
INSERT INTO roadmap_import_jobs (
|
|
id, tenant_id, roadmap_id,
|
|
filename, format, file_size, content_type,
|
|
status, error_message,
|
|
total_rows, valid_rows, invalid_rows, imported_items,
|
|
parsed_items,
|
|
created_at, updated_at, completed_at, created_by
|
|
) VALUES (
|
|
$1, $2, $3,
|
|
$4, $5, $6, $7,
|
|
$8, $9,
|
|
$10, $11, $12, $13,
|
|
$14,
|
|
$15, $16, $17, $18
|
|
)
|
|
`,
|
|
job.ID, job.TenantID, job.RoadmapID,
|
|
job.Filename, string(job.Format), job.FileSize, job.ContentType,
|
|
job.Status, job.ErrorMessage,
|
|
job.TotalRows, job.ValidRows, job.InvalidRows, job.ImportedItems,
|
|
parsedItems,
|
|
job.CreatedAt, job.UpdatedAt, job.CompletedAt, job.CreatedBy,
|
|
)
|
|
|
|
return err
|
|
}
|
|
|
|
// GetImportJob retrieves an import job by ID
|
|
func (s *Store) GetImportJob(ctx context.Context, id uuid.UUID) (*ImportJob, error) {
|
|
var job ImportJob
|
|
var format string
|
|
var parsedItems []byte
|
|
|
|
err := s.pool.QueryRow(ctx, `
|
|
SELECT
|
|
id, tenant_id, roadmap_id,
|
|
filename, format, file_size, content_type,
|
|
status, error_message,
|
|
total_rows, valid_rows, invalid_rows, imported_items,
|
|
parsed_items,
|
|
created_at, updated_at, completed_at, created_by
|
|
FROM roadmap_import_jobs WHERE id = $1
|
|
`, id).Scan(
|
|
&job.ID, &job.TenantID, &job.RoadmapID,
|
|
&job.Filename, &format, &job.FileSize, &job.ContentType,
|
|
&job.Status, &job.ErrorMessage,
|
|
&job.TotalRows, &job.ValidRows, &job.InvalidRows, &job.ImportedItems,
|
|
&parsedItems,
|
|
&job.CreatedAt, &job.UpdatedAt, &job.CompletedAt, &job.CreatedBy,
|
|
)
|
|
|
|
if err == pgx.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
job.Format = ImportFormat(format)
|
|
json.Unmarshal(parsedItems, &job.ParsedItems)
|
|
|
|
return &job, nil
|
|
}
|
|
|
|
// UpdateImportJob updates an import job
|
|
func (s *Store) UpdateImportJob(ctx context.Context, job *ImportJob) error {
|
|
job.UpdatedAt = time.Now().UTC()
|
|
|
|
parsedItems, _ := json.Marshal(job.ParsedItems)
|
|
|
|
_, err := s.pool.Exec(ctx, `
|
|
UPDATE roadmap_import_jobs SET
|
|
roadmap_id = $2,
|
|
status = $3, error_message = $4,
|
|
total_rows = $5, valid_rows = $6, invalid_rows = $7, imported_items = $8,
|
|
parsed_items = $9,
|
|
updated_at = $10, completed_at = $11
|
|
WHERE id = $1
|
|
`,
|
|
job.ID, job.RoadmapID,
|
|
job.Status, job.ErrorMessage,
|
|
job.TotalRows, job.ValidRows, job.InvalidRows, job.ImportedItems,
|
|
parsedItems,
|
|
job.UpdatedAt, job.CompletedAt,
|
|
)
|
|
|
|
return err
|
|
}
|
|
|
|
// ============================================================================
|
|
// Statistics
|
|
// ============================================================================
|
|
|
|
// GetRoadmapStats returns statistics for a roadmap
|
|
func (s *Store) GetRoadmapStats(ctx context.Context, roadmapID uuid.UUID) (*RoadmapStats, error) {
|
|
stats := &RoadmapStats{
|
|
ByStatus: make(map[string]int),
|
|
ByPriority: make(map[string]int),
|
|
ByCategory: make(map[string]int),
|
|
ByDepartment: make(map[string]int),
|
|
}
|
|
|
|
// Total count
|
|
s.pool.QueryRow(ctx,
|
|
"SELECT COUNT(*) FROM roadmap_items WHERE roadmap_id = $1",
|
|
roadmapID).Scan(&stats.TotalItems)
|
|
|
|
// By status
|
|
rows, err := s.pool.Query(ctx,
|
|
"SELECT status, COUNT(*) FROM roadmap_items WHERE roadmap_id = $1 GROUP BY status",
|
|
roadmapID)
|
|
if err == nil {
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var status string
|
|
var count int
|
|
rows.Scan(&status, &count)
|
|
stats.ByStatus[status] = count
|
|
}
|
|
}
|
|
|
|
// By priority
|
|
rows, err = s.pool.Query(ctx,
|
|
"SELECT priority, COUNT(*) FROM roadmap_items WHERE roadmap_id = $1 GROUP BY priority",
|
|
roadmapID)
|
|
if err == nil {
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var priority string
|
|
var count int
|
|
rows.Scan(&priority, &count)
|
|
stats.ByPriority[priority] = count
|
|
}
|
|
}
|
|
|
|
// By category
|
|
rows, err = s.pool.Query(ctx,
|
|
"SELECT category, COUNT(*) FROM roadmap_items WHERE roadmap_id = $1 GROUP BY category",
|
|
roadmapID)
|
|
if err == nil {
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var category string
|
|
var count int
|
|
rows.Scan(&category, &count)
|
|
stats.ByCategory[category] = count
|
|
}
|
|
}
|
|
|
|
// By department
|
|
rows, err = s.pool.Query(ctx,
|
|
"SELECT COALESCE(department, 'Unassigned'), COUNT(*) FROM roadmap_items WHERE roadmap_id = $1 GROUP BY department",
|
|
roadmapID)
|
|
if err == nil {
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var dept string
|
|
var count int
|
|
rows.Scan(&dept, &count)
|
|
stats.ByDepartment[dept] = count
|
|
}
|
|
}
|
|
|
|
// Overdue items
|
|
s.pool.QueryRow(ctx,
|
|
"SELECT COUNT(*) FROM roadmap_items WHERE roadmap_id = $1 AND planned_end < NOW() AND status NOT IN ('COMPLETED', 'DEFERRED')",
|
|
roadmapID).Scan(&stats.OverdueItems)
|
|
|
|
// Upcoming items (next 7 days)
|
|
s.pool.QueryRow(ctx,
|
|
"SELECT COUNT(*) FROM roadmap_items WHERE roadmap_id = $1 AND planned_end BETWEEN NOW() AND NOW() + INTERVAL '7 days' AND status NOT IN ('COMPLETED', 'DEFERRED')",
|
|
roadmapID).Scan(&stats.UpcomingItems)
|
|
|
|
// Total effort
|
|
s.pool.QueryRow(ctx,
|
|
"SELECT COALESCE(SUM(effort_days), 0) FROM roadmap_items WHERE roadmap_id = $1",
|
|
roadmapID).Scan(&stats.TotalEffortDays)
|
|
|
|
// Progress
|
|
completedCount := stats.ByStatus[string(ItemStatusCompleted)]
|
|
if stats.TotalItems > 0 {
|
|
stats.Progress = (completedCount * 100) / stats.TotalItems
|
|
}
|
|
|
|
return stats, nil
|
|
}
|