- 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
174 lines
6.0 KiB
PHP
174 lines
6.0 KiB
PHP
<?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;
|
|
}
|
|
} |