feat(reports): complete reporting system with deviations, progress curves, multi-format export

- Added planned/actual/baseline dates to Phase and Feature models
- Created ProgressSnapshot model + migration for historical tracking
- Implemented CaptureDailyProgressSnapshot job (scheduled daily at 06:30)
- Added DeviationCalculator logic (SPI, planned progress, deviation days)
- ReportGenerator service with complete data aggregation
- ReportController with builder, preview, generate (HTML/Excel)
- ReportBuilder Livewire component with date range, entity selection, format
- ProjectReportExport with 10 sheets (Summary, Phases, Features, Inspections, Issues, Tasks, Deviations, Media, Progress Curve, Parameters)
- Blade partials for all sections (header, summary, phases, features, inspections, issues, tasks, media, deviations, progress curve)
- New routes: /projects/{project}/reports/*
- Added 'generate reports' permission
- All 101 tests passing
This commit is contained in:
Javier Braña
2026-08-24 12:32:08 +02:00
parent c58a49f22d
commit ee32544525
30 changed files with 3170 additions and 7 deletions
+68
View File
@@ -0,0 +1,68 @@
<?php
namespace App\DTO;
use Carbon\Carbon;
use Illuminate\Support\Collection;
class ReportFilters
{
public function __construct(
public ?Carbon $dateFrom = null,
public ?Carbon $dateTo = null,
public array $entityTypes = [], // ['phases','features','inspections','issues','tasks','media']
public bool $includePhotos = false,
public string $format = 'html', // html|excel
public bool $includeCharts = false,
) {}
public static function fromRequest(array $data): self
{
return new self(
dateFrom: isset($data['date_from']) ? Carbon::parse($data['date_from']) : null,
dateTo: isset($data['date_to']) ? Carbon::parse($data['date_to']) : null,
entityTypes: $data['entity_types'] ?? ['phases','features','inspections','issues','tasks'],
includePhotos: (bool)($data['include_photos'] ?? false),
format: $data['format'] ?? 'html',
includeCharts: (bool)($data['include_charts'] ?? false),
);
}
public function toArray(): array
{
return [
'date_from' => $this->dateFrom?->toDateString(),
'date_to' => $this->dateTo?->toDateString(),
'entity_types' => $this->entityTypes,
'include_photos' => $this->includePhotos,
'format' => $this->format,
'include_charts' => $this->includeCharts,
];
}
public function getAvailableEntities(): array
{
return [
'phases' => 'Fases',
'features' => 'Elementos (Features)',
'inspections' => 'Inspecciones',
'issues' => 'Incidencias (Issues)',
'tasks' => 'Tareas',
'media' => 'Archivos/Media',
];
}
public function getDateRangeLabel(): string
{
if ($this->dateFrom && $this->dateTo) {
return $this->dateFrom->format('d/m/Y') . ' - ' . $this->dateTo->format('d/m/Y');
}
if ($this->dateFrom) {
return 'Desde ' . $this->dateFrom->format('d/m/Y');
}
if ($this->dateTo) {
return 'Hasta ' . $this->dateTo->format('d/m/Y');
}
return 'Todo el período';
}
}