project = $project; $this->filters = $filters; $this->data = $data; } public function sheets(): array { $sheets = [ new SummarySheet($this->data['summary'] ?? [], $this->project, $this->filters), ]; if (!empty($this->data['phases'])) { $sheets[] = new PhasesSheet($this->data['phases']); } if (!empty($this->data['features'])) { $sheets[] = new FeaturesSheet($this->data['features']); } if (!empty($this->data['inspections'])) { $sheets[] = new InspectionsSheet($this->data['inspections']); } if (!empty($this->data['issues'])) { $sheets[] = new IssuesSheet($this->data['issues']); } if (!empty($this->data['tasks'])) { $sheets[] = new TasksSheet($this->data['tasks']); } if (!empty($this->data['deviations'])) { $sheets[] = new DeviationsSheet($this->data['deviations']); } if (!empty($this->data['media'])) { $sheets[] = new MediaSheet($this->data['media']); } if (!empty($this->data['progress_curve'])) { $sheets[] = new ProgressCurveSheet($this->data['progress_curve']); } // Parameters sheet $sheets[] = new ParametersSheet($this->filters, $this->project); return $sheets; } } // ============================================================ // Base Sheet with common styling // ============================================================ abstract class BaseSheet implements FromArray, WithHeadings, WithStyles, WithColumnWidths, WithTitle { protected array $rows = []; public function __construct(array $data = []) { $this->rows = $data; } public function array(): array { return $this->rows; } abstract public function headings(): array; public function title(): string { return static::class; } public function styles(Worksheet $sheet) { // Header row style $sheet->getStyle('1:1')->applyFromArray([ 'font' => ['bold' => true, 'color' => ['rgb' => 'FFFFFF'], 'size' => 11], 'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => '1E3A8A']], 'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'wrapText' => true], 'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]], ]); // Data rows $highestRow = $sheet->getHighestRow(); if ($highestRow > 1) { $sheet->getStyle("2:{$highestRow}")->applyFromArray([ 'font' => ['size' => 10], 'alignment' => ['vertical' => Alignment::VERTICAL_CENTER, 'wrapText' => true], 'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]], ]); // Alternate row colors for ($row = 2; $row <= $highestRow; $row++) { if ($row % 2 === 0) { $sheet->getStyle("{$row}:{$row}")->applyFromArray([ 'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => 'F8FAFC']], ]); } } } // Auto-filter $sheet->setAutoFilter($sheet->calculateWorksheetDimension()); // Freeze header row $sheet->freezePane('A2'); } public function columnWidths(): array { return []; } } // ============================================================ // Summary Sheet // ============================================================ class SummarySheet extends BaseSheet { protected Project $project; protected ReportFilters $filters; public function __construct(array $summary, Project $project, ReportFilters $filters) { $this->project = $project; $this->filters = $filters; $rows = [ ['Informe de Proyecto', $project->name], ['Referencia', $project->reference ?? '—'], ['Dirección', $project->address ?? '—'], ['Estado', $project->status ?? '—'], ['Fecha inicio', $project->start_date?->format('d/m/Y') ?? '—'], ['Fecha fin estimada', $project->end_date_estimated?->format('d/m/Y') ?? '—'], ['', ''], ['Período del informe', $filters->getDateRangeLabel()], ['Generado el', now()->format('d/m/Y H:i')], ['Generado por', auth()->guard()->user()?->name ?? 'Sistema'], ['', ''], ['--- RESUMEN EJECUTIVO ---', ''], ['Total elementos', $summary['total_features'] ?? 0], ['Elementos completados', $summary['completed_features'] ?? 0], ['Tasa completitud (%)', $summary['completion_rate'] ?? 0], ['Progreso planificado medio (%)', $summary['avg_planned_progress'] ?? 0], ['Progreso real medio (%)', $summary['avg_actual_progress'] ?? 0], ['SPI global', $summary['overall_spi'] ?? 'N/A'], ['', ''], ['--- INSPECCIONES ---', ''], ['Total inspecciones', $summary['total_inspections'] ?? 0], ['Aprobadas', $summary['passed_inspections'] ?? 0], ['Fallidas', $summary['failed_inspections'] ?? 0], ['Tasa aprobación (%)', $summary['pass_rate'] ?? 0], ['', ''], ['--- INCIDENCIAS ---', ''], ['Abiertas', $summary['open_issues'] ?? 0], ['Cerradas', $summary['closed_issues'] ?? 0], ['', ''], ['--- TAREAS ---', ''], ['Total tareas', $summary['total_tasks'] ?? 0], ['Completadas', $summary['completed_tasks'] ?? 0], ['Tasa completitud (%)', $summary['task_completion_rate'] ?? 0], ['', ''], ['--- FASES ---', ''], ['En plazo', $summary['phases_on_track'] ?? 0], ['Retrasadas', $summary['phases_delayed'] ?? 0], ['Sin datos', $summary['phases_no_data'] ?? 0], ]; parent::__construct($rows); } public function headings(): array { return ['Concepto', 'Valor']; } public function columnWidths(): array { return ['A' => 40, 'B' => 30]; } } // ============================================================ // Phases Sheet // ============================================================ class PhasesSheet extends BaseSheet { public function headings(): array { return [ 'ID', 'Fase', 'Orden', 'Color', 'Inicio Plan', 'Fin Plan', 'Inicio Real', 'Fin Real', 'Progreso (%)', 'Progreso Plan (%)', 'Desvío Fin (días)', 'Desvío Inicio (días)', 'SPI', 'En Plazo', 'Elementos', 'Completados', 'Capas' ]; } public function array(): array { return array_map(function ($phase) { return [ $phase['id'], $phase['name'], $phase['order'], $phase['color'], $phase['planned_start'] ?? '—', $phase['planned_end'] ?? '—', $phase['actual_start'] ?? '—', $phase['actual_end'] ?? '—', $phase['progress_percent'], $phase['planned_progress'], $phase['deviation_days'] ?? '—', $phase['start_deviation_days'] ?? '—', $phase['spi'] ?? '—', $phase['is_on_track'] === true ? 'Sí' : ($phase['is_on_track'] === false ? 'No' : 'N/A'), $phase['features_count'], $phase['completed_features'], count($phase['layers']), ]; }, $this->rows); } public function columnWidths(): array { return ['A' => 8, 'B' => 25, 'C' => 8, 'D' => 10, 'E' => 12, 'F' => 12, 'G' => 12, 'H' => 12, 'I' => 12, 'J' => 14, 'K' => 14, 'L' => 14, 'M' => 8, 'N' => 10, 'O' => 10, 'P' => 12, 'Q' => 10]; } } // ============================================================ // Features Sheet // ============================================================ class FeaturesSheet extends BaseSheet { public function headings(): array { return [ 'ID', 'Elemento', 'Fase', 'Capa', 'Estado', 'Progreso (%)', 'Progreso Plan (%)', 'Inicio Plan', 'Fin Plan', 'Inicio Real', 'Fin Real', 'Desvío Fin (días)', 'Desvío Inicio (días)', 'SPI', 'En Plazo', 'Responsable', 'Template', 'Última Inspección', 'Resultado', 'Inspecciones', 'Issues Abiertos' ]; } public function array(): array { return array_map(function ($feature) { return [ $feature['id'], $feature['name'], $feature['phase'], $feature['layer'], $feature['status_label'], $feature['progress'], $feature['planned_progress'], $feature['planned_start'] ?? '—', $feature['planned_end'] ?? '—', $feature['actual_start'] ?? '—', $feature['actual_end'] ?? '—', $feature['deviation_days'] ?? '—', $feature['start_deviation_days'] ?? '—', $feature['spi'] ?? '—', $feature['is_on_track'] === true ? 'Sí' : ($feature['is_on_track'] === false ? 'No' : 'N/A'), $feature['responsible'], $feature['template'], $feature['last_inspection_date'] ?? '—', $feature['last_inspection_result'] ?? '—', $feature['inspections_count'], $feature['open_issues_count'], ]; }, $this->rows); } public function columnWidths(): array { return ['A' => 8, 'B' => 25, 'C' => 20, 'D' => 20, 'E' => 15, 'F' => 12, 'G' => 14, 'H' => 12, 'I' => 12, 'J' => 12, 'K' => 12, 'L' => 14, 'M' => 14, 'N' => 8, 'O' => 10, 'P' => 20, 'Q' => 20, 'R' => 14, 'S' => 12, 'T' => 12, 'U' => 12]; } } // ============================================================ // Inspections Sheet // ============================================================ class InspectionsSheet extends BaseSheet { public function headings(): array { return [ 'ID', 'Elemento', 'Fase', 'Template', 'Inspector', 'Fecha', 'Estado', 'Resultado', 'Notas', 'Fotos' ]; } public function array(): array { return array_map(function ($inspection) { return [ $inspection['id'], $inspection['feature'], $inspection['phase'], $inspection['template'], $inspection['inspector'], $inspection['date'], $inspection['status'], $inspection['result_label'], $inspection['notes'] ?? '—', $inspection['photos_count'], ]; }, $this->rows); } public function columnWidths(): array { return ['A' => 8, 'B' => 25, 'C' => 20, 'D' => 25, 'E' => 20, 'F' => 18, 'G' => 12, 'H' => 12, 'I' => 40, 'J' => 8]; } } // ============================================================ // Issues Sheet // ============================================================ class IssuesSheet extends BaseSheet { public function headings(): array { return [ 'ID', 'Título', 'Elemento', 'Fase', 'Prioridad', 'Estado', 'Reportado por', 'Asignado a', 'Creado', 'Cerrado', 'Días abierto', 'Tareas total', 'Tareas completadas' ]; } public function array(): array { return array_map(function ($issue) { return [ $issue['id'], $issue['title'], $issue['feature'], $issue['phase'], $issue['priority_label'], $issue['status_label'], $issue['reporter'], $issue['assignee'], $issue['created_at'], $issue['closed_at'] ?? '—', $issue['days_open'], $issue['tasks_total'], $issue['tasks_completed'], ]; }, $this->rows); } public function columnWidths(): array { return ['A' => 8, 'B' => 35, 'C' => 20, 'D' => 20, 'E' => 12, 'F' => 15, 'G' => 20, 'H' => 20, 'I' => 12, 'J' => 12, 'K' => 12, 'L' => 12, 'M' => 14]; } } // ============================================================ // Tasks Sheet // ============================================================ class TasksSheet extends BaseSheet { public function headings(): array { return [ 'ID', 'Tarea', 'Fase', 'Estado', 'Prioridad', 'Asignado', 'Creador', 'Fecha inicio', 'Fecha fin', 'Completada', 'Horas est.', 'Horas real', 'Progreso (%)', 'Vencida', 'Subtareas' ]; } public function array(): array { return array_map(function ($task) { $subtasksStr = implode('; ', array_map(fn($st) => "{$st['title']} ({$st['status']})", $task['subtasks'])); return [ $task['id'], $task['title'], $task['phase'], $task['status_label'], $task['priority_label'], $task['assignee'], $task['creator'], $task['start_date'] ?? '—', $task['due_date'] ?? '—', $task['completed_at'] ?? '—', $task['estimated_hours'] ?? '—', $task['actual_hours'] ?? '—', $task['progress'], $task['is_overdue'] ? 'Sí' : 'No', $subtasksStr ?: '—', ]; }, $this->rows); } public function columnWidths(): array { return ['A' => 8, 'B' => 30, 'C' => 20, 'D' => 15, 'E' => 12, 'F' => 20, 'G' => 20, 'H' => 12, 'I' => 12, 'J' => 12, 'K' => 10, 'L' => 10, 'M' => 12, 'N' => 10, 'O' => 40]; } } // ============================================================ // Deviations Sheet // ============================================================ class DeviationsSheet extends BaseSheet { protected array $deviations; public function __construct(array $deviations) { $this->deviations = $deviations; $rows = []; // Phase deviations if (!empty($deviations['phases'])) { $rows[] = ['=== DESVÍOS POR FASE ===', '', '', '', '', '', '', '', '', '', '']; $rows[] = ['ID', 'Fase', 'Inicio Plan', 'Fin Plan', 'Inicio Real', 'Fin Real', 'Desvío Inicio (d)', 'Desvío Fin (d)', 'Prog. Plan (%)', 'Prog. Real (%)', 'Desvío Prog.', 'SPI', 'En Plazo']; foreach ($deviations['phases'] as $phase) { $rows[] = [ $phase['id'], $phase['name'], $phase['planned_start'], $phase['planned_end'], $phase['actual_start'], $phase['actual_end'], $phase['start_deviation'] ?? '—', $phase['end_deviation'] ?? '—', $phase['planned_progress'], $phase['actual_progress'], $phase['progress_deviation'], $phase['spi'] ?? '—', $phase['is_on_track'] === true ? 'Sí' : ($phase['is_on_track'] === false ? 'No' : 'N/A'), ]; } $rows[] = ['', '', '', '', '', '', '', '', '', '', '']; } // Feature deviations if (!empty($deviations['features'])) { $rows[] = ['=== DESVÍOS POR ELEMENTO ===', '', '', '', '', '', '', '', '', '', '', '', '']; $rows[] = ['ID', 'Elemento', 'Fase', 'Inicio Plan', 'Fin Plan', 'Inicio Real', 'Fin Real', 'Desvío Inicio (d)', 'Desvío Fin (d)', 'Prog. Plan (%)', 'Prog. Real (%)', 'Desvío Prog.', 'SPI', 'En Plazo', 'Responsable']; foreach ($deviations['features'] as $feature) { $rows[] = [ $feature['id'], $feature['name'], $feature['phase'], $feature['planned_start'], $feature['planned_end'], $feature['actual_start'], $feature['actual_end'], $feature['start_deviation'] ?? '—', $feature['end_deviation'] ?? '—', $feature['planned_progress'], $feature['actual_progress'], $feature['progress_deviation'], $feature['spi'] ?? '—', $feature['is_on_track'] === true ? 'Sí' : ($feature['is_on_track'] === false ? 'No' : 'N/A'), $feature['responsible'], ]; } $rows[] = ['', '', '', '', '', '', '', '', '', '', '', '', '', '']; } // Summary if (!empty($deviations['summary'])) { $rows[] = ['=== RESUMEN DESVÍOS ===', '']; $rows[] = ['Fases retrasadas', $deviations['summary']['phases_delayed'] ?? 0]; $rows[] = ['Fases adelantadas', $deviations['summary']['phases_early'] ?? 0]; $rows[] = ['Fases en plazo', $deviations['summary']['phases_on_time'] ?? 0]; $rows[] = ['Elementos retrasados', $deviations['summary']['features_delayed'] ?? 0]; $rows[] = ['Elementos adelantados', $deviations['summary']['features_early'] ?? 0]; $rows[] = ['Elementos en plazo', $deviations['summary']['features_on_time'] ?? 0]; } $this->rows = $rows; } public function headings(): array { return []; // Headings included in data rows } public function title(): string { return 'Desvíos'; } public function columnWidths(): array { return ['A' => 25, 'B' => 25, 'C' => 20, 'D' => 12, 'E' => 12, 'F' => 12, 'G' => 12, 'H' => 14, 'I' => 14, 'J' => 14, 'K' => 14, 'L' => 10, 'M' => 10, 'N' => 20]; } } // ============================================================ // Media Sheet // ============================================================ class MediaSheet extends BaseSheet { public function headings(): array { return ['ID', 'Nombre', 'Tipo', 'Entidad', 'Entidad Nombre', 'Tamaño', 'Subido por', 'Fecha', 'URL']; } public function array(): array { return array_map(function ($media) { return [ $media['id'], $media['name'], $media['type'], $media['entity'], $media['entity_name'], $media['size'], $media['uploaded_by'], $media['uploaded_at'], $media['url'], ]; }, $this->rows); } public function columnWidths(): array { return ['A' => 8, 'B' => 30, 'C' => 12, 'D' => 15, 'E' => 30, 'F' => 12, 'G' => 20, 'H' => 18, 'I' => 50]; } } // ============================================================ // Progress Curve Sheet // ============================================================ class ProgressCurveSheet extends BaseSheet { protected array $curveData; public function __construct(array $curveData) { $this->curveData = $curveData; $rows = [['Fecha', 'Progreso Planificado (%)', 'Progreso Real (%)']]; $labels = $curveData['labels'] ?? []; $planned = $curveData['planned'] ?? []; $actual = $curveData['actual'] ?? []; for ($i = 0; $i < count($labels); $i++) { $rows[] = [$labels[$i], $planned[$i] ?? 0, $actual[$i] ?? 0]; } $this->rows = $rows; } public function headings(): array { return ['Fecha', 'Progreso Planificado (%)', 'Progreso Real (%)']; } public function title(): string { return 'Curva S'; } public function columnWidths(): array { return ['A' => 14, 'B' => 22, 'C' => 20]; } } // ============================================================ // Parameters Sheet // ============================================================ class ParametersSheet extends BaseSheet { protected ReportFilters $filters; protected Project $project; public function __construct(ReportFilters $filters, Project $project) { $this->filters = $filters; $this->project = $project; $rows = [ ['Parámetro', 'Valor'], ['Proyecto', $project->name], ['Referencia', $project->reference ?? '—'], ['Fecha desde', $filters->dateFrom?->format('d/m/Y') ?? '—'], ['Fecha hasta', $filters->dateTo?->format('d/m/Y') ?? '—'], ['Entidades incluidas', implode(', ', array_map( fn($e) => $filters->getAvailableEntities()[$e] ?? $e, $filters->entityTypes ))], ['Incluir fotos', $filters->includePhotos ? 'Sí' : 'No'], ['Formato', strtoupper($filters->format)], ['Incluir gráficos', $filters->includeCharts ? 'Sí' : 'No'], ['Generado', now()->format('d/m/Y H:i')], ['Generado por', auth()->guard()->user()?->name ?? 'Sistema'], ]; parent::__construct($rows); } public function headings(): array { return ['Parámetro', 'Valor']; } public function title(): string { return 'Parámetros'; } public function columnWidths(): array { return ['A' => 30, 'B' => 50]; } }