Added the Asset Filters in List view
This commit is contained in:
parent
c3a42d67b5
commit
9cf1f3201b
@ -61,45 +61,80 @@ const LinkField: React.FC<LinkFieldProps> = ({
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||
setDropdownOpen(false);
|
||||
|
||||
// Reset search text to current value when closing
|
||||
setSearchText('');
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
// Handle selecting an item from dropdown
|
||||
const handleSelect = (selectedValue: string) => {
|
||||
onChange(selectedValue);
|
||||
setSearchText('');
|
||||
setDropdownOpen(false);
|
||||
};
|
||||
|
||||
// Handle clearing the field
|
||||
const handleClear = () => {
|
||||
onChange('');
|
||||
setSearchText('');
|
||||
setDropdownOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="relative w-full mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
{label}
|
||||
</label>
|
||||
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
value={isDropdownOpen ? searchText : value}
|
||||
placeholder={placeholder || `Select ${label}`}
|
||||
disabled={disabled}
|
||||
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 disabled:bg-gray-100 dark:disabled:bg-gray-700
|
||||
bg-white dark:bg-gray-700 text-gray-900 dark:text-white`}
|
||||
onFocus={() => !disabled && setDropdownOpen(true)}
|
||||
bg-white dark:bg-gray-700 text-gray-900 dark:text-white ${value ? 'pr-8' : ''}`}
|
||||
onFocus={() => {
|
||||
if (!disabled) {
|
||||
setDropdownOpen(true);
|
||||
setSearchText('');
|
||||
}
|
||||
}}
|
||||
onChange={(e) => {
|
||||
const text = e.target.value;
|
||||
setSearchText(text);
|
||||
searchLink(text);
|
||||
onChange(text);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Clear button - only show when there's a selected value and not disabled */}
|
||||
{value && !disabled && !isDropdownOpen && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClear}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isDropdownOpen && searchResults.length > 0 && !disabled && (
|
||||
<ul className="absolute z-50 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600
|
||||
rounded-md mt-1 max-h-48 overflow-auto w-full shadow-lg">
|
||||
{searchResults.map((item, idx) => (
|
||||
<li
|
||||
key={idx}
|
||||
onClick={() => {
|
||||
onChange(item.value);
|
||||
setDropdownOpen(false);
|
||||
}}
|
||||
// onClick={() => {
|
||||
// onChange(item.value);
|
||||
// setDropdownOpen(false);
|
||||
// }}
|
||||
onClick={() => handleSelect(item.value)}
|
||||
className={`px-3 py-2 cursor-pointer
|
||||
text-gray-900 dark:text-gray-100
|
||||
hover:bg-blue-500 dark:hover:bg-blue-600 hover:text-white
|
||||
|
||||
@ -3,7 +3,7 @@ import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useAssetDetails, useAssetMutations } from '../hooks/useAsset';
|
||||
import { useDocTypeMeta } from '../hooks/useDocTypeMeta';
|
||||
import { FaArrowLeft, FaSave, FaEdit, FaQrcode, FaCheck } from 'react-icons/fa';
|
||||
import type { CreateAssetData } from '../services/assetService';
|
||||
import type { CreateAssetData,AssetFinanceBookRow } from '../services/assetService';
|
||||
|
||||
import LinkField from '../components/LinkField';
|
||||
import apiService from '../services/apiService';
|
||||
@ -535,6 +535,22 @@ const AssetDetail: React.FC = () => {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Device Status <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
name="custom_device_status"
|
||||
value={formData.custom_device_status}
|
||||
onChange={handleChange}
|
||||
disabled={!isEditing}
|
||||
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 disabled:bg-gray-100 dark:disabled:bg-gray-700 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
>
|
||||
{/* <option value="">Select class</option> */}
|
||||
<option value="Up">Up</option>
|
||||
<option value="Down">Down</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -710,7 +726,7 @@ const AssetDetail: React.FC = () => {
|
||||
More Details
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
{/* <div>
|
||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Condition
|
||||
</label>
|
||||
@ -726,7 +742,7 @@ const AssetDetail: React.FC = () => {
|
||||
<option value="Under Maintenance">Under Maintenance</option>
|
||||
<option value="Decommissioned">Decommissioned</option>
|
||||
</select>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{/* QR Code */}
|
||||
<div className="flex flex-col items-center my-4">
|
||||
@ -1018,14 +1034,206 @@ const AssetDetail: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Financial Details - Full Width */}
|
||||
|
||||
{/* Updated Financial Details */}
|
||||
<div className="mt-6">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-800 dark:text-white mb-4">Financial Details</h2>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
The depreciation method is an accounting method used to allocate the cost of a tangible asset over its useful life.
|
||||
</p>
|
||||
|
||||
<div className="flex items-center mb-6">
|
||||
<input
|
||||
id="calculate_depreciation"
|
||||
type="checkbox"
|
||||
checked={formData.calculate_depreciation}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
calculate_depreciation: e.target.checked,
|
||||
})
|
||||
}
|
||||
disabled={!isEditing}
|
||||
className="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
/>
|
||||
<label
|
||||
htmlFor="calculate_depreciation"
|
||||
className="ml-2 text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Calculate Depreciation
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Asset Finance Book child table — shown only when checkbox checked */}
|
||||
{formData.calculate_depreciation && (
|
||||
<div className="border-t pt-4">
|
||||
{/* Header with Add Row button */}
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-md font-semibold text-gray-800 dark:text-white">
|
||||
Asset Finance Books
|
||||
</h3>
|
||||
{isEditing && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={addFinanceRow}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 flex items-center gap-2"
|
||||
>
|
||||
<span>+</span> Add Row
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Show message if no rows */}
|
||||
{(!formData.finance_books || formData.finance_books.length === 0) && (
|
||||
<div className="text-center py-8 text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-900 rounded-lg">
|
||||
No finance books added yet. Click "Add Row" to add one.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TABLE - Full width desktop view with overflow fix */}
|
||||
{formData.finance_books && formData.finance_books.length > 0 && (
|
||||
<div className="overflow-visible">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-100 dark:bg-gray-700 border-b border-gray-300 dark:border-gray-600">
|
||||
<th className="text-left px-4 py-3 text-sm font-semibold text-gray-700 dark:text-gray-300 min-w-[200px]">
|
||||
Finance Book
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-sm font-semibold text-gray-700 dark:text-gray-300 min-w-[200px]">
|
||||
Depreciation Method*
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-sm font-semibold text-gray-700 dark:text-gray-300 min-w-[180px]">
|
||||
Total Depreciations*
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-sm font-semibold text-gray-700 dark:text-gray-300 min-w-[180px]">
|
||||
Frequency (Months)*
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-sm font-semibold text-gray-700 dark:text-gray-300 min-w-[200px]">
|
||||
Depreciation Posting Date*
|
||||
</th>
|
||||
{isEditing && (
|
||||
<th className="text-center px-4 py-3 text-sm font-semibold text-gray-700 dark:text-gray-300 min-w-[120px]">
|
||||
Action
|
||||
</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{formData.finance_books.map((row: AssetFinanceBookRow, idx: number) => (
|
||||
<tr
|
||||
key={idx}
|
||||
className="border-b border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-750"
|
||||
>
|
||||
{/* Finance Book - with overflow visible */}
|
||||
<td className="px-4 py-3 relative" style={{ overflow: 'visible' }}>
|
||||
<div className="relative z-20">
|
||||
<LinkField
|
||||
label=""
|
||||
doctype="Finance Book"
|
||||
value={row.finance_book || ''}
|
||||
onChange={(val) => updateFinanceRow(idx, { finance_book: val })}
|
||||
disabled={!isEditing}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Depreciation Method */}
|
||||
<td className="px-4 py-3">
|
||||
<select
|
||||
value={row.depreciation_method || ''}
|
||||
onChange={(e) => updateFinanceRow(idx, { depreciation_method: e.target.value })}
|
||||
disabled={!isEditing}
|
||||
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 disabled:bg-gray-100 dark:disabled:bg-gray-700 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
>
|
||||
<option value="">Select Method</option>
|
||||
<option value="Straight Line">Straight Line</option>
|
||||
<option value="Double Declining Balance">Double Declining Balance</option>
|
||||
<option value="Written Down Value">Written Down Value</option>
|
||||
<option value="Manual">Manual</option>
|
||||
</select>
|
||||
</td>
|
||||
|
||||
{/* Total Depreciations */}
|
||||
<td className="px-4 py-3">
|
||||
<input
|
||||
type="number"
|
||||
value={row.total_number_of_depreciations ?? ''}
|
||||
onChange={(e) =>
|
||||
updateFinanceRow(idx, {
|
||||
total_number_of_depreciations: Number(e.target.value),
|
||||
})
|
||||
}
|
||||
disabled={!isEditing}
|
||||
placeholder="0"
|
||||
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 disabled:bg-gray-100 dark:disabled:bg-gray-700 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
/>
|
||||
</td>
|
||||
|
||||
{/* Frequency */}
|
||||
<td className="px-4 py-3">
|
||||
<input
|
||||
type="number"
|
||||
value={row.frequency_of_depreciation ?? ''}
|
||||
onChange={(e) =>
|
||||
updateFinanceRow(idx, {
|
||||
frequency_of_depreciation: Number(e.target.value),
|
||||
})
|
||||
}
|
||||
disabled={!isEditing}
|
||||
placeholder="0"
|
||||
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 disabled:bg-gray-100 dark:disabled:bg-gray-700 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
/>
|
||||
</td>
|
||||
|
||||
{/* Start Date */}
|
||||
<td className="px-4 py-3">
|
||||
<input
|
||||
type="date"
|
||||
value={row.depreciation_start_date || ''}
|
||||
onChange={(e) =>
|
||||
updateFinanceRow(idx, { depreciation_start_date: e.target.value })
|
||||
}
|
||||
disabled={!isEditing}
|
||||
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 disabled:bg-gray-100 dark:disabled:bg-gray-700 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
/>
|
||||
</td>
|
||||
|
||||
{/* REMOVE BUTTON */}
|
||||
{isEditing && (
|
||||
<td className="px-4 py-3 text-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeFinanceRow(idx)}
|
||||
className="px-3 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* Financial Details - Full Width */}
|
||||
{/* <div className="mt-6">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
||||
<h2 className="text-base font-semibold text-gray-800 dark:text-white mb-4 pb-2 border-b border-gray-200 dark:border-gray-700">
|
||||
Financial Details
|
||||
</h2>
|
||||
|
||||
{/* Financial Input Fields */}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
@ -1081,7 +1289,7 @@ const AssetDetail: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Calculate Depreciation Checkbox */}
|
||||
|
||||
<div className="flex items-center mb-6">
|
||||
<input
|
||||
id="calculate_depreciation"
|
||||
@ -1104,7 +1312,6 @@ const AssetDetail: React.FC = () => {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Depreciation Schedule Table */}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse border border-gray-300 dark:border-gray-600">
|
||||
<thead>
|
||||
@ -1127,7 +1334,7 @@ const AssetDetail: React.FC = () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{/* Sample rows - Replace with actual data from API */}
|
||||
|
||||
<tr className="hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
||||
<td className="border border-gray-300 dark:border-gray-600 px-4 py-2 text-xs text-gray-700 dark:text-gray-300">
|
||||
Year 1
|
||||
@ -1188,7 +1395,7 @@ const AssetDetail: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAssets, useAssetMutations } from '../hooks/useAsset';
|
||||
import { FaPlus, FaSearch, FaEdit, FaEye, FaTrash, FaCopy, FaEllipsisV, FaDownload, FaPrint, FaFileExport } from 'react-icons/fa';
|
||||
import { FaPlus, FaSearch, FaEdit, FaEye, FaTrash, FaCopy, FaEllipsisV, FaDownload, FaPrint, FaFileExport, FaTimes } from 'react-icons/fa';
|
||||
import LinkField from '../components/LinkField';
|
||||
|
||||
const AssetList: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
@ -12,8 +13,65 @@ const AssetList: React.FC = () => {
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const limit = 20;
|
||||
|
||||
// Filter states
|
||||
const [filterAssetId, setFilterAssetId] = useState('');
|
||||
const [filterCompany, setFilterCompany] = useState('');
|
||||
const [filterManufacturer, setFilterManufacturer] = useState('');
|
||||
const [filterSupplier, setFilterSupplier] = useState('');
|
||||
const [filterLocation, setFilterLocation] = useState('');
|
||||
const [filterDepartment, setFilterDepartment] = useState('');
|
||||
const [filterModality, setFilterModality] = useState('');
|
||||
const [filterDeviceStatus, setFilterDeviceStatus] = useState('');
|
||||
const [filterAssetName, setFilterAssetName] = useState('');
|
||||
const [filterSerialNumber, setFilterSerialNumber] = useState('');
|
||||
|
||||
// Temporary states for text inputs (not applied until user stops typing or presses Enter)
|
||||
const [tempAssetName, setTempAssetName] = useState('');
|
||||
const [tempSerialNumber, setTempSerialNumber] = useState('');
|
||||
|
||||
// Debounce timer refs
|
||||
const assetNameDebounceRef = useRef<number | null>(null);
|
||||
const serialNumberDebounceRef = useRef<number | null>(null);
|
||||
|
||||
// Build filters object
|
||||
const filters: Record<string, any> = {};
|
||||
if (filterAssetId) {
|
||||
filters['name'] = filterAssetId;
|
||||
}
|
||||
if (filterCompany) {
|
||||
filters['company'] = filterCompany;
|
||||
}
|
||||
if (filterManufacturer) {
|
||||
filters['custom_manufacturer'] = filterManufacturer;
|
||||
}
|
||||
if (filterSupplier) {
|
||||
filters['supplier'] = filterSupplier;
|
||||
}
|
||||
if (filterLocation) {
|
||||
filters['location'] = filterLocation;
|
||||
}
|
||||
if (filterDepartment) {
|
||||
filters['department'] = filterDepartment;
|
||||
}
|
||||
if (filterModality) {
|
||||
filters['custom_modality'] = filterModality;
|
||||
}
|
||||
if (filterDeviceStatus) {
|
||||
filters['custom_device_status'] = filterDeviceStatus;
|
||||
}
|
||||
if (filterAssetName) {
|
||||
filters['asset_name'] = ['like', `%${filterAssetName}%`];
|
||||
}
|
||||
if (filterSerialNumber) {
|
||||
filters['custom_serial_number'] = ['like', `%${filterSerialNumber}%`];
|
||||
}
|
||||
if (searchTerm) {
|
||||
// Search across multiple fields
|
||||
filters['asset_name'] = ['like', `%${searchTerm}%`];
|
||||
}
|
||||
|
||||
const { assets, totalCount, hasMore, loading, error, refetch } = useAssets(
|
||||
{},
|
||||
filters,
|
||||
limit,
|
||||
page * limit,
|
||||
'creation desc'
|
||||
@ -21,6 +79,73 @@ const AssetList: React.FC = () => {
|
||||
|
||||
const { deleteAsset, loading: mutationLoading } = useAssetMutations();
|
||||
|
||||
// Reset page when filters change
|
||||
useEffect(() => {
|
||||
setPage(0);
|
||||
}, [filterAssetId, filterCompany, filterManufacturer, filterSupplier, filterLocation, filterDepartment, filterModality,
|
||||
filterDeviceStatus, filterAssetName, filterSerialNumber, searchTerm]);
|
||||
|
||||
// Debounce function for text inputs
|
||||
const handleAssetNameChange = (value: string) => {
|
||||
setTempAssetName(value);
|
||||
|
||||
// Clear existing timeout
|
||||
if (assetNameDebounceRef.current) {
|
||||
clearTimeout(assetNameDebounceRef.current);
|
||||
}
|
||||
|
||||
// Set new timeout - apply filter after 800ms of no typing
|
||||
assetNameDebounceRef.current = setTimeout(() => {
|
||||
setFilterAssetName(value);
|
||||
}, 800);
|
||||
};
|
||||
|
||||
const handleSerialNumberChange = (value: string) => {
|
||||
setTempSerialNumber(value);
|
||||
|
||||
// Clear existing timeout
|
||||
if (serialNumberDebounceRef.current) {
|
||||
clearTimeout(serialNumberDebounceRef.current);
|
||||
}
|
||||
|
||||
// Set new timeout - apply filter after 800ms of no typing
|
||||
serialNumberDebounceRef.current = setTimeout(() => {
|
||||
setFilterSerialNumber(value);
|
||||
}, 800);
|
||||
};
|
||||
|
||||
// Handle Enter key press for immediate filter application
|
||||
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>, type: 'assetName' | 'serialNumber') => {
|
||||
if (e.key === 'Enter') {
|
||||
if (type === 'assetName') {
|
||||
if (assetNameDebounceRef.current) {
|
||||
clearTimeout(assetNameDebounceRef.current);
|
||||
}
|
||||
setFilterAssetName(tempAssetName);
|
||||
} else if (type === 'serialNumber') {
|
||||
if (serialNumberDebounceRef.current) {
|
||||
clearTimeout(serialNumberDebounceRef.current);
|
||||
}
|
||||
setFilterSerialNumber(tempSerialNumber);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Cleanup timeouts on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (assetNameDebounceRef.current) {
|
||||
clearTimeout(assetNameDebounceRef.current);
|
||||
}
|
||||
if (serialNumberDebounceRef.current) {
|
||||
clearTimeout(serialNumberDebounceRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
@ -107,6 +232,34 @@ const AssetList: React.FC = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const handleClearFilters = () => {
|
||||
setFilterAssetId('');
|
||||
setFilterCompany('');
|
||||
setFilterManufacturer('');
|
||||
setFilterSupplier('');
|
||||
setFilterLocation('');
|
||||
setFilterDepartment('');
|
||||
setFilterModality('');
|
||||
setFilterDeviceStatus('');
|
||||
setFilterAssetName('');
|
||||
setFilterSerialNumber('');
|
||||
setTempAssetName('');
|
||||
setTempSerialNumber('');
|
||||
setSearchTerm('');
|
||||
|
||||
// Clear any pending debounce timers
|
||||
if (assetNameDebounceRef.current) {
|
||||
clearTimeout(assetNameDebounceRef.current);
|
||||
}
|
||||
if (serialNumberDebounceRef.current) {
|
||||
clearTimeout(serialNumberDebounceRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
const hasActiveFilters = filterAssetId || filterCompany || filterManufacturer || filterSupplier ||
|
||||
filterLocation || filterDepartment || filterModality || filterDeviceStatus ||
|
||||
filterAssetName || filterSerialNumber || searchTerm;
|
||||
|
||||
if (loading && page === 0) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen bg-gray-50 dark:bg-gray-900">
|
||||
@ -189,7 +342,7 @@ const AssetList: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Search Bar */}
|
||||
<div className="mb-6 bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
||||
{/* <div className="mb-6 bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
||||
<div className="flex items-center gap-2 border border-gray-300 dark:border-gray-600 rounded-lg px-4 py-2 bg-white dark:bg-gray-700">
|
||||
<FaSearch className="text-gray-400 dark:text-gray-500" />
|
||||
<input
|
||||
@ -199,7 +352,333 @@ const AssetList: React.FC = () => {
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="flex-1 outline-none text-gray-700 dark:text-gray-200 bg-transparent"
|
||||
/>
|
||||
{searchTerm && (
|
||||
<button
|
||||
onClick={() => setSearchTerm('')}
|
||||
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
>
|
||||
<FaTimes />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{/* Filter Section */}
|
||||
<div className="mb-6 bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300">Filters</h3>
|
||||
{hasActiveFilters && (
|
||||
<button
|
||||
onClick={handleClearFilters}
|
||||
className="text-sm text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 flex items-center gap-1"
|
||||
>
|
||||
<FaTimes />
|
||||
Clear All Filters
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* First Row - 5 filters */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-4 mb-4">
|
||||
{/* Asset ID Filter */}
|
||||
<div>
|
||||
<LinkField
|
||||
label="Asset ID"
|
||||
doctype="Asset"
|
||||
value={filterAssetId}
|
||||
onChange={(val) => setFilterAssetId(val)}
|
||||
placeholder="Select Asset ID"
|
||||
disabled={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Company Filter */}
|
||||
<div>
|
||||
<LinkField
|
||||
label="Hospital"
|
||||
doctype="Company"
|
||||
value={filterCompany}
|
||||
onChange={(val) => setFilterCompany(val)}
|
||||
placeholder="Select Hospital"
|
||||
disabled={false}
|
||||
filters={{ domain: 'Healthcare' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Location Filter */}
|
||||
<div>
|
||||
<LinkField
|
||||
label="Location"
|
||||
doctype="Location"
|
||||
value={filterLocation}
|
||||
onChange={(val) => setFilterLocation(val)}
|
||||
placeholder="Select Location"
|
||||
disabled={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Department Filter */}
|
||||
<div>
|
||||
<LinkField
|
||||
label="Department"
|
||||
doctype="Department"
|
||||
value={filterDepartment}
|
||||
onChange={(val) => setFilterDepartment(val)}
|
||||
placeholder="Select Department"
|
||||
disabled={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Modality Filter */}
|
||||
<div>
|
||||
<LinkField
|
||||
label="Modality"
|
||||
doctype="Modality"
|
||||
value={filterModality}
|
||||
onChange={(val) => setFilterModality(val)}
|
||||
placeholder="Select Modality"
|
||||
disabled={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Second Row - 5 filters */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-4">
|
||||
{/* Manufacturer Filter */}
|
||||
<div>
|
||||
<LinkField
|
||||
label="Manufacturer"
|
||||
doctype="Manufacturer"
|
||||
value={filterManufacturer}
|
||||
onChange={(val) => setFilterManufacturer(val)}
|
||||
placeholder="Select Manufacturer"
|
||||
disabled={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Supplier Filter */}
|
||||
<div>
|
||||
<LinkField
|
||||
label="Supplier"
|
||||
doctype="Supplier"
|
||||
value={filterSupplier}
|
||||
onChange={(val) => setFilterSupplier(val)}
|
||||
placeholder="Select Supplier"
|
||||
disabled={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Device Status Filter - Dropdown */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Device Status
|
||||
</label>
|
||||
<select
|
||||
value={filterDeviceStatus}
|
||||
onChange={(e) => setFilterDeviceStatus(e.target.value)}
|
||||
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"
|
||||
>
|
||||
<option value="">All Status</option>
|
||||
<option value="Up">Up</option>
|
||||
<option value="Down">Down</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Asset Name Filter - Text Input */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Asset Name
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
value={tempAssetName}
|
||||
onChange={(e) => handleAssetNameChange(e.target.value)}
|
||||
// onKeyPress={(e) => handleKeyPress(e, 'assetName')}
|
||||
onKeyDown={(e) => handleKeyPress(e, 'assetName')}
|
||||
placeholder="Type to search..."
|
||||
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"
|
||||
/>
|
||||
{tempAssetName && tempAssetName !== filterAssetName && (
|
||||
<span className="absolute right-2 top-2 text-xs text-gray-400">
|
||||
typing...
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
Press Enter or wait to apply
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Serial Number Filter - Text Input */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Serial Number
|
||||
</label>
|
||||
{/* <input
|
||||
type="text"
|
||||
value={filterSerialNumber}
|
||||
onChange={(e) => setFilterSerialNumber(e.target.value)}
|
||||
placeholder="Enter serial number"
|
||||
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"
|
||||
/> */}
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
value={tempSerialNumber}
|
||||
onChange={(e) => handleSerialNumberChange(e.target.value)}
|
||||
// onKeyPress={(e) => handleKeyPress(e, 'serialNumber')}
|
||||
onKeyDown={(e) => handleKeyPress(e, 'serialNumber')}
|
||||
placeholder="Type to search..."
|
||||
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"
|
||||
/>
|
||||
{tempSerialNumber && tempSerialNumber !== filterSerialNumber && (
|
||||
<span className="absolute right-2 top-2 text-xs text-gray-400">
|
||||
typing...
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
Press Enter or wait to apply
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Active Filters Display */}
|
||||
{hasActiveFilters && (
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{filterAssetId && (
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded-full text-sm">
|
||||
Asset ID: {filterAssetId}
|
||||
<button
|
||||
onClick={() => setFilterAssetId('')}
|
||||
className="hover:text-blue-600 dark:hover:text-blue-400"
|
||||
>
|
||||
<FaTimes className="text-xs" />
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
{filterCompany && (
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 rounded-full text-sm">
|
||||
Hospital: {filterCompany}
|
||||
<button
|
||||
onClick={() => setFilterCompany('')}
|
||||
className="hover:text-green-600 dark:hover:text-green-400"
|
||||
>
|
||||
<FaTimes className="text-xs" />
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
{filterLocation && (
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 rounded-full text-sm">
|
||||
Location: {filterLocation}
|
||||
<button
|
||||
onClick={() => setFilterLocation('')}
|
||||
className="hover:text-purple-600 dark:hover:text-purple-400"
|
||||
>
|
||||
<FaTimes className="text-xs" />
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
{filterDepartment && (
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200 rounded-full text-sm">
|
||||
Department: {filterDepartment}
|
||||
<button
|
||||
onClick={() => setFilterDepartment('')}
|
||||
className="hover:text-yellow-600 dark:hover:text-yellow-400"
|
||||
>
|
||||
<FaTimes className="text-xs" />
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
{filterModality && (
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 bg-pink-100 dark:bg-pink-900 text-pink-800 dark:text-pink-200 rounded-full text-sm">
|
||||
Modality: {filterModality}
|
||||
<button
|
||||
onClick={() => setFilterModality('')}
|
||||
className="hover:text-pink-600 dark:hover:text-pink-400"
|
||||
>
|
||||
<FaTimes className="text-xs" />
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
{filterManufacturer && (
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 bg-indigo-100 dark:bg-indigo-900 text-indigo-800 dark:text-indigo-200 rounded-full text-sm">
|
||||
Manufacturer: {filterManufacturer}
|
||||
<button
|
||||
onClick={() => setFilterManufacturer('')}
|
||||
className="hover:text-indigo-600 dark:hover:text-indigo-400"
|
||||
>
|
||||
<FaTimes className="text-xs" />
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
{filterSupplier && (
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 bg-teal-100 dark:bg-teal-900 text-teal-800 dark:text-teal-200 rounded-full text-sm">
|
||||
Supplier: {filterSupplier}
|
||||
<button
|
||||
onClick={() => setFilterSupplier('')}
|
||||
className="hover:text-teal-600 dark:hover:text-teal-400"
|
||||
>
|
||||
<FaTimes className="text-xs" />
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
{filterDeviceStatus && (
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 bg-orange-100 dark:bg-orange-900 text-orange-800 dark:text-orange-200 rounded-full text-sm">
|
||||
Status: {filterDeviceStatus}
|
||||
<button
|
||||
onClick={() => setFilterDeviceStatus('')}
|
||||
className="hover:text-orange-600 dark:hover:text-orange-400"
|
||||
>
|
||||
<FaTimes className="text-xs" />
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
{filterAssetName && (
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 bg-cyan-100 dark:bg-cyan-900 text-cyan-800 dark:text-cyan-200 rounded-full text-sm">
|
||||
Asset Name: "{filterAssetName}"
|
||||
<button
|
||||
// onClick={() => setFilterAssetName('')}
|
||||
onClick={() => {
|
||||
setFilterAssetName('');
|
||||
setTempAssetName('');
|
||||
}}
|
||||
className="hover:text-cyan-600 dark:hover:text-cyan-400"
|
||||
>
|
||||
<FaTimes className="text-xs" />
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
{filterSerialNumber && (
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 bg-lime-100 dark:bg-lime-900 text-lime-800 dark:text-lime-200 rounded-full text-sm">
|
||||
Serial: "{filterSerialNumber}"
|
||||
<button
|
||||
// onClick={() => setFilterSerialNumber('')}
|
||||
onClick={() => {
|
||||
setFilterSerialNumber('');
|
||||
setTempSerialNumber('');
|
||||
}}
|
||||
className="hover:text-lime-600 dark:hover:text-lime-400"
|
||||
>
|
||||
<FaTimes className="text-xs" />
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
{searchTerm && (
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 rounded-full text-sm">
|
||||
Search: "{searchTerm}"
|
||||
<button
|
||||
onClick={() => setSearchTerm('')}
|
||||
className="hover:text-purple-600 dark:hover:text-purple-400"
|
||||
>
|
||||
<FaTimes className="text-xs" />
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Assets Table */}
|
||||
@ -235,12 +714,29 @@ const AssetList: React.FC = () => {
|
||||
<div className="flex flex-col items-center">
|
||||
<FaSearch className="text-4xl text-gray-300 dark:text-gray-600 mb-2" />
|
||||
<p>No assets found</p>
|
||||
{/* <button
|
||||
onClick={handleCreateNew}
|
||||
className="mt-4 text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 underline"
|
||||
>
|
||||
Create your first asset
|
||||
</button> */}
|
||||
|
||||
{hasActiveFilters ? (
|
||||
<button
|
||||
onClick={handleClearFilters}
|
||||
className="mt-4 text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 underline"
|
||||
>
|
||||
Clear filters
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleCreateNew}
|
||||
className="mt-4 text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 underline"
|
||||
>
|
||||
Create your first asset
|
||||
</button>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@ -265,7 +761,8 @@ const AssetList: React.FC = () => {
|
||||
{asset.location || '-'}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className={`
|
||||
{asset.custom_device_status || '-'}
|
||||
{/* <span className={`
|
||||
px-3 py-1 inline-flex text-xs leading-5 font-semibold rounded-full
|
||||
${asset.custom_device_status === 'Operational' ? 'bg-green-100 text-green-800' : ''}
|
||||
${asset.custom_device_status === 'Under Maintenance' ? 'bg-yellow-100 text-yellow-800' : ''}
|
||||
@ -273,7 +770,7 @@ const AssetList: React.FC = () => {
|
||||
${!asset.custom_device_status ? 'bg-gray-100 text-gray-800' : ''}
|
||||
`}>
|
||||
{asset.custom_device_status || 'Unknown'}
|
||||
</span>
|
||||
</span> */}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user