CR Bug fixed
This commit is contained in:
parent
465cc8f9b0
commit
2e1bd35f24
@ -313,6 +313,7 @@ export const SelectField = ({
|
||||
error = '',
|
||||
readOnly = false,
|
||||
required = false,
|
||||
forceDropdownBelow = false,
|
||||
}) => {
|
||||
const wrapperStyle = {};
|
||||
if (width !== 'auto') {
|
||||
@ -321,20 +322,84 @@ export const SelectField = ({
|
||||
const baseStyle = baseInputStyles[variant] || baseInputStyles.default;
|
||||
const inputStyle = { ...baseStyle, ...style };
|
||||
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 hasValue = value !== undefined && value !== null && value !== '' && value !== normalizedClear;
|
||||
const showClear = allowClear && hasValue && !readOnly;
|
||||
|
||||
if (rightIcon) {
|
||||
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) => {
|
||||
setFocused(true);
|
||||
onFocus?.(event);
|
||||
};
|
||||
|
||||
const handleBlur = (event) => {
|
||||
setFocused(false);
|
||||
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);
|
||||
inputStyle.borderColor = hasError
|
||||
? '#B91C1C'
|
||||
@ -353,15 +418,98 @@ export const SelectField = ({
|
||||
inputStyle.backgroundColor = '#F9FAFB';
|
||||
inputStyle.cursor = 'not-allowed';
|
||||
}
|
||||
const handleClear = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (onClear) {
|
||||
onClear();
|
||||
} else if (onChange) {
|
||||
onChange({ target: { value: normalizedClear } });
|
||||
}
|
||||
};
|
||||
|
||||
// Custom dropdown implementation
|
||||
if (forceDropdownBelow) {
|
||||
return (
|
||||
<div className={`w-full ${wrapperStyle ? '' : ''}`} style={wrapperStyle || {}} ref={dropdownRef}>
|
||||
{label && (
|
||||
<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 (
|
||||
<div className={`w-full ${wrapperStyle ? '' : ''}`} style={wrapperStyle || {}}>
|
||||
{label && (
|
||||
|
||||
@ -7,6 +7,7 @@ const getSafeEstablishmentId = () => {
|
||||
};
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { getEstablishmentProducts } from '../../../services/submissions/submissionService';
|
||||
import { fetchEstablishmentDetail } from '../../../services/establishments/establishmentService';
|
||||
import { getEmirates } from '../../../services/masters/masterService';
|
||||
import Table from '@/components/common/Table';
|
||||
|
||||
@ -154,11 +155,11 @@ const EstablishmentInfo = ({
|
||||
|
||||
setLoadingProducts(true);
|
||||
try {
|
||||
const response = await getEstablishmentProducts(establishmentId);
|
||||
if (response.status === 'success') {
|
||||
setProducts(response.data || []);
|
||||
const response = await fetchEstablishmentDetail(establishmentId);
|
||||
if (response?.data?.establishment_products) {
|
||||
setProducts(response.data.establishment_products || []);
|
||||
} else {
|
||||
setProductsError(response.message || 'Failed to load products');
|
||||
setProductsError('Failed to load products');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching products:', error);
|
||||
@ -652,7 +653,7 @@ React.useEffect(() => {
|
||||
</Card>
|
||||
|
||||
<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 ? (
|
||||
<div className="flex justify-center py-4">
|
||||
<div className="h-6 w-6 animate-spin rounded-full border-b-2 border-[#92722A]" />
|
||||
@ -663,9 +664,9 @@ React.useEffect(() => {
|
||||
<Table
|
||||
headers={['Product Name', 'HS Code', 'Description']}
|
||||
rows={products.map(product => [
|
||||
product['product.product_name'] || 'N/A',
|
||||
product['product.hs_code'] || 'N/A',
|
||||
product['product.hs_description'] || 'N/A'
|
||||
product.product.product_name || '-',
|
||||
product.product.hs_code || '-',
|
||||
product.product.hs_description || '-'
|
||||
])}
|
||||
columnWidths={['33.33%', '33.33%', '33.33%']}
|
||||
separated={true}
|
||||
|
||||
@ -91,7 +91,8 @@ const SearchableSelect = ({
|
||||
disabled = false,
|
||||
error = false,
|
||||
className = '',
|
||||
displayValue = undefined
|
||||
displayValue = undefined,
|
||||
allowNull = false // New prop to allow null values for no selection
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
const [searchTerm, setSearchTerm] = React.useState('');
|
||||
@ -153,22 +154,25 @@ const SearchableSelect = ({
|
||||
|
||||
// Get selected option label
|
||||
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
|
||||
React.useEffect(() => {
|
||||
if (displayValue !== undefined && displayValue !== '') {
|
||||
setInternalDisplayValue(displayValue);
|
||||
} else if (allowNull && value === null) {
|
||||
// When allowNull is true and value is null, show empty (no selection)
|
||||
setInternalDisplayValue('');
|
||||
} else if (selectedOption) {
|
||||
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
|
||||
const matchedOption = options.find(opt => opt.value === value);
|
||||
setInternalDisplayValue(matchedOption?.label || '');
|
||||
} else {
|
||||
setInternalDisplayValue('');
|
||||
}
|
||||
}, [value, displayValue, selectedOption, options]);
|
||||
}, [value, displayValue, selectedOption, options, allowNull]);
|
||||
|
||||
const displayValueToShow = internalDisplayValue;
|
||||
|
||||
@ -197,7 +201,7 @@ const SearchableSelect = ({
|
||||
if (error) {
|
||||
borderColor = 'border-red-500';
|
||||
bgColor = 'bg-red-50';
|
||||
} else if (value || isOpen) {
|
||||
} else if ((value !== null && value !== undefined && value !== '') || isOpen) {
|
||||
borderColor = 'border-[#92722A]';
|
||||
}
|
||||
|
||||
@ -241,7 +245,7 @@ const SearchableSelect = ({
|
||||
<div
|
||||
key={option.value}
|
||||
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)}
|
||||
>
|
||||
@ -267,7 +271,9 @@ const Select = ({
|
||||
loading = false,
|
||||
disabled = false,
|
||||
error = false,
|
||||
className = ''
|
||||
className = '',
|
||||
allowClear = false,
|
||||
onClear
|
||||
}) => {
|
||||
const [isFocused, setIsFocused] = React.useState(false);
|
||||
|
||||
@ -281,6 +287,19 @@ const Select = ({
|
||||
} else if (value || isFocused) {
|
||||
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 (
|
||||
<div className="relative">
|
||||
@ -317,8 +336,17 @@ const Select = ({
|
||||
})
|
||||
)}
|
||||
</select>
|
||||
<div className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 flex items-center">
|
||||
{loading ? (
|
||||
<div className="absolute right-3 top-1/2 -translate-y-1/2 flex items-center">
|
||||
{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" />
|
||||
) : (
|
||||
<img src={caretDownSrc} alt="open" className="h-4 w-4" />
|
||||
@ -418,6 +446,11 @@ const ProductData = ({
|
||||
const [unitsError, setUnitsError] = React.useState(null);
|
||||
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
|
||||
|
||||
@ -441,6 +474,57 @@ const ProductData = ({
|
||||
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 value = e.target.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(() => {
|
||||
const fetchQuarterPeriods = async () => {
|
||||
try {
|
||||
@ -564,9 +751,9 @@ useEffect(() => {
|
||||
junCost: product.forecast_cost_period_three?.toString() || '0',
|
||||
|
||||
// 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 || '',
|
||||
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 || '',
|
||||
remarks: product.remarks || '',
|
||||
|
||||
@ -730,9 +917,9 @@ const existingProduct = currentSubmission.products?.find(p =>
|
||||
// Next quarter forecast (Q2) - December
|
||||
forecast_quantity: product.decQuantity || '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',
|
||||
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',
|
||||
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',
|
||||
forecast_quantity: product.decQuantity || '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',
|
||||
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',
|
||||
remarks: product.remarks || remarks || '',
|
||||
is_active: false // Mark as inactive
|
||||
@ -893,9 +1080,9 @@ useEffect(() => {
|
||||
junCost: product.forecast_cost_period_three || '0',
|
||||
|
||||
// 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 || '',
|
||||
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 || '',
|
||||
remarks: product.remarks || ''
|
||||
};
|
||||
@ -1295,8 +1482,8 @@ useEffect(() => {
|
||||
// Dynamic validation for variation reason based on cost variation
|
||||
const mandatoryVariationReason = isVariationReasonMandatory(product);
|
||||
if (mandatoryVariationReason) {
|
||||
// Check if variation reason is selected
|
||||
if (!product.variationReason || product.variationReason === '0' || product.variationReason === '') {
|
||||
// Check if variation reason is selected (null means no selection)
|
||||
if (product.variationReason === null || product.variationReason === undefined || product.variationReason === '') {
|
||||
errors[`variationReason_${index}`] = 'Reason for variation is required when cost variation exceeds ±10%';
|
||||
isValid = false;
|
||||
}
|
||||
@ -1718,8 +1905,8 @@ useEffect(() => {
|
||||
const newErrors = { ...formErrors };
|
||||
|
||||
if (mandatoryVariationReason) {
|
||||
// Check if variation reason is selected
|
||||
if (!updatedProduct.variationReason || updatedProduct.variationReason === '0' || updatedProduct.variationReason === '') {
|
||||
// Check if variation reason is selected (null means no selection)
|
||||
if (updatedProduct.variationReason === null || updatedProduct.variationReason === undefined || updatedProduct.variationReason === '') {
|
||||
newErrors[variationErrorKey] = 'Reason for variation is required when cost variation exceeds ±10%';
|
||||
} else {
|
||||
delete newErrors[variationErrorKey];
|
||||
@ -1759,8 +1946,8 @@ useEffect(() => {
|
||||
|
||||
if (mandatoryVariationReason) {
|
||||
if (field === 'variationReason') {
|
||||
// Check if variation reason is selected
|
||||
if (!updatedProduct.variationReason || updatedProduct.variationReason === '0' || updatedProduct.variationReason === '') {
|
||||
// Check if variation reason is selected (null means no selection)
|
||||
if (updatedProduct.variationReason === null || updatedProduct.variationReason === undefined || updatedProduct.variationReason === '') {
|
||||
newErrors[variationErrorKey] = 'Reason for variation is required when cost variation exceeds ±10%';
|
||||
} else {
|
||||
delete newErrors[variationErrorKey];
|
||||
@ -2566,12 +2753,14 @@ useEffect(() => {
|
||||
Reason for Variation
|
||||
{isVariationReasonMandatory(p) && <span className="text-red-600">*</span>}
|
||||
</label>
|
||||
<Select
|
||||
<SearchableSelect
|
||||
placeholder={isLoadingReasons ? 'Loading reasons...' : 'Select reason'}
|
||||
options={variationReasons}
|
||||
value={p.variationReason || ''}
|
||||
value={p.variationReason === null || p.variationReason === '' ? null : p.variationReason}
|
||||
onChange={(e) => updateProductField(p.id, 'variationReason', e.target.value, variationReasons)}
|
||||
loading={isLoadingReasons}
|
||||
error={!!formErrors[`variationReason_${idx}`]}
|
||||
allowNull={true}
|
||||
/>
|
||||
{formErrors[`variationReason_${idx}`] && (
|
||||
<p className="mt-1 text-xs text-red-600">{formErrors[`variationReason_${idx}`]}</p>
|
||||
@ -2724,11 +2913,13 @@ useEffect(() => {
|
||||
</div>
|
||||
<div>
|
||||
<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'}
|
||||
options={zeroTargetReasons}
|
||||
value={p.zeroTargetReason || ''}
|
||||
value={p.zeroTargetReason === null || p.zeroTargetReason === '' ? null : p.zeroTargetReason}
|
||||
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'))))) && (
|
||||
<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: 'uniqueLicenseNumber', label: 'Unique License Number' },
|
||||
// { 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: [
|
||||
{ field: 'contactName', label: 'Name' },
|
||||
@ -1009,7 +1009,7 @@ const CompanyProfile = () => {
|
||||
<h4 className="text-[16px] font-semibold text-[#232528]">Details</h4>
|
||||
|
||||
{/* 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>
|
||||
<TextField
|
||||
label="Permanent Factory Code"
|
||||
@ -1044,10 +1044,11 @@ const CompanyProfile = () => {
|
||||
readOnly={modalMode === 'view'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2 w-full">
|
||||
<div>
|
||||
<SelectField
|
||||
label="Industry Code(Current Production)"
|
||||
label="Principal activity Code (ISIC Rev 4 - 4 digits)"
|
||||
value={form.industryCodeProduction}
|
||||
onChange={handleFormChange('industryCodeProduction')}
|
||||
placeholder="Select ISIC Code"
|
||||
@ -1057,10 +1058,9 @@ const CompanyProfile = () => {
|
||||
readOnly={modalMode === 'view'}
|
||||
options={isicOptions}
|
||||
searchable
|
||||
forceDropdownBelow={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2 w-full">
|
||||
<div>
|
||||
<TextField
|
||||
label="ERN"
|
||||
@ -3784,7 +3784,7 @@ const handleImport = async () => {
|
||||
<span>•</span><span>HS Code</span>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
@ -122,6 +122,7 @@ const EditCompanyProfile = () => {
|
||||
const [isLoadingProducts, setIsLoadingProducts] = useState(false);
|
||||
const [productError, setProductError] = useState("");
|
||||
const [productLimitMessage, setProductLimitMessage] = useState("");
|
||||
const [isSearching, setIsSearching] = useState(false); // Flag to prevent validation during search
|
||||
const [isLoadingCities, setIsLoadingCities] = useState(false);
|
||||
const [visibleProductCount, setVisibleProductCount] = useState(14);
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||
@ -150,7 +151,7 @@ const EditCompanyProfile = () => {
|
||||
// { field: 'permanentFactoryCode', label: 'Permanent Factory Code' },
|
||||
{ field: 'uniqueLicenseNumber', label: 'Unique License Number' },
|
||||
// { 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: [
|
||||
{ field: 'contactName', label: 'Name' },
|
||||
@ -177,6 +178,11 @@ const EditCompanyProfile = () => {
|
||||
|
||||
// Special validation for Products step
|
||||
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;
|
||||
if (checkedCount === 0) {
|
||||
setProductError("At least one product must be checked.");
|
||||
@ -310,33 +316,60 @@ const EditCompanyProfile = () => {
|
||||
const handleProductSearch = (e) => {
|
||||
const searchTerm = e.target.value.toLowerCase();
|
||||
setProductSearchTerm(searchTerm);
|
||||
setIsSearching(true); // Set searching flag
|
||||
|
||||
// Preserve current checked products state before performing search
|
||||
const currentCheckedProducts = new Set(checkedProducts);
|
||||
|
||||
if (!searchTerm) {
|
||||
// If search is cleared, reload all products with selected ones filtered out
|
||||
loadProducts();
|
||||
setIsSearching(false); // Reset searching flag
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all products (including those not currently visible)
|
||||
const allProducts = [...availableProducts, ...selectedProducts];
|
||||
// Get all products from the original full list (not the filtered availableProducts)
|
||||
// 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
|
||||
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);
|
||||
performSearch();
|
||||
};
|
||||
|
||||
// Handle checkbox toggle for selected products
|
||||
@ -401,8 +434,8 @@ const EditCompanyProfile = () => {
|
||||
// Clear newly added product IDs
|
||||
setNewlyAddedProductIds(new Set());
|
||||
|
||||
// Refresh component to update UI while staying on current step
|
||||
navigate(window.location.pathname);
|
||||
// Note: Removed navigation to prevent component remount and state reset
|
||||
// The UI will update automatically through React state changes
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -880,47 +913,10 @@ const EditCompanyProfile = () => {
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<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">
|
||||
{/* <TextField
|
||||
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">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="md:col-span-1">
|
||||
<SelectField
|
||||
label="Industry Code (Current Production)"
|
||||
label="Principal activity Code (ISIC Rev 4 - 4 digits)"
|
||||
name="industryCodeCurrent"
|
||||
value={form.industryCodeCurrent || ''}
|
||||
onChange={(e) => handleFormChange("industryCodeCurrent", e.target.value)}
|
||||
@ -931,17 +927,16 @@ const EditCompanyProfile = () => {
|
||||
error={fieldErrors.industryCodeCurrent}
|
||||
/>
|
||||
</div>
|
||||
<div className="whitespace-nowrap">
|
||||
<TextField
|
||||
label="ERN"
|
||||
value={form.ERN || ""}
|
||||
onChange={(e) => handleFormChange("ERN", e.target.value)}
|
||||
placeholder="Enter ERN"
|
||||
width="100%"
|
||||
error={fieldErrors.ERN}
|
||||
readOnly
|
||||
|
||||
/>
|
||||
<div className="md:col-span-1">
|
||||
<TextField
|
||||
label="ERN"
|
||||
value={form.ERN || ""}
|
||||
onChange={(e) => handleFormChange("ERN", e.target.value)}
|
||||
placeholder="Enter ERN"
|
||||
width="100%"
|
||||
error={fieldErrors.ERN}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-full">
|
||||
{/* <TextField
|
||||
@ -1138,11 +1133,12 @@ const EditCompanyProfile = () => {
|
||||
)}
|
||||
</div>
|
||||
<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
|
||||
const hasAvailableProducts = availableProducts.length > 0;
|
||||
const hasSelectedProducts = selectedProducts.length > 0;
|
||||
const hasSearchTerm = productSearchTerm.length > 0;
|
||||
|
||||
// Count how many selected products are actually checked
|
||||
let actuallyCheckedCount = 0;
|
||||
@ -1154,7 +1150,7 @@ const EditCompanyProfile = () => {
|
||||
}
|
||||
|
||||
const hasUncheckedProducts = actuallyCheckedCount < selectedProducts.length;
|
||||
const shouldShow = hasAvailableProducts && hasSelectedProducts && hasUncheckedProducts;
|
||||
const shouldShow = (hasAvailableProducts && hasSelectedProducts && hasUncheckedProducts) || hasSearchTerm;
|
||||
|
||||
|
||||
return shouldShow;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user