1576 lines
69 KiB
TypeScript

import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { useNavigate, useSearchParams } 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';
import ListPagination from '../components/ListPagination';
import { buildDateRangeFilters, toFrappeFilterArray } from '../utils/listFilterUtils';
import DeleteRequestButton from '../components/DeleteRequestButton';
import type { DeleteStatus } from '../services/deleteRequestService';
// 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<ExportModalProps> = ({
isOpen,
onClose,
selectedCount,
totalCount,
pageCount,
onExport,
isExporting,
exportColumns
}) => {
const { t } = useTranslation();
const [scope, setScope] = useState<ExportScope>(selectedCount > 0 ? 'selected' : 'all_with_filters');
const [format, setFormat] = useState<ExportFormat>('csv');
const [selectedColumns, setSelectedColumns] = useState<string[]>(
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 (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-[70] p-4">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-hidden animate-scale-in">
{/* Header */}
<div className="bg-gradient-to-r from-green-500 to-green-600 px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<FaFileExport className="text-white text-xl" />
<h3 className="text-lg font-semibold text-white">{t('items.export.title')}</h3>
</div>
<button
onClick={onClose}
className="text-white/80 hover:text-white transition-colors"
disabled={isExporting}
>
<FaTimes size={20} />
</button>
</div>
</div>
<div className="p-6 overflow-y-auto max-h-[calc(90vh-180px)]">
{/* Export Scope */}
<div className="mb-6">
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">
{t('items.export.selectData')}
</h4>
<div className="space-y-2">
<label className={`flex items-center gap-3 p-3 rounded-lg border cursor-pointer transition-all ${
scope === 'selected'
? 'border-green-500 bg-green-50 dark:bg-green-900/20'
: 'border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50'
} ${selectedCount === 0 ? 'opacity-50 cursor-not-allowed' : ''}`}>
<input
type="radio"
name="scope"
value="selected"
checked={scope === 'selected'}
onChange={() => setScope('selected')}
disabled={selectedCount === 0}
className="text-green-600 focus:ring-green-500"
/>
<div className="flex-1">
<div className="font-medium text-gray-900 dark:text-white">
{t('items.export.selectedRows')}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
{t('items.export.selectedCount', { count: selectedCount })}
</div>
</div>
{selectedCount > 0 && (
<span className="bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-300 px-2 py-1 rounded text-xs font-medium">
{selectedCount} {t('common.selected')}
</span>
)}
</label>
<label className={`flex items-center gap-3 p-3 rounded-lg border cursor-pointer transition-all ${
scope === 'all_on_page'
? 'border-green-500 bg-green-50 dark:bg-green-900/20'
: 'border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50'
}`}>
<input
type="radio"
name="scope"
value="all_on_page"
checked={scope === 'all_on_page'}
onChange={() => setScope('all_on_page')}
className="text-green-600 focus:ring-green-500"
/>
<div className="flex-1">
<div className="font-medium text-gray-900 dark:text-white">
{t('items.export.currentPage')}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
{t('items.export.currentPageCount', { count: pageCount })}
</div>
</div>
<span className="bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300 px-2 py-1 rounded text-xs font-medium">
{pageCount} {t('common.rows')}
</span>
</label>
<label className={`flex items-center gap-3 p-3 rounded-lg border cursor-pointer transition-all ${
scope === 'all_with_filters'
? 'border-green-500 bg-green-50 dark:bg-green-900/20'
: 'border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50'
}`}>
<input
type="radio"
name="scope"
value="all_with_filters"
checked={scope === 'all_with_filters'}
onChange={() => setScope('all_with_filters')}
className="text-green-600 focus:ring-green-500"
/>
<div className="flex-1">
<div className="font-medium text-gray-900 dark:text-white">
{t('items.export.allWithFilters')}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
{t('items.export.allWithFiltersCount', { count: totalCount })}
</div>
</div>
<span className="bg-purple-100 dark:bg-purple-900/40 text-purple-700 dark:text-purple-300 px-2 py-1 rounded text-xs font-medium">
{totalCount} {t('common.total')}
</span>
</label>
</div>
</div>
{/* Export Format */}
<div className="mb-6">
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">
{t('listPages.exportFormat')}
</h4>
<div className="flex gap-3">
<label className={`flex-1 flex items-center gap-3 p-3 rounded-lg border cursor-pointer transition-all ${
format === 'csv'
? 'border-green-500 bg-green-50 dark:bg-green-900/20'
: 'border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50'
}`}>
<input
type="radio"
name="format"
value="csv"
checked={format === 'csv'}
onChange={() => setFormat('csv')}
className="text-green-600 focus:ring-green-500"
/>
<FaFileCsv className="text-green-600 text-xl" />
<div>
<div className="font-medium text-gray-900 dark:text-white">{t('listPages.csv')}</div>
<div className="text-xs text-gray-500 dark:text-gray-400">{t('items.export.csvDesc')}</div>
</div>
</label>
<label className={`flex-1 flex items-center gap-3 p-3 rounded-lg border cursor-pointer transition-all ${
format === 'excel'
? 'border-green-500 bg-green-50 dark:bg-green-900/20'
: 'border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50'
}`}>
<input
type="radio"
name="format"
value="excel"
checked={format === 'excel'}
onChange={() => setFormat('excel')}
className="text-green-600 focus:ring-green-500"
/>
<FaFileExcel className="text-green-700 text-xl" />
<div>
<div className="font-medium text-gray-900 dark:text-white">{t('listPages.excel')}</div>
<div className="text-xs text-gray-500 dark:text-gray-400">{t('items.export.excelDesc')}</div>
</div>
</label>
</div>
</div>
{/* Column Selection */}
<div>
<div className="flex items-center justify-between mb-3">
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300">
{t('items.export.columnsToExport')}
</h4>
<div className="flex gap-2">
<button
onClick={selectAllColumns}
className="text-xs text-blue-600 dark:text-blue-400 hover:underline"
>
{t('items.export.selectAll')}
</button>
<span className="text-gray-300 dark:text-gray-600">|</span>
<button
onClick={selectDefaultColumns}
className="text-xs text-blue-600 dark:text-blue-400 hover:underline"
>
{t('items.export.resetToDefault')}
</button>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2 max-h-48 overflow-y-auto p-3 bg-gray-50 dark:bg-gray-900/50 rounded-lg">
{exportColumns.map((col) => (
<label
key={col.key}
className={`flex items-center gap-2 p-2 rounded cursor-pointer transition-all ${
selectedColumns.includes(col.key)
? 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300'
: 'hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-400'
}`}
>
<input
type="checkbox"
checked={selectedColumns.includes(col.key)}
onChange={() => toggleColumn(col.key)}
className="rounded text-green-600 focus:ring-green-500"
/>
<span className="text-sm truncate">{col.label}</span>
</label>
))}
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
{t('items.export.columnsSelected', { count: selectedColumns.length })}
</p>
</div>
</div>
{/* Footer */}
<div className="px-6 py-4 bg-gray-50 dark:bg-gray-900/50 border-t border-gray-200 dark:border-gray-700 flex justify-between items-center">
<div className="text-sm text-gray-600 dark:text-gray-400">
{scope === 'selected' && t('items.export.exportingSelected', { count: selectedCount })}
{scope === 'all_on_page' && t('items.export.exportingPage', { count: pageCount })}
{scope === 'all_with_filters' && t('items.export.exportingAll', { count: totalCount })}
</div>
<div className="flex gap-3">
<button
onClick={onClose}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
disabled={isExporting}
>
{t('common.cancel')}
</button>
<button
onClick={() => onExport(scope, format, selectedColumns)}
disabled={selectedColumns.length === 0 || isExporting}
className="px-4 py-2 text-sm font-medium text-white bg-green-600 hover:bg-green-700 rounded-lg transition-colors flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isExporting ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
{t('items.export.exporting')}
</>
) : (
<>
<FaDownload />
{t('items.export.exportButton')}
</>
)}
</button>
</div>
</div>
</div>
</div>
);
};
const ItemList: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const setSearchParamsRef = useRef(setSearchParams);
useEffect(() => {
setSearchParamsRef.current = setSearchParams;
}, [setSearchParams]);
const pageFromUrl = useMemo(() => {
const p = parseInt(searchParams.get('page') || '1', 10);
return Math.max(0, Number.isNaN(p) ? 0 : p - 1);
}, [searchParams]);
const page = pageFromUrl;
const setPage = useCallback((zeroBasedPage: number) => {
setSearchParams((prev) => {
const next = new URLSearchParams(prev);
next.set('page', String(zeroBasedPage + 1));
return next;
});
}, [setSearchParams]);
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState<string | null>(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: 'custom_serial_no', label: 'Serial No', default: true },
{ key: 'custom_technical_department', label: 'Technical Department', default: true },
{ key: 'custom_date_in', label: 'Date In', default: true },
{ key: 'custom_w', label: 'Watts', default: true },
{ key: 'custom_volts', label: 'Volts', default: true },
{ key: 'custom_type', label: 'Type', default: true },
{ key: 'custom_code', label: 'Code', 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<Set<string>>(new Set());
// Export modal state
const [showExportModal, setShowExportModal] = useState(false);
const [isExporting, setIsExporting] = useState(false);
const [userRoles, setUserRoles] = useState<string[]>([]);
const [listIsSystemManager, setListIsSystemManager] = useState(false);
useEffect(() => {
const fetchRoles = async () => {
try {
const response = await fetch('/api/method/asset_lite.api.user_roles.get_user_roles', {
credentials: 'include'
});
const data = await response.json();
const rolesList = Array.isArray(data.message) ? data.message : [];
setUserRoles(rolesList);
setListIsSystemManager(rolesList.includes('System Manager'));
} catch (error) {
console.error('Failed to fetch user roles:', error);
}
};
fetchRoles();
}, []);
// Filter states (init from URL so filters persist when navigating back from detail)
const [dateFilterBy, setDateFilterBy] = useState<'' | 'creation' | 'modified'>(() => (searchParams.get('date_filter_by') as '' | 'creation' | 'modified') || '');
const [dateStart, setDateStart] = useState(() => searchParams.get('date_start') || '');
const [dateEnd, setDateEnd] = useState(() => searchParams.get('date_end') || '');
const [filterItemId, setFilterItemId] = useState(() => searchParams.get('item_id') || '');
const [filterItemGroup, setFilterItemGroup] = useState(() => searchParams.get('item_group') || '');
const [filterCompany, setFilterCompany] = useState(() => searchParams.get('company') || '');
const [filterItemName, setFilterItemName] = useState(() => searchParams.get('item_name') || '');
// Sort state (init from URL)
const sortOptions = ['creation desc', 'creation asc', 'modified desc', 'modified asc', 'item_name asc', 'item_name desc', 'name asc', 'name desc'] as const;
const [sortBy, setSortBy] = useState<string>(() => searchParams.get('sort_by') || 'creation desc');
// Advanced filter UI states
const [isFilterExpanded, setIsFilterExpanded] = useState(true);
const [activeFilterCount, setActiveFilterCount] = useState(0);
const [savedFilters, setSavedFilters] = useState<any[]>([]);
const [showSaveFilterModal, setShowSaveFilterModal] = useState(false);
const [filterPresetName, setFilterPresetName] = useState('');
// Temporary states for text inputs (item_name init from URL for persistence)
const [tempItemName, setTempItemName] = useState(() => searchParams.get('item_name') || '');
// Debounce timer refs
const itemNameDebounceRef = useRef<number | null>(null);
// Load saved filters from localStorage on mount
useEffect(() => {
const saved = localStorage.getItem('itemFilterPresets');
if (saved) {
setSavedFilters(JSON.parse(saved));
}
}, []);
const hasDateFilter = dateFilterBy && (dateStart || dateEnd);
// Update active filter count
useEffect(() => {
const count = [filterItemId, filterItemGroup, filterCompany, filterItemName].filter(Boolean).length + (hasDateFilter ? 1 : 0);
setActiveFilterCount(count);
}, [filterItemId, filterItemGroup, filterCompany, filterItemName, hasDateFilter]);
// Build filters object
const filters: Record<string, any> = {};
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}%`];
Object.assign(filters, buildDateRangeFilters(dateFilterBy, dateStart, dateEnd));
filters['custom_delete_status'] = ['!=', 'Deleted'];
const orderBy = sortOptions.includes(sortBy as any) ? sortBy : 'creation desc';
const { items, totalCount, hasMore, loading, error, refetch } = useItems(
Object.keys(filters).length > 0 ? filters : undefined,
limit,
page * limit,
orderBy
);
const { deleteItem, loading: mutationLoading } = useItemMutations();
// Mark initial load complete
useEffect(() => {
if (!loading && !initialLoadComplete) {
setInitialLoadComplete(true);
}
}, [loading, initialLoadComplete]);
// Sync filters to URL when user changes a filter (not on mount/return from detail)
const filtersChangedOnce = useRef(false);
useEffect(() => {
if (!filtersChangedOnce.current) {
filtersChangedOnce.current = true;
return;
}
setSearchParamsRef.current((prev) => {
const next = new URLSearchParams(prev);
if (dateFilterBy) next.set('date_filter_by', dateFilterBy); else next.delete('date_filter_by');
if (dateStart) next.set('date_start', dateStart); else next.delete('date_start');
if (dateEnd) next.set('date_end', dateEnd); else next.delete('date_end');
if (filterItemId) next.set('item_id', filterItemId); else next.delete('item_id');
if (filterItemGroup) next.set('item_group', filterItemGroup); else next.delete('item_group');
if (filterCompany) next.set('company', filterCompany); else next.delete('company');
if (filterItemName) next.set('item_name', filterItemName); else next.delete('item_name');
if (sortBy && sortBy !== 'creation desc') next.set('sort_by', sortBy); else next.delete('sort_by');
next.set('page', '1');
return next;
});
}, [dateFilterBy, dateStart, dateEnd, filterItemId, filterItemGroup, filterCompany, filterItemName, sortBy]);
// Clear selection when filters/page change
useEffect(() => {
setSelectedRows(new Set());
}, [dateFilterBy, dateStart, dateEnd, 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<HTMLInputElement>) => {
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: {
dateFilterBy,
dateStart,
dateEnd,
sortBy,
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;
setDateFilterBy(f.dateFilterBy || ''); setDateStart(f.dateStart || ''); setDateEnd(f.dateEnd || '');
setSortBy(f.sortBy || 'creation desc');
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 = () => {
setDateFilterBy(''); setDateStart(''); setDateEnd('');
setSortBy('creation desc');
setFilterItemId('');
setFilterItemGroup('');
setFilterCompany('');
setFilterItemName('');
setTempItemName('');
if (itemNameDebounceRef.current) clearTimeout(itemNameDebounceRef.current);
setSearchParams((prev) => {
const next = new URLSearchParams(prev);
next.delete('date_filter_by'); next.delete('date_start'); next.delete('date_end');
next.delete('sort_by');
next.delete('item_id');
next.delete('item_group');
next.delete('company');
next.delete('item_name');
next.set('page', '1');
return next;
});
};
const hasActiveFilters = hasDateFilter || filterItemId || filterItemGroup || filterCompany || filterItemName;
const getDeleteStatusRowClass = (deleteStatus: string | undefined): string => {
switch (deleteStatus) {
case 'Delete Request With Supervisor':
return 'bg-orange-50 dark:bg-orange-900/10';
case 'Delete Request With CM':
return 'bg-yellow-50 dark:bg-yellow-900/10';
case 'Deleted':
return 'bg-red-50 dark:bg-red-900/10';
default:
return '';
}
};
// 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<any[]> => {
const allItems: any[] = [];
let currentPage = 0;
const pageSize = 100;
let hasMoreData = true;
const filterArrays = toFrappeFilterArray(filters);
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: filterArrays.length > 0 ? filterArrays : {},
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(t('assets.noDataToExport'));
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 handleView = (itemName: string) => {
navigate(`/inventory/${encodeURIComponent(itemName)}`);
};
// const handleEdit = (itemName: string) => {
// navigate(`/inventory/${itemName}`);
// };
const handleEdit = (itemName: string) => {
navigate(`/inventory/${encodeURIComponent(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}`);
// };
const handleDuplicate = (itemName: string) => {
navigate(`/inventory/new?duplicate=${encodeURIComponent(itemName)}`);
};
// Only show full-page loader on initial load
if (loading && !initialLoadComplete) {
return (
<div className="flex items-center justify-center h-screen bg-gray-50 dark:bg-gray-900">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto"></div>
<p className="mt-4 text-gray-600 dark:text-gray-400">{t('items.loadingItems')}</p>
</div>
</div>
);
}
if (error) {
return (
<div className="p-6 bg-gray-50 dark:bg-gray-900 min-h-screen">
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-6">
<h2 className="text-xl font-bold text-red-800 dark:text-red-300 mb-4">{t('items.errorLoadingItems')}</h2>
<div className="text-red-700 dark:text-red-400 space-y-3">
<p><strong>{t('items.failedToLoadItems')}</strong></p>
<div className="mt-4 flex gap-3">
<button
onClick={() => navigate('/inventory/new')}
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded"
>
{t('items.addItem')}
</button>
<button
onClick={refetch}
className="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded"
>
{t('common.tryAgain')}
</button>
</div>
</div>
<div className="mt-4 p-4 bg-white dark:bg-gray-800 rounded border border-red-300 dark:border-red-700">
<p className="text-sm text-gray-600 dark:text-gray-400">
<strong>Technical Error:</strong> {error}
</p>
</div>
</div>
</div>
);
}
return (
<div className="p-6 bg-gray-50 dark:bg-gray-900 min-h-screen">
{/* Header */}
<div className="mb-6 flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold text-gray-800 dark:text-white">{t('items.listTitle')}</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
{t('items.listTotal', { count: totalCount })}
{/* Show selection count */}
{selectedRows.size > 0 && (
<span className="ml-2 text-blue-600 dark:text-blue-400">
{selectedRows.size} {t('common.selected')}
</span>
)}
{/* Show loading indicator inline when filtering */}
{loading && initialLoadComplete && (
<span className="ml-2 inline-flex items-center gap-1 text-xs text-blue-600 dark:text-blue-400">
<div className="animate-spin rounded-full h-3 w-3 border-b-2 border-blue-500"></div>
{t('common.updating')}
</span>
)}
</p>
</div>
<div className="flex gap-3">
{/* Export Button */}
<button
onClick={() => setShowExportModal(true)}
className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg flex items-center gap-2 shadow transition-all"
disabled={totalCount === 0}
>
<FaFileExport />
<span className="font-medium">{t('listPages.export')}</span>
{selectedRows.size > 0 && (
<span className="bg-white/20 px-1.5 py-0.5 rounded text-xs">
{selectedRows.size}
</span>
)}
</button>
<button
onClick={handleCreateNew}
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg flex items-center gap-2 shadow-lg transition-all hover:shadow-xl"
>
<FaPlus />
<span className="font-medium">{t('items.listAddItem')}</span>
</button>
</div>
</div>
{/* Advanced Filter Panel */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 mb-4">
{/* Filter Header */}
<div className="bg-gradient-to-r from-blue-500 to-blue-600 dark:from-blue-600 dark:to-blue-700 px-4 py-3 rounded-t-lg">
<div className="flex items-center justify-between gap-4">
{/* Left Side - Toggle & Title */}
<div className="flex items-center gap-3 flex-shrink-0">
<button
onClick={() => setIsFilterExpanded(!isFilterExpanded)}
className="text-white hover:bg-white/20 p-2 rounded-lg transition-all"
>
{isFilterExpanded ? <FaChevronUp size={14} /> : <FaChevronDown size={14} />}
</button>
<div className="flex items-center gap-2">
<FaFilter className="text-white" size={16} />
<h3 className="text-white font-semibold text-sm">{t('listPages.filters')}</h3>
</div>
{activeFilterCount > 0 && (
<span className="bg-white text-blue-600 px-2 py-0.5 rounded-full text-xs font-bold">
{activeFilterCount}
</span>
)}
</div>
{/* Center - Active Filter Tags (scrollable) */}
{hasActiveFilters && (
<div className="flex-1 overflow-x-auto scrollbar-hide mx-2">
<div className="flex items-center gap-2 py-1">
{hasDateFilter && (
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-white/90 text-indigo-700 rounded-full text-[10px] font-medium whitespace-nowrap shadow-sm">
<span className="font-semibold">{t('filters.filterBy')}:</span> {dateFilterBy === 'creation' ? t('filters.createdDate') : t('filters.latestModifiedDate')} {dateStart && ` ${dateStart}`} {dateEnd && ` - ${dateEnd}`}
<button onClick={(e) => { e.stopPropagation(); setDateFilterBy(''); setDateStart(''); setDateEnd(''); }} className="hover:text-red-500 transition-colors"><FaTimes className="text-[9px]" /></button>
</span>
)}
{filterItemId && (
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-white/90 text-blue-700 rounded-full text-[10px] font-medium whitespace-nowrap shadow-sm">
<span className="font-semibold">Item ID:</span> {filterItemId}
<button
onClick={(e) => { e.stopPropagation(); setFilterItemId(''); }}
className="hover:text-red-500 transition-colors"
>
<FaTimes className="text-[9px]" />
</button>
</span>
)}
{filterItemName && (
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-white/90 text-cyan-700 rounded-full text-[10px] font-medium whitespace-nowrap shadow-sm">
<span className="font-semibold">Name:</span> {filterItemName}
<button
onClick={(e) => { e.stopPropagation(); setFilterItemName(''); setTempItemName(''); }}
className="hover:text-red-500 transition-colors"
>
<FaTimes className="text-[9px]" />
</button>
</span>
)}
{filterCompany && (
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-white/90 text-green-700 rounded-full text-[10px] font-medium whitespace-nowrap shadow-sm">
<span className="font-semibold">Hospital:</span> {filterCompany}
<button
onClick={(e) => { e.stopPropagation(); setFilterCompany(''); }}
className="hover:text-red-500 transition-colors"
>
<FaTimes className="text-[9px]" />
</button>
</span>
)}
{filterItemGroup && (
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-white/90 text-purple-700 rounded-full text-[10px] font-medium whitespace-nowrap shadow-sm">
<span className="font-semibold">Item Group:</span> {filterItemGroup}
<button
onClick={(e) => { e.stopPropagation(); setFilterItemGroup(''); }}
className="hover:text-red-500 transition-colors"
>
<FaTimes className="text-[9px]" />
</button>
</span>
)}
</div>
</div>
)}
{/* Right Side - Action Buttons */}
<div className="flex items-center gap-2 flex-shrink-0">
{/* Save Filter Button */}
{activeFilterCount > 0 && (
<button
onClick={() => setShowSaveFilterModal(true)}
className="px-3 py-1.5 bg-white text-blue-600 hover:bg-blue-50 rounded-md text-xs font-medium transition-all flex items-center gap-1.5"
>
<FaSave size={12} />
<span className="hidden sm:inline">Save</span>
</button>
)}
{/* Clear All Button */}
{hasActiveFilters && (
<button
onClick={handleClearFilters}
className="px-3 py-1.5 bg-red-500 hover:bg-red-600 text-white rounded-md text-xs font-medium transition-all flex items-center gap-1.5"
>
<FaTimes size={12} />
<span className="hidden sm:inline">{t('listPages.clear')}</span>
</button>
)}
</div>
</div>
</div>
{/* Expandable Filter Content */}
{isFilterExpanded && (
<div className="p-4">
{/* Saved Filter Presets */}
{savedFilters.length > 0 && (
<div className="mb-4 pb-4 border-b border-gray-200 dark:border-gray-700">
<h4 className="text-xs font-semibold text-gray-700 dark:text-gray-300 mb-2 flex items-center gap-2">
<FaStar className="text-yellow-500" size={12} />
{t('listPages.savedFilters')}
</h4>
<div className="flex flex-wrap gap-2">
{savedFilters.map((preset) => (
<div
key={preset.id}
className="group relative inline-flex items-center gap-2 px-3 py-1.5 bg-gradient-to-r from-purple-100 to-blue-100 dark:from-purple-900/30 dark:to-blue-900/30 border border-purple-200 dark:border-purple-700 rounded-lg hover:shadow-md transition-all"
>
<button
onClick={() => handleLoadFilterPreset(preset)}
className="text-xs font-medium text-purple-700 dark:text-purple-300"
>
{preset.name}
</button>
<button
onClick={() => handleDeleteFilterPreset(preset.id)}
className="opacity-0 group-hover:opacity-100 text-red-500 hover:text-red-700 transition-opacity"
>
<FaTrash size={10} />
</button>
</div>
))}
</div>
</div>
)}
{/* Filter Grid */}
<div className="space-y-5">
<div className="bg-gray-50 dark:bg-gray-900/50 p-3 rounded-lg">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
{/* Sort By */}
<div className="relative">
<label className="block text-[10px] font-medium text-gray-700 dark:text-gray-300 mb-0.5">{t('filters.sortBy')}</label>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
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"
>
<option value="creation desc">{t('filters.sortCreationNewest')}</option>
<option value="creation asc">{t('filters.sortCreationOldest')}</option>
<option value="modified desc">{t('filters.sortModifiedNewest')}</option>
<option value="modified asc">{t('filters.sortModifiedOldest')}</option>
<option value="item_name asc">{t('filters.sortItemNameAsc')}</option>
<option value="item_name desc">{t('filters.sortItemNameDesc')}</option>
<option value="name asc">{t('filters.sortNameAsc')}</option>
<option value="name desc">{t('filters.sortNameDesc')}</option>
</select>
</div>
<div className="relative">
<label className="block text-[10px] font-medium text-gray-700 dark:text-gray-300 mb-0.5">{t('filters.filterBy')}</label>
<select value={dateFilterBy} onChange={(e) => { const v = e.target.value as '' | 'creation' | 'modified'; setDateFilterBy(v); }} 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">
<option value="">{t('filters.filterBy')}</option>
<option value="creation">{t('filters.createdDate')}</option>
<option value="modified">{t('filters.latestModifiedDate')}</option>
</select>
</div>
{dateFilterBy && (
<>
<div className="relative">
<label className="block text-[10px] font-medium text-gray-700 dark:text-gray-300 mb-0.5">{t('filters.startDate')}</label>
<input type="date" value={dateStart} onChange={(e) => { const v = e.target.value; setDateStart(v); if (dateEnd && v > dateEnd) setDateEnd(v); }} 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" />
</div>
<div className="relative">
<label className="block text-[10px] font-medium text-gray-700 dark:text-gray-300 mb-0.5">{t('filters.endDate')}</label>
<input type="date" value={dateEnd} onChange={(e) => setDateEnd(e.target.value)} min={dateStart || undefined} 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" />
</div>
</>
)}
{/* Item ID */}
<div className="relative z-[60]">
<LinkField
label={t('items.itemId')}
doctype="Item"
value={filterItemId}
onChange={(val) => setFilterItemId(val)}
placeholder={t('linkField.selectLabel', { label: t('items.itemId') })}
disabled={false}
compact={true}
filters={{ custom_delete_status: ['!=', 'Deleted'] }}
/>
{filterItemId && (
<button
onClick={() => setFilterItemId('')}
className="absolute right-2 top-6 text-gray-400 hover:text-red-500 transition-colors z-10"
>
<FaTimes size={10} />
</button>
)}
</div>
{/* Item Name */}
<div className="relative">
<label className="block text-[10px] font-medium text-gray-700 dark:text-gray-300 mb-0.5">
{t('items.itemName')}
</label>
<input
type="text"
value={tempItemName}
onChange={(e) => handleItemNameChange(e.target.value)}
onKeyDown={handleKeyPress}
placeholder={t('common.typeToSearch')}
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 && (
<span className="absolute right-2 top-6 text-[9px] text-blue-500 animate-pulse">
{t('common.typing')}
</span>
)}
{filterItemName && (
<button
onClick={() => {
setFilterItemName('');
setTempItemName('');
}}
className="absolute right-2 top-6 text-gray-400 hover:text-red-500 transition-colors"
>
<FaTimes size={10} />
</button>
)}
</div>
{/* Hospital */}
<div className="relative z-[59]">
<LinkField
label={t('items.selectHospital')}
doctype="Company"
value={filterCompany}
onChange={(val) => setFilterCompany(val)}
placeholder={t('linkField.selectLabel', { label: t('items.selectHospital') })}
disabled={false}
compact={true}
filters={{ domain: 'Healthcare' }}
/>
{filterCompany && (
<button
onClick={() => setFilterCompany('')}
className="absolute right-2 top-6 text-gray-400 hover:text-red-500 transition-colors z-10"
>
<FaTimes size={10} />
</button>
)}
</div>
{/* Item Group */}
<div className="relative z-[58]">
<LinkField
label={t('items.itemGroup')}
doctype="Item Group"
value={filterItemGroup}
onChange={(val) => setFilterItemGroup(val)}
placeholder={t('linkField.selectLabel', { label: t('items.itemGroup') })}
disabled={false}
compact={true}
/>
{filterItemGroup && (
<button
onClick={() => setFilterItemGroup('')}
className="absolute right-2 top-6 text-gray-400 hover:text-red-500 transition-colors z-10"
>
<FaTimes size={10} />
</button>
)}
</div>
</div>
</div>
</div>
</div>
)}
</div>
{/* Save Filter Modal */}
{showSaveFilterModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full p-6 animate-scale-in">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
{t('common.saveFilterPreset')}
</h3>
<input
type="text"
value={filterPresetName}
onChange={(e) => setFilterPresetName(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleSaveFilterPreset();
}
}}
placeholder={t('common.enterFilterNameExample')}
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
/>
<div className="flex gap-2 justify-end">
<button
onClick={() => {
setShowSaveFilterModal(false);
setFilterPresetName('');
}}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors"
>
{t('common.cancel')}
</button>
<button
onClick={handleSaveFilterPreset}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors flex items-center gap-2"
>
<FaSave size={12} />
{t('common.saveFilter')}
</button>
</div>
</div>
</div>
)}
{/* Export Modal */}
<ExportModal
isOpen={showExportModal}
onClose={() => setShowExportModal(false)}
selectedCount={selectedRows.size}
totalCount={totalCount}
pageCount={items.length}
onExport={handleExport}
isExporting={isExporting}
exportColumns={EXPORT_COLUMNS}
/>
{/* Items Table */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden relative">
{/* Inline loading overlay for filter changes */}
{loading && initialLoadComplete && (
<div className="absolute inset-0 bg-white/60 dark:bg-gray-800/60 flex items-center justify-center z-10 backdrop-blur-[1px]">
<div className="flex items-center gap-3 bg-white dark:bg-gray-700 px-4 py-2 rounded-lg shadow-lg">
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-blue-500"></div>
<span className="text-sm text-gray-600 dark:text-gray-300">
{t('common.filtering')}
</span>
</div>
</div>
)}
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-100 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
<tr>
{/* Select All Checkbox Column */}
<th className="px-4 py-3 text-left">
<button
onClick={handleSelectAll}
className="text-gray-500 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
title={isAllSelected ? "Deselect all" : "Select all"}
>
{isAllSelected ? (
<FaCheckSquare className="text-blue-600 dark:text-blue-400" size={18} />
) : isSomeSelected ? (
<div className="relative">
<FaSquare size={18} />
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-2 h-0.5 bg-current"></div>
</div>
</div>
) : (
<FaSquare size={18} />
)}
</button>
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t('items.itemCode')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t('items.itemName')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t('items.itemGroup')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t('items.selectHospital')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t('items.serialNo')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t('items.dateIn')}
</th>
{/* <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t('items.watts')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t('items.volts')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t('items.type')}
</th> */}
{/* <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t('items.code')}
</th> */}
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t('commonFields.modifiedOn')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t('listPages.actions')}
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{items.length === 0 ? (
<tr>
<td colSpan={13} className="px-6 py-12 text-center text-gray-500 dark:text-gray-400">
<div className="flex flex-col items-center">
<FaBox className="text-4xl text-gray-300 dark:text-gray-600 mb-2" />
<p>{t('items.noItemsFound')}</p>
{hasActiveFilters ? (
<button
onClick={handleClearFilters}
className="mt-4 text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 underline"
>
{t('common.clearFilters')}
</button>
) : (
<button
onClick={handleCreateNew}
className="mt-4 text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 underline"
>
{t('items.createFirstItem')}
</button>
)}
</div>
</td>
</tr>
) : (
items.map((item) => (
<tr
key={item.name}
className={`hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors cursor-pointer ${
selectedRows.has(item.name)
? 'bg-blue-50 dark:bg-blue-900/20'
: getDeleteStatusRowClass(item.custom_delete_status)
}`}
title={item.custom_delete_status ? `Delete Status: ${item.custom_delete_status}` : undefined}
onClick={() => handleView(item.name)}
>
{/* Row Checkbox */}
<td className="px-4 py-4" onClick={(e) => e.stopPropagation()}>
<button
onClick={() => handleSelectRow(item.name)}
className="text-gray-500 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
>
{selectedRows.has(item.name) ? (
<FaCheckSquare className="text-blue-600 dark:text-blue-400" size={18} />
) : (
<FaSquare size={18} />
)}
</button>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900 dark:text-white">
{item.item_code || item.name}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-700 dark:text-gray-300">
{item.item_name || '-'}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-700 dark:text-gray-300">
{item.item_group || '-'}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-700 dark:text-gray-300">
{item.custom_hospital_name || '-'}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-700 dark:text-gray-300">
{item.custom_serial_no || '-'}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-700 dark:text-gray-300">
{item.custom_date_in ? formatDate(item.custom_date_in) : '-'}
</div>
</td>
{/* <td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-700 dark:text-gray-300">
{item.custom_w ?? '-'}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-700 dark:text-gray-300">
{item.custom_volts ?? '-'}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-700 dark:text-gray-300">
{item.custom_type || '-'}
</div>
</td> */}
{/* <td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-700 dark:text-gray-300">
{item.custom_code || '-'}
</div>
</td> */}
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-700 dark:text-gray-300">
{formatDate(item.modified)}
</div>
</td>
{/* Inline Action Icons */}
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
<button
onClick={() => handleView(item.name)}
className="text-blue-600 dark:text-blue-400 hover:text-blue-900 dark:hover:text-blue-300 p-2 hover:bg-blue-50 dark:hover:bg-blue-900/30 rounded transition-colors"
title="View Details"
>
<FaEye />
</button>
<button
onClick={() => handleEdit(item.name)}
className="text-green-600 dark:text-green-400 hover:text-green-900 dark:hover:text-green-300 p-2 hover:bg-green-50 dark:hover:bg-green-900/30 rounded transition-colors"
title="Edit Item"
>
<FaEdit />
</button>
<button
onClick={() => handleDuplicate(item.name)}
className="text-purple-600 dark:text-purple-400 hover:text-purple-900 dark:hover:text-purple-300 p-2 hover:bg-purple-50 dark:hover:bg-purple-900/30 rounded transition-colors"
title="Duplicate Item"
>
<FaCopy />
</button>
{/* <button
onClick={() => setDeleteConfirmOpen(item.name)}
className="text-red-600 dark:text-red-400 hover:text-red-900 dark:hover:text-red-300 p-2 hover:bg-red-50 dark:hover:bg-red-900/30 rounded transition-colors"
title={t('items.deleteItem')}
disabled={mutationLoading}
>
<FaTrash />
</button> */}
<div onClick={(e) => e.stopPropagation()}>
<DeleteRequestButton
doctype="Item"
docname={item.name}
currentDeleteStatus={(item.custom_delete_status ?? null) as DeleteStatus}
userRoles={userRoles}
isSystemManager={listIsSystemManager}
triggerMode
redirectOnDelete="/inventory"
onStatusChange={() => refetch()}
/>
</div>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* Pagination */}
{/* <ListPagination
currentPage={page + 1}
totalCount={0}
pageSize={limit}
hasMore={hasMore}
itemLabel={t('pagination.items')}
onPageChange={(p) => setPage(Math.max(0, p - 1))}
/> */}
<ListPagination
currentPage={page + 1}
totalCount={totalCount}
pageSize={limit}
hasMore={hasMore}
itemLabel={t('pagination.items')}
onPageChange={(p) => setPage(Math.max(0, p - 1))}
/>
</div>
{/* Delete Confirmation Modal */}
{deleteConfirmOpen && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4 shadow-2xl">
<div className="flex items-start gap-4">
<div className="flex-shrink-0 w-12 h-12 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center">
<FaTrash className="text-red-600 dark:text-red-400 text-xl" />
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
{t('items.deleteItem')}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
{t('items.deleteConfirmMessage')}
</p>
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-md p-3 mb-4">
<p className="text-xs text-yellow-800 dark:text-yellow-300">
<strong>{t('items.itemId')}:</strong> {deleteConfirmOpen}
</p>
</div>
<div className="flex gap-3 justify-end">
<button
onClick={() => setDeleteConfirmOpen(null)}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-lg transition-colors"
disabled={mutationLoading}
>
{t('common.cancel')}
</button>
<button
onClick={() => handleDelete(deleteConfirmOpen)}
className="px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 rounded-lg transition-colors flex items-center gap-2 disabled:opacity-50"
disabled={mutationLoading}
>
{mutationLoading ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
{t('common.deleting')}
</>
) : (
<>
<FaTrash />
{t('items.deleteItem')}
</>
)}
</button>
</div>
</div>
</div>
</div>
</div>
)}
<style>{`
@keyframes scale-in {
from {
transform: scale(0.95);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
.animate-scale-in {
animation: scale-in 0.2s ease-out;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
`}</style>
</div>
);
};
export default ItemList;