added currency

This commit is contained in:
Malini 2025-11-17 15:08:00 +05:30
parent dea409690b
commit 54aab3740c
7 changed files with 652 additions and 20 deletions

View File

@ -0,0 +1,7 @@
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 870" width="1000" height="870">
<title>Layer copy</title>
<style>
.s0 { fill: #000000 }
</style>
<path id="Layer copy" class="s0" d="m88.3 1c0.4 0.6 2.6 3.3 4.7 5.9 15.3 18.2 26.8 47.8 33 85.1 4.1 24.5 4.3 32.2 4.3 125.6v87h-41.8c-38.2 0-42.6-0.2-50.1-1.7-11.8-2.5-24-9.2-32.2-17.8-6.5-6.9-6.3-7.3-5.9 13.6 0.5 17.3 0.7 19.2 3.2 28.6 4 14.9 9.5 26 17.8 35.9 11.3 13.6 22.8 21.2 39.2 26.3 3.5 1 10.9 1.4 37.1 1.6l32.7 0.5v43.3 43.4l-46.1-0.3-46.3-0.3-8-3.2c-9.5-3.8-13.8-6.6-23.1-14.9l-6.8-6.1 0.4 19.1c0.5 17.7 0.6 19.7 3.1 28.7 8.7 31.8 29.7 54.5 57.4 61.9 6.9 1.9 9.6 2 38.5 2.4l30.9 0.4v89.6c0 54.1-0.3 94-0.8 100.8-0.5 6.2-2.1 17.8-3.5 25.9-6.5 37.3-18.2 65.4-35 83.6l-3.4 3.7h169.1c101.1 0 176.7-0.4 187.8-0.9 19.5-1 63-5.3 72.8-7.4 3.1-0.6 8.9-1.5 12.7-2.1 8.1-1.2 21.5-4 40.8-8.9 27.2-6.8 52-15.3 76.3-26.1 7.6-3.4 29.4-14.5 35.2-18 3.1-1.8 6.8-4 8.2-4.7 3.9-2.1 10.4-6.3 19.9-13.1 4.7-3.4 9.4-6.7 10.4-7.4 4.2-2.8 18.7-14.9 25.3-21 25.1-23.1 46.1-48.8 62.4-76.3 2.3-4 5.3-9 6.6-11.1 3.3-5.6 16.9-33.6 18.2-37.8 0.6-1.9 1.4-3.9 1.8-4.3 2.6-3.4 17.6-50.6 19.4-60.9 0.6-3.3 0.9-3.8 3.4-4.3 1.6-0.3 24.9-0.3 51.8-0.1 53.8 0.4 53.8 0.4 65.7 5.9 6.7 3.1 8.7 4.5 16.1 11.2 9.7 8.7 8.8 10.1 8.2-11.7-0.4-12.8-0.9-20.7-1.8-23.9-3.4-12.3-4.2-14.9-7.2-21.1-9.8-21.4-26.2-36.7-47.2-44l-8.2-3-33.4-0.4-33.3-0.5 0.4-11.7c0.4-15.4 0.4-45.9-0.1-61.6l-0.4-12.6 44.6-0.2c38.2-0.2 45.3 0 49.5 1.1 12.6 3.5 21.1 8.3 31.5 17.8l5.8 5.4v-14.8c0-17.6-0.9-25.4-4.5-37-7.1-23.5-21.1-41-41.1-51.8-13-7-13.8-7.2-58.5-7.5-26.2-0.2-39.9-0.6-40.6-1.2-0.6-0.6-1.1-1.6-1.1-2.4 0-0.8-1.5-7.1-3.5-13.9-23.4-82.7-67.1-148.4-131-197.1-8.7-6.7-30-20.8-38.6-25.6-3.3-1.9-6.9-3.9-7.8-4.5-4.2-2.3-28.3-14.1-34.3-16.6-3.6-1.6-8.3-3.6-10.4-4.4-35.3-15.3-94.5-29.8-139.7-34.3-7.4-0.7-17.2-1.8-21.7-2.2-20.4-2.3-48.7-2.6-209.4-2.6-135.8 0-169.9 0.3-169.4 1zm330.7 43.3c33.8 2 54.6 4.6 78.9 10.5 74.2 17.6 126.4 54.8 164.3 117 3.5 5.8 18.3 36 20.5 42.1 10.5 28.3 15.6 45.1 20.1 67.3 1.1 5.4 2.6 12.6 3.3 16 0.7 3.3 1 6.4 0.7 6.7-0.5 0.4-100.9 0.6-223.3 0.5l-222.5-0.2-0.3-128.5c-0.1-70.6 0-129.3 0.3-130.4l0.4-1.9h71.1c39 0 78 0.4 86.5 0.9zm297.5 350.3c0.7 4.3 0.7 77.3 0 80.9l-0.6 2.7-227.5-0.2-227.4-0.3-0.2-42.4c-0.2-23.3 0-42.7 0.2-43.1 0.3-0.5 97.2-0.8 227.7-0.8h227.2zm-10.2 171.7c0.5 1.5-1.9 13.8-6.8 33.8-5.6 22.5-13.2 45.2-20.9 62-3.8 8.6-13.3 27.2-15.6 30.7-1.1 1.6-4.3 6.7-7.1 11.2-18 28.2-43.7 53.9-73 72.9-10.7 6.8-32.7 18.4-38.6 20.2-1.2 0.3-2.5 0.9-3 1.3-0.7 0.6-9.8 4-20.4 7.8-19.5 6.9-56.6 14.4-86.4 17.5-19.3 1.9-22.4 2-96.7 2h-76.9v-129.7-129.8l220.9-0.4c121.5-0.2 221.6-0.5 222.4-0.7 0.9-0.1 1.8 0.5 2.1 1.2z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,18 @@
import React from 'react';
const DirhamSymbol = (props) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="1em"
height="1em"
fill="currentColor"
className="inline-block w-4 h-4 -mt-0.5"
{...props}
>
<path d="M14 9h5v2h-4v1h4v2h-4v1h4v2h-5v-1h3v-1h-3v-1h3v-1h-3v-1z" />
<path d="M4 4h16v2H6v11h4v-2H8v-1h8v1h-2v2h3v2H4V4zm2 3h2v1H6V7zm0 3h2v1H6v-1z" />
</svg>
);
export default DirhamSymbol;

