diff --git a/ipi-survey-platform/public/assets/images/UAE_Dirham_Symbol.svg b/ipi-survey-platform/public/assets/images/UAE_Dirham_Symbol.svg new file mode 100644 index 0000000..a2c0d3b --- /dev/null +++ b/ipi-survey-platform/public/assets/images/UAE_Dirham_Symbol.svg @@ -0,0 +1,7 @@ + + Layer copy + + + \ No newline at end of file diff --git a/ipi-survey-platform/src/assets/DirhamSymbol.jsx b/ipi-survey-platform/src/assets/DirhamSymbol.jsx new file mode 100644 index 0000000..394b521 --- /dev/null +++ b/ipi-survey-platform/src/assets/DirhamSymbol.jsx @@ -0,0 +1,18 @@ +import React from 'react'; + +const DirhamSymbol = (props) => ( + + + + +); + +export default DirhamSymbol; diff --git a/ipi-survey-platform/src/components/overview/DetailedOverview.jsx b/ipi-survey-platform/src/components/overview/DetailedOverview.jsx index f0fc60c..8c2cdcd 100644 --- a/ipi-survey-platform/src/components/overview/DetailedOverview.jsx +++ b/ipi-survey-platform/src/components/overview/DetailedOverview.jsx @@ -21,6 +21,8 @@ const formatDate = (dateString) => { const searchIconSrc = '/assets/images/material-symbols_search-rounded.svg'; const caretDownSrc = '/assets/images/CaretDown.svg'; const downloadIconSrc = '/assets/images/DownloadSimple.svg'; +const uae_currency = '/assets/images/UAE_Dirham_Symbol.svg'; + export const DetailedOverview = ({ submission, onBack }) => { const navigate = useNavigate(); @@ -269,8 +271,8 @@ export const DetailedOverview = ({ submission, onBack }) => { , unit.uom || 'N/A', - product.current_quantity || '0', - `AED ${product.current_cost || '0'}`, + product.current_quantity || '0', + AED {product.current_cost || '0'}, // {

Total Submitted Products:

{totalProducts}

-
+ {/*

Average Cost (AED):

{averageCost}

-
+
*/}
diff --git a/ipi-survey-platform/src/components/survey/ProductData/ProductData.jsx b/ipi-survey-platform/src/components/survey/ProductData/ProductData.jsx index 355d60b..0eb5298 100644 --- a/ipi-survey-platform/src/components/survey/ProductData/ProductData.jsx +++ b/ipi-survey-platform/src/components/survey/ProductData/ProductData.jsx @@ -1,6 +1,5 @@ import React, { useState, useEffect } from 'react'; // import { useLocation } from 'react-router-dom'; - import { getQuarterPeriods, getPreviousForecastData, getEstablishmentProducts } from '@/services/submissions/submissionService'; import { fetchVariationReasons, @@ -17,6 +16,7 @@ const calendarIcon = '/assets/images/Duedate.svg'; const nextIcon = '/assets/images/mdi_page-next-outline.svg'; const trashActiveSrc = '/assets/images/Trash - active.svg'; const searchIcon = '/assets/images/material-symbols_search-rounded.svg'; +const uae_currency = '/assets/images/UAE_Dirham_Symbol.svg'; const Input = ({ placeholder = '', type = 'text', value = '', onChange = () => {}, required = false, error = false }) => { const [isFocused, setIsFocused] = React.useState(false); @@ -723,6 +723,34 @@ const location = useLocation(); // Create a copy of the current products array to work with let updatedProducts = [...products]; + // Format value for cost fields to treat last two digits as decimals + if (field.endsWith('Cost') && value !== '') { + // Remove all non-digit characters + const digitsOnly = value.replace(/\D/g, ''); + + if (digitsOnly.length > 0) { + // If there's only one digit, treat it as 0.0X + if (digitsOnly.length === 1) { + value = `0.0${digitsOnly}`; + } + // If there are two digits, treat them as 0.XX + else if (digitsOnly.length === 2) { + value = `0.${digitsOnly}`; + } + // For three or more digits, insert decimal before last two digits + else { + const wholePart = digitsOnly.slice(0, -2) || '0'; + const decimalPart = digitsOnly.slice(-2); + value = `${wholePart}.${decimalPart}`; + } + } else { + value = '0.00'; + } + + // Remove any leading zeros before the decimal point if there are other digits + value = value.replace(/^0+(\d)/, '$1'); + } + // If this is a product selection change, validate for duplicates if (field === 'product' && value) { // Check if this product is already selected in another row @@ -1389,7 +1417,7 @@ const handleProductSelect = async (productId, establishmentId, id) => {
{p.octCost || '0'} @@ -1397,7 +1425,7 @@ const handleProductSelect = async (productId, establishmentId, id) => {
{p.novCost || '0'} @@ -1405,7 +1433,7 @@ const handleProductSelect = async (productId, establishmentId, id) => {
{p.decCost || '0'} @@ -1474,7 +1502,7 @@ const handleProductSelect = async (productId, establishmentId, id) => {
{
{
{
{
{
{ + React.useEffect(() => { + const timer = setTimeout(() => { + onClose(); + }, 3000); + + return () => clearTimeout(timer); + }, [onClose]); + + const getToastStyles = () => { + switch (type) { + case 'success': + return 'bg-green-50 border-green-200 text-green-800'; + case 'error': + return 'bg-red-50 border-red-200 text-red-800'; + case 'warning': + return 'bg-yellow-50 border-yellow-200 text-yellow-800'; + default: + return 'bg-blue-50 border-blue-200 text-blue-800'; + } + }; + + const getIcon = () => { + switch (type) { + case 'success': + return ''; + case 'error': + return ''; + case 'warning': + return ''; + default: + return ''; + } + }; + + return ( +
+ {/* {getIcon()} */} + {message} + +
+ ); +}; const PASSWORD_POLICY = { @@ -210,7 +263,27 @@ const CompanyProfile = () => { const [isUpdating, setIsUpdating] = React.useState(false); const [formSubmitted, setFormSubmitted] = React.useState(false); + // Import CSV state + const [importModalOpen, setImportModalOpen] = React.useState(false); + const [file, setFile] = React.useState(null); + const [dragActive, setDragActive] = React.useState(false); + const [importLoading, setImportLoading] = React.useState(false); + const [importError, setImportError] = React.useState(''); + const fileInputRef = React.useRef(null); + const [toastData, setToastData] = React.useState(null); + // Load current user from session storage + React.useEffect(() => { + try { + const profile = sessionStorage.getItem('user_profile'); + if (profile) { + const userData = JSON.parse(profile); + setCurrentUser(userData); + } + } catch (error) { + console.error('Error parsing user profile:', error); + } + }, []); const handleCloseModal = () => { setModalMode(null); setEditingRow(null); @@ -340,6 +413,53 @@ const CompanyProfile = () => { [form, requiredFieldsByStep] ); + // Fixed Import CSV Functions + const openImportModal = () => { + setImportModalOpen(true); + setFile(null); + setImportError(''); + setDragActive(false); + }; + + const closeImportModal = () => { + setImportModalOpen(false); + setFile(null); + setImportError(''); + setDragActive(false); + }; + + const handleDrag = (e) => { + e.preventDefault(); + e.stopPropagation(); + if (e.type === "dragenter" || e.type === "dragover") { + setDragActive(true); + } else if (e.type === "dragleave") { + setDragActive(false); + } + }; + + const downloadSampleCSV = () => { + const sampleData = [ + ['Establishment Id', 'Factory Name', 'Email', 'Emirate', 'Total Employment', 'HS Code 1'], + ['EST001', 'ABC Manufacturing', 'contact@abcmanufacturing.com', 'Dubai', '150', '1234567890'], + ['EST002', 'XYZ Industries', 'info@xyzindustries.com', 'Sharjah', '200', '9876543210'], + ['EST003', 'Global Textiles Ltd', 'sales@globaltextiles.com', 'Abu Dhabi', '85', '4567890123'] + ]; + const csvContent = sampleData.map(row => + row.map(field => `"${field}"`).join(',') + ).join('\n'); + + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', 'company-profile-template.csv'); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + }; + // Add this useEffect to load the current user from session storage const currentUserId = React.useMemo(() => { try { @@ -1287,9 +1407,7 @@ const loadProducts = async () => { item.emirate, item.isicCode, item.establishmentId, - item.products && item.products.length > 0 - ? item.products.map(p => p.hsCode ? `${p.hsCode} - ${p.productName || p.label || p.product_name || 'N/A'}` : (p.productName || p.product_name || 'N/A')).filter(Boolean).join(', ') - : '-', + item.productCount ?? '0', item.totalEmployees, item.createdBy, item.createdOn, @@ -1570,7 +1688,8 @@ const corporateFieldKeys = Object.values(contactToCorporateMap); contactCityTownId: item?.establishment_city_town_id ? String(item.establishment_city_town_id) : '', contactEmirate: establishmentEmirateName, contactEmirateId: item?.establishment_emirate_id ? String(item.establishment_emirate_id) : '', - products: base.products, + products: Array.isArray(item?.products) ? item.products : [], + productCount: item?.product_count ?? 0, totalEmployees: totalEmployees !== '' && totalEmployees !== null ? String(totalEmployees) : '', status: item?.is_active === false ? 'Inactive' : 'Active', isActive: item?.is_active !== false, @@ -2591,6 +2710,235 @@ const requiredFields = [ return profiles[deletingRow]; }, [deletingRow, profiles]); + + const handleImport = async () => { + if (!file) { + const errorMsg = "Please select a CSV file to import."; + setImportError(errorMsg); + setToastData({ + message: errorMsg, + type: "error", + }); + throw new Error(errorMsg); + } + + if (file.size > 10 * 1024 * 1024) { + const errorMsg = "File size too large. Please select a file smaller than 10MB."; + setImportError(errorMsg); + setToastData({ + message: errorMsg, + type: "error", + }); + throw new Error(errorMsg); + } + + try { + 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]); + } + + let response; + let successMessage = "Company profiles imported successfully!"; + + try { + console.log('Attempting first upload...'); + response = await uploadCompanyProfileCSV(formData); + successMessage = response.message || successMessage; + } catch (firstError) { + console.log('First upload attempt failed, trying alternative:', firstError); + try { + console.log('Attempting alternative upload...'); + response = await uploadCompanyProfileCSVAlternative(formData); + successMessage = response.message || successMessage; + } catch (secondError) { + console.log('Alternative upload also failed:', secondError); + + // Extract detailed error message from backend response + let detailedErrorMessage = "Upload failed. Please try again."; + + // 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 + 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; + } else if (responseData.error) { + 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 { + // 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 + if (detailedErrorMessage.includes("No file uploaded") || + detailedErrorMessage.includes("No file found")) { + detailedErrorMessage = "Please select a valid CSV file to upload."; + } else if (detailedErrorMessage.includes("token") || + detailedErrorMessage.includes("auth") || + detailedErrorMessage.includes("Authentication")) { + detailedErrorMessage = "Authentication failed. Please check your login credentials."; + } else if (detailedErrorMessage.includes("400") || + detailedErrorMessage.includes("Bad Request")) { + detailedErrorMessage = "Invalid file format or missing required data. Please check your CSV file."; + } 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."; + } + + // Throw the detailed error with backend message + throw new Error(detailedErrorMessage); + } + } + + // SUCCESS: Show green success message + setToastData({ + message: successMessage, + type: "success", + }); + + closeImportModal(); + + // Refresh the list + try { + setIsLoading(true); + const res = await fetchEstablishments(); + const payload = res?.data ?? res; + const records = Array.isArray(payload?.data) + ? payload.data + : Array.isArray(payload) + ? payload + : []; + + setProfiles(records.map(mapApiEstablishmentToProfile)); + setTotalItems(records.length); + } catch (refreshErr) { + console.log("Error refreshing data:", refreshErr); + } finally { + setIsLoading(false); + } + + return response; + + } catch (error) { + console.error("Error importing CSV:", error); + + 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; + } else { + errorMessage = "Server error occurred. Please try again."; + } + } + } + + // Additional error handling + 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."; + } + + 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]; + + const isValidType = selectedFile.type === 'text/csv' || + selectedFile.name.toLowerCase().endsWith('.csv') || + selectedFile.type === 'application/vnd.ms-excel'; + + if (isValidType) { + setFile(selectedFile); + setImportError(''); + } else { + setImportError('Please upload a CSV file only. Supported formats: .csv'); + setFile(null); + } + } + }; + + // Fixed drag and drop handler + const handleDrop = (e) => { + e.preventDefault(); + e.stopPropagation(); + setDragActive(false); + + if (e.dataTransfer.files && e.dataTransfer.files[0]) { + const droppedFile = e.dataTransfer.files[0]; + + const isValidType = droppedFile.type === 'text/csv' || + droppedFile.name.toLowerCase().endsWith('.csv') || + droppedFile.type === 'application/vnd.ms-excel'; + + if (isValidType) { + setFile(droppedFile); + setImportError(''); + } else { + setImportError('Please upload a CSV file only. Supported formats: .csv'); + } + } + }; + + return (
{toast && ( @@ -2682,6 +3030,14 @@ const requiredFields = [ width="180px" variant="toolbar" /> +
+ + )} + {/* Fixed Import CSV Modal */} + {importModalOpen && ( +
+
+
+ {/* Header */} +
+

Import Company Profiles

+ +
+ + {/* Content */} +
+ {/* File Upload Area */} +
fileInputRef.current?.click()} + > +
+
+ Upload +
+
+

+ {file ? file.name : 'Drop your CSV file here or browse'} +

+

+ Supports .csv files only (Max 10MB) +

+ {file && ( +

+ ✓ File selected and ready to import +

+ )} +
+ +
+ +
+ + {/* Error Message */} + {importError && ( +
+

+ + + + {importError} +

+
+ )} + + {/* Sample CSV Link */} +
+ +
+
+ + {/* Footer */} +
+ + +
+
+
+ )} + {/* Custom Toast Notification */} + {toastData && ( +
+
+ setToastData(null)} + /> +
+
)}
); diff --git a/ipi-survey-platform/src/pages/Overview/Overview.jsx b/ipi-survey-platform/src/pages/Overview/Overview.jsx index 1a9d41d..4bb4097 100644 --- a/ipi-survey-platform/src/pages/Overview/Overview.jsx +++ b/ipi-survey-platform/src/pages/Overview/Overview.jsx @@ -96,6 +96,8 @@ const Overview = () => { return filtered; }, [searchTerm, selectedStatus, submissions]); + const uae_currency = '/assets/images/UAE_Dirham_Symbol.svg'; + const rows = useMemo(() => { return filteredRows.map((record) => [ record.year, @@ -103,7 +105,12 @@ const Overview = () => { `${record.quarter} ${record.year}`, 10, // Always show 10 for Total Products record.product_count || 0, // Show actual submitted products count - record.total_cost !== undefined ? record.total_cost : '-', // Show total cost from API or '-' if not available + record.total_cost !== undefined ? ( + + AED + {record.total_cost} + + ) : '-', record.status, formatDate(record.created_at), 'View Details', diff --git a/ipi-survey-platform/src/services/establishments/establishmentService.js b/ipi-survey-platform/src/services/establishments/establishmentService.js index e524f08..fe8f641 100644 --- a/ipi-survey-platform/src/services/establishments/establishmentService.js +++ b/ipi-survey-platform/src/services/establishments/establishmentService.js @@ -74,6 +74,60 @@ export const fetchEstablishmentDashboard = async (establishmentId, config = {}) return response.data; }; +// Dynamic CSV upload service using your existing API service +export const uploadCompanyProfileCSV = async (formData, config = {}) => { + const uploadEndpoint = `${establishmentsEndpoint}/uploadCSV`; + + try { + const response = await postRequest(uploadEndpoint, formData, { + ...config, + headers: { + ...config.headers, + // Don't set Content-Type for FormData - let the browser set it + 'Content-Type': undefined, + }, + }); + return response.data; + } catch (error) { + console.error('Error uploading CSV:', error); + throw error; + } +}; + +// Alternative CSV upload method with different parameter name +export const uploadCompanyProfileCSVAlternative = async (formData, config = {}) => { + const uploadEndpoint = `${establishmentsEndpoint}/uploadCSV`; + + try { + // Try with different parameter names that the server might expect + const file = formData.get('file'); + if (!file) { + throw new Error('No file found in FormData'); + } + + // Create new FormData with different parameter name + const altFormData = new FormData(); + altFormData.append('csv_file', file); // Try 'csv_file' parameter + + console.log('Alternative FormData entries:'); + for (let pair of altFormData.entries()) { + console.log(pair[0] + ': ', pair[1]); + } + + const response = await postRequest(uploadEndpoint, altFormData, { + ...config, + headers: { + ...config.headers, + 'Content-Type': undefined, + }, + }); + return response.data; + } catch (error) { + console.error('Alternative upload error:', error); + throw error; + } +}; + export const fetchEstablishmentDetail = async (establishmentId, config = {}) => { const resolvedId = resolveEstablishmentId(establishmentId); if (resolvedId === undefined) { @@ -84,6 +138,22 @@ export const fetchEstablishmentDetail = async (establishmentId, config = {}) => return response.data; }; +// Bulk operations +export const bulkUpdateEstablishments = async (payload, config = {}) => { + const url = `${establishmentsEndpoint}/bulk-update`; + const response = await putRequest(url, payload, config); + return response.data; +}; + +export const bulkDeleteEstablishments = async (establishmentIds, config = {}) => { + const url = `${establishmentsEndpoint}/bulk-delete`; + const response = await deleteRequest(url, { + ...config, + data: { ids: establishmentIds } + }); + return response.data; +}; + export default { fetchEstablishments, fetchEstablishmentDashboard, @@ -91,4 +161,8 @@ export default { createEstablishment, updateEstablishment, deleteEstablishment, -}; + uploadCompanyProfileCSV, + uploadCompanyProfileCSVAlternative, + bulkUpdateEstablishments, + bulkDeleteEstablishments, +}; \ No newline at end of file