CR Bug fixed

This commit is contained in:
Malini 2026-03-02 17:40:16 +05:30
parent 465cc8f9b0
commit 2e1bd35f24
5 changed files with 462 additions and 126 deletions

View File

@ -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 && (

View File

@ -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}

View File

@ -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">

View File

@ -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>

View File

@ -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;