feat: Phase 6.2 - Split ReportGenerator into DataAggregator + HtmlExporter + ExcelExporter
- Create DataAggregator service (data aggregation logic) - Create HtmlExporter service (HTML/preview rendering) - Create ExcelExporter service (Excel download) - Refactor ReportGenerator to delegate to services Tests: 101 passing (319 assertions)
This commit is contained in:
@@ -0,0 +1,548 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Report;
|
||||
|
||||
use App\DTO\ReportFilters;
|
||||
use App\Models\Feature;
|
||||
use App\Models\Inspection;
|
||||
use App\Models\Issue;
|
||||
use App\Models\Media;
|
||||
use App\Models\Phase;
|
||||
use App\Models\ProgressSnapshot;
|
||||
use App\Models\Project;
|
||||
use App\Models\Task;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class DataAggregator
|
||||
{
|
||||
protected Project $project;
|
||||
protected ReportFilters $filters;
|
||||
|
||||
public function __construct(Project $project, ReportFilters $filters)
|
||||
{
|
||||
$this->project = $project;
|
||||
$this->filters = $filters;
|
||||
}
|
||||
|
||||
public function aggregate(): array
|
||||
{
|
||||
$data = [
|
||||
'project' => $this->project,
|
||||
'filters' => $this->filters,
|
||||
'generated_at' => now(),
|
||||
'generated_by' => auth()->guard()->user(),
|
||||
];
|
||||
|
||||
// Always include summary KPIs
|
||||
$data['summary'] = $this->buildSummary();
|
||||
|
||||
// Include requested entity sections
|
||||
if (in_array('phases', $this->filters->entityTypes)) {
|
||||
$data['phases'] = $this->buildPhasesData();
|
||||
}
|
||||
|
||||
if (in_array('features', $this->filters->entityTypes)) {
|
||||
$data['features'] = $this->buildFeaturesData();
|
||||
}
|
||||
|
||||
if (in_array('inspections', $this->filters->entityTypes)) {
|
||||
$data['inspections'] = $this->buildInspectionsData();
|
||||
}
|
||||
|
||||
if (in_array('issues', $this->filters->entityTypes)) {
|
||||
$data['issues'] = $this->buildIssuesData();
|
||||
}
|
||||
|
||||
if (in_array('tasks', $this->filters->entityTypes)) {
|
||||
$data['tasks'] = $this->buildTasksData();
|
||||
}
|
||||
|
||||
if (in_array('media', $this->filters->entityTypes)) {
|
||||
$data['media'] = $this->buildMediaData();
|
||||
}
|
||||
|
||||
// Deviation analysis (always included if phases/features selected)
|
||||
if (in_array('phases', $this->filters->entityTypes) || in_array('features', $this->filters->entityTypes)) {
|
||||
$data['deviations'] = $this->buildDeviationsData();
|
||||
}
|
||||
|
||||
// Progress curve data (for S-curve chart)
|
||||
if ($this->filters->includeCharts) {
|
||||
$data['progress_curve'] = $this->buildProgressCurveData();
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function buildSummary(): array
|
||||
{
|
||||
$phases = $this->project->phases()
|
||||
->with(['layers.features'])
|
||||
->get();
|
||||
|
||||
$allFeatures = $phases->flatMap(fn ($p) => $p->layers->flatMap(fn ($l) => $l->features));
|
||||
$completedFeatures = $allFeatures->where('status', 'completed')->count();
|
||||
|
||||
$inspectionsQuery = Inspection::where('project_id', $this->project->id);
|
||||
$this->applyDateFilter($inspectionsQuery, 'created_at');
|
||||
$totalInspections = $inspectionsQuery->count();
|
||||
$passedInspections = (clone $inspectionsQuery)->where('result', 'pass')->count();
|
||||
$failedInspections = (clone $inspectionsQuery)->where('result', 'fail')->count();
|
||||
|
||||
$issuesQuery = Issue::where('project_id', $this->project->id);
|
||||
$this->applyDateFilter($issuesQuery, 'created_at');
|
||||
$openIssues = (clone $issuesQuery)->where('status', 'open')->count();
|
||||
$closedIssues = (clone $issuesQuery)->where('status', 'closed')->count();
|
||||
|
||||
$tasksQuery = Task::where('project_id', $this->project->id);
|
||||
$this->applyDateFilter($tasksQuery, 'created_at');
|
||||
$totalTasks = $tasksQuery->count();
|
||||
$completedTasks = (clone $tasksQuery)->where('status', 'completed')->count();
|
||||
|
||||
// Calculate overall planned vs actual progress
|
||||
$avgPlannedProgress = $phases->avg('planned_progress_at') ?? 0;
|
||||
$avgActualProgress = $phases->avg('progress_percent') ?? 0;
|
||||
$overallSpi = $avgPlannedProgress > 0 ? round($avgActualProgress / $avgPlannedProgress, 2) : null;
|
||||
|
||||
return [
|
||||
'total_features' => $allFeatures->count(),
|
||||
'completed_features' => $completedFeatures,
|
||||
'completion_rate' => $allFeatures->count() > 0 ? round($completedFeatures / $allFeatures->count() * 100, 1) : 0,
|
||||
'avg_planned_progress' => round($avgPlannedProgress, 1),
|
||||
'avg_actual_progress' => round($avgActualProgress, 1),
|
||||
'overall_spi' => $overallSpi,
|
||||
'total_inspections' => $totalInspections,
|
||||
'passed_inspections' => $passedInspections,
|
||||
'failed_inspections' => $failedInspections,
|
||||
'pass_rate' => $totalInspections > 0 ? round($passedInspections / $totalInspections * 100, 1) : 0,
|
||||
'open_issues' => $openIssues,
|
||||
'closed_issues' => $closedIssues,
|
||||
'total_tasks' => $totalTasks,
|
||||
'completed_tasks' => $completedTasks,
|
||||
'task_completion_rate' => $totalTasks > 0 ? round($completedTasks / $totalTasks * 100, 1) : 0,
|
||||
'phases_on_track' => $phases->where('is_on_track', true)->count(),
|
||||
'phases_delayed' => $phases->where('is_on_track', false)->count(),
|
||||
'phases_no_data' => $phases->where('is_on_track', null)->count(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function buildPhasesData(): array
|
||||
{
|
||||
$phases = $this->project->phases()
|
||||
->with(['layers.features'])
|
||||
->orderBy('order')
|
||||
->get();
|
||||
|
||||
return $phases->map(function ($phase) {
|
||||
$phaseFeatures = $phase->layers->flatMap(fn ($l) => $l->features);
|
||||
|
||||
return [
|
||||
'id' => $phase->id,
|
||||
'name' => $phase->name,
|
||||
'description' => $phase->description,
|
||||
'order' => $phase->order,
|
||||
'color' => $phase->color,
|
||||
'planned_start' => $phase->planned_start?->format('d/m/Y'),
|
||||
'planned_end' => $phase->planned_end?->format('d/m/Y'),
|
||||
'actual_start' => $phase->actual_start?->format('d/m/Y'),
|
||||
'actual_end' => $phase->actual_end?->format('d/m/Y'),
|
||||
'baseline_start' => $phase->baseline_start?->format('d/m/Y'),
|
||||
'baseline_end' => $phase->baseline_end?->format('d/m/Y'),
|
||||
'progress_percent' => $phase->progress_percent,
|
||||
'planned_progress' => $phase->planned_progress_at,
|
||||
'deviation_days' => $phase->deviation_days,
|
||||
'start_deviation_days' => $phase->start_deviation_days,
|
||||
'spi' => $phase->spi,
|
||||
'is_on_track' => $phase->is_on_track,
|
||||
'features_count' => $phaseFeatures->count(),
|
||||
'completed_features' => $phaseFeatures->where('status', 'completed')->count(),
|
||||
'layers' => $phase->layers->map(fn ($l) => [
|
||||
'id' => $l->id,
|
||||
'name' => $l->name,
|
||||
'features_count' => $l->features->count(),
|
||||
])->toArray(),
|
||||
];
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
protected function buildFeaturesData(): array
|
||||
{
|
||||
$query = Feature::whereHas('layer.phase', fn ($q) => $q->where('project_id', $this->project->id))
|
||||
->with(['layer.phase', 'template', 'inspections', 'issues']);
|
||||
|
||||
// Apply date filter on feature created_at if date range provided
|
||||
$this->applyDateFilter($query, 'features.created_at');
|
||||
|
||||
$features = $query->orderBy('id')->get();
|
||||
|
||||
return $features->map(function ($feature) {
|
||||
$lastInspection = $feature->inspections->sortByDesc('created_at')->first();
|
||||
|
||||
return [
|
||||
'id' => $feature->id,
|
||||
'name' => $feature->name,
|
||||
'phase' => $feature->layer?->phase?->name ?? '—',
|
||||
'layer' => $feature->layer?->name ?? '—',
|
||||
'status' => $feature->status,
|
||||
'status_label' => $this->getFeatureStatusLabel($feature->status),
|
||||
'status_color' => $feature->status_color,
|
||||
'progress' => $feature->progress,
|
||||
'planned_progress' => $feature->planned_progress_at,
|
||||
'planned_start' => $feature->planned_start?->format('d/m/Y'),
|
||||
'planned_end' => $feature->planned_end?->format('d/m/Y'),
|
||||
'actual_start' => $feature->actual_start?->format('d/m/Y'),
|
||||
'actual_end' => $feature->actual_end?->format('d/m/Y'),
|
||||
'deviation_days' => $feature->deviation_days,
|
||||
'start_deviation_days' => $feature->start_deviation_days,
|
||||
'spi' => $feature->spi,
|
||||
'is_on_track' => $feature->is_on_track,
|
||||
'responsible' => $feature->responsible ?? $feature->responsibleUser?->name ?? '—',
|
||||
'template' => $feature->template?->name ?? '—',
|
||||
'last_inspection_date' => $lastInspection?->created_at?->format('d/m/Y'),
|
||||
'last_inspection_result' => $lastInspection?->result,
|
||||
'inspections_count' => $feature->inspections->count(),
|
||||
'open_issues_count' => $feature->issues->where('status', 'open')->count(),
|
||||
];
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
protected function buildInspectionsData(): array
|
||||
{
|
||||
$query = Inspection::where('project_id', $this->project->id)
|
||||
->with(['feature.layer.phase', 'template', 'user', 'media']);
|
||||
|
||||
$this->applyDateFilter($query, 'created_at');
|
||||
|
||||
$inspections = $query->orderByDesc('created_at')->get();
|
||||
|
||||
return $inspections->map(function ($inspection) {
|
||||
return [
|
||||
'id' => $inspection->id,
|
||||
'feature' => $inspection->feature?->name ?? '—',
|
||||
'phase' => $inspection->feature?->layer?->phase?->name ?? '—',
|
||||
'template' => $inspection->template?->name ?? '—',
|
||||
'inspector' => $inspection->user?->name ?? '—',
|
||||
'date' => $inspection->created_at->format('d/m/Y H:i'),
|
||||
'status' => $inspection->status,
|
||||
'result' => $inspection->result,
|
||||
'result_label' => match ($inspection->result) {
|
||||
'pass' => 'Aprobada',
|
||||
'fail' => 'Fallida',
|
||||
'conditional' => 'Condicional',
|
||||
default => '—',
|
||||
},
|
||||
'notes' => $inspection->notes,
|
||||
'photos_count' => $inspection->media->count(),
|
||||
'data' => $inspection->data ?? [],
|
||||
];
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
protected function buildIssuesData(): array
|
||||
{
|
||||
$query = Issue::where('project_id', $this->project->id)
|
||||
->with(['feature.layer.phase', 'reporter', 'assignee', 'tasks']);
|
||||
|
||||
$this->applyDateFilter($query, 'created_at');
|
||||
|
||||
$issues = $query->orderByDesc('created_at')->get();
|
||||
|
||||
return $issues->map(function ($issue) {
|
||||
return [
|
||||
'id' => $issue->id,
|
||||
'title' => $issue->title,
|
||||
'feature' => $issue->feature?->name ?? '—',
|
||||
'phase' => $issue->feature?->layer?->phase?->name ?? '—',
|
||||
'priority' => $issue->priority,
|
||||
'priority_label' => match ($issue->priority) {
|
||||
'low' => 'Baja',
|
||||
'medium' => 'Media',
|
||||
'high' => 'Alta',
|
||||
'critical' => 'Crítica',
|
||||
default => ucfirst($issue->priority ?? ''),
|
||||
},
|
||||
'status' => $issue->status,
|
||||
'status_label' => match ($issue->status) {
|
||||
'open' => 'Abierta',
|
||||
'in_review' => 'En revisión',
|
||||
'closed' => 'Cerrada',
|
||||
default => ucfirst($issue->status ?? ''),
|
||||
},
|
||||
'reporter' => $issue->reporter?->name ?? '—',
|
||||
'assignee' => $issue->assignee?->name ?? '—',
|
||||
'created_at' => $issue->created_at->format('d/m/Y'),
|
||||
'closed_at' => $issue->closed_at?->format('d/m/Y'),
|
||||
'days_open' => $issue->created_at->diffInDays($issue->closed_at ?? now()),
|
||||
'tasks_total' => $issue->tasks->count(),
|
||||
'tasks_completed' => $issue->tasks->where('is_done', true)->count(),
|
||||
];
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
protected function buildTasksData(): array
|
||||
{
|
||||
$query = Task::where('project_id', $this->project->id)
|
||||
->with(['phase', 'assignee', 'creator', 'subtasks']);
|
||||
|
||||
$this->applyDateFilter($query, 'created_at');
|
||||
|
||||
$tasks = $query->whereNull('parent_task_id')->orderBy('order')->get();
|
||||
|
||||
return $tasks->map(function ($task) {
|
||||
return [
|
||||
'id' => $task->id,
|
||||
'title' => $task->title,
|
||||
'phase' => $task->phase?->name ?? '—',
|
||||
'status' => $task->status,
|
||||
'status_label' => $task->status_label,
|
||||
'priority' => $task->priority,
|
||||
'priority_label' => $task->priority_label,
|
||||
'assignee' => $task->assignee?->name ?? '—',
|
||||
'creator' => $task->creator?->name ?? '—',
|
||||
'due_date' => $task->due_date?->format('d/m/Y'),
|
||||
'start_date' => $task->start_date?->format('d/m/Y'),
|
||||
'completed_at' => $task->completed_at?->format('d/m/Y'),
|
||||
'estimated_hours' => $task->estimated_hours,
|
||||
'actual_hours' => $task->actual_hours,
|
||||
'progress' => $task->progress,
|
||||
'is_overdue' => $task->is_overdue,
|
||||
'subtasks' => $task->subtasks->map(fn ($st) => [
|
||||
'id' => $st->id,
|
||||
'title' => $st->title,
|
||||
'status' => $st->status,
|
||||
'assignee' => $st->assignee?->name ?? '—',
|
||||
'progress' => $st->progress,
|
||||
])->toArray(),
|
||||
];
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
protected function buildMediaData(): array
|
||||
{
|
||||
$query = Media::whereHas('mediable', function ($q) {
|
||||
$q->where(function ($sub) {
|
||||
$sub->where('mediable_type', Phase::class)
|
||||
->orWhere('mediable_type', Feature::class)
|
||||
->orWhere('mediable_type', Inspection::class)
|
||||
->orWhere('mediable_type', Issue::class)
|
||||
->orWhere('mediable_type', Task::class);
|
||||
});
|
||||
})->whereHas('mediable', function ($q) {
|
||||
// Filter by project through relationships
|
||||
// This is complex with polymorphic, so we filter in PHP for simplicity
|
||||
});
|
||||
|
||||
$this->applyDateFilter($query, 'created_at');
|
||||
|
||||
$media = $query->orderByDesc('created_at')->get();
|
||||
|
||||
// Filter by project in PHP (polymorphic complexity)
|
||||
$filtered = $media->filter(function ($m) {
|
||||
$mediable = $m->mediable;
|
||||
if (! $mediable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($mediable instanceof Phase) {
|
||||
return $mediable->project_id === $this->project->id;
|
||||
}
|
||||
if ($mediable instanceof Feature) {
|
||||
return $mediable->layer?->phase?->project_id === $this->project->id;
|
||||
}
|
||||
if ($mediable instanceof Inspection) {
|
||||
return $mediable->project_id === $this->project->id;
|
||||
}
|
||||
if ($mediable instanceof Issue) {
|
||||
return $mediable->project_id === $this->project->id;
|
||||
}
|
||||
if ($mediable instanceof Task) {
|
||||
return $mediable->project_id === $this->project->id;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return $filtered->map(function ($m) {
|
||||
return [
|
||||
'id' => $m->id,
|
||||
'name' => $m->name,
|
||||
'type' => $m->category,
|
||||
'entity' => class_basename($m->mediable_type),
|
||||
'entity_name' => $m->mediable?->name ?? $m->mediable?->title ?? '—',
|
||||
'size' => $m->file_size ? $this->formatBytes($m->file_size) : '—',
|
||||
'uploaded_by' => $m->uploadedBy?->name ?? '—',
|
||||
'uploaded_at' => $m->created_at->format('d/m/Y H:i'),
|
||||
'url' => $m->url,
|
||||
];
|
||||
})->values()->toArray();
|
||||
}
|
||||
|
||||
protected function buildDeviationsData(): array
|
||||
{
|
||||
$phases = $this->project->phases()
|
||||
->with(['layers.features'])
|
||||
->get();
|
||||
|
||||
$phaseDeviations = $phases->map(function ($phase) {
|
||||
return [
|
||||
'id' => $phase->id,
|
||||
'name' => $phase->name,
|
||||
'planned_start' => $phase->planned_start?->format('d/m/Y'),
|
||||
'planned_end' => $phase->planned_end?->format('d/m/Y'),
|
||||
'actual_start' => $phase->actual_start?->format('d/m/Y'),
|
||||
'actual_end' => $phase->actual_end?->format('d/m/Y'),
|
||||
'start_deviation' => $phase->start_deviation_days,
|
||||
'end_deviation' => $phase->deviation_days,
|
||||
'planned_progress' => $phase->planned_progress_at,
|
||||
'actual_progress' => $phase->progress_percent,
|
||||
'progress_deviation' => round($phase->progress_percent - $phase->planned_progress_at, 1),
|
||||
'spi' => $phase->spi,
|
||||
'is_on_track' => $phase->is_on_track,
|
||||
];
|
||||
})->filter(fn ($p) => $p['planned_start'] || $p['planned_end'])->toArray();
|
||||
|
||||
$featureDeviations = Feature::whereHas('layer.phase', fn ($q) => $q->where('project_id', $this->project->id))
|
||||
->whereNotNull('planned_end')
|
||||
->with(['layer.phase'])
|
||||
->get()
|
||||
->map(function ($feature) {
|
||||
return [
|
||||
'id' => $feature->id,
|
||||
'name' => $feature->name,
|
||||
'phase' => $feature->layer?->phase?->name ?? '—',
|
||||
'planned_start' => $feature->planned_start?->format('d/m/Y'),
|
||||
'planned_end' => $feature->planned_end?->format('d/m/Y'),
|
||||
'actual_start' => $feature->actual_start?->format('d/m/Y'),
|
||||
'actual_end' => $feature->actual_end?->format('d/m/Y'),
|
||||
'start_deviation' => $feature->start_deviation_days,
|
||||
'end_deviation' => $feature->deviation_days,
|
||||
'planned_progress' => $feature->planned_progress_at,
|
||||
'actual_progress' => $feature->progress,
|
||||
'progress_deviation' => round($feature->progress - $feature->planned_progress_at, 1),
|
||||
'spi' => $feature->spi,
|
||||
'is_on_track' => $feature->is_on_track,
|
||||
'responsible' => $feature->responsible ?? $feature->responsibleUser?->name ?? '—',
|
||||
];
|
||||
})->toArray();
|
||||
|
||||
return [
|
||||
'phases' => $phaseDeviations,
|
||||
'features' => $featureDeviations,
|
||||
'summary' => [
|
||||
'phases_delayed' => count(array_filter($phaseDeviations, fn ($p) => ($p['end_deviation'] ?? 0) > 0)),
|
||||
'phases_early' => count(array_filter($phaseDeviations, fn ($p) => ($p['end_deviation'] ?? 0) < 0)),
|
||||
'phases_on_time' => count(array_filter($phaseDeviations, fn ($p) => ($p['end_deviation'] ?? 0) === 0)),
|
||||
'features_delayed' => count(array_filter($featureDeviations, fn ($f) => ($f['end_deviation'] ?? 0) > 0)),
|
||||
'features_early' => count(array_filter($featureDeviations, fn ($f) => ($f['end_deviation'] ?? 0) < 0)),
|
||||
'features_on_time' => count(array_filter($featureDeviations, fn ($f) => ($f['end_deviation'] ?? 0) === 0)),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
protected function buildProgressCurveData(): array
|
||||
{
|
||||
// Get snapshots for this project's phases and features
|
||||
$phaseIds = $this->project->phases->pluck('id')->toArray();
|
||||
$featureIds = Feature::whereHas('layer.phase', fn ($q) => $q->whereIn('phase_id', $phaseIds))
|
||||
->pluck('id')->toArray();
|
||||
$taskIds = Task::where('project_id', $this->project->id)->pluck('id')->toArray();
|
||||
|
||||
$from = $this->filters->dateFrom ?? now()->subMonths(6);
|
||||
$to = $this->filters->dateTo ?? now();
|
||||
|
||||
$snapshots = ProgressSnapshot::whereIn('trackable_type', [Phase::class, Feature::class, Task::class])
|
||||
->where(function ($q) use ($phaseIds, $featureIds, $taskIds) {
|
||||
$q->where(function ($sub) use ($phaseIds) {
|
||||
$sub->where('trackable_type', Phase::class)
|
||||
->whereIn('trackable_id', $phaseIds);
|
||||
})->orWhere(function ($sub) use ($featureIds) {
|
||||
$sub->where('trackable_type', Feature::class)
|
||||
->whereIn('trackable_id', $featureIds);
|
||||
})->orWhere(function ($sub) use ($taskIds) {
|
||||
$sub->where('trackable_type', Task::class)
|
||||
->whereIn('trackable_id', $taskIds);
|
||||
});
|
||||
})
|
||||
->whereBetween('snapshot_date', [$from, $to])
|
||||
->orderBy('snapshot_date')
|
||||
->get();
|
||||
|
||||
// Group by date and calculate average progress
|
||||
$grouped = $snapshots->groupBy('snapshot_date');
|
||||
|
||||
$dates = [];
|
||||
$planned = [];
|
||||
$actual = [];
|
||||
|
||||
foreach ($grouped as $date => $snaps) {
|
||||
$dates[] = Carbon::parse($date)->format('d/m/Y');
|
||||
|
||||
$phaseSnaps = $snaps->where('trackable_type', Phase::class);
|
||||
$featureSnaps = $snaps->where('trackable_type', Feature::class);
|
||||
$taskSnaps = $snaps->where('trackable_type', Task::class);
|
||||
|
||||
$avgPlanned = 0;
|
||||
$avgActual = 0;
|
||||
$count = 0;
|
||||
|
||||
foreach ($phaseSnaps as $snap) {
|
||||
$avgPlanned += $snap->metadata['planned_progress'] ?? 0;
|
||||
$avgActual += $snap->progress;
|
||||
$count++;
|
||||
}
|
||||
foreach ($featureSnaps as $snap) {
|
||||
$avgPlanned += $snap->metadata['planned_progress'] ?? 0;
|
||||
$avgActual += $snap->progress;
|
||||
$count++;
|
||||
}
|
||||
foreach ($taskSnaps as $snap) {
|
||||
$avgPlanned += $snap->progress; // Tasks don't have planned progress easily
|
||||
$avgActual += $snap->progress;
|
||||
$count++;
|
||||
}
|
||||
|
||||
$dates[] = Carbon::parse($date)->format('d/m/Y');
|
||||
$planned[] = $count > 0 ? round($avgPlanned / $count, 1) : 0;
|
||||
$actual[] = $count > 0 ? round($avgActual / $count, 1) : 0;
|
||||
}
|
||||
|
||||
return [
|
||||
'dates' => $dates,
|
||||
'planned' => $planned,
|
||||
'actual' => $actual,
|
||||
];
|
||||
}
|
||||
|
||||
protected function applyDateFilter($query, string $column): void
|
||||
{
|
||||
if ($this->filters->dateFrom) {
|
||||
$query->whereDate($column, '>=', $this->filters->dateFrom);
|
||||
}
|
||||
if ($this->filters->dateTo) {
|
||||
$query->whereDate($column, '<=', $this->filters->dateTo);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getFeatureStatusLabel(string $status): string
|
||||
{
|
||||
return match ($status) {
|
||||
'planned' => 'Planificado',
|
||||
'started' => 'Iniciado',
|
||||
'in_progress' => 'En progreso',
|
||||
'completed' => 'Completado',
|
||||
'verified' => 'Verificado',
|
||||
default => ucfirst($status),
|
||||
};
|
||||
}
|
||||
|
||||
protected function formatBytes(int $bytes): string
|
||||
{
|
||||
if ($bytes >= 1048576) {
|
||||
return round($bytes / 1048576, 1).' MB';
|
||||
}
|
||||
if ($bytes >= 1024) {
|
||||
return round($bytes / 1024, 1).' KB';
|
||||
}
|
||||
return $bytes.' B';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Report;
|
||||
|
||||
use App\Models\Project;
|
||||
use App\DTO\ReportFilters;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use App\Exports\ProjectReportExport;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
|
||||
class ExcelExporter
|
||||
{
|
||||
public function export(Project $project, ReportFilters $filters, array $data): BinaryFileResponse
|
||||
{
|
||||
$export = new ProjectReportExport($project, $filters, $data);
|
||||
$filename = 'informe_'.$project->name.'_'.now()->format('Ymd_His').'.xlsx';
|
||||
|
||||
return Excel::download($export, $filename);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Report;
|
||||
|
||||
use App\Models\Project;
|
||||
use App\Services\Report\DataAggregator;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class HtmlExporter
|
||||
{
|
||||
public function export(Project $project, array $data): View
|
||||
{
|
||||
return view('reports.complete', $data);
|
||||
}
|
||||
|
||||
public function exportPreview(array $data): View
|
||||
{
|
||||
return view('reports.partials._preview', $data);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user