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 }