2026-03-17 04:50:00 +05:30

379 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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(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');
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 break-all whitespace-normal">
"{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>
);
}