import React, { useEffect, useState, useCallback, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { FaPlus, FaSearch, FaSpinner, FaUserFriends, FaArrowLeft, FaFileExport } from 'react-icons/fa'; import masterService, { Customer } from '../services/masterService'; import { toast, ToastContainer } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; import DynamicExportModal from '../components/DynamicExportModal'; import { fetchAllRowsForExport } from '../utils/frappeListExport'; import { useListPageSelection } from '../hooks/useListPageSelection'; const statusBadge = (disabled?: number) => disabled ? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300' : 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300'; const CustomerList: React.FC = () => { const { t } = useTranslation(); const navigate = useNavigate(); const [customers, setCustomers] = useState([]); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(''); const [page, setPage] = useState(0); const [total, setTotal] = useState(0); const [showExportModal, setShowExportModal] = useState(false); const PAGE = 20; const apiFilters = useMemo(() => { const f: Record = {}; if (search.trim()) f.customer_name = ['like', `%${search.trim()}%`]; return f; }, [search]); const load = useCallback(async (p = 0) => { setLoading(true); try { const [{ data }, cnt] = await Promise.all([ masterService.getCustomers({ limit_start: p * PAGE, limit_page_length: PAGE, filters: apiFilters }), masterService.getCustomerCount(apiFilters), ]); setCustomers(data); setTotal(cnt); setPage(p); } catch (e) { toast.error(e instanceof Error ? e.message : 'Failed to load customers'); } finally { setLoading(false); } }, [apiFilters]); useEffect(() => { load(0); }, [load]); const selectionResetKey = useMemo(() => `${page}|${JSON.stringify(apiFilters)}`, [page, apiFilters]); const { selectedRows, toggleRow, toggleAllOnPage, allOnPageSelected, someOnPageSelected, } = useListPageSelection(customers, selectionResetKey); const fetchAllForExport = useCallback( () => fetchAllRowsForExport({ doctype: 'Customer', filters: apiFilters, orderBy: 'modified desc' }), [apiFilters], ); const handleSearch = (e: React.ChangeEvent) => { setSearch(e.target.value); }; return (
{/* Header */}

Customers

{total} total

setShowExportModal(false)} doctype="Customer" selectedCount={selectedRows.size} pageCount={customers.length} totalCount={total} pageData={customers} selectedRows={selectedRows} rowKey="name" onFetchAll={fetchAllForExport} fileNamePrefix="customers" /> {/* Table */}
{loading ? (
) : customers.length === 0 ? (

No customers found

) : ( <> {/* Header row */}
{ if (el) el.indeterminate = someOnPageSelected; }} onChange={toggleAllOnPage} aria-label="Select all on page" />
Customer Name
Type
Customer Group
Territory
Status
{customers.map(c => (
navigate(`/customers/${encodeURIComponent(c.name)}`)} onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); navigate(`/customers/${encodeURIComponent(c.name)}`); } }} className={`grid grid-cols-[2.5rem_1fr_1fr_1fr_1fr_100px] w-full text-left hover:bg-gray-50 dark:hover:bg-gray-700/30 transition-colors cursor-pointer ${selectedRows.has(c.name) ? 'bg-cyan-50/70 dark:bg-cyan-900/15' : ''}`} >
e.stopPropagation()}> toggleRow(c.name)} aria-label={`Select ${c.name}`} />

{c.customer_name || c.name}

{c.name}

{c.customer_type || '-'}
{c.customer_group || '-'}
{c.territory || '-'}
{c.disabled ? 'Disabled' : 'Active'}
))}
{/* Pagination */}
Page {page + 1}
)}
); }; export default CustomerList;