updates 2

This commit is contained in:
ashok 2026-03-14 17:13:13 +05:30
parent e1e55959e1
commit 50ba5c23e8
19 changed files with 379 additions and 137 deletions

View File

@ -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')]);
}

View File

@ -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();

View File

@ -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)

View File

@ -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);
// Base Query from Account table (Ledger)
$query = Account::query()->with('accountable');
$query = Account::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()
]);
}
}

View File

@ -63,7 +63,7 @@ export default function List() {
{ header: 'Manager', key: 'manager_name' },
{
header: 'Revenue (AED)',
render: () => <span className="font-bold text-gray-900">0.00</span>
render: (row) => <span className="font-bold text-emerald-600">{(row.total_revenue || 0).toLocaleString()} AED</span>
},
{
header: 'Status',

View File

@ -215,12 +215,16 @@ export default function AddCollectionModal({ isOpen, onClose, onSave, branches,
<label className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-2 ml-1">Branch</label>
<select
required
className="w-full px-6 py-4 bg-gray-50/50 border border-gray-100 rounded-2xl outline-none focus:ring-2 focus:ring-red-500/10 focus:border-red-500 transition-all font-bold text-gray-900 appearance-none"
className="w-full max-w-full px-6 py-4 bg-gray-50/50 border border-gray-100 rounded-2xl outline-none focus:ring-2 focus:ring-red-500/10 focus:border-red-500 transition-all font-bold text-gray-900 appearance-none truncate"
value={formData.branch_id}
onChange={e => setFormData({...formData, branch_id: e.target.value, items: [], amount: ''})}
>
<option value="">Select Branch</option>
{branches.map(b => <option key={b.id} value={b.id}>{b.name}</option>)}
{branches.map(b => (
<option key={b.id} value={b.id}>
{b.name.length > 40 ? b.name.substring(0, 40) + '...' : b.name}
</option>
))}
</select>
</div>
) : (
@ -236,7 +240,7 @@ export default function AddCollectionModal({ isOpen, onClose, onSave, branches,
<label className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-2 ml-1">Collection Type</label>
<select
required
className="w-full px-6 py-4 bg-gray-50/50 border border-gray-100 rounded-2xl outline-none focus:ring-2 focus:ring-red-500/10 focus:border-red-500 transition-all font-bold text-gray-900 appearance-none"
className="w-full max-w-full px-6 py-4 bg-gray-50/50 border border-gray-100 rounded-2xl outline-none focus:ring-2 focus:ring-red-500/10 focus:border-red-500 transition-all font-bold text-gray-900 appearance-none truncate"
value={formData.collection_type_id}
onChange={e => {
const typeId = e.target.value;
@ -250,7 +254,11 @@ export default function AddCollectionModal({ isOpen, onClose, onSave, branches,
}}
>
<option value="">Select Type</option>
{types.map(t => <option key={t.id} value={t.id}>{t.name}</option>)}
{types.map(t => (
<option key={t.id} value={t.id}>
{t.name.length > 40 ? t.name.substring(0, 40) + '...' : t.name}
</option>
))}
</select>
</div>
@ -258,7 +266,7 @@ export default function AddCollectionModal({ isOpen, onClose, onSave, branches,
<label className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-2 ml-1">Payment Method</label>
<select
required
className="w-full px-6 py-4 bg-gray-50/50 border border-gray-100 rounded-2xl outline-none focus:ring-2 focus:ring-red-500/10 focus:border-red-500 transition-all font-bold text-gray-900 appearance-none"
className="w-full max-w-full px-6 py-4 bg-gray-50/50 border border-gray-100 rounded-2xl outline-none focus:ring-2 focus:ring-red-500/10 focus:border-red-500 transition-all font-bold text-gray-900 appearance-none truncate"
value={formData.payment_method}
onChange={e => setFormData({...formData, payment_method: e.target.value})}
>

View File

@ -1,7 +1,7 @@
import React from 'react';
import { MoreVertical, ArrowDownRight, ArrowUpRight } from 'lucide-react';
import { MoreVertical, ArrowDownRight, ArrowUpRight, ChevronLeft, ChevronRight } from 'lucide-react';
export default function AccountsTable({ data = [] }) {
export default function AccountsTable({ data = [], pagination = null, onPageChange }) {
const formatCurrency = (val) => {
return new Intl.NumberFormat('en-AE', { style: 'currency', currency: 'AED' }).format(val);
};
@ -68,6 +68,44 @@ export default function AccountsTable({ data = [] }) {
</tbody>
</table>
</div>
{/* Pagination Controls */}
{pagination && pagination.last_page > 1 && (
<div className="flex items-center justify-between mt-8 pt-6 border-t border-gray-50">
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest">
Showing page {pagination.current_page} of {pagination.last_page} ({pagination.total} total)
</p>
<div className="flex items-center gap-2">
<button
disabled={pagination.current_page === 1}
onClick={() => onPageChange(pagination.current_page - 1)}
className="p-2 bg-gray-50 text-gray-400 rounded-lg hover:bg-red-50 hover:text-red-500 transition-all disabled:opacity-30 disabled:hover:bg-gray-50 disabled:hover:text-gray-400"
>
<ChevronLeft size={18} />
</button>
{[...Array(pagination.last_page)].map((_, i) => (
<button
key={i + 1}
onClick={() => onPageChange(i + 1)}
className={`w-9 h-9 flex items-center justify-center rounded-lg text-[10px] font-black transition-all ${
pagination.current_page === i + 1
? 'bg-red-500 text-white shadow-lg shadow-red-200'
: 'bg-gray-50 text-gray-400 hover:bg-gray-100 hover:text-gray-900'
}`}
>
{i + 1}
</button>
))}
<button
disabled={pagination.current_page === pagination.last_page}
onClick={() => onPageChange(pagination.current_page + 1)}
className="p-2 bg-gray-50 text-gray-400 rounded-lg hover:bg-red-50 hover:text-red-500 transition-all disabled:opacity-30 disabled:hover:bg-gray-50 disabled:hover:text-gray-400"
>
<ChevronRight size={18} />
</button>
</div>
</div>
)}
</div>
);
}

View File

@ -8,16 +8,15 @@ export default function Dashboard() {
total_income: 0,
total_expense: 0,
net_profit: 0,
low_stock_count: 0,
transactions: []
});
const [branches, setBranches] = useState([]);
const [loading, setLoading] = useState(true);
const [filterBranch, setFilterBranch] = useState('');
const [startDate, setStartDate] = useState(() => {
const d = new Date();
d.setMonth(d.getMonth() - 1);
return d.toISOString().split('T')[0];
});
const [currentPage, setCurrentPage] = useState(1);
const [paginationData, setPaginationData] = useState(null);
const [startDate, setStartDate] = useState(new Date().toISOString().split('T')[0]);
const [endDate, setEndDate] = useState(new Date().toISOString().split('T')[0]);
useEffect(() => {
@ -32,7 +31,8 @@ export default function Dashboard() {
const params = new URLSearchParams({
branch_id: filterBranch,
start_date: startDate,
end_date: endDate
end_date: endDate,
page: currentPage
});
fetch(`/api/reports/profit?${params}`)
.then(res => res.json())
@ -41,14 +41,21 @@ export default function Dashboard() {
total_income: data.total_income,
total_expense: data.total_expense,
net_profit: data.net_profit,
low_stock_count: data.low_stock_count,
transactions: data.transactions || []
});
setPaginationData(data.pagination);
setLoading(false);
})
.catch(err => {
console.error("Error fetching dashboard stats:", err);
setLoading(false);
});
}, [filterBranch, startDate, endDate, currentPage]);
// Reset page when filters change
useEffect(() => {
setCurrentPage(1);
}, [filterBranch, startDate, endDate]);
const formatCurrency = (val) => {
@ -69,10 +76,14 @@ export default function Dashboard() {
<select
value={filterBranch}
onChange={(e) => 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"
>
<option value="">All Branches</option>
{branches.map(b => <option key={b.id} value={b.id}>{b.name}</option>)}
{branches.map(b => (
<option key={b.id} value={b.id}>
{b.name.length > 30 ? b.name.substring(0, 30) + '...' : b.name}
</option>
))}
</select>
<ChevronDown size={14} className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 group-focus-within:text-red-500 transition-all" />
</div>
@ -104,33 +115,44 @@ export default function Dashboard() {
</div>
{/* Stat Cards Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
<StatCard
title="Aggregated amount of all credits"
subtitle="Total Credited"
title="Total Income Collected"
subtitle="Total Credits"
value={loading ? "..." : formatCurrency(stats.total_income)}
color="green"
icon={DollarSign}
/>
<StatCard
title="Aggregated amount of all debits"
subtitle="Total Debited"
title="Total Expenses Paid"
subtitle="Total Debits"
value={loading ? "..." : formatCurrency(stats.total_expense)}
color="red"
icon={TrendingDown}
/>
<StatCard
title={loading ? "Loading..." : formatCurrency(stats.net_profit)}
title="Total Net Savings"
subtitle="Net Profit"
value={loading ? "..." : formatCurrency(stats.net_profit)}
color="blue"
icon={TrendingUp}
/>
<StatCard
title="Items Below Reorder Level"
subtitle="Low Stock Products"
value={loading ? "..." : stats.low_stock_count}
color="red"
icon={TrendingDown}
/>
</div>
{/* Main Content Area */}
<div className="grid grid-cols-1 gap-8">
<AccountsTable data={stats.transactions} />
<AccountsTable
data={stats.transactions}
pagination={paginationData}
onPageChange={(p) => setCurrentPage(p)}
/>
</div>
</main>
</>

View File

@ -69,7 +69,9 @@ export default function ExpenseList() {
fetchExpenses();
fetchMetadata();
fetchPendingSalaries();
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() {
</div>
</div>
{/* Tabs */}
<div className="flex items-center gap-8 border-b border-gray-100 mb-6">
{['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 => (
<button
key={tab}
onClick={() => setActiveTab(tab)}

View File

@ -107,7 +107,7 @@ export default function AddProductModal({ isOpen, onClose, onSave, branches, cat
<label className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-2 ml-1">Category</label>
<select
required
className="w-full px-6 py-4 bg-gray-50/50 border border-gray-100 rounded-2xl outline-none focus:ring-2 focus:ring-red-500/10 focus:border-red-500 transition-all font-bold text-gray-900 appearance-none truncate"
className="w-full max-w-full px-6 py-4 bg-gray-50/50 border border-gray-100 rounded-2xl outline-none focus:ring-2 focus:ring-red-500/10 focus:border-red-500 transition-all font-bold text-gray-900 appearance-none truncate"
value={formData.product_category_id}
onChange={e => setFormData({...formData, product_category_id: e.target.value})}
>
@ -115,7 +115,7 @@ export default function AddProductModal({ isOpen, onClose, onSave, branches, cat
{categories.length === 0 && <option disabled className="whitespace-normal">No active categories found. Please add them in Masters.</option>}
{categories.map(c => (
<option key={c.id} value={c.id} className="whitespace-normal" title={c.name}>
{c.name.length > 50 ? c.name.substring(0, 50) + '...' : c.name}
{c.name.length > 40 ? c.name.substring(0, 40) + '...' : c.name}
</option>
))}
</select>
@ -126,7 +126,7 @@ export default function AddProductModal({ isOpen, onClose, onSave, branches, cat
<label className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-2 ml-1">Branch</label>
<select
required
className="w-full px-6 py-4 bg-gray-50/50 border border-gray-100 rounded-2xl outline-none focus:ring-2 focus:ring-red-500/10 focus:border-red-500 transition-all font-bold text-gray-900 appearance-none"
className="w-full max-w-full px-6 py-4 bg-gray-50/50 border border-gray-100 rounded-2xl outline-none focus:ring-2 focus:ring-red-500/10 focus:border-red-500 transition-all font-bold text-gray-900 appearance-none truncate"
value={formData.branch_id}
onChange={e => setFormData({...formData, branch_id: e.target.value})}
>

View File

@ -107,7 +107,7 @@ export default function AdjustStockModal({ isOpen, onClose, onSave, product }) {
<label className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-2 ml-1">Reason</label>
<select
required
className="w-full px-6 py-4 bg-gray-50/50 border border-gray-100 rounded-2xl outline-none focus:ring-2 focus:ring-blue-500/10 focus:border-blue-500 transition-all font-bold text-gray-900 appearance-none"
className="w-full max-w-full px-6 py-4 bg-gray-50/50 border border-gray-100 rounded-2xl outline-none focus:ring-2 focus:ring-blue-500/10 focus:border-blue-500 transition-all font-bold text-gray-900 appearance-none truncate"
value={formData.reason}
onChange={e => setFormData({...formData, reason: e.target.value})}
>

View File

@ -160,14 +160,18 @@ export default function NewSaleModal({ isOpen, onClose, onSave, branches, produc
</div>
{window.__APP_DATA__?.role !== 'receptionist' && (
<select
className="px-5 py-3 bg-white border border-gray-100 rounded-2xl outline-none font-black text-[10px] uppercase tracking-widest cursor-pointer shadow-sm"
className="px-5 py-3 bg-white border border-gray-100 rounded-2xl outline-none font-black text-[10px] uppercase tracking-widest cursor-pointer shadow-sm max-w-full truncate"
value={selectedBranch}
onChange={e => {
setSelectedBranch(e.target.value);
setCart([]);
}}
>
{branches.map(b => <option key={b.id} value={b.id}>{b.name}</option>)}
{branches.map(b => (
<option key={b.id} value={b.id}>
{b.name.length > 30 ? b.name.substring(0, 30) + '...' : b.name}
</option>
))}
</select>
)}
</div>

View File

@ -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,8 +377,6 @@ export default function ReportIndex() {
</select>
</div>
)}
{activeTab !== 'Expiry Reminders' && (
<>
<div className="min-w-[160px]">
<label className="block text-[9px] font-black text-[#A3AED0] uppercase tracking-widest mb-1.5 ml-1">From Date</label>
<input
@ -389,8 +395,14 @@ export default function ReportIndex() {
onChange={(e) => setEndDate(e.target.value)}
/>
</div>
</>
)}
<button
onClick={resetFilters}
className="flex items-center gap-2 px-6 py-2.5 mt-auto bg-gray-50 text-gray-400 rounded-xl text-xs font-black uppercase tracking-widest hover:bg-gray-100 hover:text-[#1B254B] transition-all border border-gray-100 mb-0.5"
title="Reset Filters"
>
<RotateCcw size={14} />
Reset
</button>
</div>
</div>
@ -954,7 +966,24 @@ export default function ReportIndex() {
<td className="px-10 py-6 text-sm font-bold text-[#1B254B]">{s.payment_method}</td>
<td className="px-10 py-6 text-right text-xs font-black text-[#A3AED0]">{parseFloat(s.subtotal_amount || 0).toFixed(2)} AED</td>
<td className="px-10 py-6 text-right text-xs font-black text-[#A3AED0]">{parseFloat(s.vat_amount || 0).toFixed(2)} AED</td>
<td className="px-10 py-6 text-right text-sm font-black text-emerald-500">{parseFloat(s.total_amount).toLocaleString()} AED</td>
<td className="px-10 py-6 text-right text-sm font-black text-emerald-500">
<div className="flex flex-col items-end gap-1">
<span>{parseFloat(s.total_amount).toLocaleString()} AED</span>
{s.is_adjusted && (
<button
onClick={() => setSelectedItem({
transaction_id: s.transaction_id,
original_amount: s.original_amount,
amount: s.total_amount,
remarks: s.remarks
})}
className="px-2 py-0.5 bg-amber-50 text-amber-600 rounded-md text-[9px] font-black uppercase tracking-wider border border-amber-100 hover:bg-amber-100 transition-colors outline-none"
>
Adjusted
</button>
)}
</div>
</td>
</tr>
))
) : (

View File

@ -383,6 +383,17 @@ export default function StaffEdit({ id }) {
))}
</select>
</div>
{!isReceptionist && (
<div>
<label className="block text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Branch *</label>
<select name="branch_id" value={formData.branch_id} onChange={handleChange} className="w-full bg-gray-50 border-none rounded-xl px-4 py-3 text-sm focus:ring-2 focus:ring-red-500 transition-all font-medium appearance-none">
<option value="">Select Branch...</option>
{branches.map(branch => (
<option key={branch.id} value={branch.id}>{branch.name}</option>
))}
</select>
</div>
)}
<div>
<label className="block text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Joining Date *</label>
<input required type="date" name="joining_date" value={formData.joining_date} onChange={handleChange} className="w-full bg-gray-50 border-none rounded-xl px-4 py-3 text-sm focus:ring-2 focus:ring-red-500 transition-all font-medium" />

View File

@ -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 }) => (
<div className={`${bgColor} p-6 rounded-[1.5rem] shadow-sm relative overflow-hidden group`}>
@ -103,11 +120,56 @@ export default function ReceptionistDashboard() {
<div className="animate-in fade-in duration-700">
<main className="p-8 max-w-[1600px] mx-auto space-y-12 animate-in fade-in duration-700">
{/* Welcome Section */}
{/* Welcome Section & Filters */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div className="flex flex-col gap-2">
<h1 className="text-4xl font-black text-gray-900 tracking-tight flex items-center gap-4">
Receptionist Dashboard
</h1>
<p className="text-sm font-bold text-gray-400 uppercase tracking-widest">Branch Operations Overview</p>
</div>
<div className="flex items-center gap-3">
{/* Branch Selector (Hardcoded to their branch for receptionists) */}
<div className="relative group">
<select
value={filterBranch}
disabled={true}
className="appearance-none pl-4 pr-10 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-sm font-bold text-gray-400 cursor-not-allowed outline-none max-w-[200px] truncate"
>
{branches.map(b => (
<option key={b.id} value={b.id}>
{b.name}
</option>
))}
</select>
<ChevronDown size={14} className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-300" />
</div>
{/* Date Range */}
<div className="flex items-center bg-white border border-gray-200 rounded-xl p-1 gap-1">
<div className="flex items-center gap-2 px-3 py-1.5 text-xs font-bold text-gray-500 border-r border-gray-100">
<span className="text-gray-400 font-medium whitespace-nowrap">From</span>
<input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="bg-transparent border-none p-0 focus:ring-0 text-gray-700 outline-none w-28"
/>
<Calendar size={14} className="text-gray-400" />
</div>
<div className="flex items-center gap-2 px-3 py-1.5 text-xs font-bold text-gray-500">
<span className="text-gray-400 font-medium whitespace-nowrap">To</span>
<input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
className="bg-transparent border-none p-0 focus:ring-0 text-gray-700 outline-none w-28"
/>
<Calendar size={14} className="text-gray-400" />
</div>
</div>
</div>
</div>
{/* Stats Grid */}
@ -188,7 +250,7 @@ export default function ReceptionistDashboard() {
</thead>
<tbody className="divide-y divide-gray-50">
{transactions.length > 0 ? (
transactions.map((tx, idx) => (
transactions.slice(0, 10).map((tx, idx) => (
<tr key={idx} className="hover:bg-gray-50/30 transition-colors">
<td className="px-8 py-5">
<div className="flex flex-col">
@ -201,15 +263,15 @@ export default function ReceptionistDashboard() {
</td>
<td className="px-8 py-5">
<span className={`px-2.5 py-1 rounded-full text-[10px] font-black shadow-sm uppercase tracking-wider ${
tx.debit > 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'}
</span>
</td>
<td className="px-8 py-5 text-right">
<span className={`text-sm font-black ${tx.debit > 0 ? 'text-red-500' : 'text-emerald-500'}`}>
{tx.debit > 0 ? '-' : '+'}
{(tx.debit || tx.credit || 0).toLocaleString('en-AE', { minimumFractionDigits: 2 })}
<span className={`text-sm font-black ${tx.type === 'Expense' ? 'text-red-500' : 'text-emerald-500'}`}>
{tx.type === 'Expense' ? '-' : '+'}
{(tx.amount || 0).toLocaleString('en-AE', { minimumFractionDigits: 2 })}
<span className="text-[10px] ml-1 uppercase">AED</span>
</span>
</td>

View File

@ -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() {
</div>
</div>
</main>
{/* Success Modal */}
{showSuccessModal && (
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-gray-900/60 backdrop-blur-sm animate-in fade-in duration-300">
<div className="bg-white rounded-[2.5rem] p-10 max-w-md w-full mx-4 shadow-2xl border border-gray-100 animate-in zoom-in-95 duration-300 text-center">
<div className="w-20 h-20 bg-emerald-50 rounded-full flex items-center justify-center mx-auto mb-6 text-emerald-500 shadow-inner">
<CheckCircle2 size={40} />
</div>
<h3 className="text-2xl font-black text-gray-900 mb-2">Sale Successful!</h3>
<p className="text-gray-500 font-bold mb-8">The transaction has been processed and recorded successfully.</p>
<button
onClick={() => setShowSuccessModal(false)}
className="w-full py-4 bg-gray-900 text-white rounded-2xl font-black uppercase tracking-widest text-xs hover:bg-gray-800 transition-all active:scale-95 shadow-lg shadow-gray-200"
>
Got it, thanks!
</button>
</div>
</div>
)}
</>
);
}

View File

@ -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');

View File

@ -169,7 +169,7 @@ function MainApp() {
const id = path.split('/').pop();
component = <InvestorView id={id} />;
} else if (path === '/receptionist/reports') {
component = <ReceptionistReportIndex />;
component = <ReportIndex />;
}
if (component) {

View File

@ -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']);
});