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; } }