Files
construprogress/app/Exports/ProjectReportExport.php
T

655 lines
22 KiB
PHP
Raw Normal View History

<?php
namespace App\Exports;
use App\DTO\ReportFilters;
use App\Models\Project;
use Maatwebsite\Excel\Concerns\FromArray;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
use Maatwebsite\Excel\Concerns\WithTitle;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithStyles;
use Maatwebsite\Excel\Concerns\WithColumnWidths;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Style\Border;
use PhpOffice\PhpSpreadsheet\Style\Fill;
class ProjectReportExport implements WithMultipleSheets
{
protected Project $project;
protected ReportFilters $filters;
protected array $data;
public function __construct(Project $project, ReportFilters $filters, array $data)
{
$this->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];
}
}