From e1e55959e1796420980436432f544f2accd38c44 Mon Sep 17 00:00:00 2001 From: ashok Date: Fri, 13 Mar 2026 10:08:46 +0530 Subject: [PATCH] bug fix day 1 --- app/Http/Controllers/BranchController.php | 75 ++++- app/Http/Controllers/InvestorController.php | 20 ++ app/Http/Controllers/MasterController.php | 2 + app/Http/Controllers/ReportController.php | 49 +++- app/Http/Controllers/StaffController.php | 270 ++++++++++-------- app/Models/StaffRole.php | 10 + app/Providers/AppServiceProvider.php | 4 +- bootstrap/app.php | 8 +- ..._03_12_000001_create_staff_roles_table.php | 33 +++ mobile_api_documentation.txt | 133 +++++++++ .../Branches/Components/AddBranchModal.jsx | 23 +- .../Branches/Components/EditBranchModal.jsx | 97 +++++++ resources/js/Pages/Owner/Branches/List.jsx | 31 +- .../js/Pages/Owner/Collections/Index.jsx | 2 +- .../Pages/Owner/Components/AccountsTable.jsx | 73 +++++ .../js/Pages/Owner/Components/Header.jsx | 29 +- .../js/Pages/Owner/Components/ProfitTable.jsx | 32 ++- resources/js/Pages/Owner/Dashboard.jsx | 72 +++-- .../Pages/Owner/Inventory/AddProductModal.jsx | 27 +- resources/js/Pages/Owner/Inventory/Index.jsx | 23 +- resources/js/Pages/Owner/Investors/Add.jsx | 29 +- resources/js/Pages/Owner/Investors/Edit.jsx | 77 ++++- resources/js/Pages/Owner/Investors/List.jsx | 3 +- .../js/Pages/Owner/Masters/MasterTable.jsx | 11 +- resources/js/Pages/Owner/Masters/index.jsx | 5 +- resources/js/Pages/Owner/Reports/Index.jsx | 209 ++++++++++++-- resources/js/Pages/Owner/Staff/Add.jsx | 32 ++- resources/js/Pages/Owner/Staff/Edit.jsx | 51 +++- resources/js/Pages/Owner/Staff/List.jsx | 2 +- resources/js/Pages/Receptionist/POS.jsx | 38 ++- routes/web.php | 1 + 31 files changed, 1142 insertions(+), 329 deletions(-) create mode 100644 app/Models/StaffRole.php create mode 100644 database/migrations/2026_03_12_000001_create_staff_roles_table.php create mode 100644 mobile_api_documentation.txt create mode 100644 resources/js/Pages/Owner/Components/AccountsTable.jsx 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 (
+ {toast && setToast(null)} />}
{/* Header */}
@@ -245,7 +259,6 @@ export default function AddBranchModal({ isOpen, onClose, onRefresh }) { className="hidden" id={`file-${index}`} onChange={(e) => handleDocChange(index, 'file', e.target.files[0])} - required={!doc.file} />
+ + {formData.status === 'Inactive' && (loadingStaff ? ( +
Checking for active staff...
+ ) : activeStaff.length > 0 && ( +
+
+ +

Active Staff Detected ({activeStaff.length})

+
+

+ This branch has active staff members. Please choose what to do with them before inactivating the branch. +

+ +
+
+ + +
+ + {staffAction === 'move' && ( + + )} +
+ +
+ {activeStaff.map(s => ( +
+ {s.full_name} + {s.role} +
+ ))} +
+
+ ))}
diff --git a/resources/js/Pages/Owner/Branches/List.jsx b/resources/js/Pages/Owner/Branches/List.jsx index f4e4ed5..4c153a3 100644 --- a/resources/js/Pages/Owner/Branches/List.jsx +++ b/resources/js/Pages/Owner/Branches/List.jsx @@ -4,7 +4,7 @@ import EditBranchModal from './Components/EditBranchModal'; import DeleteConfirmationModal from './Components/DeleteConfirmationModal'; import DataTable from '../../../Components/DataTable'; import StatusPill from '../../../Components/StatusPill'; -import { Plus, MapPin, Search, Eye, Box, Edit2, Trash2, MoreVertical } from 'lucide-react'; +import { Plus, MapPin, Search, Eye, Edit2, Trash2, Lock } from 'lucide-react'; export default function List() { const [branches, setBranches] = useState([]); @@ -81,12 +81,6 @@ export default function List() { > - - + {row.is_deletable ? ( + + ) : ( + + + + )} ) } diff --git a/resources/js/Pages/Owner/Collections/Index.jsx b/resources/js/Pages/Owner/Collections/Index.jsx index 3c371b8..efbad49 100644 --- a/resources/js/Pages/Owner/Collections/Index.jsx +++ b/resources/js/Pages/Owner/Collections/Index.jsx @@ -42,7 +42,7 @@ export default function CollectionsIndex() { const fetchMetadata = async () => { try { const [bRes, tRes] = await Promise.all([ - fetch('/api/branches'), + fetch('/api/branches?status=Active'), fetch('/api/masters/collection') ]); if (bRes.ok) setBranches(await bRes.json()); diff --git a/resources/js/Pages/Owner/Components/AccountsTable.jsx b/resources/js/Pages/Owner/Components/AccountsTable.jsx new file mode 100644 index 0000000..c3b26d3 --- /dev/null +++ b/resources/js/Pages/Owner/Components/AccountsTable.jsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { MoreVertical, ArrowDownRight, ArrowUpRight } from 'lucide-react'; + +export default function AccountsTable({ data = [] }) { + const formatCurrency = (val) => { + return new Intl.NumberFormat('en-AE', { style: 'currency', currency: 'AED' }).format(val); + }; + + return ( +
+
+
+

Recent Transactions

+

Detailed breakdown of income and expenses.

+
+ +
+ +
+ + + + + + + + + + + {data.length === 0 && ( + + + + )} + {data.map((row, index) => ( + + + + + + + ))} + +
DateTypeCategory / DescriptionAmount
No transactions found for the selected period.
+
+ {new Date(row.date).toLocaleDateString()} + {new Date(row.date).toLocaleDateString(undefined, { weekday: 'short' })} +
+
+ + {row.type === 'Income' ? : } + {row.type} + + +
+ {row.category} + {row.description || 'No description provided'} +
+
+ + {row.type === 'Income' ? '+' : '-'}{formatCurrency(row.amount)} + +
+
+
+ ); +} diff --git a/resources/js/Pages/Owner/Components/Header.jsx b/resources/js/Pages/Owner/Components/Header.jsx index e2233d5..1b912b9 100644 --- a/resources/js/Pages/Owner/Components/Header.jsx +++ b/resources/js/Pages/Owner/Components/Header.jsx @@ -47,22 +47,8 @@ export default function Header({ profile }) {

GymPro

- {/* Search Bar */} -
-
-
- -
- -
-
- {/* User Actions */} -
+
- - -
-
diff --git a/resources/js/Pages/Owner/Investors/Edit.jsx b/resources/js/Pages/Owner/Investors/Edit.jsx index 7508bd8..1a12b1e 100644 --- a/resources/js/Pages/Owner/Investors/Edit.jsx +++ b/resources/js/Pages/Owner/Investors/Edit.jsx @@ -10,6 +10,7 @@ export default function InvestorEdit({ id }) { const [saving, setSaving] = useState(false); const [branches, setBranches] = useState([]); const [toast, setToast] = useState(null); + const [hasPayouts, setHasPayouts] = useState(false); const [formData, setFormData] = useState({ name: '', @@ -29,7 +30,7 @@ export default function InvestorEdit({ id }) { useEffect(() => { const fetchData = async () => { try { - const bRes = await fetch('/api/branches'); + const bRes = await fetch('/api/branches?status=Active'); const bData = await bRes.json(); setBranches(bData); @@ -44,6 +45,10 @@ export default function InvestorEdit({ id }) { existing_document: iData.security_proof_document, security_proof_document: null }); + + const pRes = await fetch(`/api/investors/${id}/payouts`); + const pData = await pRes.json(); + setHasPayouts(pData.length > 0); } catch (error) { console.error('Error fetching data:', error); } finally { @@ -143,22 +148,61 @@ export default function InvestorEdit({ id }) {

Edit Investor

+ {hasPayouts && ( +
+
+ +
+
+

Financial Terms Locked

+

Core investment terms (Amount, Date, ROI) cannot be modified because payments have already been processed for this investor to maintain accounting integrity.

+
+
+ )} +
- + { + if (e.target.value.length <= 30) handleChange(e); + else setToast({ message: 'Maximum 30 characters allowed', type: 'error' }); + }} + className="w-full border border-gray-200 rounded-xl px-4 py-3.5 text-sm focus:ring-2 focus:ring-emerald-500 transition-all outline-none" + />
- +
- +
@@ -211,7 +255,13 @@ export default function InvestorEdit({ id }) {
- @@ -221,12 +271,25 @@ export default function InvestorEdit({ id }) { - +
- diff --git a/resources/js/Pages/Owner/Investors/List.jsx b/resources/js/Pages/Owner/Investors/List.jsx index 71b0bad..25efa50 100644 --- a/resources/js/Pages/Owner/Investors/List.jsx +++ b/resources/js/Pages/Owner/Investors/List.jsx @@ -168,7 +168,7 @@ export default function InvestorList() {
diff --git a/resources/js/Pages/Owner/Masters/index.jsx b/resources/js/Pages/Owner/Masters/index.jsx index e6a85b7..d381678 100644 --- a/resources/js/Pages/Owner/Masters/index.jsx +++ b/resources/js/Pages/Owner/Masters/index.jsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; import Toast from '../Components/Toast'; import MasterTable from './MasterTable'; -import { Tag, Briefcase, Box, Plus, CreditCard } from 'lucide-react'; +import { Tag, Briefcase, Box, Plus, CreditCard, Users } from 'lucide-react'; export default function MasterManagement() { const [activeTab, setActiveTab] = useState('collection'); @@ -11,7 +11,8 @@ export default function MasterManagement() { { id: 'collection', label: 'Collection Types', icon: Tag, desc: 'Manage categories for payment collection.', btnLabel: 'Type' }, { id: 'expense', label: 'Expense Categories', icon: Briefcase, desc: 'Organize various types of business expenses.', btnLabel: 'Category' }, { id: 'product', label: 'Product Categories', icon: Box, desc: 'Group products into searchable categories.', btnLabel: 'Category' }, - { id: 'payment_method', label: 'Payment Methods', icon: CreditCard, desc: 'Manage available payment methods.', btnLabel: 'Method' } + { id: 'payment_method', label: 'Payment Methods', icon: CreditCard, desc: 'Manage available payment methods.', btnLabel: 'Method' }, + { id: 'staff_role', label: 'Staff Roles', icon: Users, desc: 'Define staff roles for the staff management module.', btnLabel: 'Role' } ]; const currentTab = tabs.find(t => t.id === activeTab); diff --git a/resources/js/Pages/Owner/Reports/Index.jsx b/resources/js/Pages/Owner/Reports/Index.jsx index 8ec8eca..3f4d0ce 100644 --- a/resources/js/Pages/Owner/Reports/Index.jsx +++ b/resources/js/Pages/Owner/Reports/Index.jsx @@ -217,6 +217,122 @@ export default function ReportIndex() { } }; + const exportToExcel = (data, fileName) => { + if (!data || data.length === 0) { + alert('No data to export'); + return; + } + + const headers = Object.keys(data[0]); + const csvRows = []; + csvRows.push('\ufeff' + headers.join(',')); // Added BOM for Excel UTF-8 support + + for (const row of data) { + const values = headers.map(header => { + let cell = row[header] === null || row[header] === undefined ? '' : row[header]; + cell = typeof cell === 'string' ? `"${cell.replace(/"/g, '""')}"` : cell; + return cell; + }); + csvRows.push(values.join(',')); + } + + const csvString = csvRows.join('\n'); + const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8;' }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.setAttribute('hidden', ''); + a.setAttribute('href', url); + a.setAttribute('download', `${fileName}.csv`); + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + }; + + const exportTabContent = () => { + let dataToExport = []; + let name = activeTab.toLowerCase().replace(/ /g, '_'); + + if (activeTab === 'Profit Report') { + dataToExport = (profitData?.transactions || []).map(t => ({ + Date: new Date(t.date).toLocaleDateString(), + Type: t.type, + Category: t.category, + Description: t.description, + Branch: t.branch, + Amount: t.amount + })); + } else if (activeTab === 'Expense Report') { + dataToExport = expenseData.map(e => ({ + Date: new Date(e.date).toLocaleDateString(), + Category: e.category?.name || 'N/A', + Branch: e.branch?.name || 'N/A', + Amount: e.amount + })); + } else if (activeTab === 'Collection Report') { + dataToExport = collectionData.map(c => ({ + Date: new Date(c.date).toLocaleDateString(), + Type: c.type?.name || 'N/A', + Branch: c.branch?.name || 'N/A', + Amount: c.amount + })); + } else if (activeTab === 'Low Stock Report') { + dataToExport = lowStockData.map(p => ({ + Product: p.name, + Branch: p.branch?.name || 'N/A', + Current_Stock: p.current_stock, + Reorder_Level: p.reorder_level + })); + } else if (activeTab === 'Inventory Report') { + dataToExport = movementsData.map(m => ({ + Date: new Date(m.date).toLocaleDateString(), + Product: m.product_name, + Branch: m.branch, + Reason: m.reason, + Change: m.change, + Stock_After: m.new_stock + })); + } else if (activeTab === 'Product Sales') { + dataToExport = salesData.map(s => ({ + Date: new Date(s.date).toLocaleDateString(), + Transaction_ID: s.transaction_id, + Branch: s.branch?.name || 'N/A', + Method: s.payment_method?.name || 'N/A', + SubTotal: s.sub_total, + VAT: s.vat_amount, + Total: s.total_amount + })); + } else if (activeTab === 'Investment Report') { + dataToExport = investmentData.map(inv => ({ + Investor: inv.investor_name, + Date: new Date(inv.investment_date).toLocaleDateString(), + ROI_Plan: inv.roi_type === 'Percentage' ? `${inv.roi_value}% ${inv.roi_period}` : `${inv.roi_value} AED ${inv.roi_period}`, + Invested_Amount: inv.investment_amount, + Returns_Earned: inv.returns_earned, + Total_Pending: inv.total_pending + })); + } else if (activeTab === 'Salary Report') { + dataToExport = salaryData.map(s => ({ + Date: new Date(s.date).toLocaleDateString(), + Type: s.is_bulk ? 'Bulk Release' : 'Individual Release', + Staff_Count: s.count, + Branch: s.branch, + Remarks: s.remarks, + Amount: s.amount + })); + } else if (activeTab === 'Expiry Reminders') { + dataToExport = expiryReminders.map(r => ({ + Staff: r.entity_name, + Branch: r.branch_name, + Document: r.document_name, + Doc_Number: r.document_number || '---', + Expiry_Date: new Date(r.expiry_date).toLocaleDateString(), + Status: r.days_left <= 0 ? 'Expired' : 'Expiring Soon' + })); + } + + exportToExcel(dataToExport, name); + }; + const comingSoon = (tabName) => (
@@ -253,24 +369,28 @@ export default function ReportIndex() {
)} -
- - setStartDate(e.target.value)} - /> -
-
- - setEndDate(e.target.value)} - /> -
+ {activeTab !== 'Expiry Reminders' && ( + <> +
+ + setStartDate(e.target.value)} + /> +
+
+ + setEndDate(e.target.value)} + /> +
+ + )}
@@ -304,7 +424,7 @@ export default function ReportIndex() {

Total Income

-

{profitData?.total_credited?.toLocaleString() || '0.00'} AED

+

{profitData?.total_income?.toLocaleString() || '0.00'} AED

{profitData?.transactions?.filter(t => t.type === 'Income').length || 0} Records

@@ -336,7 +456,10 @@ export default function ReportIndex() {

Detailed Breakdown

- @@ -447,7 +570,10 @@ export default function ReportIndex() {

Detailed Breakdown

- @@ -536,6 +662,13 @@ export default function ReportIndex() {

Expense Records

+
@@ -582,6 +715,13 @@ export default function ReportIndex() {

Collection Records

+
@@ -645,6 +785,13 @@ export default function ReportIndex() {

Low Stock Inventory

+
@@ -700,7 +847,10 @@ export default function ReportIndex() {

Inventory Alerts & Movements

Global audit log of all stock adjustments and alerts.

- @@ -773,7 +923,10 @@ export default function ReportIndex() {

Sales Records

- @@ -855,7 +1008,10 @@ export default function ReportIndex() {

Investment Performance

Overview of investor contributions, returns and pending payouts.

- @@ -923,7 +1079,10 @@ export default function ReportIndex() {

Salary Release History

Overview of all individual and bulk salary payouts.

- diff --git a/resources/js/Pages/Owner/Staff/Add.jsx b/resources/js/Pages/Owner/Staff/Add.jsx index fa22f10..51da63e 100644 --- a/resources/js/Pages/Owner/Staff/Add.jsx +++ b/resources/js/Pages/Owner/Staff/Add.jsx @@ -6,6 +6,7 @@ export default function StaffAdd() { const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [branches, setBranches] = useState([]); + const [roles, setRoles] = useState([]); const [toast, setToast] = useState(null); const isReceptionist = window.__APP_DATA__?.role === 'receptionist'; @@ -42,18 +43,27 @@ export default function StaffAdd() { }); useEffect(() => { - const fetchBranches = async () => { + const fetchData = async () => { try { - const response = await fetch('/api/branches'); - const data = await response.json(); - setBranches(data); + const [bRes, rRes] = await Promise.all([ + fetch('/api/branches?status=Active'), + fetch('/api/masters/staff_role') + ]); + const bData = await bRes.json(); + const rData = await rRes.json(); + setBranches(bData); + const activeRoles = rData.filter(r => r.status === 'Active'); + setRoles(activeRoles); + if (activeRoles.length > 0) { + setFormData(prev => ({ ...prev, role: prev.role || activeRoles[0].name })); + } } catch (error) { - console.error('Error fetching branches:', error); + console.error('Error fetching data:', error); } finally { setLoading(false); } }; - fetchBranches(); + fetchData(); }, []); const handleChange = (e) => { @@ -167,6 +177,7 @@ export default function StaffAdd() { if (!doc.name) errors.push(`Document ${idx + 1}: Name is required.`); if (!doc.document_number) errors.push(`Document ${idx + 1}: Document Number is required.`); if (!doc.expiry_date) errors.push(`Document ${idx + 1}: Expiry Date is required.`); + if (!doc.file) errors.push(`Document ${idx + 1}: Please upload a file.`); } }); @@ -222,7 +233,7 @@ export default function StaffAdd() { const errorData = await response.json().catch(() => ({})); if (errorData.errors) { const message = Object.entries(errorData.errors) - .map(([field, msgs]) => `${field.replace('_', ' ')}: ${msgs.join(', ')}`) + .map(([field, msgs]) => `• ${field.replace(/_/g, ' ')}: ${msgs.join(', ')}`) .join('\n'); setToast({ message: 'Validation Error:\n' + message, type: 'error' }); } else { @@ -281,9 +292,10 @@ export default function StaffAdd() {
{!isReceptionist && ( diff --git a/resources/js/Pages/Owner/Staff/Edit.jsx b/resources/js/Pages/Owner/Staff/Edit.jsx index c999865..8cda8e7 100644 --- a/resources/js/Pages/Owner/Staff/Edit.jsx +++ b/resources/js/Pages/Owner/Staff/Edit.jsx @@ -8,6 +8,7 @@ export default function StaffEdit({ id }) { const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [branches, setBranches] = useState([]); + const [roles, setRoles] = useState([]); const [history, setHistory] = useState([]); const [showHistory, setShowHistory] = useState(false); const [loadingHistory, setLoadingHistory] = useState(false); @@ -49,10 +50,15 @@ export default function StaffEdit({ id }) { useEffect(() => { const fetchData = async () => { try { - // Fetch Branches - const bRes = await fetch('/api/branches'); + // Fetch Branches and Roles + const [bRes, rRes] = await Promise.all([ + fetch('/api/branches?status=Active'), + fetch('/api/masters/staff_role') + ]); const bData = await bRes.json(); + const rData = await rRes.json(); setBranches(bData); + setRoles(rData.filter(r => r.status === 'Active')); // Fetch Staff Details const sRes = await fetch(`/api/staff/${id}`); @@ -154,9 +160,13 @@ export default function StaffEdit({ id }) { }; const handleDocumentChange = (index, e) => { - const { name, value } = e.target; + const { name, value, files } = e.target; const newDocs = [...formData.documents]; - newDocs[index][name] = value; + if (name === 'file') { + newDocs[index][name] = files[0]; + } else { + newDocs[index][name] = value; + } setFormData({ ...formData, documents: newDocs }); }; @@ -232,6 +242,30 @@ export default function StaffEdit({ id }) { } } + // Document Validations + for (let i = 0; i < formData.documents.length; i++) { + const doc = formData.documents[i]; + if (doc.name || doc.document_number || doc.expiry_date || doc.file) { + if (!doc.name) { + setToast({ message: `Document ${i + 1}: Name is required.`, type: 'error' }); + return; + } + if (!doc.document_number) { + setToast({ message: `Document ${i + 1}: Document Number is required.`, type: 'error' }); + return; + } + if (!doc.expiry_date) { + setToast({ message: `Document ${i + 1}: Expiry Date is required.`, type: 'error' }); + return; + } + // Only require file if it's a new document row (no id) and no existing path + if (!doc.id && !doc.file && !doc.path) { + setToast({ message: `Document ${i + 1}: Please upload a file.`, type: 'error' }); + return; + } + } + } + setSaving(true); const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'); @@ -284,7 +318,7 @@ export default function StaffEdit({ id }) { const errorData = await response.json().catch(() => ({})); if (errorData.errors) { const message = Object.entries(errorData.errors) - .map(([field, msgs]) => `${field.replace('_', ' ')}: ${msgs.join(', ')}`) + .map(([field, msgs]) => `• ${field.replace(/_/g, ' ')}: ${msgs.join(', ')}`) .join('\n'); setToast({ message: 'Validation Error:\n' + message, type: 'error' }); } else { @@ -343,9 +377,10 @@ export default function StaffEdit({ id }) {
diff --git a/resources/js/Pages/Owner/Staff/List.jsx b/resources/js/Pages/Owner/Staff/List.jsx index 6bf93c6..d2000dd 100644 --- a/resources/js/Pages/Owner/Staff/List.jsx +++ b/resources/js/Pages/Owner/Staff/List.jsx @@ -39,7 +39,7 @@ export default function StaffList() { const fetchBranches = async () => { try { - const response = await fetch('/api/branches'); + const response = await fetch('/api/branches?status=Active'); const data = await response.json(); setBranches(data); } catch (error) { diff --git a/resources/js/Pages/Receptionist/POS.jsx b/resources/js/Pages/Receptionist/POS.jsx index c194c88..c35ec08 100644 --- a/resources/js/Pages/Receptionist/POS.jsx +++ b/resources/js/Pages/Receptionist/POS.jsx @@ -12,6 +12,7 @@ import { CheckCircle2, MapPin } from 'lucide-react'; +import Toast from '../Owner/Components/Toast'; export default function POS() { const [products, setProducts] = useState([]); @@ -26,12 +27,18 @@ export default function POS() { const [adjustmentRemarks, setAdjustmentRemarks] = useState(''); const [branches, setBranches] = useState([]); const [selectedBranch, setSelectedBranch] = useState(window.__APP_DATA__?.user?.branch_id || ''); + const [toast, setToast] = useState(null); + + const showToast = (message, type = 'success') => { + setToast({ message, type }); + setTimeout(() => setToast(null), 3000); + }; useEffect(() => { if (window.__APP_DATA__?.role === 'owner') { const fetchBranches = async () => { try { - const response = await fetch('/api/branches'); + const response = await fetch('/api/branches?status=Active'); const data = await response.json(); setBranches(data || []); if (data?.length > 0 && !selectedBranch) { @@ -99,21 +106,33 @@ export default function POS() { setCart(prev => { const existing = prev.find(item => item.id === product.id); if (existing) { + if (existing.quantity >= product.current_stock) { + showToast(`Only ${product.current_stock} units available in stock.`, 'error'); + return prev; + } return prev.map(item => item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item ); } + if (product.current_stock <= 0) { + showToast("Product is out of stock.", 'error'); + return prev; + } return [...prev, { ...product, quantity: 1 }]; }); }; - const updateQuantity = (id, delta) => { - setCart(prev => prev.map(item => { - if (item.id === id) { - const newQty = Math.max(1, item.quantity + delta); - return { ...item, quantity: newQty }; + const updateQuantity = (item, delta) => { + setCart(prev => prev.map(cartItem => { + if (cartItem.id === item.id) { + const newQty = cartItem.quantity + delta; + if (newQty > item.current_stock) { + showToast(`Only ${item.current_stock} units available in stock.`, 'error'); + return cartItem; + } + return { ...cartItem, quantity: Math.max(1, newQty) }; } - return item; + return cartItem; })); }; @@ -162,6 +181,7 @@ export default function POS() { return ( <> + {toast && setToast(null)} />}
{/* Product Catalog Column */}
@@ -281,9 +301,9 @@ export default function POS() {
- + {item.quantity} - +

{parseFloat(item.selling_price * item.quantity).toFixed(2)}

diff --git a/routes/web.php b/routes/web.php index 86a2ef0..203f8f3 100644 --- a/routes/web.php +++ b/routes/web.php @@ -48,6 +48,7 @@ Route::get('/', [BranchController::class, 'index']); Route::post('/', [BranchController::class, 'store']); Route::get('/{id}', [BranchController::class, 'show']); + Route::get('/{id}/active-staff', [BranchController::class, 'activeStaff']); Route::put('/{id}', [BranchController::class, 'update']); Route::delete('/{id}', [BranchController::class, 'destroy']); });