import React, { useState, useEffect, useRef, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useItems, useItemMutations } from '../hooks/useItem'; import * as XLSX from 'xlsx'; import { FaPlus, FaEdit, FaEye, FaTrash, FaCopy, FaFileExport, FaBox, FaTimes, FaFilter, FaChevronDown, FaChevronUp, FaSave, FaStar, FaCheckSquare, FaSquare, FaFileExcel, FaFileCsv, FaDownload } from 'react-icons/fa'; import LinkField from '../components/LinkField'; // Export types type ExportFormat = 'csv' | 'excel'; type ExportScope = 'selected' | 'all_on_page' | 'all_with_filters'; interface ExportModalProps { isOpen: boolean; onClose: () => void; selectedCount: number; totalCount: number; pageCount: number; onExport: (scope: ExportScope, format: ExportFormat, columns: string[]) => void; isExporting: boolean; exportColumns: Array<{key: string, label: string, default: boolean}>; } const ExportModal: React.FC = ({ isOpen, onClose, selectedCount, totalCount, pageCount, onExport, isExporting, exportColumns }) => { const { t } = useTranslation(); const [scope, setScope] = useState(selectedCount > 0 ? 'selected' : 'all_with_filters'); const [format, setFormat] = useState('csv'); const [selectedColumns, setSelectedColumns] = useState( exportColumns.filter(c => c.default).map(c => c.key) ); useEffect(() => { if (selectedCount > 0) { setScope('selected'); } else { setScope('all_with_filters'); } }, [selectedCount]); const toggleColumn = (key: string) => { setSelectedColumns(prev => prev.includes(key) ? prev.filter(k => k !== key) : [...prev, key] ); }; const selectAllColumns = () => { setSelectedColumns(exportColumns.map(c => c.key)); }; const selectDefaultColumns = () => { setSelectedColumns(exportColumns.filter(c => c.default).map(c => c.key)); }; if (!isOpen) return null; return (
{/* Header */}

{t('listPages.export')} Items

{/* Export Scope */}

Select Data to Export

{/* Export Format */}

Export Format

{/* Column Selection */}

Columns to Export

|
{exportColumns.map((col) => ( ))}

{selectedColumns.length} column{selectedColumns.length !== 1 ? 's' : ''} selected

