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:
@@ -13,6 +13,7 @@ use Modules\ProjectManagement\Enums\ProjectStatus;
|
||||
use Modules\ProjectManagement\Enums\WeatherCondition;
|
||||
use Modules\ProjectManagement\Events\ProjectStatusChanged;
|
||||
use Modules\ProjectManagement\Models\Project;
|
||||
use Modules\ProjectManagement\Models\ProjectClassification;
|
||||
use Modules\ProjectManagement\Models\ProjectMilestone;
|
||||
use Modules\ProjectManagement\Models\Task;
|
||||
use Modules\Labors\Models\Labor;
|
||||
@@ -79,21 +80,23 @@ class ProjectController extends Controller
|
||||
{
|
||||
abort_unless($this->canCreateProject(request()->user()), 403);
|
||||
|
||||
$employees = User::whereIn('user_type', ['admin', 'employee'])
|
||||
$employees = User::whereIn('user_type', ['admin', 'employee', 'contractor'])
|
||||
->where(function ($q) {
|
||||
$q->whereIn('user_type', ['admin'])
|
||||
->orWhereHas('roles', function ($rq) {
|
||||
$rq->whereIn('name', ['Project Manager', 'Main Contractor Admin', 'Super Admin', 'admin', 'Construction Supervisor', 'Site Technical']);
|
||||
$rq->whereIn('name', ['Project Manager', 'Contractor Project Manager', 'Main Contractor Admin', 'Super Admin', 'admin', 'Construction Supervisor', 'Site Technical']);
|
||||
});
|
||||
})
|
||||
->with('employeeProfile')
|
||||
->select('id', 'ulid', 'name', 'email', 'profile_picture')
|
||||
->with(['employeeProfile', 'roles'])
|
||||
->select('id', 'ulid', 'name', 'email', 'user_type', 'profile_picture')
|
||||
->get();
|
||||
$projects = Project::active()->with('parentProject:id,ulid,name,code')->get(['id', 'ulid', 'name', 'code', 'parent_project_id', 'project_type']);
|
||||
$classifications = ProjectClassification::orderBy('id')->get(['id', 'code', 'name', 'description']);
|
||||
|
||||
return Inertia::render('ProjectManagement::Projects/Create', [
|
||||
'employees' => $employees,
|
||||
'projects' => $projects,
|
||||
'classifications' => $classifications,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -127,6 +130,8 @@ class ProjectController extends Controller
|
||||
'start_date' => 'nullable|date',
|
||||
'target_end_date' => 'nullable|date|after_or_equal:start_date',
|
||||
'project_type' => 'required|string|in:standard,special,extension',
|
||||
'classifications' => 'nullable|array',
|
||||
'classifications.*' => 'string',
|
||||
'parent_project_id' => 'nullable|string',
|
||||
'is_unprofitable' => 'nullable|boolean',
|
||||
]);
|
||||
@@ -136,6 +141,7 @@ class ProjectController extends Controller
|
||||
}
|
||||
$validated['is_unprofitable'] = $validated['is_unprofitable'] ?? false;
|
||||
$validated['contract_value'] = $validated['contract_value'] ?? 0;
|
||||
$validated['classifications'] = $validated['classifications'] ?? [];
|
||||
$validated['current_wizard_step'] = 2;
|
||||
|
||||
$project = Project::create($validated);
|
||||
@@ -145,6 +151,10 @@ class ProjectController extends Controller
|
||||
$pmId = User::resolveUlidToId($request->pm_id);
|
||||
if ($pmId) {
|
||||
$project->personnel()->attach($pmId, ['role' => 'pm']);
|
||||
$pmUser = User::find($pmId);
|
||||
if ($pmUser && $pmUser->contractor_id) {
|
||||
$project->contractors()->syncWithoutDetaching([$pmUser->contractor_id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,9 +210,15 @@ class ProjectController extends Controller
|
||||
{
|
||||
$project->load(['personnel:id,name,email']);
|
||||
|
||||
$employees = User::whereIn('user_type', ['admin', 'employee'])
|
||||
->with('employeeProfile')
|
||||
->select('id', 'ulid', 'name', 'email', 'profile_picture')
|
||||
$employees = User::whereIn('user_type', ['admin', 'employee', 'contractor'])
|
||||
->where(function ($q) {
|
||||
$q->whereIn('user_type', ['admin'])
|
||||
->orWhereHas('roles', function ($rq) {
|
||||
$rq->whereIn('name', ['Project Manager', 'Contractor Project Manager', 'Main Contractor Admin', 'Super Admin', 'admin', 'Construction Supervisor', 'Site Technical']);
|
||||
});
|
||||
})
|
||||
->with(['employeeProfile', 'roles'])
|
||||
->select('id', 'ulid', 'name', 'email', 'user_type', 'profile_picture')
|
||||
->get();
|
||||
|
||||
return Inertia::render('ProjectManagement::Projects/Team', [
|
||||
@@ -260,15 +276,15 @@ class ProjectController extends Controller
|
||||
'materialsEstimates.material',
|
||||
]);
|
||||
|
||||
$employees = User::whereIn('user_type', ['admin', 'employee'])
|
||||
$employees = User::whereIn('user_type', ['admin', 'employee', 'contractor'])
|
||||
->where(function ($q) {
|
||||
$q->whereIn('user_type', ['admin'])
|
||||
->orWhereHas('roles', function ($rq) {
|
||||
$rq->whereIn('name', ['Project Manager', 'Main Contractor Admin', 'Super Admin', 'admin', 'Construction Supervisor', 'Site Technical']);
|
||||
$rq->whereIn('name', ['Project Manager', 'Contractor Project Manager', 'Main Contractor Admin', 'Super Admin', 'admin', 'Construction Supervisor', 'Site Technical']);
|
||||
});
|
||||
})
|
||||
->with('employeeProfile')
|
||||
->select('id', 'ulid', 'name', 'email', 'profile_picture')
|
||||
->with(['employeeProfile', 'roles'])
|
||||
->select('id', 'ulid', 'name', 'email', 'user_type', 'profile_picture')
|
||||
->get();
|
||||
$projects = Project::active()->with('parentProject:id,ulid,name,code')->where('id', '!=', $project->id)->get(['id', 'ulid', 'name', 'code', 'parent_project_id', 'project_type']);
|
||||
|
||||
@@ -304,6 +320,7 @@ class ProjectController extends Controller
|
||||
'project' => $project,
|
||||
'employees' => $employees,
|
||||
'projects' => $projects,
|
||||
'classifications' => ProjectClassification::orderBy('id')->get(['id', 'code', 'name', 'description']),
|
||||
'materials' => $materials,
|
||||
'materialGroups' => $materialGroups,
|
||||
'labors' => $labors,
|
||||
@@ -327,6 +344,8 @@ class ProjectController extends Controller
|
||||
'start_date' => 'nullable|date',
|
||||
'target_end_date' => 'nullable|date|after_or_equal:start_date',
|
||||
'project_type' => 'required|string|in:standard,special,extension',
|
||||
'classifications' => 'nullable|array',
|
||||
'classifications.*' => 'string',
|
||||
'parent_project_id' => 'nullable|string',
|
||||
'is_unprofitable' => 'nullable|boolean',
|
||||
]);
|
||||
@@ -337,6 +356,7 @@ class ProjectController extends Controller
|
||||
$validated['parent_project_id'] = null;
|
||||
}
|
||||
$validated['is_unprofitable'] = $validated['is_unprofitable'] ?? false;
|
||||
$validated['classifications'] = $validated['classifications'] ?? [];
|
||||
|
||||
$project->update($validated);
|
||||
|
||||
@@ -379,8 +399,21 @@ class ProjectController extends Controller
|
||||
return back()->with('error', 'User is already assigned to this project.');
|
||||
}
|
||||
|
||||
// If role is PM, ensure single PM by demoting existing PMs to member
|
||||
if ($request->role === 'pm') {
|
||||
\DB::table('project_user')
|
||||
->where('project_id', $project->id)
|
||||
->where('role', 'pm')
|
||||
->update(['role' => 'member']);
|
||||
}
|
||||
|
||||
$project->personnel()->attach($userId, ['role' => $request->role]);
|
||||
|
||||
$user = User::find($userId);
|
||||
if ($user && $user->contractor_id) {
|
||||
$project->contractors()->syncWithoutDetaching([$user->contractor_id]);
|
||||
}
|
||||
|
||||
return back()->with('success', 'Team member added.');
|
||||
}
|
||||
|
||||
@@ -440,11 +473,11 @@ class ProjectController extends Controller
|
||||
'personnel:id,ulid,name,email',
|
||||
]);
|
||||
|
||||
$employees = User::whereIn('user_type', ['admin', 'employee'])
|
||||
$employees = User::whereIn('user_type', ['admin', 'employee', 'contractor'])
|
||||
->where(function ($q) {
|
||||
$q->whereIn('user_type', ['admin'])
|
||||
->orWhereHas('roles', function ($rq) {
|
||||
$rq->whereIn('name', ['Project Manager', 'Main Contractor Admin', 'Super Admin', 'admin', 'Construction Supervisor', 'Site Technical']);
|
||||
$rq->whereIn('name', ['Project Manager', 'Contractor Project Manager', 'Main Contractor Admin', 'Super Admin', 'admin', 'Construction Supervisor', 'Site Technical']);
|
||||
});
|
||||
})
|
||||
->with(['employeeProfile', 'roles'])
|
||||
@@ -488,6 +521,7 @@ class ProjectController extends Controller
|
||||
'step' => $step,
|
||||
'employees' => $employees,
|
||||
'projects' => $parentProjects,
|
||||
'classifications' => ProjectClassification::orderBy('id')->get(['id', 'code', 'name', 'description']),
|
||||
'materials' => $materials,
|
||||
'materialGroups' => $materialGroups,
|
||||
'labors' => $labors,
|
||||
@@ -507,12 +541,15 @@ class ProjectController extends Controller
|
||||
'name' => 'required|string|max:255',
|
||||
'client_name' => 'nullable|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'pm_id' => 'nullable|string',
|
||||
'location' => 'nullable|string|max:255',
|
||||
'contract_value' => 'nullable|numeric|min:0',
|
||||
'contract_duration' => 'nullable|integer|min:0',
|
||||
'start_date' => 'nullable|date',
|
||||
'target_end_date' => 'nullable|date|after_or_equal:start_date',
|
||||
'project_type' => 'required|string|in:standard,special,extension',
|
||||
'classifications' => 'nullable|array',
|
||||
'classifications.*' => 'string',
|
||||
'parent_project_id' => 'nullable|string',
|
||||
'is_unprofitable' => 'nullable|boolean',
|
||||
]);
|
||||
@@ -523,6 +560,7 @@ class ProjectController extends Controller
|
||||
$validated['parent_project_id'] = null;
|
||||
}
|
||||
$validated['is_unprofitable'] = $validated['is_unprofitable'] ?? false;
|
||||
$validated['classifications'] = $validated['classifications'] ?? [];
|
||||
|
||||
if ($project->current_wizard_step < 2) {
|
||||
$validated['current_wizard_step'] = 2;
|
||||
@@ -530,6 +568,25 @@ class ProjectController extends Controller
|
||||
|
||||
$project->update($validated);
|
||||
|
||||
if ($request->filled('pm_id')) {
|
||||
$newPmId = User::resolveUlidToId($request->pm_id);
|
||||
if ($newPmId) {
|
||||
// Strictly single PM rule: demote any other existing PM
|
||||
\DB::table('project_user')
|
||||
->where('project_id', $project->id)
|
||||
->where('role', 'pm')
|
||||
->where('user_id', '!=', $newPmId)
|
||||
->update(['role' => 'member']);
|
||||
|
||||
$project->personnel()->syncWithoutDetaching([$newPmId => ['role' => 'pm']]);
|
||||
|
||||
$pmUser = User::find($newPmId);
|
||||
if ($pmUser && $pmUser->contractor_id) {
|
||||
$project->contractors()->syncWithoutDetaching([$pmUser->contractor_id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()->route('projects.wizard', [$project, 'step' => 2])
|
||||
->with('success', 'Project details saved.');
|
||||
}
|
||||
@@ -747,7 +804,7 @@ class ProjectController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
// Keep existing PM if present
|
||||
// Keep existing single PM if present
|
||||
$pm = $project->personnel()->wherePivot('role', 'pm')->first();
|
||||
if ($pm) {
|
||||
$userIds[$pm->id] = ['role' => 'pm'];
|
||||
@@ -756,6 +813,17 @@ class ProjectController extends Controller
|
||||
// Sync the project personnel
|
||||
if (isset($validated['team_ulids']) || isset($validated['user_ulids'])) {
|
||||
$project->personnel()->sync($userIds);
|
||||
|
||||
// Auto-link all contractors of assigned personnel to project_contractor
|
||||
$contractorIds = User::whereIn('id', array_keys($userIds))
|
||||
->whereNotNull('contractor_id')
|
||||
->pluck('contractor_id')
|
||||
->unique()
|
||||
->toArray();
|
||||
|
||||
if (!empty($contractorIds)) {
|
||||
$project->contractors()->syncWithoutDetaching($contractorIds);
|
||||
}
|
||||
}
|
||||
|
||||
if ($project->current_wizard_step < 5) {
|
||||
|
||||
Reference in New Issue
Block a user