feat: enhance module RBAC permissions, financial billing workflows, task evidence uploading, and role analytics
Some checks failed
Tests / PHP 8.3 (push) Has been cancelled
Tests / PHP 8.4 (push) Has been cancelled
Tests / PHP 8.5 (push) Has been cancelled

This commit is contained in:
Ajjj
2026-08-13 18:46:26 +08:00
parent 10eb7be033
commit ccbd23d474
40 changed files with 2324 additions and 353 deletions

View File

@@ -407,15 +407,38 @@ class DashboardController extends Controller
->count();
// 2. PM Milestone & Progress Analytics
$milestonesQuery = \Modules\TimelineScheduling\Models\Milestone::query();
$milestonesQuery = \Modules\ProjectManagement\Models\ProjectMilestone::with('tasks');
if ($project) {
$milestonesQuery->where('project_id', $project->id);
} else {
$milestonesQuery->whereIn('project_id', $visibleProjectIds);
}
$totalMilestones = $milestonesQuery->count();
$completedMilestones = (clone $milestonesQuery)->where('status', 'completed')->count();
$inProgressMilestones = (clone $milestonesQuery)->where('status', 'in_progress')->count();
$allMilestones = $milestonesQuery->get();
$totalMilestones = $allMilestones->count();
$completedMilestones = 0;
$inProgressMilestones = 0;
foreach ($allMilestones as $m) {
$taskCount = $m->tasks->count();
$completedTaskCount = $m->tasks->filter(function ($t) {
$st = is_object($t->status) ? $t->status->value : $t->status;
return in_array(strtolower($st ?? ''), ['completed', 'closed', 'done']);
})->count();
$isCompleted = ($m->actual_date !== null) || ($taskCount > 0 && $completedTaskCount === $taskCount);
if ($isCompleted) {
$completedMilestones++;
} else {
$hasActivity = $m->planned_date !== null || $m->tasks->contains(function ($t) {
$st = is_object($t->status) ? $t->status->value : $t->status;
return in_array(strtolower($st ?? ''), ['in_progress', 'completed', 'closed', 'done']);
});
if ($hasActivity) {
$inProgressMilestones++;
}
}
}
// 3. Subcontractor / Bidding Analytics
$bidsQuery = \Modules\BiddingManagement\Models\BidPackage::query();
@@ -425,11 +448,33 @@ class DashboardController extends Controller
$bidsQuery->whereIn('project_id', $visibleProjectIds);
}
$activeBids = $bidsQuery->count();
$openInvitations = \Modules\BiddingManagement\Models\BidInvitation::where('status', 'sent')
->whereHas('package', function ($query) use ($visibleProjectIds, $project) {
$query->whereIn('project_id', $project ? [$project->id] : $visibleProjectIds);
})
->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
$totalItems = \Modules\MasterData\Models\Material::count();
@@ -437,6 +482,41 @@ class DashboardController extends Controller
$totalDocuments = \Modules\DocumentManagement\Models\Document::whereIn('project_id', $project ? [$project->id] : $visibleProjectIds)->count();
$pendingCashAdvances = \Modules\FinancialManagement\Models\CashAdvance::where('status', 'pending')->count();
// 5. Unconfirmed Payments
$pendingInvoicesList = \Modules\FinancialManagement\Models\FinancialInvoice::with('project:id,name,code')
->whereIn('project_id', $visibleProjectIds)
->where('status', 'payment_sent')
->get(['id', 'ulid', 'invoice_number', 'project_id', 'total_amount', 'status'])
->map(fn ($inv) => [
'id' => 'inv_' . $inv->id,
'ulid' => $inv->ulid,
'number' => $inv->invoice_number,
'project_name' => $inv->project?->name ?? 'Project',
'amount' => (float) $inv->total_amount,
'type' => 'invoice',
'confirm_url' => route('finance.confirm-payment', $inv->ulid),
'view_url' => route('finance.show', $inv->ulid),
]);
$pendingRetentionsList = \Modules\FinancialManagement\Models\RetentionEntry::with('project:id,name,code')
->whereIn('project_id', $visibleProjectIds)
->where('type', 'credit')
->whereIn('status', ['payment_sent', 'submitted'])
->get(['id', 'ulid', 'project_id', 'amount', 'status', 'description'])
->map(fn ($ret) => [
'id' => 'ret_' . $ret->id,
'ulid' => $ret->ulid,
'number' => 'Retention Release',
'project_name' => $ret->project?->name ?? 'Project',
'amount' => (float) $ret->amount,
'type' => 'retention',
'status' => $ret->status,
'confirm_url' => route('retention.confirm', $ret->ulid),
'view_url' => route('retention.index'),
]);
$mergedPayments = $pendingInvoicesList->concat($pendingRetentionsList);
return [
'role' => $roleName,
'roles' => $user?->getRoleNames()->values()->all() ?? [],
@@ -470,6 +550,11 @@ class DashboardController extends Controller
'documents' => $totalDocuments,
'pending_cash_advances' => $pendingCashAdvances,
],
'unconfirmed_payments' => [
'count' => $mergedPayments->count(),
'total_amount' => (float) $mergedPayments->sum('amount'),
'items' => $mergedPayments->values()->all(),
],
];
}
}