Files
GSB-Construction/Modules/BiddingManagement/app/Http/Controllers/BidPackageController.php

283 lines
10 KiB
PHP

<?php
namespace Modules\BiddingManagement\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Inertia\Inertia;
use Modules\BiddingManagement\Enums\BidPackageStatus;
use Modules\BiddingManagement\Events\BidPackageOpened;
use Modules\BiddingManagement\Models\BidPackage;
use Modules\BiddingManagement\Traits\AuthorizesBiddingManagement;
use Modules\ContractorManagement\Models\Contractor;
use Modules\ProjectManagement\Models\Project;
class BidPackageController extends Controller
{
use AuthorizesBiddingManagement;
public function index(Request $request)
{
$user = Auth::user();
abort_unless(
$this->isBiddingManager($user) || $this->isContractorUser($user),
403,
'Only Project Manager, executive, or invited contractor roles can access bidding.'
);
$query = BidPackage::with(['project:id,name,code', 'creator:id,name', 'invitations:id,bid_package_id,ulid,contractor_id,status'])
->withCount(['invitations', 'submissions']);
if ($search = $request->search) {
$query->where('title', 'like', "%{$search}%");
}
if ($status = $request->status) {
$query->where('status', $status);
}
if ($mode = $request->evaluation_mode) {
$query->where('evaluation_mode', $mode);
}
if ($projectUlid = $request->project) {
$projectId = Project::resolveUlidToId($projectUlid);
$query->where('project_id', $projectId);
}
// Contractor Admin users should ONLY see published (non-draft) bid packages where their company is invited
if (! $this->isBiddingManager($user) && $this->isContractorUser($user)) {
$query->where('status', '!=', BidPackageStatus::Draft->value)
->whereHas('invitations', function ($q) use ($user) {
$q->where('contractor_id', $user->contractor_id);
});
}
$packages = $query->latest()->paginate(15)->withQueryString();
return Inertia::render('BiddingManagement::Bids/Index', [
'packages' => $packages,
'filters' => $request->only(['search', 'status', 'evaluation_mode', 'project']),
'projects' => Project::select('id', 'ulid', 'name', 'code')->get(),
]);
}
public function create()
{
$this->authorizePackageManagement();
return Inertia::render('BiddingManagement::Bids/Create', [
'projects' => Project::where('current_wizard_step', '>=', 7)->select('id', 'ulid', 'name', 'code')->get(),
]);
}
public function store(Request $request)
{
$this->authorizePackageManagement();
$validated = $request->validate([
'project_id' => 'required|string',
'title' => 'required|string|max:255',
'description' => 'nullable|string',
'evaluation_mode' => 'required|in:simple,scored',
'submission_deadline' => 'nullable|date',
'validity_period' => 'nullable|date',
'instructions' => 'nullable|string',
'criteria' => 'nullable|array',
'criteria.*.name' => 'required_with:criteria|string|max:100',
'criteria.*.weight' => 'required_with:criteria|numeric|min:0|max:100',
'criteria.*.description' => 'nullable|string',
]);
$validated['project_id'] = Project::resolveUlidToId($validated['project_id']);
$project = Project::find($validated['project_id']);
abort_unless($project, 422, 'The selected project could not be found.');
abort_unless(
$project->current_wizard_step >= 7,
422,
'The project must finish initialization before a bid package can be created.'
);
abort_unless(
$this->isExecutive(Auth::user())
|| $project->personnel()->where('users.id', Auth::id())->wherePivot('role', 'pm')->exists(),
403,
'Only the assigned Project Manager or an executive can create this bid package.'
);
$validated['created_by'] = Auth::id();
$criteria = $validated['criteria'] ?? [];
unset($validated['criteria']);
if ($validated['evaluation_mode'] === 'scored' && count($criteria) > 0) {
$totalWeight = array_sum(array_column($criteria, 'weight'));
if ($totalWeight > 100) {
return back()->withErrors(['criteria' => "Total criteria weight ({$totalWeight}%) exceeds 100%."]);
}
}
$package = BidPackage::create($validated);
foreach ($criteria as $index => $criterion) {
$package->criteria()->create([
'name' => $criterion['name'],
'weight' => $criterion['weight'],
'description' => $criterion['description'] ?? null,
'sort_order' => $index,
]);
}
return redirect()->route('bids.show', $package)->with('success', 'Bid package created.');
}
public function show(BidPackage $bid)
{
$user = Auth::user();
abort_unless(
$this->isBiddingManager($user) || $this->isContractorUser($user),
403,
'Only Project Manager, executive, or invited contractor roles can access bidding.'
);
if (! $this->isBiddingManager($user) && $this->isContractorUser($user)) {
abort_if($bid->status === BidPackageStatus::Draft, 403, 'This bid package is currently in draft state.');
abort_unless($bid->invitations()->where('contractor_id', $user->contractor_id)->exists(), 403, 'Your company has not been invited to this bid package.');
}
$bid->load([
'project:id,ulid,name,code',
'creator:id,name',
'criteria',
'invitations.contractor:id,ulid,company_name,specialization',
'invitations.submission.scores',
'invitations.submission.documents',
'award.submission.invitation.contractor:id,company_name',
'award.awardedBy:id,name',
'documents',
]);
$contractors = Contractor::where('status', 'active')
->select('id', 'ulid', 'company_name', 'specialization')
->get();
return Inertia::render('BiddingManagement::Bids/Show', [
'package' => $bid,
'contractors' => $contractors,
]);
}
public function edit(BidPackage $bid)
{
$this->authorizePackageManagement();
abort_unless($bid->status === BidPackageStatus::Draft, 403, 'Only draft packages can be edited.');
$bid->load('criteria');
return Inertia::render('BiddingManagement::Bids/Edit', [
'package' => $bid,
'projects' => Project::where('current_wizard_step', '>=', 7)->select('id', 'ulid', 'name', 'code')->get(),
]);
}
public function update(Request $request, BidPackage $bid)
{
$this->authorizePackageManagement();
abort_unless($bid->status === BidPackageStatus::Draft, 403, 'Only draft packages can be edited.');
$validated = $request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|string',
'evaluation_mode' => 'required|in:simple,scored',
'submission_deadline' => 'nullable|date',
'validity_period' => 'nullable|date',
'instructions' => 'nullable|string',
'criteria' => 'nullable|array',
'criteria.*.name' => 'required_with:criteria|string|max:100',
'criteria.*.weight' => 'required_with:criteria|numeric|min:0|max:100',
'criteria.*.description' => 'nullable|string',
]);
$criteria = $validated['criteria'] ?? [];
unset($validated['criteria']);
$bid->update($validated);
// Replace criteria wholesale
$bid->criteria()->delete();
foreach ($criteria as $index => $criterion) {
$bid->criteria()->create([
'name' => $criterion['name'],
'weight' => $criterion['weight'],
'description' => $criterion['description'] ?? null,
'sort_order' => $index,
]);
}
return redirect()->route('bids.show', $bid)->with('success', 'Bid package updated.');
}
public function destroy(BidPackage $bid)
{
$this->authorizePackageManagement();
abort_unless($bid->status === BidPackageStatus::Draft, 403, 'Only draft packages can be deleted.');
$bid->delete();
return redirect()->route('bids.index')->with('success', 'Bid package deleted.');
}
public function publish(BidPackage $bid)
{
$this->authorizePackageManagement();
try {
$bid->transitionTo(BidPackageStatus::Open);
BidPackageOpened::dispatch($bid);
} catch (\InvalidArgumentException $e) {
return back()->with('error', $e->getMessage());
}
return back()->with('success', 'Bid package is now open for submissions.');
}
public function startEvaluation(BidPackage $bid)
{
$this->authorizePackageManagement();
try {
$bid->transitionTo(BidPackageStatus::Evaluating);
} catch (\InvalidArgumentException $e) {
return back()->with('error', $e->getMessage());
}
return back()->with('success', 'Evaluation phase started.');
}
public function cancel(BidPackage $bid)
{
$this->authorizePackageManagement();
try {
$bid->transitionTo(BidPackageStatus::Cancelled);
} catch (\InvalidArgumentException $e) {
return back()->with('error', $e->getMessage());
}
return back()->with('success', 'Bid package cancelled.');
}
private function authorizePackageManagement(): void
{
$this->authorizeBiddingManagement();
}
private function isContractorUser($user): bool
{
return $user !== null
&& ! $this->isBiddingManager($user)
&& ($user->user_type === 'contractor' || $user->contractor_id !== null);
}
}