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:
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Feature;
|
||||
use App\Models\Phase;
|
||||
use App\Models\ProgressSnapshot;
|
||||
use App\Models\Task;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class CaptureDailyProgressSnapshot implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(
|
||||
public ?string $snapshotDate = null
|
||||
) {}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$date = $this->snapshotDate ? Carbon::parse($this->snapshotDate) : Carbon::today();
|
||||
|
||||
Log::info('Starting daily progress snapshot capture', ['date' => $date->toDateString()]);
|
||||
|
||||
$captured = 0;
|
||||
|
||||
// Capture Phase snapshots
|
||||
$captured += $this->capturePhaseSnapshots($date);
|
||||
|
||||
// Capture Feature snapshots
|
||||
$captured += $this->captureFeatureSnapshots($date);
|
||||
|
||||
// Capture Task snapshots
|
||||
$captured += $this->captureTaskSnapshots($date);
|
||||
|
||||
Log::info('Daily progress snapshot capture completed', [
|
||||
'date' => $date->toDateString(),
|
||||
'snapshots_captured' => $captured,
|
||||
]);
|
||||
}
|
||||
|
||||
private function capturePhaseSnapshots(Carbon $date): int
|
||||
{
|
||||
$count = 0;
|
||||
|
||||
Phase::with(['project'])->chunkById(100, function ($phases) use ($date, &$count) {
|
||||
foreach ($phases as $phase) {
|
||||
// Skip if snapshot already exists for this date
|
||||
if (ProgressSnapshot::where('trackable_type', Phase::class)
|
||||
->where('trackable_id', $phase->id)
|
||||
->where('snapshot_date', $date)
|
||||
->exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$metadata = [
|
||||
'project_id' => $phase->project_id,
|
||||
'spi' => $phase->spi,
|
||||
'is_on_track' => $phase->is_on_track,
|
||||
'deviation_days' => $phase->deviation_days,
|
||||
'start_deviation_days' => $phase->start_deviation_days,
|
||||
];
|
||||
|
||||
ProgressSnapshot::create([
|
||||
'trackable_type' => Phase::class,
|
||||
'trackable_id' => $phase->id,
|
||||
'snapshot_date' => $date,
|
||||
'progress' => $phase->progress_percent,
|
||||
'planned_start' => $phase->planned_start,
|
||||
'planned_end' => $phase->planned_end,
|
||||
'actual_start' => $phase->actual_start,
|
||||
'actual_end' => $phase->actual_end,
|
||||
'method' => 'manual',
|
||||
'metadata' => $metadata,
|
||||
]);
|
||||
|
||||
$count++;
|
||||
}
|
||||
});
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
private function captureFeatureSnapshots(Carbon $date): int
|
||||
{
|
||||
$count = 0;
|
||||
|
||||
Feature::with(['layer.phase.project'])->chunkById(200, function ($features) use ($date, &$count) {
|
||||
foreach ($features as $feature) {
|
||||
if (ProgressSnapshot::where('trackable_type', Feature::class)
|
||||
->where('trackable_id', $feature->id)
|
||||
->where('snapshot_date', $date)
|
||||
->exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$metadata = [
|
||||
'project_id' => $feature->layer->phase->project_id ?? null,
|
||||
'phase_id' => $feature->layer->phase_id ?? null,
|
||||
'layer_id' => $feature->layer_id,
|
||||
'spi' => $feature->spi,
|
||||
'is_on_track' => $feature->is_on_track,
|
||||
'deviation_days' => $feature->deviation_days,
|
||||
'start_deviation_days' => $feature->start_deviation_days,
|
||||
'status' => $feature->status,
|
||||
];
|
||||
|
||||
ProgressSnapshot::create([
|
||||
'trackable_type' => Feature::class,
|
||||
'trackable_id' => $feature->id,
|
||||
'snapshot_date' => $date,
|
||||
'progress' => $feature->progress,
|
||||
'planned_start' => $feature->planned_start,
|
||||
'planned_end' => $feature->planned_end,
|
||||
'actual_start' => $feature->actual_start,
|
||||
'actual_end' => $feature->actual_end,
|
||||
'method' => 'manual',
|
||||
'metadata' => $metadata,
|
||||
]);
|
||||
|
||||
$count++;
|
||||
}
|
||||
});
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
private function captureTaskSnapshots(Carbon $date): int
|
||||
{
|
||||
$count = 0;
|
||||
|
||||
Task::with(['project', 'phase'])->chunkById(200, function ($tasks) use ($date, &$count) {
|
||||
foreach ($tasks as $task) {
|
||||
if (ProgressSnapshot::where('trackable_type', Task::class)
|
||||
->where('trackable_id', $task->id)
|
||||
->where('snapshot_date', $date)
|
||||
->exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$metadata = [
|
||||
'project_id' => $task->project_id,
|
||||
'phase_id' => $task->phase_id,
|
||||
'status' => $task->status,
|
||||
'priority' => $task->priority,
|
||||
'assigned_to' => $task->assigned_to,
|
||||
'is_overdue' => $task->is_overdue,
|
||||
'estimated_hours' => $task->estimated_hours,
|
||||
'actual_hours' => $task->actual_hours,
|
||||
];
|
||||
|
||||
ProgressSnapshot::create([
|
||||
'trackable_type' => Task::class,
|
||||
'trackable_id' => $task->id,
|
||||
'snapshot_date' => $date,
|
||||
'progress' => $task->progress,
|
||||
'planned_start' => $task->start_date,
|
||||
'planned_end' => $task->due_date,
|
||||
'actual_start' => $task->start_date,
|
||||
'actual_end' => $task->completed_at?->toDateString(),
|
||||
'method' => 'task',
|
||||
'metadata' => $metadata,
|
||||
]);
|
||||
|
||||
$count++;
|
||||
}
|
||||
});
|
||||
|
||||
return $count;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user