added currency
This commit is contained in:
parent
dea409690b
commit
54aab3740c
@ -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 |
18
ipi-survey-platform/src/assets/DirhamSymbol.jsx
Normal file
18
ipi-survey-platform/src/assets/DirhamSymbol.jsx
Normal 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;
|
||||
@ -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">
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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,
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user