{/* Footer */}
{scope === 'selected' && `Exporting ${selectedCount} selected row${selectedCount !== 1 ? 's' : ''}`} {scope === 'all_on_page' && `Exporting ${pageCount} row${pageCount !== 1 ? 's' : ''} from current page`} {scope === 'all_with_filters' && `Exporting all ${totalCount} row${totalCount !== 1 ? 's' : ''}`}
); }; const ItemList: React.FC = () => { const { t } = useTranslation(); const navigate = useNavigate(); const [page, setPage] = useState(0); const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(null); const limit = 20; // Export column configuration const EXPORT_COLUMNS = [ { key: 'name', label: 'Item ID', default: true }, { key: 'item_code', label: 'Item Code', default: true }, { key: 'item_name', label: 'Item Name', default: true }, { key: 'item_group', label: 'Item Group', default: true }, { key: 'custom_hospital_name', label: 'Hospital', default: true }, { key: 'stock_uom', label: 'Stock UOM', default: false }, { key: 'custom_part_description', label: 'Part Description', default: false }, { key: 'brand', label: 'Brand', default: false }, { key: 'valuation_rate', label: 'Valuation Rate', default: false }, { key: 'opening_stock', label: 'Opening Stock', default: false }, { key: 'custom_last_calibration_date', label: 'Last Calibration Date', default: false }, { key: 'custom_next_due_calibration_date', label: 'Next Calibration Date', default: false }, { key: 'description', label: 'Description', default: false }, { key: 'creation', label: 'Created On', default: false }, { key: 'modified', label: 'Modified On', default: true }, { key: 'owner', label: 'Created By', default: false }, { key: 'modified_by', label: 'Modified By', default: false }, ]; // Track initial load to prevent full-page refresh on filter changes const [initialLoadComplete, setInitialLoadComplete] = useState(false); // Row selection state const [selectedRows, setSelectedRows] = useState>(new Set()); // Export modal state const [showExportModal, setShowExportModal] = useState(false); const [isExporting, setIsExporting] = useState(false); // Filter states const [filterItemId, setFilterItemId] = useState(''); const [filterItemGroup, setFilterItemGroup] = useState(''); const [filterCompany, setFilterCompany] = useState(''); const [filterItemName, setFilterItemName] = useState(''); // Advanced filter UI states const [isFilterExpanded, setIsFilterExpanded] = useState(true); const [activeFilterCount, setActiveFilterCount] = useState(0); const [savedFilters, setSavedFilters] = useState([]); const [showSaveFilterModal, setShowSaveFilterModal] = useState(false); const [filterPresetName, setFilterPresetName] = useState(''); // Temporary states for text inputs const [tempItemName, setTempItemName] = useState(''); // Debounce timer refs const itemNameDebounceRef = useRef(null); // Load saved filters from localStorage on mount useEffect(() => { const saved = localStorage.getItem('itemFilterPresets'); if (saved) { setSavedFilters(JSON.parse(saved)); } }, []); // Update active filter count useEffect(() => { const count = [ filterItemId, filterItemGroup, filterCompany, filterItemName ].filter(Boolean).length; setActiveFilterCount(count); }, [filterItemId, filterItemGroup, filterCompany, filterItemName]); // Build filters object const filters: Record = {}; if (filterItemId) filters['name'] = filterItemId; if (filterItemGroup) filters['item_group'] = filterItemGroup; if (filterCompany) filters['custom_hospital_name'] = filterCompany; if (filterItemName) filters['item_name'] = ['like', `%${filterItemName}%`]; const { items, totalCount, hasMore, loading, error, refetch } = useItems( Object.keys(filters).length > 0 ? filters : undefined, limit, page * limit, 'creation desc' ); const { deleteItem, loading: mutationLoading } = useItemMutations(); // Mark initial load complete useEffect(() => { if (!loading && !initialLoadComplete) { setInitialLoadComplete(true); } }, [loading, initialLoadComplete]); // Reset page when filters change useEffect(() => { if (page !== 0) { setPage(0); } }, [filterItemId, filterItemGroup, filterCompany, filterItemName]); // Clear selection when filters/page change useEffect(() => { setSelectedRows(new Set()); }, [filterItemId, filterItemGroup, filterCompany, filterItemName, page]); // Debounce function for item name const handleItemNameChange = (value: string) => { setTempItemName(value); if (itemNameDebounceRef.current) clearTimeout(itemNameDebounceRef.current); itemNameDebounceRef.current = window.setTimeout(() => { setFilterItemName(value); }, 800); }; // Handle Enter key press const handleKeyPress = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { e.preventDefault(); if (itemNameDebounceRef.current) clearTimeout(itemNameDebounceRef.current); setFilterItemName(tempItemName); } }; // Save filter preset const handleSaveFilterPreset = () => { if (!filterPresetName.trim()) { alert('Please enter a filter name'); return; } const preset = { id: Date.now(), name: filterPresetName, filters: { filterItemId, filterItemGroup, filterCompany, filterItemName } }; const updated = [...savedFilters, preset]; setSavedFilters(updated); setFilterPresetName(''); setShowSaveFilterModal(false); localStorage.setItem('itemFilterPresets', JSON.stringify(updated)); }; // Load filter preset const handleLoadFilterPreset = (preset: any) => { const f = preset.filters; setFilterItemId(f.filterItemId || ''); setFilterItemGroup(f.filterItemGroup || ''); setFilterCompany(f.filterCompany || ''); setFilterItemName(f.filterItemName || ''); setTempItemName(f.filterItemName || ''); }; // Delete filter preset const handleDeleteFilterPreset = (id: number) => { const updated = savedFilters.filter(f => f.id !== id); setSavedFilters(updated); localStorage.setItem('itemFilterPresets', JSON.stringify(updated)); }; // Cleanup timeouts on unmount useEffect(() => { return () => { if (itemNameDebounceRef.current) clearTimeout(itemNameDebounceRef.current); }; }, []); // Clear all filters const handleClearFilters = () => { setFilterItemId(''); setFilterItemGroup(''); setFilterCompany(''); setFilterItemName(''); setTempItemName(''); if (itemNameDebounceRef.current) clearTimeout(itemNameDebounceRef.current); }; const hasActiveFilters = filterItemId || filterItemGroup || filterCompany || filterItemName; // Format date helper const formatDate = (dateString?: string) => { if (!dateString) return '-'; const date = new Date(dateString); return date.toLocaleString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); }; // Row selection handlers const handleSelectRow = (itemName: string) => { setSelectedRows(prev => { const newSet = new Set(prev); if (newSet.has(itemName)) { newSet.delete(itemName); } else { newSet.add(itemName); } return newSet; }); }; const handleSelectAll = () => { if (selectedRows.size === items.length) { setSelectedRows(new Set()); } else { setSelectedRows(new Set(items.map(i => i.name))); } }; const isAllSelected = items.length > 0 && selectedRows.size === items.length; const isSomeSelected = selectedRows.size > 0 && selectedRows.size < items.length; // Fetch all items for export const fetchAllItemsForExport = useCallback(async (): Promise => { const allItems: any[] = []; let currentPage = 0; const pageSize = 100; let hasMoreData = true; while (hasMoreData) { try { const response = await fetch('/api/method/frappe.client.get_list', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ doctype: 'Item', filters: filters, fields: ['*'], limit_start: currentPage * pageSize, limit_page_length: pageSize, order_by: 'creation desc' }) }); const data = await response.json(); const results = data.message || []; allItems.push(...results); if (results.length < pageSize) { hasMoreData = false; } else { currentPage++; } if (currentPage > 100) { console.warn('Export safety limit reached'); hasMoreData = false; } } catch (error) { console.error('Error fetching items for export:', error); throw error; } } return allItems; }, [filters]); // Export handler const handleExport = async (scope: ExportScope, format: ExportFormat, columns: string[]) => { setIsExporting(true); try { let dataToExport: any[] = []; switch (scope) { case 'selected': dataToExport = items.filter(i => selectedRows.has(i.name)); break; case 'all_on_page': dataToExport = items; break; case 'all_with_filters': dataToExport = await fetchAllItemsForExport(); break; } if (dataToExport.length === 0) { alert('No data to export'); return; } const columnLabels = columns.map(key => { const col = EXPORT_COLUMNS.find(c => c.key === key); return col?.label || key; }); if (format === 'csv') { const csvContent = [ columnLabels.join(','), ...dataToExport.map(item => columns.map(key => { let value = item[key] || ''; if (typeof value === 'string' && (value.includes(',') || value.includes('"') || value.includes('\n'))) { value = `"${value.replace(/"/g, '""')}"`; } return value; }).join(',') ) ].join('\n'); const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `items_export_${new Date().toISOString().split('T')[0]}.csv`; link.click(); URL.revokeObjectURL(url); } else if (format === 'excel') { const worksheetData = [ columnLabels, ...dataToExport.map(item => columns.map(key => item[key] || '') ) ]; const worksheet = XLSX.utils.aoa_to_sheet(worksheetData); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, 'Items'); XLSX.writeFile(workbook, `items_export_${new Date().toISOString().split('T')[0]}.xlsx`); } setShowExportModal(false); setSelectedRows(new Set()); } catch (error) { console.error('Export failed:', error); alert(`Export failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } finally { setIsExporting(false); } }; const handleCreateNew = () => { navigate('/inventory/new'); }; const handleView = (itemName: string) => { navigate(`/inventory/${itemName}`); }; const handleEdit = (itemName: string) => { navigate(`/inventory/${itemName}`); }; const handleDelete = async (itemName: string) => { try { await deleteItem(itemName); setDeleteConfirmOpen(null); refetch(); alert('Item deleted successfully!'); } catch (err) { alert(`Failed to delete: ${err instanceof Error ? err.message : 'Unknown error'}`); } }; const handleDuplicate = (itemName: string) => { navigate(`/inventory/new?duplicate=${itemName}`); }; // Only show full-page loader on initial load if (loading && !initialLoadComplete) { return (

Loading items...

); } if (error) { return (

⚠️ Error Loading Items

Failed to load items.

Technical Error: {error}

); } return (
{/* Header */}

Inventory

Total: {totalCount} item{totalCount !== 1 ? 's' : ''} {/* Show selection count */} {selectedRows.size > 0 && ( • {selectedRows.size} selected )} {/* Show loading indicator inline when filtering */} {loading && initialLoadComplete && (

Updating... )}

{/* Export Button */}
{/* Advanced Filter Panel */}
{/* Filter Header */}
{/* Left Side - Toggle & Title */}

{t('listPages.filters')}

{activeFilterCount > 0 && ( {activeFilterCount} )}
{/* Center - Active Filter Tags (scrollable) */} {hasActiveFilters && (
{filterItemId && ( Item ID: {filterItemId} )} {filterItemName && ( Name: {filterItemName} )} {filterCompany && ( Hospital: {filterCompany} )} {filterItemGroup && ( Item Group: {filterItemGroup} )}
)} {/* Right Side - Action Buttons */}
{/* Save Filter Button */} {activeFilterCount > 0 && ( )} {/* Clear All Button */} {hasActiveFilters && ( )}
{/* Expandable Filter Content */} {isFilterExpanded && (
{/* Saved Filter Presets */} {savedFilters.length > 0 && (

Saved Filters

{savedFilters.map((preset) => (
))}
)} {/* Filter Grid */}
{/* Item ID */}
setFilterItemId(val)} placeholder="Select Item" disabled={false} compact={true} /> {filterItemId && ( )}
{/* Item Name */}
handleItemNameChange(e.target.value)} onKeyDown={handleKeyPress} placeholder="Type to search..." className="w-full px-2 py-1 text-xs border border-gray-300 dark:border-gray-600 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-white" /> {tempItemName && tempItemName !== filterItemName && ( typing... )} {filterItemName && ( )}
{/* Hospital */}
setFilterCompany(val)} placeholder="Select Hospital" disabled={false} compact={true} filters={{ domain: 'Healthcare' }} /> {filterCompany && ( )}
{/* Item Group */}
setFilterItemGroup(val)} placeholder="Select Item Group" disabled={false} compact={true} /> {filterItemGroup && ( )}
)}
{/* Save Filter Modal */} {showSaveFilterModal && (

Save Filter Preset

setFilterPresetName(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); handleSaveFilterPreset(); } }} placeholder="Enter filter name (e.g., 'Medical Supplies')" className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-white mb-4" autoFocus />
)} {/* Export Modal */} setShowExportModal(false)} selectedCount={selectedRows.size} totalCount={totalCount} pageCount={items.length} onExport={handleExport} isExporting={isExporting} exportColumns={EXPORT_COLUMNS} /> {/* Items Table */}
{/* Inline loading overlay for filter changes */} {loading && initialLoadComplete && (
Filtering...
)}
{/* Select All Checkbox Column */} {items.length === 0 ? ( ) : ( items.map((item) => ( handleView(item.name)} > {/* Row Checkbox */} {/* Inline Action Icons */} )) )}
Item Code Item Name Item Group Hospital Updated On Actions

No items found

{hasActiveFilters ? ( ) : ( )}
e.stopPropagation()}>
{item.item_code || item.name}
{item.item_name || '-'}
{item.item_group || '-'}
{item.custom_hospital_name || '-'}
{formatDate(item.modified)}
e.stopPropagation()}>
{/* Pagination */} {(hasMore || page > 0) && (
Showing {page * limit + 1} to {Math.min((page + 1) * limit, totalCount)} of {totalCount} results
)}
{/* Delete Confirmation Modal */} {deleteConfirmOpen && (

Delete Item

Are you sure you want to delete this item? This action cannot be undone.

Item ID: {deleteConfirmOpen}

)}
); }; export default ItemList;