230 lines
9.9 KiB
JavaScript
230 lines
9.9 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
import DataTable from '../../../Components/DataTable';
|
|
import Toast from '../Components/Toast';
|
|
import {
|
|
Search,
|
|
Plus,
|
|
Eye,
|
|
Filter,
|
|
Calendar,
|
|
Building,
|
|
CreditCard,
|
|
ArrowUpRight,
|
|
X,
|
|
ChevronRight,
|
|
SearchIcon,
|
|
CalendarIcon
|
|
} from 'lucide-react';
|
|
import AddCollectionModal from './AddCollectionModal';
|
|
|
|
export default function CollectionsIndex() {
|
|
const [collections, setCollections] = useState([]);
|
|
const [branches, setBranches] = useState([]);
|
|
const [collectionTypes, setCollectionTypes] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const [toast, setToast] = useState(null);
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
|
|
// Filter States
|
|
const [filterFromDate, setFilterFromDate] = useState('');
|
|
const [filterToDate, setFilterToDate] = useState('');
|
|
const [filterBranch, setFilterBranch] = useState(window.__APP_DATA__?.branch?.id || '');
|
|
const [filterMethod, setFilterMethod] = useState('');
|
|
|
|
const isReceptionist = window.__APP_DATA__?.role === 'receptionist';
|
|
|
|
useEffect(() => {
|
|
fetchMetadata();
|
|
fetchCollections();
|
|
}, [filterBranch, filterFromDate, filterToDate, filterMethod]);
|
|
|
|
const fetchMetadata = async () => {
|
|
try {
|
|
const [bRes, tRes] = await Promise.all([
|
|
fetch('/api/branches'),
|
|
fetch('/api/masters/collection')
|
|
]);
|
|
if (bRes.ok) setBranches(await bRes.json());
|
|
if (tRes.ok) setCollectionTypes(await tRes.json());
|
|
} catch (error) {
|
|
console.error('Error fetching metadata:', error);
|
|
}
|
|
};
|
|
|
|
const fetchCollections = async () => {
|
|
setLoading(true);
|
|
try {
|
|
let url = '/api/collections?';
|
|
if (filterBranch) url += `branch_id=${filterBranch}&`;
|
|
if (filterFromDate) url += `start_date=${filterFromDate}&`;
|
|
if (filterToDate) url += `end_date=${filterToDate}&`;
|
|
if (filterMethod) url += `payment_method=${filterMethod}&`;
|
|
|
|
const response = await fetch(url);
|
|
const data = await response.json();
|
|
setCollections(data);
|
|
} catch (error) {
|
|
console.error('Error fetching collections:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const columns = [
|
|
{
|
|
header: 'DATE',
|
|
render: (row) => (
|
|
<span className="text-sm font-bold text-gray-500">{new Date(row.date).toLocaleDateString()}</span>
|
|
)
|
|
},
|
|
{
|
|
header: 'BRANCH',
|
|
render: (row) => (
|
|
<span className="text-gray-500 font-bold text-sm tracking-tight">{row.branch?.name}</span>
|
|
)
|
|
},
|
|
{
|
|
header: 'ITEMS',
|
|
render: (row) => (
|
|
<div className="flex flex-col">
|
|
<span className="font-black text-[#111827] text-sm">
|
|
{row.type?.name === 'Product sale' || row.type?.name === 'Product saled'
|
|
? (row.items && row.items.length > 0 ? row.items[0].product?.name : 'Product Sale')
|
|
: row.type?.name
|
|
}
|
|
</span>
|
|
{row.items && row.items.length > 1 && (
|
|
<span className="text-[10px] text-gray-400 font-bold">+{row.items.length - 1} more items</span>
|
|
)}
|
|
</div>
|
|
)
|
|
},
|
|
{
|
|
header: 'PAYMENT METHOD',
|
|
render: (row) => (
|
|
<span className="text-gray-500 font-bold text-sm">{row.payment_method}</span>
|
|
)
|
|
},
|
|
{
|
|
header: 'AMOUNT',
|
|
render: (row) => (
|
|
<span className="font-black text-[#10B981] text-sm">{parseFloat(row.amount).toFixed(2)} AED</span>
|
|
)
|
|
},
|
|
{
|
|
header: 'REMARKS',
|
|
render: (row) => (
|
|
<span className="text-xs text-gray-400 font-medium italic max-w-[250px] truncate block">{row.remarks || '-'}</span>
|
|
)
|
|
}
|
|
];
|
|
|
|
const filteredCollections = collections.filter(c =>
|
|
(c.remarks?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
c.type?.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
c.items?.some(i => i.product?.name.toLowerCase().includes(searchTerm.toLowerCase())))
|
|
);
|
|
|
|
return (
|
|
<>
|
|
{toast && <Toast message={toast.message} type={toast.type} onClose={() => setToast(null)} />}
|
|
|
|
<main className="px-10 py-8 max-w-[1600px] mx-auto space-y-8">
|
|
<div className="flex items-center justify-between">
|
|
<h1 className="text-[40px] font-black text-[#111827] tracking-tighter">Collections</h1>
|
|
|
|
<div className="flex items-center gap-3">
|
|
{!isReceptionist && (
|
|
<div className="flex items-center bg-white border border-gray-100 rounded-xl px-4 py-2 text-sm font-bold text-gray-500 shadow-sm">
|
|
<Building size={16} className="text-gray-300 mr-2" />
|
|
<select
|
|
className="outline-none bg-transparent appearance-none cursor-pointer"
|
|
value={filterBranch}
|
|
onChange={e => setFilterBranch(e.target.value)}
|
|
>
|
|
<option value="">All Branche</option>
|
|
{branches.map(b => <option key={b.id} value={b.id}>{b.name}</option>)}
|
|
</select>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex items-center bg-white border border-gray-100 rounded-xl px-4 py-2 text-sm font-bold text-gray-500 shadow-sm">
|
|
<CreditCard size={16} className="text-gray-300 mr-2" />
|
|
<select
|
|
className="outline-none bg-transparent appearance-none cursor-pointer"
|
|
value={filterMethod}
|
|
onChange={e => setFilterMethod(e.target.value)}
|
|
>
|
|
<option value="">All Methods</option>
|
|
<option value="Cash">Cash</option>
|
|
<option value="Card">Card</option>
|
|
<option value="Online">Online</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 bg-white border border-gray-100 rounded-xl px-4 py-2 text-sm font-bold text-gray-500 shadow-sm">
|
|
<input
|
|
type="date"
|
|
className="outline-none bg-transparent text-xs"
|
|
value={filterFromDate}
|
|
onChange={e => setFilterFromDate(e.target.value)}
|
|
/>
|
|
<span className="text-gray-300">-</span>
|
|
<input
|
|
type="date"
|
|
className="outline-none bg-transparent text-xs"
|
|
value={filterToDate}
|
|
onChange={e => setFilterToDate(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="relative">
|
|
<Search className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-300" size={18} />
|
|
<input
|
|
className="pl-12 pr-6 py-2.5 bg-white border border-gray-100 rounded-xl outline-none focus:ring-2 focus:ring-red-500/5 focus:border-red-500 transition-all font-bold text-sm text-gray-900 shadow-sm w-[300px]"
|
|
placeholder="Search items, remark"
|
|
value={searchTerm}
|
|
onChange={e => setSearchTerm(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => setIsModalOpen(true)}
|
|
className="flex items-center gap-2 px-6 py-2.5 bg-[#FF4D4D] text-white rounded-xl font-black text-sm hover:bg-[#E60000] transition-all shadow-lg shadow-red-100"
|
|
>
|
|
<Plus size={20} />
|
|
<span>Add Collection</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-[32px] border border-gray-100 shadow-sm overflow-hidden animate-in fade-in slide-in-from-bottom-4 duration-700">
|
|
<DataTable
|
|
columns={columns}
|
|
data={filteredCollections}
|
|
loading={loading}
|
|
actions={(row) => (
|
|
<button className="p-2 hover:bg-gray-50 rounded-lg text-blue-500 transition-all">
|
|
<Eye size={18} />
|
|
</button>
|
|
)}
|
|
emptyMessage="No collection entries found."
|
|
/>
|
|
</div>
|
|
</main>
|
|
|
|
<AddCollectionModal
|
|
isOpen={isModalOpen}
|
|
onClose={() => setIsModalOpen(false)}
|
|
onSave={(newCol) => {
|
|
setCollections([newCol, ...collections]);
|
|
setToast({ message: 'Collection recorded successfully!', type: 'success' });
|
|
}}
|
|
branches={branches}
|
|
types={collectionTypes}
|
|
/>
|
|
</>
|
|
);
|
|
}
|