import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { FaShoppingCart, FaPlus, FaSync, FaChevronDown, FaChevronUp, FaTimes, FaSearch, FaFileExport, FaEye, FaEdit, FaCopy, FaCheckSquare, FaSquare } from 'react-icons/fa'; import { toast, ToastContainer } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; import salesOrderService, { SalesOrder } from '../services/salesOrderService'; import DynamicExportModal from '../components/DynamicExportModal'; import { fetchAllRowsForExport } from '../utils/frappeListExport'; import { useListPageSelection } from '../hooks/useListPageSelection'; const PAGE_SIZE = 20; function buildSalesOrderExportFilters(f: { search: string; status: string; project: string }) { const filters: any[] = []; if (f.search) filters.push(['Sales Order', 'name', 'like', `%${f.search}%`]); if (f.project) filters.push(['Sales Order', 'project', '=', f.project]); if (f.status === 'Draft') filters.push(['Sales Order', 'docstatus', '=', 0]); if (f.status === 'Submitted') filters.push(['Sales Order', 'docstatus', '=', 1]); if (f.status === 'Cancelled') filters.push(['Sales Order', 'docstatus', '=', 2]); return filters; } function getStatusStyle(so: SalesOrder) { if (so.docstatus === 2) return 'bg-red-100 text-red-700'; if (so.docstatus === 1) { if (so.billing_status === 'Fully Billed') return 'bg-green-100 text-green-700'; if (so.delivery_status === 'Fully Delivered') return 'bg-blue-100 text-blue-700'; return 'bg-blue-100 text-blue-700'; } return 'bg-yellow-100 text-yellow-800'; } function getStatusLabel(so: SalesOrder) { if (so.docstatus === 2) return 'Cancelled'; if (so.docstatus === 1) return so.status || 'Submitted'; return 'Draft'; } const SalesOrderList: React.FC = () => { const { t } = useTranslation(); const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const [orders, setOrders] = useState([]); const [loading, setLoading] = useState(true); const [total, setTotal] = useState(0); const [page, setPage] = useState(0); const [filtersOpen, setFiltersOpen] = useState(false); const initialProject = searchParams.get('project')?.trim() || ''; const [searchQuery, setSearchQuery] = useState(''); const [statusFilter, setStatusFilter] = useState(''); const [projectFilter, setProjectFilter] = useState(initialProject); const [applied, setApplied] = useState({ search: '', status: '', project: initialProject }); const [showExportModal, setShowExportModal] = useState(false); const didInitUrlSync = useRef(false); const searchDebounceRef = useRef(null); useEffect(() => { const p = searchParams.get('project')?.trim() || ''; setProjectFilter(p); setApplied(a => { if (a.project === p) return a; setPage(0); return { ...a, project: p }; }); }, [searchParams]); const load = useCallback(async (off: number, f: typeof applied) => { setLoading(true); try { const filters: any[] = []; if (f.search) filters.push(['Sales Order', 'name', 'like', `%${f.search}%`]); if (f.project) filters.push(['Sales Order', 'project', '=', f.project]); if (f.status === 'Draft') filters.push(['Sales Order', 'docstatus', '=', 0]); if (f.status === 'Submitted') filters.push(['Sales Order', 'docstatus', '=', 1]); if (f.status === 'Cancelled') filters.push(['Sales Order', 'docstatus', '=', 2]); const [rows, cnt] = await Promise.all([ salesOrderService.getSalesOrders({ filters, limit_start: off, limit_page_length: PAGE_SIZE }), salesOrderService.getSalesOrderCount(filters), ]); setOrders(rows); setTotal(cnt); } catch (e: any) { toast.error(e.message || 'Failed to load'); } finally { setLoading(false); } }, []); useEffect(() => { load(0, applied); }, [load, applied]); const selectionResetKey = useMemo( () => `${page}|${applied.search}|${applied.status}|${applied.project}`, [page, applied.search, applied.status, applied.project], ); const { selectedRows, toggleRow, toggleAllOnPage, allOnPageSelected, someOnPageSelected, } = useListPageSelection(orders, selectionResetKey); // Auto-apply filters (matches other lists; no explicit Apply button) useEffect(() => { if (!didInitUrlSync.current) { didInitUrlSync.current = true; return; } const nextApplied = { search: applied.search, status: statusFilter, project: projectFilter.trim() }; setApplied(nextApplied); setPage(0); setSearchParams(prev => { const n = new URLSearchParams(prev); if (nextApplied.project) n.set('project', nextApplied.project); else n.delete('project'); return n; }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [statusFilter, projectFilter]); useEffect(() => { if (!didInitUrlSync.current) return; if (searchDebounceRef.current) window.clearTimeout(searchDebounceRef.current); searchDebounceRef.current = window.setTimeout(() => { const nextApplied = { search: searchQuery, status: statusFilter, project: projectFilter.trim() }; setApplied(nextApplied); setPage(0); }, 450); return () => { if (searchDebounceRef.current) window.clearTimeout(searchDebounceRef.current); }; }, [searchQuery, statusFilter, projectFilter]); const clear = () => { setSearchQuery(''); setStatusFilter(''); setProjectFilter(''); setApplied({ search: '', status: '', project: '' }); setPage(0); setSearchParams(prev => { const n = new URLSearchParams(prev); n.delete('project'); return n; }); }; const hasActive = !!(applied.search || applied.status || applied.project); const goPage = (p: number) => { setPage(p); load(p * PAGE_SIZE, applied); }; const handleView = (name: string) => navigate(`/sales-orders/${encodeURIComponent(name)}`); const handleEdit = (name: string) => navigate(`/sales-orders/${encodeURIComponent(name)}?edit=1`); const handleDuplicate = (name: string) => navigate(`/sales-orders/new?duplicate=${encodeURIComponent(name)}`); const fetchAllForExport = useCallback( () => fetchAllRowsForExport({ doctype: 'Sales Order', filters: buildSalesOrderExportFilters(applied), orderBy: 'modified desc', }), [applied], ); return (

Sales Orders

{total} total

setShowExportModal(false)} doctype="Sales Order" selectedCount={selectedRows.size} pageCount={orders.length} totalCount={total} pageData={orders} selectedRows={selectedRows} rowKey="name" onFetchAll={fetchAllForExport} fileNamePrefix="sales_orders" /> {/* Filter Panel */}
{hasActive && (
{applied.search && ID: {applied.search}} {applied.project && Project: {applied.project}} {applied.status && Status: {applied.status}}
)} {filtersOpen && (
setSearchQuery(e.target.value)} onKeyDown={e => e.key === 'Enter' && e.preventDefault()} placeholder="Search…" className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 focus:outline-none focus:ring-1 focus:ring-blue-400" />
setProjectFilter(e.target.value)} onKeyDown={e => e.key === 'Enter' && e.preventDefault()} placeholder="Project name…" className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 focus:outline-none focus:ring-1 focus:ring-blue-400" />
)}
{/* Table */}
{loading ? ( ) : orders.length === 0 ? ( ) : orders.map(so => ( handleView(so.name)} className={`cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors ${selectedRows.has(so.name) ? 'bg-blue-50 dark:bg-blue-900/20' : ''}`}> ))}
Order ID Customer Date Status Grand Total
Loading…
No sales orders found
e.stopPropagation()}> {so.name} {so.customer_name || so.customer || '-'} {so.transaction_date || '-'} {getStatusLabel(so)} {so.currency || 'SAR'} {(so.grand_total ?? 0).toFixed(2)} e.stopPropagation()}>
{total > PAGE_SIZE && (
{page * PAGE_SIZE + 1}–{Math.min((page + 1) * PAGE_SIZE, total)} of {total}
)}
); }; export default SalesOrderList;