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

@@ -212,60 +212,8 @@ class DashboardController extends Controller
$isContractorUser = $user && ($user->user_type === 'contractor' || $user->contractor_id !== null || $user->hasRole('Main Contractor Admin') || $user->hasRole('Contractor Admin'));
if ($isContractorUser && $user->contractor_id) {
// 1. Bid Invitations
$invitations = \Modules\BiddingManagement\Models\BidInvitation::with(['package:id,project_id,title', 'package.project:id,code,name'])
->where('contractor_id', $user->contractor_id)
->whereHas('package', function ($q) use ($project, $visibleProjectIds) {
if ($project) {
$q->where('project_id', $project->id);
} elseif (!empty($visibleProjectIds)) {
$q->whereIn('project_id', $visibleProjectIds);
}
})
->orderByDesc('created_at')
->take(5)
->get();
foreach ($invitations as $inv) {
$pkgTitle = $inv->package->title ?? 'Tender Package';
$projCode = $inv->package->project->code ?? '';
$activitiesList[] = [
'id' => 'bid_inv_' . $inv->id,
'time' => $inv->created_at?->diffForHumans() ?? 'Recently',
'timestamp' => $inv->created_at?->timestamp ?? 0,
'title' => sprintf('Received tender invitation for %s (%s)', $pkgTitle, $projCode),
'status' => 'info',
];
}
// 2. Bid Submissions
$submissions = \Modules\BiddingManagement\Models\BidSubmission::with(['invitation:id,bid_package_id,contractor_id', 'invitation.package:id,project_id,title', 'invitation.package.project:id,code,name'])
->whereHas('invitation', function ($q) use ($user, $project, $visibleProjectIds) {
$q->where('contractor_id', $user->contractor_id);
if ($project) {
$q->whereHas('package', fn($pq) => $pq->where('project_id', $project->id));
} elseif (!empty($visibleProjectIds)) {
$q->whereHas('package', fn($pq) => $pq->whereIn('project_id', $visibleProjectIds));
}
})
->orderByDesc('submitted_at')
->take(5)
->get();
foreach ($submissions as $sub) {
$pkgTitle = $sub->invitation->package->title ?? 'Bid Package';
$price = number_format((float) $sub->submitted_price, 2);
$activitiesList[] = [
'id' => 'bid_sub_' . $sub->id,
'time' => $sub->submitted_at ? \Carbon\Carbon::parse($sub->submitted_at)->diffForHumans() : 'Recently',
'timestamp' => $sub->submitted_at ? \Carbon\Carbon::parse($sub->submitted_at)->timestamp : 0,
'title' => sprintf('Submitted bid proposal for %s: ₱%s (%s)', $pkgTitle, $price, ucfirst($sub->status ?? 'submitted')),
'status' => $sub->status === 'awarded' ? 'completed' : 'in_progress',
];
}
// 3. Financial Invoices
$invoices = \Modules\FinancialManagement\Models\FinancialInvoice::with('project')
// 1. Contractor Invoices
$invoices = \Modules\ContractorManagement\Models\ContractorInvoice::with('project')
->where('contractor_id', $user->contractor_id)
->where(function ($q) use ($project, $visibleProjectIds) {
if ($project) {
@@ -280,12 +228,15 @@ class DashboardController extends Controller
foreach ($invoices as $inv) {
$projCode = $inv->project->code ?? '';
$statusVal = $inv->status instanceof \Modules\ContractorManagement\Enums\InvoiceStatus
? $inv->status->value
: (string) ($inv->status ?? '');
$activitiesList[] = [
'id' => 'inv_act_' . $inv->id,
'time' => $inv->updated_at?->diffForHumans() ?? 'Recently',
'timestamp' => $inv->updated_at?->timestamp ?? 0,
'title' => sprintf('Invoice %s status: %s (%s)', $inv->invoice_number, str_replace('_', ' ', $inv->status), $projCode),
'status' => $inv->status === 'paid' ? 'completed' : 'info',
'title' => sprintf('Invoice %s status: %s (%s)', $inv->invoice_number, str_replace('_', ' ', $statusVal), $projCode),
'status' => $statusVal === 'paid' ? 'completed' : 'info',
];
}
@@ -513,8 +464,8 @@ class DashboardController extends Controller
$pendingApprovalsQuery = \Modules\ApprovalWorkflow\Models\ApprovalChain::whereIn('status', ['in_review', 'pending']);
if ($user->hasRole('Project Manager') && !($user->user_type === 'admin' || $user->hasRole(['Super Admin', 'admin', 'Main Contractor Admin']))) {
$pendingApprovalsQuery->where(function ($q) {
$q->whereNull('chain_type')
->orWhere('chain_type', '!=', 'project_estimation');
$q->whereNull('type')
->orWhere('type', '!=', 'project_estimation');
})->where('approvable_type', '!=', Project::class);
}
$pendingApprovalsCount = $pendingApprovalsQuery->count();
@@ -532,7 +483,7 @@ class DashboardController extends Controller
$activeProjectsCount = (int) ($projectStats->active_count ?? 0);
// 2. PM Milestone & Progress Analytics (Scoped Relation Columns)
$milestonesQuery = \Modules\ProjectManagement\Models\ProjectMilestone::with('tasks:id,project_milestone_id,status');
$milestonesQuery = \Modules\ProjectManagement\Models\ProjectMilestone::with('tasks:id,milestone_id,status');
if ($project) {
$milestonesQuery->where('project_id', $project->id);
} else {
@@ -565,43 +516,7 @@ class DashboardController extends Controller
}
}
// 3. Subcontractor / Bidding Analytics
$bidsQuery = \Modules\BiddingManagement\Models\BidPackage::query();
if ($project) {
$bidsQuery->where('project_id', $project->id);
} else {
$bidsQuery->whereIn('project_id', $visibleProjectIds);
}
$activeBids = $bidsQuery->count();
$openInvitationsQuery = \Modules\BiddingManagement\Models\BidInvitation::query()
->whereIn('status', ['invited', 'accepted'])
->whereDoesntHave('submission');
if ($user?->contractor_id) {
$openInvitationsQuery->where('contractor_id', $user->contractor_id);
$openInvitations = $openInvitationsQuery
->whereHas('package', function ($query) use ($project) {
$query->where('status', 'open');
if ($project) {
$query->where('project_id', $project->id);
}
})
->count();
} else {
$openInvitations = $openInvitationsQuery
->whereHas('package', function ($query) use ($visibleProjectIds, $project) {
$query->where('status', 'open');
if ($project) {
$query->where('project_id', $project->id);
} elseif (!empty($visibleProjectIds)) {
$query->whereIn('project_id', $visibleProjectIds);
}
})
->count();
}
// 4. Warehouse & Technical Inventory (Cached Short TTL)
// 3. Warehouse & Technical Inventory (Cached Short TTL)
$totalItems = Cache::remember('master_materials_count', 300, fn () => \Modules\MasterData\Models\Material::count());
$totalWarehouses = Cache::remember('master_warehouses_count', 300, fn () => \Modules\MaterialLogistics\Models\Warehouse::count());
$totalDocuments = \Modules\DocumentManagement\Models\Document::whereIn('project_id', $project ? [$project->id] : $visibleProjectIds)->count();
@@ -635,7 +550,7 @@ class DashboardController extends Controller
'project_name' => $ret->project?->name ?? 'Project',
'amount' => (float) $ret->amount,
'type' => 'retention',
'status' => $ret->status,
'status' => is_object($ret->status) ? ($ret->status->value ?? (string) $ret->status) : (string) $ret->status,
'confirm_url' => route('retention.confirm', $ret->ulid),
'view_url' => route('retention.index'),
]);
@@ -681,10 +596,6 @@ class DashboardController extends Controller
'in_progress' => $inProgressMilestones,
'completion_rate' => $totalMilestones > 0 ? round(($completedMilestones / $totalMilestones) * 100, 1) : 0,
],
'bidding' => [
'active_packages' => $activeBids,
'open_invitations' => $openInvitations,
],
'logistics' => [
'catalog_items' => $totalItems,
'warehouses' => $totalWarehouses,