feat: implement core ERP modules, multi-tenant architecture, and comprehensive system workflow documentation.
This commit is contained in:
@@ -25,7 +25,11 @@ class FinanceController extends Controller
|
||||
// --- Invoice List ---
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = FinancialInvoice::with('project:id,name,code');
|
||||
$query = FinancialInvoice::with([
|
||||
'project' => fn ($query) => $query
|
||||
->withoutGlobalScopes()
|
||||
->select('id', 'ulid', 'name', 'code'),
|
||||
]);
|
||||
|
||||
if ($status = $request->status) {
|
||||
$query->where('status', $status);
|
||||
@@ -34,17 +38,22 @@ class FinanceController extends Controller
|
||||
$query->where('project_id', $projectId);
|
||||
}
|
||||
|
||||
$query->whereIn('project_id', $this->availableProjectIdsQuery());
|
||||
|
||||
$invoices = $query->latest()->paginate(15)->withQueryString();
|
||||
$projects = Project::select('id', 'ulid', 'name', 'code')->get();
|
||||
$projects = Project::whereIn('projects.id', $this->availableProjectIdsQuery())
|
||||
->select('id', 'ulid', 'name', 'code')->get();
|
||||
|
||||
// Compute summary stats
|
||||
$allInvoices = FinancialInvoice::query();
|
||||
$allInvoices = FinancialInvoice::whereIn('project_id', $this->availableProjectIdsQuery());
|
||||
$summary = [
|
||||
'total_billed' => (float) $allInvoices->sum('total_amount'),
|
||||
'total_paid' => (float) $allInvoices->sum('paid_amount'),
|
||||
'outstanding' => (float) $allInvoices->whereNotIn('status', ['paid'])->sum(\DB::raw('total_amount - paid_amount')),
|
||||
'total_retention' => (float) RetentionEntry::where('type', 'debit')->sum('amount')
|
||||
- (float) RetentionEntry::where('type', 'credit')->sum('amount'),
|
||||
'total_retention' => (float) RetentionEntry::where('type', 'debit')
|
||||
->whereIn('project_id', $this->availableProjectIdsQuery())->sum('amount')
|
||||
- (float) RetentionEntry::where('type', 'credit')
|
||||
->whereIn('project_id', $this->availableProjectIdsQuery())->sum('amount'),
|
||||
];
|
||||
|
||||
return Inertia::render('FinancialManagement::Invoices/Index', [
|
||||
@@ -58,7 +67,8 @@ class FinanceController extends Controller
|
||||
// --- Create (Progress Billing) ---
|
||||
public function create()
|
||||
{
|
||||
$projects = Project::select('id', 'ulid', 'name', 'code', 'contract_value', 'last_billed_percentage')
|
||||
$projects = Project::whereIn('projects.id', $this->availableProjectIdsQuery())
|
||||
->select('id', 'ulid', 'name', 'code', 'contract_value', 'last_billed_percentage')
|
||||
->where('current_wizard_step', '>=', 7)
|
||||
->whereNotIn('status', ['completed', 'closed'])
|
||||
->get();
|
||||
@@ -98,6 +108,7 @@ class FinanceController extends Controller
|
||||
// --- Show Invoice ---
|
||||
public function show(FinancialInvoice $invoice)
|
||||
{
|
||||
abort_unless($this->canAccessProject(auth()->user(), $invoice->project_id), 403);
|
||||
$invoice->load(['project:id,name,code', 'lineItems', 'retentionEntries']);
|
||||
|
||||
return Inertia::render('FinancialManagement::Invoices/Show', [
|
||||
@@ -108,13 +119,14 @@ class FinanceController extends Controller
|
||||
// --- State Transitions ---
|
||||
public function submit(Request $request, FinancialInvoice $invoice)
|
||||
{
|
||||
abort_unless($this->canAccessProject($request->user(), $invoice->project_id), 403);
|
||||
try {
|
||||
$invoice->transitionTo(InvoiceStatus::Submitted);
|
||||
|
||||
// Fetch Admins and Super Admins as approvers
|
||||
$adminIds = User::where('user_type', 'admin')
|
||||
->orWhereHas('roles', function ($q) {
|
||||
$q->whereIn('name', ['Super Admin', 'admin']);
|
||||
$q->whereIn('name', ['Super Admin', 'admin', 'Project Manager', 'Main Contractor Admin']);
|
||||
})
|
||||
->pluck('id')
|
||||
->toArray();
|
||||
@@ -138,6 +150,7 @@ class FinanceController extends Controller
|
||||
public function approve(FinancialInvoice $invoice)
|
||||
{
|
||||
$user = auth()->user();
|
||||
abort_unless($this->canAccessProject($user, $invoice->project_id), 403);
|
||||
$isApprover = $user->user_type === 'admin' ||
|
||||
$user->roles()->whereIn('name', ['Super Admin', 'admin'])->exists();
|
||||
|
||||
@@ -158,6 +171,7 @@ class FinanceController extends Controller
|
||||
public function reject(FinancialInvoice $invoice)
|
||||
{
|
||||
$user = auth()->user();
|
||||
abort_unless($this->canAccessProject($user, $invoice->project_id), 403);
|
||||
$isApprover = $user->user_type === 'admin' ||
|
||||
$user->roles()->whereIn('name', ['Super Admin', 'admin'])->exists();
|
||||
|
||||
@@ -176,6 +190,7 @@ class FinanceController extends Controller
|
||||
|
||||
public function send(FinancialInvoice $invoice)
|
||||
{
|
||||
abort_unless($this->canAccessProject(auth()->user(), $invoice->project_id), 403);
|
||||
try {
|
||||
$invoice->transitionTo(InvoiceStatus::Sent);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
@@ -187,6 +202,7 @@ class FinanceController extends Controller
|
||||
|
||||
public function recordPayment(Request $request, FinancialInvoice $invoice)
|
||||
{
|
||||
abort_unless($this->canAccessProject($request->user(), $invoice->project_id), 403);
|
||||
$validated = $request->validate([
|
||||
'amount' => 'required|numeric|min:0.01',
|
||||
]);
|
||||
@@ -203,17 +219,26 @@ class FinanceController extends Controller
|
||||
// --- Retention Ledger ---
|
||||
public function retention(Request $request)
|
||||
{
|
||||
$query = RetentionEntry::with('project:id,name,code', 'invoice:id,invoice_number');
|
||||
$query = RetentionEntry::with([
|
||||
'project' => fn ($query) => $query
|
||||
->withoutGlobalScopes()
|
||||
->select('id', 'ulid', 'name', 'code'),
|
||||
'invoice:id,invoice_number',
|
||||
]);
|
||||
|
||||
if ($projectId = $request->project_id) {
|
||||
$query->where('project_id', $projectId);
|
||||
}
|
||||
|
||||
$query->whereIn('project_id', $this->availableProjectIdsQuery());
|
||||
|
||||
$entries = $query->latest()->paginate(20)->withQueryString();
|
||||
$projects = Project::select('id', 'ulid', 'name', 'code')->get();
|
||||
$projects = Project::whereIn('projects.id', $this->availableProjectIdsQuery())
|
||||
->select('id', 'ulid', 'name', 'code')->get();
|
||||
|
||||
// Compute per-project totals
|
||||
$projectTotals = RetentionEntry::selectRaw('project_id, type, SUM(amount) as total')
|
||||
$projectTotals = RetentionEntry::whereIn('project_id', $this->availableProjectIdsQuery())
|
||||
->selectRaw('project_id, type, SUM(amount) as total')
|
||||
->groupBy('project_id', 'type')
|
||||
->get()
|
||||
->groupBy('project_id')
|
||||
@@ -234,14 +259,22 @@ class FinanceController extends Controller
|
||||
// Cash Advances
|
||||
public function cashAdvances(Request $request)
|
||||
{
|
||||
$query = \Modules\FinancialManagement\Models\CashAdvance::with(['project:id,name,code', 'requester:id,name,email', 'approver:id,name,email']);
|
||||
$user = $request->user();
|
||||
$isApprover = $this->canApproveCashAdvance($user);
|
||||
$query = $isApprover
|
||||
? \Modules\FinancialManagement\Models\CashAdvance::withoutGlobalScopes()
|
||||
: \Modules\FinancialManagement\Models\CashAdvance::query();
|
||||
|
||||
$query->with(['project:id,name,code', 'requester:id,name,email', 'approver:id,name,email']);
|
||||
$query->whereIn('project_id', $this->availableProjectIdsQuery());
|
||||
|
||||
if ($projectId = $request->project_id) {
|
||||
$query->where('project_id', $projectId);
|
||||
}
|
||||
|
||||
$cashAdvances = $query->latest()->paginate(20)->withQueryString();
|
||||
$projects = Project::select('id', 'ulid', 'name', 'code')->get();
|
||||
$projects = Project::whereIn('projects.id', $this->availableProjectIdsQuery())
|
||||
->select('id', 'ulid', 'name', 'code')->get();
|
||||
|
||||
return Inertia::render('FinancialManagement::CashAdvances/Index', [
|
||||
'cashAdvances' => $cashAdvances,
|
||||
@@ -271,20 +304,28 @@ class FinanceController extends Controller
|
||||
return back()->with('success', 'Cash advance request submitted successfully.');
|
||||
}
|
||||
|
||||
public function approveCashAdvance(\Modules\FinancialManagement\Models\CashAdvance $cashAdvance)
|
||||
public function approveCashAdvance(string $cashAdvance)
|
||||
{
|
||||
$user = auth()->user();
|
||||
$cashAdvance = \Modules\FinancialManagement\Models\CashAdvance::withoutGlobalScopes()
|
||||
->where('ulid', $cashAdvance)
|
||||
->firstOrFail();
|
||||
|
||||
if ($cashAdvance->requested_by === $user->id && $user->user_type !== 'admin' && !$user->hasRole('Super Admin')) {
|
||||
return back()->with('error', 'You cannot approve your own cash advance request.');
|
||||
}
|
||||
|
||||
$isApprover = $user->user_type === 'admin' ||
|
||||
$user->roles()->whereIn('name', ['Super Admin', 'admin', 'Project Manager', 'Main Contractor Admin'])->exists();
|
||||
$isApprover = $this->canApproveCashAdvance($user);
|
||||
|
||||
if (! $isApprover) {
|
||||
return back()->with('error', 'Unauthorized to approve cash advance requests.');
|
||||
}
|
||||
|
||||
if (! $this->isPlatformUser($user)
|
||||
&& ! $this->availableProjectIdsQuery()->where('projects.id', $cashAdvance->project_id)->exists()) {
|
||||
return back()->with('error', 'You cannot approve a cash advance for an unrelated project.');
|
||||
}
|
||||
|
||||
$cashAdvance->update([
|
||||
'status' => 'approved',
|
||||
'approved_by' => auth()->id(),
|
||||
@@ -292,4 +333,27 @@ class FinanceController extends Controller
|
||||
|
||||
return back()->with('success', 'Cash advance request approved.');
|
||||
}
|
||||
|
||||
private function canApproveCashAdvance(User $user): bool
|
||||
{
|
||||
return $user->user_type === 'admin'
|
||||
|| $user->hasAnyRole(['Super Admin', 'admin', 'Project Manager', 'Main Contractor Admin']);
|
||||
}
|
||||
|
||||
private function availableProjectIdsQuery()
|
||||
{
|
||||
return Project::query()->select('projects.id');
|
||||
}
|
||||
|
||||
private function isPlatformUser(User $user): bool
|
||||
{
|
||||
return $user->user_type === 'admin' || $user->hasAnyRole(['Super Admin', 'admin']);
|
||||
}
|
||||
|
||||
private function canAccessProject(User $user, ?int $projectId): bool
|
||||
{
|
||||
return $projectId !== null
|
||||
&& ($this->isPlatformUser($user)
|
||||
|| $this->availableProjectIdsQuery()->where('projects.id', $projectId)->exists());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user