View File

@ -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 }) => {
</div>
</div>,
unit.uom || 'N/A',
product.current_quantity || '0',
`AED ${product.current_cost || '0'}`,
product.current_quantity || '0',
<span><img src={uae_currency} alt="AED" className="inline-block w-3.5 h-3 -mt-0.5 opacity-80" /> {product.current_cost || '0'}</span>,
// <span
// key={`status-${index}`}
// className={`inline-flex items-center justify-center rounded-md px-3 py-1 text-xs font-medium ${
@ -483,10 +485,10 @@ export const DetailedOverview = ({ submission, onBack }) => {
<p className="text-sm text-[#232528] font-medium">Total Submitted Products:</p>
<p className="text-sm text-[#232528]">{totalProducts}</p>
</div>
<div className="flex items-center space-x-2 pl-6">
{/* <div className="flex items-center space-x-2 pl-6">
<p className="text-sm text-[#232528] font-medium">Average Cost (AED):</p>
<p className="text-sm text-[#232528]">{averageCost}</p>
</div>
</div> */}
</div>
<div className="flex items-center space-x-4">
<div className="relative">

View File

@ -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) => {
<div className="flex items-end space-x-4 mt-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{quarterPeriods?.previous_month?.previous_period_one } Cost (AED)
{quarterPeriods?.previous_month?.previous_period_one } Cost (<img src={uae_currency} alt="AED" className="inline-block w-3.5 h-3 -mt-0.5 opacity-80" />)
</label>
<div className="flex items-center h-10 px-3 py-2 text-sm text-gray-700 rounded-md w-24" style={{ border: '1px solid #E6D7A2' }}>
{p.octCost || '0'}
@ -1397,7 +1425,7 @@ const handleProductSelect = async (productId, establishmentId, id) => {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{quarterPeriods?.previous_month?.previous_period_two } Cost (AED)
{quarterPeriods?.previous_month?.previous_period_two } Cost (<img src={uae_currency} alt="AED" className="inline-block w-3.5 h-3 -mt-0.5 opacity-80" />)
</label>
<div className="flex items-center h-10 px-3 py-2 text-sm text-gray-700 rounded-md w-24" style={{ border: '1px solid #E6D7A2' }}>
{p.novCost || '0'}
@ -1405,7 +1433,7 @@ const handleProductSelect = async (productId, establishmentId, id) => {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{quarterPeriods?.previous_month?.previous_period_three } Cost (AED)
{quarterPeriods?.previous_month?.previous_period_three } Cost (<img src={uae_currency} alt="AED" className="inline-block w-3.5 h-3 -mt-0.5 opacity-80" />)
</label>
<div className="flex items-center h-10 px-3 py-2 text-sm text-gray-700 rounded-md w-24" style={{ border: '1px solid #E6D7A2' }}>
{p.decCost || '0'}
@ -1474,7 +1502,7 @@ const handleProductSelect = async (productId, establishmentId, id) => {
<div className="flex items-end space-x-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{quarterPeriods?.current_month?.current_period_one } Cost (AED)<span className="text-red-600">*</span>
{quarterPeriods?.current_month?.current_period_one } Cost (<img src={uae_currency} alt="AED" className="inline-block w-3.5 h-3 -mt-0.5 opacity-80" />)<span className="text-red-600">*</span>
</label>
<Input
className="w-24"
@ -1490,7 +1518,7 @@ const handleProductSelect = async (productId, establishmentId, id) => {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{quarterPeriods?.current_month?.current_period_two} Cost (AED)<span className="text-red-600">*</span>
{quarterPeriods?.current_month?.current_period_two} Cost (<img src={uae_currency} alt="AED" className="inline-block w-3.5 h-3 -mt-0.5 opacity-80" />)<span className="text-red-600">*</span>
</label>
<Input
className="w-24"
@ -1506,7 +1534,7 @@ const handleProductSelect = async (productId, establishmentId, id) => {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{quarterPeriods?.current_month?.current_period_three} Cost (AED)<span className="text-red-600">*</span>
{quarterPeriods?.current_month?.current_period_three} Cost (<img src={uae_currency} alt="AED" className="inline-block w-3.5 h-3 -mt-0.5 opacity-80" />)<span className="text-red-600">*</span>
</label>
<Input
className="w-24"
@ -1619,7 +1647,7 @@ const handleProductSelect = async (productId, establishmentId, id) => {
<div className="flex items-end space-x-3">
<div>
<label className="block text-sm font-medium text-gray-700 w-26 mb-1">
{quarterPeriods?.forecast_month?.forecast_period_one} Cost (AED)<span className="text-red-600">*</span>
{quarterPeriods?.forecast_month?.forecast_period_one} Cost (<img src={uae_currency} alt="AED" className="inline-block w-3.5 h-3 -mt-0.5 opacity-80" />)<span className="text-red-600">*</span>
</label>
<Input
className="w-24"
@ -1636,7 +1664,7 @@ const handleProductSelect = async (productId, establishmentId, id) => {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 w-27 mb-1">
{quarterPeriods?.forecast_month?.forecast_period_two } Cost (AED)<span className="text-red-600">*</span>
{quarterPeriods?.forecast_month?.forecast_period_two } Cost (<img src={uae_currency} alt="AED" className="inline-block w-3.5 h-3 -mt-0.5 opacity-80" />)<span className="text-red-600">*</span>
</label>
<Input
className="w-24"
@ -1653,7 +1681,7 @@ const handleProductSelect = async (productId, establishmentId, id) => {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 w-27 mb-1">
{quarterPeriods?.forecast_month?.forecast_period_three } Cost (AED)<span className="text-red-600">*</span>
{quarterPeriods?.forecast_month?.forecast_period_three } Cost (<img src={uae_currency} alt="AED" className="inline-block w-3.5 h-3 -mt-0.5 opacity-80" />)<span className="text-red-600">*</span>
</label>
<Input
className="w-24"

View File

@ -10,6 +10,8 @@ import {
fetchEstablishmentDetail,
updateEstablishment,
deleteEstablishment,
uploadCompanyProfileCSV,
uploadCompanyProfileCSVAlternative
} from '@/services/establishments/establishmentService';
import { fetchProducts } from '@/services/masters/masterService';
const searchIconSrc = '/assets/images/material-symbols_search-rounded.svg';
@ -24,6 +26,57 @@ const resetPasswordInactiveSrc = '/assets/images/hugeicons_reset-password-inacti
const deleteIconSrc = '/assets/images/delete.svg';
const activeToggleSrc = '/assets/images/Toggle.svg';
const inactiveToggleSrc = '/assets/images/Toggle-inactive.svg';
const uploadImportIconSrc = '/assets/images/UploadSimple.svg';
// Custom Toast Component
const CustomToast = ({ message, type, onClose }) => {
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 (
<div className={`flex items-center p-4 mb-4 rounded-lg border ${getToastStyles()} shadow-lg min-w-80 max-w-md`}>
{/* <span className="text-lg mr-3">{getIcon()}</span> */}
<span className="flex-1 text-sm font-medium">{message}</span>
<button
onClick={onClose}
className="ml-4 text-gray-400 hover:text-gray-600 transition-colors"
>
</button>
</div>
);
};
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 (
<div className="bg-white w-full rounded-lg shadow-sm ring-1 ring-[#E5E7EB]">
{toast && (
@ -2682,6 +3030,14 @@ const requiredFields = [
width="180px"
variant="toolbar"
/>
<button
type="button"
onClick={openImportModal}
className="h-10 px-4 rounded-[6px] border border-[#C3C6CB] text-sm inline-flex items-center gap-2 bg-[#F7F7F7] text-[#232528] hover:bg-gray-50"
>
<img src={uploadImportIconSrc} alt="Import" className="h-5 w-5" />
<span className="font-medium">Import CSV</span>
</button>
<button
type="button"
className="h-10 px-4 rounded-[6px] bg-[#F7F7F7] border border-[#C3C6CB] text-sm inline-flex items-center gap-2 cursor-pointer"
@ -2975,6 +3331,146 @@ const requiredFields = [
</div>
</div>
</div>
)}
{/* Fixed Import CSV Modal */}
{importModalOpen && (
<div className="fixed inset-0 z-50">
<div className="absolute inset-0 bg-black/40" onClick={closeImportModal} />
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[520px] bg-white rounded-lg shadow-xl border border-[#E5E7EB]">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-[#E5E7EB]">
<h3 className="text-lg font-semibold text-[#111827]">Import Company Profiles</h3>
<button
type="button"
className="text-gray-400 hover:text-gray-500"
onClick={closeImportModal}
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Content */}
<div className="p-6 space-y-4">
{/* File Upload Area */}
<div
className={`border-2 border-dashed rounded-lg p-6 text-center cursor-pointer transition-colors ${
dragActive
? 'border-[#92722A] bg-[#FDF8EA]'
: 'border-[#D1D5DB] hover:border-[#92722A] hover:bg-gray-50'
}`}
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()}
>
<div className="flex flex-col items-center justify-center space-y-3">
<div className="w-12 h-12 bg-[#FDF8EA] rounded-full flex items-center justify-center">
<img src={uploadImportIconSrc} alt="Upload" className="w-6 h-6" />
</div>
<div>
<p className="text-sm font-medium text-gray-900">
{file ? file.name : 'Drop your CSV file here or browse'}
</p>
<p className="text-sm text-gray-500 mt-1">
Supports .csv files only (Max 10MB)
</p>
{file && (
<p className="text-xs text-green-600 mt-1">
File selected and ready to import
</p>
)}
</div>
<button
type="button"
className="px-4 py-2 text-sm font-medium text-[#92722A] bg-white border border-[#92722A] rounded-md hover:bg-[#FDF8EA]"
onClick={(e) => {
e.stopPropagation();
fileInputRef.current?.click();
}}
>
Browse Files
</button>
</div>
<input
ref={fileInputRef}
type="file"
accept=".csv,text/csv,application/vnd.ms-excel"
onChange={handleFileChange}
className="hidden"
/>
</div>
{/* Error Message */}
{importError && (
<div className="p-3 bg-red-50 border border-red-200 rounded-md">
<p className="text-sm text-red-800 flex items-center gap-2">
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
{importError}
</p>
</div>
)}
{/* Sample CSV Link */}
<div className="text-center">
<button
type="button"
onClick={downloadSampleCSV}
className="text-sm text-[#92722A] hover:text-[#7a5f22] underline"
>
Download sample CSV template
</button>
</div>
</div>
{/* Footer */}
<div className="flex justify-end p-6 border-t border-[#E5E7EB] space-x-3">
<button
type="button"
onClick={closeImportModal}
className="h-10 px-6 text-sm font-medium text-[#374151] bg-white border border-[#D1D5DB] rounded-md hover:bg-gray-50"
>
Cancel
</button>
<button
type="button"
onClick={handleImport}
disabled={!file || importLoading}
className={`h-10 px-6 text-sm font-medium text-white rounded-md ${
!file || importLoading
? 'bg-gray-300 cursor-not-allowed'
: 'bg-[#92722A] hover:bg-[#7a5f22]'
}`}
>
{importLoading ? (
<div className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
Importing...
</div>
) : (
'Import'
)}
</button>
</div>
</div>
</div>
)}
{/* Custom Toast Notification */}
{toastData && (
<div className="fixed inset-0 z-[9999] pointer-events-none flex items-start justify-center pt-20">
<div className="pointer-events-auto">
<CustomToast
message={toastData.message}
type={toastData.type}
onClose={() => setToastData(null)}
/>
</div>
</div>
)}
</div>
);

View File

@ -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 ? (
<span className="inline-flex items-center">
<img src={uae_currency} alt="AED" className="inline-block w-3.5 h-3 -mt-0.5 opacity-80 mr-1" />
{record.total_cost}
</span>
) : '-',
record.status,
formatDate(record.created_at),
'View Details',

View File

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