diff --git a/ipi-survey-platform/src/pages/Admin/configuration/CompanyProfile.jsx b/ipi-survey-platform/src/pages/Admin/configuration/CompanyProfile.jsx index b0dea13..3e90e08 100644 --- a/ipi-survey-platform/src/pages/Admin/configuration/CompanyProfile.jsx +++ b/ipi-survey-platform/src/pages/Admin/configuration/CompanyProfile.jsx @@ -14,6 +14,7 @@ import { uploadCompanyProfileCSVAlternative } from '@/services/establishments/establishmentService'; import { fetchProducts } from '@/services/masters/masterService'; + const searchIconSrc = '/assets/images/material-symbols_search-rounded.svg'; const downloadIconSrc = '/assets/images/DownloadSimple.svg'; const addIconSrc = '/assets/images/ic_baseline-plus.svg'; @@ -51,22 +52,8 @@ const CustomToast = ({ message, type, onClose }) => { } }; - const getIcon = () => { - switch (type) { - case 'success': - return ''; - case 'error': - return ''; - case 'warning': - return ''; - default: - return ''; - } - }; - return (
- {/* {getIcon()} */} {message}
@@ -677,9 +618,8 @@ const CompanyProfile = () => { /> { - // When emirate changes, update both the ID and name const selectedEmirate = emirateOptions.find(opt => opt.value === e.target.value); handleFormChange('contactEmirateId')(e); handleFormChange('contactEmirate')({ @@ -712,7 +652,6 @@ const CompanyProfile = () => { label="Postal Code" value={form.contactPostalCode} onChange={(e) => { - // Only allow numeric input if (e.target.value === '' || /^[0-9\b]+$/.test(e.target.value)) { handleFormChange('contactPostalCode')(e); } @@ -744,7 +683,6 @@ const CompanyProfile = () => { onChange={handleFormChange('contactPersonName')} placeholder="Enter Name" width="100%" - // required error={fieldErrors.contactPersonName} /> { onCountryCodeChange={handleFormChange('contactCountryCode')} value={form.contactMobileNumber} onChange={handleFormChange('contactMobileNumber')} - // placeholder="---------" width="100%" required error={fieldErrors.contactMobileNumber} @@ -786,164 +723,6 @@ const CompanyProfile = () => { ); - // case 4: - return ( -
-
-
-

Corporate/Head Office Contact Details

-
- -
-
- - - { - // When emirate changes, update both the ID and name - const selectedEmirate = emirateOptions.find(opt => opt.value === e.target.value); - handleFormChange('corporateEmirateId')(e); - handleFormChange('corporateEmirate')({ - target: { value: selectedEmirate?.label || '' } - }); - }} - options={emirateOptions} - placeholder="Select Emirate" - width="100%" - required - error={fieldErrors.corporateEmirate} - readOnly={form.corporateSameAs} - /> - { - const selectedCity = corporateCityOptions.find(opt => opt.value === e.target.value); - handleFormChange('corporateCityTownId')(e); - handleFormChange('corporateCityTown')({ - target: { value: selectedCity?.label || '' } - }); - }} - options={corporateCityOptions} - placeholder="Select City/Town" - width="100%" - required - error={fieldErrors.corporateCityTown} - readOnly={form.corporateSameAs} - /> - - { - // Only allow numeric input - if (e.target.value === '' || /^[0-9\b]+$/.test(e.target.value)) { - handleFormChange('corporatePostalCode')(e); - } - }} - placeholder="Enter postal code" - width="100%" - type="tel" - inputMode="numeric" - pattern="[0-9]*" - /> - - - - - - - -
-
- ); case 4: return (
@@ -956,7 +735,6 @@ const CompanyProfile = () => { placeholder="Enter Count" width="100%" type="number" - // required error={fieldErrors.employmentEmiratiMale} /> { placeholder="Enter Count" width="100%" type="number" - // required error={fieldErrors.employmentNonEmiratiMale} /> { placeholder="Enter Count" width="100%" type="number" - // required error={fieldErrors.employmentEmiratiFemale} /> { placeholder="Enter Count" width="100%" type="number" - // required error={fieldErrors.employmentNonEmiratiFemale} /> {
  • - {product.hsCode || ''} - {product.hsCode && (product.productName || product.label || product.product_name) ? ' - ' : ''} + {product.hsCode || product.hs_code || ''} + {(product.hsCode || product.hs_code) && (product.productName || product.label || product.product_name) ? ' - ' : ''} {product.productName || product.label || product.product_name || ''}
    @@ -1060,7 +835,6 @@ const CompanyProfile = () => { className="text-[#92722A] hover:text-[#7A5F1E] text-sm font-medium" > Add -
  • ))} @@ -1092,7 +866,7 @@ const CompanyProfile = () => {
    {product.hs_code || product.hsCode || ''} - {product.hs_code || product.hsCode ? ' - ' : ''} + {(product.hs_code || product.hsCode) ? ' - ' : ''} {product.product_name || product.productName || product.label || 'Unnamed Product'}
    @@ -1116,247 +890,191 @@ const CompanyProfile = () => {
    ); - default: - return ( -
    -
    -

    Administrative Details

    -
    -
    - - - - - - - - - -
    -
    - ); + return null; + } + }; + + // Enhanced product loading with better error handling + const loadProducts = async () => { + try { + console.log('Fetching products...'); + setIsLoadingProducts(true); + const response = await fetchProducts(); + console.log('Products API Response:', response); + + let productsData = []; + + if (Array.isArray(response)) { + productsData = response; + } else if (response?.data) { + productsData = Array.isArray(response.data) ? response.data : response.data.products || []; + } + + console.log('Processed products data:', productsData); + + // Enhanced product formatting to handle various API response structures + const formattedProducts = productsData.map(p => ({ + id: p.id || p.value || p.product_id || 0, + product_id: p.product_id || p.id || p.value || 0, + hs_code: p.hs_code || p.hsCode || '', + hsCode: p.hsCode || p.hs_code || '', + product_name: p.product_name || p.productName || p.label || '', + productName: p.productName || p.product_name || p.label || '', + label: p.label || `${p.product_name || p.productName || 'Unnamed Product'}${(p.hs_code || p.hsCode) ? ` (${p.hs_code || p.hsCode})` : ''}`, + value: p.value || p.id || p.product_id || 0, + ...p + })); + + setProducts(formattedProducts); + + // Filter out already selected products + const available = formattedProducts.filter(p => + !selectedProducts.some(sp => sp.id === p.id || sp.product_id === p.product_id) + ); + setAvailableProducts(available); + + } catch (error) { + console.error('Error loading products:', error); + showToast('error', 'Failed to load products'); + } finally { + setIsLoadingProducts(false); } }; // Fetch products when Products step is active React.useEffect(() => { if (activeStep === 3) { - // In the loadProducts function (around line 1044), update the product processing: -const loadProducts = async () => { - try { - console.log('Fetching products...'); - setIsLoadingProducts(true); - const response = await fetchProducts(); - console.log('Products API Response:', response); - - let productsData = []; - - if (Array.isArray(response)) { - productsData = response; - } else if (response?.data) { - productsData = Array.isArray(response.data) ? response.data : response.data.products || []; - } - - ('Processed products data:', productsData); - - // Update the products state with the formatted data - const formattedProducts = productsData.map(p => ({ - id: p.value, - label: p.label, - value: p.value - })); - - setProducts(formattedProducts); - setAvailableProducts(formattedProducts.filter(p => - !selectedProducts.some(sp => sp.id === p.id) - )); - - } catch (error) { - console.error('Error loading products:', error); - showToast('error', 'Failed to load products'); - } finally { - setIsLoadingProducts(false); - } -}; - loadProducts(); } }, [activeStep, selectedProducts]); const handleProductSearch = (e) => { - const searchTerm = e.target.value.toLowerCase(); - setProductSearchTerm(searchTerm); - - if (!searchTerm) { - setAvailableProducts(products.filter(p => - !selectedProducts.some(sp => sp.id === p.id) - )); - return; - } - - const filtered = products.filter(product => - product.label.toLowerCase().includes(searchTerm) && - !selectedProducts.some(sp => sp.id === product.id) - ); - - setAvailableProducts(filtered); -}; + const searchTerm = e.target.value.toLowerCase(); + setProductSearchTerm(searchTerm); + + if (!searchTerm) { + setAvailableProducts(products.filter(p => + !selectedProducts.some(sp => sp.id === p.id) + )); + return; + } + + const filtered = products.filter(product => { + const productName = (product.productName || product.label || product.product_name || '').toLowerCase(); + const hsCode = (product.hsCode || product.hs_code || '').toLowerCase(); + + return (productName.includes(searchTerm) || hsCode.includes(searchTerm)) && + !selectedProducts.some(sp => sp.id === product.id); + }); + + setAvailableProducts(filtered); + }; const handleAddProduct = (product) => { - // Clear any previous product errors setProductError(''); setSaveError(''); + + // Enhanced product formatting to ensure all required fields + const enhancedProduct = { + id: product.id || product.product_id || 0, + product_id: product.product_id || product.id || 0, + hs_code: product.hs_code || product.hsCode || '', + hsCode: product.hsCode || product.hs_code || '', + product_name: product.product_name || product.productName || product.label || '', + productName: product.productName || product.product_name || product.label || '', + label: product.label || `${product.product_name || product.productName || 'Unnamed Product'}${(product.hs_code || product.hsCode) ? ` (${product.hs_code || product.hsCode})` : ''}`, + value: product.value || product.id || product.product_id || 0, + ...product + }; + setSelectedProducts(prev => { - const updated = [...prev, product]; + const updated = [...prev, enhancedProduct]; return updated; }); - // Remove from available products + setAvailableProducts(prev => prev.filter(p => p.id !== product.id)); - setAvailableProducts(prev => { - const updated = prev.filter(p => p.id !== product.id); - return updated; - }); }; const handleRemoveProduct = (product) => { - // Clear any previous product errors setProductError(''); setSaveError(''); + setSelectedProducts(prev => { const updated = prev.filter(p => p.id !== product.id); return updated; }); - // Add back to available products if not already there - setAvailableProducts(prev => { - if (!prev.some(p => p.id === product.id)) { - return [...prev, product]; - } - return prev; - }); - // Only add back to available if it matches the search term or there's no search term + // Add back to available products if it matches the search term or there's no search term if (!productSearchTerm || - (product.hs_code && product.hs_code.toLowerCase().includes(productSearchTerm)) || - (product.product_name && product.product_name.toLowerCase().includes(productSearchTerm))) { + ((product.hs_code && product.hs_code.toLowerCase().includes(productSearchTerm)) || + (product.product_name && product.product_name.toLowerCase().includes(productSearchTerm)))) { setAvailableProducts(prev => { - const updated = [...prev, product]; - console.log('Updated available products after remove:', updated); - return updated; + if (!prev.some(p => p.id === product.id)) { + return [...prev, product]; + } + return prev; }); } }; - const handleNextStep = React.useCallback(() => { - // Set form as submitted when user tries to proceed - setFormSubmitted(true); - - // Handle product selection validation for step 3 - if (activeStep === 3) { - if (!selectedProducts || selectedProducts.length === 0) { - setProductError('Please select at least one product'); - showToast('error', 'Please select at least one product'); + const handleNextStep = React.useCallback(() => { + setFormSubmitted(true); + + // Handle product selection validation for step 3 + if (activeStep === 3) { + if (!selectedProducts || selectedProducts.length === 0) { + setProductError('Please select at least one product'); + showToast('error', 'Please select at least one product'); + return; + } + } + + // Password validation for step 0 + if (activeStep === 0 && form.userProfilePassword && form.userProfilePassword !== form.userProfileConfirmPassword) { + setConfirmError('Passwords do not match'); return; } - } + + if (activeStep === 0 && form.userProfilePassword) { + const complexityMessage = validatePasswordComplexity(form.userProfilePassword || ''); + if (complexityMessage) { + setPasswordError(complexityMessage); + return; + } + } - // Rest of your validation logic - if (activeStep === 0 && form.userProfilePassword && form.userProfilePassword !== form.userProfileConfirmPassword) { - setConfirmError('Passwords do not match'); - return; - } - - if (activeStep === 0 && form.userProfilePassword) { - const complexityMessage = validatePasswordComplexity(form.userProfilePassword || ''); - if (complexityMessage) { - setPasswordError(complexityMessage); + // Step field validation + if (!validateStepFields(activeStep)) { return; } - } - // If all validations pass, reset form submitted state and proceed - setFormSubmitted(false); - setConfirmError(''); - setActiveStep((prev) => Math.min(prev + 1, formSteps.length - 1)); -}, [activeStep, form.userProfilePassword, form.userProfileConfirmPassword, formSteps.length, validateStepFields, selectedProducts, showToast]); + // If all validations pass, reset form submitted state and proceed + setFormSubmitted(false); + setConfirmError(''); + setActiveStep((prev) => Math.min(prev + 1, formSteps.length - 1)); + }, [activeStep, form.userProfilePassword, form.userProfileConfirmPassword, formSteps.length, validateStepFields, selectedProducts, showToast]); const handleToggleStatus = async (establishmentId, currentStatus) => { - try { - const newStatus = !currentStatus; - await updateEstablishment(establishmentId, { is_active: newStatus }); - - // Update the local state to reflect the change - setProfiles(prevProfiles => - prevProfiles.map(profile => - profile.apiId === establishmentId - ? { ...profile, isActive: newStatus, status: newStatus ? 'Active' : 'Inactive' } - : profile - ) - ); - - showToast('success', `Establishment ${newStatus ? 'activated' : 'deactivated'} successfully`); - } catch (error) { - console.error('Error updating status:', error); - const message = error?.response?.data?.message || 'Failed to update status. Please try again.'; - showToast('error', message); - } -}; - + try { + const newStatus = !currentStatus; + await updateEstablishment(establishmentId, { is_active: newStatus }); + + setProfiles(prevProfiles => + prevProfiles.map(profile => + profile.apiId === establishmentId + ? { ...profile, isActive: newStatus, status: newStatus ? 'Active' : 'Inactive' } + : profile + ) + ); + + showToast('success', `Establishment ${newStatus ? 'activated' : 'deactivated'} successfully`); + } catch (error) { + console.error('Error updating status:', error); + const message = error?.response?.data?.message || 'Failed to update status. Please try again.'; + showToast('error', message); + } + }; const filteredProfiles = React.useMemo(() => { if (!debouncedSearch.trim()) return profiles; @@ -1383,7 +1101,6 @@ const loadProducts = async () => { active: { text: 'Active', bgColor: 'bg-[#F3FAF4]', textColor: 'text-[#2F663C]' }, inactive: { text: 'Inactive', bgColor: 'bg-[#E9EDF3]', textColor: 'text-[#232528]' }, pending: { text: 'Pending', bgColor: 'bg-[#FFFBEB]', textColor: 'text-[#B45309]' }, - // Add more status mappings as needed }; const statusConfig = statusMap[status?.toLowerCase()] || { text: status, bgColor: 'bg-gray-100', textColor: 'text-gray-800' }; @@ -1396,11 +1113,6 @@ const loadProducts = async () => { }; const displayedRows = filteredProfiles.map((item) => { - const isEditing = - editingRow !== null && profiles[editingRow]?.establishmentId === item.establishmentId; - const isDeleting = - deletingRow !== null && profiles[deletingRow]?.establishmentId === item.establishmentId; - return [ item.establishmentName, item.userProfileName || item.contactPersonName || item.createdBy || '', @@ -1423,7 +1135,7 @@ const loadProducts = async () => { onClick={() => handleEdit(item.apiId ?? item.establishmentId)} > Edit @@ -1442,24 +1154,7 @@ const loadProducts = async () => { alt={item.status === 'Active' ? 'Active' : 'Inactive'} className="h-[20px] w-[40px] object-contain" /> - - {/* */} - - - + ), ]; @@ -1546,34 +1241,34 @@ const loadProducts = async () => { ); const captureCorporateValues = (formData) => { - const corporateValues = {}; - const corporateFields = [ - 'corporateName', - 'corporateAddress', - 'corporateCityTown', - 'corporateCityTownId', - 'corporateEmirate', - 'corporateEmirateId', - 'corporatePostalCode', - 'corporatePoBox', - 'corporateMakaniNumber', - 'corporateContactPersonName', - 'corporateContactPersonDesignation', - 'corporateCountryCode', - 'corporateMobileNumber', - 'corporateEmail', - 'corporateWebsite' - ]; + const corporateValues = {}; + const corporateFields = [ + 'corporateName', + 'corporateAddress', + 'corporateCityTown', + 'corporateCityTownId', + 'corporateEmirate', + 'corporateEmirateId', + 'corporatePostalCode', + 'corporatePoBox', + 'corporateMakaniNumber', + 'corporateContactPersonName', + 'corporateContactPersonDesignation', + 'corporateCountryCode', + 'corporateMobileNumber', + 'corporateEmail', + 'corporateWebsite' + ]; - corporateFields.forEach(field => { - corporateValues[field] = formData[field] || ''; - }); + corporateFields.forEach(field => { + corporateValues[field] = formData[field] || ''; + }); - return corporateValues; -}; + return corporateValues; + }; + + const corporateFieldKeys = Object.values(contactToCorporateMap); -// Add this array as well -const corporateFieldKeys = Object.values(contactToCorporateMap); const loadCorporateCityOptions = React.useCallback( async (emirateId) => { if (!emirateId) { @@ -1629,7 +1324,6 @@ const corporateFieldKeys = Object.values(contactToCorporateMap); }, []); const mapApiEstablishmentToProfile = React.useCallback((item) => { - // Get current user from session storage let currentUser = null; try { const profile = sessionStorage.getItem('user_profile'); @@ -1639,6 +1333,7 @@ const corporateFieldKeys = Object.values(contactToCorporateMap); } catch (error) { console.error('Error parsing user profile:', error); } + const base = createEmptyProfile(); const primaryUser = Array.isArray(item?.users) && item.users.length ? item.users[0] : item?.establishment_user ?? null; const emiratiMale = item?.emirati_male ?? ''; @@ -1651,26 +1346,18 @@ const corporateFieldKeys = Object.values(contactToCorporateMap); const corporateEmirateName = item?.corporate_emirate?.name ?? item?.emirate ?? ''; const establishmentCityName = item?.establishment_city?.name ?? base.contactCityTown; const corporateCityName = item?.corporate_city?.name ?? base.corporateCityTown; - const contactEmail = item?.establishment_contact_email ?? base.contactEmail; - // Get creator's name - check if current user is the creator let creatorName = '-'; if (currentUser && item?.created_by && currentUser.id === item.created_by) { - // If current user is the creator, use their name const name = currentUser.name || currentUser.username || `User ${item.created_by}`; - // Ensure proper spacing between first name and last initial creatorName = name.replace(/([a-zA-Z])([A-Z])/g, '$1 $2'); } else if (item?.created_by) { - // If not the current user, try to get creator's name from the item or use ID as fallback const name = item?.created_by_name || item?.created_by_user?.name || item?.creator?.name || `User ${item.created_by}`; - // Ensure proper spacing between first name and last initial creatorName = name.replace(/([a-zA-Z])([A-Z])/g, '$1 $2'); } - - return { ...base, @@ -1699,7 +1386,6 @@ const corporateFieldKeys = Object.values(contactToCorporateMap); updated_at: item?.updated_at ? new Date(item.updated_at).toISOString() : null, lastUpdated: formatApiDate(item?.updated_at), contactName: item?.factory_name || '', - // contactEmail: item?.email ?? '', contactEmail: item?.establishment_contact_email ?? base.contactEmail, contactPostalCode: item?.establishment_postal_code ?? base.contactPostalCode, contactPoBox: item?.establishment_po_box ?? base.contactPoBox, @@ -1733,15 +1419,25 @@ const corporateFieldKeys = Object.values(contactToCorporateMap); userProfileName: primaryUser?.name ?? base.userProfileName, userProfileEmail: primaryUser?.email ?? base.userProfileEmail, establishment_products: Array.isArray(item?.establishment_products) - ? item.establishment_products.map(p => ({ - id: p.id || p.product_id || 0, - product_id: p.product_id || p.id || 0, - hsCode: p.hs_code || p.hsCode || '', - productName: p.product_name || p.productName || p.label || '', - label: p.product_name || p.productName || p.label || '', - value: p.hs_code || p.hsCode || '', - ...p - })) + ? item.establishment_products.map(p => { + const productData = p.product || {}; + const productName = productData.product_name || p.product_name || 'Unnamed Product'; + const hsCode = productData.hs_code || p.hs_code || ''; + + return { + id: p.id || 0, + product_id: p.product_id || 0, + hs_code: hsCode, + hsCode: hsCode, + product_name: productName, + productName: productName, + label: `${productName}${hsCode ? ` (${hsCode})` : ''}`, + value: p.product_id ? String(p.product_id) : '', + establishment_id: p.establishment_id, + ...productData, + product: productData + }; + }) : [], }; }, []); @@ -1752,60 +1448,50 @@ const corporateFieldKeys = Object.values(contactToCorporateMap); let cancelled = false; const controller = new AbortController(); -const loadProfiles = async () => { - setIsLoading(true); - // First, get the total number of records - const countParams = { - search: debouncedSearch || undefined, - emirateId: selectedEmirateFilter || undefined, - status: selectedStatusFilter || undefined, - page: 1, - limit: 1, // Just need the total count - }; + const loadProfiles = async () => { + setIsLoading(true); + const countParams = { + search: debouncedSearch || undefined, + emirateId: selectedEmirateFilter || undefined, + status: selectedStatusFilter || undefined, + page: 1, + limit: 1, + }; - try { - // First, get the total count - const countResponse = await fetchEstablishments({ params: countParams, signal: controller.signal }); - const countPayload = countResponse?.data ?? countResponse; - const totalRecords = countPayload?.pagination?.total_records || 0; - - // Then fetch all records with the total count as limit - const params = { - search: debouncedSearch || undefined, - emirateId: selectedEmirateFilter || undefined, - status: selectedStatusFilter || undefined, - page: 1, - limit: totalRecords || 1000, + try { + const countResponse = await fetchEstablishments({ params: countParams, signal: controller.signal }); + const countPayload = countResponse?.data ?? countResponse; + const totalRecords = countPayload?.pagination?.total_records || 0; + + const params = { + search: debouncedSearch || undefined, + emirateId: selectedEmirateFilter || undefined, + status: selectedStatusFilter || undefined, + page: 1, + limit: totalRecords || 1000, + }; + + const response = await fetchEstablishments({ params, signal: controller.signal }); + const payload = response?.data ?? response; + + const records = Array.isArray(payload?.data) ? payload.data : Array.isArray(payload) ? payload : []; + if (cancelled) return; + + setProfiles(records.map(mapApiEstablishmentToProfile)); + setTotalItems(records.length); + } catch (error) { + if (cancelled) return; + if (error.name === 'CanceledError' || error.name === 'AbortError') return; + console.error('Failed to fetch establishments list.', error); + const message = error?.response?.data?.message || error?.message || 'Unable to load establishments.'; + showToast('error', message); + } finally { + if (!cancelled) { + setIsLoading(false); + } + } }; - // Fetch all records - const response = await fetchEstablishments({ params, signal: controller.signal }); - const payload = response?.data ?? response; - - // Log the complete response structure for debugging - console.group('API Response Details'); - console.groupEnd(); - - const records = Array.isArray(payload?.data) ? payload.data : Array.isArray(payload) ? payload : []; - if (cancelled) return; - - // Set all records to the profiles state - setProfiles(records.map(mapApiEstablishmentToProfile)); - // Set total items to the number of records - setTotalItems(records.length); - } catch (error) { - if (cancelled) return; - if (error.name === 'CanceledError' || error.name === 'AbortError') return; - console.error('Failed to fetch establishments list.', error); - const message = error?.response?.data?.message || error?.message || 'Unable to load establishments.'; - showToast('error', message); - } finally { - if (!cancelled) { - setIsLoading(false); - } - } -}; - if (!exportInProgressRef.current) { loadProfiles(); } @@ -1870,130 +1556,128 @@ const loadProfiles = async () => { ); const handleEdit = async (identifier) => { - if (modalMode && isDirty) { - const shouldContinue = window.confirm('You have unsaved changes. Do you want to discard them and continue?'); - if (!shouldContinue) { - return; - } - } - - // Update URL with the establishment ID - navigate(`?edit=${encodeURIComponent(identifier)}`); - if (!identifier) return; - - try { - // Show loading state - setModalMode('loading'); - - // Find the profile record - const originalIndex = profiles.findIndex((item) => { - if (item.apiId !== null && item.apiId !== undefined && item.apiId === identifier) { - return true; + if (modalMode && isDirty) { + const shouldContinue = window.confirm('You have unsaved changes. Do you want to discard them and continue?'); + if (!shouldContinue) { + return; } - return item.establishmentId === identifier; - }); - - if (originalIndex === -1) { - showToast('error', 'Establishment not found'); - return; } - const profileRecord = profiles[originalIndex]; - setEditingRow(originalIndex); + navigate(`?edit=${encodeURIComponent(identifier)}`); + if (!identifier) return; - // If we have an API ID, fetch the latest data - if (profileRecord?.apiId) { - const response = await fetchEstablishmentDetail(profileRecord.apiId); - const detail = response?.data || response; + try { + setModalMode('loading'); - if (!detail) { - showToast('error', 'No data received from server'); + const originalIndex = profiles.findIndex((item) => { + if (item.apiId !== null && item.apiId !== undefined && item.apiId === identifier) { + return true; + } + return item.establishmentId === identifier; + }); + + if (originalIndex === -1) { + showToast('error', 'Establishment not found'); return; } - // Map the API response to form fields - const mapped = { - ...createEmptyProfile(), - ...mapApiEstablishmentToProfile(detail), - }; + const profileRecord = profiles[originalIndex]; + setEditingRow(originalIndex); - mapped.apiId = mapped.apiId ?? profileRecord.apiId; - mapped.corporateSameAs = Boolean(mapped.corporateSameAs); - - if (mapped.corporateSameAs) { - Object.entries(contactToCorporateMap).forEach(([source, target]) => { - mapped[target] = mapped[source]; - }); - } - - // Set the form with the mapped data - setForm(prev => ({ - ...prev, - ...mapped, - ...computeEmploymentTotals(mapped) - })); - - // Set selected products if available - if (mapped.establishment_products && Array.isArray(mapped.establishment_products)) { - // Map the products to ensure they have all required fields - const formattedProducts = mapped.establishment_products.map(p => { - // Use nested product object if it exists, otherwise fall back to root properties - const productData = p.product || {}; - const productName = productData.product_name || p.product_name || 'Unnamed Product'; - const hsCode = productData.hs_code || p.hs_code || ''; - - return { - id: p.id || 0, - product_id: p.product_id || 0, - hs_code: hsCode, - hsCode: hsCode, - product_name: productName, - productName: productName, - label: `${productName}${hsCode ? ` (${hsCode})` : ''}`, - value: p.product_id ? String(p.product_id) : '', - establishment_id: p.establishment_id, - ...productData, // Spread the nested product data - product: productData // Keep the nested product object for reference - }; - }); + if (profileRecord?.apiId) { + const response = await fetchEstablishmentDetail(profileRecord.apiId); + const detail = response?.data || response; - setSelectedProducts(formattedProducts); + if (!detail) { + showToast('error', 'No data received from server'); + return; + } + + const mapped = { + ...createEmptyProfile(), + ...mapApiEstablishmentToProfile(detail), + }; + + mapped.apiId = mapped.apiId ?? profileRecord.apiId; + mapped.corporateSameAs = Boolean(mapped.corporateSameAs); + + if (mapped.corporateSameAs) { + Object.entries(contactToCorporateMap).forEach(([source, target]) => { + mapped[target] = mapped[source]; + }); + } + + setForm(prev => ({ + ...prev, + ...mapped, + ...computeEmploymentTotals(mapped) + })); + + // Enhanced product handling for edit mode + if (mapped.establishment_products && Array.isArray(mapped.establishment_products)) { + const formattedProducts = mapped.establishment_products.map(p => { + const productData = p.product || {}; + const productName = productData.product_name || p.product_name || 'Unnamed Product'; + const hsCode = productData.hs_code || p.hs_code || ''; + + return { + id: p.id || 0, + product_id: p.product_id || 0, + hs_code: hsCode, + hsCode: hsCode, + product_name: productName, + productName: productName, + label: `${productName}${hsCode ? ` (${hsCode})` : ''}`, + value: p.product_id ? String(p.product_id) : '', + establishment_id: p.establishment_id, + ...productData, + product: productData + }; + }); + + setSelectedProducts(formattedProducts); + } else { + setSelectedProducts([]); + } + initialSnapshotRef.current = { ...mapped, ...computeEmploymentTotals(mapped) }; + corporateBackupRef.current = captureCorporateValues(mapped); } else { - setSelectedProducts([]); - } - initialSnapshotRef.current = { ...mapped, ...computeEmploymentTotals(mapped) }; - corporateBackupRef.current = captureCorporateValues(mapped); - } else { - // Fallback to local data if no API ID - const base = createEmptyProfile(); - const nextForm = { - ...base, - ...profileRecord, - corporateSameAs: Boolean(profileRecord?.corporateSameAs), - }; - - if (nextForm.corporateSameAs) { - Object.entries(contactToCorporateMap).forEach(([source, target]) => { - nextForm[target] = nextForm[source]; - }); + const base = createEmptyProfile(); + const nextForm = { + ...base, + ...profileRecord, + corporateSameAs: Boolean(profileRecord?.corporateSameAs), + }; + + if (nextForm.corporateSameAs) { + Object.entries(contactToCorporateMap).forEach(([source, target]) => { + nextForm[target] = nextForm[source]; + }); + } + + Object.assign(nextForm, computeEmploymentTotals(nextForm)); + setForm(nextForm); + initialSnapshotRef.current = { ...nextForm }; + corporateBackupRef.current = captureCorporateValues(nextForm); + + // Handle products for non-API edits + if (profileRecord.products && Array.isArray(profileRecord.products)) { + setSelectedProducts(profileRecord.products); + } else { + setSelectedProducts([]); + } } - Object.assign(nextForm, computeEmploymentTotals(nextForm)); - setForm(nextForm); - initialSnapshotRef.current = { ...nextForm }; - corporateBackupRef.current = captureCorporateValues(nextForm); + setModalMode('edit'); + setIsDirty(false); + + } catch (error) { + console.error('Error in handleEdit:', error); + const message = error?.response?.data?.message || error?.message || 'Failed to load establishment details'; + showToast('error', message); + setModalMode(null); } - - setModalMode('edit'); - setIsDirty(false); - - } catch (error) { - console.error('Error in handleEdit:', error); - const message = error?.response?.data?.message || error?.message || 'Failed to load establishment details'; - showToast('error', message); - setModalMode(null); - } -}; + }; const handleDelete = (establishmentId) => { if (!establishmentId) return; @@ -2016,7 +1700,6 @@ const loadProfiles = async () => { } } - // Clear any edit ID from URL when adding new navigate('?add=new'); const emptyProfile = createEmptyProfile(); Object.assign(emptyProfile, computeEmploymentTotals(emptyProfile)); @@ -2056,11 +1739,10 @@ const loadProfiles = async () => { } setModalMode(null); setForm(createEmptyProfile()); - setSelectedProducts([]); // Clear selected products when modal is closed + setSelectedProducts([]); setEditingRow(null); setFieldErrors({}); setIsDirty(false); - // Clear URL parameters when closing modal navigate(''); setSaveWarning(false); initialSnapshotRef.current = createEmptyProfile(); @@ -2073,72 +1755,54 @@ const loadProfiles = async () => { }; const computeFieldErrorMessage = (field, value) => { - // Required field validation - if (!value && requiredFields.some(f => f.field === field)) { - return 'This field is required'; - } - - // Email validation - if ((field === 'contactEmail' || field === 'corporateEmail' || field === 'userProfileEmail') && value) { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!emailRegex.test(value)) { - return 'Please enter a valid email address'; + if (!value && requiredFields.some(f => f.field === field)) { + return 'This field is required'; } - } - // Website URL validation - if ((field === 'contactWebsite' || field === 'corporateWebsite') && value) { - // Supports: - // - http://example.com - // - https://example.com - // - www.example.com - // - example.com - // - subdomain.example.com - // - localhost:3000 (for development) - // - IP addresses (e.g., 192.168.1.1) - // - With or without path/query parameters - const urlRegex = /^(https?:\/\/)?(www\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(\.[a-zA-Z]{2,})?(\/\S*)?$/; - - // Also allow localhost with optional port for development - // const localhostRegex = /^https?:\/\/localhost(:\d+)?(\/\S*)?$/; - - // Also allow IP addresses with optional port - const ipRegex = /^https?:\/\/(\d{1,3}\.){3}\d{1,3}(:\d+)?(\/\S*)?$/; - - if (!urlRegex.test(value)&& !ipRegex.test(value)) { - return 'Please enter a valid URL (e.g., https://example.com, example.com, or www.example.com)'; + if ((field === 'contactEmail' || field === 'corporateEmail' || field === 'userProfileEmail') && value) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(value)) { + return 'Please enter a valid email address'; + } } - } - // Numeric field validation - const numericFields = [ - 'employmentEmiratiMale', - 'employmentEmiratiFemale', - 'employmentNonEmiratiMale', - 'employmentNonEmiratiFemale', - 'employmentTotalEmirati', - 'employmentTotalEmployees' + if ((field === 'contactWebsite' || field === 'corporateWebsite') && value) { + const urlRegex = /^(https?:\/\/)?(www\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(\.[a-zA-Z]{2,})?(\/\S*)?$/; + const ipRegex = /^https?:\/\/(\d{1,3}\.){3}\d{1,3}(:\d+)?(\/\S*)?$/; + + if (!urlRegex.test(value) && !ipRegex.test(value)) { + return 'Please enter a valid URL (e.g., https://example.com, example.com, or www.example.com)'; + } + } + + const numericFields = [ + 'employmentEmiratiMale', + 'employmentEmiratiFemale', + 'employmentNonEmiratiMale', + 'employmentNonEmiratiFemale', + 'employmentTotalEmirati', + 'employmentTotalEmployees' + ]; + if (numericFields.includes(field) && value && !/^\d+$/.test(value)) { + return 'Please enter a valid number'; + } + + return ''; + }; + + const requiredFields = [ + { field: 'userProfileName', label: 'Name' }, + { field: 'userProfileEmail', label: 'Email' }, + { field: 'contactName', label: 'Contact Name' }, + { field: 'contactAddress', label: 'Contact Address' }, + { field: 'contactEmirate', label: 'Emirate' }, + { field: 'contactMobileNumber', label: 'Mobile Number' }, + { field: 'corporateName', label: 'Corporate Name' }, + { field: 'corporateAddress', label: 'Corporate Address' }, + { field: 'corporateEmirate', label: 'Corporate Emirate' }, + { field: 'corporateMobileNumber', label: 'Corporate Mobile Number' } ]; - if (numericFields.includes(field) && value && !/^\d+$/.test(value)) { - return 'Please enter a valid number'; - } - return ''; -}; - -// Add this array to define required fields -const requiredFields = [ - { field: 'userProfileName', label: 'Name' }, - { field: 'userProfileEmail', label: 'Email' }, - { field: 'contactName', label: 'Contact Name' }, - { field: 'contactAddress', label: 'Contact Address' }, - { field: 'contactEmirate', label: 'Emirate' }, - { field: 'contactMobileNumber', label: 'Mobile Number' }, - { field: 'corporateName', label: 'Corporate Name' }, - { field: 'corporateAddress', label: 'Corporate Address' }, - { field: 'corporateEmirate', label: 'Corporate Emirate' }, - { field: 'corporateMobileNumber', label: 'Corporate Mobile Number' } -]; const handleDeleteConfirm = async () => { if (deletingRow === null) return; if (isDeletingApi) return; @@ -2207,66 +1871,35 @@ const requiredFields = [ if (next.corporateSameAs && contactToCorporateMap[field]) { next[contactToCorporateMap[field]] = value; } - // if (field === 'contactEmirate') { - // const foundOption = emirateOptions.find((option) => option.value === value || option.label === value); - // if (foundOption) { - // next.contactEmirate = foundOption.label; - // next.contactEmirateId = foundOption.value; - // } else { - // next.contactEmirateId = ''; - // } - // loadContactCityOptions(next.contactEmirateId); - // next.contactCityTown = ''; - // next.contactCityTownId = ''; - // } - // Handle emirate changes - if (field === 'contactEmirateId') { - const selectedEmirate = emirateOptions.find(opt => opt.value === value); - if (selectedEmirate) { - next.contactEmirate = selectedEmirate.label; - next.contactEmirateId = selectedEmirate.value; - loadContactCityOptions(selectedEmirate.value); - } else { - next.contactEmirate = ''; - next.contactEmirateId = ''; + + if (field === 'contactEmirateId') { + const selectedEmirate = emirateOptions.find(opt => opt.value === value); + if (selectedEmirate) { + next.contactEmirate = selectedEmirate.label; + next.contactEmirateId = selectedEmirate.value; + loadContactCityOptions(selectedEmirate.value); + } else { + next.contactEmirate = ''; + next.contactEmirateId = ''; + } + next.contactCityTown = ''; + next.contactCityTownId = ''; } - next.contactCityTown = ''; - next.contactCityTownId = ''; - } - // if (field === 'corporateEmirate') { - // const foundOption = emirateOptions.find((option) => option.value === value || option.label === value); - // if (foundOption) { - // next.corporateEmirate = foundOption.label; - // next.corporateEmirateId = foundOption.value; - // } else { - // next.corporateEmirateId = ''; - // } - // loadCorporateCityOptions(next.corporateEmirateId); - // next.corporateCityTown = ''; - // next.corporateCityTownId = ''; - // } + if (field === 'corporateEmirateId') { - const selectedEmirate = emirateOptions.find(opt => opt.value === value); - if (selectedEmirate) { - next.corporateEmirate = selectedEmirate.label; - next.corporateEmirateId = selectedEmirate.value; - loadCorporateCityOptions(selectedEmirate.value); - } else { - next.corporateEmirate = ''; - next.corporateEmirateId = ''; + const selectedEmirate = emirateOptions.find(opt => opt.value === value); + if (selectedEmirate) { + next.corporateEmirate = selectedEmirate.label; + next.corporateEmirateId = selectedEmirate.value; + loadCorporateCityOptions(selectedEmirate.value); + } else { + next.corporateEmirate = ''; + next.corporateEmirateId = ''; + } + next.corporateCityTown = ''; + next.corporateCityTownId = ''; } - next.corporateCityTown = ''; - next.corporateCityTownId = ''; - } - // if (field === 'contactCityTown') { - // const foundCity = contactCityOptions.find((option) => option.value === value || option.label === value); - // if (foundCity) { - // next.contactCityTown = foundCity.label; - // next.contactCityTownId = foundCity.value; - // } else { - // next.contactCityTownId = ''; - // } - // } + if (field === 'contactCityTownId') { const selectedCity = contactCityOptions.find(opt => opt.value === value); if (selectedCity) { @@ -2277,15 +1910,7 @@ const requiredFields = [ next.contactCityTownId = ''; } } - // if (field === 'corporateCityTown') { - // const foundCity = corporateCityOptions.find((option) => option.value === value || option.label === value); - // if (foundCity) { - // next.corporateCityTown = foundCity.label; - // next.corporateCityTownId = foundCity.value; - // } else { - // next.corporateCityTownId = ''; - // } - // } + if (field === 'corporateCityTownId') { const selectedCity = corporateCityOptions.find(opt => opt.value === value); if (selectedCity) { @@ -2296,22 +1921,27 @@ const requiredFields = [ next.corporateCityTownId = ''; } } + if (field === 'uniqueLicenseNumber') { next.establishmentId = value; } + if (field === 'userProfilePassword') { setPasswordError(validatePasswordComplexity(String(value || ''))); setPasswordReuseError(''); } + if (field === 'userProfileConfirmPassword') { setConfirmError(''); } + const fieldsToValidate = new Set([field]); if (field === 'corporateSameAs') { corporateFieldKeys.forEach((key) => fieldsToValidate.add(key)); } else if (next.corporateSameAs && contactToCorporateMap[field]) { fieldsToValidate.add(contactToCorporateMap[field]); } + setFieldErrors((prevErrors) => { const updatedErrors = { ...prevErrors }; fieldsToValidate.forEach((targetField) => { @@ -2319,6 +1949,7 @@ const requiredFields = [ }); return updatedErrors; }); + setIsDirty(computeIsDirty(next)); return next; }); @@ -2336,19 +1967,16 @@ const requiredFields = [ return new Date().toISOString(); }; - const handleSaveForm = async (e) => { - // Prevent default form submission behavior if (e) { e.preventDefault(); } - // Prevent multiple submissions if (isUpdating || isSaving) return; - // Show loading state setIsSaving(true); if (!validateStepFields(activeStep)) { + setIsSaving(false); return; } @@ -2356,15 +1984,18 @@ const requiredFields = [ const complexityMessage = validatePasswordComplexity(form.userProfilePassword); if (complexityMessage) { setPasswordError(complexityMessage); + setIsSaving(false); return; } const history = Array.isArray(form.passwordHistory) ? form.passwordHistory.slice(0, MAX_PASSWORD_HISTORY) : []; if (history.some((item) => item?.value === form.userProfilePassword)) { setPasswordReuseError('Password must not match any of the last five passwords.'); + setIsSaving(false); return; } if (!form.userProfileConfirmPassword || form.userProfilePassword !== form.userProfileConfirmPassword) { setConfirmError('Passwords do not match'); + setIsSaving(false); return; } } @@ -2384,6 +2015,7 @@ const requiredFields = [ } else if (websiteErrorMap.corporateWebsite) { setActiveStep(3); } + setIsSaving(false); return; } @@ -2411,7 +2043,9 @@ const requiredFields = [ let createdRecordId = null; let apiResultData = null; + if (modalMode !== 'edit') { + // CREATE NEW ESTABLISHMENT const apiPayload = { establishment_code: form.establishmentId || '', factory_name: form.contactName || '', @@ -2460,8 +2094,9 @@ const requiredFields = [ }, establishment_products: Array.isArray(selectedProducts) && selectedProducts.length > 0 ? selectedProducts.map(p => ({ product_id: p.id || p.product_id || 0 })) - : [{ product_id: 0 }], + : [], }; + try { const apiResponse = await createEstablishment(apiPayload); const successMessage = apiResponse?.message || 'Establishment added successfully.'; @@ -2486,91 +2121,95 @@ const requiredFields = [ } } } else { + // UPDATE EXISTING ESTABLISHMENT let targetId = form.apiId ?? profiles[editingRow]?.apiId ?? null; if (!targetId) { showToast('error', 'Unable to update: Missing establishment ID'); setIsSaving(false); return; } + setIsUpdating(true); if (targetId === null || targetId === undefined) { setIsUpdating(false); showToast('error', 'Unable to update establishment. Missing identifier.'); return; } + const establishmentUserPayload = { name: form.userProfileName || '', email: form.userProfileEmail || '', }; + if (form.userProfilePassword) { establishmentUserPayload.password = form.userProfilePassword; } + if (!establishmentUserPayload.name && !establishmentUserPayload.email && !establishmentUserPayload.password) { delete establishmentUserPayload.name; delete establishmentUserPayload.email; } + + // Enhanced update payload with better product handling const updatePayload = { - establishment_code: form.establishmentId || '', - factory_name: form.contactName || '', - permanent_factory_code: form.permanentFactoryCode || '', - industry_code: form.industryCodeBusiness || '', - industry_code_production: form.industryCodeProduction || form.industryCodeCurrent || '', - license_number: form.uniqueLicenseNumber || '', - isic_code: form.industryCodeProduction || '', - description: form.industryDescription || '', - establishment_address: form.contactAddress || '', - establishment_city_town_id: Number(form.contactCityTownId) || 0, - establishment_emirate_id: Number(form.contactEmirateId) || 0, - establishment_postal_code: form.contactPostalCode || '', - establishment_po_box: form.contactPoBox || '', - establishment_makani_number: form.contactMakaniNumber || '', - establishment_contact_person_name: form.contactPersonName || '', - establishment_contact_person_designation: form.contactPersonDesignation || '', - establishment_mobile_number: form.contactMobileNumber || '', - establishment_contact_email: form.contactEmail || '', - establishment_website: form.contactWebsite || '', - corporate_same_as_establishment: Boolean(form.corporateSameAs), - corporate_name: form.corporateName || '', - corporate_address: form.corporateAddress || '', - corporate_city_town_id: Number(form.corporateCityTownId) || 0, - corporate_emirate_id: Number(form.corporateEmirateId) || 0, - corporate_postal_code: form.corporatePostalCode || '', - corporate_po_box: form.corporatePoBox || '', - corporate_makani_number: form.corporateMakaniNumber || '', - corporate_contact_person_name: form.corporateContactPersonName || '', - corporate_contact_person_designation: form.corporateContactPersonDesignation || '', - corporate_mobile_number: form.corporateMobileNumber || '', - corporate_email: form.corporateEmail || '', - corporate_website: form.corporateWebsite || '', - emirati_male: Number(form.employmentEmiratiMale) || 0, - emirati_female: Number(form.employmentEmiratiFemale) || 0, - non_emirati_male: Number(form.employmentNonEmiratiMale) || 0, - non_emirati_female: Number(form.employmentNonEmiratiFemale) || 0, - total_emirati: (Number(form.employmentEmiratiMale) || 0) + (Number(form.employmentEmiratiFemale) || 0), - total_employees: Number(form.employmentTotalEmployees) || 0, - updated_by: 1, - updated_at: getCurrentISODate(), - establishment_products: Array.isArray(selectedProducts) && selectedProducts.length > 0 - ? selectedProducts.map(p => ({ - id: p.id, // Include the establishment_product ID for updates - product_id: p.product_id || p.product?.id || p.id, // The actual product ID - _destroy: false // Flag to indicate not to delete this association - })) - : [{ product_id: 0 }] + establishment_code: form.establishmentId || '', + factory_name: form.contactName || '', + permanent_factory_code: form.permanentFactoryCode || '', + industry_code: form.industryCodeBusiness || '', + industry_code_production: form.industryCodeProduction || form.industryCodeCurrent || '', + license_number: form.uniqueLicenseNumber || '', + isic_code: form.industryCodeProduction || '', + description: form.industryDescription || '', + establishment_address: form.contactAddress || '', + establishment_city_town_id: Number(form.contactCityTownId) || 0, + establishment_emirate_id: Number(form.contactEmirateId) || 0, + establishment_postal_code: form.contactPostalCode || '', + establishment_po_box: form.contactPoBox || '', + establishment_makani_number: form.contactMakaniNumber || '', + establishment_contact_person_name: form.contactPersonName || '', + establishment_contact_person_designation: form.contactPersonDesignation || '', + establishment_mobile_number: form.contactMobileNumber || '', + establishment_contact_email: form.contactEmail || '', + establishment_website: form.contactWebsite || '', + corporate_same_as_establishment: Boolean(form.corporateSameAs), + corporate_name: form.corporateName || '', + corporate_address: form.corporateAddress || '', + corporate_city_town_id: Number(form.corporateCityTownId) || 0, + corporate_emirate_id: Number(form.corporateEmirateId) || 0, + corporate_postal_code: form.corporatePostalCode || '', + corporate_po_box: form.corporatePoBox || '', + corporate_makani_number: form.corporateMakaniNumber || '', + corporate_contact_person_name: form.corporateContactPersonName || '', + corporate_contact_person_designation: form.corporateContactPersonDesignation || '', + corporate_mobile_number: form.corporateMobileNumber || '', + corporate_email: form.corporateEmail || '', + corporate_website: form.corporateWebsite || '', + emirati_male: Number(form.employmentEmiratiMale) || 0, + emirati_female: Number(form.employmentEmiratiFemale) || 0, + non_emirati_male: Number(form.employmentNonEmiratiMale) || 0, + non_emirati_female: Number(form.employmentNonEmiratiFemale) || 0, + total_emirati: (Number(form.employmentEmiratiMale) || 0) + (Number(form.employmentEmiratiFemale) || 0), + total_employees: Number(form.employmentTotalEmployees) || 0, + updated_by: 1, + updated_at: getCurrentISODate(), + // Enhanced product handling for updates - only send product IDs that exist + establishment_products: Array.isArray(selectedProducts) && selectedProducts.length > 0 + ? selectedProducts.map(p => ({ + product_id: p.product_id || p.id || 0 + })) + : [] }; if (establishmentUserPayload.name || establishmentUserPayload.email || establishmentUserPayload.password) { updatePayload.establishment_user = establishmentUserPayload; } + try { - // Show loading state setIsUpdating(true); - const apiResponse = await updateEstablishment(targetId, updatePayload); const successMessage = apiResponse?.message || 'Establishment updated successfully.'; showToast('success', successMessage); - // Update the local state with the updated data const updatedProfile = { ...form, ...updatePayload, @@ -2578,28 +2217,31 @@ const requiredFields = [ apiId: targetId }; - // Update the profiles list setProfiles(prev => prev.map(profile => profile.apiId === targetId ? updatedProfile : profile ) ); - // Close the modal and reset form handleCloseModal(); - // Refresh the page after a short delay to show the success message setTimeout(() => { window.location.reload(); }, 1000); - // Return the updated data return apiResponse?.data; } catch (error) { console.error('Error updating establishment:', error); - const errorMessage = error.response?.data?.message || error.message || 'Failed to update establishment'; + + // Enhanced error handling for HS code issues + let errorMessage = error.response?.data?.message || error.message || 'Failed to update establishment'; + + if (errorMessage.includes('hs_code') || errorMessage.includes('product') || errorMessage.includes('HS code')) { + errorMessage = 'One or more selected products have invalid HS codes. Please check the products and try again.'; + } + showToast('error', errorMessage); - throw error; // Re-throw to be caught by the outer try-catch + throw error; } finally { setIsUpdating(false); } @@ -2641,24 +2283,20 @@ const requiredFields = [ await new Promise((resolve) => requestAnimationFrame(resolve)); - // For edits, update the local state immediately if (modalMode === 'edit' && editingRow !== null) { const updatedProfiles = [...profiles]; updatedProfiles[editingRow] = nextForm; setProfiles(updatedProfiles); } - // For new records, wait for the API response before updating the UI if (modalMode !== 'edit') { if (apiResultData) { - // Use the API response data to update the state const newProfile = mapApiEstablishmentToProfile(apiResultData); setProfiles(prev => [newProfile, ...prev]); } } else { - // For edits, refresh the data to ensure consistency try { - const response = await getEstablishments(); + const response = await fetchEstablishments(); if (response?.data) { const mappedProfiles = response.data.map(mapApiEstablishmentToProfile); setProfiles(mappedProfiles); @@ -2668,7 +2306,6 @@ const requiredFields = [ } } - // Reset form and close modal const emptyProfile = createEmptyProfile(); Object.assign(emptyProfile, computeEmploymentTotals(emptyProfile)); setForm(emptyProfile); @@ -2710,8 +2347,7 @@ const requiredFields = [ return profiles[deletingRow]; }, [deletingRow, profiles]); - - const handleImport = async () => { + const handleImport = async () => { if (!file) { const errorMsg = "Please select a CSV file to import."; setImportError(errorMsg); @@ -2736,11 +2372,9 @@ const requiredFields = [ setImportLoading(true); setImportError(""); - // Create FormData with proper file field const formData = new FormData(); formData.append("file", file); - // Debug: Check FormData contents console.log('FormData entries before upload:'); for (let pair of formData.entries()) { console.log(pair[0] + ': ', pair[1]); @@ -2762,20 +2396,34 @@ const requiredFields = [ } catch (secondError) { console.log('Alternative upload also failed:', secondError); - // Extract detailed error message from backend response let detailedErrorMessage = "Upload failed. Please try again."; + let validationErrorCount = 1; // Default count + + // Extract validation error count from response if available + if (secondError.response?.data) { + const responseData = secondError.response.data; + + // Try to extract validation error count from different response structures + if (responseData.validation_errors) { + validationErrorCount = responseData.validation_errors.length || 1; + } else if (responseData.errors) { + validationErrorCount = responseData.errors.length || 1; + } else if (responseData.details && Array.isArray(responseData.details)) { + validationErrorCount = responseData.details.length || 1; + } else if (typeof responseData === 'string') { + // Try to extract count from error message + const countMatch = responseData.match(/(\d+)\s*validation errors?/i); + if (countMatch) { + validationErrorCount = parseInt(countMatch[1]) || 1; + } + } + } - // Check for backend error response structure - prioritize backend message if (secondError.response?.data?.message) { detailedErrorMessage = secondError.response.data.message; - } - // If no specific backend message, check for common error patterns - else if (secondError.message) { - // Remove "Request failed with status code 500" type messages + } else if (secondError.message) { if (secondError.message.includes("Request failed with status code")) { - // Check if there's any additional data with the error if (secondError.response?.data) { - // Try to extract meaningful message from response data const responseData = secondError.response.data; if (typeof responseData === 'string' && responseData.includes("Duplicate Establishment IDs")) { detailedErrorMessage = responseData; @@ -2783,22 +2431,18 @@ const requiredFields = [ detailedErrorMessage = responseData.error; } else if (responseData.details) { detailedErrorMessage = responseData.details; - } else { - // Generic server error without specific details - detailedErrorMessage = "Data already exists in the system. Please remove or update the duplicate entries and try again."; -} - + } else { + detailedErrorMessage = "Data already exists in the system. Please remove or update the duplicate entries and try again."; + } } else { - // Generic server error without specific details detailedErrorMessage = "Server error occurred during upload. Please try again."; } } else { - // For other non-status code errors, use the original message detailedErrorMessage = secondError.message; } } - // Check for specific error patterns and provide user-friendly messages + // Enhanced validation error detection with dynamic count if (detailedErrorMessage.includes("No file uploaded") || detailedErrorMessage.includes("No file found")) { detailedErrorMessage = "Please select a valid CSV file to upload."; @@ -2812,16 +2456,46 @@ const requiredFields = [ } else if (detailedErrorMessage.includes("Duplicate Establishment IDs") || detailedErrorMessage.includes("duplicate") || detailedErrorMessage.includes("already exist")) { - // Keep the original backend message for duplicate IDs detailedErrorMessage = "Duplicate Establishment IDs found (in file or already exist in system). Resolve and re-upload."; + } else if (detailedErrorMessage.includes("validation error") || + detailedErrorMessage.includes("validation errors") || + detailedErrorMessage.includes("No establishments imported") || + detailedErrorMessage.includes("HS Codes do not exist") || + detailedErrorMessage.includes("HS Code")) { + // Format validation error with dynamic count and HS code message +detailedErrorMessage = `Import failed. ${validationErrorCount} HS Codes in your file do not exist in the Product Master. Please use the correct HS Code and try again.`; } - // Throw the detailed error with backend message throw new Error(detailedErrorMessage); } } - // SUCCESS: Show green success message + // Check if the response indicates validation errors + if (response && (response.message?.includes("validation error") || + response.message?.includes("validation errors") || + response.message?.includes("No establishments imported") || + response.message?.includes("HS Codes do not exist") || + response.message?.includes("HS Code"))) { + + let validationErrorCount = 1; + // Extract count from response if available + if (response.validation_errors) { + validationErrorCount = response.validation_errors.length || 1; + } else if (response.errors) { + validationErrorCount = response.errors.length || 1; + } + +const validationErrorMessage = `Import failed. ${validationErrorCount} HS Codes in your file do not exist in the Product Master. Please use the correct HS Code and try again.`; + + // Show validation error in red + setImportError(validationErrorMessage); + setToastData({ + message: validationErrorMessage, + type: "error", + }); + throw new Error(validationErrorMessage); + } + setToastData({ message: successMessage, type: "success", @@ -2829,7 +2503,6 @@ const requiredFields = [ closeImportModal(); - // Refresh the list try { setIsLoading(true); const res = await fetchEstablishments(); @@ -2855,12 +2528,9 @@ const requiredFields = [ let errorMessage = error.message || "Upload failed. Please try again."; - // Remove any "Request failed with status code" prefixes if (errorMessage.includes("Request failed with status code")) { - // Extract the actual error message after the status code const parts = errorMessage.split("Request failed with status code"); if (parts.length > 1) { - // Try to find the actual error content const actualError = parts[1].replace(/^\d+\s*/, '').trim(); if (actualError && actualError.length > 0) { errorMessage = actualError; @@ -2870,34 +2540,47 @@ const requiredFields = [ } } - // Additional error handling + // Enhanced error message handling for validation errors with dynamic count if (errorMessage.includes("expired") || errorMessage.includes("Authentication")) { errorMessage = "Authentication failed. Please check your login credentials."; } - // Ensure duplicate ID error shows the proper message if (errorMessage.includes("Duplicate Establishment IDs") || errorMessage.includes("duplicate") || errorMessage.includes("already exist")) { errorMessage = "Duplicate Establishment IDs found (in file or already exist in system). Resolve and re-upload."; } + if (errorMessage.includes("validation error") || + errorMessage.includes("validation errors") || + errorMessage.includes("No establishments imported") || + errorMessage.includes("HS Codes do not exist") || + errorMessage.includes("HS Code")) { + + let validationErrorCount = 1; + // Try to extract count from existing error message + const countMatch = errorMessage.match(/(\d+)\s*validation errors?/i); + if (countMatch) { + validationErrorCount = parseInt(countMatch[1]) || 1; + } + +errorMessage = `Import failed. ${validationErrorCount} HS Code${validationErrorCount > 1 ? "s" : ""} in your file ${validationErrorCount > 1 ? "do" : "does"} not exist in the Product Master. Please use the correct HS Code${validationErrorCount > 1 ? "s" : ""} and try again.`; + } + setImportError(errorMessage); - // ERROR: Show red error message setToastData({ message: errorMessage, type: "error", }); - // Re-throw the error so calling code can handle it throw error; } finally { setImportLoading(false); } }; - // Fixed file validation + const handleFileChange = (e) => { if (e.target.files && e.target.files[0]) { const selectedFile = e.target.files[0]; @@ -2916,7 +2599,6 @@ const requiredFields = [ } }; - // Fixed drag and drop handler const handleDrop = (e) => { e.preventDefault(); e.stopPropagation(); @@ -2938,7 +2620,6 @@ const requiredFields = [ } }; - return (
    {toast && ( @@ -3101,7 +2782,6 @@ const requiredFields = [ currentPage, onPageChange: (page) => { setCurrentPage(page); - // Scroll to top when changing pages window.scrollTo({ top: 0, behavior: 'smooth' }); }, pageSize, @@ -3109,7 +2789,7 @@ const requiredFields = [ pageSizeOptions: [10, 20, 50, 100], onPageSizeChange: (size) => { setPageSize(size); - setCurrentPage(1); // Reset to first page when changing page size + setCurrentPage(1); } }} /> @@ -3331,9 +3011,9 @@ const requiredFields = [
    - )} - {/* Fixed Import CSV Modal */} + + {/* Fixed Import CSV Modal */} {importModalOpen && (
    @@ -3460,6 +3140,7 @@ const requiredFields = [
    )} + {/* Custom Toast Notification */} {toastData && (
    @@ -3476,4 +3157,4 @@ const requiredFields = [ ); }; -export default CompanyProfile; +export default CompanyProfile; \ No newline at end of file diff --git a/ipi-survey-platform/src/pages/Admin/configuration/UnitMaster.jsx b/ipi-survey-platform/src/pages/Admin/configuration/UnitMaster.jsx index f8b890f..3adf1e5 100644 --- a/ipi-survey-platform/src/pages/Admin/configuration/UnitMaster.jsx +++ b/ipi-survey-platform/src/pages/Admin/configuration/UnitMaster.jsx @@ -551,7 +551,7 @@ const UnitMaster = () => {

    - Unit Master ({units.length} {units.length === 1 ? 'unit' : 'units'}) + Unit Master ({units.length})