diff --git a/src/components/LinkField.tsx b/src/components/LinkField.tsx index c5a11c9..5f40da2 100644 --- a/src/components/LinkField.tsx +++ b/src/components/LinkField.tsx @@ -61,34 +61,68 @@ const LinkField: React.FC = ({ 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 (
- !disabled && setDropdownOpen(true)} - onChange={(e) => { - const text = e.target.value; - setSearchText(text); - searchLink(text); - onChange(text); - }} - /> +
+ { + if (!disabled) { + setDropdownOpen(true); + setSearchText(''); + } + }} + onChange={(e) => { + const text = e.target.value; + setSearchText(text); + searchLink(text); + }} + /> + + {/* Clear button - only show when there's a selected value and not disabled */} + {value && !disabled && !isDropdownOpen && ( + + )} +
{isDropdownOpen && searchResults.length > 0 && !disabled && (
@@ -710,7 +726,7 @@ const AssetDetail: React.FC = () => { More Details
-
+ {/*
@@ -726,7 +742,7 @@ const AssetDetail: React.FC = () => { -
+
*/} {/* QR Code */}
@@ -1018,14 +1034,206 @@ const AssetDetail: React.FC = () => {
- {/* Financial Details - Full Width */} + + {/* Updated Financial Details */}
+
+

Financial Details

+

+ The depreciation method is an accounting method used to allocate the cost of a tangible asset over its useful life. +

+ +
+ + 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" + /> + +
+ + {/* Asset Finance Book child table — shown only when checkbox checked */} + {formData.calculate_depreciation && ( +
+ {/* Header with Add Row button */} +
+

+ Asset Finance Books +

+ {isEditing && ( + + )} +
+ + {/* Show message if no rows */} + {(!formData.finance_books || formData.finance_books.length === 0) && ( +
+ No finance books added yet. Click "Add Row" to add one. +
+ )} + + {/* TABLE - Full width desktop view with overflow fix */} + {formData.finance_books && formData.finance_books.length > 0 && ( +
+
+ + + + + + + + + {isEditing && ( + + )} + + + + {formData.finance_books.map((row: AssetFinanceBookRow, idx: number) => ( + + {/* Finance Book - with overflow visible */} + + + {/* Depreciation Method */} + + + {/* Total Depreciations */} + + + {/* Frequency */} + + + {/* Start Date */} + + + {/* REMOVE BUTTON */} + {isEditing && ( + + )} + + ))} + +
+ Finance Book + + Depreciation Method* + + Total Depreciations* + + Frequency (Months)* + + Depreciation Posting Date* + + Action +
+
+ updateFinanceRow(idx, { finance_book: val })} + disabled={!isEditing} + /> +
+
+ + + + 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" + /> + + + 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" + /> + + + 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" + /> + + +
+
+
+ )} +
+ )} +
+
+ + + + {/* Financial Details - Full Width */} + {/*

Financial Details

- {/* Financial Input Fields */} +
- {/* Calculate Depreciation Checkbox */} +
{
- {/* Depreciation Schedule Table */}
@@ -1127,7 +1334,7 @@ const AssetDetail: React.FC = () => { - {/* Sample rows - Replace with actual data from API */} + @@ -265,7 +761,8 @@ const AssetList: React.FC = () => { {asset.location || '-'}
Year 1 @@ -1188,7 +1395,7 @@ const AssetDetail: React.FC = () => { )} - + */} ); diff --git a/src/pages/AssetList.tsx b/src/pages/AssetList.tsx index fb4b715..3179665 100644 --- a/src/pages/AssetList.tsx +++ b/src/pages/AssetList.tsx @@ -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(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(null); + const serialNumberDebounceRef = useRef(null); + + // Build filters object + const filters: Record = {}; + 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, 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 (
@@ -189,7 +342,7 @@ const AssetList: React.FC = () => {
{/* Search Bar */} -
+ {/*
{ onChange={(e) => setSearchTerm(e.target.value)} className="flex-1 outline-none text-gray-700 dark:text-gray-200 bg-transparent" /> + {searchTerm && ( + + )}
+
*/} + + {/* Filter Section */} +
+
+

Filters

+ {hasActiveFilters && ( + + )} +
+ + {/* First Row - 5 filters */} +
+ {/* Asset ID Filter */} +
+ setFilterAssetId(val)} + placeholder="Select Asset ID" + disabled={false} + /> +
+ + {/* Company Filter */} +
+ setFilterCompany(val)} + placeholder="Select Hospital" + disabled={false} + filters={{ domain: 'Healthcare' }} + /> +
+ + {/* Location Filter */} +
+ setFilterLocation(val)} + placeholder="Select Location" + disabled={false} + /> +
+ + {/* Department Filter */} +
+ setFilterDepartment(val)} + placeholder="Select Department" + disabled={false} + /> +
+ + {/* Modality Filter */} +
+ setFilterModality(val)} + placeholder="Select Modality" + disabled={false} + /> +
+
+ + {/* Second Row - 5 filters */} +
+ {/* Manufacturer Filter */} +
+ setFilterManufacturer(val)} + placeholder="Select Manufacturer" + disabled={false} + /> +
+ + {/* Supplier Filter */} +
+ setFilterSupplier(val)} + placeholder="Select Supplier" + disabled={false} + /> +
+ + {/* Device Status Filter - Dropdown */} +
+ + +
+ + {/* Asset Name Filter - Text Input */} +
+ +
+ 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 && ( + + typing... + + )} +
+

+ Press Enter or wait to apply +

+ +
+ + {/* Serial Number Filter - Text Input */} +
+ + {/* 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" + /> */} +
+ 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 && ( + + typing... + + )} +
+

+ Press Enter or wait to apply +

+
+
+ + {/* Active Filters Display */} + {hasActiveFilters && ( +
+ {filterAssetId && ( + + Asset ID: {filterAssetId} + + + )} + {filterCompany && ( + + Hospital: {filterCompany} + + + )} + {filterLocation && ( + + Location: {filterLocation} + + + )} + {filterDepartment && ( + + Department: {filterDepartment} + + + )} + {filterModality && ( + + Modality: {filterModality} + + + )} + {filterManufacturer && ( + + Manufacturer: {filterManufacturer} + + + )} + {filterSupplier && ( + + Supplier: {filterSupplier} + + + )} + {filterDeviceStatus && ( + + Status: {filterDeviceStatus} + + + )} + {filterAssetName && ( + + Asset Name: "{filterAssetName}" + + + )} + {filterSerialNumber && ( + + Serial: "{filterSerialNumber}" + + + )} + {searchTerm && ( + + Search: "{searchTerm}" + + + )} +
+ )}
{/* Assets Table */} @@ -235,12 +714,29 @@ const AssetList: React.FC = () => {

No assets found

- + */} + + {hasActiveFilters ? ( + + ) : ( + + )} +
- { ${!asset.custom_device_status ? 'bg-gray-100 text-gray-800' : ''} `}> {asset.custom_device_status || 'Unknown'} - + */}
e.stopPropagation()}>