diff --git a/app/Http/Controllers/BranchController.php b/app/Http/Controllers/BranchController.php index 8d00039..c3ee064 100644 --- a/app/Http/Controllers/BranchController.php +++ b/app/Http/Controllers/BranchController.php @@ -9,9 +9,27 @@ class BranchController extends Controller { - public function index() + public function index(Request $request) { - return response()->json(Branch::with('documents')->get()); + $query = Branch::with('documents'); + if ($request->has('status')) { + $query->where('status', $request->status); + } + $branches = $query->get(); + + // Attach is_deletable flag to each branch + $branches->each(function ($branch) { + $inUse = \App\Models\Staff::where('branch_id', $branch->id)->exists() + || \App\Models\Product::where('branch_id', $branch->id)->exists() + || \App\Models\Expense::where('branch_id', $branch->id)->exists() + || \App\Models\Collection::where('branch_id', $branch->id)->exists() + || \App\Models\Account::where('branch_id', $branch->id)->exists() + || \App\Models\ProductSale::where('branch_id', $branch->id)->exists() + || \App\Models\Receptionist::where('branch_id', $branch->id)->exists(); + $branch->is_deletable = !$inUse; + }); + + return response()->json($branches); } public function store(Request $request) @@ -89,17 +107,19 @@ public function update(Request $request, $id) 'status' => $validated['status'], ]); - if (isset($validated['new_docs'])) { - foreach ($validated['new_docs'] as $doc) { - $path = $doc['file']->store('branch_documents', 'public'); - BranchDocument::create([ - 'branch_id' => $branch->id, - 'name' => $doc['name'], - 'document_number' => $doc['document_number'] ?? null, - 'path' => $path, - 'expiry_date' => $doc['expiry_date'], - 'reminder_days' => $doc['reminder_days'] ?? 30 - ]); + if ($validated['status'] === 'Inactive') { + $staffAction = $request->input('staff_action'); + if ($staffAction === 'move') { + $targetBranchId = $request->input('move_to_branch_id'); + if ($targetBranchId) { + \App\Models\Staff::where('branch_id', $id) + ->where('status', 'Active') + ->update(['branch_id' => $targetBranchId]); + } + } elseif ($staffAction === 'inactivate') { + \App\Models\Staff::where('branch_id', $id) + ->where('status', 'Active') + ->update(['status' => 'Inactive']); } } @@ -114,9 +134,36 @@ public function show($id) public function destroy($id) { $branch = Branch::findOrFail($id); - // Documents will be auto-deleted via cascade constraint in migration + + // Check for dependencies + $dependencies = [ + 'Staff' => \App\Models\Staff::where('branch_id', $id)->exists(), + 'Products' => \App\Models\Product::where('branch_id', $id)->exists(), + 'Expenses' => \App\Models\Expense::where('branch_id', $id)->exists(), + 'Collections' => \App\Models\Collection::where('branch_id', $id)->exists(), + 'Accounts' => \App\Models\Account::where('branch_id', $id)->exists(), + 'Sales' => \App\Models\ProductSale::where('branch_id', $id)->exists(), + 'Receptionists' => \App\Models\Receptionist::where('branch_id', $id)->exists(), + ]; + + $usedIn = array_keys(array_filter($dependencies)); + + if (!empty($usedIn)) { + return response()->json([ + 'message' => 'Branch cannot be deleted because it is being used in: ' . implode(', ', $usedIn) + ], 422); + } + $branch->delete(); return response()->json(['message' => 'Branch deleted successfully']); } + + public function activeStaff($id) + { + $staff = \App\Models\Staff::where('branch_id', $id) + ->where('status', 'Active') + ->get(['id', 'full_name', 'role']); + return response()->json($staff); + } } diff --git a/app/Http/Controllers/InvestorController.php b/app/Http/Controllers/InvestorController.php index c02875d..80dc0f0 100644 --- a/app/Http/Controllers/InvestorController.php +++ b/app/Http/Controllers/InvestorController.php @@ -39,6 +39,9 @@ public function store(Request $request) 'branch_ids' => 'nullable|array', 'branch_ids.*' => 'exists:branches,id', 'security_proof_document' => 'nullable|file|mimes:pdf,png,jpg,jpeg|max:10240', + ], [ + 'security_proof_document.file' => 'The security proof document must be a file.', + 'security_proof_document.mimes' => 'The security proof document must be a PDF, PNG, or JPG.', ]); if ($request->hasFile('security_proof_document')) { @@ -103,8 +106,25 @@ public function update(Request $request, $id) 'branch_ids' => 'nullable|array', 'branch_ids.*' => 'exists:branches,id', 'security_proof_document' => 'nullable|file|mimes:pdf,png,jpg,jpeg|max:10240', + ], [ + 'security_proof_document.file' => 'The security proof document must be a file.', + 'security_proof_document.mimes' => 'The security proof document must be a PDF, PNG, or JPG.', ]); + // Check if payouts exist before allowing core financial changes + $hasPayouts = \App\Models\InvestorPayout::where('investor_id', $id)->exists(); + if ($hasPayouts) { + $coreFields = ['investment_date', 'investment_amount', 'roi_type', 'roi_value', 'roi_period']; + foreach ($coreFields as $field) { + if (isset($validated[$field]) && $validated[$field] != $investor->$field) { + return response()->json([ + 'message' => 'Cannot modify core investment terms after payouts have been processed.', + 'errors' => [$field => ['Modification restricted due to existing payouts.']] + ], 422); + } + } + } + if ($request->hasFile('security_proof_document')) { if ($investor->security_proof_document) { Storage::disk('public')->delete($investor->security_proof_document); diff --git a/app/Http/Controllers/MasterController.php b/app/Http/Controllers/MasterController.php index a8a9e25..7f51931 100644 --- a/app/Http/Controllers/MasterController.php +++ b/app/Http/Controllers/MasterController.php @@ -7,6 +7,7 @@ use App\Models\ExpenseCategory; use App\Models\ProductCategory; use App\Models\PaymentMethod; +use App\Models\StaffRole; use Illuminate\Support\Facades\Log; class MasterController extends Controller @@ -18,6 +19,7 @@ private function getModel($type) case 'expense': return new ExpenseCategory(); case 'product': return new ProductCategory(); case 'payment_method': return new PaymentMethod(); + case 'staff_role': return new StaffRole(); default: return null; } } diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php index 2c86483..6fc50c8 100644 --- a/app/Http/Controllers/ReportController.php +++ b/app/Http/Controllers/ReportController.php @@ -34,14 +34,13 @@ public function getProfitReport(Request $request) $query->where('date', '<=', $endDate); } - $totalCredits = $query->sum('credit'); + $totalCredits = (clone $query)->sum('credit'); $totalDebits = (clone $query)->sum('debit'); - // Note: We use Account table for both to ensure consistency with the "Total Received" and "Total Debited" requirement. - // If Expenses are also tracked in Accounts as debits (which they should be), this is correct. - // Fetch Transactions for the breakdown - $accounts = Account::select('date', 'credit as amount', 'type', 'description') - ->where('credit', '>', 0); + // Fetch All Ledger Transactions for the breakdown + $accounts = Account::where(function($q) { + $q->where('credit', '>', 0)->orWhere('debit', '>', 0); + }); if ($branchId) { $accounts->where('branch_id', $branchId); @@ -52,29 +51,28 @@ public function getProfitReport(Request $request) if ($endDate) { $accounts->where('date', '<=', $endDate); } - $accounts = $accounts->get() ->map(function($a) { $isAdjusted = false; - $originalAmount = $a->amount; + $originalAmount = $a->credit > 0 ? $a->credit : $a->debit; $remarks = ''; if ($a->accountable_type === \App\Models\ProductSale::class && $a->accountable) { $originalAmount = $a->accountable->subtotal_amount + $a->accountable->vat_amount; - $isAdjusted = abs($a->amount - $originalAmount) > 0.01; + $isAdjusted = abs($a->credit - $originalAmount) > 0.01; $remarks = $a->accountable->remarks; } elseif ($a->accountable_type === \App\Models\Collection::class && $a->accountable) { $originalAmount = $a->accountable->items()->sum('subtotal'); - $isAdjusted = $originalAmount > 0 && abs($a->amount - $originalAmount) > 0.01; + $isAdjusted = $originalAmount > 0 && abs($a->credit - $originalAmount) > 0.01; $remarks = $a->accountable->remarks; } return [ 'date' => $a->date, - 'type' => 'Income', + 'type' => $a->credit > 0 ? 'Income' : 'Expense', 'category' => $a->type, 'description' => $a->description, - 'amount' => $a->amount, + 'amount' => $a->credit > 0 ? $a->credit : $a->debit, 'branch' => 'N/A', 'is_adjusted' => $isAdjusted, 'original_amount' => $originalAmount, @@ -112,12 +110,37 @@ public function getProfitReport(Request $request) } $lowStockCount = $lowStockCount->whereRaw('current_stock <= reorder_level')->count(); + // Calculate 6-month trend for Dashboard table/chart + $trend = []; + for ($i = 5; $i >= 0; $i--) { + $monthStart = Carbon::now()->subMonths($i)->startOfMonth(); + $monthEnd = Carbon::now()->subMonths($i)->endOfMonth(); + + $monthIncome = Account::where('branch_id', $branchId ?: '!=', 0) + ->when($branchId, fn($q) => $q->where('branch_id', $branchId)) + ->whereBetween('date', [$monthStart->toDateString(), $monthEnd->toDateString()]) + ->sum('credit'); + + $monthExpense = Expense::when($branchId, fn($q) => $q->where('branch_id', $branchId)) + ->whereBetween('date', [$monthStart->toDateString(), $monthEnd->toDateString()]) + ->sum('amount'); + + $trend[] = [ + 'month' => $monthStart->format('M'), + 'income' => round($monthIncome, 2), + 'expense' => round($monthExpense, 2), + 'profit' => round($monthIncome - $monthExpense, 2), + 'status' => ($monthIncome - $monthExpense) >= 0 ? 'Profit' : 'Loss' + ]; + } + return response()->json([ 'total_income' => $totalCredits, 'total_expense' => $totalDebits, 'net_profit' => $totalCredits - $totalDebits, 'low_stock_count' => $lowStockCount, - 'transactions' => $transactions + 'transactions' => $transactions, + 'trend' => $trend ]); } diff --git a/app/Http/Controllers/StaffController.php b/app/Http/Controllers/StaffController.php index 5dc5469..296dee8 100644 --- a/app/Http/Controllers/StaffController.php +++ b/app/Http/Controllers/StaffController.php @@ -10,6 +10,7 @@ use Carbon\Carbon; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\DB; class StaffController extends Controller { @@ -95,48 +96,56 @@ public function store(Request $request) $status = $validated['status'] ?? 'Active'; $validated['status'] = $status; - $staff = Staff::create($validated); + return DB::transaction(function() use ($validated, $request) { + $staff = Staff::create($validated); - // Handle Salary Advance Deductions - if ($staff->advance_enabled) { - if ($staff->advance_repayment_mode === 'Divide by Months') { - $months = $staff->advance_months ?: 1; - $monthlyDeduction = $staff->advance_amount / $months; - - SalaryAdvanceDeduction::create([ - 'staff_id' => $staff->id, - 'advance_amount' => $staff->advance_amount, - 'total_months' => $months, - 'monthly_deduction' => $monthlyDeduction, - 'remaining_amount' => $staff->advance_amount, - 'paid_amount' => 0, - 'status' => 'Pending' - ]); - } - - // Record immediate account debit and expense for the advance - $this->recordFinancialsForAdvance($staff, $staff->advance_amount); - } - - // Handle Documents - if ($request->has('documents')) { - foreach ($request->input('documents') as $index => $doc) { - $path = null; - if ($request->hasFile("documents.{$index}.file")) { - $path = $request->file("documents.{$index}.file")->store('staff_documents', 'public'); + // Handle Salary Advance Deductions + if ($staff->advance_enabled) { + if ($staff->advance_repayment_mode === 'Divide by Months') { + $months = $staff->advance_months ?: 1; + $monthlyDeduction = $staff->advance_amount / $months; + + SalaryAdvanceDeduction::create([ + 'staff_id' => $staff->id, + 'advance_amount' => $staff->advance_amount, + 'total_months' => $months, + 'monthly_deduction' => $monthlyDeduction, + 'remaining_amount' => $staff->advance_amount, + 'paid_amount' => 0, + 'status' => 'Pending' + ]); } - $staff->documents()->create([ - 'name' => $doc['name'], - 'document_number' => $doc['document_number'] ?? null, - 'expiry_date' => $doc['expiry_date'] ?? null, - 'reminder_days' => $doc['reminder_days'] ?? 30, - 'path' => $path, - ]); + // Record immediate account debit and expense for the advance + $this->recordFinancialsForAdvance($staff, $staff->advance_amount); } - } - return response()->json(['message' => 'Staff created successfully', 'staff' => $staff], 201); + // Handle Documents + if ($request->has('documents')) { + foreach ($request->input('documents') as $index => $doc) { + if (empty($doc['name']) && empty($doc['document_number'])) continue; + + $path = null; + if ($request->hasFile("documents.{$index}.file")) { + $path = $request->file("documents.{$index}.file")->store('staff_documents', 'public'); + } + + if (!$path) { + throw new \Exception("Document file is missing for '{$doc['name']}'"); + } + + $staff->documents()->create([ + 'name' => $doc['name'], + 'document_number' => $doc['document_number'] ?? null, + 'expiry_date' => $doc['expiry_date'] ?? null, + 'reminder_days' => $doc['reminder_days'] ?? 30, + 'path' => $path, + ]); + } + } + + return response()->json(['message' => 'Staff created successfully', 'staff' => $staff], 201); + }); } public function update(Request $request, $id) @@ -191,114 +200,121 @@ public function update(Request $request, $id) $status = $validated['status'] ?? 'Active'; $validated['status'] = $status; - $staff->update($validated); + return DB::transaction(function() use ($staff, $validated, $request) { + $staff->update($validated); - // Handle Documents Update - if ($request->has('documents')) { - $existingDocIds = []; - foreach ($request->input('documents') as $index => $doc) { - $path = null; - if ($request->hasFile("documents.{$index}.file")) { - $path = $request->file("documents.{$index}.file")->store('staff_documents', 'public'); - } + // Handle Documents Update + if ($request->has('documents')) { + $existingDocIds = []; + foreach ($request->input('documents') as $index => $doc) { + if (empty($doc['name']) && empty($doc['document_number']) && !$request->hasFile("documents.{$index}.file")) continue; - if (isset($doc['id'])) { - $staffDoc = \App\Models\StaffDocument::find($doc['id']); - if ($staffDoc && $staffDoc->staff_id == $staff->id) { - $updateData = [ + $path = null; + if ($request->hasFile("documents.{$index}.file")) { + $path = $request->file("documents.{$index}.file")->store('staff_documents', 'public'); + } + + if (isset($doc['id'])) { + $staffDoc = \App\Models\StaffDocument::find($doc['id']); + if ($staffDoc && $staffDoc->staff_id == $staff->id) { + $updateData = [ + 'name' => $doc['name'], + 'document_number' => $doc['document_number'] ?? null, + 'expiry_date' => $doc['expiry_date'] ?? null, + 'reminder_days' => $doc['reminder_days'] ?? 30, + ]; + if ($path) $updateData['path'] = $path; + $staffDoc->update($updateData); + $existingDocIds[] = $staffDoc->id; + } + } else { + if (!$path) { + throw new \Exception("Document file is missing for '{$doc['name']}'"); + } + $newDoc = $staff->documents()->create([ 'name' => $doc['name'], 'document_number' => $doc['document_number'] ?? null, 'expiry_date' => $doc['expiry_date'] ?? null, 'reminder_days' => $doc['reminder_days'] ?? 30, - ]; - if ($path) $updateData['path'] = $path; - $staffDoc->update($updateData); - $existingDocIds[] = $staffDoc->id; + 'path' => $path, + ]); + $existingDocIds[] = $newDoc->id; } - } else { - $newDoc = $staff->documents()->create([ - 'name' => $doc['name'], - 'document_number' => $doc['document_number'] ?? null, - 'expiry_date' => $doc['expiry_date'] ?? null, - 'reminder_days' => $doc['reminder_days'] ?? 30, - 'path' => $path, - ]); - $existingDocIds[] = $newDoc->id; } + // Delete removed documents + $staff->documents()->whereNotIn('id', $existingDocIds)->get()->each(function($doc) { + if ($doc->path) Storage::disk('public')->delete($doc->path); + $doc->delete(); + }); } - // Delete removed documents - $staff->documents()->whereNotIn('id', $existingDocIds)->get()->each(function($doc) { - if ($doc->path) Storage::disk('public')->delete($doc->path); - $doc->delete(); - }); - } - // Handle Salary Advance Deductions - if ($staff->advance_enabled) { - // Only record financials if advance amount has increased - if ($request->has('advance_amount')) { - $oldAdvance = $staff->getOriginal('advance_amount') ?: 0; - $newAdvance = $staff->advance_amount; + // Handle Salary Advance Deductions + if ($staff->advance_enabled) { + // Only record financials if advance amount has increased + if ($request->has('advance_amount')) { + $oldAdvance = $staff->getOriginal('advance_amount') ?: 0; + $newAdvance = $staff->advance_amount; - if ($newAdvance > $oldAdvance) { - $additionalAmount = $newAdvance - $oldAdvance; - $this->recordFinancialsForAdvance($staff, $additionalAmount); + if ($newAdvance > $oldAdvance) { + $additionalAmount = $newAdvance - $oldAdvance; + $this->recordFinancialsForAdvance($staff, $additionalAmount); + } + } + + if ($staff->advance_repayment_mode === 'Divide by Months') { + $existing = SalaryAdvanceDeduction::where('staff_id', $staff->id)->where('status', 'Pending')->first(); + $months = $staff->advance_months ?: 1; + $monthlyDeduction = $staff->advance_amount / $months; + + if ($existing) { + $existing->update([ + 'advance_amount' => $staff->advance_amount, + 'total_months' => $months, + 'monthly_deduction' => $monthlyDeduction, + 'remaining_amount' => $staff->advance_amount, + ]); + } else { + SalaryAdvanceDeduction::create([ + 'staff_id' => $staff->id, + 'advance_amount' => $staff->advance_amount, + 'total_months' => $months, + 'monthly_deduction' => $monthlyDeduction, + 'remaining_amount' => $staff->advance_amount, + 'paid_amount' => 0, + 'status' => 'Pending' + ]); + } + } + } else { + // If advance was enabled but now disabled, mark active deduction as Closed + if ($staff->getOriginal('advance_enabled')) { + SalaryAdvanceDeduction::where('staff_id', $staff->id) + ->where('status', 'Pending') + ->update(['status' => 'Closed']); } } - if ($staff->advance_repayment_mode === 'Divide by Months') { - $existing = SalaryAdvanceDeduction::where('staff_id', $staff->id)->where('status', 'Pending')->first(); - $months = $staff->advance_months ?: 1; - $monthlyDeduction = $staff->advance_amount / $months; + // Handle Trainer Commission History + if ($staff->commission_enabled && $request->has('commission_member_count')) { + $effectiveMonth = $request->input('apply_from') === 'next_month' + ? Carbon::now()->addMonth()->format('Y-m') + : Carbon::now()->format('Y-m'); - if ($existing) { - $existing->update([ - 'advance_amount' => $staff->advance_amount, - 'total_months' => $months, - 'monthly_deduction' => $monthlyDeduction, - 'remaining_amount' => $staff->advance_amount, - ]); - } else { - SalaryAdvanceDeduction::create([ + TrainerCommission::updateOrCreate( + [ 'staff_id' => $staff->id, - 'advance_amount' => $staff->advance_amount, - 'total_months' => $months, - 'monthly_deduction' => $monthlyDeduction, - 'remaining_amount' => $staff->advance_amount, - 'paid_amount' => 0, - 'status' => 'Pending' - ]); - } + 'effective_month' => $effectiveMonth + ], + [ + 'member_count' => $request->input('commission_member_count'), + 'amount_per_head' => $request->input('commission_amount'), + 'total_amount' => $request->input('commission_member_count') * $request->input('commission_amount') + ] + ); } - } else { - // If advance was enabled but now disabled, mark active deduction as Closed - if ($staff->getOriginal('advance_enabled')) { - SalaryAdvanceDeduction::where('staff_id', $staff->id) - ->where('status', 'Pending') - ->update(['status' => 'Closed']); - } - } - // Handle Trainer Commission History - if ($staff->commission_enabled && $request->has('commission_member_count')) { - $effectiveMonth = $request->input('apply_from') === 'next_month' - ? Carbon::now()->addMonth()->format('Y-m') - : Carbon::now()->format('Y-m'); - - TrainerCommission::updateOrCreate( - [ - 'staff_id' => $staff->id, - 'effective_month' => $effectiveMonth - ], - [ - 'member_count' => $request->input('commission_member_count'), - 'amount_per_head' => $request->input('commission_amount'), - 'total_amount' => $request->input('commission_member_count') * $request->input('commission_amount') - ] - ); - } - - return response()->json(['message' => 'Staff updated successfully', 'staff' => $staff]); + return response()->json(['message' => 'Staff updated successfully', 'staff' => $staff]); + }); } public function destroy($id) diff --git a/app/Models/StaffRole.php b/app/Models/StaffRole.php new file mode 100644 index 0000000..3f9de28 --- /dev/null +++ b/app/Models/StaffRole.php @@ -0,0 +1,10 @@ +withMiddleware(function (Middleware $middleware): void { - // + $middleware->trustProxies(at: '*'); + $middleware->validateCsrfTokens(except: [ + 'api/*', + 'login', + 'logout', + 'receptionist/login', + ]); }) ->withExceptions(function (Exceptions $exceptions): void { // diff --git a/database/migrations/2026_03_12_000001_create_staff_roles_table.php b/database/migrations/2026_03_12_000001_create_staff_roles_table.php new file mode 100644 index 0000000..8d2b65f --- /dev/null +++ b/database/migrations/2026_03_12_000001_create_staff_roles_table.php @@ -0,0 +1,33 @@ +id(); + $table->string('name'); + $table->string('status')->default('Active'); + $table->timestamps(); + }); + + // Seed default roles + DB::table('staff_roles')->insert([ + ['name' => 'Trainer', 'status' => 'Active', 'created_at' => now(), 'updated_at' => now()], + ['name' => 'Receptionist', 'status' => 'Active', 'created_at' => now(), 'updated_at' => now()], + ['name' => 'Manager', 'status' => 'Active', 'created_at' => now(), 'updated_at' => now()], + ['name' => 'Cleaner', 'status' => 'Active', 'created_at' => now(), 'updated_at' => now()], + ['name' => 'Security', 'status' => 'Active', 'created_at' => now(), 'updated_at' => now()], + ]); + } + + public function down(): void + { + Schema::dropIfExists('staff_roles'); + } +}; diff --git a/mobile_api_documentation.txt b/mobile_api_documentation.txt new file mode 100644 index 0000000..a06a418 --- /dev/null +++ b/mobile_api_documentation.txt @@ -0,0 +1,133 @@ +# Flutter Mobile App API Documentation (Owner Role) + +Base URL: http://127.0.0.1:8000/api +Note: For testing on a real physical mobile device, replace 127.0.0.1 with your computer's local IP address (e.g., 192.168.1.5). + +## Authentication +- POST /login + - Params: email, password + - Returns: CSRF cookie and session (for web-based auth) or Auth token (if configured). + +- POST /logout + - Action: End session + +- GET /profile + - Returns: Current logged-in user details and role. + +## Branch Management +- GET /branches + - List all branches. +- POST /branches + - Create new branch (Multipart/form-data for documents). +- GET /branches/{id} + - View specific branch details. +- PUT /branches/{id} + - Update branch details. +- DELETE /branches/{id} + - Delete a branch. + +- GET /branches/{branch}/receptionist + - View receptionist for a branch. +- POST /branches/{branch}/receptionist + - Create/Update receptionist credentials. +- DELETE /branches/{branch}/receptionist + - Remove receptionist. + +## Staff Management +- GET /staff + - List all staff members. +- POST /staff + - Add new staff (Multipart/form-data for documents). +- GET /staff/{id} + - View staff profile. +- PUT /staff/{id} + - Update staff profile. +- DELETE /staff/{id} + - Delete staff member. + +- GET /staff/pending-salaries + - List all pending salaries across branches. +- POST /staff/bulk-settle + - Params: staff_ids[] + - Settle multiple salaries at once. +- GET /staff/{id}/payments + - Salary payment history. +- GET /staff/{id}/payroll-status + - Current month's payroll calculation. +- POST /staff/{id}/settle + - Settle individual salary for a month. +- GET /staff/{id}/advance-history + - List of advance payments and deductions. + +## Investor & ROI Management +- GET /investors + - List all investors. +- POST /investors + - Add new investor (Multipart/form-data for documents). +- GET /investors/{id} + - View investor details. +- PUT /investors/{id} + - Update investor. +- DELETE /investors/{id} + - Delete investor. + +- GET /investors/pending-roi + - List all pending ROI settlements. +- GET /investors/{id}/roi-status + - Monthly ROI status breakdown (Base ROI, Carry Over, Paid, Net Due). +- POST /investors/{id}/settle-roi + - Params: payout_month, amount, payout_date, payment_method, remarks. + - Settle a month's ROI. + +## Financials & Expenses +- GET /accounts + - General ledger of all credit/debit transactions. +- GET /expenses + - List of all business expenses. +- POST /expenses + - Params: date, branch_id, expense_category_id, expense_type (Account/Petty Cash), amount, remarks. + - Record a new expense. +- GET /expense-categories + - List of master expense categories. + +## Inventory Management +- GET /inventory/products + - List all products and stock levels. +- POST /inventory/products + - Add new product with image. +- POST /inventory/products/{id}/adjust + - Adjuts stock (Add/Remove) with remarks. +- GET /inventory/products/{id}/history + - Stock movement history for a specific product. +- GET /inventory/sales + - List of all POS sales. +- POST /inventory/sales + - Record a new product sale. +- GET /inventory/movements + - Global stock movement log. + +## Collections +- GET /collections + - List all daily collections. +- POST /collections + - Record new collection. +- GET /collections/{id} + - View collection details. + +## Reports +- GET /reports/profit + - Profit & Loss report (Income vs Expenses). +- GET /reports/expiry-reminders + - Documents (Trade license, staff IDs, etc.) expiring soon. +- GET /reports/investments + - Summary of total investments and ROI distributed. + +## Master Settings +- GET /masters/{type} + - Types: expense_categories, product_categories, payment_methods, etc. +- POST /masters/{type} + - Add master entry. +- PUT /masters/{type}/{id} + - Edit master entry. +- DELETE /masters/{type}/{id} + - Delete master entry. diff --git a/resources/js/Pages/Owner/Branches/Components/AddBranchModal.jsx b/resources/js/Pages/Owner/Branches/Components/AddBranchModal.jsx index ab45c95..66553bd 100644 --- a/resources/js/Pages/Owner/Branches/Components/AddBranchModal.jsx +++ b/resources/js/Pages/Owner/Branches/Components/AddBranchModal.jsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; import { X, Upload, Calendar, Plus, Trash2 } from 'lucide-react'; +import Toast from '../../Components/Toast'; export default function AddBranchModal({ isOpen, onClose, onRefresh }) { const [loading, setLoading] = useState(false); @@ -12,6 +13,7 @@ export default function AddBranchModal({ isOpen, onClose, onRefresh }) { payroll_to_day: 28, salary_generation_day: 2, }); + const [toast, setToast] = useState(null); const [docs, setDocs] = useState([ { name: '', file: null, document_number: '', expiry_date: '', reminder_days: 30 }, @@ -40,6 +42,14 @@ export default function AddBranchModal({ isOpen, onClose, onRefresh }) { const handleSubmit = async (e) => { e.preventDefault(); + + // Ensure at least one document is uploaded + const hasFile = docs.some(doc => doc.file); + if (!hasFile) { + setToast({ message: 'Please upload at least one document for the branch.', type: 'error' }); + return; + } + setLoading(true); const data = new FormData(); @@ -71,14 +81,17 @@ export default function AddBranchModal({ isOpen, onClose, onRefresh }) { }); if (res.ok) { - onRefresh(); - onClose(); + setToast({ message: 'Branch added successfully!', type: 'success' }); + setTimeout(() => { + onRefresh(); + onClose(); + }, 1500); } else { const err = await res.json(); - alert(err.message || 'Error creating branch'); + setToast({ message: err.message || 'Error creating branch', type: 'error' }); } } catch (error) { - alert('An error occurred. Please try again.'); + setToast({ message: 'An error occurred. Please try again.', type: 'error' }); } finally { setLoading(false); } @@ -88,6 +101,7 @@ export default function AddBranchModal({ isOpen, onClose, onRefresh }) { return (
+ This branch has active staff members. Please choose what to do with them before inactivating the branch. +
+ +Detailed breakdown of income and expenses.
+| Date | +Type | +Category / Description | +Amount | +
|---|---|---|---|
| No transactions found for the selected period. | +|||
|
+
+ {new Date(row.date).toLocaleDateString()}
+ {new Date(row.date).toLocaleDateString(undefined, { weekday: 'short' })}
+
+ |
+
+
+ {row.type === 'Income' ? |
+
+
+ {row.category}
+ {row.description || 'No description provided'}
+
+ |
+ + + {row.type === 'Income' ? '+' : '-'}{formatCurrency(row.amount)} + + | +