379 lines
24 KiB
JavaScript
379 lines
24 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
||
import {
|
||
Search,
|
||
Download,
|
||
Calendar,
|
||
DollarSign,
|
||
ArrowDownRight,
|
||
Filter,
|
||
CreditCard
|
||
} from 'lucide-react';
|
||
|
||
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 [method, setMethod] = useState('All Methods');
|
||
const [type, setType] = useState('All Types');
|
||
const [expenseType, setExpenseType] = useState('All Types');
|
||
|
||
const [collections, setCollections] = useState([]);
|
||
const [expenses, setExpenses] = useState([]);
|
||
const [userBranch, setUserBranch] = useState('Downtown Main'); // Fallback
|
||
const [selectedItem, setSelectedItem] = useState(null);
|
||
|
||
useEffect(() => {
|
||
const user = window.__APP_DATA__;
|
||
if (user && user.branch) {
|
||
setUserBranch(user.branch.name);
|
||
}
|
||
fetchData();
|
||
}, [activeTab, fromDate, toDate]);
|
||
|
||
const fetchData = async () => {
|
||
setLoading(true);
|
||
try {
|
||
if (activeTab === 'Collections') {
|
||
const query = new URLSearchParams({
|
||
start_date: fromDate,
|
||
end_date: toDate
|
||
});
|
||
const res = await fetch(`/api/collections?${query}`);
|
||
const data = await res.json();
|
||
setCollections(data || []);
|
||
} else {
|
||
const res = await fetch('/api/expenses');
|
||
const data = await res.json();
|
||
// Client-side date filtering for expenses if backend doesn't support it yet
|
||
const filtered = data.filter(e => {
|
||
const d = e.date;
|
||
return d >= fromDate && d <= toDate;
|
||
});
|
||
setExpenses(filtered || []);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error fetching reports data:', error);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const filteredCollections = collections.filter(item => {
|
||
const matchesSearch = item.remarks?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||
item.type?.name?.toLowerCase().includes(searchQuery.toLowerCase());
|
||
const matchesMethod = method === 'All Methods' || item.payment_method === method;
|
||
const matchesType = type === 'All Types' || item.type?.name === type;
|
||
return matchesSearch && matchesMethod && matchesType;
|
||
});
|
||
|
||
const filteredExpenses = expenses.filter(item => {
|
||
const matchesSearch = item.remarks?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||
item.category?.name?.toLowerCase().includes(searchQuery.toLowerCase());
|
||
const matchesType = expenseType === 'All Types' || item.expense_type === expenseType;
|
||
return matchesSearch && matchesType;
|
||
});
|
||
|
||
const totalCollections = filteredCollections.reduce((sum, item) => sum + parseFloat(item.amount), 0);
|
||
const totalExpenses = filteredExpenses.reduce((sum, item) => sum + parseFloat(item.amount), 0);
|
||
|
||
return (
|
||
<main className="max-w-[1700px] mx-auto p-4 md:p-10 space-y-8 animate-in fade-in duration-500">
|
||
{/* Header Section */}
|
||
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
||
<div>
|
||
<h1 className="text-4xl font-black text-[#1B254B] tracking-tight">Financial Reports</h1>
|
||
<p className="text-[#A3AED0] font-bold mt-1 uppercase tracking-wider text-xs">Branch: <span className="text-[#1B254B]">{userBranch}</span></p>
|
||
</div>
|
||
<button className="flex items-center gap-2 px-6 py-3 bg-[#00C566] text-white rounded-xl text-sm font-bold hover:shadow-lg hover:shadow-[#00C566]/20 transition-all active:scale-95">
|
||
<Download size={20} />
|
||
Export Report
|
||
</button>
|
||
</div>
|
||
|
||
{/* Filters Bar */}
|
||
<div className="bg-white p-6 rounded-[2rem] shadow-sm border border-[#F4F7FE] grid grid-cols-1 md:grid-cols-5 gap-6 items-end">
|
||
<div className="space-y-2">
|
||
<label className="text-[10px] font-black text-[#A3AED0] uppercase tracking-widest ml-1">Search</label>
|
||
<div className="relative group">
|
||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 text-[#A3AED0] group-focus-within:text-red-500 transition-colors" size={18} />
|
||
<input
|
||
type="text"
|
||
placeholder="Search records..."
|
||
className="w-full pl-12 pr-4 py-3 bg-[#F9FAFB] border border-[#E0E5F2] rounded-xl text-sm font-bold text-[#1B254B] focus:ring-2 focus:ring-red-500/10 focus:border-red-500 outline-none transition-all"
|
||
value={searchQuery}
|
||
onChange={(e) => setSearchQuery(e.target.value)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<label className="text-[10px] font-black text-[#A3AED0] uppercase tracking-widest ml-1">From Date</label>
|
||
<div className="relative">
|
||
<Calendar className="absolute left-4 top-1/2 -translate-y-1/2 text-[#A3AED0]" size={18} />
|
||
<input
|
||
type="date"
|
||
className="w-full pl-12 pr-4 py-3 bg-[#F9FAFB] border border-[#E0E5F2] rounded-xl text-sm font-bold text-[#1B254B] outline-none"
|
||
value={fromDate}
|
||
onChange={(e) => setFromDate(e.target.value)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<label className="text-[10px] font-black text-[#A3AED0] uppercase tracking-widest ml-1">To Date</label>
|
||
<div className="relative">
|
||
<Calendar className="absolute left-4 top-1/2 -translate-y-1/2 text-[#A3AED0]" size={18} />
|
||
<input
|
||
type="date"
|
||
className="w-full pl-12 pr-4 py-3 bg-[#F9FAFB] border border-[#E0E5F2] rounded-xl text-sm font-bold text-[#1B254B] outline-none"
|
||
value={toDate}
|
||
onChange={(e) => setToDate(e.target.value)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{activeTab === 'Collections' ? (
|
||
<>
|
||
<div className="space-y-2">
|
||
<label className="text-[10px] font-black text-[#A3AED0] uppercase tracking-widest ml-1">Method</label>
|
||
<select
|
||
className="w-full px-4 py-3 bg-[#F9FAFB] border border-[#E0E5F2] rounded-xl text-sm font-bold text-[#1B254B] outline-none appearance-none"
|
||
value={method}
|
||
onChange={(e) => setMethod(e.target.value)}
|
||
>
|
||
<option>All Methods</option>
|
||
<option>Cash</option>
|
||
<option>Online</option>
|
||
<option>Card</option>
|
||
</select>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<label className="text-[10px] font-black text-[#A3AED0] uppercase tracking-widest ml-1">Type</label>
|
||
<select
|
||
className="w-full px-4 py-3 bg-[#F9FAFB] border border-[#E0E5F2] rounded-xl text-sm font-bold text-[#1B254B] outline-none appearance-none"
|
||
value={type}
|
||
onChange={(e) => setType(e.target.value)}
|
||
>
|
||
<option>All Types</option>
|
||
<option>Product sale</option>
|
||
<option>PT fee</option>
|
||
<option>Membership</option>
|
||
</select>
|
||
</div>
|
||
</>
|
||
) : (
|
||
<div className="space-y-2 md:col-span-2">
|
||
<label className="text-[10px] font-black text-[#A3AED0] uppercase tracking-widest ml-1">Expense Type</label>
|
||
<select
|
||
className="w-full px-4 py-3 bg-[#F9FAFB] border border-[#E0E5F2] rounded-xl text-sm font-bold text-[#1B254B] outline-none appearance-none"
|
||
value={expenseType}
|
||
onChange={(e) => setExpenseType(e.target.value)}
|
||
>
|
||
<option>All Types</option>
|
||
<option>Account</option>
|
||
<option>Petty Cash</option>
|
||
</select>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Tabs Sidebar/Style */}
|
||
<div className="flex items-center gap-8 border-b border-gray-100">
|
||
{['Collections', 'Expenses'].map((tab) => (
|
||
<button
|
||
key={tab}
|
||
onClick={() => setActiveTab(tab)}
|
||
className={`pb-4 text-sm font-black transition-all relative ${
|
||
activeTab === tab
|
||
? 'text-red-500'
|
||
: 'text-[#A3AED0] hover:text-[#1B254B]'
|
||
}`}
|
||
>
|
||
{tab}
|
||
{activeTab === tab && (
|
||
<div className="absolute bottom-0 left-0 w-full h-1 bg-red-500 rounded-t-full" />
|
||
)}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* Content Area */}
|
||
{activeTab === 'Collections' ? (
|
||
<div className="space-y-6 animate-in slide-in-from-bottom-2 duration-500">
|
||
{/* Summary Banner */}
|
||
<div className="bg-[#F0FDF4] border border-emerald-100 rounded-[1.5rem] p-6 flex items-center justify-between">
|
||
<div className="flex items-center gap-6">
|
||
<div className="w-14 h-14 bg-white rounded-full flex items-center justify-center text-[#22C55E] shadow-sm">
|
||
<DollarSign size={28} />
|
||
</div>
|
||
<div>
|
||
<p className="text-[10px] font-black text-emerald-600 uppercase tracking-widest mb-1">Total Collections (Filtered)</p>
|
||
<h3 className="text-3xl font-black text-[#1B254B] tracking-tight">{totalCollections.toLocaleString('en-AE', { minimumFractionDigits: 2 })} AED</h3>
|
||
</div>
|
||
</div>
|
||
<div className="text-right">
|
||
<p className="text-[10px] font-black text-emerald-600 uppercase tracking-widest">Records: {filteredCollections.length}</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Table */}
|
||
<div className="bg-white rounded-[2rem] shadow-sm border border-[#F4F7FE] overflow-hidden">
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full text-left">
|
||
<thead>
|
||
<tr className="bg-[#F9FAFB]">
|
||
<th className="px-8 py-5 text-[10px] font-black text-[#A3AED0] uppercase tracking-[0.2em] border-b border-[#F4F7FE]">Date</th>
|
||
<th className="px-8 py-5 text-[10px] font-black text-[#A3AED0] uppercase tracking-[0.2em] border-b border-[#F4F7FE]">Items / Details</th>
|
||
<th className="px-8 py-5 text-[10px] font-black text-[#A3AED0] uppercase tracking-[0.2em] border-b border-[#F4F7FE]">Type</th>
|
||
<th className="px-8 py-5 text-[10px] font-black text-[#A3AED0] uppercase tracking-[0.2em] border-b border-[#F4F7FE]">Method</th>
|
||
<th className="px-8 py-5 text-right text-[10px] font-black text-[#A3AED0] uppercase tracking-[0.2em] border-b border-[#F4F7FE]">Amount</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-[#F4F7FE]">
|
||
{filteredCollections.map((item, idx) => (
|
||
<tr key={idx} className="hover:bg-[#F4F7FE]/30 transition-colors group">
|
||
<td className="px-8 py-6 text-sm font-bold text-[#1B254B]">{new Date(item.date).toLocaleDateString('en-GB')}</td>
|
||
<td className="px-8 py-6">
|
||
<p className="text-sm font-black text-[#1B254B] leading-tight">{item.remarks || 'No details provided'}</p>
|
||
</td>
|
||
<td className="px-8 py-6">
|
||
<span className="text-xs font-bold text-[#A3AED0]">{item.type?.name}</span>
|
||
</td>
|
||
<td className="px-8 py-6">
|
||
<span className="text-xs font-bold text-[#A3AED0]">{item.payment_method}</span>
|
||
</td>
|
||
<td className="px-8 py-6 text-right">
|
||
<div className="flex flex-col items-end gap-1">
|
||
<p className="text-sm font-black text-[#22C55E]">{parseFloat(item.amount).toLocaleString('en-AE', { minimumFractionDigits: 2 })} AED</p>
|
||
{item.is_adjusted && (
|
||
<button
|
||
onClick={() => setSelectedItem(item)}
|
||
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"
|
||
>
|
||
Adjusted
|
||
</button>
|
||
)}
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
{filteredCollections.length === 0 && (
|
||
<tr>
|
||
<td colSpan="5" className="px-8 py-16 text-center text-[#A3AED0] font-bold uppercase tracking-widest text-xs">No records found</td>
|
||
</tr>
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="space-y-6 animate-in slide-in-from-bottom-2 duration-500">
|
||
{/* Summary Banner */}
|
||
<div className="bg-[#FEF2F2] border border-red-50 rounded-[1.5rem] p-6 flex items-center justify-between">
|
||
<div className="flex items-center gap-6">
|
||
<div className="w-14 h-14 bg-white rounded-full flex items-center justify-center text-[#EF4444] shadow-sm">
|
||
<ArrowDownRight size={28} />
|
||
</div>
|
||
<div>
|
||
<p className="text-[10px] font-black text-red-600 uppercase tracking-widest mb-1">Total Expenses (Filtered)</p>
|
||
<h3 className="text-3xl font-black text-[#1B254B] tracking-tight">{totalExpenses.toLocaleString('en-AE', { minimumFractionDigits: 2 })} AED</h3>
|
||
</div>
|
||
</div>
|
||
<div className="text-right">
|
||
<p className="text-[10px] font-black text-red-600 uppercase tracking-widest">Records: {filteredExpenses.length}</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Table */}
|
||
<div className="bg-white rounded-[2rem] shadow-sm border border-[#F4F7FE] overflow-hidden">
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full text-left">
|
||
<thead>
|
||
<tr className="bg-[#F9FAFB]">
|
||
<th className="px-8 py-5 text-[10px] font-black text-[#A3AED0] uppercase tracking-[0.2em] border-b border-[#F4F7FE]">Date</th>
|
||
<th className="px-8 py-5 text-[10px] font-black text-[#A3AED0] uppercase tracking-[0.2em] border-b border-[#F4F7FE]">Category</th>
|
||
<th className="px-8 py-5 text-[10px] font-black text-[#A3AED0] uppercase tracking-[0.2em] border-b border-[#F4F7FE]">Type</th>
|
||
<th className="px-8 py-5 text-[10px] font-black text-[#A3AED0] uppercase tracking-[0.2em] border-b border-[#F4F7FE]">Remarks</th>
|
||
<th className="px-8 py-5 text-right text-[10px] font-black text-[#A3AED0] uppercase tracking-[0.2em] border-b border-[#F4F7FE]">Amount</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-[#F4F7FE]">
|
||
{filteredExpenses.map((item, idx) => (
|
||
<tr key={idx} className="hover:bg-[#F4F7FE]/30 transition-colors group">
|
||
<td className="px-8 py-6 text-sm font-bold text-[#1B254B]">{new Date(item.date).toLocaleDateString('en-GB')}</td>
|
||
<td className="px-8 py-6">
|
||
<p className="text-sm font-black text-[#1B254B] leading-tight">{item.category?.name}</p>
|
||
</td>
|
||
<td className="px-8 py-6 text-xs font-bold text-[#A3AED0] uppercase tracking-wider">{item.expense_type}</td>
|
||
<td className="px-8 py-6">
|
||
<p className="text-xs font-bold text-[#A3AED0] max-w-xs truncate">{item.remarks || '---'}</p>
|
||
</td>
|
||
<td className="px-8 py-6 text-right">
|
||
<p className="text-sm font-black text-[#EF4444]">{parseFloat(item.amount).toLocaleString('en-AE', { minimumFractionDigits: 2 })} AED</p>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
{filteredExpenses.length === 0 && (
|
||
<tr>
|
||
<td colSpan="5" className="px-8 py-16 text-center text-[#A3AED0] font-bold uppercase tracking-widest text-xs">No records found</td>
|
||
</tr>
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
{/* Details Modal */}
|
||
{selectedItem && (
|
||
<div className="fixed inset-0 bg-[#1B254B]/40 backdrop-blur-sm z-[100] flex items-center justify-center p-4 animate-in fade-in duration-300">
|
||
<div className="bg-white rounded-[2.5rem] w-full max-w-lg overflow-hidden shadow-2xl animate-in zoom-in-95 duration-300">
|
||
<div className="p-8 border-b border-gray-100 flex items-center justify-between bg-gray-50/50">
|
||
<div>
|
||
<h3 className="text-xl font-black text-[#1B254B]">Adjustment Details</h3>
|
||
<p className="text-[10px] font-black text-[#A3AED0] uppercase tracking-widest mt-1">Transaction: #{selectedItem.transaction_id || 'N/A'}</p>
|
||
</div>
|
||
<button
|
||
onClick={() => setSelectedItem(null)}
|
||
className="p-2 hover:bg-white rounded-xl transition-all text-[#A3AED0] hover:text-[#1B254B]"
|
||
>
|
||
<Search size={20} className="rotate-45" /> {/* Using search icon as X replacement if X not imported, but wait, I can use X if I import it */}
|
||
<span className="font-black text-xl">×</span>
|
||
</button>
|
||
</div>
|
||
<div className="p-8 space-y-8">
|
||
<div className="grid grid-cols-2 gap-8">
|
||
<div className="space-y-1">
|
||
<p className="text-[10px] font-black text-[#A3AED0] uppercase tracking-widest">Original Total</p>
|
||
<p className="text-lg font-black text-gray-400 line-through">{(selectedItem.original_amount || 0).toLocaleString('en-AE', { minimumFractionDigits: 2 })} AED</p>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<p className="text-[10px] font-black text-emerald-600 uppercase tracking-widest">Adjusted Total</p>
|
||
<p className="text-lg font-black text-[#22C55E]">{(parseFloat(selectedItem.amount) || 0).toLocaleString('en-AE', { minimumFractionDigits: 2 })} AED</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2 p-6 bg-amber-50/50 rounded-2xl border border-amber-100">
|
||
<p className="text-[10px] font-black text-amber-600 uppercase tracking-widest block mb-2">Remarks / Reason</p>
|
||
<p className="text-sm font-bold text-[#1B254B] leading-relaxed italic">
|
||
"{selectedItem.remarks || 'No remarks provided for this adjustment.'}"
|
||
</p>
|
||
</div>
|
||
|
||
<button
|
||
onClick={() => setSelectedItem(null)}
|
||
className="w-full py-4 bg-[#1B254B] text-white rounded-2xl font-black text-xs uppercase tracking-[0.2em] hover:bg-[#2B3674] transition-all shadow-lg shadow-[#1B254B]/10"
|
||
>
|
||
Close Details
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</main>
|
||
);
|
||
}
|