326 lines
12 KiB
PHP
326 lines
12 KiB
PHP
<?php
|
|
|
|
namespace Modules\ContractorManagement\Http\Controllers;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\User;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Inertia\Inertia;
|
|
use Modules\ContractorManagement\Enums\InvoiceStatus;
|
|
use Modules\ContractorManagement\Events\ContractorInvoicePaid;
|
|
use Modules\ContractorManagement\Events\ContractorInvoiceSubmitted;
|
|
use Modules\ContractorManagement\Models\Contractor;
|
|
use Modules\ContractorManagement\Models\ContractorInvoice;
|
|
use Modules\ProjectManagement\Models\Project;
|
|
use Modules\FinancialManagement\Models\FinancialInvoice;
|
|
use Spatie\Permission\Models\Role;
|
|
|
|
class ContractorController extends Controller
|
|
{
|
|
public function index(Request $request)
|
|
{
|
|
$query = Contractor::query();
|
|
|
|
if ($search = $request->search) {
|
|
$query->where(function ($q) use ($search) {
|
|
$q->where('company_name', 'like', "%{$search}%")
|
|
->orWhere('contact_person', 'like', "%{$search}%")
|
|
->orWhere('email', 'like', "%{$search}%");
|
|
});
|
|
}
|
|
|
|
if ($spec = $request->specialization) {
|
|
$query->where('specialization', $spec);
|
|
}
|
|
|
|
if ($status = $request->status) {
|
|
$query->where('status', $status);
|
|
}
|
|
|
|
$contractors = $query->withCount(['equipment', 'certifications', 'invoices', 'projects'])
|
|
->latest()
|
|
->paginate(15)
|
|
->withQueryString();
|
|
|
|
return Inertia::render('ContractorManagement::Contractors/Index', [
|
|
'contractors' => $contractors,
|
|
'filters' => $request->only(['search', 'specialization', 'status']),
|
|
]);
|
|
}
|
|
|
|
private function ensureCanManageContractors(): void
|
|
{
|
|
$user = auth()->user();
|
|
if ($user && $user->hasAnyRole(['Site Technical', 'Construction Supervisor', 'Site Operations', 'Site Engineer', 'Site Supervisor', 'Field Engineer', 'Safety Officer', 'Quality Inspector', 'Warehouse Staff'])) {
|
|
abort(403, 'Unauthorized action. Site Operations roles cannot modify contractors.');
|
|
}
|
|
}
|
|
|
|
public function create()
|
|
{
|
|
$this->ensureCanManageContractors();
|
|
return Inertia::render('ContractorManagement::Contractors/Create');
|
|
}
|
|
|
|
public function store(Request $request)
|
|
{
|
|
$this->ensureCanManageContractors();
|
|
$validated = $request->validate([
|
|
'company_name' => 'required|string|max:255',
|
|
'contact_person' => 'required|string|max:255',
|
|
'email' => 'required|email|unique:contractors,email',
|
|
'phone' => 'nullable|string|max:50',
|
|
'specialization' => 'nullable|string|max:100',
|
|
'address' => 'nullable|string',
|
|
'tax_id' => 'nullable|string|max:50',
|
|
'payment_terms' => 'required|in:net_15,net_30,net_60',
|
|
'shares_materials_catalog' => 'nullable|boolean',
|
|
'create_admin' => 'nullable|boolean',
|
|
'admin_name' => 'required_if:create_admin,true,1|nullable|string|max:255',
|
|
'admin_email' => 'required_if:create_admin,true,1|nullable|email|unique:users,email',
|
|
'admin_password' => 'required_if:create_admin,true,1|nullable|string|min:8',
|
|
]);
|
|
|
|
DB::transaction(function () use ($validated, $request) {
|
|
$contractorData = collect($validated)->except([
|
|
'create_admin',
|
|
'admin_name',
|
|
'admin_email',
|
|
'admin_password'
|
|
])->toArray();
|
|
|
|
$contractorData['status'] = 'active';
|
|
$contractorData['type'] = 'main';
|
|
|
|
$contractor = Contractor::create($contractorData);
|
|
|
|
if ($request->boolean('create_admin')) {
|
|
$user = User::create([
|
|
'name' => $validated['admin_name'],
|
|
'email' => $validated['admin_email'],
|
|
'password' => Hash::make($validated['admin_password']),
|
|
'user_type' => 'contractor',
|
|
'contractor_id' => $contractor->id,
|
|
'status' => 'active',
|
|
'must_change_password' => true,
|
|
]);
|
|
|
|
// Provision every contractor administrator with the single supported contractor role.
|
|
$roleName = 'Main Contractor Admin';
|
|
$role = Role::firstOrCreate(['name' => $roleName, 'guard_name' => 'web']);
|
|
$permission = \Spatie\Permission\Models\Permission::firstOrCreate([
|
|
'name' => 'users.access',
|
|
'guard_name' => 'web',
|
|
]);
|
|
$role->givePermissionTo($permission);
|
|
$user->assignRole($role);
|
|
}
|
|
});
|
|
|
|
return redirect()->route('contractors.index')->with('success', 'Contractor created successfully.');
|
|
}
|
|
|
|
public function show(Contractor $contractor)
|
|
{
|
|
$contractor->load([
|
|
'equipment.deployments.project:id,name',
|
|
'certifications',
|
|
'invoices.project:id,name',
|
|
'projects:id,name,code,status',
|
|
]);
|
|
|
|
// Projects can be linked through the legacy pivot or directly through
|
|
// projects.contractor_id. Show both sources in the contractor profile.
|
|
$directProjects = $contractor->directProjects()
|
|
->select('id', 'ulid', 'name', 'code', 'status', 'contract_value')
|
|
->get();
|
|
$contractor->setRelation(
|
|
'projects',
|
|
$contractor->projects->concat($directProjects)->unique('id')->values()
|
|
);
|
|
|
|
$financialInvoices = FinancialInvoice::with('project:id,ulid,name,code')
|
|
->whereIn('project_id', $contractor->projects->pluck('id'))
|
|
->latest('invoice_date')
|
|
->get(['id', 'ulid', 'project_id', 'invoice_number', 'status', 'total_amount', 'paid_amount', 'invoice_date', 'due_date']);
|
|
|
|
$projects = Project::select('id', 'ulid', 'name', 'code')->get();
|
|
$employees = User::select('id', 'ulid', 'name')->get();
|
|
|
|
return Inertia::render('ContractorManagement::Contractors/Show', [
|
|
'contractor' => $contractor,
|
|
'projects' => $projects,
|
|
'employees' => $employees,
|
|
'financialInvoices' => $financialInvoices,
|
|
]);
|
|
}
|
|
|
|
public function edit(Contractor $contractor)
|
|
{
|
|
$this->ensureCanManageContractors();
|
|
return Inertia::render('ContractorManagement::Contractors/Edit', [
|
|
'contractor' => $contractor,
|
|
]);
|
|
}
|
|
|
|
public function update(Request $request, Contractor $contractor)
|
|
{
|
|
$this->ensureCanManageContractors();
|
|
$validated = $request->validate([
|
|
'company_name' => 'required|string|max:255',
|
|
'contact_person' => 'required|string|max:255',
|
|
'email' => "required|email|unique:contractors,email,{$contractor->id}",
|
|
'phone' => 'nullable|string|max:50',
|
|
'specialization' => 'nullable|string|max:100',
|
|
'address' => 'nullable|string',
|
|
'tax_id' => 'nullable|string|max:50',
|
|
'payment_terms' => 'required|in:net_15,net_30,net_60',
|
|
'shares_materials_catalog' => 'nullable|boolean',
|
|
]);
|
|
|
|
$contractor->update($validated);
|
|
|
|
return redirect()->route('contractors.show', $contractor)->with('success', 'Contractor updated.');
|
|
}
|
|
|
|
public function destroy(Contractor $contractor)
|
|
{
|
|
$this->ensureCanManageContractors();
|
|
$contractor->delete();
|
|
return redirect()->route('contractors.index')->with('success', 'Contractor deleted.');
|
|
}
|
|
|
|
// --- Equipment ---
|
|
public function storeEquipment(Request $request, Contractor $contractor)
|
|
{
|
|
$validated = $request->validate([
|
|
'name' => 'required|string|max:255',
|
|
'type' => 'nullable|string|max:50',
|
|
'serial_number' => 'nullable|string|max:100',
|
|
'daily_rate' => 'nullable|numeric|min:0',
|
|
]);
|
|
|
|
$contractor->equipment()->create($validated);
|
|
|
|
return back()->with('success', 'Equipment added.');
|
|
}
|
|
|
|
// --- Certifications ---
|
|
public function storeCertification(Request $request, Contractor $contractor)
|
|
{
|
|
$validated = $request->validate([
|
|
'name' => 'required|string|max:255',
|
|
'certification_number' => 'nullable|string|max:100',
|
|
'issued_date' => 'nullable|date',
|
|
'expiry_date' => 'nullable|date|after:issued_date',
|
|
]);
|
|
|
|
$contractor->certifications()->create($validated);
|
|
|
|
return back()->with('success', 'Certification added.');
|
|
}
|
|
|
|
// --- Project Assignment ---
|
|
public function assignProject(Request $request, Contractor $contractor)
|
|
{
|
|
$validated = $request->validate([
|
|
'project_id' => 'required|string',
|
|
'role' => 'required|in:subcontractor,supplier,consultant',
|
|
'contract_amount' => 'nullable|numeric|min:0',
|
|
]);
|
|
|
|
$projectId = Project::resolveUlidToId($validated['project_id']);
|
|
|
|
$contractor->projects()->syncWithoutDetaching([
|
|
$projectId => [
|
|
'role' => $validated['role'],
|
|
'contract_amount' => $validated['contract_amount'] ?? 0,
|
|
],
|
|
]);
|
|
|
|
return back()->with('success', 'Contractor assigned to project.');
|
|
}
|
|
|
|
public function removeProject(Contractor $contractor, Project $project)
|
|
{
|
|
$contractor->projects()->detach($project->id);
|
|
return back()->with('success', 'Contractor removed from project.');
|
|
}
|
|
|
|
// --- Invoices ---
|
|
public function storeInvoice(Request $request, Contractor $contractor)
|
|
{
|
|
$validated = $request->validate([
|
|
'project_id' => 'nullable|string',
|
|
'invoice_number' => 'required|string|unique:contractor_invoices',
|
|
'amount' => 'required|numeric|min:0.01',
|
|
'description' => 'nullable|string',
|
|
'invoice_date' => 'required|date',
|
|
'due_date' => 'nullable|date',
|
|
]);
|
|
|
|
// Resolve ULID → ID for project
|
|
if (!empty($validated['project_id'])) {
|
|
$validated['project_id'] = Project::resolveUlidToId($validated['project_id']);
|
|
}
|
|
|
|
$contractor->invoices()->create($validated);
|
|
|
|
return back()->with('success', 'Invoice created.');
|
|
}
|
|
|
|
public function submitInvoice(ContractorInvoice $invoice)
|
|
{
|
|
try {
|
|
$invoice->transitionTo(InvoiceStatus::Submitted);
|
|
ContractorInvoiceSubmitted::dispatch($invoice);
|
|
} catch (\InvalidArgumentException $e) {
|
|
return back()->with('error', $e->getMessage());
|
|
}
|
|
|
|
return back()->with('success', 'Invoice submitted for approval.');
|
|
}
|
|
|
|
public function approveInvoice(ContractorInvoice $invoice)
|
|
{
|
|
$user = auth()->user();
|
|
$isHigherUp = $user->user_type === 'admin' ||
|
|
$user->roles()->whereIn('name', ['Super Admin', 'admin', 'Main Contractor Admin'])->exists();
|
|
|
|
if (!$isHigherUp) {
|
|
return back()->with('error', 'Unauthorized. Only Main Contractor Admin or Super Admin can approve subcontractor invoices.');
|
|
}
|
|
|
|
try {
|
|
$invoice->transitionTo(InvoiceStatus::Approved);
|
|
} catch (\InvalidArgumentException $e) {
|
|
return back()->with('error', $e->getMessage());
|
|
}
|
|
|
|
return back()->with('success', 'Invoice approved.');
|
|
}
|
|
|
|
public function payInvoice(Request $request, ContractorInvoice $invoice)
|
|
{
|
|
$validated = $request->validate([
|
|
'amount' => 'required|numeric|min:0.01',
|
|
'payment_method' => 'required|in:bank_transfer,check,cash',
|
|
'reference_number' => 'nullable|string',
|
|
'payment_date' => 'required|date',
|
|
]);
|
|
|
|
$invoice->payments()->create($validated);
|
|
|
|
try {
|
|
$invoice->transitionTo(InvoiceStatus::Paid);
|
|
ContractorInvoicePaid::dispatch($invoice);
|
|
} catch (\InvalidArgumentException $e) {
|
|
return back()->with('error', $e->getMessage());
|
|
}
|
|
|
|
return back()->with('success', 'Payment recorded and invoice marked as paid.');
|
|
}
|
|
}
|