242 lines
9.0 KiB
PHP
242 lines
9.0 KiB
PHP
<?php
|
|
|
|
namespace Modules\ProjectProgress\Tests\Feature;
|
|
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Inertia\Testing\AssertableInertia as Assert;
|
|
use Modules\ProjectManagement\Models\Project;
|
|
use Modules\ProjectManagement\Models\Task;
|
|
use Modules\ProjectProgress\Services\ProjectProgressService;
|
|
use Spatie\Permission\Models\Permission;
|
|
use Spatie\Permission\Models\Role;
|
|
use Tests\TestCase;
|
|
|
|
class ProgressMonitoringFeatureTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected User $user;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
$role = Role::firstOrCreate(['name' => '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']);
|
|
}
|
|
}
|