updates 2
This commit is contained in:
parent
e1e55959e1
commit
50ba5c23e8
@ -17,7 +17,7 @@ public function index(Request $request)
|
|||||||
}
|
}
|
||||||
$branches = $query->get();
|
$branches = $query->get();
|
||||||
|
|
||||||
// Attach is_deletable flag to each branch
|
// Attach is_deletable flag and total revenue to each branch
|
||||||
$branches->each(function ($branch) {
|
$branches->each(function ($branch) {
|
||||||
$inUse = \App\Models\Staff::where('branch_id', $branch->id)->exists()
|
$inUse = \App\Models\Staff::where('branch_id', $branch->id)->exists()
|
||||||
|| \App\Models\Product::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\ProductSale::where('branch_id', $branch->id)->exists()
|
||||||
|| \App\Models\Receptionist::where('branch_id', $branch->id)->exists();
|
|| \App\Models\Receptionist::where('branch_id', $branch->id)->exists();
|
||||||
$branch->is_deletable = !$inUse;
|
$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);
|
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')]);
|
return response()->json(['message' => 'Branch updated successfully', 'branch' => $branch->load('documents')]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -104,8 +104,18 @@ public function getSalaryHistory(Request $request)
|
|||||||
$query = Expense::with(['branch'])
|
$query = Expense::with(['branch'])
|
||||||
->where('expense_category_id', $salaryCategory->id);
|
->where('expense_category_id', $salaryCategory->id);
|
||||||
|
|
||||||
if ($user->isReceptionist()) {
|
$branchId = $user->isReceptionist() ? $user->branch_id : $request->query('branch_id');
|
||||||
$query->where('branch_id', $user->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();
|
$expenses = $query->orderBy('date', 'desc')->get();
|
||||||
|
|||||||
@ -145,7 +145,14 @@ public function getSales(Request $request)
|
|||||||
$query->where('date', '<=', $endDate);
|
$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)
|
public function storeSale(Request $request)
|
||||||
|
|||||||
@ -22,8 +22,12 @@ public function getProfitReport(Request $request)
|
|||||||
$branchId = $user->isReceptionist() ? $user->branch_id : $request->query('branch_id');
|
$branchId = $user->isReceptionist() ? $user->branch_id : $request->query('branch_id');
|
||||||
$startDate = $request->query('start_date');
|
$startDate = $request->query('start_date');
|
||||||
$endDate = $request->query('end_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) {
|
if ($branchId) {
|
||||||
$query->where('branch_id', $branchId);
|
$query->where('branch_id', $branchId);
|
||||||
}
|
}
|
||||||
@ -34,24 +38,18 @@ public function getProfitReport(Request $request)
|
|||||||
$query->where('date', '<=', $endDate);
|
$query->where('date', '<=', $endDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stats from the same filtered query
|
||||||
$totalCredits = (clone $query)->sum('credit');
|
$totalCredits = (clone $query)->sum('credit');
|
||||||
$totalDebits = (clone $query)->sum('debit');
|
$totalDebits = (clone $query)->sum('debit');
|
||||||
|
|
||||||
// Fetch All Ledger Transactions for the breakdown
|
// Fetch Paginated Transactions from the same filtered query
|
||||||
$accounts = Account::where(function($q) {
|
$allTransactions = $query->where(function($q) {
|
||||||
$q->where('credit', '>', 0)->orWhere('debit', '>', 0);
|
$q->where('credit', '>', 0)->orWhere('debit', '>', 0);
|
||||||
});
|
})
|
||||||
|
->orderBy('date', 'desc')
|
||||||
if ($branchId) {
|
->orderBy('time', 'desc')
|
||||||
$accounts->where('branch_id', $branchId);
|
->orderBy('id', 'desc')
|
||||||
}
|
->get()
|
||||||
if ($startDate) {
|
|
||||||
$accounts->where('date', '>=', $startDate);
|
|
||||||
}
|
|
||||||
if ($endDate) {
|
|
||||||
$accounts->where('date', '<=', $endDate);
|
|
||||||
}
|
|
||||||
$accounts = $accounts->get()
|
|
||||||
->map(function($a) {
|
->map(function($a) {
|
||||||
$isAdjusted = false;
|
$isAdjusted = false;
|
||||||
$originalAmount = $a->credit > 0 ? $a->credit : $a->debit;
|
$originalAmount = $a->credit > 0 ? $a->credit : $a->debit;
|
||||||
@ -68,41 +66,22 @@ public function getProfitReport(Request $request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
'id' => $a->id,
|
||||||
'date' => $a->date,
|
'date' => $a->date,
|
||||||
|
'time' => $a->time || '00:00:00',
|
||||||
'type' => $a->credit > 0 ? 'Income' : 'Expense',
|
'type' => $a->credit > 0 ? 'Income' : 'Expense',
|
||||||
'category' => $a->type,
|
'category' => $a->type,
|
||||||
'description' => $a->description,
|
'description' => $a->description,
|
||||||
'amount' => $a->credit > 0 ? $a->credit : $a->debit,
|
'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,
|
'is_adjusted' => $isAdjusted,
|
||||||
'original_amount' => $originalAmount,
|
'original_amount' => $originalAmount,
|
||||||
'remarks' => $remarks
|
'remarks' => $remarks
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
$expensesQuery = Expense::with('category', 'branch');
|
$totalTransactions = $allTransactions->count();
|
||||||
if ($branchId) {
|
$paginatedTransactions = $allTransactions->forPage($page, $perPage)->values();
|
||||||
$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();
|
|
||||||
|
|
||||||
$lowStockCount = \App\Models\Product::query();
|
$lowStockCount = \App\Models\Product::query();
|
||||||
if ($branchId) {
|
if ($branchId) {
|
||||||
@ -121,9 +100,11 @@ public function getProfitReport(Request $request)
|
|||||||
->whereBetween('date', [$monthStart->toDateString(), $monthEnd->toDateString()])
|
->whereBetween('date', [$monthStart->toDateString(), $monthEnd->toDateString()])
|
||||||
->sum('credit');
|
->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()])
|
->whereBetween('date', [$monthStart->toDateString(), $monthEnd->toDateString()])
|
||||||
->sum('amount');
|
->sum('debit');
|
||||||
|
|
||||||
$trend[] = [
|
$trend[] = [
|
||||||
'month' => $monthStart->format('M'),
|
'month' => $monthStart->format('M'),
|
||||||
@ -139,7 +120,13 @@ public function getProfitReport(Request $request)
|
|||||||
'total_expense' => $totalDebits,
|
'total_expense' => $totalDebits,
|
||||||
'net_profit' => $totalCredits - $totalDebits,
|
'net_profit' => $totalCredits - $totalDebits,
|
||||||
'low_stock_count' => $lowStockCount,
|
'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
|
'trend' => $trend
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -151,6 +138,8 @@ public function getExpiryReminders(Request $request)
|
|||||||
if (!$user) return response()->json(['message' => 'Unauthenticated'], 401);
|
if (!$user) return response()->json(['message' => 'Unauthenticated'], 401);
|
||||||
|
|
||||||
$branchId = $user->isReceptionist() ? $user->branch_id : $request->query('branch_id');
|
$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')
|
$staffDocsQuery = \App\Models\StaffDocument::with('staff.branch')
|
||||||
->whereNotNull('expiry_date');
|
->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()
|
$staffDocs = $staffDocsQuery->get()
|
||||||
->filter(function($doc) use ($today) {
|
->filter(function($doc) use ($today) {
|
||||||
$expiryDate = Carbon::parse($doc->expiry_date)->startOfDay();
|
$expiryDate = Carbon::parse($doc->expiry_date)->startOfDay();
|
||||||
@ -192,6 +188,13 @@ public function getExpiryReminders(Request $request)
|
|||||||
$branchDocsQuery->where('branch_id', $branchId);
|
$branchDocsQuery->where('branch_id', $branchId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($startDate) {
|
||||||
|
$branchDocsQuery->where('expiry_date', '>=', $startDate);
|
||||||
|
}
|
||||||
|
if ($endDate) {
|
||||||
|
$branchDocsQuery->where('expiry_date', '<=', $endDate);
|
||||||
|
}
|
||||||
|
|
||||||
$branchReminders = $branchDocsQuery->get()
|
$branchReminders = $branchDocsQuery->get()
|
||||||
->filter(function($doc) use ($today) {
|
->filter(function($doc) use ($today) {
|
||||||
$expiryDate = Carbon::parse($doc->expiry_date);
|
$expiryDate = Carbon::parse($doc->expiry_date);
|
||||||
@ -216,7 +219,7 @@ public function getExpiryReminders(Request $request)
|
|||||||
});
|
});
|
||||||
|
|
||||||
return response()->json([
|
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([
|
return response()->json([
|
||||||
'total_invested' => $totalInvested,
|
'total_invested' => $totalInvested,
|
||||||
'total_roi_returned' => $totalROIReturned,
|
'total_roi_returned' => $totalROIReturned,
|
||||||
'investors' => $reportData
|
'investors' => collect($reportData)->sortByDesc('investment_date')->values()
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,7 +63,7 @@ export default function List() {
|
|||||||
{ header: 'Manager', key: 'manager_name' },
|
{ header: 'Manager', key: 'manager_name' },
|
||||||
{
|
{
|
||||||
header: 'Revenue (AED)',
|
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',
|
header: 'Status',
|
||||||
|
|||||||
@ -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>
|
<label className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-2 ml-1">Branch</label>
|
||||||
<select
|
<select
|
||||||
required
|
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}
|
value={formData.branch_id}
|
||||||
onChange={e => setFormData({...formData, branch_id: e.target.value, items: [], amount: ''})}
|
onChange={e => setFormData({...formData, branch_id: e.target.value, items: [], amount: ''})}
|
||||||
>
|
>
|
||||||
<option value="">Select Branch</option>
|
<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>
|
</select>
|
||||||
</div>
|
</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>
|
<label className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-2 ml-1">Collection Type</label>
|
||||||
<select
|
<select
|
||||||
required
|
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}
|
value={formData.collection_type_id}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
const typeId = e.target.value;
|
const typeId = e.target.value;
|
||||||
@ -250,7 +254,11 @@ export default function AddCollectionModal({ isOpen, onClose, onSave, branches,
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="">Select Type</option>
|
<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>
|
</select>
|
||||||
</div>
|
</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>
|
<label className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-2 ml-1">Payment Method</label>
|
||||||
<select
|
<select
|
||||||
required
|
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}
|
value={formData.payment_method}
|
||||||
onChange={e => setFormData({...formData, payment_method: e.target.value})}
|
onChange={e => setFormData({...formData, payment_method: e.target.value})}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
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) => {
|
const formatCurrency = (val) => {
|
||||||
return new Intl.NumberFormat('en-AE', { style: 'currency', currency: 'AED' }).format(val);
|
return new Intl.NumberFormat('en-AE', { style: 'currency', currency: 'AED' }).format(val);
|
||||||
};
|
};
|
||||||
@ -68,6 +68,44 @@ export default function AccountsTable({ data = [] }) {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,16 +8,15 @@ export default function Dashboard() {
|
|||||||
total_income: 0,
|
total_income: 0,
|
||||||
total_expense: 0,
|
total_expense: 0,
|
||||||
net_profit: 0,
|
net_profit: 0,
|
||||||
|
low_stock_count: 0,
|
||||||
transactions: []
|
transactions: []
|
||||||
});
|
});
|
||||||
const [branches, setBranches] = useState([]);
|
const [branches, setBranches] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [filterBranch, setFilterBranch] = useState('');
|
const [filterBranch, setFilterBranch] = useState('');
|
||||||
const [startDate, setStartDate] = useState(() => {
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const d = new Date();
|
const [paginationData, setPaginationData] = useState(null);
|
||||||
d.setMonth(d.getMonth() - 1);
|
const [startDate, setStartDate] = useState(new Date().toISOString().split('T')[0]);
|
||||||
return d.toISOString().split('T')[0];
|
|
||||||
});
|
|
||||||
const [endDate, setEndDate] = useState(new Date().toISOString().split('T')[0]);
|
const [endDate, setEndDate] = useState(new Date().toISOString().split('T')[0]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -32,7 +31,8 @@ export default function Dashboard() {
|
|||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
branch_id: filterBranch,
|
branch_id: filterBranch,
|
||||||
start_date: startDate,
|
start_date: startDate,
|
||||||
end_date: endDate
|
end_date: endDate,
|
||||||
|
page: currentPage
|
||||||
});
|
});
|
||||||
fetch(`/api/reports/profit?${params}`)
|
fetch(`/api/reports/profit?${params}`)
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
@ -41,14 +41,21 @@ export default function Dashboard() {
|
|||||||
total_income: data.total_income,
|
total_income: data.total_income,
|
||||||
total_expense: data.total_expense,
|
total_expense: data.total_expense,
|
||||||
net_profit: data.net_profit,
|
net_profit: data.net_profit,
|
||||||
|
low_stock_count: data.low_stock_count,
|
||||||
transactions: data.transactions || []
|
transactions: data.transactions || []
|
||||||
});
|
});
|
||||||
|
setPaginationData(data.pagination);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error("Error fetching dashboard stats:", err);
|
console.error("Error fetching dashboard stats:", err);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
|
}, [filterBranch, startDate, endDate, currentPage]);
|
||||||
|
|
||||||
|
// Reset page when filters change
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentPage(1);
|
||||||
}, [filterBranch, startDate, endDate]);
|
}, [filterBranch, startDate, endDate]);
|
||||||
|
|
||||||
const formatCurrency = (val) => {
|
const formatCurrency = (val) => {
|
||||||
@ -69,10 +76,14 @@ export default function Dashboard() {
|
|||||||
<select
|
<select
|
||||||
value={filterBranch}
|
value={filterBranch}
|
||||||
onChange={(e) => setFilterBranch(e.target.value)}
|
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>
|
<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>
|
</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" />
|
<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>
|
</div>
|
||||||
@ -104,33 +115,44 @@ export default function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stat Cards Grid */}
|
{/* 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
|
<StatCard
|
||||||
title="Aggregated amount of all credits"
|
title="Total Income Collected"
|
||||||
subtitle="Total Credited"
|
subtitle="Total Credits"
|
||||||
value={loading ? "..." : formatCurrency(stats.total_income)}
|
value={loading ? "..." : formatCurrency(stats.total_income)}
|
||||||
color="green"
|
color="green"
|
||||||
icon={DollarSign}
|
icon={DollarSign}
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
title="Aggregated amount of all debits"
|
title="Total Expenses Paid"
|
||||||
subtitle="Total Debited"
|
subtitle="Total Debits"
|
||||||
value={loading ? "..." : formatCurrency(stats.total_expense)}
|
value={loading ? "..." : formatCurrency(stats.total_expense)}
|
||||||
color="red"
|
color="red"
|
||||||
icon={TrendingDown}
|
icon={TrendingDown}
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
title={loading ? "Loading..." : formatCurrency(stats.net_profit)}
|
title="Total Net Savings"
|
||||||
subtitle="Net Profit"
|
subtitle="Net Profit"
|
||||||
value={loading ? "..." : formatCurrency(stats.net_profit)}
|
value={loading ? "..." : formatCurrency(stats.net_profit)}
|
||||||
color="blue"
|
color="blue"
|
||||||
icon={TrendingUp}
|
icon={TrendingUp}
|
||||||
/>
|
/>
|
||||||
|
<StatCard
|
||||||
|
title="Items Below Reorder Level"
|
||||||
|
subtitle="Low Stock Products"
|
||||||
|
value={loading ? "..." : stats.low_stock_count}
|
||||||
|
color="red"
|
||||||
|
icon={TrendingDown}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Content Area */}
|
{/* Main Content Area */}
|
||||||
<div className="grid grid-cols-1 gap-8">
|
<div className="grid grid-cols-1 gap-8">
|
||||||
<AccountsTable data={stats.transactions} />
|
<AccountsTable
|
||||||
|
data={stats.transactions}
|
||||||
|
pagination={paginationData}
|
||||||
|
onPageChange={(p) => setCurrentPage(p)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -69,7 +69,9 @@ export default function ExpenseList() {
|
|||||||
fetchExpenses();
|
fetchExpenses();
|
||||||
fetchMetadata();
|
fetchMetadata();
|
||||||
fetchPendingSalaries();
|
fetchPendingSalaries();
|
||||||
fetchPendingROIs();
|
if (!isReceptionist) {
|
||||||
|
fetchPendingROIs();
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -185,6 +187,9 @@ export default function ExpenseList() {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
setToast({ message: 'Salaries released successfully!', type: 'success' });
|
setToast({ message: 'Salaries released successfully!', type: 'success' });
|
||||||
setIsBulkModalOpen(false);
|
setIsBulkModalOpen(false);
|
||||||
|
if (!isReceptionist) {
|
||||||
|
setFilterBranch('');
|
||||||
|
}
|
||||||
fetchPendingSalaries();
|
fetchPendingSalaries();
|
||||||
fetchExpenses();
|
fetchExpenses();
|
||||||
} else {
|
} else {
|
||||||
@ -337,9 +342,10 @@ export default function ExpenseList() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tabs */}
|
|
||||||
<div className="flex items-center gap-8 border-b border-gray-100 mb-6">
|
<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
|
<button
|
||||||
key={tab}
|
key={tab}
|
||||||
onClick={() => setActiveTab(tab)}
|
onClick={() => setActiveTab(tab)}
|
||||||
|
|||||||
@ -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>
|
<label className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-2 ml-1">Category</label>
|
||||||
<select
|
<select
|
||||||
required
|
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}
|
value={formData.product_category_id}
|
||||||
onChange={e => setFormData({...formData, product_category_id: e.target.value})}
|
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.length === 0 && <option disabled className="whitespace-normal">No active categories found. Please add them in Masters.</option>}
|
||||||
{categories.map(c => (
|
{categories.map(c => (
|
||||||
<option key={c.id} value={c.id} className="whitespace-normal" title={c.name}>
|
<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>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</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>
|
<label className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-2 ml-1">Branch</label>
|
||||||
<select
|
<select
|
||||||
required
|
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}
|
value={formData.branch_id}
|
||||||
onChange={e => setFormData({...formData, branch_id: e.target.value})}
|
onChange={e => setFormData({...formData, branch_id: e.target.value})}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -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>
|
<label className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-2 ml-1">Reason</label>
|
||||||
<select
|
<select
|
||||||
required
|
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}
|
value={formData.reason}
|
||||||
onChange={e => setFormData({...formData, reason: e.target.value})}
|
onChange={e => setFormData({...formData, reason: e.target.value})}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -160,14 +160,18 @@ export default function NewSaleModal({ isOpen, onClose, onSave, branches, produc
|
|||||||
</div>
|
</div>
|
||||||
{window.__APP_DATA__?.role !== 'receptionist' && (
|
{window.__APP_DATA__?.role !== 'receptionist' && (
|
||||||
<select
|
<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}
|
value={selectedBranch}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
setSelectedBranch(e.target.value);
|
setSelectedBranch(e.target.value);
|
||||||
setCart([]);
|
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>
|
</select>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -18,16 +18,19 @@ import {
|
|||||||
Clock,
|
Clock,
|
||||||
Shield,
|
Shield,
|
||||||
Building,
|
Building,
|
||||||
AlertCircle
|
AlertCircle,
|
||||||
|
RotateCcw
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
export default function ReportIndex() {
|
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 [profitData, setProfitData] = useState(null);
|
||||||
const [expiryReminders, setExpiryReminders] = useState([]);
|
const [expiryReminders, setExpiryReminders] = useState([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [selectedBranch, setSelectedBranch] = useState('');
|
const [selectedBranch, setSelectedBranch] = useState(isReceptionist ? (window.__APP_DATA__?.branch?.id || '') : '');
|
||||||
const [startDate, setStartDate] = useState('');
|
const [startDate, setStartDate] = useState('');
|
||||||
const [endDate, setEndDate] = useState('');
|
const [endDate, setEndDate] = useState('');
|
||||||
const [branches, setBranches] = useState([]);
|
const [branches, setBranches] = useState([]);
|
||||||
@ -43,13 +46,9 @@ export default function ReportIndex() {
|
|||||||
const [salaryData, setSalaryData] = useState([]);
|
const [salaryData, setSalaryData] = useState([]);
|
||||||
const [selectedItem, setSelectedItem] = useState(null);
|
const [selectedItem, setSelectedItem] = useState(null);
|
||||||
|
|
||||||
const tabs = [
|
const tabs = isReceptionist
|
||||||
'Profit Report', 'Expense Report', 'Collection Report',
|
? ['Expense Report', 'Collection Report', 'Low Stock Report', 'Inventory Report', 'Product Sales', 'Expiry Reminders']
|
||||||
'Low Stock Report', 'Inventory Report', 'Product Sales',
|
: ['Profit Report', 'Expense Report', 'Collection Report', 'Low Stock Report', 'Inventory Report', 'Product Sales', 'Investment Report', 'Salary Report', 'Expiry Reminders'];
|
||||||
'Investment Report', 'Salary Report', 'Expiry Reminders'
|
|
||||||
];
|
|
||||||
|
|
||||||
const isReceptionist = window.__APP_DATA__?.role === 'receptionist';
|
|
||||||
|
|
||||||
const buildQueryString = () => {
|
const buildQueryString = () => {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
@ -59,6 +58,15 @@ export default function ReportIndex() {
|
|||||||
return params.toString();
|
return params.toString();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetFilters = () => {
|
||||||
|
setStartDate('');
|
||||||
|
setEndDate('');
|
||||||
|
setSearchQuery('');
|
||||||
|
if (!isReceptionist) {
|
||||||
|
setSelectedBranch('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchMetadata();
|
fetchMetadata();
|
||||||
}, []);
|
}, []);
|
||||||
@ -369,28 +377,32 @@ export default function ReportIndex() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</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>
|
||||||
<div className="min-w-[160px]">
|
<input
|
||||||
<label className="block text-[9px] font-black text-[#A3AED0] uppercase tracking-widest mb-1.5 ml-1">From Date</label>
|
type="date"
|
||||||
<input
|
className="w-full px-4 py-2.5 bg-[#F4F7FE] border-none rounded-xl text-sm font-bold text-[#1B254B] focus:ring-2 focus:ring-[#E31B1B]/20 outline-none"
|
||||||
type="date"
|
value={startDate}
|
||||||
className="w-full px-4 py-2.5 bg-[#F4F7FE] border-none rounded-xl text-sm font-bold text-[#1B254B] focus:ring-2 focus:ring-[#E31B1B]/20 outline-none"
|
onChange={(e) => setStartDate(e.target.value)}
|
||||||
value={startDate}
|
/>
|
||||||
onChange={(e) => setStartDate(e.target.value)}
|
</div>
|
||||||
/>
|
<div className="min-w-[160px]">
|
||||||
</div>
|
<label className="block text-[9px] font-black text-[#A3AED0] uppercase tracking-widest mb-1.5 ml-1">To Date</label>
|
||||||
<div className="min-w-[160px]">
|
<input
|
||||||
<label className="block text-[9px] font-black text-[#A3AED0] uppercase tracking-widest mb-1.5 ml-1">To Date</label>
|
type="date"
|
||||||
<input
|
className="w-full px-4 py-2.5 bg-[#F4F7FE] border-none rounded-xl text-sm font-bold text-[#1B254B] focus:ring-2 focus:ring-[#E31B1B]/20 outline-none"
|
||||||
type="date"
|
value={endDate}
|
||||||
className="w-full px-4 py-2.5 bg-[#F4F7FE] border-none rounded-xl text-sm font-bold text-[#1B254B] focus:ring-2 focus:ring-[#E31B1B]/20 outline-none"
|
onChange={(e) => setEndDate(e.target.value)}
|
||||||
value={endDate}
|
/>
|
||||||
onChange={(e) => setEndDate(e.target.value)}
|
</div>
|
||||||
/>
|
<button
|
||||||
</div>
|
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>
|
||||||
</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-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.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-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>
|
</tr>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -382,9 +382,20 @@ export default function StaffEdit({ id }) {
|
|||||||
<option key={r.id} value={r.name}>{r.name}</option>
|
<option key={r.id} value={r.name}>{r.name}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{!isReceptionist && (
|
||||||
<label className="block text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Joining Date *</label>
|
<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" />
|
<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" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
// Header and SubHeader are now part of the global Layout
|
// Header and SubHeader are now part of the global Layout
|
||||||
import {
|
import {
|
||||||
Wallet,
|
|
||||||
TrendingUp,
|
|
||||||
ArrowUpRight,
|
|
||||||
ArrowDownRight,
|
|
||||||
DollarSign,
|
DollarSign,
|
||||||
|
TrendingUp,
|
||||||
|
Calendar,
|
||||||
|
ChevronDown,
|
||||||
Activity,
|
Activity,
|
||||||
ShoppingCart,
|
ShoppingCart,
|
||||||
Package,
|
Package,
|
||||||
@ -23,11 +22,29 @@ export default function ReceptionistDashboard() {
|
|||||||
});
|
});
|
||||||
const [transactions, setTransactions] = useState([]);
|
const [transactions, setTransactions] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
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(() => {
|
useEffect(() => {
|
||||||
const fetchDashboardData = async () => {
|
const fetchDashboardData = async () => {
|
||||||
|
setLoading(true);
|
||||||
try {
|
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();
|
const data = await response.json();
|
||||||
|
|
||||||
setStats({
|
setStats({
|
||||||
@ -45,7 +62,7 @@ export default function ReceptionistDashboard() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fetchDashboardData();
|
fetchDashboardData();
|
||||||
}, []);
|
}, [filterBranch, startDate, endDate]);
|
||||||
|
|
||||||
const StatCard = ({ title, amount, icon: Icon, color, trend, iconColor, bgColor, textColor, label }) => (
|
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`}>
|
<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">
|
<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">
|
<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 gap-2">
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
||||||
<h1 className="text-4xl font-black text-gray-900 tracking-tight flex items-center gap-4">
|
<div className="flex flex-col gap-2">
|
||||||
Receptionist Dashboard
|
<h1 className="text-4xl font-black text-gray-900 tracking-tight flex items-center gap-4">
|
||||||
</h1>
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Stats Grid */}
|
{/* Stats Grid */}
|
||||||
@ -188,7 +250,7 @@ export default function ReceptionistDashboard() {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-50">
|
<tbody className="divide-y divide-gray-50">
|
||||||
{transactions.length > 0 ? (
|
{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">
|
<tr key={idx} className="hover:bg-gray-50/30 transition-colors">
|
||||||
<td className="px-8 py-5">
|
<td className="px-8 py-5">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
@ -201,15 +263,15 @@ export default function ReceptionistDashboard() {
|
|||||||
</td>
|
</td>
|
||||||
<td className="px-8 py-5">
|
<td className="px-8 py-5">
|
||||||
<span className={`px-2.5 py-1 rounded-full text-[10px] font-black shadow-sm uppercase tracking-wider ${
|
<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>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-8 py-5 text-right">
|
<td className="px-8 py-5 text-right">
|
||||||
<span className={`text-sm font-black ${tx.debit > 0 ? 'text-red-500' : 'text-emerald-500'}`}>
|
<span className={`text-sm font-black ${tx.type === 'Expense' ? 'text-red-500' : 'text-emerald-500'}`}>
|
||||||
{tx.debit > 0 ? '-' : '+'}
|
{tx.type === 'Expense' ? '-' : '+'}
|
||||||
{(tx.debit || tx.credit || 0).toLocaleString('en-AE', { minimumFractionDigits: 2 })}
|
{(tx.amount || 0).toLocaleString('en-AE', { minimumFractionDigits: 2 })}
|
||||||
<span className="text-[10px] ml-1 uppercase">AED</span>
|
<span className="text-[10px] ml-1 uppercase">AED</span>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@ -28,6 +28,7 @@ export default function POS() {
|
|||||||
const [branches, setBranches] = useState([]);
|
const [branches, setBranches] = useState([]);
|
||||||
const [selectedBranch, setSelectedBranch] = useState(window.__APP_DATA__?.user?.branch_id || '');
|
const [selectedBranch, setSelectedBranch] = useState(window.__APP_DATA__?.user?.branch_id || '');
|
||||||
const [toast, setToast] = useState(null);
|
const [toast, setToast] = useState(null);
|
||||||
|
const [showSuccessModal, setShowSuccessModal] = useState(false);
|
||||||
|
|
||||||
const showToast = (message, type = 'success') => {
|
const showToast = (message, type = 'success') => {
|
||||||
setToast({ message, type });
|
setToast({ message, type });
|
||||||
@ -168,6 +169,7 @@ export default function POS() {
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
setSuccess(true);
|
setSuccess(true);
|
||||||
|
setShowSuccessModal(true);
|
||||||
setCart([]);
|
setCart([]);
|
||||||
setAdjustmentRemarks('');
|
setAdjustmentRemarks('');
|
||||||
setTimeout(() => setSuccess(false), 3000);
|
setTimeout(() => setSuccess(false), 3000);
|
||||||
@ -416,6 +418,25 @@ export default function POS() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</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>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,8 +13,8 @@ export default function ReceptionistReportIndex() {
|
|||||||
const [activeTab, setActiveTab] = useState('Collections');
|
const [activeTab, setActiveTab] = useState('Collections');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [fromDate, setFromDate] = useState('2026-02-06'); // Defaulting based on screenshot
|
const [fromDate, setFromDate] = useState(new Date().toISOString().split('T')[0]);
|
||||||
const [toDate, setToDate] = useState('2026-03-08');
|
const [toDate, setToDate] = useState(new Date().toISOString().split('T')[0]);
|
||||||
const [method, setMethod] = useState('All Methods');
|
const [method, setMethod] = useState('All Methods');
|
||||||
const [type, setType] = useState('All Types');
|
const [type, setType] = useState('All Types');
|
||||||
const [expenseType, setExpenseType] = useState('All Types');
|
const [expenseType, setExpenseType] = useState('All Types');
|
||||||
|
|||||||
@ -169,7 +169,7 @@ function MainApp() {
|
|||||||
const id = path.split('/').pop();
|
const id = path.split('/').pop();
|
||||||
component = <InvestorView id={id} />;
|
component = <InvestorView id={id} />;
|
||||||
} else if (path === '/receptionist/reports') {
|
} else if (path === '/receptionist/reports') {
|
||||||
component = <ReceptionistReportIndex />;
|
component = <ReportIndex />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (component) {
|
if (component) {
|
||||||
|
|||||||
@ -118,5 +118,8 @@
|
|||||||
Route::get('/receptionist/expenses', [OwnerController::class, 'index']);
|
Route::get('/receptionist/expenses', [OwnerController::class, 'index']);
|
||||||
Route::get('/receptionist/inventory', [OwnerController::class, 'index']);
|
Route::get('/receptionist/inventory', [OwnerController::class, 'index']);
|
||||||
Route::get('/receptionist/staff', [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']);
|
Route::get('/receptionist/reports', [OwnerController::class, 'index']);
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user