This commit is contained in:
Senthamilselvi 2025-11-06 09:39:54 +05:30
commit 00491af737
8 changed files with 959 additions and 262 deletions

View File

@ -268,7 +268,7 @@ function App() {
</RequireRole>
}
/>
<Route path="profile">
{/* <Route path="profile">
<Route
index
element={
@ -277,7 +277,20 @@ function App() {
</RequireRole>
}
/>
</Route>
</Route> */}
<Route path="/profile*" element={
<RequireRole allowedRoles={['EstablishmentUser']}>
<EditCompanyProfile />
</RequireRole>
} />
{/* <Route
path="/admin/configuration/profile/:id?"
element={
<RequireRole allowedRoles={['EstablishmentUser']}>
<EditCompanyProfile />
</RequireRole>
}
/> */}
<Route path="/change-password" element={<ChangePassword />} />
<Route path="/forgot-password" element={<ForgotPassword />} />
<Route path="/reset-password" element={<PublicContactForm />} />

View File

@ -329,6 +329,7 @@ export const SelectField = ({
clearValue = '',
error = '',
readOnly = false,
required = false,
}) => {
const wrapperStyle = {};
if (width !== 'auto') {
@ -383,6 +384,7 @@ export const SelectField = ({
{label && (
<label htmlFor={id || name} className="block text-[14px] leading-[20px] font-medium text-[#232528] mb-1">
{label}
{required && <span className="text-[#B91C1C] ml-1">*</span>}
</label>
)}
<div className="relative" style={{ width: '100%' }}>
@ -399,6 +401,7 @@ export const SelectField = ({
style={inputStyle}
onFocus={handleFocus}
onBlur={handleBlur}
required={required}
>
{placeholder && (
<option value="" disabled hidden>

View File

@ -257,7 +257,7 @@ const handleEditProfile = (e) => {
<p className="text-sm text-[#043DFF] bg-[#E7F5FF]">
<span className="font-medium">Need to update details?</span> Go to Profile {' '}
<a
href={`/admin/configuration/profile?edit=${sessionStorage.getItem('establishment_id') || ''}`}
href={`/admin/configuration/profile/edit=${sessionStorage.getItem('establishment_id') || ''}`}
className="underline font-medium text-[#043DFF] hover:text-[#063B82]"
onClick={handleEditProfile}
@ -343,7 +343,7 @@ const handleEditProfile = (e) => {
/>
<Field
label="Permanent Factory Code"
label="Permanent Factory Code(PFC)"
placeholder="Enter Permanent Factory Code"
value={info.permanentFactoryCode}
onChange={handleFieldChange('permanentFactoryCode')}

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import Table from '@/components/common/Table';
import Loader from '@/components/common/Loader';
@ -45,6 +45,24 @@ const validatePasswordComplexity = (password) => {
const countryCodeOptions = [{ label: '+971', value: '+971' }];
const contactToCorporateMap = {
contactName: 'corporateName',
contactAddress: 'corporateAddress',
contactCityTown: 'corporateCityTown',
contactCityTownId: 'corporateCityTownId',
contactEmirate: 'corporateEmirate',
contactEmirateId: 'corporateEmirateId',
contactPostalCode: 'corporatePostalCode',
contactPoBox: 'corporatePoBox',
contactMakaniNumber: 'corporateMakaniNumber',
contactPersonName: 'corporateContactPersonName',
contactPersonDesignation: 'corporateContactPersonDesignation',
contactCountryCode: 'corporateCountryCode',
contactMobileNumber: 'corporateMobileNumber',
contactEmail: 'corporateEmail',
contactWebsite: 'corporateWebsite'
};
const WEBSITE_URL_PATTERN = /^(https?:\/\/)?([\w-]+\.)+[\w-]{2,}(\/[^\s]*)?$/i;
const WEBSITE_ERROR_MESSAGE = 'Enter a valid URL (e.g., https://example.com).';
@ -164,9 +182,11 @@ const CompanyProfile = () => {
const [fieldErrors, setFieldErrors] = React.useState({});
const [isDirty, setIsDirty] = React.useState(false);
const initialSnapshotRef = React.useRef(createEmptyProfile());
const corporateBackupRef = useRef(null);
const toastTimeoutRef = React.useRef(null);
const [isDeletingApi, setIsDeletingApi] = React.useState(false);
const [totalItems, setTotalItems] = React.useState(0);
const [currentUser, setCurrentUser] = React.useState(null);
const showToast = React.useCallback((type, message) => {
if (!message) return;
@ -194,6 +214,7 @@ const CompanyProfile = () => {
};
}, []);
const computeIsDirty = React.useCallback(
(nextForm) => {
const initial = initialSnapshotRef.current;
@ -282,6 +303,33 @@ const CompanyProfile = () => {
[form, requiredFieldsByStep]
);
// Add this useEffect to load the current user from session storage
const currentUserId = React.useMemo(() => {
try {
const profile = sessionStorage.getItem('user_profile');
console.log("profile678",profile)
if (!profile) return null;
const parsed = JSON.parse(profile);
console.log('Current user:', parsed);
return parsed;
} catch (error) {
console.error('Error parsing user profile:', error);
return null;
}
}, []);
const profile = sessionStorage.getItem('user_profile');
// console.log("profile123",profile)
const userProfile = React.useMemo(() => {
try {
const profile = sessionStorage.getItem('user_profile');
// console.log("profile",profile)
return profile ? JSON.parse(profile) : null;
} catch (error) {
console.error('Error parsing user profile:', error);
return null;
}
}, []);
const renderStepContent = () => {
switch (activeStep) {
case 0:
@ -309,7 +357,60 @@ const CompanyProfile = () => {
required
error={fieldErrors.userProfileEmail}
/>
<div className="space-y-1">
{modalMode === 'add' && (
<>
<div className="space-y-1">
<TextField
label="New Password"
value={form.userProfilePassword}
onChange={handleFormChange('userProfilePassword')}
placeholder="Enter password"
width="100%"
type="password"
showToggle
className={passwordError || passwordReuseError ? 'border-red-500' : ''}
required
/>
{passwordError && <p className="text-xs text-[#B91C1C]">{passwordError}</p>}
{passwordReuseError && <p className="text-xs text-[#B91C1C]">{passwordReuseError}</p>}
</div>
<div className="space-y-1">
<TextField
label="Confirm Password"
value={form.userProfileConfirmPassword}
onChange={handleFormChange('userProfileConfirmPassword')}
placeholder="Confirm password"
width="100%"
type="password"
showToggle
className={confirmError ? 'border-red-500' : ''}
required
/>
{confirmError && <p className="text-xs text-[#B91C1C]">{confirmError}</p>}
</div>
</>
)}
{/* For Edit Mode - Show only password field */}
{modalMode === 'edit' && (
<div className="space-y-1">
<TextField
label="Password"
value="••••••••"
placeholder="Enter new password (leave blank to keep current)"
width="100%"
type="password"
disabled={true} // ensures non-editable
showToggle={false} // optional: hide toggle if disabled
className={passwordError ? 'border-red-500' : ''}
/>
{passwordError && (
<p className="text-xs text-[#B91C1C]">{passwordError}</p>
)}
</div>
)}
{/* <div className="space-y-1">
<TextField
label="New Password"
value={form.userProfilePassword}
@ -322,8 +423,8 @@ const CompanyProfile = () => {
/>
{passwordError && <p className="text-xs text-[#B91C1C]">{passwordError}</p>}
{passwordReuseError && <p className="text-xs text-[#B91C1C]">{passwordReuseError}</p>}
</div>
<div className="space-y-1">
</div> */}
{/* <div className="space-y-1">
<TextField
label="Confirm Password"
value={form.userProfileConfirmPassword}
@ -335,7 +436,7 @@ const CompanyProfile = () => {
className={confirmError ? 'border-red-500' : ''}
/>
{confirmError && <p className="text-xs text-[#B91C1C]">{confirmError}</p>}
</div>
</div> */}
</div>
</div>
</div>
@ -385,11 +486,12 @@ const CompanyProfile = () => {
</div>
<div className="mt-4">
<TextField
label="Description of Industry"
label="Description"
value={form.industryDescription}
onChange={handleFormChange('industryDescription')}
placeholder="Enter Description"
width="100%"
required
style={{ height: '72px' }}
/>
</div>
@ -1027,6 +1129,35 @@ const CompanyProfile = () => {
[showToast]
);
const captureCorporateValues = (formData) => {
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] || '';
});
return corporateValues;
};
// Add this array as well
const corporateFieldKeys = Object.values(contactToCorporateMap);
const loadCorporateCityOptions = React.useCallback(
async (emirateId) => {
if (!emirateId) {
@ -1082,6 +1213,16 @@ const CompanyProfile = () => {
}, []);
const mapApiEstablishmentToProfile = React.useCallback((item) => {
// Get current user from session storage
let currentUser = null;
try {
const profile = sessionStorage.getItem('user_profile');
if (profile) {
currentUser = JSON.parse(profile);
}
} 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 ?? '';
@ -1094,6 +1235,24 @@ const CompanyProfile = () => {
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
creatorName = currentUser.name || currentUser.username || `User ${item.created_by}`;
} else if (item?.created_by) {
// If not the current user, try to get creator's name from the item or use ID as fallback
creatorName = item?.created_by_name ||
item?.created_by_user?.name ||
item?.creator?.name ||
`User ${item.created_by}`;
}
console.log('Creator ID:', item?.created_by, 'Current User ID:', currentUser?.id);
console.log('Creator Name:', creatorName);
return {
...base,
@ -1114,12 +1273,13 @@ const CompanyProfile = () => {
totalEmployees: totalEmployees !== '' && totalEmployees !== null ? String(totalEmployees) : '',
status: item?.is_active === false ? 'Inactive' : 'Active',
isActive: item?.is_active !== false,
createdBy: item?.created_by_name ?? primaryUser?.name ?? '',
createdBy: creatorName,
createdById: item?.created_by ?? base.createdById,
createdOn: formatApiDate(item?.created_at),
lastUpdated: item?.updated_by ?? formatApiDate(item?.updated_at),
contactName: item?.factory_name ?? '',
contactEmail: item?.email ?? '',
// contactEmail: item?.email ?? '',
contactEmail: item?.establishment_contact_email ?? base.contactEmail,
contactPostalCode: item?.establishment_postal_code ?? base.contactPostalCode,
contactPoBox: item?.establishment_po_box ?? base.contactPoBox,
contactMakaniNumber: item?.establishment_makani_number ?? base.contactMakaniNumber,
@ -1173,6 +1333,7 @@ const loadProfiles = async () => {
try {
const response = await fetchEstablishments({ params, signal: controller.signal });
const payload = response?.data ?? response;
console.log("payloadtest",payload)
const records = Array.isArray(payload?.data) ? payload.data : Array.isArray(payload) ? payload : [];
if (!cancelled) {
@ -1257,73 +1418,103 @@ const loadProfiles = async () => {
[]
);
const handleEdit = (identifier) => {
if (modalMode && isDirty) {
const shouldContinue = window.confirm('You have unsaved changes. Do you want to discard them and continue?');
if (!shouldContinue) {
return;
}
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');
// Update URL with the establishment ID
navigate(`?edit=${encodeURIComponent(identifier)}`);
if (!identifier) return;
// Find the profile record
const originalIndex = profiles.findIndex((item) => {
if (item.apiId !== null && item.apiId !== undefined && item.apiId === identifier) {
return true;
}
return item.establishmentId === identifier;
});
if (originalIndex === -1) return;
setEditingRow(originalIndex);
if (originalIndex === -1) {
showToast('error', 'Establishment not found');
return;
}
const profileRecord = profiles[originalIndex];
const base = createEmptyProfile();
let nextForm = {
...base,
...profileRecord,
corporateSameAs: Boolean(profileRecord?.corporateSameAs),
};
if (nextForm.corporateSameAs) {
Object.entries(contactToCorporateMap).forEach(([source, target]) => {
nextForm[target] = nextForm[source];
});
setEditingRow(originalIndex);
// If we have an API ID, fetch the latest data
if (profileRecord?.apiId) {
const response = await fetchEstablishmentDetail(profileRecord.apiId);
const detail = response?.data || response;
if (!detail) {
showToast('error', 'No data received from server');
return;
}
// Map the API response to form fields
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];
});
}
// Set the form with the mapped data
setForm(prev => ({
...prev,
...mapped,
...computeEmploymentTotals(mapped)
}));
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];
});
}
Object.assign(nextForm, computeEmploymentTotals(nextForm));
setForm(nextForm);
initialSnapshotRef.current = { ...nextForm };
corporateBackupRef.current = captureCorporateValues(nextForm);
}
Object.assign(nextForm, computeEmploymentTotals(nextForm));
setForm(nextForm);
setModalMode('edit');
initialSnapshotRef.current = nextForm;
corporateBackupRef.current = captureCorporateValues(nextForm);
setIsDirty(false);
if (profileRecord?.apiId !== null && profileRecord?.apiId !== undefined) {
(async () => {
try {
const response = await fetchEstablishmentDetail(profileRecord.apiId);
const detail = response?.data || response;
if (!detail) 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];
});
}
Object.assign(mapped, computeEmploymentTotals(mapped));
setForm(mapped);
initialSnapshotRef.current = mapped;
corporateBackupRef.current = captureCorporateValues(mapped);
setIsDirty(false);
} catch (error) {
const message = error?.response?.data?.message || error?.message || 'Failed to load establishment details.';
showToast('error', message);
}
})();
}
};
} 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;
@ -1387,7 +1578,7 @@ const loadProfiles = async () => {
setModalMode(null);
setForm(createEmptyProfile());
setEditingRow(null);
setErrors({});
setFieldErrors({});
setIsDirty(false);
// Clear URL parameters when closing modal
navigate('');
@ -1401,6 +1592,56 @@ const loadProfiles = async () => {
setDeletingRow(null);
};
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';
}
}
// Website URL validation
if ((field === 'contactWebsite' || field === 'corporateWebsite') && value) {
if (!/^https?:\/\/.+\..+/.test(value)) {
return 'Please enter a valid URL (e.g., https://example.com)';
}
}
// Numeric field validation
const numericFields = [
'employmentEmiratiMale',
'employmentEmiratiFemale',
'employmentNonEmiratiMale',
'employmentNonEmiratiFemale',
'employmentTotalEmirati',
'employmentTotalEmployees'
];
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;
@ -1650,7 +1891,8 @@ const loadProfiles = async () => {
non_emirati_female: Number(form.employmentNonEmiratiFemale) || 0,
total_emirati: Number(totalEmirati) || 0,
total_employees: Number(employmentTotals) || 0,
created_by: Number(form.createdById) || 0,
created_by: currentUserId?.id,
created_by_name: currentUserId?.name, // Add this line
establishment_user: {
name: form.userProfileName || '',
email: form.userProfileEmail || '',
@ -1659,6 +1901,7 @@ const loadProfiles = async () => {
};
try {
const apiResponse = await createEstablishment(apiPayload);
console.log('API Response:', apiResponse);
const successMessage = apiResponse?.message || 'Establishment added successfully.';
showToast('success', successMessage);
const createdRecord = apiResponse?.data;
@ -1689,25 +1932,50 @@ const loadProfiles = async () => {
delete establishmentUserPayload.email;
}
const updatePayload = {
establishment_code: form.establishmentId || '',
factory_name: form.establishmentName || form.contactName || '',
email: form.contactEmail || form.userProfileEmail || '',
permanent_factory_code: form.permanentFactoryCode || '',
industry_code: form.industryCodeCurrent || form.industryCodeBusiness || '',
license_number: form.uniqueLicenseNumber || '',
isic_code: form.industryCodeBusiness || form.isicCode || '',
emirate: form.emirate || form.contactEmirate || '',
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(totalEmirati) || 0,
total_employees: Number(employmentTotals) || 0,
establishment_code: form.establishmentId || '',
factory_name: form.establishmentName || form.contactName || '',
permanent_factory_code: form.permanentFactoryCode || '',
industry_code: form.industryCodeBusiness || form.industryCodeCurrent || '',
license_number: form.uniqueLicenseNumber || '',
isic_code: form.isicCode || form.industryCodeBusiness || '',
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 // Replace with actual user ID from your auth context
};
if (establishmentUserPayload.name || establishmentUserPayload.email || establishmentUserPayload.password) {
updatePayload.establishment_user = establishmentUserPayload;
}
const apiResponse = await updateEstablishment(targetId, updatePayload);
console.log("apiResponse",apiResponse)
const successMessage = apiResponse?.message || 'Establishment updated successfully.';
showToast('success', successMessage);
apiResultData = apiResponse?.data;
@ -1932,7 +2200,7 @@ const loadProfiles = async () => {
120, // ISIC Code (120px)
150, // Establishment ID (150px)
100, // Products (100px)
120, // Total Employees (120px)
130, // Total Employees (120px)
120, // Created By (120px)
120, // Created On (120px)
120, // Last Updated (120px)

View File

@ -56,6 +56,17 @@ const IsicHsCodes = () => {
const [form, setForm] = React.useState(createEmptyCodeForm());
const [unitOptions, setUnitOptions] = React.useState([]);
const [toast, setToast] = React.useState({ show: false, message: '' });
// Get user profile from session
const userProfile = React.useMemo(() => {
try {
const profile = sessionStorage.getItem('user_profile');
return profile ? JSON.parse(profile) : null;
} catch (error) {
console.error('Error parsing user profile:', error);
return null;
}
}, []);
// Fetch units on mount
useEffect(() => {
@ -111,18 +122,32 @@ const IsicHsCodes = () => {
: response.data.data || [];
if (products.length > 0) {
const formattedData = products.map((product) => ({
id: product.id,
code: product.hs_code || 'N/A',
product: product.product_name || 'N/A',
unit: 'Unit',
estimatedMapped: 0,
createdBy: product.created_by || 'System',
updated: product.updated_at
? new Date(product.updated_at).toLocaleDateString()
: '-',
status: product.is_active ? 'Active' : 'Inactive',
description: product.hs_description || '',
const formattedData = await Promise.all(products.map(async (product) => {
// Try to get creator's name if available
let createdByName = '-';
if (product.created_by) {
try {
// If the API returns the creator's name in the product data, use it
createdByName = product.created_by_user?.name || '-';
} catch (error) {
console.error('Error fetching creator info:', error);
}
}
return {
id: product.id,
code: product.hs_code || 'N/A',
product: product.product_name || 'N/A',
unit: product.unit?.uom || 'N/A',
estimatedMapped: 0,
createdBy: createdByName,
createdById: product.created_by || null,
updated: product.updated_at
? new Date(product.updated_at).toLocaleDateString()
: '-',
status: product.is_active ? 'Active' : 'Inactive',
description: product.hs_description || '',
};
}));
setRowsData(formattedData);
setFilteredData(formattedData);
@ -199,7 +224,7 @@ const IsicHsCodes = () => {
setForm({
code: product.hs_code || '',
product: product.product_name || '',
unit: 'Unit', // Update this if you have unit in the API response
unit: product.unit_id ? String(product.unit_id) : '',
status: product.is_active ? 'Active' : 'Inactive',
description: product.hs_description || ''
});
@ -235,7 +260,7 @@ const IsicHsCodes = () => {
setForm({
code: product.hs_code || '',
product: product.product_name || '',
unit: 'Unit', // Update this if you have unit in the API response
unit: product.unit_id ? String(product.unit_id) : '',
status: product.is_active ? 'Active' : 'Inactive',
description: product.hs_description || ''
});
@ -253,30 +278,98 @@ const IsicHsCodes = () => {
}
};
const handleDelete = (index) => {
setDeletingRow(index);
setShowDeleteConfirm(true);
};
// Store the row index when delete is clicked
const [deletingRowIndex, setDeletingRowIndex] = React.useState(null);
const deletingCode = React.useMemo(() => {
if (deletingRow === null || deletingRow < 0 || deletingRow >= rowsData.length)
return null;
return rowsData[deletingRow];
}, [deletingRow, rowsData]);
const closeDeleteConfirm = () => {
setShowDeleteConfirm(false);
setDeletingRow(null);
};
const handleDeleteConfirm = async () => {
if (deletingRow === null) return;
const handleDelete = async (paginationIndex) => {
// Convert pagination index to actual data index
const dataIndex = (currentPage - 1) * pageSize + paginationIndex;
console.log('Delete button clicked, paginationIndex:', paginationIndex, 'dataIndex:', dataIndex);
if (dataIndex < 0 || dataIndex >= rowsData.length) {
console.error('Invalid row index for deletion');
return;
}
const productId = rowsData[dataIndex]?.id;
if (!productId) {
console.error('No product ID found for deletion');
return;
}
// Store the row index for later use
setDeletingRowIndex(dataIndex);
try {
setIsLoading(true);
const productId = rowsData[deletingRow]?.id;
console.log('Fetching product details before deletion, ID:', productId);
// Fetch the latest product data by ID
const response = await productService.getProductById(productId);
console.log('Product details from API:', response);
if (!response || !response.data) {
throw new Error('Invalid product data received');
}
// Update the row data with the latest from the server
const updatedRowsData = [...rowsData];
updatedRowsData[dataIndex] = {
...updatedRowsData[dataIndex],
...response.data
};
setRowsData(updatedRowsData);
setShowDeleteConfirm(true);
} catch (error) {
console.error('Error preparing product for deletion:', error);
showToast('Error loading product details for deletion', 'error');
setDeletingRowIndex(null);
} finally {
setIsLoading(false);
}
};
const deletingCode = React.useMemo(() => {
if (deletingRowIndex === null || deletingRowIndex < 0 || deletingRowIndex >= rowsData.length)
return null;
return rowsData[deletingRowIndex];
}, [deletingRowIndex, rowsData]);
const closeDeleteConfirm = () => {
setShowDeleteConfirm(false);
setDeletingRowIndex(null);
};
const handleDeleteConfirm = async () => {
console.log('Delete confirmed, deletingRowIndex:', deletingRowIndex);
if (deletingRowIndex === null || deletingRowIndex < 0 || deletingRowIndex >= rowsData.length) {
const errorMsg = 'Invalid row selected for deletion';
console.error(errorMsg, { deletingRowIndex, rowsDataLength: rowsData.length });
showToast(errorMsg, 'error');
setShowDeleteConfirm(false);
setDeletingRowIndex(null);
return;
}
try {
setIsLoading(true);
const product = rowsData[deletingRowIndex];
const productId = product?.id;
if (!productId) {
throw new Error('Product ID not found for deletion');
throw new Error('No product ID found for deletion');
}
console.log('Deleting product:', { productId, product });
console.log('Product ID to delete:', productId);
if (!productId) {
const errorMsg = 'Product ID not found for deletion';
console.error(errorMsg);
throw new Error(errorMsg);
}
// Call delete API
@ -376,6 +469,8 @@ const IsicHsCodes = () => {
product_name: form.product,
is_active: form.status === 'Active',
hs_code: form.code,
unit_id: form.unit ? parseInt(form.unit) : null,
created_by: userProfile?.id || null, // Add created_by with user ID
...(form.description && { hs_description: form.description })
};
@ -391,35 +486,60 @@ const IsicHsCodes = () => {
console.log(`Updating product with ID: ${productId}`);
try {
// Call the update API with only the necessary fields
console.log('Sending update request with data:', {
productId,
productData,
formUnit: form.unit,
unitOptions: unitOptions
});
const response = await productService.updateProduct(productId, productData);
console.log('Update response:', response);
// Update the local state with the updated product data
setRowsData(prev =>
prev.map(item =>
item.id === productId
? {
...item,
code: form.code,
product: form.product,
status: form.status,
description: form.description || '',
updated: new Date().toLocaleDateString('en-GB')
}
: item
)
);
// Find the selected unit from unitOptions to get the uom
const selectedUnit = unitOptions.find(u => u.value === form.unit);
console.log('Selected unit for update:', selectedUnit);
// Create the updated product object
const updatedProduct = {
...rowsData[editingRow],
code: form.code,
product: form.product,
unit: selectedUnit ? selectedUnit.label : 'N/A',
unit_id: form.unit ? parseInt(form.unit) : null,
updated: new Date().toLocaleDateString('en-GB'),
status: form.status,
description: form.description || ''
};
// Update the product in the list while maintaining the same position
setRowsData(prev => {
const updated = [...prev];
updated[editingRow] = updatedProduct;
return updated;
});
// Also update filteredData if needed
setFilteredData(prev => {
const updated = [...prev];
const index = updated.findIndex(item => item.id === updatedProduct.id);
if (index !== -1) {
updated[index] = updatedProduct;
}
return updated;
});
console.log('Product updated successfully');
showToast('Product updated successfully!');
closeModal();
return; // Exit the function after successful update
return;
} catch (updateError) {
console.error('Error updating product:', updateError);
const errorMessage = updateError.response?.data?.message || 'Failed to update product. Please try again.';
let errorMessage = 'Failed to update product. Please try again.';
if (updateError.response?.data?.message) {
errorMessage = updateError.response.data.message;
}
setError(errorMessage);
return; // Exit the function after error
return;
}
}
@ -429,19 +549,26 @@ const IsicHsCodes = () => {
const response = await productService.createProduct(productData);
console.log('Create response:', response);
// Find the selected unit from unitOptions to get the uom
const selectedUnit = unitOptions.find(u => u.value === form.unit);
const newProduct = {
id: response.data?.id || Date.now(), // Fallback to timestamp if no ID in response
code: form.code,
product: form.product,
unit: 'Unit',
unit: selectedUnit ? selectedUnit.label : 'N/A',
unit_id: form.unit ? parseInt(form.unit) : null,
estimatedMapped: 0,
createdBy: 'Current User',
createdBy: userProfile?.name || 'System', // Use user's name or fallback to 'System'
createdById: userProfile?.id || null, // Store user ID for reference
updated: new Date().toLocaleDateString('en-GB'),
status: form.status,
description: form.description || ''
};
setRowsData(prev => [...prev, newProduct]);
// Add new product to the beginning of the array to show it at the top
setRowsData(prev => [newProduct, ...prev]);
setFilteredData(prev => [newProduct, ...prev]);
console.log('Product created successfully');
showToast('Product created successfully!');
closeModal();
@ -782,14 +909,14 @@ const IsicHsCodes = () => {
<div className="flex justify-end gap-4">
<button
type="button"
className="h-10 px-6 rounded-md border border-[#92722A] text-sm font-medium text-[#92722A] bg-white"
onClick={handleDeleteConfirm}
className="h-10 px-6 rounded-md border border-[#92722A] text-sm font-medium text-[#92722A] bg-white hover:bg-gray-50"
onClick={closeDeleteConfirm}
>
Cancel
</button>
<button
type="button"
className="h-10 px-6 rounded-md bg-[#92722A] text-white text-sm font-semibold"
className="h-10 px-6 rounded-md bg-red-600 text-white text-sm font-semibold hover:bg-red-700"
onClick={handleDeleteConfirm}
>
Delete

View File

@ -347,7 +347,7 @@ const QuarterlyWindows = () => {
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search Establishment"
placeholder="Search Survey"
className="h-10 w-full rounded-[4px] border border-[#C3C6CB] pl-10 pr-3 text-sm text-[#232528] focus:outline-none"
/>
<img src={searchIconSrc} alt="Search" className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5" />
@ -590,62 +590,65 @@ const QuarterlyWindows = () => {
)}
{showDeleteConfirm && (
<div className="fixed inset-0 z-50">
<div className="absolute inset-0 bg-black/40" onClick={closeDeleteConfirm} />
<div
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-lg bg-white border"
style={{
width: '480px',
maxWidth: '92vw',
borderColor: '#CBD5E1',
boxShadow: '0 20px 45px rgba(0,0,0,0.12)',
}}
>
<div className="px-8 py-6 space-y-6">
<div className="flex items-start gap-3">
<span
className="flex items-center justify-center"
style={{ width: '32px', height: '36px' }}
>
<img src={deleteIconSrc} alt="Delete" className="h-6 w-6" />
</span>
<div className="flex-1">
<h3 className="text-[18px] font-semibold text-[#232528]">Delete Quarter</h3>
<p className="mt-1 text-sm leading-5 text-[#4B5563]">
Are you sure you want to delete{' '}
{deletingQuarter && (
<span className="font-semibold text-[#232528]">
{`${deletingQuarter.year}-${deletingQuarter.quarter}`}
</span>
)}
?
</p>
<p className="mt-2 text-sm leading-5 text-[#4B5563]">
This action will permanently delete the data and cannot be undone.
</p>
</div>
</div>
<div className="flex items-center justify-end gap-4">
<button
type="button"
className="text-sm font-medium text-[#92722A] cursor-pointer"
onClick={closeDeleteConfirm}
>
Cancel
</button>
<button
type="button"
className="h-10 px-6 rounded-md bg-[#92722A] text-white text-sm font-semibold cursor-pointer shadow-sm"
onClick={handleDelete}
>
Delete
</button>
</div>
</div>
<div className="fixed inset-0 z-50">
<div
className="absolute inset-0 bg-black/40"
onClick={() => {
setShowDeleteConfirm(false);
setDeletingRow(null);
}}
/>
<div
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-lg bg-white border"
style={{
width: '480px',
borderColor: '#CBD5E1',
boxShadow: '0 20px 45px rgba(0,0,0,0.12)',
}}
>
<div className="px-8 py-6 space-y-6">
<div className="flex items-start gap-3">
<img src={deleteIconSrc} alt="Delete" className="h-6 w-6 mt-1" />
<div className="flex-1">
<h3 className="text-[18px] font-semibold text-[#232528]">
Delete Product
</h3>
<p className="mt-1 text-sm text-[#4B5563]">
Are you sure you want to delete{' '}
<span className="font-semibold text-[#232528]">
{rowsData[deletingRow]?.product || 'this product'}?
</span>
</p>
<p className="mt-2 text-sm text-[#4B5563]">
This action will permanently delete the data and cannot be undone.
</p>
</div>
</div>
)}
<div className="flex justify-end gap-4">
<button
type="button"
className="h-10 px-6 rounded-md border border-[#92722A] text-sm font-medium text-[#92722A] bg-white"
onClick={() => {
setShowDeleteConfirm(false);
setDeletingRow(null);
}}
>
Cancel
</button>
<button
type="button"
className="h-10 px-6 rounded-md bg-[#92722A] text-white text-sm font-semibold"
onClick={handleConfirmDelete}
disabled={isLoading}
>
{isLoading ? 'Deleting...' : 'Delete'}
</button>
</div>
</div>
</div>
</div>
)}
</div>
</>
);

View File

@ -3,7 +3,8 @@ import Table from '@/components/common/Table';
import StatusBadge from '@/components/common/StatusBadge';
import { TextField, SelectField } from '@/components/common/FormControls';
import { getUnits, createUnit, updateUnit, deleteUnit } from '@/services/configuration/unitService';
import { toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
const downloadIconSrc = '/assets/images/DownloadSimple.svg';
const addIconSrc = '/assets/images/ic_baseline-plus.svg';
const pencilActiveSrc = '/assets/images/PencilSimple.svg';
@ -40,17 +41,47 @@ const UnitMaster = () => {
fetchUnits();
}, []);
const fetchUnits = async () => {
try {
setLoading(true);
const response = await getUnits();
setUnits(response.data || []);
} catch (error) {
console.error('Error fetching units:', error);
} finally {
setLoading(false);
}
};
// const fetchUnits = async () => {
// try {
// setLoading(true);
// const response = await getUnits();
// setUnits(response.data || []);
// } catch (error) {
// console.error('Error fetching units:', error);
// } finally {
// setLoading(false);
// }
// };
const fetchUnits = async () => {
try {
setLoading(true);
const response = await getUnits();
console.log("resposne",response)
const unitsData = response.data || [];
// Process units to ensure mapped_products_count is included
const processedUnits = unitsData.map(unit => ({
...unit,
// Use existing mapped_products_count or calculate from productsMapped array
mapped_products_count: unit.mapped_products_count ||
(Array.isArray(unit.productsMapped) ? unit.productsMapped.length : 0)
}));
console.log("processedUnits",processedUnits)
// Sort by latest created_at date (newest first)
const sortedUnits = [...processedUnits].sort(
(a, b) => new Date(b.created_at) - new Date(a.created_at)
);
setUnits(sortedUnits);
} catch (error) {
console.error('Error fetching units:', error);
toast.error('Failed to fetch units. Please try again.');
} finally {
setLoading(false);
}
};
useEffect(() => {
const userProfile = sessionStorage.getItem('user_profile');
@ -59,36 +90,45 @@ const UnitMaster = () => {
setCurrentUser(JSON.parse(userProfile));
}
}, []);
const headers = [
'Unit Name',
'Description',
'Mapped Products',
'Created By',
'Created On',
'Status',
'Actions',
];
const headers = [
'Unit Name',
'Description',
'Mapped Products',
'Created By',
'Created On',
'Last Updated',
'Status',
'Actions',
];
const rows = useMemo(() => {
return units.map((item) => {
const createdDate = item.created_at ? new Date(item.created_at).toLocaleDateString('en-GB') : '-';
const currentUserName = currentUser?.name || '';
const createdDate = item.created_at
? new Date(item.created_at).toLocaleDateString('en-GB')
: '-';
const updatedDate =
item.updated_at && item.updated_at !== item.created_at
? new Date(item.updated_at).toLocaleDateString('en-GB')
: '-';
const currentUserName = currentUser?.name || '';
return [
item.uom || item.unitName,
item.uom_short_name || item.description,
Array.isArray(item.productsMapped) ? item.productsMapped.length : 0,
item.created_by_name || currentUserName || '-',
createdDate,
<StatusBadge status={item.is_active ? 'Active' : 'Inactive'} tone={item.is_active ? 'green' : 'gray'} />,
'actions',
item.uom || item.unitName, // Unit Name
item.uom_short_name || item.description, // Description
Array.isArray(item.productsMapped) ? item.productsMapped.length : 0, // Mapped Products
item.created_by_name || currentUserName || '-', // Created By
createdDate, // Created On
updatedDate, // Last Update (moved here)
<StatusBadge
status={item.is_active ? 'Active' : 'Inactive'}
tone={item.is_active ? 'green' : 'gray'}
/>, // Status
'actions', // Actions
];
});
}, [units]);
}, [units, currentUser]);
const statusOptions = React.useMemo(
() => [
@ -201,9 +241,89 @@ const UnitMaster = () => {
return `${day}/${month}/${year}`;
};
const handleSaveForm = async () => {
// const handleSaveForm = async () => {
// const selectedProducts = Array.isArray(form.productsMapped) ? form.productsMapped : [];
// if (!form.unitName || !form.description || !form.status) {
// return;
// }
// const unitData = {
// uom: form.unitName,
// uom_short_name: form.description,
// is_active: form.status === 'Active',
// };
// try {
// setLoading(true);
// if (modalMode === 'edit' && editingRow !== null) {
// const updatedUnitData = {
// ...unitData,
// updated_at: new Date().toISOString(),
// updated_by: currentUser?.id || null,
// };
// const updatedUnit = await updateUnit(units[editingRow].id, updatedUnitData);
// setUnits(prev =>
// prev.map((item, index) =>
// index === editingRow
// ? {
// ...item,
// ...updatedUnit,
// updated_at: updatedUnitData.updated_at,
// created_at: item.created_at,
// created_by: item.created_by,
// created_by_name: item.created_by_name,
// }
// : item
// )
// );
// } else {
// const newUnit = await createUnit({
// ...unitData,
// created_at: new Date().toISOString(),
// created_by: currentUser?.id || null,
// created_by_name: currentUser?.name || '',
// });
// setUnits(prev => [newUnit, ...prev]);
// }
// await fetchUnits();
// closeModal();
// } catch (error) {
// console.error('Error saving unit:', error);
// } finally {
// setLoading(false);
// }
// };
const handleSave = async () => {
const selectedProducts = Array.isArray(form.productsMapped) ? form.productsMapped : [];
if (!form.unitName || !form.description || !form.status) {
// Validation checks
if (!form.unitName) {
toast.error('Please enter Unit Name.');
return;
}
if (!form.description) {
toast.error('Please enter Description.');
return;
}
if (!form.status) {
toast.error('Please select Status.');
return;
}
// Check for duplicate unit name
const nameExists = units.some(
unit => unit.uom?.toLowerCase().trim() === form.unitName.toLowerCase().trim()
);
// Only check duplicates when adding (not editing)
if (modalMode !== 'edit' && nameExists) {
toast.error('Unit name already exists!');
return;
}
@ -211,6 +331,7 @@ const handleSaveForm = async () => {
uom: form.unitName,
uom_short_name: form.description,
is_active: form.status === 'Active',
productsMapped: selectedProducts,
};
try {
@ -235,10 +356,13 @@ const handleSaveForm = async () => {
created_at: item.created_at,
created_by: item.created_by,
created_by_name: item.created_by_name,
productsMapped: selectedProducts,
}
: item
)
);
toast.success('Unit updated successfully!');
} else {
const newUnit = await createUnit({
...unitData,
@ -247,13 +371,22 @@ const handleSaveForm = async () => {
created_by_name: currentUser?.name || '',
});
setUnits(prev => [newUnit, ...prev]);
setUnits(prev => [
{
...newUnit,
productsMapped: selectedProducts,
},
...prev,
]);
toast.success('New unit added successfully!');
}
await fetchUnits();
closeModal();
} catch (error) {
console.error('Error saving unit:', error);
toast.error('Something went wrong while saving the unit.');
} finally {
setLoading(false);
}
@ -261,6 +394,10 @@ const handleSaveForm = async () => {
const deletingUnit = React.useMemo(() => {
if (deletingRow === null || deletingRow < 0 || deletingRow >= units.length) {
return null;
@ -289,10 +426,60 @@ const handleSaveForm = async () => {
<div className="px-6 py-3 flex items-center justify-between border-b border-[#E5E7EB]">
<h3 className="text-[16px] font-medium text-[#232528]">Unit Master</h3>
<div className="flex items-center gap-2">
<button className="h-10 px-4 rounded-[6px] bg-[#F7F7F7] border border-[#C3C6CB] text-sm inline-flex items-center gap-2 cursor-pointer">
{/* <button className="h-10 px-4 rounded-[6px] bg-[#F7F7F7] border border-[#C3C6CB] text-sm inline-flex items-center gap-2 cursor-pointer">
<img src={downloadIconSrc} alt="Export" className="h-5 w-5" />
<span className="font-medium text-[#232528]">Export CSV</span>
</button>
</button> */}
<button
type="button"
onClick={() => {
if (!filteredRows.length) return;
//Remove "Actions" column from header
const csvHeader = headers.slice(0, headers.length - 1).join(',');
//Map data fields based on Unit Master structure
const csvRows = filteredRows.map((item) =>
[
item.unitName,
item.description,
// (item.productsMapped || []).join('; '), // Mapped Products
item.mapped_products_count , // Mapped Products Count
item.createdBy,
item.createdOn,
item.status,
]
.map((value) => `"${String(value ?? '').replace(/"/g, '""')}"`)
.join(',')
);
// Create CSV file
const blob = new Blob([csvHeader + '\n' + csvRows.join('\n')], {
type: 'text/csv;charset=utf-8;',
});
// Download the file
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', 'unit-master.csv');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}}
className={`h-10 px-4 rounded-[6px] border text-sm inline-flex items-center gap-2 ${
filteredRows.length
? 'bg-[#F7F7F7] border-[#C3C6CB] text-[#232528] cursor-pointer'
: 'bg-[#F3F4F6] border-[#E5E7EB] text-[#9CA3AF] cursor-not-allowed'
}`}
disabled={!filteredRows.length}
>
<img src={downloadIconSrc} alt="Export" className="h-5 w-5" />
<span className="font-medium">Export CSV</span>
</button>
<button
type="button"
className="h-10 px-4 rounded-[6px] bg-[#92722A] text-white text-sm inline-flex items-center gap-2 cursor-pointer"
@ -385,13 +572,16 @@ const handleSaveForm = async () => {
<div className="px-6 py-5">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<TextField
label="Unit Name"
value={form.unitName}
onChange={handleFormChange('unitName')}
placeholder="Enter Unit Name"
width="100%"
/>
<TextField
label={
<>
Unit Name <span className="text-red-500">*</span>
</>
}
value={form.unitName}
onChange={handleFormChange('unitName')}
placeholder="Enter Unit Name"
/>
<TextField
label="Description"
value={form.description}
@ -445,14 +635,18 @@ const handleSaveForm = async () => {
</div>
)}
</div>
<SelectField
label="Status"
value={form.status}
onChange={handleFormChange('status')}
options={statusOptions}
placeholder="Select Status"
width="100%"
/>
<SelectField
label={
<>
Status <span className="text-red-500">*</span>
</>
}
value={form.status}
onChange={handleFormChange('status')}
options={statusOptions}
placeholder="Select Status"
/>
</div>
<div className="mt-6 flex items-center justify-end gap-3">
@ -463,19 +657,21 @@ const handleSaveForm = async () => {
>
Cancel
</button>
<button
type="button"
className="h-10 px-4 rounded-[6px] bg-[#92722A] text-white text-sm font-medium disabled:opacity-50"
onClick={handleSaveForm}
disabled={loading}
>
{loading ? (
<div className="flex items-center justify-center">
<div className="animate-spin rounded-full h-5 w-5 border-t-2 border-b-2 border-white mr-2"></div>
{modalMode === 'edit' ? 'Updating...' : 'Saving...'}
</div>
) : modalMode === 'edit' ? 'Update' : 'Save'}
</button>
<button
type="button"
className="h-10 px-4 rounded-[6px] bg-[#92722A] text-white text-sm font-medium disabled:opacity-50"
onClick={handleSave}
disabled={loading}
>
{loading ? (
<div className="flex items-center justify-center">
<div className="animate-spin rounded-full h-5 w-5 border-t-2 border-b-2 border-white mr-2"></div>
{modalMode === 'edit' ? 'Updating...' : 'Saving...'}
</div>
) : modalMode === 'edit' ? 'Update' : 'Save'}
</button>
</div>
</div>
</div>

View File

@ -257,6 +257,61 @@ const History = () => {
}));
};
const downloadCsv = () => {
if (!filteredRows.length) return;
// Define CSV headers
const headers = [
'Submission Date & Time',
'Year',
'Quarter',
'HS Code',
'Product',
'Status',
'Actors',
'Details'
];
// Helper function to safely get field values
const getFieldValue = (value) => {
if (value === null || value === undefined || value === '') return '-';
return String(value);
};
// Convert data to CSV rows
const csvRows = [
headers.join(','),
...filteredRows.map(row =>
[
getFieldValue(formatDateTime(row.created_at)),
getFieldValue(row.year),
getFieldValue(row.quarter),
getFieldValue(row.hs_code),
getFieldValue(row.product_name),
getFieldValue(formatStatus(row.status)),
(row.actors && row.actors.length ? row.actors.join(', ') : '-').replace(/"/g, '""'),
'Quantity and cost updates' // This is a placeholder for the details column
].map(field => `"${field}"`).join(',')
)
];
// Create CSV content
const csvContent = csvRows.join('\n');
// Create download link
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute('download', `submission-history-${new Date().toISOString().split('T')[0]}.csv`);
link.style.visibility = 'hidden';
// Trigger download
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
const tableRows = filteredRows.map((entry, index) => {
const isExpanded = expandedRow === index;
return [
@ -511,6 +566,38 @@ const History = () => {
</div>
<div className="bg-white w-full rounded-lg shadow-sm ring-1 ring-gray-200 mt-1">
<div className="px-6 pt-6 pb-4 flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<h2 className="text-[18px] font-medium text-[#232528]">Submission History</h2>
</div>
<div className="flex flex-col md:flex-row md:items-center gap-3">
<div className="relative w-full md:w-72">
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search establishment"
className="w-full h-10 rounded-md border border-[#E2E8F0] pl-10 pr-3 text-sm text-[#232528] focus:outline-none"
/>
<img
src={searchIconSrc}
alt="Search"
className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4"
/>
</div>
<button
type="button"
onClick={downloadCsv}
disabled={!filteredRows.length}
className={`inline-flex items-center gap-2 h-10 rounded-md border px-4 text-sm font-medium ${filteredRows.length ? 'border-[#92722A] text-[#92722A] hover:bg-[#F2ECCF]' : 'border-gray-200 text-gray-400 cursor-not-allowed'}`}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
<span>Export CSV</span>
</button>
</div>
</div>
{filteredRows.length === 0 && !loading ? (
<div className="flex flex-col items-center justify-center py-12">
<p className="text-gray-500 text-lg">No details found</p>