655 lines
22 KiB
PHP
655 lines
22 KiB
PHP
<?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];
|
||
|
|
}
|
||
|
|
}
|