bug fixed
This commit is contained in:
parent
346790205a
commit
234f6c4794
@ -279,11 +279,11 @@ function App() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Route> */}
|
</Route> */}
|
||||||
<Route path="/profile/edit/:id" element={
|
{/* <Route path="/profile/edit/:id" element={
|
||||||
<RequireRole allowedRoles={['EstablishmentUser']}>
|
<RequireRole allowedRoles={['EstablishmentUser']}>
|
||||||
<EditCompanyProfile />
|
<EditCompanyProfile />
|
||||||
</RequireRole>
|
</RequireRole>
|
||||||
} />
|
} /> */}
|
||||||
{/* <Route
|
{/* <Route
|
||||||
path="/admin/configuration/profile/:id?"
|
path="/admin/configuration/profile/:id?"
|
||||||
element={
|
element={
|
||||||
@ -292,6 +292,14 @@ function App() {
|
|||||||
</RequireRole>
|
</RequireRole>
|
||||||
}
|
}
|
||||||
/> */}
|
/> */}
|
||||||
|
<Route
|
||||||
|
path="/edit-profile/:id"
|
||||||
|
element={
|
||||||
|
<RequireRole allowedRoles={['EstablishmentUser']}>
|
||||||
|
<EditCompanyProfile />
|
||||||
|
</RequireRole>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route path="/change-password" element={<ChangePassword />} />
|
<Route path="/change-password" element={<ChangePassword />} />
|
||||||
<Route path="/forgot-password" element={<ForgotPassword />} />
|
<Route path="/forgot-password" element={<ForgotPassword />} />
|
||||||
<Route path="/reset-password" element={<PublicContactForm />} />
|
<Route path="/reset-password" element={<PublicContactForm />} />
|
||||||
|
|||||||
@ -18,6 +18,7 @@ const HeaderBar = () => {
|
|||||||
const [userOpen, setUserOpen] = React.useState(false);
|
const [userOpen, setUserOpen] = React.useState(false);
|
||||||
const profileRef = React.useRef(null);
|
const profileRef = React.useRef(null);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const establishmentId = sessionStorage.getItem("establishment_id") || "";
|
||||||
|
|
||||||
// Close dropdown when clicking outside
|
// Close dropdown when clicking outside
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@ -130,6 +131,37 @@ const HeaderBar = () => {
|
|||||||
<span>Overview</span>
|
<span>Overview</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</NavLink>
|
||||||
|
<NavLink
|
||||||
|
to={`/edit-profile/${establishmentId}`}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
`inline-flex items-center gap-2 pb-1 border-b-2 ${
|
||||||
|
isActive
|
||||||
|
? "text-[#92722A] font-medium border-[#92722A]"
|
||||||
|
: "text-[#232528] hover:text-gray-900 border-transparent"
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{({ isActive }) => (
|
||||||
|
<>
|
||||||
|
<svg
|
||||||
|
className={`h-[16px] w-[18px] ${
|
||||||
|
isActive ? "text-[#92722A]" : "text-[#232528]"
|
||||||
|
}`}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>Profile</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
{/* <NavLink
|
{/* <NavLink
|
||||||
to="/history"
|
to="/history"
|
||||||
|
|||||||
@ -236,6 +236,19 @@ const EstablishmentInfo = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// const handleEditProfile = (e) => {
|
||||||
|
// e.preventDefault();
|
||||||
|
|
||||||
|
// const establishmentId = sessionStorage.getItem('establishment_id');
|
||||||
|
// if (!establishmentId) {
|
||||||
|
// alert('No establishment ID found in session');
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// localStorage.setItem('returnTo', window.location.pathname);
|
||||||
|
// sessionStorage.setItem('edit_profile_from', 'EstablishmentUser');
|
||||||
|
// // navigate(`/profile/edit/${establishmentId}`);
|
||||||
|
// };
|
||||||
const handleEditProfile = (e) => {
|
const handleEditProfile = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@ -247,8 +260,9 @@ const EstablishmentInfo = ({
|
|||||||
|
|
||||||
localStorage.setItem('returnTo', window.location.pathname);
|
localStorage.setItem('returnTo', window.location.pathname);
|
||||||
sessionStorage.setItem('edit_profile_from', 'EstablishmentUser');
|
sessionStorage.setItem('edit_profile_from', 'EstablishmentUser');
|
||||||
navigate(`/profile/edit/${establishmentId}`);
|
navigate(`/edit-profile/${establishmentId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEmployeeChange = (field) => (event) => {
|
const handleEmployeeChange = (field) => (event) => {
|
||||||
onChange({
|
onChange({
|
||||||
...info,
|
...info,
|
||||||
@ -375,7 +389,7 @@ const EstablishmentInfo = ({
|
|||||||
here
|
here
|
||||||
</a>.
|
</a>.
|
||||||
</p> */}
|
</p> */}
|
||||||
<p className="text-sm text-[#043DFF] bg-[#E7F5FF]">
|
{/* <p className="text-sm text-[#043DFF] bg-[#E7F5FF]">
|
||||||
<span className="font-medium">Need to update details?</span> Go to Profile →{' '}
|
<span className="font-medium">Need to update details?</span> Go to Profile →{' '}
|
||||||
<a
|
<a
|
||||||
href={`/admin/configuration/profile/edit=${sessionStorage.getItem('establishment_id') || ''}`}
|
href={`/admin/configuration/profile/edit=${sessionStorage.getItem('establishment_id') || ''}`}
|
||||||
@ -386,6 +400,16 @@ const EstablishmentInfo = ({
|
|||||||
Edit Profile
|
Edit Profile
|
||||||
</a>
|
</a>
|
||||||
. Changes saved there will appear here.
|
. Changes saved there will appear here.
|
||||||
|
</p> */}
|
||||||
|
<p className="text-sm text-[#043DFF] bg-[#E7F5FF]">
|
||||||
|
<span className="font-medium">Need to update details?</span> Go to Profile →{' '}
|
||||||
|
<a
|
||||||
|
href={`/admin/configuration/edit-profile/${sessionStorage.getItem('establishment_id') || ''}`}
|
||||||
|
className="underline font-medium text-[#043DFF] hover:text-[#063B82]"
|
||||||
|
onClick={handleEditProfile}
|
||||||
|
>
|
||||||
|
Edit Profile
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
|||||||
@ -15,6 +15,8 @@ const AddAdminUsers = ({ isOpen, onClose, onSave }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [errors, setErrors] = useState({});
|
const [errors, setErrors] = useState({});
|
||||||
|
const [passwordError, setPasswordError] = useState('');
|
||||||
|
const [passwordTouched, setPasswordTouched] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [toastData, setToastData] = useState(null); // 👈 for custom toast
|
const [toastData, setToastData] = useState(null); // 👈 for custom toast
|
||||||
|
|
||||||
@ -41,16 +43,14 @@ const AddAdminUsers = ({ isOpen, onClose, onSave }) => {
|
|||||||
|
|
||||||
if (!formData.status) newErrors.status = 'Status is required';
|
if (!formData.status) newErrors.status = 'Status is required';
|
||||||
|
|
||||||
if (!formData.password) {
|
const passwordValidation = validatePassword(formData.password);
|
||||||
newErrors.password = 'Password is required';
|
if (passwordValidation) {
|
||||||
} else if (formData.password.length < 8) {
|
newErrors.password = passwordValidation;
|
||||||
newErrors.password = 'Password must be at least 8 characters';
|
|
||||||
} else if (formData.password.length > 12) {
|
|
||||||
newErrors.password = 'Password cannot exceed 12 characters';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (formData.password !== formData.confirmPassword)
|
if (formData.password !== formData.confirmPassword) {
|
||||||
newErrors.confirmPassword = 'Passwords do not match';
|
newErrors.confirmPassword = 'Passwords do not match';
|
||||||
|
}
|
||||||
|
|
||||||
return newErrors;
|
return newErrors;
|
||||||
};
|
};
|
||||||
@ -70,10 +70,38 @@ const AddAdminUsers = ({ isOpen, onClose, onSave }) => {
|
|||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const validatePassword = (password) => {
|
||||||
|
if (!password) return 'Password is required';
|
||||||
|
if (password.length < 8) return 'Password must be at least 8 characters';
|
||||||
|
if (!/[A-Z]/.test(password)) return 'Must contain at least one uppercase letter';
|
||||||
|
if (!/[a-z]/.test(password)) return 'Must contain at least one lowercase letter';
|
||||||
|
if (!/\d/.test(password)) return 'Must contain at least one number';
|
||||||
|
if (!/[!@#$%^&*]/.test(password)) return 'Must contain at least one special character (!@#$%^&*)';
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
const handleChange = (e) => {
|
const handleChange = (e) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||||
|
|
||||||
|
// Clear any existing error for this field
|
||||||
if (errors[name]) setErrors((prev) => ({ ...prev, [name]: '' }));
|
if (errors[name]) setErrors((prev) => ({ ...prev, [name]: '' }));
|
||||||
|
|
||||||
|
// Real-time password validation
|
||||||
|
if (name === 'password') {
|
||||||
|
setPasswordTouched(true);
|
||||||
|
const error = validatePassword(value);
|
||||||
|
setPasswordError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear confirm password error when either password changes
|
||||||
|
if ((name === 'password' || name === 'confirmPassword') && formData.password && formData.confirmPassword) {
|
||||||
|
if (formData.password !== formData.confirmPassword) {
|
||||||
|
setErrors(prev => ({ ...prev, confirmPassword: 'Passwords do not match' }));
|
||||||
|
} else {
|
||||||
|
setErrors(prev => ({ ...prev, confirmPassword: '' }));
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async (e) => {
|
||||||
@ -156,18 +184,22 @@ const AddAdminUsers = ({ isOpen, onClose, onSave }) => {
|
|||||||
placeholder="Enter email"
|
placeholder="Enter email"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
<TextField
|
<TextField
|
||||||
label="Password"
|
label="Password"
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
value={formData.password}
|
value={formData.password}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
error={errors.password}
|
onBlur={() => setPasswordTouched(true)}
|
||||||
|
error={passwordTouched && passwordError ? passwordError : ''}
|
||||||
required
|
required
|
||||||
placeholder="Enter password"
|
placeholder="Enter password"
|
||||||
showToggle
|
showToggle
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
<TextField
|
<TextField
|
||||||
label="Confirm Password"
|
label="Confirm Password"
|
||||||
name="confirmPassword"
|
name="confirmPassword"
|
||||||
@ -179,6 +211,10 @@ const AddAdminUsers = ({ isOpen, onClose, onSave }) => {
|
|||||||
placeholder="Confirm password"
|
placeholder="Confirm password"
|
||||||
showToggle
|
showToggle
|
||||||
/>
|
/>
|
||||||
|
{formData.confirmPassword && !errors.confirmPassword && (
|
||||||
|
<p className="mt-1 text-xs text-green-600">Passwords match</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="md:col-span-2 space-y-2">
|
<div className="md:col-span-2 space-y-2">
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
|||||||
@ -3,7 +3,8 @@ import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
|||||||
import { TextField, SelectField, PhoneField } from "@/components/common/FormControls";
|
import { TextField, SelectField, PhoneField } from "@/components/common/FormControls";
|
||||||
import Loader from "@/components/common/Loader";
|
import Loader from "@/components/common/Loader";
|
||||||
import { fetchEstablishmentDetail, updateEstablishment } from '@/services/establishments/establishmentService';
|
import { fetchEstablishmentDetail, updateEstablishment } from '@/services/establishments/establishmentService';
|
||||||
import { fetchEmirates, fetchCityTowns } from '@/services/masters/masterService';
|
import { fetchEmirates, fetchCityTowns, fetchProducts } from '@/services/masters/masterService';
|
||||||
|
import HeaderBar from "@/components/layout/HeaderBar";
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
const createEmptyProfile = () => ({
|
const createEmptyProfile = () => ({
|
||||||
@ -26,20 +27,6 @@ const createEmptyProfile = () => ({
|
|||||||
contactPostalCode: '',
|
contactPostalCode: '',
|
||||||
contactPoBox: '',
|
contactPoBox: '',
|
||||||
contactWebsite: '',
|
contactWebsite: '',
|
||||||
corporateName: '',
|
|
||||||
corporateAddress: '',
|
|
||||||
corporateCityTown: '',
|
|
||||||
corporateCityTownId: '',
|
|
||||||
corporateEmirate: '',
|
|
||||||
corporateEmirateId: '',
|
|
||||||
corporateMakaniNumber: '',
|
|
||||||
corporateContactPersonName: '',
|
|
||||||
corporateMobileNumber: '',
|
|
||||||
corporateEmail: '',
|
|
||||||
corporatePostalCode: '',
|
|
||||||
corporatePoBox: '',
|
|
||||||
corporateWebsite: '',
|
|
||||||
corporateSameAs: false,
|
|
||||||
employmentEmiratiMale: 0,
|
employmentEmiratiMale: 0,
|
||||||
employmentEmiratiFemale: 0,
|
employmentEmiratiFemale: 0,
|
||||||
employmentNonEmiratiMale: 0,
|
employmentNonEmiratiMale: 0,
|
||||||
@ -69,20 +56,6 @@ const mapApiEstablishmentToProfile = (apiData) => {
|
|||||||
contactPostalCode: data.establishment_postal_code || '',
|
contactPostalCode: data.establishment_postal_code || '',
|
||||||
contactPoBox: data.establishment_po_box || '',
|
contactPoBox: data.establishment_po_box || '',
|
||||||
contactWebsite: data.establishment_website || '',
|
contactWebsite: data.establishment_website || '',
|
||||||
corporateName: data.corporate_name || '',
|
|
||||||
corporateAddress: data.corporate_address || '',
|
|
||||||
corporateCityTown: data.corporate_city?.name || '',
|
|
||||||
corporateCityTownId: data.corporate_city?.id || '',
|
|
||||||
corporateEmirate: data.corporate_emirate?.name || '',
|
|
||||||
corporateEmirateId: data.corporate_emirate?.id || '',
|
|
||||||
corporateMakaniNumber: data.corporate_makani_number || '',
|
|
||||||
corporateContactPersonName: data.corporate_contact_person_name || '',
|
|
||||||
corporateMobileNumber: data.corporate_mobile_number || '',
|
|
||||||
corporateEmail: data.corporate_email || '',
|
|
||||||
corporatePostalCode: data.corporate_postal_code || '',
|
|
||||||
corporatePoBox: data.corporate_po_box || '',
|
|
||||||
corporateWebsite: data.corporate_website || '',
|
|
||||||
corporateSameAs: data.corporate_same_as_establishment || false,
|
|
||||||
employmentEmiratiMale: data.emirati_male || 0,
|
employmentEmiratiMale: data.emirati_male || 0,
|
||||||
employmentEmiratiFemale: data.emirati_female || 0,
|
employmentEmiratiFemale: data.emirati_female || 0,
|
||||||
employmentNonEmiratiMale: data.non_emirati_male || 0,
|
employmentNonEmiratiMale: data.non_emirati_male || 0,
|
||||||
@ -90,22 +63,6 @@ const mapApiEstablishmentToProfile = (apiData) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const contactToCorporateMap = {
|
|
||||||
contactName: 'corporateName',
|
|
||||||
contactAddress: 'corporateAddress',
|
|
||||||
contactCityTown: 'corporateCityTown',
|
|
||||||
contactEmirate: 'corporateEmirate',
|
|
||||||
contactCityTownId: 'corporateCityTownId',
|
|
||||||
contactEmirateId: 'corporateEmirateId',
|
|
||||||
contactMakaniNumber: 'corporateMakaniNumber',
|
|
||||||
contactPersonName: 'corporateContactPersonName',
|
|
||||||
contactMobileNumber: 'corporateMobileNumber',
|
|
||||||
contactEmail: 'corporateEmail',
|
|
||||||
contactPostalCode: 'corporatePostalCode',
|
|
||||||
contactPoBox: 'corporatePoBox',
|
|
||||||
contactWebsite: 'corporateWebsite',
|
|
||||||
};
|
|
||||||
|
|
||||||
const computeEmploymentTotals = (data) => {
|
const computeEmploymentTotals = (data) => {
|
||||||
const emiratiMale = parseInt(data.employmentEmiratiMale) || 0;
|
const emiratiMale = parseInt(data.employmentEmiratiMale) || 0;
|
||||||
const nonEmiratiMale = parseInt(data.employmentNonEmiratiMale) || 0;
|
const nonEmiratiMale = parseInt(data.employmentNonEmiratiMale) || 0;
|
||||||
@ -121,18 +78,11 @@ const computeEmploymentTotals = (data) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const captureCorporateValues = (data) => {
|
const EditCompanyProfile = () => {
|
||||||
const corporateData = {};
|
|
||||||
Object.values(contactToCorporateMap).forEach(key => {
|
|
||||||
corporateData[key] = data[key];
|
|
||||||
});
|
|
||||||
return corporateData;
|
|
||||||
};
|
|
||||||
|
|
||||||
const EditCompanyProfile = ({ onClose: propOnClose }) => {
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { id: establishmentId } = useParams();
|
const { id: establishmentId } = useParams();
|
||||||
const onClose = propOnClose || (() => navigate('/survey'));
|
console.log('EditCompanyProfile mounted, establishmentId from params:', establishmentId);
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [form, setForm] = useState(createEmptyProfile());
|
const [form, setForm] = useState(createEmptyProfile());
|
||||||
const [activeStep, setActiveStep] = useState(0);
|
const [activeStep, setActiveStep] = useState(0);
|
||||||
@ -144,16 +94,18 @@ const EditCompanyProfile = ({ onClose: propOnClose }) => {
|
|||||||
const [emirateOptions, setEmirateOptions] = useState([]);
|
const [emirateOptions, setEmirateOptions] = useState([]);
|
||||||
const [emirateLookup, setEmirateLookup] = useState({});
|
const [emirateLookup, setEmirateLookup] = useState({});
|
||||||
const [contactCityOptions, setContactCityOptions] = useState([]);
|
const [contactCityOptions, setContactCityOptions] = useState([]);
|
||||||
const [corporateCityOptions, setCorporateCityOptions] = useState([]);
|
const [availableProducts, setAvailableProducts] = useState([]);
|
||||||
|
const [selectedProducts, setSelectedProducts] = useState([]);
|
||||||
|
const [productSearchTerm, setProductSearchTerm] = useState('');
|
||||||
|
const [isLoadingProducts, setIsLoadingProducts] = useState(false);
|
||||||
const initialSnapshotRef = useRef(null);
|
const initialSnapshotRef = useRef(null);
|
||||||
const corporateBackupRef = useRef(null);
|
|
||||||
|
|
||||||
const formSteps = React.useMemo(
|
const formSteps = React.useMemo(
|
||||||
() => [
|
() => [
|
||||||
{ label: 'User Profile' },
|
{ label: 'User Profile' },
|
||||||
{ label: 'Identification Particulars' },
|
{ label: 'Identification Particulars' },
|
||||||
{ label: 'Establishment Contact Details' },
|
{ label: 'Establishment Contact Details' },
|
||||||
{ label: 'Corporate / Head Office Contact' },
|
{ label: 'Products' },
|
||||||
{ label: 'Employment in Establishment' },
|
{ label: 'Employment in Establishment' },
|
||||||
],
|
],
|
||||||
[]
|
[]
|
||||||
@ -182,14 +134,7 @@ const EditCompanyProfile = ({ onClose: propOnClose }) => {
|
|||||||
{ field: 'contactEmail', label: 'Email' },
|
{ field: 'contactEmail', label: 'Email' },
|
||||||
],
|
],
|
||||||
3: [
|
3: [
|
||||||
{ field: 'corporateName', label: 'Name' },
|
// Products are optional, no required fields
|
||||||
{ field: 'corporateAddress', label: 'Address' },
|
|
||||||
{ field: 'corporateCityTown', label: 'City/Town' },
|
|
||||||
{ field: 'corporateEmirate', label: 'Emirate' },
|
|
||||||
{ field: 'corporateMakaniNumber', label: 'Makani Number' },
|
|
||||||
{ field: 'corporateContactPersonName', label: 'Contact Person Name' },
|
|
||||||
{ field: 'corporateMobileNumber', label: 'Mobile Number' },
|
|
||||||
{ field: 'corporateEmail', label: 'Email' },
|
|
||||||
],
|
],
|
||||||
4: [
|
4: [
|
||||||
{ field: 'employmentEmiratiMale', label: 'Number of Emirati Male' },
|
{ field: 'employmentEmiratiMale', label: 'Number of Emirati Male' },
|
||||||
@ -231,16 +176,6 @@ const EditCompanyProfile = ({ onClose: propOnClose }) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field === 'corporateCityTown') {
|
|
||||||
const foundCity = corporateCityOptions.find(option => option.value === value || option.label === value);
|
|
||||||
setForm(prev => ({
|
|
||||||
...prev,
|
|
||||||
corporateCityTown: foundCity?.name || '',
|
|
||||||
corporateCityTownId: foundCity?.id || ''
|
|
||||||
}));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setForm((prev) => ({ ...prev, [field]: value }));
|
setForm((prev) => ({ ...prev, [field]: value }));
|
||||||
setFieldErrors((prev) => ({ ...prev, [field]: "" }));
|
setFieldErrors((prev) => ({ ...prev, [field]: "" }));
|
||||||
|
|
||||||
@ -251,7 +186,7 @@ const EditCompanyProfile = ({ onClose: propOnClose }) => {
|
|||||||
if (field === 'userProfileConfirmPassword') {
|
if (field === 'userProfileConfirmPassword') {
|
||||||
setConfirmError("");
|
setConfirmError("");
|
||||||
}
|
}
|
||||||
}, [contactCityOptions, corporateCityOptions]);
|
}, [contactCityOptions]);
|
||||||
|
|
||||||
const handleNext = useCallback(() => {
|
const handleNext = useCallback(() => {
|
||||||
if (validateStepFields(activeStep)) {
|
if (validateStepFields(activeStep)) {
|
||||||
@ -292,15 +227,108 @@ const EditCompanyProfile = ({ onClose: propOnClose }) => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Load products
|
||||||
|
const loadProducts = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setIsLoadingProducts(true);
|
||||||
|
const response = await fetchProducts();
|
||||||
|
|
||||||
|
let productsData = [];
|
||||||
|
|
||||||
|
if (Array.isArray(response)) {
|
||||||
|
productsData = response;
|
||||||
|
} else if (response?.data) {
|
||||||
|
productsData = Array.isArray(response.data) ? response.data : response.data.products || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Processed products data:', productsData);
|
||||||
|
|
||||||
|
// Format products data
|
||||||
|
const formattedProducts = productsData.map(p => ({
|
||||||
|
id: p.id || p.value,
|
||||||
|
product_id: p.id || p.value,
|
||||||
|
hs_code: p.hs_code || p.hsCode || '',
|
||||||
|
hsCode: p.hs_code || p.hsCode || '',
|
||||||
|
product_name: p.product_name || p.productName || p.label || '',
|
||||||
|
productName: p.product_name || p.productName || p.label || '',
|
||||||
|
label: p.product_name || p.productName || p.label || '',
|
||||||
|
value: p.id || p.value
|
||||||
|
}));
|
||||||
|
|
||||||
|
setAvailableProducts(formattedProducts);
|
||||||
|
return formattedProducts;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading products:', error);
|
||||||
|
return [];
|
||||||
|
} finally {
|
||||||
|
setIsLoadingProducts(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Product search handler
|
||||||
|
const handleProductSearch = (e) => {
|
||||||
|
const searchTerm = e.target.value.toLowerCase();
|
||||||
|
setProductSearchTerm(searchTerm);
|
||||||
|
|
||||||
|
if (!searchTerm) {
|
||||||
|
setAvailableProducts(prev => prev.filter(p =>
|
||||||
|
!selectedProducts.some(sp => sp.id === p.id)
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filtered = availableProducts.filter(product =>
|
||||||
|
(product.label?.toLowerCase().includes(searchTerm) ||
|
||||||
|
product.hs_code?.toLowerCase().includes(searchTerm)) &&
|
||||||
|
!selectedProducts.some(sp => sp.id === product.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
setAvailableProducts(filtered);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add product handler
|
||||||
|
const handleAddProduct = (product) => {
|
||||||
|
setSelectedProducts(prev => {
|
||||||
|
const updated = [...prev, product];
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove from available products
|
||||||
|
setAvailableProducts(prev => prev.filter(p => p.id !== product.id));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove product handler
|
||||||
|
const handleRemoveProduct = (product) => {
|
||||||
|
setSelectedProducts(prev => prev.filter(p => p.id !== product.id));
|
||||||
|
|
||||||
|
// Add back to available products if not already there and matches search
|
||||||
|
if (!productSearchTerm ||
|
||||||
|
(product.hs_code && product.hs_code.toLowerCase().includes(productSearchTerm)) ||
|
||||||
|
(product.product_name && product.product_name.toLowerCase().includes(productSearchTerm))) {
|
||||||
|
setAvailableProducts(prev => {
|
||||||
|
if (!prev.some(p => p.id === product.id)) {
|
||||||
|
return [...prev, product];
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log('useEffect triggered, establishmentId:', establishmentId);
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
if (!establishmentId) {
|
if (!establishmentId) {
|
||||||
|
console.log('No establishmentId found, skipping fetch');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
console.log('Starting API call to fetch establishment details for ID:', establishmentId);
|
||||||
|
|
||||||
const response = await fetchEstablishmentDetail(establishmentId);
|
const response = await fetchEstablishmentDetail(establishmentId);
|
||||||
|
console.log('API Response received:', response);
|
||||||
|
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Empty response received');
|
console.error('Empty response received');
|
||||||
@ -308,26 +336,49 @@ useEffect(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mappedProfile = mapApiEstablishmentToProfile(response);
|
const mappedProfile = mapApiEstablishmentToProfile(response);
|
||||||
|
console.log('Mapped Profile:', mappedProfile);
|
||||||
|
|
||||||
const formData = {
|
const formData = {
|
||||||
...createEmptyProfile(),
|
...createEmptyProfile(),
|
||||||
...mappedProfile,
|
...mappedProfile,
|
||||||
apiId: establishmentId,
|
apiId: establishmentId
|
||||||
corporateSameAs: Boolean(mappedProfile.corporateSameAs)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (formData.corporateSameAs) {
|
|
||||||
Object.entries(contactToCorporateMap).forEach(([source, target]) => {
|
|
||||||
formData[target] = formData[source];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const totals = computeEmploymentTotals(formData);
|
const totals = computeEmploymentTotals(formData);
|
||||||
Object.assign(formData, totals);
|
Object.assign(formData, totals);
|
||||||
|
|
||||||
|
console.log('Final Form Data:', formData);
|
||||||
|
|
||||||
setForm(formData);
|
setForm(formData);
|
||||||
initialSnapshotRef.current = formData;
|
initialSnapshotRef.current = formData;
|
||||||
corporateBackupRef.current = captureCorporateValues(formData);
|
|
||||||
|
// Load products and set selected products
|
||||||
|
const allProducts = await loadProducts();
|
||||||
|
|
||||||
|
// Set selected products from API response
|
||||||
|
const establishmentProducts = response.data?.establishment_products || response.establishment_products || [];
|
||||||
|
if (Array.isArray(establishmentProducts) && establishmentProducts.length > 0) {
|
||||||
|
const formattedSelectedProducts = establishmentProducts.map(ep => {
|
||||||
|
const productData = ep.product || {};
|
||||||
|
return {
|
||||||
|
id: ep.id || 0,
|
||||||
|
product_id: ep.product_id || 0,
|
||||||
|
hs_code: productData.hs_code || ep.hs_code || '',
|
||||||
|
hsCode: productData.hs_code || ep.hs_code || '',
|
||||||
|
product_name: productData.product_name || ep.product_name || 'Unnamed Product',
|
||||||
|
productName: productData.product_name || ep.product_name || 'Unnamed Product',
|
||||||
|
label: `${productData.product_name || ep.product_name || 'Unnamed Product'}${productData.hs_code ? ` (${productData.hs_code})` : ''}`,
|
||||||
|
value: ep.product_id ? String(ep.product_id) : '',
|
||||||
|
...productData
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setSelectedProducts(formattedSelectedProducts);
|
||||||
|
|
||||||
|
// Remove selected products from available products
|
||||||
|
setAvailableProducts(prev =>
|
||||||
|
prev.filter(p => !formattedSelectedProducts.some(sp => sp.id === p.id))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load establishment details:', error);
|
console.error('Failed to load establishment details:', error);
|
||||||
@ -338,7 +389,7 @@ useEffect(() => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [establishmentId]);
|
}, [establishmentId, loadProducts]);
|
||||||
|
|
||||||
// Load emirates on component mount
|
// Load emirates on component mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -391,19 +442,6 @@ useEffect(() => {
|
|||||||
loadContactCities();
|
loadContactCities();
|
||||||
}, [form.contactEmirateId, loadCities]);
|
}, [form.contactEmirateId, loadCities]);
|
||||||
|
|
||||||
// Load corporate cities when emirate ID changes
|
|
||||||
useEffect(() => {
|
|
||||||
const loadCorporateCities = async () => {
|
|
||||||
if (form.corporateEmirateId) {
|
|
||||||
await loadCities(form.corporateEmirateId, setCorporateCityOptions);
|
|
||||||
} else {
|
|
||||||
setCorporateCityOptions([]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadCorporateCities();
|
|
||||||
}, [form.corporateEmirateId, loadCities]);
|
|
||||||
|
|
||||||
// Calculate totals when employment numbers change
|
// Calculate totals when employment numbers change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const totals = computeEmploymentTotals(form);
|
const totals = computeEmploymentTotals(form);
|
||||||
@ -418,34 +456,9 @@ useEffect(() => {
|
|||||||
form.employmentNonEmiratiFemale
|
form.employmentNonEmiratiFemale
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Handle corporate same as establishment checkbox
|
|
||||||
const handleCorporateSameAsChange = useCallback((checked) => {
|
|
||||||
if (checked) {
|
|
||||||
// Save current corporate values before overwriting
|
|
||||||
corporateBackupRef.current = captureCorporateValues(form);
|
|
||||||
|
|
||||||
// Copy contact details to corporate
|
|
||||||
const corporateUpdates = { corporateSameAs: true };
|
|
||||||
Object.entries(contactToCorporateMap).forEach(([source, target]) => {
|
|
||||||
corporateUpdates[target] = form[source];
|
|
||||||
});
|
|
||||||
|
|
||||||
setForm(prev => ({
|
|
||||||
...prev,
|
|
||||||
...corporateUpdates
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
// Restore original corporate values
|
|
||||||
setForm(prev => ({
|
|
||||||
...prev,
|
|
||||||
corporateSameAs: false,
|
|
||||||
...(corporateBackupRef.current || {})
|
|
||||||
}));
|
|
||||||
corporateBackupRef.current = null;
|
|
||||||
}
|
|
||||||
}, [form]);
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
|
console.log("Submitted data:", form);
|
||||||
|
console.log("Selected products:", selectedProducts);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -461,8 +474,6 @@ useEffect(() => {
|
|||||||
establishment_address: form.contactAddress || "",
|
establishment_address: form.contactAddress || "",
|
||||||
establishment_city_town_id: form.contactCityTownId ? Number(form.contactCityTownId) : null,
|
establishment_city_town_id: form.contactCityTownId ? Number(form.contactCityTownId) : null,
|
||||||
establishment_emirate_id: form.contactEmirateId ? Number(form.contactEmirateId) : null,
|
establishment_emirate_id: form.contactEmirateId ? Number(form.contactEmirateId) : null,
|
||||||
corporate_city_town_id: form.corporateCityTownId ? Number(form.corporateCityTownId) : null,
|
|
||||||
corporate_emirate_id: form.corporateEmirateId ? Number(form.corporateEmirateId) : null,
|
|
||||||
establishment_postal_code: form.contactPostalCode || "",
|
establishment_postal_code: form.contactPostalCode || "",
|
||||||
establishment_po_box: form.contactPoBox || "",
|
establishment_po_box: form.contactPoBox || "",
|
||||||
establishment_makani_number: form.contactMakaniNumber || "",
|
establishment_makani_number: form.contactMakaniNumber || "",
|
||||||
@ -471,17 +482,6 @@ useEffect(() => {
|
|||||||
establishment_mobile_number: form.contactMobileNumber || "",
|
establishment_mobile_number: form.contactMobileNumber || "",
|
||||||
establishment_contact_email: form.contactEmail || "",
|
establishment_contact_email: form.contactEmail || "",
|
||||||
establishment_website: form.contactWebsite || "",
|
establishment_website: form.contactWebsite || "",
|
||||||
corporate_same_as_establishment: Boolean(form.corporateSameAs),
|
|
||||||
corporate_name: form.corporateName || "",
|
|
||||||
corporate_address: form.corporateAddress || "",
|
|
||||||
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_male: Number(form.employmentEmiratiMale) || 0,
|
||||||
emirati_female: Number(form.employmentEmiratiFemale) || 0,
|
emirati_female: Number(form.employmentEmiratiFemale) || 0,
|
||||||
non_emirati_male: Number(form.employmentNonEmiratiMale) || 0,
|
non_emirati_male: Number(form.employmentNonEmiratiMale) || 0,
|
||||||
@ -495,12 +495,20 @@ useEffect(() => {
|
|||||||
(Number(form.employmentNonEmiratiMale) || 0) +
|
(Number(form.employmentNonEmiratiMale) || 0) +
|
||||||
(Number(form.employmentNonEmiratiFemale) || 0),
|
(Number(form.employmentNonEmiratiFemale) || 0),
|
||||||
updated_by: 1,
|
updated_by: 1,
|
||||||
|
establishment_products: Array.isArray(selectedProducts) && selectedProducts.length > 0
|
||||||
|
? selectedProducts.map(p => ({
|
||||||
|
product_id: p.product_id || p.id || 0
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log("Payload sent:", establishmentData);
|
||||||
|
|
||||||
if (establishmentId) {
|
if (establishmentId) {
|
||||||
|
console.log("Updating establishment with ID:", establishmentId);
|
||||||
await updateEstablishment(establishmentId, establishmentData);
|
await updateEstablishment(establishmentId, establishmentData);
|
||||||
onClose();
|
console.log("Profile updated successfully!");
|
||||||
|
navigate('/survey');
|
||||||
} else {
|
} else {
|
||||||
console.error("No establishment ID provided for update");
|
console.error("No establishment ID provided for update");
|
||||||
alert("Error: No establishment ID provided for update");
|
alert("Error: No establishment ID provided for update");
|
||||||
@ -760,184 +768,106 @@ useEffect(() => {
|
|||||||
);
|
);
|
||||||
case 3:
|
case 3:
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-6">
|
||||||
<div className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
|
<h4 className="text-[16px] font-semibold text-[#232528]">Products</h4>
|
||||||
<div>
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
<h4 className="text-[16px] font-semibold text-[#232528]">Corporate/Head Office Contact</h4>
|
{/* Available Products Panel */}
|
||||||
|
<div className="bg-white rounded-lg border border-[#E5E7EB] overflow-hidden">
|
||||||
|
<div className="bg-[#F9FAFB] px-4 py-3 border-b border-[#E5E7EB]">
|
||||||
|
<h5 className="text-sm font-medium text-[#111827]">Available Products</h5>
|
||||||
</div>
|
</div>
|
||||||
<label className="inline-flex items-center gap-2 text-sm text-[#232528]">
|
<div className="p-4">
|
||||||
|
<div className="relative mb-4">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="text"
|
||||||
checked={form.corporateSameAs}
|
placeholder="Search products..."
|
||||||
onChange={(e) => handleCorporateSameAsChange(e.target.checked)}
|
value={productSearchTerm}
|
||||||
className="h-4 w-4 rounded border-2 border-[#CBA344] text-[#92722A] focus:ring-[#92722A]"
|
onChange={handleProductSearch}
|
||||||
|
className="w-full px-3 py-2 border border-[#D1D5DB] rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-[#92722A] focus:border-transparent"
|
||||||
/>
|
/>
|
||||||
Same as Establishment Contact
|
<svg
|
||||||
</label>
|
className="absolute right-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="border border-[#E5E7EB] rounded-md overflow-hidden">
|
||||||
<TextField
|
{isLoadingProducts ? (
|
||||||
label="Name"
|
<div className="flex justify-center items-center p-4">
|
||||||
value={form.corporateName || ""}
|
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-[#92722A]"></div>
|
||||||
onChange={(e) => handleFormChange("corporateName", e.target.value)}
|
</div>
|
||||||
placeholder="Enter Name"
|
) : availableProducts.length > 0 ? (
|
||||||
width="100%"
|
<ul className="divide-y divide-[#E5E7EB] max-h-96 overflow-y-auto">
|
||||||
required
|
{availableProducts.map((product) => (
|
||||||
disabled={form.corporateSameAs}
|
<li key={product.id} className="p-3 hover:bg-[#F9FAFB] flex justify-between items-center">
|
||||||
error={fieldErrors.corporateName}
|
<div>
|
||||||
/>
|
<div className="text-sm font-medium text-[#111827]">
|
||||||
<TextField
|
{product.hsCode || ''}
|
||||||
label="Address"
|
{product.hsCode && (product.productName || product.label || product.product_name) ? ' - ' : ''}
|
||||||
value={form.corporateAddress || ""}
|
{product.productName || product.label || product.product_name || ''}
|
||||||
onChange={(e) => handleFormChange("corporateAddress", e.target.value)}
|
</div>
|
||||||
placeholder="Enter Address"
|
</div>
|
||||||
width="100%"
|
<button
|
||||||
required
|
type="button"
|
||||||
disabled={form.corporateSameAs}
|
onClick={() => handleAddProduct(product)}
|
||||||
error={fieldErrors.corporateAddress}
|
className="text-[#92722A] hover:text-[#7A5F1E] text-sm font-medium"
|
||||||
/>
|
>
|
||||||
{form.corporateSameAs ? (
|
Add
|
||||||
<>
|
</button>
|
||||||
<TextField
|
</li>
|
||||||
label="Emirate"
|
))}
|
||||||
value={form.contactEmirate || ""}
|
</ul>
|
||||||
disabled={true}
|
|
||||||
width="100%"
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
label="City/Town"
|
|
||||||
value={form.contactCityTown || ""}
|
|
||||||
disabled={true}
|
|
||||||
width="100%"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<div className="p-4 text-center text-sm text-[#6B7280]">
|
||||||
<SelectField
|
{productSearchTerm ? 'No matching products found' : 'No products available'}
|
||||||
label="Emirate"
|
</div>
|
||||||
name="corporateEmirate"
|
|
||||||
value={form.corporateEmirateId || ""}
|
|
||||||
onChange={async (e) => {
|
|
||||||
const selectedValue = e.target.value;
|
|
||||||
const selectedEmirate = emirateOptions.find(opt => opt.value === selectedValue);
|
|
||||||
|
|
||||||
handleFormChange("corporateEmirate", selectedEmirate?.name || '');
|
|
||||||
handleFormChange("corporateEmirateId", selectedEmirate?.id || '');
|
|
||||||
handleFormChange("corporateCityTown", '');
|
|
||||||
handleFormChange("corporateCityTownId", '');
|
|
||||||
|
|
||||||
if (selectedEmirate?.id) {
|
|
||||||
const cities = await loadCities(selectedEmirate.id, setCorporateCityOptions);
|
|
||||||
setCorporateCityOptions(cities);
|
|
||||||
} else {
|
|
||||||
setCorporateCityOptions([]);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
options={emirateOptions.map(opt => ({
|
|
||||||
value: opt.value,
|
|
||||||
label: opt.label
|
|
||||||
}))}
|
|
||||||
placeholder="Select Emirate"
|
|
||||||
width="100%"
|
|
||||||
required
|
|
||||||
error={fieldErrors.corporateEmirate}
|
|
||||||
/>
|
|
||||||
<SelectField
|
|
||||||
label="City/Town"
|
|
||||||
name="corporateCityTown"
|
|
||||||
value={form.corporateCityTownId || ''}
|
|
||||||
onChange={(e) => {
|
|
||||||
const selectedValue = e.target.value;
|
|
||||||
const selectedCity = corporateCityOptions.find(opt => opt.value === selectedValue);
|
|
||||||
if (selectedCity) {
|
|
||||||
handleFormChange("corporateCityTown", selectedCity.value);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
options={corporateCityOptions.map(opt => ({
|
|
||||||
value: opt.value,
|
|
||||||
label: opt.label
|
|
||||||
}))}
|
|
||||||
placeholder={form.corporateEmirateId ? "Select City/Town" : "Select emirate first"}
|
|
||||||
width="100%"
|
|
||||||
required
|
|
||||||
disabled={!form.corporateEmirateId}
|
|
||||||
error={fieldErrors.corporateCityTown}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
<TextField
|
</div>
|
||||||
label="Postal Code"
|
</div>
|
||||||
value={form.corporatePostalCode || ""}
|
</div>
|
||||||
onChange={(e) => handleFormChange("corporatePostalCode", e.target.value)}
|
|
||||||
placeholder="Enter Postal Code"
|
{/* Selected Products Panel */}
|
||||||
width="100%"
|
<div className="bg-white rounded-lg border border-[#E5E7EB] overflow-hidden">
|
||||||
disabled={form.corporateSameAs}
|
<div className="bg-[#F9FAFB] px-4 py-3 border-b border-[#E5E7EB] flex justify-between items-center">
|
||||||
/>
|
<h5 className="text-sm font-medium text-[#111827]">Selected Products</h5>
|
||||||
<TextField
|
{selectedProducts.length > 0 && (
|
||||||
label="PO Box"
|
<span className="bg-[#FEF3C7] text-[#92400E] text-xs font-medium px-2 py-0.5 rounded-full">
|
||||||
value={form.corporatePoBox || ""}
|
{selectedProducts.length} selected
|
||||||
onChange={(e) => handleFormChange("corporatePoBox", e.target.value)}
|
</span>
|
||||||
placeholder="Enter P.O. Box"
|
)}
|
||||||
width="100%"
|
</div>
|
||||||
disabled={form.corporateSameAs}
|
<div className="p-4">
|
||||||
/>
|
{selectedProducts.length > 0 ? (
|
||||||
<TextField
|
<ul className="divide-y divide-[#E5E7EB] max-h-96 overflow-y-auto">
|
||||||
label="Makani Number"
|
{selectedProducts.map((product) => (
|
||||||
value={form.corporateMakaniNumber || ""}
|
<li key={product.id} className="p-3 hover:bg-[#F9FAFB] flex justify-between items-center">
|
||||||
onChange={(e) => handleFormChange("corporateMakaniNumber", e.target.value)}
|
<div>
|
||||||
placeholder="Enter Makani Number"
|
<div className="text-sm font-medium text-[#111827]">
|
||||||
width="100%"
|
{product.hs_code || product.hsCode || ''}
|
||||||
required
|
{product.hs_code || product.hsCode ? ' - ' : ''}
|
||||||
disabled={form.corporateSameAs}
|
{product.product_name || product.productName || product.label || 'Unnamed Product'}
|
||||||
error={fieldErrors.corporateMakaniNumber}
|
</div>
|
||||||
/>
|
</div>
|
||||||
<TextField
|
<button
|
||||||
label="Contact Person Name"
|
type="button"
|
||||||
value={form.corporateContactPersonName || ""}
|
onClick={() => handleRemoveProduct(product)}
|
||||||
onChange={(e) => handleFormChange("corporateContactPersonName", e.target.value)}
|
className="text-[#EF4444] hover:text-[#DC2626] text-sm font-medium"
|
||||||
placeholder="Enter Contact Person Name"
|
>
|
||||||
width="100%"
|
Remove
|
||||||
required
|
</button>
|
||||||
disabled={form.corporateSameAs}
|
</li>
|
||||||
error={fieldErrors.corporateContactPersonName}
|
))}
|
||||||
/>
|
</ul>
|
||||||
<TextField
|
) : (
|
||||||
label="Contact Person Designation"
|
<div className="p-4 text-center text-sm text-[#6B7280] border border-[#E5E7EB] rounded-md">
|
||||||
value={form.corporateContactPersonDesignation || ""}
|
No products selected
|
||||||
onChange={(e) => handleFormChange("corporateContactPersonDesignation", e.target.value)}
|
</div>
|
||||||
placeholder="Enter contact person designation"
|
)}
|
||||||
width="100%"
|
</div>
|
||||||
disabled={form.corporateSameAs}
|
</div>
|
||||||
/>
|
|
||||||
<PhoneField
|
|
||||||
label="Mobile Number"
|
|
||||||
value={form.corporateMobileNumber || ""}
|
|
||||||
onChange={(value) => handleFormChange("corporateMobileNumber", value)}
|
|
||||||
placeholder="Enter Mobile Number"
|
|
||||||
width="100%"
|
|
||||||
required
|
|
||||||
disabled={form.corporateSameAs}
|
|
||||||
error={fieldErrors.corporateMobileNumber}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
label="Email"
|
|
||||||
value={form.corporateEmail || ""}
|
|
||||||
onChange={(e) => handleFormChange("corporateEmail", e.target.value)}
|
|
||||||
placeholder="Enter Email"
|
|
||||||
width="100%"
|
|
||||||
type="email"
|
|
||||||
required
|
|
||||||
disabled={form.corporateSameAs}
|
|
||||||
error={fieldErrors.corporateEmail}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
label="Website"
|
|
||||||
value={form.corporateWebsite || ""}
|
|
||||||
onChange={(e) => handleFormChange("corporateWebsite", e.target.value)}
|
|
||||||
placeholder="Enter Website"
|
|
||||||
width="100%"
|
|
||||||
disabled={form.corporateSameAs}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -1013,27 +943,23 @@ useEffect(() => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
|
<div className="min-h-screen bg-[#F8FAFC]">
|
||||||
<div className="bg-white w-full max-w-7xl rounded-xl shadow-2xl overflow-hidden flex flex-col h-[75vh]">
|
<HeaderBar />
|
||||||
|
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<div className="bg-white rounded-xl shadow-lg overflow-hidden">
|
||||||
{loading && <Loader />}
|
{loading && <Loader />}
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex justify-between items-center border-b border-[#F1F2F4] px-8 py-5 bg-white sticky top-0 z-10">
|
<div className="flex justify-between items-center border-b border-[#F1F2F4] px-8 py-5 bg-white">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-gray-900">Edit Company Profile</h2>
|
<h2 className="text-xl font-bold text-gray-900">Edit Company Profile</h2>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
onClick={onClose}
|
|
||||||
className="flex h-7 w-7 text-lg items-center justify-center rounded hover:bg-gray-100 hover:bg-opacity-10 text-[#92722A] hover:text-[#92722A] transition-colors"
|
|
||||||
aria-label="Close"
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Body */}
|
{/* Body */}
|
||||||
<div className="flex flex-1 overflow-hidden">
|
<div className="flex flex-1">
|
||||||
<aside className="w-64 bg-[#F9FAFB] border-r border-[#E5E7EB] overflow-y-auto" style={{ maxHeight: 'calc(75vh - 120px)' }}>
|
<aside className="w-64 bg-[#F9FAFB] border-r border-[#E5E7EB]">
|
||||||
<ol className="py-6 px-4 space-y-2">
|
<ol className="py-6 px-4 space-y-2">
|
||||||
{formSteps.map((step, index) => {
|
{formSteps.map((step, index) => {
|
||||||
const isActive = index === activeStep;
|
const isActive = index === activeStep;
|
||||||
@ -1071,7 +997,7 @@ useEffect(() => {
|
|||||||
})}
|
})}
|
||||||
</ol>
|
</ol>
|
||||||
</aside>
|
</aside>
|
||||||
<div className="flex-1 overflow-y-auto" style={{ maxHeight: 'calc(85vh - 120px)' }}>
|
<div className="flex-1">
|
||||||
<div className="px-6 py-6">
|
<div className="px-6 py-6">
|
||||||
{renderStepContent()}
|
{renderStepContent()}
|
||||||
</div>
|
</div>
|
||||||
@ -1098,12 +1024,24 @@ useEffect(() => {
|
|||||||
<span> </span>
|
<span> </span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{activeStep === formSteps.length - 1 ? (
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
{activeStep > 0 && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="inline-flex justify-center rounded-md border border-[#92722A] bg-white px-4 py-2 text-sm font-medium text-[#92722A] shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
className="inline-flex justify-center rounded-md border border-[#92722A] bg-white px-4 py-2 text-sm font-medium text-[#92722A] shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||||
onClick={() => onClose && onClose()}
|
onClick={handlePrev}
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeStep === formSteps.length - 1 ? (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex justify-center rounded-md border border-[#92722A] bg-white px-4 py-2 text-sm font-medium text-[#92722A] shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||||
|
onClick={() => navigate('/survey')}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
@ -1115,7 +1053,7 @@ useEffect(() => {
|
|||||||
>
|
>
|
||||||
{loading ? 'Saving…' : 'Update'}
|
{loading ? 'Saving…' : 'Update'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -1129,7 +1067,10 @@ useEffect(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EditCompanyProfile;
|
export default EditCompanyProfile;
|
||||||
|
|
||||||
@ -1,8 +1,7 @@
|
|||||||
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
||||||
import Table from '@/components/common/Table';
|
import Table from '@/components/common/Table';
|
||||||
import StatusBadge from '@/components/common/StatusBadge';
|
import { TextField } from '@/components/common/FormControls';
|
||||||
import { TextField, SelectField } from '@/components/common/FormControls';
|
import { getUnits, createUnit, updateUnit, deleteUnit, uploadUnitCSV } from '@/services/configuration/unitService';
|
||||||
import { getUnits, createUnit, updateUnit, deleteUnit } from '@/services/configuration/unitService';
|
|
||||||
|
|
||||||
const downloadIconSrc = '/assets/images/DownloadSimple.svg';
|
const downloadIconSrc = '/assets/images/DownloadSimple.svg';
|
||||||
const addIconSrc = '/assets/images/ic_baseline-plus.svg';
|
const addIconSrc = '/assets/images/ic_baseline-plus.svg';
|
||||||
@ -11,9 +10,7 @@ const pencilInactiveSrc = '/assets/images/pencilsimple-inactive.svg';
|
|||||||
const trashActiveSrc = '/assets/images/Trash - active.svg';
|
const trashActiveSrc = '/assets/images/Trash - active.svg';
|
||||||
const trashInactiveSrc = '/assets/images/Trash - Inactive.svg';
|
const trashInactiveSrc = '/assets/images/Trash - Inactive.svg';
|
||||||
const deleteIconSrc = '/assets/images/delete.svg';
|
const deleteIconSrc = '/assets/images/delete.svg';
|
||||||
const caretDownIconSrc = '/assets/images/caretdown-active.svg';
|
const uploadImportIconSrc = '/assets/images/UploadSimple.svg';
|
||||||
const rectangleIconSrc = '/assets/images/Rectangle.svg';
|
|
||||||
const checkboxIconSrc = '/assets/images/checkbox.svg';
|
|
||||||
|
|
||||||
// Custom Toast Component
|
// Custom Toast Component
|
||||||
const CustomToast = ({ message, type, onClose }) => {
|
const CustomToast = ({ message, type, onClose }) => {
|
||||||
@ -41,13 +38,13 @@ const CustomToast = ({ message, type, onClose }) => {
|
|||||||
const getIcon = () => {
|
const getIcon = () => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'success':
|
case 'success':
|
||||||
return '✅';
|
return '';
|
||||||
case 'error':
|
case 'error':
|
||||||
return '❌';
|
return '';
|
||||||
case 'warning':
|
case 'warning':
|
||||||
return '⚠️';
|
return '';
|
||||||
default:
|
default:
|
||||||
return 'ℹ️';
|
return '';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -66,10 +63,8 @@ const CustomToast = ({ message, type, onClose }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createEmptyUnitForm = () => ({
|
const createEmptyUnitForm = () => ({
|
||||||
unitName: '',
|
uom: '',
|
||||||
description: '',
|
description: '',
|
||||||
productsMapped: [],
|
|
||||||
status: '',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const UnitMaster = () => {
|
const UnitMaster = () => {
|
||||||
@ -78,16 +73,23 @@ const UnitMaster = () => {
|
|||||||
const [editingRow, setEditingRow] = useState(null);
|
const [editingRow, setEditingRow] = useState(null);
|
||||||
const [deletingRow, setDeletingRow] = useState(null);
|
const [deletingRow, setDeletingRow] = useState(null);
|
||||||
const [modalMode, setModalMode] = useState(null);
|
const [modalMode, setModalMode] = useState(null);
|
||||||
const [selectedUnit, setSelectedUnit] = useState(null);
|
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [form, setForm] = useState(createEmptyUnitForm());
|
const [form, setForm] = useState(createEmptyUnitForm());
|
||||||
const [formErrors, setFormErrors] = useState({});
|
|
||||||
const [isProductDropdownOpen, setIsProductDropdownOpen] = useState(false);
|
|
||||||
const productDropdownRef = useRef(null);
|
|
||||||
const pageSize = 10;
|
|
||||||
const [currentUser, setCurrentUser] = useState(null);
|
const [currentUser, setCurrentUser] = useState(null);
|
||||||
const [toastData, setToastData] = useState(null);
|
const [toastData, setToastData] = useState(null);
|
||||||
const [searchTerm, setSearchTerm] = React.useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [saveLoading, setSaveLoading] = useState(false);
|
||||||
|
const [selectedUnit, setSelectedUnit] = useState(null);
|
||||||
|
|
||||||
|
// Import CSV state
|
||||||
|
const [importModalOpen, setImportModalOpen] = useState(false);
|
||||||
|
const [file, setFile] = useState(null);
|
||||||
|
const [dragActive, setDragActive] = useState(false);
|
||||||
|
const [importLoading, setImportLoading] = useState(false);
|
||||||
|
const [importError, setImportError] = useState('');
|
||||||
|
const fileInputRef = useRef(null);
|
||||||
|
|
||||||
|
const pageSize = 10;
|
||||||
|
|
||||||
// Fetch units on component mount
|
// Fetch units on component mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -98,27 +100,45 @@ const UnitMaster = () => {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await getUnits();
|
const response = await getUnits();
|
||||||
const unitsData = response.data || [];
|
|
||||||
|
|
||||||
// ✅ Sort by latest created_at date (newest first)
|
// Handle different response formats
|
||||||
|
let unitsData = [];
|
||||||
|
if (Array.isArray(response)) {
|
||||||
|
unitsData = response;
|
||||||
|
} else if (response && Array.isArray(response.data)) {
|
||||||
|
unitsData = response.data;
|
||||||
|
} else if (response && response.data) {
|
||||||
|
// If response.data is an object, convert to array
|
||||||
|
if (typeof response.data === 'object' && !Array.isArray(response.data)) {
|
||||||
|
unitsData = Object.values(response.data);
|
||||||
|
} else {
|
||||||
|
unitsData = response.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by latest created_at date (newest first)
|
||||||
const sortedUnits = [...unitsData].sort(
|
const sortedUnits = [...unitsData].sort(
|
||||||
(a, b) => new Date(b.created_at) - new Date(a.created_at)
|
(a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0)
|
||||||
);
|
);
|
||||||
|
|
||||||
setUnits(sortedUnits);
|
setUnits(sortedUnits);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching units:', error);
|
console.error('Error fetching units:', error);
|
||||||
|
setToastData({
|
||||||
|
message: 'Failed to load units. Please try again.',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
setUnits([]);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredUnits = React.useMemo(() => {
|
const filteredUnits = useMemo(() => {
|
||||||
if (!searchTerm) return units;
|
if (!searchTerm) return units;
|
||||||
const term = searchTerm.toLowerCase();
|
const term = searchTerm.toLowerCase();
|
||||||
return units.filter(unit =>
|
return units.filter(unit =>
|
||||||
(unit.uom && unit.uom.toLowerCase().includes(term)) ||
|
(unit.uom && unit.uom.toLowerCase().includes(term)) ||
|
||||||
(unit.uom_short_name && unit.uom_short_name.toLowerCase().includes(term)) ||
|
|
||||||
(unit.description && unit.description.toLowerCase().includes(term))
|
(unit.description && unit.description.toLowerCase().includes(term))
|
||||||
);
|
);
|
||||||
}, [units, searchTerm]);
|
}, [units, searchTerm]);
|
||||||
@ -126,14 +146,20 @@ const UnitMaster = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const userProfile = sessionStorage.getItem('user_profile');
|
const userProfile = sessionStorage.getItem('user_profile');
|
||||||
if (userProfile) {
|
if (userProfile) {
|
||||||
|
try {
|
||||||
setCurrentUser(JSON.parse(userProfile));
|
setCurrentUser(JSON.parse(userProfile));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing user profile:', error);
|
||||||
|
setCurrentUser({ name: 'System User', id: 1 });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setCurrentUser({ name: 'System User', id: 1 });
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const headers = [
|
const headers = [
|
||||||
'Unit Name',
|
'Unit Name',
|
||||||
'Description',
|
'Description',
|
||||||
'Mapped Products',
|
|
||||||
'Created By',
|
'Created By',
|
||||||
'Created On',
|
'Created On',
|
||||||
'Last Updated',
|
'Last Updated',
|
||||||
@ -141,65 +167,32 @@ const UnitMaster = () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const rows = useMemo(() => {
|
const rows = useMemo(() => {
|
||||||
return units.map((item) => {
|
return filteredUnits.map((item) => {
|
||||||
const createdDate = item.created_at
|
const createdDate = item.created_at
|
||||||
? new Date(item.created_at).toLocaleDateString('en-GB')
|
? new Date(item.created_at).toLocaleDateString('en-GB')
|
||||||
: '-';
|
: '-';
|
||||||
const updatedDate =
|
const updatedDate = item.updated_at
|
||||||
item.updated_at && item.updated_at !== item.created_at
|
|
||||||
? new Date(item.updated_at).toLocaleDateString('en-GB')
|
? new Date(item.updated_at).toLocaleDateString('en-GB')
|
||||||
: '-';
|
: '-';
|
||||||
const currentUserName = currentUser?.name || '';
|
|
||||||
|
const createdByName = item.created_by_name || currentUser?.name || '-';
|
||||||
|
|
||||||
return [
|
return [
|
||||||
item.uom || item.unitName,
|
item.uom || '-',
|
||||||
item.uom_short_name || item.description,
|
item.description || '-',
|
||||||
item.mapped_products_count !== undefined
|
createdByName,
|
||||||
? item.mapped_products_count
|
|
||||||
: (Array.isArray(item.productsMapped) ? item.productsMapped.length : 0),
|
|
||||||
item.created_by_name || currentUserName || '-',
|
|
||||||
createdDate,
|
createdDate,
|
||||||
updatedDate,
|
updatedDate,
|
||||||
'actions',
|
'actions',
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
}, [units, currentUser, filteredUnits]);
|
}, [filteredUnits, currentUser]);
|
||||||
|
|
||||||
const statusOptions = React.useMemo(
|
|
||||||
() => [
|
|
||||||
{ label: 'Active', value: 'Active' },
|
|
||||||
{ label: 'Inactive', value: 'Inactive' },
|
|
||||||
],
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const filteredRows = units;
|
|
||||||
|
|
||||||
const validateForm = () => {
|
|
||||||
const errors = {};
|
|
||||||
|
|
||||||
// Unit Name validation
|
|
||||||
if (!form.unitName.trim()) {
|
|
||||||
errors.unitName = 'Unit Name is required';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Description validation - red color validation
|
|
||||||
if (!form.description.trim()) {
|
|
||||||
errors.description = 'Please enter a description.';
|
|
||||||
}
|
|
||||||
|
|
||||||
setFormErrors(errors);
|
|
||||||
return Object.keys(errors).length === 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const openAddModal = () => {
|
const openAddModal = () => {
|
||||||
setEditingRow(null);
|
setEditingRow(null);
|
||||||
setForm(createEmptyUnitForm());
|
setForm(createEmptyUnitForm());
|
||||||
setFormErrors({});
|
|
||||||
setModalMode('add');
|
setModalMode('add');
|
||||||
setIsProductDropdownOpen(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleView = (index) => {
|
const handleView = (index) => {
|
||||||
const selected = units[index];
|
const selected = units[index];
|
||||||
if (!selected) return;
|
if (!selected) return;
|
||||||
@ -208,19 +201,15 @@ const UnitMaster = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (index) => {
|
const handleEdit = (index) => {
|
||||||
const selected = units[index];
|
const selected = filteredUnits[index];
|
||||||
if (!selected) return;
|
if (!selected) return;
|
||||||
setEditingRow(index);
|
setEditingRow(index);
|
||||||
setSelectedUnit(selected);
|
setSelectedUnit(selected);
|
||||||
setForm({
|
setForm({
|
||||||
unitName: selected.uom || selected.unitName,
|
uom: selected.uom || '',
|
||||||
description: selected.uom_short_name || selected.description,
|
description: selected.description || '',
|
||||||
productsMapped: Array.isArray(selected.productsMapped) ? [...selected.productsMapped] : [],
|
|
||||||
status: selected.is_active ? 'Active' : 'Inactive',
|
|
||||||
});
|
});
|
||||||
setFormErrors({});
|
|
||||||
setModalMode('edit');
|
setModalMode('edit');
|
||||||
setIsProductDropdownOpen(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = (index) => {
|
const handleDelete = (index) => {
|
||||||
@ -231,108 +220,74 @@ const UnitMaster = () => {
|
|||||||
setModalMode(null);
|
setModalMode(null);
|
||||||
setSelectedUnit(null);
|
setSelectedUnit(null);
|
||||||
setForm(createEmptyUnitForm());
|
setForm(createEmptyUnitForm());
|
||||||
setFormErrors({});
|
|
||||||
setEditingRow(null);
|
setEditingRow(null);
|
||||||
setIsProductDropdownOpen(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFormChange = (field) => (event) => {
|
const handleFormChange = (field) => (event) => {
|
||||||
const value = event?.target?.value ?? event;
|
const value = event?.target?.value ?? event;
|
||||||
setForm((prev) => ({ ...prev, [field]: value }));
|
setForm((prev) => ({ ...prev, [field]: value }));
|
||||||
|
|
||||||
// Clear error when user starts typing
|
|
||||||
if (formErrors[field]) {
|
|
||||||
setFormErrors(prev => ({
|
|
||||||
...prev,
|
|
||||||
[field]: ''
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleProductToggle = (value) => {
|
|
||||||
setForm((prev) => {
|
|
||||||
const current = Array.isArray(prev.productsMapped) ? prev.productsMapped : [];
|
|
||||||
const exists = current.includes(value);
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
productsMapped: exists ? current.filter((item) => item !== value) : [...current, value],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const handleClickOutside = (event) => {
|
|
||||||
if (!productDropdownRef.current) return;
|
|
||||||
if (!productDropdownRef.current.contains(event.target)) {
|
|
||||||
setIsProductDropdownOpen(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isProductDropdownOpen) {
|
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
|
||||||
};
|
|
||||||
}, [isProductDropdownOpen]);
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
// Validate form before saving
|
// Validation checks
|
||||||
if (!validateForm()) {
|
if (!form.uom?.trim()) {
|
||||||
|
setToastData({
|
||||||
|
message: 'Please enter Unit Name.',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!form.description?.trim()) {
|
||||||
|
setToastData({
|
||||||
|
message: 'Please enter Description.',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedProducts = Array.isArray(form.productsMapped) ? form.productsMapped : [];
|
// Check for duplicate unit name (only when adding)
|
||||||
|
if (modalMode !== 'edit') {
|
||||||
// Check for duplicate unit name
|
|
||||||
const nameExists = units.some(
|
const nameExists = units.some(
|
||||||
unit => unit.uom?.toLowerCase().trim() === form.unitName.toLowerCase().trim()
|
unit => unit.uom?.toLowerCase().trim() === form.uom.toLowerCase().trim()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Only check duplicates when adding (not editing)
|
if (nameExists) {
|
||||||
if (modalMode !== 'edit' && nameExists) {
|
|
||||||
setToastData({
|
setToastData({
|
||||||
message: 'Unit name already exists!',
|
message: 'Unit name already exists!',
|
||||||
type: 'error'
|
type: 'error'
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare unit data - ensure proper format for backend
|
||||||
const unitData = {
|
const unitData = {
|
||||||
uom: form.unitName,
|
uom: form.uom.trim(),
|
||||||
uom_short_name: form.description,
|
description: form.description.trim(),
|
||||||
is_active: form.status === 'Active',
|
|
||||||
productsMapped: selectedProducts.length,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add user information if available
|
||||||
|
if (currentUser?.id) {
|
||||||
|
unitData.created_by = currentUser.id;
|
||||||
|
unitData.created_by_name = currentUser.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Saving unit data:', unitData);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setSaveLoading(true);
|
||||||
|
|
||||||
if (modalMode === 'edit' && editingRow !== null) {
|
if (modalMode === 'edit' && editingRow !== null) {
|
||||||
const updatedUnitData = {
|
const unitToUpdate = filteredUnits[editingRow];
|
||||||
...unitData,
|
await updateUnit(unitToUpdate.id, unitData);
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
updated_by: currentUser?.id || null,
|
|
||||||
};
|
|
||||||
|
|
||||||
await updateUnit(units[editingRow].id, updatedUnitData);
|
|
||||||
|
|
||||||
// ✅ Show success message for edit
|
|
||||||
setToastData({
|
setToastData({
|
||||||
message: 'Unit updated successfully!',
|
message: 'Unit updated successfully!',
|
||||||
type: 'success'
|
type: 'success'
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
//Create new record
|
await createUnit(unitData);
|
||||||
await createUnit({
|
|
||||||
...unitData,
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
created_by: currentUser?.id || null,
|
|
||||||
created_by_name: currentUser?.name || '',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show success message for add
|
|
||||||
setToastData({
|
setToastData({
|
||||||
message: 'New unit added successfully!',
|
message: 'New unit added successfully!',
|
||||||
type: 'success'
|
type: 'success'
|
||||||
@ -343,23 +298,94 @@ const UnitMaster = () => {
|
|||||||
closeModal();
|
closeModal();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving unit:', error);
|
console.error('Error saving unit:', error);
|
||||||
|
|
||||||
|
// Get detailed error message with better error handling
|
||||||
|
let errorMessage = 'Failed to save unit. Please check the data and try again.';
|
||||||
|
|
||||||
|
if (error.response?.data) {
|
||||||
|
const errorData = error.response.data;
|
||||||
|
|
||||||
|
console.log('Error response data:', errorData);
|
||||||
|
|
||||||
|
// Handle different error response formats
|
||||||
|
if (typeof errorData === 'string') {
|
||||||
|
errorMessage = errorData;
|
||||||
|
} else if (errorData.message) {
|
||||||
|
errorMessage = errorData.message;
|
||||||
|
} else if (errorData.error) {
|
||||||
|
errorMessage = errorData.error;
|
||||||
|
} else if (Array.isArray(errorData)) {
|
||||||
|
errorMessage = errorData.join(', ');
|
||||||
|
} else if (typeof errorData === 'object') {
|
||||||
|
// Extract error messages from validation object
|
||||||
|
const errorMessages = [];
|
||||||
|
|
||||||
|
// Check for common validation error formats
|
||||||
|
if (errorData.errors) {
|
||||||
|
// Laravel-style validation errors
|
||||||
|
Object.values(errorData.errors).forEach(errArray => {
|
||||||
|
if (Array.isArray(errArray)) {
|
||||||
|
errorMessages.push(...errArray);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Generic object with error messages
|
||||||
|
Object.values(errorData).forEach(value => {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
errorMessages.push(...value);
|
||||||
|
} else if (typeof value === 'string') {
|
||||||
|
errorMessages.push(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorMessages.length > 0) {
|
||||||
|
errorMessage = errorMessages.join(', ');
|
||||||
|
} else {
|
||||||
|
// If no specific messages found, show the first value
|
||||||
|
const firstError = Object.values(errorData)[0];
|
||||||
|
if (Array.isArray(firstError)) {
|
||||||
|
errorMessage = firstError[0];
|
||||||
|
} else {
|
||||||
|
errorMessage = firstError || errorMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (error.message) {
|
||||||
|
errorMessage = error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special handling for common backend validation errors
|
||||||
|
if (errorMessage.toLowerCase().includes('uom') || errorMessage.toLowerCase().includes('unit')) {
|
||||||
|
if (errorMessage.toLowerCase().includes('required')) {
|
||||||
|
errorMessage = 'Unit name is required.';
|
||||||
|
} else if (errorMessage.toLowerCase().includes('unique') || errorMessage.toLowerCase().includes('already exists')) {
|
||||||
|
errorMessage = 'Unit name already exists. Please choose a different name.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorMessage.toLowerCase().includes('description') && errorMessage.toLowerCase().includes('required')) {
|
||||||
|
errorMessage = 'Description is required.';
|
||||||
|
}
|
||||||
|
|
||||||
setToastData({
|
setToastData({
|
||||||
message: 'Something went wrong while saving the unit.',
|
message: errorMessage,
|
||||||
type: 'error'
|
type: 'error'
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setSaveLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteConfirm = async () => {
|
const handleDeleteConfirm = async () => {
|
||||||
if (deletingRow === null) return;
|
if (deletingRow === null) return;
|
||||||
try {
|
try {
|
||||||
const unitToDelete = units[deletingRow];
|
const unitToDelete = filteredUnits[deletingRow];
|
||||||
await deleteUnit(unitToDelete.id);
|
await deleteUnit(unitToDelete.id);
|
||||||
setUnits(prev => prev.filter((_, index) => index !== deletingRow));
|
|
||||||
|
|
||||||
// ✅ Show success message for delete
|
// Update local state
|
||||||
|
setUnits(prev => prev.filter(unit => unit.id !== unitToDelete.id));
|
||||||
|
|
||||||
setToastData({
|
setToastData({
|
||||||
message: 'Unit deleted successfully!',
|
message: 'Unit deleted successfully!',
|
||||||
type: 'success'
|
type: 'success'
|
||||||
@ -367,8 +393,16 @@ const UnitMaster = () => {
|
|||||||
setDeletingRow(null);
|
setDeletingRow(null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting unit:', error);
|
console.error('Error deleting unit:', error);
|
||||||
|
|
||||||
|
let errorMessage = 'Failed to delete unit. Please try again.';
|
||||||
|
if (error.response?.data?.message) {
|
||||||
|
errorMessage = error.response.data.message;
|
||||||
|
} else if (error.message) {
|
||||||
|
errorMessage = error.message;
|
||||||
|
}
|
||||||
|
|
||||||
setToastData({
|
setToastData({
|
||||||
message: 'Failed to delete unit. Please try again.',
|
message: errorMessage,
|
||||||
type: 'error'
|
type: 'error'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -378,53 +412,202 @@ const UnitMaster = () => {
|
|||||||
setDeletingRow(null);
|
setDeletingRow(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deletingUnit = React.useMemo(() => {
|
const deletingUnit = useMemo(() => {
|
||||||
if (deletingRow === null || deletingRow < 0 || deletingRow >= units.length) {
|
if (deletingRow === null || deletingRow < 0 || deletingRow >= filteredUnits.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return units[deletingRow];
|
return filteredUnits[deletingRow];
|
||||||
}, [deletingRow, units]);
|
}, [deletingRow, filteredUnits]);
|
||||||
|
|
||||||
|
// 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 handleDrop = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setDragActive(false);
|
||||||
|
|
||||||
|
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
|
||||||
|
const droppedFile = e.dataTransfer.files[0];
|
||||||
|
if (droppedFile.type === 'text/csv' || droppedFile.name.endsWith('.csv')) {
|
||||||
|
setFile(droppedFile);
|
||||||
|
setImportError('');
|
||||||
|
} else {
|
||||||
|
setImportError('Please upload a CSV file only.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileChange = (e) => {
|
||||||
|
if (e.target.files && e.target.files[0]) {
|
||||||
|
const selectedFile = e.target.files[0];
|
||||||
|
if (selectedFile.type === 'text/csv' || selectedFile.name.endsWith('.csv')) {
|
||||||
|
setFile(selectedFile);
|
||||||
|
setImportError('');
|
||||||
|
} else {
|
||||||
|
setImportError('Please upload a CSV file only.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadSampleCSV = () => {
|
||||||
|
const sampleData = [
|
||||||
|
['uom', 'description'],
|
||||||
|
['Kilogram', 'Weight measurement in kilograms'],
|
||||||
|
['Gram', 'Weight measurement in grams'],
|
||||||
|
['Liter', 'Volume measurement in liters'],
|
||||||
|
['Meter', 'Length measurement in meters']
|
||||||
|
];
|
||||||
|
|
||||||
|
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', 'unit-master-template.csv');
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImport = async () => {
|
||||||
|
if (!file) {
|
||||||
|
setImportError('Please select a CSV file to import.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setImportLoading(true);
|
||||||
|
setImportError('');
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
// Add user info if needed by backend
|
||||||
|
if (currentUser?.id) {
|
||||||
|
formData.append('uploaded_by', currentUser.id);
|
||||||
|
formData.append('uploaded_by_name', currentUser.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await uploadUnitCSV(formData);
|
||||||
|
|
||||||
|
setToastData({
|
||||||
|
message: response.message || 'Units imported successfully!',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
|
||||||
|
closeImportModal();
|
||||||
|
await fetchUnits();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error importing CSV:', error);
|
||||||
|
|
||||||
|
let errorMessage = 'Failed to import units. Please try again.';
|
||||||
|
|
||||||
|
if (error.response?.status === 404) {
|
||||||
|
errorMessage = 'Unit CSV upload endpoint not found. Please contact administrator.';
|
||||||
|
} else if (error.response?.status === 400) {
|
||||||
|
errorMessage = error.response.data?.message || 'Invalid CSV format. Please check the file.';
|
||||||
|
} else if (error.response?.data?.message) {
|
||||||
|
errorMessage = error.response.data.message;
|
||||||
|
} else if (error.message) {
|
||||||
|
errorMessage = error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
setImportError(errorMessage);
|
||||||
|
setToastData({
|
||||||
|
message: errorMessage,
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setImportLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white w-full rounded-lg shadow-sm ring-1 ring-[#E5E7EB]">
|
<div className="bg-white w-full rounded-lg shadow-sm ring-1 ring-[#E5E7EB]">
|
||||||
<div className="px-6 py-3 flex items-center justify-between border-b border-[#E5E7EB]">
|
<div className="px-6 py-3 flex items-center justify-between border-b border-[#E5E7EB]">
|
||||||
<h3 className="text-[16px] font-medium text-[#232528]">
|
<h3 className="text-[16px] font-medium text-[#232528]">
|
||||||
Unit Master ({units.length}{units.length === 1 ? ' unit' : ''})
|
Unit Master ({units.length} {units.length === 1 ? 'unit' : 'units'})
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
{/* Search Input */}
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search units..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="h-10 px-4 pr-10 rounded-[6px] border border-[#C3C6CB] text-sm focus:outline-none focus:ring-2 focus:ring-[#92722A] focus:border-transparent"
|
||||||
|
/>
|
||||||
|
<div className="absolute right-3 top-1/2 transform -translate-y-1/2">
|
||||||
|
<svg className="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!units.length) return;
|
if (!units.length) return;
|
||||||
|
|
||||||
const csvHeader = headers.slice(0, headers.length - 1).join(',');
|
const csvHeader = ['Unit Name', 'Description', 'Created By', 'Created On', 'Last Updated'];
|
||||||
const csvRows = units.map((item) => {
|
const csvRows = units.map((item) => {
|
||||||
const createdDate = item.created_at
|
const createdDate = item.created_at
|
||||||
? new Date(item.created_at).toLocaleDateString('en-GB')
|
? new Date(item.created_at).toLocaleDateString('en-GB')
|
||||||
: '-';
|
: '-';
|
||||||
const updatedDate =
|
const updatedDate = item.updated_at
|
||||||
item.updated_at && item.updated_at !== item.created_at
|
|
||||||
? new Date(item.updated_at).toLocaleDateString('en-GB')
|
? new Date(item.updated_at).toLocaleDateString('en-GB')
|
||||||
: '-';
|
: '-';
|
||||||
const status = item.is_active ? 'Active' : 'Inactive';
|
|
||||||
const mappedProducts = Array.isArray(item.productsMapped)
|
|
||||||
? item.productsMapped.join('; ')
|
|
||||||
: '0';
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
item.uom || item.unitName || '',
|
item.uom || '',
|
||||||
item.uom_short_name || item.description || '',
|
item.description || '',
|
||||||
mappedProducts,
|
|
||||||
item.created_by_name || currentUser?.name || '-',
|
item.created_by_name || currentUser?.name || '-',
|
||||||
createdDate,
|
createdDate,
|
||||||
updatedDate,
|
updatedDate,
|
||||||
status,
|
|
||||||
]
|
]
|
||||||
.map((value) => `"${String(value ?? '').replace(/"/g, '""')}"`)
|
.map((value) => `"${String(value ?? '').replace(/"/g, '""')}"`)
|
||||||
.join(',');
|
.join(',');
|
||||||
});
|
});
|
||||||
|
|
||||||
const csvContent = csvHeader + '\n' + csvRows.join('\n');
|
const csvContent = csvHeader.join(',') + '\n' + csvRows.join('\n');
|
||||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
@ -437,7 +620,7 @@ const UnitMaster = () => {
|
|||||||
}}
|
}}
|
||||||
className={`h-10 px-4 rounded-[6px] border text-sm inline-flex items-center gap-2 ${
|
className={`h-10 px-4 rounded-[6px] border text-sm inline-flex items-center gap-2 ${
|
||||||
units.length
|
units.length
|
||||||
? 'bg-[#F7F7F7] border-[#C3C6CB] text-[#232528]'
|
? 'bg-[#F7F7F7] border-[#C3C6CB] text-[#232528] hover:bg-gray-50'
|
||||||
: 'bg-[#F3F4F6] border-[#E5E7EB] text-[#9CA3AF] cursor-not-allowed'
|
: 'bg-[#F3F4F6] border-[#E5E7EB] text-[#9CA3AF] cursor-not-allowed'
|
||||||
}`}
|
}`}
|
||||||
disabled={!units.length}
|
disabled={!units.length}
|
||||||
@ -448,7 +631,7 @@ const UnitMaster = () => {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="h-10 px-4 rounded-[6px] bg-[#92722A] text-white text-sm inline-flex items-center gap-2 cursor-pointer"
|
className="h-10 px-4 rounded-[6px] bg-[#92722A] text-white text-sm inline-flex items-center gap-2 cursor-pointer hover:bg-[#7a5f22]"
|
||||||
onClick={openAddModal}
|
onClick={openAddModal}
|
||||||
>
|
>
|
||||||
<img src={addIconSrc} alt="Add" className="h-5 w-5" />
|
<img src={addIconSrc} alt="Add" className="h-5 w-5" />
|
||||||
@ -459,7 +642,7 @@ const UnitMaster = () => {
|
|||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex justify-center items-center h-64">
|
<div className="flex justify-center items-center h-64">
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-gray-900"></div>
|
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-[#92722A]"></div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Table
|
<Table
|
||||||
@ -517,7 +700,7 @@ const UnitMaster = () => {
|
|||||||
currentPage,
|
currentPage,
|
||||||
onPageChange: setCurrentPage,
|
onPageChange: setCurrentPage,
|
||||||
pageSize,
|
pageSize,
|
||||||
totalItems: filteredRows.length,
|
totalItems: filteredUnits.length,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -616,26 +799,17 @@ const UnitMaster = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-6 py-5">
|
<div className="px-6 py-5">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div>
|
|
||||||
<TextField
|
<TextField
|
||||||
label={
|
label={
|
||||||
<>
|
<>
|
||||||
Unit Name <span className="text-red-500">*</span>
|
Unit Name <span className="text-red-500">*</span>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
value={form.unitName}
|
value={form.uom}
|
||||||
onChange={handleFormChange('unitName')}
|
onChange={handleFormChange('uom')}
|
||||||
placeholder="Enter Unit Name"
|
placeholder="Enter Unit Name"
|
||||||
error={formErrors.unitName}
|
|
||||||
className={formErrors.unitName ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : ''}
|
|
||||||
/>
|
/>
|
||||||
{formErrors.unitName && (
|
|
||||||
<p className="mt-1 text-sm text-red-600"></p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<TextField
|
<TextField
|
||||||
label={
|
label={
|
||||||
<>
|
<>
|
||||||
@ -645,31 +819,24 @@ const UnitMaster = () => {
|
|||||||
value={form.description}
|
value={form.description}
|
||||||
onChange={handleFormChange('description')}
|
onChange={handleFormChange('description')}
|
||||||
placeholder="Enter Description"
|
placeholder="Enter Description"
|
||||||
width="100%"
|
|
||||||
error={formErrors.description}
|
|
||||||
className={formErrors.description ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : ''}
|
|
||||||
/>
|
/>
|
||||||
{formErrors.description && (
|
|
||||||
<p className="mt-1 text-sm text-red-600"></p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 flex items-center justify-end gap-3">
|
<div className="mt-6 flex items-center justify-end gap-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="h-10 px-6 rounded-md border border-[#92722A] text-sm font-medium text-[#92722A] bg-white"
|
className="h-10 px-6 rounded-md border border-[#92722A] text-sm font-medium text-[#92722A] bg-white hover:bg-gray-50"
|
||||||
onClick={closeModal}
|
onClick={closeModal}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="h-10 px-4 rounded-[6px] bg-[#92722A] text-white text-sm font-medium disabled:opacity-50"
|
className="h-10 px-6 rounded-[6px] bg-[#92722A] text-white text-sm font-medium hover:bg-[#7a5f22] disabled:opacity-50"
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={loading}
|
disabled={saveLoading}
|
||||||
>
|
>
|
||||||
{loading ? (
|
{saveLoading ? (
|
||||||
<div className="flex items-center justify-center">
|
<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>
|
<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...'}
|
{modalMode === 'edit' ? 'Updating...' : 'Saving...'}
|
||||||
@ -708,7 +875,7 @@ const UnitMaster = () => {
|
|||||||
<p className="mt-1 text-sm leading-5 text-[#4B5563]">
|
<p className="mt-1 text-sm leading-5 text-[#4B5563]">
|
||||||
Are you sure you want to delete{' '}
|
Are you sure you want to delete{' '}
|
||||||
<span className="font-semibold text-[#232528]">
|
<span className="font-semibold text-[#232528]">
|
||||||
{`${deletingUnit?.unitName || deletingUnit?.uom || 'N/A'} - ${deletingUnit?.description || deletingUnit?.uom_short_name || 'N/A'}`}
|
{`${deletingUnit?.uom || 'N/A'} - ${deletingUnit?.description || 'N/A'}`}
|
||||||
</span>
|
</span>
|
||||||
?
|
?
|
||||||
</p>
|
</p>
|
||||||
@ -721,14 +888,14 @@ const UnitMaster = () => {
|
|||||||
<div className="flex items-center justify-end gap-4">
|
<div className="flex items-center justify-end gap-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="h-10 px-6 rounded-md border border-[#92722A] text-sm font-medium text-[#92722A] bg-white"
|
className="h-10 px-6 rounded-md border border-[#92722A] text-sm font-medium text-[#92722A] bg-white hover:bg-gray-50"
|
||||||
onClick={closeDeleteConfirm}
|
onClick={closeDeleteConfirm}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="h-10 px-6 rounded-md bg-[#92722A] text-white text-sm font-semibold cursor-pointer shadow-sm"
|
className="h-10 px-6 rounded-md bg-[#92722A] text-white text-sm font-semibold cursor-pointer shadow-sm hover:bg-[#7a5f22]"
|
||||||
onClick={handleDeleteConfirm}
|
onClick={handleDeleteConfirm}
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
@ -739,6 +906,129 @@ const UnitMaster = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 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 Units</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>
|
||||||
|
</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"
|
||||||
|
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 */}
|
{/* Custom Toast Notification */}
|
||||||
{toastData && (
|
{toastData && (
|
||||||
<div className="fixed inset-0 z-[9999] pointer-events-none flex items-start justify-center pt-20">
|
<div className="fixed inset-0 z-[9999] pointer-events-none flex items-start justify-center pt-20">
|
||||||
|
|||||||
@ -15,20 +15,33 @@ export const getUnits = async () => {
|
|||||||
|
|
||||||
export const createUnit = async (unitData) => {
|
export const createUnit = async (unitData) => {
|
||||||
try {
|
try {
|
||||||
|
// Log the data being sent for debugging
|
||||||
|
console.log('Creating unit with data:', unitData);
|
||||||
|
|
||||||
const response = await postRequest(UNIT_MASTER_ENDPOINT, unitData);
|
const response = await postRequest(UNIT_MASTER_ENDPOINT, unitData);
|
||||||
|
console.log('Unit created successfully:', response.data);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating unit:', error);
|
console.error('Error creating unit:', error);
|
||||||
|
console.error('Error details:', {
|
||||||
|
status: error.response?.status,
|
||||||
|
data: error.response?.data,
|
||||||
|
headers: error.response?.headers
|
||||||
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateUnit = async (id, unitData) => {
|
export const updateUnit = async (id, unitData) => {
|
||||||
try {
|
try {
|
||||||
|
console.log('Updating unit with data:', unitData);
|
||||||
|
|
||||||
const response = await putRequest(`${UNIT_MASTER_ENDPOINT}/${id}`, unitData);
|
const response = await putRequest(`${UNIT_MASTER_ENDPOINT}/${id}`, unitData);
|
||||||
|
console.log('Unit updated successfully:', response.data);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating unit:', error);
|
console.error('Error updating unit:', error);
|
||||||
|
console.error('Error details:', error.response?.data);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -36,9 +49,25 @@ export const updateUnit = async (id, unitData) => {
|
|||||||
export const deleteUnit = async (id) => {
|
export const deleteUnit = async (id) => {
|
||||||
try {
|
try {
|
||||||
const response = await deleteRequest(`${UNIT_MASTER_ENDPOINT}/${id}`);
|
const response = await deleteRequest(`${UNIT_MASTER_ENDPOINT}/${id}`);
|
||||||
|
console.log('Unit deleted successfully:', response.data);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting unit:', error);
|
console.error('Error deleting unit:', error);
|
||||||
|
console.error('Error details:', error.response?.data);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const uploadUnitCSV = async (formData) => {
|
||||||
|
try {
|
||||||
|
const response = await postRequest(`${UNIT_MASTER_ENDPOINT}/uploadCSV`, formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error uploading unit CSV:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Loading…
Reference in New Issue
Block a user