'Super Admin']); Permission::firstOrCreate(['name' => 'projects.access']); $this->user = User::factory()->create([ 'status' => 'active', 'user_type' => 'admin', ]); $this->user->assignRole($role); } public function test_progress_monitoring_page_renders_with_inertia_props_when_project_selected(): void { $project = Project::factory()->create([ 'start_date' => now()->subDays(20)->format('Y-m-d'), 'target_end_date' => now()->addDays(20)->format('Y-m-d'), ]); Task::factory()->create([ 'project_id' => $project->id, 'status' => 'completed', 'completion_percentage' => 100, 'start_date' => now()->subDays(20)->format('Y-m-d'), 'actual_end_date' => now()->subDays(5)->format('Y-m-d'), ]); $response = $this->actingAs($this->user) ->get(route('projects.progress.index', ['project' => $project->ulid])); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->component('ProjectManagement::Projects/Modules/Progress', false) ->has('project') ->has('projects') ->where('currentTab', 'progress') ->has('evmData', fn (Assert $evm) => $evm ->has('timeSeries') ->has('milestones') ->has('summary', fn (Assert $summary) => $summary ->has('planned_pv') ->has('actual_ev') ->has('schedule_variance') ->has('spi') ->has('status') ->has('target_end_date') ->has('projected_end_date') ->has('days_variance') ->has('is_completed') ) ) ); } public function test_progress_monitoring_page_without_project_param_renders_project_selection_grid(): void { Project::factory()->count(3)->create(); $response = $this->actingAs($this->user) ->get(route('projects.progress.index')); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->component('ProjectManagement::Projects/Modules/Progress', false) ->where('project', null) ->where('evmData', null) ->has('projects', 3) ); } public function test_project_overview_page_provides_evm_data_prop(): void { $project = Project::factory()->create([ 'start_date' => now()->subDays(15)->format('Y-m-d'), 'target_end_date' => now()->addDays(15)->format('Y-m-d'), ]); $response = $this->actingAs($this->user) ->get(route('projects.show', $project->ulid)); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->component('ProjectManagement::Projects/Overview', false) ->has('evmData') ->has('evmData.timeSeries') ->has('evmData.summary') ->has('evmData.milestones') ); } public function test_evm_calculation_handles_ahead_of_schedule_status(): void { $project = Project::factory()->create([ 'start_date' => now()->subDays(30)->format('Y-m-d'), 'target_end_date' => now()->addDays(30)->format('Y-m-d'), ]); // Task completed way ahead of schedule Task::factory()->create([ 'project_id' => $project->id, 'status' => 'completed', 'completion_percentage' => 100, 'start_date' => now()->subDays(30)->format('Y-m-d'), 'actual_end_date' => now()->subDays(20)->format('Y-m-d'), 'end_date' => now()->addDays(20)->format('Y-m-d'), ]); $service = new ProjectProgressService(); $evmData = $service->getEvmAnalysisData($project); $summary = $evmData['summary']; $this->assertEquals(100.0, $summary['actual_ev']); $this->assertGreaterThan(0, $summary['schedule_variance']); $this->assertGreaterThanOrEqual(1.0, $summary['spi']); $this->assertEquals('Completed Ahead of Schedule', $summary['status']); $this->assertTrue($summary['is_completed']); } public function test_evm_calculation_handles_delayed_status_and_forecasted_end_date(): void { $project = Project::factory()->create([ 'start_date' => now()->subDays(30)->format('Y-m-d'), 'target_end_date' => now()->addDays(10)->format('Y-m-d'), ]); // Only 10% completed while planned is ~75% Task::factory()->create([ 'project_id' => $project->id, 'status' => 'in_progress', 'completion_percentage' => 10, 'start_date' => now()->subDays(30)->format('Y-m-d'), 'end_date' => now()->addDays(10)->format('Y-m-d'), ]); $service = new ProjectProgressService(); $evmData = $service->getEvmAnalysisData($project); $summary = $evmData['summary']; $this->assertLessThan(0, $summary['schedule_variance']); $this->assertLessThan(1.0, $summary['spi']); $this->assertEquals('Delayed', $summary['status']); $this->assertGreaterThan(0, $summary['days_variance']); } public function test_evm_calculation_handles_empty_project_gracefully(): void { $project = Project::factory()->create([ 'start_date' => now()->format('Y-m-d'), 'target_end_date' => now()->addDays(30)->format('Y-m-d'), ]); $service = new ProjectProgressService(); $evmData = $service->getEvmAnalysisData($project); $this->assertIsArray($evmData); $this->assertNotEmpty($evmData['timeSeries']); $this->assertEquals(0.0, $evmData['summary']['actual_ev']); } public function test_task_and_scheduling_coordinated_with_milestone_turnover(): void { $project = Project::factory()->create([ 'start_date' => now()->subDays(30)->format('Y-m-d'), 'target_end_date' => now()->addDays(30)->format('Y-m-d'), ]); // Milestone 1: Structural Works (Turnovered Ahead) $m1 = \Modules\ProjectManagement\Models\ProjectMilestone::create([ 'project_id' => $project->id, 'name' => 'Foundation & Structural Turnover', 'weight_percentage' => 60.0, 'planned_date' => now()->subDays(5)->format('Y-m-d'), 'actual_date' => now()->subDays(10)->format('Y-m-d'), ]); Task::factory()->create([ 'project_id' => $project->id, 'milestone_id' => $m1->id, 'status' => 'completed', 'completion_percentage' => 100, 'start_date' => now()->subDays(30)->format('Y-m-d'), 'actual_end_date' => now()->subDays(10)->format('Y-m-d'), ]); // Milestone 2: Finishing Works (In Progress / Ready for Turnover) $m2 = \Modules\ProjectManagement\Models\ProjectMilestone::create([ 'project_id' => $project->id, 'name' => 'Finishing & Final Turnover', 'weight_percentage' => 40.0, 'planned_date' => now()->addDays(20)->format('Y-m-d'), 'actual_date' => null, ]); Task::factory()->create([ 'project_id' => $project->id, 'milestone_id' => $m2->id, 'status' => 'in_progress', 'completion_percentage' => 50, 'start_date' => now()->subDays(9)->format('Y-m-d'), 'end_date' => now()->addDays(20)->format('Y-m-d'), ]); $service = new ProjectProgressService(); $evmData = $service->getEvmAnalysisData($project); $this->assertNotEmpty($evmData['milestones']); $this->assertCount(2, $evmData['milestones']); $m1Data = collect($evmData['milestones'])->firstWhere('id', $m1->id); $this->assertEquals('Turnovered Ahead', $m1Data['turnover_status']); $this->assertTrue($m1Data['is_turnovered']); $this->assertEquals(100.0, $m1Data['progress_percentage']); $m2Data = collect($evmData['milestones'])->firstWhere('id', $m2->id); $this->assertEquals('In Progress', $m2Data['turnover_status']); $this->assertFalse($m2Data['is_turnovered']); $this->assertEquals(50.0, $m2Data['progress_percentage']); } }