feat: complete GSB construction ERP implementation with dual-mode MR, capitalization cap engine, single PM rule, and system sign-off documentation

This commit is contained in:
GSB Construction Engineering
2026-08-25 17:08:03 +08:00
parent 778f27e0a5
commit d4e2b468ff
114 changed files with 3923 additions and 10031 deletions

View File

@@ -605,8 +605,6 @@ class ProjectWizardFlowTest extends TestCase
$this->assertFalse($project->personnel()->where('user_id', $user->id)->exists());
}
// ─── Wizard Step Clamping ─────────────────────────────────────────────────
/** @test */
public function cannot_skip_ahead_wizard_steps(): void
{
@@ -617,4 +615,166 @@ class ProjectWizardFlowTest extends TestCase
->assertOk()
->assertInertia(fn ($page) => $page->where('step', 2));
}
/** @test */
public function contractor_project_manager_appears_in_project_wizard_roster_and_can_be_assigned(): void
{
$cpmRole = Role::firstOrCreate(['name' => 'Contractor Project Manager', 'guard_name' => 'web']);
$contractor = \Modules\ContractorManagement\Models\Contractor::create([
'company_name' => 'Titan Prime Subcontractor Corp',
'contact_person' => 'Juan Dela Cruz',
'email' => 'juan@titanprime.test',
'phone' => '09171234567',
'status' => 'active',
]);
$contractorPM = User::factory()->create([
'name' => 'Juan Contractor PM',
'email' => 'juan.cpm@titanprime.test',
'user_type' => 'contractor',
'contractor_id' => $contractor->id,
'status' => 'active',
]);
$contractorPM->assignRole($cpmRole);
// 1. Verify Contractor PM appears in projects.create employees list
$this->actingAs($this->admin)
->get(route('projects.create'))
->assertOk()
->assertInertia(fn ($page) =>
$page->where('employees', fn ($emps) =>
collect($emps)->contains('email', 'juan.cpm@titanprime.test')
)
);
// 2. Verify Contractor PM can be assigned as project PM on project store
$storeResponse = $this->actingAs($this->admin)
->post(route('projects.store'), [
'name' => 'Titan Construction Tower A',
'project_type' => 'standard',
'pm_id' => $contractorPM->ulid,
'contract_duration' => 120,
]);
$storeResponse->assertRedirect();
$newProject = Project::where('name', 'Titan Construction Tower A')->firstOrFail();
$this->assertTrue($newProject->personnel()->where('user_id', $contractorPM->id)->wherePivot('role', 'pm')->exists());
// 3. Verify Contractor PM appears in Step 4 wizard roster and can be assigned as team member
$wizardStep4Resp = $this->actingAs($this->admin)
->get(route('projects.wizard', [$newProject, 'step' => 4]));
$wizardStep4Resp->assertOk()
->assertInertia(fn ($page) =>
$page->where('employees', fn ($emps) =>
collect($emps)->contains('email', 'juan.cpm@titanprime.test')
)
);
// 4. Save Step 4 manpower with Contractor PM in team roster
$saveLaborResp = $this->actingAs($this->admin)
->post(route('projects.wizard.labor', $newProject), [
'user_ulids' => [$contractorPM->ulid],
'labor' => [],
]);
$saveLaborResp->assertRedirect(route('projects.wizard', [$newProject, 'step' => 5]));
$this->assertTrue($newProject->personnel()->where('user_id', $contractorPM->id)->exists());
// Verify Contractor was automatically attached to project_contractor
$this->assertTrue($newProject->contractors()->where('contractors.id', $contractor->id)->exists());
}
/** @test */
public function test_wizard_roster_auto_links_contractors_and_enforces_single_project_manager_with_multiple_site_operations(): void
{
$pmRole = Role::firstOrCreate(['name' => 'Project Manager', 'guard_name' => 'web']);
$siteTechRole = Role::firstOrCreate(['name' => 'Site Technical', 'guard_name' => 'web']);
$supervisorRole = Role::firstOrCreate(['name' => 'Construction Supervisor', 'guard_name' => 'web']);
$mainContractor = \Modules\ContractorManagement\Models\Contractor::create([
'company_name' => 'Main Structural Corp',
'contact_person' => 'Main Person',
'email' => 'main@structural.test',
'phone' => '09181111111',
'status' => 'active',
]);
$subcontractor = \Modules\ContractorManagement\Models\Contractor::create([
'company_name' => 'Subcon Electrical Services',
'contact_person' => 'Sub Person',
'email' => 'sub@electrical.test',
'phone' => '09182222222',
'status' => 'active',
]);
$firstPM = User::factory()->create([
'name' => 'Alice First PM',
'user_type' => 'contractor',
'contractor_id' => $mainContractor->id,
'status' => 'active',
]);
$firstPM->assignRole($pmRole);
$secondPM = User::factory()->create([
'name' => 'Bob Second PM',
'user_type' => 'employee',
'status' => 'active',
]);
$secondPM->assignRole($pmRole);
$siteTechUser = User::factory()->create([
'name' => 'Charlie Site Tech',
'user_type' => 'contractor',
'contractor_id' => $subcontractor->id,
'status' => 'active',
]);
$siteTechUser->assignRole($siteTechRole);
$supervisorUser = User::factory()->create([
'name' => 'David Supervisor',
'user_type' => 'contractor',
'contractor_id' => $subcontractor->id,
'status' => 'active',
]);
$supervisorUser->assignRole($supervisorRole);
// 1. Create project with First PM
$this->actingAs($this->admin)->post(route('projects.store'), [
'name' => 'Skyline Heights Commercial Center',
'project_type' => 'standard',
'pm_id' => $firstPM->ulid,
'contract_duration' => 180,
]);
$project = Project::where('name', 'Skyline Heights Commercial Center')->firstOrFail();
$this->assertEquals(1, $project->personnel()->wherePivot('role', 'pm')->count());
$this->assertTrue($project->contractors()->where('contractors.id', $mainContractor->id)->exists());
// 2. Change PM in Wizard Step 1 details -> Strictly 1 PM remains
$this->actingAs($this->admin)->post(route('projects.wizard.save-details', $project), [
'name' => 'Skyline Heights Commercial Center',
'project_type' => 'standard',
'pm_id' => $secondPM->ulid,
'contract_duration' => 180,
]);
$project->refresh();
$this->assertEquals(1, $project->personnel()->wherePivot('role', 'pm')->count());
$this->assertEquals($secondPM->id, $project->personnel()->wherePivot('role', 'pm')->first()->id);
// 3. In Wizard Step 4, assign multiple site operations personnel from Subcontractor
$this->actingAs($this->admin)->post(route('projects.wizard.labor', $project), [
'user_ulids' => [$siteTechUser->ulid, $supervisorUser->ulid],
'labor' => [],
]);
$project->refresh();
// 4. Verify exactly 1 PM and multiple site operations
$this->assertEquals(1, $project->personnel()->wherePivot('role', 'pm')->count());
$this->assertEquals(2, $project->personnel()->wherePivot('role', 'member')->count());
// 5. Verify Subcontractor was automatically linked to the project in project_contractor
$this->assertTrue($project->contractors()->where('contractors.id', $subcontractor->id)->exists());
}
}