diff --git a/app/Http/Controllers/BranchController.php b/app/Http/Controllers/BranchController.php index c3ee064..12912ac 100644 --- a/app/Http/Controllers/BranchController.php +++ b/app/Http/Controllers/BranchController.php @@ -17,7 +17,7 @@ public function index(Request $request) } $branches = $query->get(); - // Attach is_deletable flag to each branch + // Attach is_deletable flag and total revenue 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() @@ -27,6 +27,9 @@ public function index(Request $request) || \App\Models\ProductSale::where('branch_id', $branch->id)->exists() || \App\Models\Receptionist::where('branch_id', $branch->id)->exists(); $branch->is_deletable = !$inUse; + + // Calculate total revenue (total credit in accounts) + $branch->total_revenue = \App\Models\Account::where('branch_id', $branch->id)->sum('credit'); }); return response()->json($branches); @@ -123,6 +126,21 @@ public function update(Request $request, $id) } } + // Process new documents if provided + if (!empty($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 + ]); + } + } + return response()->json(['message' => 'Branch updated successfully', 'branch' => $branch->load('documents')]); } diff --git a/app/Http/Controllers/ExpenseController.php b/app/Http/Controllers/ExpenseController.php index f0a0233..f4d290a 100644 --- a/app/Http/Controllers/ExpenseController.php +++ b/app/Http/Controllers/ExpenseController.php @@ -104,8 +104,18 @@ public function getSalaryHistory(Request $request) $query = Expense::with(['branch']) ->where('expense_category_id', $salaryCategory->id); - if ($user->isReceptionist()) { - $query->where('branch_id', $user->branch_id); + $branchId = $user->isReceptionist() ? $user->branch_id : $request->query('branch_id'); + $startDate = $request->query('start_date'); + $endDate = $request->query('end_date'); + + if ($branchId) { + $query->where('branch_id', $branchId); + } + if ($startDate) { + $query->where('date', '>=', $startDate); + } + if ($endDate) { + $query->where('date', '<=', $endDate); } $expenses = $query->orderBy('date', 'desc')->get(); diff --git a/app/Http/Controllers/InventoryController.php b/app/Http/Controllers/InventoryController.php index 0b263b9..2061cc9 100644 --- a/app/Http/Controllers/InventoryController.php +++ b/app/Http/Controllers/InventoryController.php @@ -145,7 +145,14 @@ public function getSales(Request $request) $query->where('date', '<=', $endDate); } - return response()->json($query->orderBy('date', 'desc')->get()); + $sales = $query->orderBy('date', 'desc')->get()->map(function($s) { + $originalTotal = ($s->subtotal_amount ?? 0) + ($s->vat_amount ?? 0); + $s->is_adjusted = abs($s->total_amount - $originalTotal) > 0.01; + $s->original_amount = $originalTotal; + return $s; + }); + + return response()->json($sales); } public function storeSale(Request $request) diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php index 6fc50c8..b7a7a81 100644 --- a/app/Http/Controllers/ReportController.php +++ b/app/Http/Controllers/ReportController.php @@ -22,8 +22,12 @@ public function getProfitReport(Request $request) $branchId = $user->isReceptionist() ? $user->branch_id : $request->query('branch_id'); $startDate = $request->query('start_date'); $endDate = $request->query('end_date'); + $page = $request->query('page', 1); + $perPage = $request->query('per_page', 10); - $query = Account::with('accountable'); + // Base Query from Account table (Ledger) + $query = Account::query()->with('accountable'); + if ($branchId) { $query->where('branch_id', $branchId); } @@ -34,24 +38,18 @@ public function getProfitReport(Request $request) $query->where('date', '<=', $endDate); } + // Stats from the same filtered query $totalCredits = (clone $query)->sum('credit'); $totalDebits = (clone $query)->sum('debit'); - // Fetch All Ledger Transactions for the breakdown - $accounts = Account::where(function($q) { + // Fetch Paginated Transactions from the same filtered query + $allTransactions = $query->where(function($q) { $q->where('credit', '>', 0)->orWhere('debit', '>', 0); - }); - - if ($branchId) { - $accounts->where('branch_id', $branchId); - } - if ($startDate) { - $accounts->where('date', '>=', $startDate); - } - if ($endDate) { - $accounts->where('date', '<=', $endDate); - } - $accounts = $accounts->get() + }) + ->orderBy('date', 'desc') + ->orderBy('time', 'desc') + ->orderBy('id', 'desc') + ->get() ->map(function($a) { $isAdjusted = false; $originalAmount = $a->credit > 0 ? $a->credit : $a->debit; @@ -68,41 +66,22 @@ public function getProfitReport(Request $request) } return [ + 'id' => $a->id, 'date' => $a->date, + 'time' => $a->time || '00:00:00', 'type' => $a->credit > 0 ? 'Income' : 'Expense', 'category' => $a->type, 'description' => $a->description, 'amount' => $a->credit > 0 ? $a->credit : $a->debit, - 'branch' => 'N/A', + 'branch' => 'N/A', // Branch name if needed can be added via relation 'is_adjusted' => $isAdjusted, 'original_amount' => $originalAmount, 'remarks' => $remarks ]; }); - $expensesQuery = Expense::with('category', 'branch'); - if ($branchId) { - $expensesQuery->where('branch_id', $branchId); - } - if ($startDate) { - $expensesQuery->where('date', '>=', $startDate); - } - if ($endDate) { - $expensesQuery->where('date', '<=', $endDate); - } - - $expenses = $expensesQuery->get()->map(function($e) { - return [ - 'date' => $e->date, - 'type' => 'Expense', - 'category' => $e->category->name ?? 'Other', - 'description' => $e->remarks, - 'amount' => $e->amount, - 'branch' => $e->branch->name ?? 'Global' - ]; - }); - - $transactions = $accounts->concat($expenses)->sortByDesc('date')->values(); + $totalTransactions = $allTransactions->count(); + $paginatedTransactions = $allTransactions->forPage($page, $perPage)->values(); $lowStockCount = \App\Models\Product::query(); if ($branchId) { @@ -121,9 +100,11 @@ public function getProfitReport(Request $request) ->whereBetween('date', [$monthStart->toDateString(), $monthEnd->toDateString()]) ->sum('credit'); - $monthExpense = Expense::when($branchId, fn($q) => $q->where('branch_id', $branchId)) + // For trend, we can also use Account table for expenses + $monthExpense = Account::where('branch_id', $branchId ?: '!=', 0) + ->when($branchId, fn($q) => $q->where('branch_id', $branchId)) ->whereBetween('date', [$monthStart->toDateString(), $monthEnd->toDateString()]) - ->sum('amount'); + ->sum('debit'); $trend[] = [ 'month' => $monthStart->format('M'), @@ -139,7 +120,13 @@ public function getProfitReport(Request $request) 'total_expense' => $totalDebits, 'net_profit' => $totalCredits - $totalDebits, 'low_stock_count' => $lowStockCount, - 'transactions' => $transactions, + 'transactions' => $paginatedTransactions, + 'pagination' => [ + 'total' => $totalTransactions, + 'per_page' => (int)$perPage, + 'current_page' => (int)$page, + 'last_page' => ceil($totalTransactions / $perPage) + ], 'trend' => $trend ]); } @@ -151,6 +138,8 @@ public function getExpiryReminders(Request $request) if (!$user) return response()->json(['message' => 'Unauthenticated'], 401); $branchId = $user->isReceptionist() ? $user->branch_id : $request->query('branch_id'); + $startDate = $request->query('start_date'); + $endDate = $request->query('end_date'); $staffDocsQuery = \App\Models\StaffDocument::with('staff.branch') ->whereNotNull('expiry_date'); @@ -161,6 +150,13 @@ public function getExpiryReminders(Request $request) }); } + if ($startDate) { + $staffDocsQuery->where('expiry_date', '>=', $startDate); + } + if ($endDate) { + $staffDocsQuery->where('expiry_date', '<=', $endDate); + } + $staffDocs = $staffDocsQuery->get() ->filter(function($doc) use ($today) { $expiryDate = Carbon::parse($doc->expiry_date)->startOfDay(); @@ -192,6 +188,13 @@ public function getExpiryReminders(Request $request) $branchDocsQuery->where('branch_id', $branchId); } + if ($startDate) { + $branchDocsQuery->where('expiry_date', '>=', $startDate); + } + if ($endDate) { + $branchDocsQuery->where('expiry_date', '<=', $endDate); + } + $branchReminders = $branchDocsQuery->get() ->filter(function($doc) use ($today) { $expiryDate = Carbon::parse($doc->expiry_date); @@ -216,7 +219,7 @@ public function getExpiryReminders(Request $request) }); return response()->json([ - 'reminders' => $staffDocs->concat($branchReminders)->values() + 'reminders' => $staffDocs->concat($branchReminders)->sortBy('expiry_date')->values() ]); } @@ -311,7 +314,7 @@ public function getInvestmentReport(Request $request) return response()->json([ 'total_invested' => $totalInvested, 'total_roi_returned' => $totalROIReturned, - 'investors' => $reportData + 'investors' => collect($reportData)->sortByDesc('investment_date')->values() ]); } } diff --git a/resources/js/Pages/Owner/Branches/List.jsx b/resources/js/Pages/Owner/Branches/List.jsx index 4c153a3..34a934f 100644 --- a/resources/js/Pages/Owner/Branches/List.jsx +++ b/resources/js/Pages/Owner/Branches/List.jsx @@ -63,7 +63,7 @@ export default function List() { { header: 'Manager', key: 'manager_name' }, { header: 'Revenue (AED)', - render: () => 0.00 + render: (row) => {(row.total_revenue || 0).toLocaleString()} AED }, { header: 'Status', diff --git a/resources/js/Pages/Owner/Collections/AddCollectionModal.jsx b/resources/js/Pages/Owner/Collections/AddCollectionModal.jsx index fca64b9..35f15b2 100644 --- a/resources/js/Pages/Owner/Collections/AddCollectionModal.jsx +++ b/resources/js/Pages/Owner/Collections/AddCollectionModal.jsx @@ -215,12 +215,16 @@ export default function AddCollectionModal({ isOpen, onClose, onSave, branches, ) : ( @@ -236,7 +240,7 @@ export default function AddCollectionModal({ isOpen, onClose, onSave, branches, @@ -258,7 +266,7 @@ export default function AddCollectionModal({ isOpen, onClose, onSave, branches, setFilterBranch(e.target.value)} - className="appearance-none pl-4 pr-10 py-2.5 bg-white border border-gray-200 rounded-xl text-sm font-bold text-gray-600 focus:border-red-500/30 focus:ring-4 focus:ring-red-500/5 transition-all outline-none" + className="appearance-none pl-4 pr-10 py-2.5 bg-white border border-gray-200 rounded-xl text-sm font-bold text-gray-600 focus:border-red-500/30 focus:ring-4 focus:ring-red-500/5 transition-all outline-none max-w-[200px] truncate" > - {branches.map(b => )} + {branches.map(b => ( + + ))} @@ -104,33 +115,44 @@ export default function Dashboard() { {/* Stat Cards Grid */} -
+
+
{/* Main Content Area */}
- + setCurrentPage(p)} + />
diff --git a/resources/js/Pages/Owner/Expenses/List.jsx b/resources/js/Pages/Owner/Expenses/List.jsx index 23d0100..40d69ca 100644 --- a/resources/js/Pages/Owner/Expenses/List.jsx +++ b/resources/js/Pages/Owner/Expenses/List.jsx @@ -69,7 +69,9 @@ export default function ExpenseList() { fetchExpenses(); fetchMetadata(); fetchPendingSalaries(); - fetchPendingROIs(); + if (!isReceptionist) { + fetchPendingROIs(); + } }, []); useEffect(() => { @@ -185,6 +187,9 @@ export default function ExpenseList() { if (response.ok) { setToast({ message: 'Salaries released successfully!', type: 'success' }); setIsBulkModalOpen(false); + if (!isReceptionist) { + setFilterBranch(''); + } fetchPendingSalaries(); fetchExpenses(); } else { @@ -337,9 +342,10 @@ export default function ExpenseList() {
- {/* Tabs */}
- {['General Expenses', 'Pending Salaries', 'Pending ROIs', 'Salary Release History'].map(tab => ( + {['General Expenses', 'Pending Salaries', 'Pending ROIs', 'Salary Release History'] + .filter(tab => !isReceptionist || tab !== 'Pending ROIs') + .map(tab => (
{window.__APP_DATA__?.role !== 'receptionist' && ( )} diff --git a/resources/js/Pages/Owner/Reports/Index.jsx b/resources/js/Pages/Owner/Reports/Index.jsx index 3f4d0ce..5052f44 100644 --- a/resources/js/Pages/Owner/Reports/Index.jsx +++ b/resources/js/Pages/Owner/Reports/Index.jsx @@ -18,16 +18,19 @@ import { Clock, Shield, Building, - AlertCircle + AlertCircle, + RotateCcw } from 'lucide-react'; export default function ReportIndex() { - const [activeTab, setActiveTab] = useState('Profit Report'); + const isReceptionist = window.__APP_DATA__?.role === 'receptionist'; + + const [activeTab, setActiveTab] = useState(isReceptionist ? 'Expense Report' : 'Profit Report'); const [profitData, setProfitData] = useState(null); const [expiryReminders, setExpiryReminders] = useState([]); const [loading, setLoading] = useState(false); const [searchQuery, setSearchQuery] = useState(''); - const [selectedBranch, setSelectedBranch] = useState(''); + const [selectedBranch, setSelectedBranch] = useState(isReceptionist ? (window.__APP_DATA__?.branch?.id || '') : ''); const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); const [branches, setBranches] = useState([]); @@ -43,13 +46,9 @@ export default function ReportIndex() { const [salaryData, setSalaryData] = useState([]); const [selectedItem, setSelectedItem] = useState(null); - const tabs = [ - 'Profit Report', 'Expense Report', 'Collection Report', - 'Low Stock Report', 'Inventory Report', 'Product Sales', - 'Investment Report', 'Salary Report', 'Expiry Reminders' - ]; - - const isReceptionist = window.__APP_DATA__?.role === 'receptionist'; + const tabs = isReceptionist + ? ['Expense Report', 'Collection Report', 'Low Stock Report', 'Inventory Report', 'Product Sales', 'Expiry Reminders'] + : ['Profit Report', 'Expense Report', 'Collection Report', 'Low Stock Report', 'Inventory Report', 'Product Sales', 'Investment Report', 'Salary Report', 'Expiry Reminders']; const buildQueryString = () => { const params = new URLSearchParams(); @@ -59,6 +58,15 @@ export default function ReportIndex() { return params.toString(); }; + const resetFilters = () => { + setStartDate(''); + setEndDate(''); + setSearchQuery(''); + if (!isReceptionist) { + setSelectedBranch(''); + } + }; + useEffect(() => { fetchMetadata(); }, []); @@ -369,28 +377,32 @@ export default function ReportIndex() { )} - {activeTab !== 'Expiry Reminders' && ( - <> -
- - setStartDate(e.target.value)} - /> -
-
- - setEndDate(e.target.value)} - /> -
- - )} +
+ + setStartDate(e.target.value)} + /> +
+
+ + setEndDate(e.target.value)} + /> +
+ @@ -954,7 +966,24 @@ export default function ReportIndex() { {s.payment_method} {parseFloat(s.subtotal_amount || 0).toFixed(2)} AED {parseFloat(s.vat_amount || 0).toFixed(2)} AED - {parseFloat(s.total_amount).toLocaleString()} AED + +
+ {parseFloat(s.total_amount).toLocaleString()} AED + {s.is_adjusted && ( + + )} +
+ )) ) : ( diff --git a/resources/js/Pages/Owner/Staff/Edit.jsx b/resources/js/Pages/Owner/Staff/Edit.jsx index 8cda8e7..e6e5901 100644 --- a/resources/js/Pages/Owner/Staff/Edit.jsx +++ b/resources/js/Pages/Owner/Staff/Edit.jsx @@ -382,9 +382,20 @@ export default function StaffEdit({ id }) { ))} - -
- +
+ {!isReceptionist && ( +
+ + +
+ )} +
+
diff --git a/resources/js/Pages/Receptionist/Dashboard.jsx b/resources/js/Pages/Receptionist/Dashboard.jsx index d164a04..5c1cd0d 100644 --- a/resources/js/Pages/Receptionist/Dashboard.jsx +++ b/resources/js/Pages/Receptionist/Dashboard.jsx @@ -1,11 +1,10 @@ import React, { useState, useEffect } from 'react'; // Header and SubHeader are now part of the global Layout import { - Wallet, - TrendingUp, - ArrowUpRight, - ArrowDownRight, DollarSign, + TrendingUp, + Calendar, + ChevronDown, Activity, ShoppingCart, Package, @@ -23,11 +22,29 @@ export default function ReceptionistDashboard() { }); const [transactions, setTransactions] = useState([]); const [loading, setLoading] = useState(true); + const [filterBranch, setFilterBranch] = useState(window.__APP_DATA__?.branch?.id || ''); + const [startDate, setStartDate] = useState(new Date().toISOString().split('T')[0]); + const [endDate, setEndDate] = useState(new Date().toISOString().split('T')[0]); + const [branches, setBranches] = useState([]); + + useEffect(() => { + // Fetch branches for the selector (even if disabled for receptionist) + fetch('/api/branches?status=Active') + .then(res => res.json()) + .then(data => setBranches(data)) + .catch(err => console.error("Error fetching branches:", err)); + }, []); useEffect(() => { const fetchDashboardData = async () => { + setLoading(true); try { - const response = await fetch('/api/reports/profit'); + const query = new URLSearchParams({ + branch_id: filterBranch, + start_date: startDate, + end_date: endDate + }); + const response = await fetch(`/api/reports/profit?${query}`); const data = await response.json(); setStats({ @@ -45,7 +62,7 @@ export default function ReceptionistDashboard() { }; fetchDashboardData(); - }, []); + }, [filterBranch, startDate, endDate]); const StatCard = ({ title, amount, icon: Icon, color, trend, iconColor, bgColor, textColor, label }) => (
@@ -103,11 +120,56 @@ export default function ReceptionistDashboard() {
- {/* Welcome Section */} -
-

- Receptionist Dashboard -

+ {/* Welcome Section & Filters */} +
+
+

+ Receptionist Dashboard +

+

Branch Operations Overview

+
+ +
+ {/* Branch Selector (Hardcoded to their branch for receptionists) */} +
+ + +
+ + {/* Date Range */} +
+
+ From + setStartDate(e.target.value)} + className="bg-transparent border-none p-0 focus:ring-0 text-gray-700 outline-none w-28" + /> + +
+
+ To + setEndDate(e.target.value)} + className="bg-transparent border-none p-0 focus:ring-0 text-gray-700 outline-none w-28" + /> + +
+
+
{/* Stats Grid */} @@ -188,7 +250,7 @@ export default function ReceptionistDashboard() { {transactions.length > 0 ? ( - transactions.map((tx, idx) => ( + transactions.slice(0, 10).map((tx, idx) => (
@@ -201,15 +263,15 @@ export default function ReceptionistDashboard() { 0 ? 'bg-red-50 text-red-600' : 'bg-emerald-50 text-emerald-600' + tx.type === 'Expense' ? 'bg-red-50 text-red-600' : 'bg-emerald-50 text-emerald-600' }`}> - {tx.debit > 0 ? 'DEBITED' : 'RECEIVED'} + {tx.type === 'Expense' ? 'DEBITED' : 'RECEIVED'} - 0 ? 'text-red-500' : 'text-emerald-500'}`}> - {tx.debit > 0 ? '-' : '+'} - {(tx.debit || tx.credit || 0).toLocaleString('en-AE', { minimumFractionDigits: 2 })} + + {tx.type === 'Expense' ? '-' : '+'} + {(tx.amount || 0).toLocaleString('en-AE', { minimumFractionDigits: 2 })} AED diff --git a/resources/js/Pages/Receptionist/POS.jsx b/resources/js/Pages/Receptionist/POS.jsx index c35ec08..7f182f4 100644 --- a/resources/js/Pages/Receptionist/POS.jsx +++ b/resources/js/Pages/Receptionist/POS.jsx @@ -28,6 +28,7 @@ export default function POS() { const [branches, setBranches] = useState([]); const [selectedBranch, setSelectedBranch] = useState(window.__APP_DATA__?.user?.branch_id || ''); const [toast, setToast] = useState(null); + const [showSuccessModal, setShowSuccessModal] = useState(false); const showToast = (message, type = 'success') => { setToast({ message, type }); @@ -168,6 +169,7 @@ export default function POS() { if (response.ok) { setSuccess(true); + setShowSuccessModal(true); setCart([]); setAdjustmentRemarks(''); setTimeout(() => setSuccess(false), 3000); @@ -416,6 +418,25 @@ export default function POS() {
+ + {/* Success Modal */} + {showSuccessModal && ( +
+
+
+ +
+

Sale Successful!

+

The transaction has been processed and recorded successfully.

+ +
+
+ )} ); } diff --git a/resources/js/Pages/Receptionist/Reports/Index.jsx b/resources/js/Pages/Receptionist/Reports/Index.jsx index 10fe517..1aeb5cf 100644 --- a/resources/js/Pages/Receptionist/Reports/Index.jsx +++ b/resources/js/Pages/Receptionist/Reports/Index.jsx @@ -13,8 +13,8 @@ export default function ReceptionistReportIndex() { const [activeTab, setActiveTab] = useState('Collections'); const [loading, setLoading] = useState(false); const [searchQuery, setSearchQuery] = useState(''); - const [fromDate, setFromDate] = useState('2026-02-06'); // Defaulting based on screenshot - const [toDate, setToDate] = useState('2026-03-08'); + const [fromDate, setFromDate] = useState(new Date().toISOString().split('T')[0]); + const [toDate, setToDate] = useState(new Date().toISOString().split('T')[0]); const [method, setMethod] = useState('All Methods'); const [type, setType] = useState('All Types'); const [expenseType, setExpenseType] = useState('All Types'); diff --git a/resources/js/app.jsx b/resources/js/app.jsx index 0dd90df..81cba9e 100644 --- a/resources/js/app.jsx +++ b/resources/js/app.jsx @@ -169,7 +169,7 @@ function MainApp() { const id = path.split('/').pop(); component = ; } else if (path === '/receptionist/reports') { - component = ; + component = ; } if (component) { diff --git a/routes/web.php b/routes/web.php index 203f8f3..426199c 100644 --- a/routes/web.php +++ b/routes/web.php @@ -118,5 +118,8 @@ Route::get('/receptionist/expenses', [OwnerController::class, 'index']); Route::get('/receptionist/inventory', [OwnerController::class, 'index']); Route::get('/receptionist/staff', [OwnerController::class, 'index']); + Route::get('/receptionist/staff/add', [OwnerController::class, 'index']); + Route::get('/receptionist/staff/edit/{id}', [OwnerController::class, 'index']); + Route::get('/receptionist/staff/view/{id}', [OwnerController::class, 'index']); Route::get('/receptionist/reports', [OwnerController::class, 'index']); });