feat: implement core ERP modules, multi-tenant architecture, and comprehensive system workflow documentation.

This commit is contained in:
Ajjj
2026-08-05 01:18:31 +08:00
parent 1fcb468dc1
commit c03de704e2
46 changed files with 1902 additions and 387 deletions

View File

@@ -20,12 +20,18 @@ class MaterialRequisitionController extends Controller
public function index(Request $request)
{
$query = MaterialRequisition::with([
'requester:id,ulid,name',
'approver:id,ulid,name',
// The requisition is already tenant/project-scoped. Load the
// display names without User's tenant scope so a valid requester
// from the assigned project is not serialized as null.
'requester' => fn ($userQuery) => $userQuery->withoutGlobalScopes()->select('id', 'ulid', 'name'),
'approver' => fn ($userQuery) => $userQuery->withoutGlobalScopes()->select('id', 'ulid', 'name'),
'project:id,ulid,name,code',
'items:id,material_requisition_id,quantity,unit_cost'
]);
// Requisitions inherit visibility from their projects.
$query->whereIn('project_id', $this->availableProjectsQuery()->select('projects.id'));
if ($request->filled('project_ulid')) {
$project = Project::where('ulid', $request->project_ulid)->first();
if ($project) {
@@ -47,7 +53,8 @@ class MaterialRequisitionController extends Controller
$prefilledItems = [];
if ($request->filled('project_ulid')) {
$selectedProject = Project::where('ulid', $request->project_ulid)
$selectedProject = $this->availableProjectsQuery()
->where('ulid', $request->project_ulid)
->with('materialsEstimates.material:id,ulid,name,unit,unit_cost')
->firstOrFail();
@@ -97,7 +104,12 @@ class MaterialRequisitionController extends Controller
return Inertia::render('MaterialLogistics::Requisitions/Form', [
'materials' => $materials,
'materialGroups' => $materialGroups,
'projects' => Project::active()->with('contractor:id,company_name')->select('id', 'ulid', 'name', 'code', 'contractor_id', 'client_name', 'location', 'start_date', 'target_end_date', 'status', 'contract_value')->get(),
'projects' => $this->availableProjectsQuery()
->active()
->where('current_wizard_step', '>=', 8)
->with('contractor:id,company_name')
->select('id', 'ulid', 'name', 'code', 'contractor_id', 'client_name', 'location', 'start_date', 'target_end_date', 'status', 'contract_value')
->get(),
'selectedProject' => $selectedProject,
'prefilledItems' => $prefilledItems,
]);
@@ -114,7 +126,9 @@ class MaterialRequisitionController extends Controller
'items.*.unit_cost' => 'required|numeric|min:0',
]);
$project = Project::where('ulid', $validated['project_ulid'])->firstOrFail();
$project = $this->availableProjectsQuery()
->where('ulid', $validated['project_ulid'])
->firstOrFail();
if ($project->current_wizard_step < 7) {
return redirect()->back()->with('error', 'Cannot create material requisition: Project estimation is not submitted or approved.');
}
@@ -197,7 +211,11 @@ class MaterialRequisitionController extends Controller
'requisition' => $requisition,
'materials' => $materials,
'materialGroups' => $materialGroups,
'projects' => Project::active()->where('current_wizard_step', '>=', 8)->select('id', 'ulid', 'name', 'code', 'client_name', 'location', 'start_date', 'target_end_date', 'status', 'contract_value')->get(),
'projects' => $this->availableProjectsQuery()
->active()
->where('current_wizard_step', '>=', 8)
->select('id', 'ulid', 'name', 'code', 'client_name', 'location', 'start_date', 'target_end_date', 'status', 'contract_value')
->get(),
]);
}
@@ -216,7 +234,9 @@ class MaterialRequisitionController extends Controller
'items.*.unit_cost' => 'required|numeric|min:0',
]);
$project = Project::where('ulid', $validated['project_ulid'])->firstOrFail();
$project = $this->availableProjectsQuery()
->where('ulid', $validated['project_ulid'])
->firstOrFail();
if ($project->current_wizard_step < 8) {
return redirect()->back()->with('error', 'Cannot update material requisition: Project estimation is not approved.');
}
@@ -279,8 +299,27 @@ class MaterialRequisitionController extends Controller
return back()->with('error', 'Only draft requisitions can be submitted.');
}
// Find personnel with approve_mr permission across the whole system
$approverIds = \App\Models\User::permission('approve_mr')
// Material Requests may be approved by an explicit permission or by
// the system's Project Manager/executive roles. Query without the
// tenant scope so a site user can submit a request for an assigned
// project even when the approver belongs to another tenant context.
$approverIds = \App\Models\User::withoutGlobalScopes()
->where(function ($query) {
$query->whereHas('roles', function ($roleQuery) {
$roleQuery->whereIn('name', [
'Project Manager',
'project_manager',
'Super Admin',
'admin',
'Main Contractor Admin',
]);
})
->orWhere('user_type', 'admin')
->orWhereHas('permissions', function ($permissionQuery) {
$permissionQuery->where('name', 'approve_mr');
});
})
->where('status', 'active')
->pluck('id')
->values()
->toArray();
@@ -341,4 +380,45 @@ class MaterialRequisitionController extends Controller
return $pdf->download("MR-{$requisition->document_number}.pdf");
}
private function availableProjectsQuery()
{
$query = Project::withoutGlobalScope(\App\Scopes\TenantScope::class);
$user = auth()->user();
$isSiteOperationsUser = $user && $user->hasAnyRole([
'Site Technical',
'Construction Supervisor',
'Site Operations',
'Site Engineer',
'Site Supervisor',
]);
if ($user?->contractor_id) {
$contractorIds = DB::table('contractors')
->where('id', $user->contractor_id)
->orWhere('parent_id', $user->contractor_id)
->pluck('id')
->all();
$query->where(function ($projectQuery) use ($contractorIds, $user, $isSiteOperationsUser) {
$projectQuery
->whereIn('projects.contractor_id', $contractorIds)
->orWhereHas('contractors', function ($contractorQuery) use ($contractorIds) {
$contractorQuery->whereIn('contractors.id', $contractorIds);
});
if ($isSiteOperationsUser) {
$projectQuery->orWhereHas('personnel', function ($personnelQuery) use ($user) {
$personnelQuery->where('users.id', $user->id);
});
}
});
} elseif ($isSiteOperationsUser) {
$query->whereHas('personnel', function ($personnelQuery) use ($user) {
$personnelQuery->where('users.id', $user->id);
});
}
return $query;
}
}