CR Bug fixed
This commit is contained in:
parent
465cc8f9b0
commit
2e1bd35f24
@ -313,6 +313,7 @@ export const SelectField = ({
|
|||||||
error = '',
|
error = '',
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
required = false,
|
required = false,
|
||||||
|
forceDropdownBelow = false,
|
||||||
}) => {
|
}) => {
|
||||||
const wrapperStyle = {};
|
const wrapperStyle = {};
|
||||||
if (width !== 'auto') {
|
if (width !== 'auto') {
|
||||||
@ -321,20 +322,84 @@ export const SelectField = ({
|
|||||||
const baseStyle = baseInputStyles[variant] || baseInputStyles.default;
|
const baseStyle = baseInputStyles[variant] || baseInputStyles.default;
|
||||||
const inputStyle = { ...baseStyle, ...style };
|
const inputStyle = { ...baseStyle, ...style };
|
||||||
const [focused, setFocused] = React.useState(false);
|
const [focused, setFocused] = React.useState(false);
|
||||||
|
const [isOpen, setIsOpen] = React.useState(false);
|
||||||
|
const [searchTerm, setSearchTerm] = React.useState('');
|
||||||
|
const dropdownRef = React.useRef(null);
|
||||||
|
const inputRef = React.useRef(null);
|
||||||
const normalizedClear = clearValue ?? '';
|
const normalizedClear = clearValue ?? '';
|
||||||
const hasValue = value !== undefined && value !== null && value !== '' && value !== normalizedClear;
|
const hasValue = value !== undefined && value !== null && value !== '' && value !== normalizedClear;
|
||||||
const showClear = allowClear && hasValue && !readOnly;
|
const showClear = allowClear && hasValue && !readOnly;
|
||||||
|
|
||||||
if (rightIcon) {
|
if (rightIcon) {
|
||||||
inputStyle.paddingRight = '40px';
|
inputStyle.paddingRight = '40px';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter options based on search term
|
||||||
|
const filteredOptions = React.useMemo(() => {
|
||||||
|
if (!searchTerm) return options;
|
||||||
|
return options.filter(opt =>
|
||||||
|
opt.label.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
}, [options, searchTerm]);
|
||||||
|
|
||||||
|
// Close dropdown when clicking outside
|
||||||
|
React.useEffect(() => {
|
||||||
|
const handleClickOutside = (event) => {
|
||||||
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||||
|
setIsOpen(false);
|
||||||
|
setSearchTerm('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
const handleFocus = (event) => {
|
const handleFocus = (event) => {
|
||||||
setFocused(true);
|
setFocused(true);
|
||||||
onFocus?.(event);
|
onFocus?.(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBlur = (event) => {
|
const handleBlur = (event) => {
|
||||||
setFocused(false);
|
setFocused(false);
|
||||||
onBlur?.(event);
|
onBlur?.(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleClear = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (onClear) {
|
||||||
|
onClear();
|
||||||
|
} else if (onChange) {
|
||||||
|
onChange({ target: { value: normalizedClear } });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelect = (selectedValue) => {
|
||||||
|
if (!readOnly) {
|
||||||
|
onChange({ target: { value: selectedValue } });
|
||||||
|
setIsOpen(false);
|
||||||
|
setSearchTerm('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleDropdown = () => {
|
||||||
|
if (!readOnly) {
|
||||||
|
setIsOpen(!isOpen);
|
||||||
|
setSearchTerm('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDisplayValue = () => {
|
||||||
|
const selected = options.find(opt => opt.value === value);
|
||||||
|
return selected ? selected.label : (placeholder || '');
|
||||||
|
};
|
||||||
|
|
||||||
const hasError = Boolean(error);
|
const hasError = Boolean(error);
|
||||||
inputStyle.borderColor = hasError
|
inputStyle.borderColor = hasError
|
||||||
? '#B91C1C'
|
? '#B91C1C'
|
||||||
@ -353,15 +418,98 @@ export const SelectField = ({
|
|||||||
inputStyle.backgroundColor = '#F9FAFB';
|
inputStyle.backgroundColor = '#F9FAFB';
|
||||||
inputStyle.cursor = 'not-allowed';
|
inputStyle.cursor = 'not-allowed';
|
||||||
}
|
}
|
||||||
const handleClear = (event) => {
|
|
||||||
event.preventDefault();
|
// Custom dropdown implementation
|
||||||
event.stopPropagation();
|
if (forceDropdownBelow) {
|
||||||
if (onClear) {
|
return (
|
||||||
onClear();
|
<div className={`w-full ${wrapperStyle ? '' : ''}`} style={wrapperStyle || {}} ref={dropdownRef}>
|
||||||
} else if (onChange) {
|
{label && (
|
||||||
onChange({ target: { value: normalizedClear } });
|
<label htmlFor={id || name} className="block text-sm font-medium text-gray-900 mb-1">
|
||||||
}
|
{label}
|
||||||
};
|
{required && <span className="text-red-700 ml-1">*</span>}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<div className="relative w-full">
|
||||||
|
<div
|
||||||
|
className={`w-full text-sm focus:outline-none ${className}`}
|
||||||
|
style={{
|
||||||
|
...inputStyle,
|
||||||
|
cursor: readOnly ? 'not-allowed' : 'pointer',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
minHeight: '40px',
|
||||||
|
padding: '0 16px'
|
||||||
|
}}
|
||||||
|
onClick={toggleDropdown}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
tabIndex={readOnly ? -1 : 0}
|
||||||
|
>
|
||||||
|
<span style={{ color: hasValue ? '#232528' : '#9CA3AF' }}>
|
||||||
|
{getDisplayValue()}
|
||||||
|
</span>
|
||||||
|
{showClear ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="h-5 w-5 rounded-full text-[#9CA3AF] hover:text-[#4B5563]"
|
||||||
|
onClick={handleClear}
|
||||||
|
aria-label="Clear selection"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
rightIcon && <img src={rightIcon} alt="" className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isOpen && !readOnly && (
|
||||||
|
<div
|
||||||
|
className="absolute z-50 w-full mt-1 bg-white border border-[#CBA344] rounded-md shadow-lg max-h-60 overflow-auto"
|
||||||
|
style={{
|
||||||
|
top: '100%',
|
||||||
|
left: 0,
|
||||||
|
right: 0
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Search input */}
|
||||||
|
<div className="p-2 border-b border-gray-200">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search..."
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-[#92722A] focus:border-transparent"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Options list */}
|
||||||
|
{filteredOptions.length === 0 ? (
|
||||||
|
<div className="px-3 py-2 text-gray-500 text-sm">No options found</div>
|
||||||
|
) : (
|
||||||
|
filteredOptions.map((opt) => (
|
||||||
|
<div
|
||||||
|
key={opt.value}
|
||||||
|
className={`px-3 py-2 cursor-pointer text-sm hover:bg-[#F9FAFB] ${
|
||||||
|
opt.value === value ? 'bg-[#E6D7A2] text-[#232528]' : 'text-[#232528]'
|
||||||
|
}`}
|
||||||
|
onClick={() => handleSelect(opt.value)}
|
||||||
|
>
|
||||||
|
{opt.label}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{readOnly && <div className="absolute inset-0 pointer-events-none" />}
|
||||||
|
{error && <p className="mt-1 text-xs text-[#B91C1C]">{error}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Original native select implementation
|
||||||
return (
|
return (
|
||||||
<div className={`w-full ${wrapperStyle ? '' : ''}`} style={wrapperStyle || {}}>
|
<div className={`w-full ${wrapperStyle ? '' : ''}`} style={wrapperStyle || {}}>
|
||||||
{label && (
|
{label && (
|
||||||
|
|||||||
@ -7,6 +7,7 @@ const getSafeEstablishmentId = () => {
|
|||||||
};
|
};
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { getEstablishmentProducts } from '../../../services/submissions/submissionService';
|
import { getEstablishmentProducts } from '../../../services/submissions/submissionService';
|
||||||
|
import { fetchEstablishmentDetail } from '../../../services/establishments/establishmentService';
|
||||||
import { getEmirates } from '../../../services/masters/masterService';
|
import { getEmirates } from '../../../services/masters/masterService';
|
||||||
import Table from '@/components/common/Table';
|
import Table from '@/components/common/Table';
|
||||||
|
|
||||||
@ -154,11 +155,11 @@ const EstablishmentInfo = ({
|
|||||||
|
|
||||||
setLoadingProducts(true);
|
setLoadingProducts(true);
|
||||||
try {
|
try {
|
||||||
const response = await getEstablishmentProducts(establishmentId);
|
const response = await fetchEstablishmentDetail(establishmentId);
|
||||||
if (response.status === 'success') {
|
if (response?.data?.establishment_products) {
|
||||||
setProducts(response.data || []);
|
setProducts(response.data.establishment_products || []);
|
||||||
} else {
|
} else {
|
||||||
setProductsError(response.message || 'Failed to load products');
|
setProductsError('Failed to load products');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching products:', error);
|
console.error('Error fetching products:', error);
|
||||||
@ -652,7 +653,7 @@ React.useEffect(() => {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="mt-10">
|
<Card className="mt-10">
|
||||||
<h3 className="text-sm font-semibold text-gray-900 mb-4">Products</h3>
|
<h3 className="text-sm font-semibold text-gray-900 mb-4">Productss</h3>
|
||||||
{loadingProducts ? (
|
{loadingProducts ? (
|
||||||
<div className="flex justify-center py-4">
|
<div className="flex justify-center py-4">
|
||||||
<div className="h-6 w-6 animate-spin rounded-full border-b-2 border-[#92722A]" />
|
<div className="h-6 w-6 animate-spin rounded-full border-b-2 border-[#92722A]" />
|
||||||
@ -663,9 +664,9 @@ React.useEffect(() => {
|
|||||||
<Table
|
<Table
|
||||||
headers={['Product Name', 'HS Code', 'Description']}
|
headers={['Product Name', 'HS Code', 'Description']}
|
||||||
rows={products.map(product => [
|
rows={products.map(product => [
|
||||||
product['product.product_name'] || 'N/A',
|
product.product.product_name || '-',
|
||||||
product['product.hs_code'] || 'N/A',
|
product.product.hs_code || '-',
|
||||||
product['product.hs_description'] || 'N/A'
|
product.product.hs_description || '-'
|
||||||
])}
|
])}
|
||||||
columnWidths={['33.33%', '33.33%', '33.33%']}
|
columnWidths={['33.33%', '33.33%', '33.33%']}
|
||||||
separated={true}
|
separated={true}
|
||||||
|
|||||||
@ -91,7 +91,8 @@ const SearchableSelect = ({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
error = false,
|
error = false,
|
||||||
className = '',
|
className = '',
|
||||||
displayValue = undefined
|
displayValue = undefined,
|
||||||
|
allowNull = false // New prop to allow null values for no selection
|
||||||
}) => {
|
}) => {
|
||||||
const [isOpen, setIsOpen] = React.useState(false);
|
const [isOpen, setIsOpen] = React.useState(false);
|
||||||
const [searchTerm, setSearchTerm] = React.useState('');
|
const [searchTerm, setSearchTerm] = React.useState('');
|
||||||
@ -153,22 +154,25 @@ const SearchableSelect = ({
|
|||||||
|
|
||||||
// Get selected option label
|
// Get selected option label
|
||||||
const [internalDisplayValue, setInternalDisplayValue] = React.useState(displayValue || '');
|
const [internalDisplayValue, setInternalDisplayValue] = React.useState(displayValue || '');
|
||||||
const selectedOption = options.find(opt => opt.value === value);
|
const selectedOption = allowNull ? (value === null ? null : options.find(opt => opt.value === value)) : options.find(opt => opt.value === value);
|
||||||
|
|
||||||
// Update internal display value when value, displayValue prop or options change
|
// Update internal display value when value, displayValue prop or options change
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (displayValue !== undefined && displayValue !== '') {
|
if (displayValue !== undefined && displayValue !== '') {
|
||||||
setInternalDisplayValue(displayValue);
|
setInternalDisplayValue(displayValue);
|
||||||
|
} else if (allowNull && value === null) {
|
||||||
|
// When allowNull is true and value is null, show empty (no selection)
|
||||||
|
setInternalDisplayValue('');
|
||||||
} else if (selectedOption) {
|
} else if (selectedOption) {
|
||||||
setInternalDisplayValue(selectedOption.label);
|
setInternalDisplayValue(selectedOption.label);
|
||||||
} else if (value) {
|
} else if (value !== null && value !== undefined && value !== '') {
|
||||||
// If we have a value but no matching option, try to find it in the options
|
// If we have a value but no matching option, try to find it in the options
|
||||||
const matchedOption = options.find(opt => opt.value === value);
|
const matchedOption = options.find(opt => opt.value === value);
|
||||||
setInternalDisplayValue(matchedOption?.label || '');
|
setInternalDisplayValue(matchedOption?.label || '');
|
||||||
} else {
|
} else {
|
||||||
setInternalDisplayValue('');
|
setInternalDisplayValue('');
|
||||||
}
|
}
|
||||||
}, [value, displayValue, selectedOption, options]);
|
}, [value, displayValue, selectedOption, options, allowNull]);
|
||||||
|
|
||||||
const displayValueToShow = internalDisplayValue;
|
const displayValueToShow = internalDisplayValue;
|
||||||
|
|
||||||
@ -197,7 +201,7 @@ const SearchableSelect = ({
|
|||||||
if (error) {
|
if (error) {
|
||||||
borderColor = 'border-red-500';
|
borderColor = 'border-red-500';
|
||||||
bgColor = 'bg-red-50';
|
bgColor = 'bg-red-50';
|
||||||
} else if (value || isOpen) {
|
} else if ((value !== null && value !== undefined && value !== '') || isOpen) {
|
||||||
borderColor = 'border-[#92722A]';
|
borderColor = 'border-[#92722A]';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,7 +245,7 @@ const SearchableSelect = ({
|
|||||||
<div
|
<div
|
||||||
key={option.value}
|
key={option.value}
|
||||||
className={`px-4 py-2 text-sm cursor-pointer hover:bg-gray-100 ${
|
className={`px-4 py-2 text-sm cursor-pointer hover:bg-gray-100 ${
|
||||||
value === option.value ? 'bg-[#F5F0E1]' : ''
|
(allowNull ? value === option.value : value === option.value) ? 'bg-[#F5F0E1]' : ''
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleSelect(option)}
|
onClick={() => handleSelect(option)}
|
||||||
>
|
>
|
||||||
@ -267,7 +271,9 @@ const Select = ({
|
|||||||
loading = false,
|
loading = false,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
error = false,
|
error = false,
|
||||||
className = ''
|
className = '',
|
||||||
|
allowClear = false,
|
||||||
|
onClear
|
||||||
}) => {
|
}) => {
|
||||||
const [isFocused, setIsFocused] = React.useState(false);
|
const [isFocused, setIsFocused] = React.useState(false);
|
||||||
|
|
||||||
@ -281,6 +287,19 @@ const Select = ({
|
|||||||
} else if (value || isFocused) {
|
} else if (value || isFocused) {
|
||||||
borderColor = 'border-[#92722A]';
|
borderColor = 'border-[#92722A]';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasValue = value !== undefined && value !== null && value !== '';
|
||||||
|
const showClear = allowClear && hasValue && !disabled && !loading;
|
||||||
|
|
||||||
|
const handleClear = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (onClear) {
|
||||||
|
onClear();
|
||||||
|
} else if (onChange) {
|
||||||
|
onChange({ target: { value: '' } });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@ -317,8 +336,17 @@ const Select = ({
|
|||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
</select>
|
</select>
|
||||||
<div className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 flex items-center">
|
<div className="absolute right-3 top-1/2 -translate-y-1/2 flex items-center">
|
||||||
{loading ? (
|
{showClear ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="h-5 w-5 rounded-full text-[#9CA3AF] hover:text-[#4B5563] flex items-center justify-center"
|
||||||
|
onClick={handleClear}
|
||||||
|
aria-label="Clear selection"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
) : loading ? (
|
||||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-[#92722A] border-t-transparent" />
|
<div className="h-4 w-4 animate-spin rounded-full border-2 border-[#92722A] border-t-transparent" />
|
||||||
) : (
|
) : (
|
||||||
<img src={caretDownSrc} alt="open" className="h-4 w-4" />
|
<img src={caretDownSrc} alt="open" className="h-4 w-4" />
|
||||||
@ -418,6 +446,11 @@ const ProductData = ({
|
|||||||
const [unitsError, setUnitsError] = React.useState(null);
|
const [unitsError, setUnitsError] = React.useState(null);
|
||||||
const [isSavingDraft, setIsSavingDraft] = React.useState(false);
|
const [isSavingDraft, setIsSavingDraft] = React.useState(false);
|
||||||
|
|
||||||
|
const latestDraftRef = React.useRef({ products: [], deletedProducts: [], remarks: '' });
|
||||||
|
const draftKeyRef = React.useRef('');
|
||||||
|
const restoredDraftKeyRef = React.useRef('');
|
||||||
|
const restoredDraftSignatureRef = React.useRef('');
|
||||||
|
|
||||||
|
|
||||||
const location = useLocation(); // Add this line
|
const location = useLocation(); // Add this line
|
||||||
|
|
||||||
@ -441,6 +474,57 @@ const ProductData = ({
|
|||||||
return '';
|
return '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getLocalDraftKey = React.useCallback(() => {
|
||||||
|
if (typeof window === 'undefined') return '';
|
||||||
|
|
||||||
|
const currentSubmissionStr = localStorage.getItem('currentSubmission');
|
||||||
|
let currentSubmission = null;
|
||||||
|
try {
|
||||||
|
currentSubmission = currentSubmissionStr ? JSON.parse(currentSubmissionStr) : null;
|
||||||
|
} catch (e) {
|
||||||
|
currentSubmission = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const establishmentId =
|
||||||
|
currentSubmission?.establishment_id ||
|
||||||
|
currentSubmission?.establishment?.id ||
|
||||||
|
localStorage.getItem('establishment_id') ||
|
||||||
|
localStorage.getItem('establishmentId') ||
|
||||||
|
'';
|
||||||
|
|
||||||
|
const effectiveQuarter = quarter || localStorage.getItem('currentQuarter') || localStorage.getItem('current_quarter') || '';
|
||||||
|
const effectiveYear = year || localStorage.getItem('currentYear') || localStorage.getItem('current_year') || '';
|
||||||
|
|
||||||
|
if (!establishmentId || !effectiveQuarter || !effectiveYear) return '';
|
||||||
|
const formattedQuarter = String(effectiveQuarter).startsWith('Q')
|
||||||
|
? String(effectiveQuarter)
|
||||||
|
: `Q${effectiveQuarter}`;
|
||||||
|
|
||||||
|
return `productDataDraft:${establishmentId}:${effectiveYear}:${formattedQuarter}`;
|
||||||
|
}, [quarter, year]);
|
||||||
|
|
||||||
|
const persistLocalDraft = React.useCallback((payload) => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
const key = draftKeyRef.current || getLocalDraftKey();
|
||||||
|
if (!key) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem(
|
||||||
|
key,
|
||||||
|
JSON.stringify({
|
||||||
|
...payload,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// ignore localStorage errors
|
||||||
|
}
|
||||||
|
}, [getLocalDraftKey]);
|
||||||
|
|
||||||
|
const flushLocalDraft = React.useCallback(() => {
|
||||||
|
persistLocalDraft(latestDraftRef.current);
|
||||||
|
}, [persistLocalDraft]);
|
||||||
|
|
||||||
const handleRemarksChange = (e) => {
|
const handleRemarksChange = (e) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
setRemarks(value);
|
setRemarks(value);
|
||||||
@ -449,6 +533,109 @@ const ProductData = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Keep latest draft snapshot in a ref (used by beforeunload/offline handlers)
|
||||||
|
React.useEffect(() => {
|
||||||
|
latestDraftRef.current = {
|
||||||
|
products,
|
||||||
|
deletedProducts,
|
||||||
|
remarks,
|
||||||
|
};
|
||||||
|
}, [products, deletedProducts, remarks]);
|
||||||
|
|
||||||
|
// Initialize draft key and restore any existing draft for this establishment/period
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
const key = getLocalDraftKey();
|
||||||
|
draftKeyRef.current = key;
|
||||||
|
if (!key) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(key);
|
||||||
|
if (!raw) return;
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
if (!parsed) return;
|
||||||
|
|
||||||
|
const parsedSignature = JSON.stringify({
|
||||||
|
products: Array.isArray(parsed.products) ? parsed.products : [],
|
||||||
|
deletedProducts: Array.isArray(parsed.deletedProducts) ? parsed.deletedProducts : [],
|
||||||
|
remarks: typeof parsed.remarks === 'string' ? parsed.remarks : '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restore only when the incoming props don't already have meaningful user-entered data
|
||||||
|
const hasExistingData = Array.isArray(products) && products.some((p) => {
|
||||||
|
if (!p) return false;
|
||||||
|
return Boolean(
|
||||||
|
p.product || p.productId || p.product_id ||
|
||||||
|
p.unit || p.unit_id ||
|
||||||
|
p.capacity ||
|
||||||
|
p.octQuantity || p.novQuantity || p.decQuantity ||
|
||||||
|
p.janQuantity || p.febQuantity || p.marQuantity ||
|
||||||
|
p.aprQuantity || p.mayQuantity || p.junQuantity ||
|
||||||
|
p.octCost || p.novCost || p.decCost ||
|
||||||
|
p.janCost || p.febCost || p.marCost ||
|
||||||
|
p.aprCost || p.mayCost || p.junCost ||
|
||||||
|
p.variationReason || p.zeroTargetReason || p.remarks
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hasExistingData && Array.isArray(parsed.products)) {
|
||||||
|
// If we've already restored this exact payload for this key, don't keep re-applying.
|
||||||
|
if (restoredDraftKeyRef.current === key && restoredDraftSignatureRef.current === parsedSignature) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onProductsChange(parsed.products);
|
||||||
|
if (Array.isArray(parsed.deletedProducts)) setDeletedProducts(parsed.deletedProducts);
|
||||||
|
if (typeof parsed.remarks === 'string') setRemarks(parsed.remarks);
|
||||||
|
restoredDraftKeyRef.current = key;
|
||||||
|
restoredDraftSignatureRef.current = parsedSignature;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore parse errors
|
||||||
|
}
|
||||||
|
}, [getLocalDraftKey, onProductsChange, products]);
|
||||||
|
|
||||||
|
// Debounced autosave to localStorage whenever user changes anything
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
const key = draftKeyRef.current || getLocalDraftKey();
|
||||||
|
if (!key) return;
|
||||||
|
|
||||||
|
const handle = setTimeout(() => {
|
||||||
|
persistLocalDraft({ products, deletedProducts, remarks });
|
||||||
|
}, 400);
|
||||||
|
|
||||||
|
return () => clearTimeout(handle);
|
||||||
|
}, [products, deletedProducts, remarks, getLocalDraftKey, persistLocalDraft]);
|
||||||
|
|
||||||
|
// Accidental exit handling: save on tab close, refresh, background, offline
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
const onBeforeUnload = () => {
|
||||||
|
flushLocalDraft();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onVisibilityChange = () => {
|
||||||
|
if (document.visibilityState === 'hidden') {
|
||||||
|
flushLocalDraft();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onOffline = () => {
|
||||||
|
flushLocalDraft();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('beforeunload', onBeforeUnload);
|
||||||
|
document.addEventListener('visibilitychange', onVisibilityChange);
|
||||||
|
window.addEventListener('offline', onOffline);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('beforeunload', onBeforeUnload);
|
||||||
|
document.removeEventListener('visibilitychange', onVisibilityChange);
|
||||||
|
window.removeEventListener('offline', onOffline);
|
||||||
|
};
|
||||||
|
}, [flushLocalDraft]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchQuarterPeriods = async () => {
|
const fetchQuarterPeriods = async () => {
|
||||||
try {
|
try {
|
||||||
@ -564,9 +751,9 @@ useEffect(() => {
|
|||||||
junCost: product.forecast_cost_period_three?.toString() || '0',
|
junCost: product.forecast_cost_period_three?.toString() || '0',
|
||||||
|
|
||||||
// Variation reasons
|
// Variation reasons
|
||||||
variationReason: product.variation_reason_master_id?.toString() || '0',
|
variationReason: product.variation_reason_master_id !== null && product.variation_reason_master_id !== undefined ? product.variation_reason_master_id.toString() : null,
|
||||||
otherVariationReason: product.other_variation_reason || '',
|
otherVariationReason: product.other_variation_reason || '',
|
||||||
zeroTargetReason: product.zero_target_reason_master_id?.toString() || '',
|
zeroTargetReason: product.zero_target_reason_master_id !== null && product.zero_target_reason_master_id !== undefined ? product.zero_target_reason_master_id.toString() : null,
|
||||||
otherZeroTargetReason: product.other_zero_target_reason || '',
|
otherZeroTargetReason: product.other_zero_target_reason || '',
|
||||||
remarks: product.remarks || '',
|
remarks: product.remarks || '',
|
||||||
|
|
||||||
@ -730,9 +917,9 @@ const existingProduct = currentSubmission.products?.find(p =>
|
|||||||
// Next quarter forecast (Q2) - December
|
// Next quarter forecast (Q2) - December
|
||||||
forecast_quantity: product.decQuantity || '0',
|
forecast_quantity: product.decQuantity || '0',
|
||||||
forecast_cost: product.decCost || '0',
|
forecast_cost: product.decCost || '0',
|
||||||
variation_reason_master_id: product.variationReason || '0',
|
variation_reason_master_id: product.variationReason === null ? null : (product.variationReason || '0'),
|
||||||
other_variation_reason: product.otherVariationReason || '0',
|
other_variation_reason: product.otherVariationReason || '0',
|
||||||
zero_target_reason_master_id: product.zeroTargetReason || '0',
|
zero_target_reason_master_id: product.zeroTargetReason === null ? null : (product.zeroTargetReason || '0'),
|
||||||
other_zero_target_reason: product.otherZeroTargetReason || '0',
|
other_zero_target_reason: product.otherZeroTargetReason || '0',
|
||||||
remarks: product.remarks || remarks || '' // Use product.remarks if available, otherwise use the component's remarks state
|
remarks: product.remarks || remarks || '' // Use product.remarks if available, otherwise use the component's remarks state
|
||||||
};
|
};
|
||||||
@ -775,9 +962,9 @@ const existingProduct = currentSubmission.products?.find(p =>
|
|||||||
current_cost: product.novCost || '0',
|
current_cost: product.novCost || '0',
|
||||||
forecast_quantity: product.decQuantity || '0',
|
forecast_quantity: product.decQuantity || '0',
|
||||||
forecast_cost: product.decCost || '0',
|
forecast_cost: product.decCost || '0',
|
||||||
variation_reason_master_id: product.variationReason || '0',
|
variation_reason_master_id: product.variationReason === null ? null : (product.variationReason || '0'),
|
||||||
other_variation_reason: product.otherVariationReason || '0',
|
other_variation_reason: product.otherVariationReason || '0',
|
||||||
zero_target_reason_master_id: product.zeroTargetReason || '0',
|
zero_target_reason_master_id: product.zeroTargetReason === null ? null : (product.zeroTargetReason || '0'),
|
||||||
other_zero_target_reason: product.otherZeroTargetReason || '0',
|
other_zero_target_reason: product.otherZeroTargetReason || '0',
|
||||||
remarks: product.remarks || remarks || '',
|
remarks: product.remarks || remarks || '',
|
||||||
is_active: false // Mark as inactive
|
is_active: false // Mark as inactive
|
||||||
@ -893,9 +1080,9 @@ useEffect(() => {
|
|||||||
junCost: product.forecast_cost_period_three || '0',
|
junCost: product.forecast_cost_period_three || '0',
|
||||||
|
|
||||||
// Variation reasons
|
// Variation reasons
|
||||||
variationReason: product.variation_reason_master_id?.toString() || '',
|
variationReason: product.variation_reason_master_id !== null && product.variation_reason_master_id !== undefined ? product.variation_reason_master_id.toString() : null,
|
||||||
otherVariationReason: product.other_variation_reason || '',
|
otherVariationReason: product.other_variation_reason || '',
|
||||||
zeroTargetReason: product.zero_target_reason_master_id?.toString() || '',
|
zeroTargetReason: product.zero_target_reason_master_id !== null && product.zero_target_reason_master_id !== undefined ? product.zero_target_reason_master_id.toString() : null,
|
||||||
otherZeroTargetReason: product.other_zero_target_reason || '',
|
otherZeroTargetReason: product.other_zero_target_reason || '',
|
||||||
remarks: product.remarks || ''
|
remarks: product.remarks || ''
|
||||||
};
|
};
|
||||||
@ -1295,8 +1482,8 @@ useEffect(() => {
|
|||||||
// Dynamic validation for variation reason based on cost variation
|
// Dynamic validation for variation reason based on cost variation
|
||||||
const mandatoryVariationReason = isVariationReasonMandatory(product);
|
const mandatoryVariationReason = isVariationReasonMandatory(product);
|
||||||
if (mandatoryVariationReason) {
|
if (mandatoryVariationReason) {
|
||||||
// Check if variation reason is selected
|
// Check if variation reason is selected (null means no selection)
|
||||||
if (!product.variationReason || product.variationReason === '0' || product.variationReason === '') {
|
if (product.variationReason === null || product.variationReason === undefined || product.variationReason === '') {
|
||||||
errors[`variationReason_${index}`] = 'Reason for variation is required when cost variation exceeds ±10%';
|
errors[`variationReason_${index}`] = 'Reason for variation is required when cost variation exceeds ±10%';
|
||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
@ -1718,8 +1905,8 @@ useEffect(() => {
|
|||||||
const newErrors = { ...formErrors };
|
const newErrors = { ...formErrors };
|
||||||
|
|
||||||
if (mandatoryVariationReason) {
|
if (mandatoryVariationReason) {
|
||||||
// Check if variation reason is selected
|
// Check if variation reason is selected (null means no selection)
|
||||||
if (!updatedProduct.variationReason || updatedProduct.variationReason === '0' || updatedProduct.variationReason === '') {
|
if (updatedProduct.variationReason === null || updatedProduct.variationReason === undefined || updatedProduct.variationReason === '') {
|
||||||
newErrors[variationErrorKey] = 'Reason for variation is required when cost variation exceeds ±10%';
|
newErrors[variationErrorKey] = 'Reason for variation is required when cost variation exceeds ±10%';
|
||||||
} else {
|
} else {
|
||||||
delete newErrors[variationErrorKey];
|
delete newErrors[variationErrorKey];
|
||||||
@ -1759,8 +1946,8 @@ useEffect(() => {
|
|||||||
|
|
||||||
if (mandatoryVariationReason) {
|
if (mandatoryVariationReason) {
|
||||||
if (field === 'variationReason') {
|
if (field === 'variationReason') {
|
||||||
// Check if variation reason is selected
|
// Check if variation reason is selected (null means no selection)
|
||||||
if (!updatedProduct.variationReason || updatedProduct.variationReason === '0' || updatedProduct.variationReason === '') {
|
if (updatedProduct.variationReason === null || updatedProduct.variationReason === undefined || updatedProduct.variationReason === '') {
|
||||||
newErrors[variationErrorKey] = 'Reason for variation is required when cost variation exceeds ±10%';
|
newErrors[variationErrorKey] = 'Reason for variation is required when cost variation exceeds ±10%';
|
||||||
} else {
|
} else {
|
||||||
delete newErrors[variationErrorKey];
|
delete newErrors[variationErrorKey];
|
||||||
@ -2566,12 +2753,14 @@ useEffect(() => {
|
|||||||
Reason for Variation
|
Reason for Variation
|
||||||
{isVariationReasonMandatory(p) && <span className="text-red-600">*</span>}
|
{isVariationReasonMandatory(p) && <span className="text-red-600">*</span>}
|
||||||
</label>
|
</label>
|
||||||
<Select
|
<SearchableSelect
|
||||||
placeholder={isLoadingReasons ? 'Loading reasons...' : 'Select reason'}
|
placeholder={isLoadingReasons ? 'Loading reasons...' : 'Select reason'}
|
||||||
options={variationReasons}
|
options={variationReasons}
|
||||||
value={p.variationReason || ''}
|
value={p.variationReason === null || p.variationReason === '' ? null : p.variationReason}
|
||||||
onChange={(e) => updateProductField(p.id, 'variationReason', e.target.value, variationReasons)}
|
onChange={(e) => updateProductField(p.id, 'variationReason', e.target.value, variationReasons)}
|
||||||
|
loading={isLoadingReasons}
|
||||||
error={!!formErrors[`variationReason_${idx}`]}
|
error={!!formErrors[`variationReason_${idx}`]}
|
||||||
|
allowNull={true}
|
||||||
/>
|
/>
|
||||||
{formErrors[`variationReason_${idx}`] && (
|
{formErrors[`variationReason_${idx}`] && (
|
||||||
<p className="mt-1 text-xs text-red-600">{formErrors[`variationReason_${idx}`]}</p>
|
<p className="mt-1 text-xs text-red-600">{formErrors[`variationReason_${idx}`]}</p>
|
||||||
@ -2724,11 +2913,13 @@ useEffect(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Reason for 0 Target</label>
|
<label className="block text-sm font-medium text-gray-700 mb-1">Reason for 0 Target</label>
|
||||||
<Select
|
<SearchableSelect
|
||||||
placeholder={isLoadingZeroReasons ? 'Loading reasons...' : 'Select reason'}
|
placeholder={isLoadingZeroReasons ? 'Loading reasons...' : 'Select reason'}
|
||||||
options={zeroTargetReasons}
|
options={zeroTargetReasons}
|
||||||
value={p.zeroTargetReason || ''}
|
value={p.zeroTargetReason === null || p.zeroTargetReason === '' ? null : p.zeroTargetReason}
|
||||||
onChange={(e) => updateProductField(p.id, 'zeroTargetReason', e.target.value, zeroTargetReasons)}
|
onChange={(e) => updateProductField(p.id, 'zeroTargetReason', e.target.value, zeroTargetReasons)}
|
||||||
|
loading={isLoadingZeroReasons}
|
||||||
|
allowNull={true}
|
||||||
/>
|
/>
|
||||||
{(showOtherZeroTargetReason[p.id] || (p.zeroTargetReason && (p.zeroTargetReason.toString().toLowerCase().includes('other') || (zeroTargetReasons.find(r => r.value === p.zeroTargetReason)?.name?.toLowerCase().includes('other') || zeroTargetReasons.find(r => r.value === p.zeroTargetReason)?.label?.toLowerCase().includes('other'))))) && (
|
{(showOtherZeroTargetReason[p.id] || (p.zeroTargetReason && (p.zeroTargetReason.toString().toLowerCase().includes('other') || (zeroTargetReasons.find(r => r.value === p.zeroTargetReason)?.name?.toLowerCase().includes('other') || zeroTargetReasons.find(r => r.value === p.zeroTargetReason)?.label?.toLowerCase().includes('other'))))) && (
|
||||||
<div className="mt-3 p-3 bg-gray-50 rounded-md border border-gray-200">
|
<div className="mt-3 p-3 bg-gray-50 rounded-md border border-gray-200">
|
||||||
|
|||||||
@ -696,7 +696,7 @@ const CompanyProfile = () => {
|
|||||||
{ field: 'permanentFactoryCode', label: 'Permanent Factory Code' },
|
{ field: 'permanentFactoryCode', label: 'Permanent Factory Code' },
|
||||||
{ field: 'uniqueLicenseNumber', label: 'Unique License Number' },
|
{ field: 'uniqueLicenseNumber', label: 'Unique License Number' },
|
||||||
// { field: 'industryCodeBusiness', label: 'Industry Code (Business Register)' },
|
// { field: 'industryCodeBusiness', label: 'Industry Code (Business Register)' },
|
||||||
{ field: 'industryCodeProduction', label: 'Industry Code (Current Production)' },
|
{ field: 'industryCodeProduction', label: 'Principal activity Code (ISIC Rev 4 - 4 digits)' },
|
||||||
],
|
],
|
||||||
2: [
|
2: [
|
||||||
{ field: 'contactName', label: 'Name' },
|
{ field: 'contactName', label: 'Name' },
|
||||||
@ -1009,7 +1009,7 @@ const CompanyProfile = () => {
|
|||||||
<h4 className="text-[16px] font-semibold text-[#232528]">Details</h4>
|
<h4 className="text-[16px] font-semibold text-[#232528]">Details</h4>
|
||||||
|
|
||||||
{/* Responsive grid */}
|
{/* Responsive grid */}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-2 w-full overflow-x-hidden [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2 w-full overflow-x-hidden [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]">
|
||||||
<div>
|
<div>
|
||||||
<TextField
|
<TextField
|
||||||
label="Permanent Factory Code"
|
label="Permanent Factory Code"
|
||||||
@ -1044,10 +1044,11 @@ const CompanyProfile = () => {
|
|||||||
readOnly={modalMode === 'view'}
|
readOnly={modalMode === 'view'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2 w-full">
|
||||||
<div>
|
<div>
|
||||||
<SelectField
|
<SelectField
|
||||||
label="Industry Code(Current Production)"
|
label="Principal activity Code (ISIC Rev 4 - 4 digits)"
|
||||||
value={form.industryCodeProduction}
|
value={form.industryCodeProduction}
|
||||||
onChange={handleFormChange('industryCodeProduction')}
|
onChange={handleFormChange('industryCodeProduction')}
|
||||||
placeholder="Select ISIC Code"
|
placeholder="Select ISIC Code"
|
||||||
@ -1057,10 +1058,9 @@ const CompanyProfile = () => {
|
|||||||
readOnly={modalMode === 'view'}
|
readOnly={modalMode === 'view'}
|
||||||
options={isicOptions}
|
options={isicOptions}
|
||||||
searchable
|
searchable
|
||||||
|
forceDropdownBelow={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2 w-full">
|
|
||||||
<div>
|
<div>
|
||||||
<TextField
|
<TextField
|
||||||
label="ERN"
|
label="ERN"
|
||||||
@ -3784,7 +3784,7 @@ const handleImport = async () => {
|
|||||||
<span>•</span><span>HS Code</span>
|
<span>•</span><span>HS Code</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span>•</span><span>Industry Code (Current Production)</span>
|
<span>•</span><span>Principal activity Code (ISIC Rev 4 - 4 digits)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -122,6 +122,7 @@ const EditCompanyProfile = () => {
|
|||||||
const [isLoadingProducts, setIsLoadingProducts] = useState(false);
|
const [isLoadingProducts, setIsLoadingProducts] = useState(false);
|
||||||
const [productError, setProductError] = useState("");
|
const [productError, setProductError] = useState("");
|
||||||
const [productLimitMessage, setProductLimitMessage] = useState("");
|
const [productLimitMessage, setProductLimitMessage] = useState("");
|
||||||
|
const [isSearching, setIsSearching] = useState(false); // Flag to prevent validation during search
|
||||||
const [isLoadingCities, setIsLoadingCities] = useState(false);
|
const [isLoadingCities, setIsLoadingCities] = useState(false);
|
||||||
const [visibleProductCount, setVisibleProductCount] = useState(14);
|
const [visibleProductCount, setVisibleProductCount] = useState(14);
|
||||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||||
@ -150,7 +151,7 @@ const EditCompanyProfile = () => {
|
|||||||
// { field: 'permanentFactoryCode', label: 'Permanent Factory Code' },
|
// { field: 'permanentFactoryCode', label: 'Permanent Factory Code' },
|
||||||
{ field: 'uniqueLicenseNumber', label: 'Unique License Number' },
|
{ field: 'uniqueLicenseNumber', label: 'Unique License Number' },
|
||||||
// { field: 'industryCodeBusiness', label: 'Industry Code (Business Register)' },
|
// { field: 'industryCodeBusiness', label: 'Industry Code (Business Register)' },
|
||||||
{ field: 'industryCodeCurrent', label: 'Industry Code (Current Production)' },
|
{ field: 'industryCodeCurrent', label: 'Principal activity Code (ISIC Rev 4 - 4 digits)' },
|
||||||
],
|
],
|
||||||
2: [
|
2: [
|
||||||
{ field: 'contactName', label: 'Name' },
|
{ field: 'contactName', label: 'Name' },
|
||||||
@ -177,6 +178,11 @@ const EditCompanyProfile = () => {
|
|||||||
|
|
||||||
// Special validation for Products step
|
// Special validation for Products step
|
||||||
if (stepIndex === 3) {
|
if (stepIndex === 3) {
|
||||||
|
// Skip validation if search is in progress to prevent interference
|
||||||
|
if (isSearching) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const checkedCount = selectedProducts.filter(p => checkedProducts.has(p.value || p.id || p.product_id)).length;
|
const checkedCount = selectedProducts.filter(p => checkedProducts.has(p.value || p.id || p.product_id)).length;
|
||||||
if (checkedCount === 0) {
|
if (checkedCount === 0) {
|
||||||
setProductError("At least one product must be checked.");
|
setProductError("At least one product must be checked.");
|
||||||
@ -310,33 +316,60 @@ const EditCompanyProfile = () => {
|
|||||||
const handleProductSearch = (e) => {
|
const handleProductSearch = (e) => {
|
||||||
const searchTerm = e.target.value.toLowerCase();
|
const searchTerm = e.target.value.toLowerCase();
|
||||||
setProductSearchTerm(searchTerm);
|
setProductSearchTerm(searchTerm);
|
||||||
|
setIsSearching(true); // Set searching flag
|
||||||
|
|
||||||
|
// Preserve current checked products state before performing search
|
||||||
|
const currentCheckedProducts = new Set(checkedProducts);
|
||||||
|
|
||||||
if (!searchTerm) {
|
if (!searchTerm) {
|
||||||
// If search is cleared, reload all products with selected ones filtered out
|
// If search is cleared, reload all products with selected ones filtered out
|
||||||
loadProducts();
|
loadProducts();
|
||||||
|
setIsSearching(false); // Reset searching flag
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all products (including those not currently visible)
|
// Get all products from the original full list (not the filtered availableProducts)
|
||||||
const allProducts = [...availableProducts, ...selectedProducts];
|
// We need to fetch all products again to ensure we have the complete list
|
||||||
|
const performSearch = async () => {
|
||||||
|
try {
|
||||||
|
const allProducts = await fetchProducts();
|
||||||
|
|
||||||
|
// Filter out selected products from the full list
|
||||||
|
const availableFromAll = allProducts.filter(product => {
|
||||||
|
return !selectedProducts.some(selected =>
|
||||||
|
(selected.value && (selected.value === product.value || selected.value === product.id || selected.value === product.product_id)) ||
|
||||||
|
(selected.id && (selected.id === product.id || selected.id === product.value || selected.id === product.product_id)) ||
|
||||||
|
(selected.product_id && (selected.product_id === product.product_id || selected.product_id === product.id || selected.product_id === product.value))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter products based on search term
|
||||||
|
const filtered = availableFromAll.filter(product => {
|
||||||
|
const matchesSearch =
|
||||||
|
(product.label?.toLowerCase().includes(searchTerm) ||
|
||||||
|
product.hs_code?.toLowerCase().includes(searchTerm) ||
|
||||||
|
(product.product_name && product.product_name.toLowerCase().includes(searchTerm)));
|
||||||
|
|
||||||
|
return matchesSearch;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update availableProducts with search results (even if empty)
|
||||||
|
// This allows the "No data found" message to be displayed when search yields no results
|
||||||
|
setAvailableProducts(filtered);
|
||||||
|
// CRITICAL: Always preserve checked products state after search completes
|
||||||
|
// This prevents any accidental reset of checked state from any source
|
||||||
|
setTimeout(() => {
|
||||||
|
setCheckedProducts(currentCheckedProducts);
|
||||||
|
setIsSearching(false); // Reset searching flag after state is restored
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error searching products:', error);
|
||||||
|
setIsSearching(false); // Reset searching flag on error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Filter products based on search term and exclude selected ones
|
performSearch();
|
||||||
const filtered = allProducts.filter(product => {
|
|
||||||
const matchesSearch =
|
|
||||||
(product.label?.toLowerCase().includes(searchTerm) ||
|
|
||||||
product.hs_code?.toLowerCase().includes(searchTerm) ||
|
|
||||||
(product.product_name && product.product_name.toLowerCase().includes(searchTerm)));
|
|
||||||
|
|
||||||
const isSelected = selectedProducts.some(selected =>
|
|
||||||
(selected.value && (selected.value === product.value || selected.value === product.id || selected.value === product.product_id)) ||
|
|
||||||
(selected.id && (selected.id === product.id || selected.id === product.value || selected.id === product.product_id)) ||
|
|
||||||
(selected.product_id && (selected.product_id === product.product_id || selected.product_id === product.id || selected.product_id === product.value))
|
|
||||||
);
|
|
||||||
|
|
||||||
return matchesSearch && !isSelected;
|
|
||||||
});
|
|
||||||
|
|
||||||
setAvailableProducts(filtered);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle checkbox toggle for selected products
|
// Handle checkbox toggle for selected products
|
||||||
@ -401,8 +434,8 @@ const EditCompanyProfile = () => {
|
|||||||
// Clear newly added product IDs
|
// Clear newly added product IDs
|
||||||
setNewlyAddedProductIds(new Set());
|
setNewlyAddedProductIds(new Set());
|
||||||
|
|
||||||
// Refresh component to update UI while staying on current step
|
// Note: Removed navigation to prevent component remount and state reset
|
||||||
navigate(window.location.pathname);
|
// The UI will update automatically through React state changes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -880,47 +913,10 @@ const EditCompanyProfile = () => {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h4 className="text-[16px] font-semibold text-[#232528]">Details</h4>
|
<h4 className="text-[16px] font-semibold text-[#232528]">Details</h4>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{/* <TextField
|
<div className="md:col-span-1">
|
||||||
label="Permanent Factory Code"
|
|
||||||
type="hidden"
|
|
||||||
value={form.permanentFactoryCode || ""}
|
|
||||||
onChange={(e) => handleFormChange("permanentFactoryCode", e.target.value)}
|
|
||||||
placeholder="Enter Factory Code"
|
|
||||||
width="100%"
|
|
||||||
// required
|
|
||||||
error={fieldErrors.permanentFactoryCode}
|
|
||||||
/> */}
|
|
||||||
{/* <TextField
|
|
||||||
|
|
||||||
label="Unique License Number"
|
|
||||||
value={form.uniqueLicenseNumber || ""}
|
|
||||||
onChange={(e) => handleFormChange("uniqueLicenseNumber", e.target.value)}
|
|
||||||
placeholder="Enter License Number"
|
|
||||||
width="100%"
|
|
||||||
required
|
|
||||||
error={fieldErrors.uniqueLicenseNumber}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
label="ERN"
|
|
||||||
value={form.ERN || ""}
|
|
||||||
onChange={(e) => handleFormChange("ERN", e.target.value)}
|
|
||||||
placeholder="Enter ERN"
|
|
||||||
width="100%"
|
|
||||||
error={fieldErrors.ERN}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
label="Industry Code (Business Register)"
|
|
||||||
value={form.industryCodeBusiness || ""}
|
|
||||||
onChange={(e) => handleFormChange("industryCodeBusiness", e.target.value)}
|
|
||||||
placeholder="Enter Code"
|
|
||||||
width="100%"
|
|
||||||
// required
|
|
||||||
error={fieldErrors.industryCodeBusiness}
|
|
||||||
/> */}
|
|
||||||
<div className="whitespace-nowrap">
|
|
||||||
<SelectField
|
<SelectField
|
||||||
label="Industry Code (Current Production)"
|
label="Principal activity Code (ISIC Rev 4 - 4 digits)"
|
||||||
name="industryCodeCurrent"
|
name="industryCodeCurrent"
|
||||||
value={form.industryCodeCurrent || ''}
|
value={form.industryCodeCurrent || ''}
|
||||||
onChange={(e) => handleFormChange("industryCodeCurrent", e.target.value)}
|
onChange={(e) => handleFormChange("industryCodeCurrent", e.target.value)}
|
||||||
@ -931,17 +927,16 @@ const EditCompanyProfile = () => {
|
|||||||
error={fieldErrors.industryCodeCurrent}
|
error={fieldErrors.industryCodeCurrent}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="whitespace-nowrap">
|
<div className="md:col-span-1">
|
||||||
<TextField
|
<TextField
|
||||||
label="ERN"
|
label="ERN"
|
||||||
value={form.ERN || ""}
|
value={form.ERN || ""}
|
||||||
onChange={(e) => handleFormChange("ERN", e.target.value)}
|
onChange={(e) => handleFormChange("ERN", e.target.value)}
|
||||||
placeholder="Enter ERN"
|
placeholder="Enter ERN"
|
||||||
width="100%"
|
width="100%"
|
||||||
error={fieldErrors.ERN}
|
error={fieldErrors.ERN}
|
||||||
readOnly
|
readOnly
|
||||||
|
/>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-full">
|
<div className="col-span-full">
|
||||||
{/* <TextField
|
{/* <TextField
|
||||||
@ -1138,11 +1133,12 @@ const EditCompanyProfile = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
{/* Available Products Panel - Only show if there are available products and not all selected products are checked */}
|
{/* Available Products Panel - Show if there are available products and not all selected products are checked, OR if there's a search term */}
|
||||||
{(() => {
|
{(() => {
|
||||||
// More robust condition that handles state inconsistencies
|
// More robust condition that handles state inconsistencies
|
||||||
const hasAvailableProducts = availableProducts.length > 0;
|
const hasAvailableProducts = availableProducts.length > 0;
|
||||||
const hasSelectedProducts = selectedProducts.length > 0;
|
const hasSelectedProducts = selectedProducts.length > 0;
|
||||||
|
const hasSearchTerm = productSearchTerm.length > 0;
|
||||||
|
|
||||||
// Count how many selected products are actually checked
|
// Count how many selected products are actually checked
|
||||||
let actuallyCheckedCount = 0;
|
let actuallyCheckedCount = 0;
|
||||||
@ -1154,7 +1150,7 @@ const EditCompanyProfile = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hasUncheckedProducts = actuallyCheckedCount < selectedProducts.length;
|
const hasUncheckedProducts = actuallyCheckedCount < selectedProducts.length;
|
||||||
const shouldShow = hasAvailableProducts && hasSelectedProducts && hasUncheckedProducts;
|
const shouldShow = (hasAvailableProducts && hasSelectedProducts && hasUncheckedProducts) || hasSearchTerm;
|
||||||
|
|
||||||
|
|
||||||
return shouldShow;
|
return shouldShow;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user