changes in survey page and submission history
This commit is contained in:
parent
2bbce71b96
commit
492931d488
@ -0,0 +1,3 @@
|
||||
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.125 0C6.51803 0 4.94714 0.476523 3.611 1.36931C2.27485 2.2621 1.23344 3.53105 0.618482 5.0157C0.00352044 6.50035 -0.157382 8.13401 0.156123 9.71011C0.469628 11.2862 1.24346 12.7339 2.37976 13.8702C3.51606 15.0065 4.9638 15.7804 6.5399 16.0939C8.11599 16.4074 9.74966 16.2465 11.2343 15.6315C12.719 15.0166 13.9879 13.9752 14.8807 12.639C15.7735 11.3029 16.25 9.73197 16.25 8.125C16.2477 5.97081 15.391 3.90551 13.8677 2.38227C12.3445 0.85903 10.2792 0.00227486 8.125 0ZM7.8125 3.75C7.99792 3.75 8.17918 3.80498 8.33335 3.908C8.48752 4.01101 8.60768 4.15743 8.67864 4.32873C8.7496 4.50004 8.76816 4.68854 8.73199 4.8704C8.69582 5.05225 8.60653 5.2193 8.47542 5.35041C8.3443 5.48152 8.17726 5.57081 7.9954 5.60699C7.81354 5.64316 7.62504 5.62459 7.45374 5.55364C7.28243 5.48268 7.13601 5.36252 7.033 5.20835C6.92999 5.05418 6.875 4.87292 6.875 4.6875C6.875 4.43886 6.97378 4.2004 7.14959 4.02459C7.32541 3.84877 7.56386 3.75 7.8125 3.75ZM8.75 12.5C8.41848 12.5 8.10054 12.3683 7.86612 12.1339C7.6317 11.8995 7.5 11.5815 7.5 11.25V8.125C7.33424 8.125 7.17527 8.05915 7.05806 7.94194C6.94085 7.82473 6.875 7.66576 6.875 7.5C6.875 7.33424 6.94085 7.17527 7.05806 7.05806C7.17527 6.94085 7.33424 6.875 7.5 6.875C7.83152 6.875 8.14947 7.0067 8.38389 7.24112C8.61831 7.47554 8.75 7.79348 8.75 8.125V11.25C8.91576 11.25 9.07474 11.3158 9.19195 11.4331C9.30916 11.5503 9.375 11.7092 9.375 11.875C9.375 12.0408 9.30916 12.1997 9.19195 12.3169C9.07474 12.4342 8.91576 12.5 8.75 12.5Z" fill="#286CFF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@ -77,14 +77,56 @@ function App() {
|
||||
</RequireRole>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path="/admin/configuration"
|
||||
element={(
|
||||
<RequireRole allowedRoles={['Admin']}>
|
||||
<Configuration />
|
||||
</RequireRole>
|
||||
)}
|
||||
/>
|
||||
<Route path="/admin/configuration">
|
||||
<Route
|
||||
index
|
||||
element={
|
||||
<RequireRole allowedRoles={['Admin','EstablishmentUser']}>
|
||||
<Configuration />
|
||||
</RequireRole>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="quarterly"
|
||||
element={
|
||||
<RequireRole allowedRoles={['Admin']}>
|
||||
<Configuration />
|
||||
</RequireRole>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="hscodes"
|
||||
element={
|
||||
<RequireRole allowedRoles={['Admin']}>
|
||||
<Configuration />
|
||||
</RequireRole>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="unitmaster"
|
||||
element={
|
||||
<RequireRole allowedRoles={['Admin']}>
|
||||
<Configuration />
|
||||
</RequireRole>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="admin-users"
|
||||
element={
|
||||
<RequireRole allowedRoles={['Admin']}>
|
||||
<Configuration />
|
||||
</RequireRole>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="profile"
|
||||
element={
|
||||
<RequireRole allowedRoles={['Admin', 'EstablishmentUser']}>
|
||||
<Configuration />
|
||||
</RequireRole>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path="/admin/users"
|
||||
element={(
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ChevronLeft, ChevronRight, Pause } from 'lucide-react';
|
||||
|
||||
const SurveyCarousel = ({
|
||||
@ -9,6 +10,7 @@ const SurveyCarousel = ({
|
||||
}) => {
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoRotate || isPaused) return;
|
||||
@ -45,7 +47,10 @@ const SurveyCarousel = ({
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => onStartSurvey?.(current)}
|
||||
onClick={() => {
|
||||
onStartSurvey?.(current);
|
||||
navigate('/survey', { state: { survey: current } });
|
||||
}}
|
||||
className="inline-flex items-center justify-center w-[196px] h-[52px] px-7 py-3 text-[18px] leading-[28px] font-medium rounded-lg text-white bg-[#92722A] hover:bg-[#7b5c1f] cursor-pointer whitespace-nowrap"
|
||||
>
|
||||
Start Survey Now
|
||||
|
||||
@ -6,6 +6,7 @@ const phoneIconSrc = '/assets/images/line-md_phone.svg';
|
||||
const calendarIcon = '/assets/images/Duedate.svg';
|
||||
const clockIcon = '/assets/images/mingcute_time-line.svg';
|
||||
const nextIcon = '/assets/images/mdi_page-next-outline.svg';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const Field = ({ label, placeholder = '', type = 'text', value = '', onChange = () => {}, disabled = true }) => (
|
||||
<div>
|
||||
@ -75,9 +76,11 @@ const EstablishmentInfo = ({
|
||||
loading = false,
|
||||
error = '',
|
||||
isComplete = false,
|
||||
|
||||
}) => {
|
||||
const [showInfoCard, setShowInfoCard] = React.useState(true);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleCloseInfo = () => {
|
||||
setShowInfoCard(false);
|
||||
};
|
||||
@ -107,6 +110,28 @@ const EstablishmentInfo = ({
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const handleEditProfile = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Get establishment_id directly from sessionStorage
|
||||
const establishmentId = sessionStorage.getItem('establishment_id');
|
||||
|
||||
if (!establishmentId) {
|
||||
alert('No establishment ID found in session');
|
||||
return;
|
||||
}
|
||||
|
||||
// Save current page to return after editing (optional)
|
||||
localStorage.setItem('returnTo', window.location.pathname);
|
||||
|
||||
// Store context info
|
||||
sessionStorage.setItem('edit_profile_from', 'EstablishmentUser');
|
||||
|
||||
// Navigate to profile edit page
|
||||
navigate(`/admin/configuration/profile?edit=${establishmentId}`);
|
||||
};
|
||||
|
||||
const handleEmployeeChange = (field) => (event) => {
|
||||
onChange({
|
||||
...info,
|
||||
@ -177,18 +202,18 @@ const EstablishmentInfo = ({
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<ul className="text-sm text-[#6C4527] list-disc pl-5 space-y-1">
|
||||
<li><b>After submission</b>: responses are locked. A receipt appears in History after submission</li>
|
||||
<li><b>After submission</b>: responses are used for statistical reporting and handled per our privacy policy</li>
|
||||
<li><b>After submission</b>: Your responses are locked. A receipt will be available in History.</li>
|
||||
<li className="whitespace-nowrap"><b>Privacy</b>: Your information is collected for statistical purposes only and handled in accordance with our Privacy Policy. Results are published only in aggregated form.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="flex items-center gap-6 mt-2 text-sm text-[#6C4527]">
|
||||
<div className="flex items-center gap-2">
|
||||
<img src={mailIconSrc} alt="email" className="w-4 h-4" />
|
||||
<span>support@ipi.gov.example</span>
|
||||
<span><b>Support Email:</b> support@ipi.gov.example</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<img src={phoneIconSrc} alt="phone" className="w-4 h-4" />
|
||||
<span>+971-2-000-0000</span>
|
||||
<span><b>Contact:</b>+971-2-000-0000</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -223,14 +248,10 @@ const EstablishmentInfo = ({
|
||||
<p className="text-sm text-[#043DFF] bg-[#E7F5FF]">
|
||||
<span className="font-medium">Need to update details?</span> Go to Profile →{' '}
|
||||
<a
|
||||
href={`/admin/configuration/${data.id || ''}`}
|
||||
href={`/admin/configuration/profile?edit=${sessionStorage.getItem('establishment_id') || ''}`}
|
||||
className="underline font-medium text-[#043DFF] hover:text-[#063B82]"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
// Store current URL to return after login
|
||||
localStorage.setItem('returnTo', window.location.pathname);
|
||||
window.location.href = e.currentTarget.href;
|
||||
}}
|
||||
onClick={handleEditProfile}
|
||||
|
||||
>
|
||||
Edit Profile
|
||||
</a>
|
||||
@ -297,6 +318,7 @@ const EstablishmentInfo = ({
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Establishment Information Form */}
|
||||
<Card>
|
||||
@ -347,7 +369,8 @@ const EstablishmentInfo = ({
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Employee Information */}
|
||||
<div className="mt-6">
|
||||
{/* Employee Information */}
|
||||
<Card>
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-4">Employee Information</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@ -403,7 +426,7 @@ const EstablishmentInfo = ({
|
||||
</Card>
|
||||
|
||||
{/* Footer actions */}
|
||||
<div className="flex flex-col sm:flex-row justify-between gap-3">
|
||||
<div className="mt-8 flex flex-col sm:flex-row justify-between gap-3">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="inline-flex items-center gap-2 h-9 px-4 rounded-md border border-[#92722A] text-[#92722A] bg-transparent hover:bg-[#92722A]/10"
|
||||
|
||||
@ -25,7 +25,7 @@ const Input = ({ placeholder = '', type = 'text', value = '', onChange = () => {
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
required={required}
|
||||
className={`block w-full h-10 rounded-md border-2 ${borderColor} focus:ring-0 px-4 text-sm bg-white transition-colors`}
|
||||
className={`block w-full h-10 rounded-md border-2 ${borderColor} focus:ring-0 px-4 text-sm bg-white transition-colors [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@ -157,6 +157,7 @@ const ProductData = ({
|
||||
const [isLoadingUnits, setIsLoadingUnits] = React.useState(false);
|
||||
const [unitsError, setUnitsError] = React.useState('');
|
||||
const [isInitialLoad, setIsInitialLoad] = React.useState(true);
|
||||
const [remarks, setRemarks] = React.useState('');
|
||||
|
||||
React.useEffect(() => {
|
||||
const variationController = new AbortController();
|
||||
@ -429,16 +430,7 @@ const ProductData = ({
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Left card - Data definitions */}
|
||||
{showDataDefinitions && (
|
||||
<div className="bg-[#F2ECCF] rounded-md p-4 text-[#6C4527] text-sm relative">
|
||||
<button
|
||||
onClick={() => setShowDataDefinitions(false)}
|
||||
className="absolute top-2 right-2 text-gray-600 hover:text-gray-800"
|
||||
aria-label="Close data definitions"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
<div className="bg-[#F2ECCF] rounded-md p-4 text-[#6C4527] text-sm">
|
||||
<h3 className="font-semibold mb-4 text-base">Data definitions</h3>
|
||||
<p className="mb-3"><strong>Product (HS Code – Name):</strong> Standard trade classification. Start typing to search by code or name.</p>
|
||||
<p className="mb-3"><strong>Quantity (Qty):</strong> Physical output produced in the month, measured in the selected unit.</p>
|
||||
@ -449,16 +441,7 @@ const ProductData = ({
|
||||
|
||||
{/* Right card - Entry rules */}
|
||||
{showEntryRules && (
|
||||
<div className="bg-[#F2ECCF] rounded-md p-4 text-[#6C4527] text-sm relative">
|
||||
<button
|
||||
onClick={() => setShowEntryRules(false)}
|
||||
className="absolute top-2 right-2 text-gray-600 hover:text-gray-800"
|
||||
aria-label="Close entry rules"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
<div className="bg-[#F2ECCF] rounded-md p-4 text-[#6C4527] text-sm">
|
||||
<h3 className="font-semibold mb-4 text-base">Entry rules & validation</h3>
|
||||
<p className="mb-3"><strong>Mandatory:</strong> All fields in this step are required except "Reason for variation/0 Target" and free-text Notes.</p>
|
||||
<p className="mb-3"><strong>Monthly entries:</strong> Enter Qty and Cost for each month in the Current and Forecast quarters. Use 0 where there is no output or cost.</p>
|
||||
@ -703,7 +686,7 @@ const ProductData = ({
|
||||
|
||||
<SectionBox title="NEXT QUARTER FORECAST (Q2-2025)" badgeBg="#FFF7E9" badgeText="#F29F0E">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-end space-x-4">
|
||||
<div className="flex items-end space-x-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Apr Qty<span className="text-red-600">*</span></label>
|
||||
<Input
|
||||
@ -715,17 +698,20 @@ const ProductData = ({
|
||||
onChange={(e) => updateProductField(p.id, 'aprQuantity', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">May Qty<span className="text-red-600">*</span></label>
|
||||
<div className="flex flex-col whitespace-nowrap">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1 whitespace-nowrap">
|
||||
May Cost (AED)<span className="text-red-600">*</span>
|
||||
</label>
|
||||
<Input
|
||||
className="w-24"
|
||||
className="w-28" // Slightly wider input for balance
|
||||
type="number"
|
||||
required
|
||||
placeholder="Enter"
|
||||
value={p.mayQuantity || ''}
|
||||
onChange={(e) => updateProductField(p.id, 'mayQuantity', e.target.value)}
|
||||
value={p.mayCost || ''}
|
||||
onChange={(e) => updateProductField(p.id, 'mayCost', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Jun Qty<span className="text-red-600">*</span></label>
|
||||
<Input
|
||||
@ -792,7 +778,24 @@ const ProductData = ({
|
||||
</Card>
|
||||
))}
|
||||
|
||||
<div className="flex flex-col sm:flex-row justify-between gap-3">
|
||||
{/* Remarks Section */}
|
||||
<SectionBox
|
||||
title="Remarks"
|
||||
// Text="#232528"
|
||||
// className="bg-white [&_.badge]:bg-transparent [&_.badge]:shadow-none"
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<Textarea
|
||||
placeholder="Enter your remarks here..."
|
||||
value={remarks}
|
||||
onChange={(e) => setRemarks(e.target.value)}
|
||||
className="min-h-[60px]"
|
||||
/>
|
||||
</div>
|
||||
</SectionBox>
|
||||
|
||||
|
||||
<div className="flex flex-col sm:flex-row justify-between gap-5 mt-6">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="inline-flex items-center gap-2 h-9 px-4 rounded-md border border-[#92722A] text-[#92722A] bg-transparent hover:bg-[#92722A]/10"
|
||||
|
||||
@ -11,7 +11,7 @@ const maleEmployeesIconSrc = '/assets/images/maleemployes.svg';
|
||||
const femaleEmployeesIconSrc = '/assets/images/femaleemployess.svg';
|
||||
const uaeIconSrc = '/assets/images/uae.svg';
|
||||
const nonUaeIconSrc = '/assets/images/nonuae.svg';
|
||||
|
||||
const agreeIconSrc = '/assets/images/Vector-agree.svg';
|
||||
const Card = ({ title, children, className = '' }) => (
|
||||
<div className={`bg-white rounded-lg border border-gray-200 overflow-hidden ${className}`}>
|
||||
{title && (
|
||||
@ -439,7 +439,7 @@ const ReviewSubmit = ({
|
||||
<span>I hereby confirm that all data entered is accurate and complete to the best of my knowledge.</span>
|
||||
</label>
|
||||
</div> */}
|
||||
|
||||
<h2 className="text-xl font-semibold text-[#232528] mb-2">Review & Submit</h2>
|
||||
<div className="space-y-6">
|
||||
{/* Establishment and Employment Information */}
|
||||
<section className="min-h-[219px] rounded-[16px] border border-[#E6EAF5] bg-white px-0 py-6 shadow-[0_16px_32px_rgba(15,23,42,0.08)]">
|
||||
@ -713,10 +713,19 @@ const ReviewSubmit = ({
|
||||
{/* Confirmation and Buttons */}
|
||||
<div className="rounded-[16px] border border-[#E6EAF5] bg-white p-6 shadow-[0_16px_32px_rgba(15,23,42,0.08)]">
|
||||
{!hasSubmitted && (
|
||||
<div className="mb-6 rounded-md bg-[#E7F0FF] border border-[#C6DBFF] px-4 py-3 text-sm text-[#2563EB]">
|
||||
Review your data carefully before submission. You can go back to previous steps to edit any details. Once submitted, changes will require admin approval.
|
||||
<div className="mb-6 flex items-start gap-2 rounded-md bg-[#E7F0FF] border border-[#C6DBFF] px-4 py-3 text-sm text-[#2563EB]">
|
||||
<img
|
||||
src={agreeIconSrc}
|
||||
alt="Info"
|
||||
className="w-5 h-4 mt-[2px] flex-shrink-0"
|
||||
/>
|
||||
<p className="leading-snug">
|
||||
Review your data carefully before submission. You can go back to previous steps to edit any details. Once submitted, changes will require admin approval.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{/* </div> */}
|
||||
|
||||
|
||||
<div className="bg-white rounded-md">
|
||||
<label className="flex items-start gap-3 text-sm text-[#4B5563]">
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
||||
import AdminHeader from '@/components/admin/AdminHeader';
|
||||
import QuarterlyWindows from '@/pages/Admin/configuration/QuarterlyWindows';
|
||||
import IsicHsCodes from '@/pages/Admin/configuration/IsicHsCodes';
|
||||
@ -10,9 +10,42 @@ import CompanyProfile from '@/pages/Admin/configuration/CompanyProfile';
|
||||
const caretUpSrc = '/assets/images/caret-up.svg';
|
||||
|
||||
const Configuration = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const tabs = ['Quarterly Windows', 'HS Codes', 'Unit Master', 'Admin Users', 'Company Profile'];
|
||||
const [active, setActive] = React.useState(tabs[0]);
|
||||
|
||||
// Update URL when active tab changes
|
||||
useEffect(() => {
|
||||
const tabToPath = {
|
||||
'Quarterly Windows': 'quarterly',
|
||||
'HS Codes': 'hscodes',
|
||||
'Unit Master': 'unitmaster',
|
||||
'Admin Users': 'admin-users',
|
||||
'Company Profile': 'profile'
|
||||
};
|
||||
|
||||
if (tabToPath[active]) {
|
||||
navigate(`/admin/configuration/${tabToPath[active]}`, { replace: true });
|
||||
}
|
||||
}, [active, navigate]);
|
||||
|
||||
// Update active tab based on URL
|
||||
useEffect(() => {
|
||||
const pathToTab = {
|
||||
'quarterly': 'Quarterly Windows',
|
||||
'hscodes': 'HS Codes',
|
||||
'unitmaster': 'Unit Master',
|
||||
'admin-users': 'Admin Users',
|
||||
'profile': 'Company Profile'
|
||||
};
|
||||
|
||||
const path = location.pathname.split('/').pop();
|
||||
if (pathToTab[path] && pathToTab[path] !== active) {
|
||||
setActive(pathToTab[path]);
|
||||
}
|
||||
}, [location.pathname]);
|
||||
|
||||
const Breadcrumbs = (
|
||||
<nav className="mb-6 text-sm text-[#8F9299] flex items-center gap-3">
|
||||
<Link to="/admin/dashboard" className="flex items-center gap-1 text-[#232528] hover:underline">
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import Table from '@/components/common/Table';
|
||||
import Loader from '@/components/common/Loader';
|
||||
import { TextField, SelectField, PhoneField } from '@/components/common/FormControls';
|
||||
@ -281,6 +282,7 @@ const createEmptyProfile = () => ({
|
||||
// ];
|
||||
|
||||
const CompanyProfile = () => {
|
||||
const navigate = useNavigate();
|
||||
const [profiles, setProfiles] = React.useState([]);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const [form, setForm] = React.useState(createEmptyProfile());
|
||||
@ -1553,6 +1555,9 @@ const CompanyProfile = () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Update URL with the establishment ID
|
||||
navigate(`?edit=${encodeURIComponent(identifier)}`);
|
||||
if (!identifier) return;
|
||||
const originalIndex = profiles.findIndex((item) => {
|
||||
if (item.apiId !== null && item.apiId !== undefined && item.apiId === identifier) {
|
||||
@ -1631,6 +1636,9 @@ const CompanyProfile = () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear any edit ID from URL when adding new
|
||||
navigate('?add=new');
|
||||
const emptyProfile = createEmptyProfile();
|
||||
Object.assign(emptyProfile, computeEmploymentTotals(emptyProfile));
|
||||
setModalMode('add');
|
||||
@ -1665,21 +1673,18 @@ const CompanyProfile = () => {
|
||||
const closeModal = () => {
|
||||
if (isDirty) {
|
||||
const shouldClose = window.confirm('You have unsaved changes. Do you really want to discard them?');
|
||||
if (!shouldClose) {
|
||||
return;
|
||||
}
|
||||
if (!shouldClose) return;
|
||||
}
|
||||
const emptyProfile = createEmptyProfile();
|
||||
Object.assign(emptyProfile, computeEmploymentTotals(emptyProfile));
|
||||
setModalMode(null);
|
||||
setForm(emptyProfile);
|
||||
setForm(createEmptyProfile());
|
||||
setEditingRow(null);
|
||||
setConfirmError('');
|
||||
setIsSaving(false);
|
||||
setSaveWarning(false);
|
||||
setErrors({});
|
||||
setIsDirty(false);
|
||||
initialSnapshotRef.current = emptyProfile;
|
||||
corporateBackupRef.current = captureCorporateValues(emptyProfile);
|
||||
// Clear URL parameters when closing modal
|
||||
navigate('');
|
||||
setSaveWarning(false);
|
||||
initialSnapshotRef.current = createEmptyProfile();
|
||||
corporateBackupRef.current = captureCorporateValues(createEmptyProfile());
|
||||
};
|
||||
|
||||
const closeDeleteConfirm = () => {
|
||||
|
||||
@ -1,16 +1,20 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import Table from '@/components/common/Table';
|
||||
import StatusBadge from '@/components/common/StatusBadge';
|
||||
import { TextField, SelectField } from '@/components/common/FormControls';
|
||||
import { productService } from '@/services/configuration/productService';
|
||||
import masterService from '@/services/masters/masterService';
|
||||
|
||||
const downloadIconSrc = '/assets/images/DownloadSimple.svg';
|
||||
const uploadImportIconSrc = '/assets/images/Upload-import.svg';
|
||||
const addIconSrc = '/assets/images/ic_baseline-plus.svg';
|
||||
const pencilActiveSrc = '/assets/images/PencilSimple.svg';
|
||||
const pencilInactiveSrc = '/assets/images/PencilSimple-inactive.svg';
|
||||
const pencilInactiveSrc = '/assets/images/pencilsimple-inactive.svg';
|
||||
const trashActiveSrc = '/assets/images/Trash - active.svg';
|
||||
const trashInactiveSrc = '/assets/images/Trash - Inactive.svg';
|
||||
const deleteIconSrc = '/assets/images/delete.svg';
|
||||
const eyeIconSrc = '/assets/images/Eye.svg';
|
||||
const searchIconSrc = '/assets/images/material-symbols_search-rounded.svg';
|
||||
|
||||
const createEmptyCodeForm = () => ({
|
||||
code: '',
|
||||
@ -19,22 +23,30 @@ const createEmptyCodeForm = () => ({
|
||||
status: '',
|
||||
});
|
||||
|
||||
// Toast notification component
|
||||
const Toast = ({ message, onClose }) => {
|
||||
React.useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
onClose();
|
||||
}, 3000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [onClose]);
|
||||
|
||||
return (
|
||||
<div className="fixed top-0 left-0 w-full flex justify-center z-50 mt-4">
|
||||
<div className="bg-[#F3FAF4] text-[#3F8E50] px-6 py-3 rounded-md shadow-lg font-medium">
|
||||
{message}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const IsicHsCodes = () => {
|
||||
const [rowsData, setRowsData] = React.useState([
|
||||
{ code: '0111', product: 'Wheat', unit: 'Kg', estimatedMapped: 18, updated: '31/03/2025', status: 'Active' },
|
||||
{ code: '0112', product: 'Barley', unit: 'Kg', estimatedMapped: 22, updated: '15/05/2025', status: 'Active' },
|
||||
{ code: '0113', product: 'Oats', unit: 'Kg', estimatedMapped: 20, updated: '10/06/2025', status: 'Active' },
|
||||
{ code: '0114', product: 'Rice', unit: 'Kg', estimatedMapped: 25, updated: '01/07/2025', status: 'Active' },
|
||||
{ code: '0115', product: 'Corn', unit: 'Kg', estimatedMapped: 30, updated: '20/08/2025', status: 'Active' },
|
||||
{ code: '0116', product: 'Soybeans', unit: 'Kg', estimatedMapped: 28, updated: '15/09/2025', status: 'Active' },
|
||||
{ code: '0117', product: 'Sorghum', unit: 'Kg', estimatedMapped: 19, updated: '12/10/2025', status: 'Active' },
|
||||
{ code: '0118', product: 'Millet', unit: 'Kg', estimatedMapped: 21, updated: '18/11/2025', status: 'Active' },
|
||||
{ code: '0119', product: 'Quinoa', unit: 'Kg', estimatedMapped: 27, updated: '25/12/2025', status: 'Active' },
|
||||
{ code: '0120', product: 'Rye', unit: 'Kg', estimatedMapped: 16, updated: '04/01/2026', status: 'Active' },
|
||||
{ code: '0121', product: 'Buckwheat', unit: 'Kg', estimatedMapped: 12, updated: '18/01/2026', status: 'Active' },
|
||||
{ code: '0122', product: 'Barley Malt', unit: 'Tones', estimatedMapped: 14, updated: '05/02/2026', status: 'Inactive' },
|
||||
{ code: '0123', product: 'Chickpeas', unit: 'Kg', estimatedMapped: 23, updated: '20/02/2026', status: 'Active' },
|
||||
]);
|
||||
const [rowsData, setRowsData] = React.useState([]);
|
||||
const [filteredData, setFilteredData] = React.useState([]);
|
||||
const [searchTerm, setSearchTerm] = React.useState('');
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
const [error, setError] = React.useState(null);
|
||||
const [editingRow, setEditingRow] = React.useState(null);
|
||||
const [deletingRow, setDeletingRow] = React.useState(null);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = React.useState(false);
|
||||
@ -42,29 +54,203 @@ const IsicHsCodes = () => {
|
||||
const pageSize = 10;
|
||||
const [modalMode, setModalMode] = React.useState(null);
|
||||
const [form, setForm] = React.useState(createEmptyCodeForm());
|
||||
const [unitOptions, setUnitOptions] = React.useState([]);
|
||||
const [toast, setToast] = React.useState({ show: false, message: '' });
|
||||
|
||||
const headers = ['Code', 'Product Name', 'Unit', 'Est mapped', 'Last Updated', 'Status', 'Actions'];
|
||||
const rows = rowsData.map((item) => [
|
||||
// Fetch units on mount
|
||||
useEffect(() => {
|
||||
const fetchUnits = async () => {
|
||||
console.log('Starting to fetch units...');
|
||||
setIsLoading(true);
|
||||
try {
|
||||
console.log('Calling masterService.fetchUnits()...');
|
||||
const units = await masterService.fetchUnits();
|
||||
console.log('Fetched units:', units);
|
||||
|
||||
if (units && units.length > 0) {
|
||||
const formattedUnits = units.map(unit => ({
|
||||
label: unit.label || 'N/A',
|
||||
value: unit.value || 'N/A'
|
||||
}));
|
||||
|
||||
console.log('Formatted units:', formattedUnits);
|
||||
setUnitOptions(formattedUnits);
|
||||
setError(null);
|
||||
} else {
|
||||
console.warn('No units found in the API response');
|
||||
setError('No units available');
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = `Failed to fetch units: ${error.message}`;
|
||||
console.error(errorMsg, {
|
||||
response: error.response,
|
||||
config: error.config,
|
||||
stack: error.stack
|
||||
});
|
||||
setError(errorMsg);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchUnits();
|
||||
}, []);
|
||||
|
||||
// Fetch products on mount
|
||||
useEffect(() => {
|
||||
const fetchProducts = async () => {
|
||||
try {
|
||||
console.log('Fetching products...');
|
||||
setIsLoading(true);
|
||||
const response = await productService.getProducts();
|
||||
console.log('Complete API Response:', JSON.stringify(response, null, 2));
|
||||
|
||||
if (response && response.data) {
|
||||
const products = Array.isArray(response.data)
|
||||
? response.data
|
||||
: response.data.data || [];
|
||||
|
||||
if (products.length > 0) {
|
||||
const formattedData = products.map((product) => ({
|
||||
id: product.id,
|
||||
code: product.hs_code || 'N/A',
|
||||
product: product.product_name || 'N/A',
|
||||
unit: 'Unit',
|
||||
estimatedMapped: 0,
|
||||
createdBy: product.created_by || 'System',
|
||||
updated: product.updated_at
|
||||
? new Date(product.updated_at).toLocaleDateString()
|
||||
: '-',
|
||||
status: product.is_active ? 'Active' : 'Inactive',
|
||||
description: product.hs_description || '',
|
||||
}));
|
||||
setRowsData(formattedData);
|
||||
setFilteredData(formattedData);
|
||||
} else {
|
||||
setRowsData([]);
|
||||
}
|
||||
} else {
|
||||
setError('Invalid data format received from server');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch products:', err);
|
||||
setError('Failed to load products. Please try again later.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchProducts();
|
||||
}, []);
|
||||
|
||||
const headers = [
|
||||
'Code',
|
||||
'Product Name',
|
||||
'Unit',
|
||||
'Mapped Establishments',
|
||||
'Created by',
|
||||
'Last Updated',
|
||||
'Status',
|
||||
'Actions',
|
||||
];
|
||||
|
||||
// Filter data based on search term
|
||||
React.useEffect(() => {
|
||||
if (!searchTerm.trim()) {
|
||||
setFilteredData(rowsData);
|
||||
} else {
|
||||
const lowercasedSearch = searchTerm.toLowerCase();
|
||||
const filtered = rowsData.filter(
|
||||
(item) =>
|
||||
item.code.toLowerCase().includes(lowercasedSearch) ||
|
||||
item.product.toLowerCase().includes(lowercasedSearch)
|
||||
);
|
||||
setFilteredData(filtered);
|
||||
}
|
||||
setCurrentPage(1); // Reset to first page when searching
|
||||
}, [searchTerm, rowsData]);
|
||||
|
||||
const rows = filteredData.map((item) => [
|
||||
item.code,
|
||||
item.product,
|
||||
item.unit,
|
||||
item.estimatedMapped,
|
||||
item.createdBy || '',
|
||||
item.updated,
|
||||
<StatusBadge status={item.status} tone={item.status === 'Active' ? 'green' : 'gray'} />,
|
||||
item.status,
|
||||
'actions',
|
||||
]);
|
||||
|
||||
const handleEdit = (index) => {
|
||||
const handleView = async (index) => {
|
||||
const selected = rowsData[index];
|
||||
if (!selected) return;
|
||||
setEditingRow(index);
|
||||
setForm({
|
||||
code: selected.code,
|
||||
product: selected.product,
|
||||
unit: selected.unit,
|
||||
status: selected.status,
|
||||
});
|
||||
setModalMode('edit');
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const productId = selected.id;
|
||||
console.log(`Fetching product details for ID: ${productId}`);
|
||||
|
||||
// Make API call to get product details
|
||||
const response = await productService.getProductById(productId);
|
||||
console.log('Product details response:', response);
|
||||
|
||||
if (response && response.data) {
|
||||
const product = response.data;
|
||||
// Map API response to form fields
|
||||
setForm({
|
||||
code: product.hs_code || '',
|
||||
product: product.product_name || '',
|
||||
unit: 'Unit', // Update this if you have unit in the API response
|
||||
status: product.is_active ? 'Active' : 'Inactive',
|
||||
description: product.hs_description || ''
|
||||
});
|
||||
setModalMode('view');
|
||||
} else {
|
||||
console.error('Invalid product data received from API');
|
||||
showToast('Failed to load product details');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching product details:', error);
|
||||
showToast('Error loading product details');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = async (index) => {
|
||||
const selected = rowsData[index];
|
||||
if (!selected) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const productId = selected.id;
|
||||
console.log(`Fetching product details for editing ID: ${productId}`);
|
||||
|
||||
// Fetch the latest product data by ID
|
||||
const response = await productService.getProductById(productId);
|
||||
console.log('Product details for edit:', response);
|
||||
|
||||
if (response && response.data) {
|
||||
const product = response.data;
|
||||
// Map API response to form fields
|
||||
setForm({
|
||||
code: product.hs_code || '',
|
||||
product: product.product_name || '',
|
||||
unit: 'Unit', // Update this if you have unit in the API response
|
||||
status: product.is_active ? 'Active' : 'Inactive',
|
||||
description: product.hs_description || ''
|
||||
});
|
||||
setEditingRow(index);
|
||||
setModalMode('edit');
|
||||
} else {
|
||||
console.error('Invalid product data received from API');
|
||||
showToast('Failed to load product details for editing');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching product details for edit:', error);
|
||||
showToast('Error loading product details for editing');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (index) => {
|
||||
@ -73,7 +259,8 @@ const IsicHsCodes = () => {
|
||||
};
|
||||
|
||||
const deletingCode = React.useMemo(() => {
|
||||
if (deletingRow === null || deletingRow < 0 || deletingRow >= rowsData.length) return null;
|
||||
if (deletingRow === null || deletingRow < 0 || deletingRow >= rowsData.length)
|
||||
return null;
|
||||
return rowsData[deletingRow];
|
||||
}, [deletingRow, rowsData]);
|
||||
|
||||
@ -82,32 +269,64 @@ const IsicHsCodes = () => {
|
||||
setDeletingRow(null);
|
||||
};
|
||||
|
||||
const handleDeleteConfirm = () => {
|
||||
if (deletingRow !== null) {
|
||||
setRowsData((prev) => prev.filter((_, index) => index !== deletingRow));
|
||||
closeDeleteConfirm();
|
||||
const handleDeleteConfirm = async () => {
|
||||
if (deletingRow === null) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const productId = rowsData[deletingRow]?.id;
|
||||
if (!productId) {
|
||||
throw new Error('Product ID not found for deletion');
|
||||
}
|
||||
|
||||
// Call delete API
|
||||
console.log(`Deleting product with ID: ${productId}`);
|
||||
|
||||
try {
|
||||
await productService.deleteProduct(productId);
|
||||
// If we get here, the delete was successful (204 No Content)
|
||||
|
||||
// Remove from local state
|
||||
const newData = [...rowsData];
|
||||
newData.splice(deletingRow, 1);
|
||||
setRowsData(newData);
|
||||
|
||||
showToast('Product deleted successfully!');
|
||||
} catch (apiError) {
|
||||
console.error('API Error:', apiError);
|
||||
// If we get a 204, it's actually a success (No Content)
|
||||
if (apiError.response && apiError.response.status === 204) {
|
||||
// Remove from local state
|
||||
const newData = [...rowsData];
|
||||
newData.splice(deletingRow, 1);
|
||||
setRowsData(newData);
|
||||
|
||||
showToast('Product deleted successfully!');
|
||||
} else {
|
||||
// Handle other errors
|
||||
const errorMessage = apiError.response?.data?.message || 'Failed to delete product. Please try again.';
|
||||
setError(errorMessage);
|
||||
showToast(errorMessage, 'error');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in delete process:', error);
|
||||
const errorMessage = error.message || 'An unexpected error occurred';
|
||||
setError(errorMessage);
|
||||
showToast(errorMessage, 'error');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setShowDeleteConfirm(false);
|
||||
setDeletingRow(null);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredRows = rowsData;
|
||||
// Unit options are now fetched from the API and will be used in the dropdown
|
||||
|
||||
const unitOptions = React.useMemo(
|
||||
() => [
|
||||
{ label: 'Kilograms (Kg)', value: 'Kg' },
|
||||
{ label: 'Tones', value: 'Tones' },
|
||||
{ label: 'Liters', value: 'Liters' },
|
||||
{ label: 'Pounds (Lb)', value: 'Lb' },
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const statusOptions = React.useMemo(
|
||||
() => [
|
||||
{ label: 'Active', value: 'Active' },
|
||||
{ label: 'Inactive', value: 'Inactive' },
|
||||
],
|
||||
[]
|
||||
);
|
||||
const statusOptions = [
|
||||
{ label: 'Active', value: 'Active' },
|
||||
{ label: 'Inactive', value: 'Inactive' },
|
||||
];
|
||||
|
||||
const openAddModal = () => {
|
||||
setEditingRow(null);
|
||||
@ -121,6 +340,13 @@ const IsicHsCodes = () => {
|
||||
setForm(createEmptyCodeForm());
|
||||
};
|
||||
|
||||
const showToast = (message) => {
|
||||
setToast({ show: true, message });
|
||||
setTimeout(() => {
|
||||
setToast(prev => ({ ...prev, show: false }));
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const handleFormChange = (field) => (event) => {
|
||||
const value = event?.target?.value ?? event;
|
||||
setForm((prev) => ({ ...prev, [field]: value }));
|
||||
@ -128,69 +354,180 @@ const IsicHsCodes = () => {
|
||||
|
||||
const getToday = () => {
|
||||
const now = new Date();
|
||||
const day = String(now.getDate()).padStart(2, '0');
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const year = now.getFullYear();
|
||||
return `${day}/${month}/${year}`;
|
||||
return now.toLocaleDateString('en-GB');
|
||||
};
|
||||
|
||||
const handleSaveForm = () => {
|
||||
if (!form.code || !form.product || !form.unit || !form.status) {
|
||||
const handleSaveForm = async () => {
|
||||
console.log('handleSaveForm called with mode:', modalMode);
|
||||
|
||||
if (!form.code || !form.product) {
|
||||
const errorMsg = 'Please fill in all required fields';
|
||||
console.error(errorMsg);
|
||||
setError(errorMsg);
|
||||
return;
|
||||
}
|
||||
if (modalMode === 'edit' && editingRow !== null) {
|
||||
setRowsData((prev) =>
|
||||
prev.map((item, index) =>
|
||||
index === editingRow
|
||||
? {
|
||||
...item,
|
||||
code: form.code,
|
||||
product: form.product,
|
||||
unit: form.unit,
|
||||
status: form.status,
|
||||
}
|
||||
: item
|
||||
)
|
||||
);
|
||||
} else {
|
||||
setRowsData((prev) => [
|
||||
{
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null); // Clear previous errors
|
||||
|
||||
// Always include these fields for both create and update
|
||||
const productData = {
|
||||
product_name: form.product,
|
||||
is_active: form.status === 'Active',
|
||||
hs_code: form.code,
|
||||
...(form.description && { hs_description: form.description })
|
||||
};
|
||||
|
||||
console.log('Saving product data:', JSON.stringify(productData, null, 2));
|
||||
|
||||
if (modalMode === 'edit' && editingRow !== null) {
|
||||
// Update existing product
|
||||
const productId = rowsData[editingRow]?.id;
|
||||
if (!productId) {
|
||||
throw new Error('Product ID not found for editing');
|
||||
}
|
||||
|
||||
console.log(`Updating product with ID: ${productId}`);
|
||||
|
||||
try {
|
||||
// Call the update API with only the necessary fields
|
||||
const response = await productService.updateProduct(productId, productData);
|
||||
console.log('Update response:', response);
|
||||
|
||||
// Update the local state with the updated product data
|
||||
setRowsData(prev =>
|
||||
prev.map(item =>
|
||||
item.id === productId
|
||||
? {
|
||||
...item,
|
||||
code: form.code,
|
||||
product: form.product,
|
||||
status: form.status,
|
||||
description: form.description || '',
|
||||
updated: new Date().toLocaleDateString('en-GB')
|
||||
}
|
||||
: item
|
||||
)
|
||||
);
|
||||
|
||||
console.log('Product updated successfully');
|
||||
showToast('Product updated successfully!');
|
||||
closeModal();
|
||||
return; // Exit the function after successful update
|
||||
} catch (updateError) {
|
||||
console.error('Error updating product:', updateError);
|
||||
const errorMessage = updateError.response?.data?.message || 'Failed to update product. Please try again.';
|
||||
setError(errorMessage);
|
||||
return; // Exit the function after error
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, it's a create operation
|
||||
console.log('Creating new product...');
|
||||
try {
|
||||
const response = await productService.createProduct(productData);
|
||||
console.log('Create response:', response);
|
||||
|
||||
const newProduct = {
|
||||
id: response.data?.id || Date.now(), // Fallback to timestamp if no ID in response
|
||||
code: form.code,
|
||||
product: form.product,
|
||||
unit: form.unit,
|
||||
unit: 'Unit',
|
||||
estimatedMapped: 0,
|
||||
updated: getToday(),
|
||||
createdBy: 'Current User',
|
||||
updated: new Date().toLocaleDateString('en-GB'),
|
||||
status: form.status,
|
||||
},
|
||||
...prev,
|
||||
]);
|
||||
description: form.description || ''
|
||||
};
|
||||
|
||||
setRowsData(prev => [...prev, newProduct]);
|
||||
console.log('Product created successfully');
|
||||
showToast('Product created successfully!');
|
||||
closeModal();
|
||||
} catch (createError) {
|
||||
console.error('Error in product creation:', createError);
|
||||
console.error('Error details:', {
|
||||
message: createError.message,
|
||||
response: createError.response,
|
||||
config: createError.config
|
||||
});
|
||||
|
||||
let errorMessage = 'Failed to create product. Please try again.';
|
||||
if (createError.response?.data?.message) {
|
||||
errorMessage = createError.response.data.message;
|
||||
}
|
||||
setError(errorMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in handleSaveForm:', error);
|
||||
let errorMessage = 'An unexpected error occurred. Please try again.';
|
||||
|
||||
if (error.response) {
|
||||
console.error('Response data:', error.response.data);
|
||||
console.error('Response status:', error.response.status);
|
||||
|
||||
if (error.response.status === 401) {
|
||||
errorMessage = 'Authentication required. Please log in again.';
|
||||
} else if (error.response.status === 403) {
|
||||
errorMessage = 'You do not have permission to perform this action.';
|
||||
} else if (error.response.status === 404) {
|
||||
errorMessage = 'The requested resource was not found.';
|
||||
} else if (error.response.status >= 500) {
|
||||
errorMessage = 'Server error. Please try again later.';
|
||||
} else if (error.response.data?.message) {
|
||||
errorMessage = error.response.data.message;
|
||||
}
|
||||
} else if (error.request) {
|
||||
console.error('No response received:', error.request);
|
||||
errorMessage = 'No response from server. Please check your network connection.';
|
||||
} else {
|
||||
console.error('Request setup error:', error.message);
|
||||
errorMessage = error.message || 'Error setting up the request.';
|
||||
}
|
||||
|
||||
setError(errorMessage);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
closeModal();
|
||||
};
|
||||
|
||||
const handleResetSelection = () => {
|
||||
setEditingRow(null);
|
||||
setDeletingRow(null);
|
||||
};
|
||||
|
||||
React.useEffect(() => handleResetSelection, []);
|
||||
|
||||
return (
|
||||
<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="relative">
|
||||
{toast.show && (
|
||||
<Toast
|
||||
message={toast.message}
|
||||
onClose={() => setToast(prev => ({ ...prev, show: false }))}
|
||||
/>
|
||||
)}
|
||||
<div className="bg-white w-full rounded-lg shadow-sm ring-1 ring-[#E5E7EB]">
|
||||
{/* Header */}
|
||||
<div className="px-6 py-3 flex items-center justify-between">
|
||||
<h3 className="text-[16px] font-medium text-[#232528]">HS Codes</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<button className="h-10 px-4 rounded-[6px] bg-[#F7F7F7] border border-[#C3C6CB] text-sm inline-flex items-center gap-2 cursor-pointer">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative w-80">
|
||||
<div className="absolute left-3 top-1/2 -translate-y-1/2">
|
||||
<img src={searchIconSrc} alt="Search" className="h-4 w-4 text-[#5F646D]" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
placeholder="Search by code or name"
|
||||
className="h-10 w-full rounded-md border border-[#C3C6CB] pl-9 pr-3 text-sm focus:outline-none focus:ring-1 focus:ring-[#92722A]"
|
||||
/>
|
||||
</div>
|
||||
<button className="h-10 px-4 rounded-[6px] bg-[#F7F7F7] border border-[#C3C6CB] text-sm inline-flex items-center gap-2">
|
||||
<img src={uploadImportIconSrc} alt="Import" className="h-5 w-5" />
|
||||
<span className="font-medium text-[#232528]">Import CSV</span>
|
||||
</button>
|
||||
<button className="h-10 px-4 rounded-[6px] bg-[#F7F7F7] border border-[#C3C6CB] text-sm inline-flex items-center gap-2 cursor-pointer">
|
||||
<button className="h-10 px-4 rounded-[6px] bg-[#F7F7F7] border border-[#C3C6CB] text-sm inline-flex items-center gap-2">
|
||||
<img src={downloadIconSrc} alt="Export" className="h-5 w-5" />
|
||||
<span className="font-medium text-[#232528]">Export CSV</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="h-10 px-4 rounded-[6px] bg-[#92722A] text-white text-sm inline-flex items-center gap-2 cursor-pointer"
|
||||
className="h-10 px-4 rounded-[6px] bg-[#92722A] text-white text-sm inline-flex items-center gap-2"
|
||||
onClick={openAddModal}
|
||||
>
|
||||
<img src={addIconSrc} alt="Add" className="h-5 w-5" />
|
||||
@ -199,11 +536,23 @@ const IsicHsCodes = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<Table
|
||||
headers={headers}
|
||||
rows={rows}
|
||||
renderCell={(value, rowIndex, colIndex) => {
|
||||
// STATUS COLUMN
|
||||
if (colIndex === 6) {
|
||||
return (
|
||||
<StatusBadge
|
||||
status={value}
|
||||
tone={value === 'Active' ? 'green' : 'gray'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// ACTIONS COLUMN
|
||||
if (colIndex === 7) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
@ -212,6 +561,8 @@ const IsicHsCodes = () => {
|
||||
title="Edit"
|
||||
aria-label="Edit code"
|
||||
onClick={() => handleEdit(rowIndex)}
|
||||
onMouseEnter={() => setEditingRow(rowIndex)}
|
||||
onMouseLeave={() => setEditingRow(null)}
|
||||
>
|
||||
<img
|
||||
src={editingRow === rowIndex ? pencilActiveSrc : pencilInactiveSrc}
|
||||
@ -222,14 +573,33 @@ const IsicHsCodes = () => {
|
||||
<button
|
||||
type="button"
|
||||
className="h-8 w-8 grid place-items-center rounded hover:bg-gray-50 cursor-pointer"
|
||||
title="View"
|
||||
aria-label="View code"
|
||||
onClick={() => handleView(rowIndex)}
|
||||
>
|
||||
<img
|
||||
src={eyeIconSrc}
|
||||
alt="View"
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="h-8 w-8 flex items-center justify-center rounded hover:bg-gray-50 cursor-pointer focus:outline-none"
|
||||
title="Delete"
|
||||
aria-label="Delete code"
|
||||
onClick={() => handleDelete(rowIndex)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
console.log('Delete button clicked for row:', rowIndex);
|
||||
handleDelete(rowIndex);
|
||||
}}
|
||||
onMouseEnter={() => setDeletingRow(rowIndex)}
|
||||
onMouseLeave={() => setDeletingRow(null)}
|
||||
>
|
||||
<img
|
||||
src={deletingRow === rowIndex ? trashActiveSrc : trashInactiveSrc}
|
||||
alt="Delete"
|
||||
className="h-4 w-4"
|
||||
className="h-4 w-4 pointer-events-none"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
@ -242,93 +612,142 @@ const IsicHsCodes = () => {
|
||||
currentPage,
|
||||
onPageChange: setCurrentPage,
|
||||
pageSize,
|
||||
totalItems: filteredRows.length,
|
||||
totalItems: filteredData.length,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Add/Edit/View Modal */}
|
||||
{modalMode && (
|
||||
<div className="fixed inset-0 z-50">
|
||||
<div className="absolute inset-0 bg-black/40" onClick={closeModal} />
|
||||
<div
|
||||
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-[16px] bg-white"
|
||||
style={{
|
||||
width: '540px',
|
||||
maxWidth: '92vw',
|
||||
boxShadow: '0 24px 40px rgba(27, 29, 33, 0.16)',
|
||||
border: '1px solid #E5E7EB',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-[#F1F2F4]">
|
||||
<h3 className="text-[18px] font-medium text-[#232528]">
|
||||
{modalMode === 'edit' ? 'Edit HS Codes' : 'Add HS Codes'}
|
||||
<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] max-h-[90vh] overflow-y-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-[#E5E7EB]">
|
||||
<h3 className="text-lg font-semibold text-[#111827]">
|
||||
{modalMode === 'add' ? 'Add New HS Code' : modalMode === 'edit' ? 'Edit HS Code' : 'View HS Code'}
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Close"
|
||||
className="h-8 w-8 grid place-items-center rounded hover:bg-gray-100"
|
||||
className="text-gray-400 hover:text-gray-500"
|
||||
onClick={closeModal}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" strokeWidth="2" className="h-4 w-4 text-[#92722A]" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 6l12 12M18 6L6 18" />
|
||||
<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>
|
||||
|
||||
<div className="px-6 py-5">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<TextField
|
||||
label="Code"
|
||||
value={form.code}
|
||||
onChange={handleFormChange('code')}
|
||||
placeholder="Enter Code"
|
||||
width="100%"
|
||||
/>
|
||||
<TextField
|
||||
label="Product Name"
|
||||
value={form.product}
|
||||
onChange={handleFormChange('product')}
|
||||
placeholder="Enter Product Name"
|
||||
width="100%"
|
||||
/>
|
||||
<SelectField
|
||||
label="Unit"
|
||||
value={form.unit}
|
||||
onChange={handleFormChange('unit')}
|
||||
options={unitOptions}
|
||||
placeholder="Select Unit"
|
||||
width="100%"
|
||||
/>
|
||||
<SelectField
|
||||
label="Status"
|
||||
value={form.status}
|
||||
onChange={handleFormChange('status')}
|
||||
options={statusOptions}
|
||||
placeholder="Select Status"
|
||||
width="100%"
|
||||
/>
|
||||
{/* Form */}
|
||||
<div className="p-6 space-y-4">
|
||||
{/* HS Code */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[#374151] mb-1">
|
||||
Code <span className="text-[#EF4444]">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.code}
|
||||
onChange={handleFormChange("code")}
|
||||
disabled={modalMode === "view"}
|
||||
className="w-full px-3 py-2 text-sm border border-[#CBA344] rounded-md focus:outline-none focus:ring-1 focus:ring-[#92722A] focus:border-[#92722A]"
|
||||
placeholder="Enter HS Code"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[#374151] mb-1">
|
||||
Product Name <span className="text-[#EF4444]">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.product}
|
||||
onChange={handleFormChange("product")}
|
||||
disabled={modalMode === "view"}
|
||||
className="w-full px-3 py-2 text-sm border border-[#CBA344] rounded-md focus:outline-none focus:ring-1 focus:ring-[#92722A] focus:border-[#92722A]"
|
||||
placeholder="Enter Product Name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex items-center justify-end gap-3">
|
||||
{/* Row 2: Unit + Status */}
|
||||
<div className="grid grid-cols-2 gap-4 mt-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[#374151] mb-1">
|
||||
Unit <span className="text-[#EF4444]">*</span>
|
||||
</label>
|
||||
{isLoading ? (
|
||||
<div className="w-full px-3 py-2 text-sm text-gray-500 bg-gray-100 rounded-md">
|
||||
Loading units...
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="w-full px-3 py-2 text-sm text-red-600 bg-red-50 rounded-md">
|
||||
{error}
|
||||
</div>
|
||||
) : (
|
||||
<select
|
||||
value={form.unit}
|
||||
onChange={handleFormChange("unit")}
|
||||
disabled={modalMode === "view" || isLoading}
|
||||
className="w-full px-3 py-2 text-sm border border-[#CBA344] rounded-md focus:outline-none focus:ring-1 focus:ring-[#92722A] focus:border-[#92722A]"
|
||||
>
|
||||
<option value="">Select Unit</option>
|
||||
{unitOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[#374151] mb-1">
|
||||
Status <span className="text-[#EF4444]">*</span>
|
||||
</label>
|
||||
<select
|
||||
value={form.status}
|
||||
onChange={handleFormChange("status")}
|
||||
disabled={modalMode === "view"}
|
||||
className="w-full px-3 py-2 text-sm border border-[#CBA344] rounded-md focus:outline-none focus:ring-1 focus:ring-[#92722A] focus:border-[#92722A]"
|
||||
>
|
||||
<option value="">Select</option>
|
||||
<option value="Active">Active</option>
|
||||
<option value="Inactive">Inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
{modalMode !== 'view' && (
|
||||
<div className="flex justify-end p-6 border-t border-[#E5E7EB] space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
className="h-10 px-6 rounded-md border border-[#92722A] text-sm font-medium text-[#92722A] bg-white"
|
||||
onClick={closeModal}
|
||||
className="h-10 px-6 text-sm font-medium text-[#374151] bg-white border border-[#D1D5DB] rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#92722A]"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="h-10 px-8 rounded-md bg-[#92722A] text-sm font-medium text-white shadow-[0_8px_18px_rgba(146,114,42,0.35)]"
|
||||
onClick={handleSaveForm}
|
||||
disabled={!form.code || !form.product || !form.unit || !form.status}
|
||||
className={`h-10 px-6 text-sm font-medium text-white rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#92722A] ${
|
||||
!form.code || !form.product || !form.unit || !form.status
|
||||
? 'bg-gray-300 cursor-not-allowed'
|
||||
: 'bg-[#92722A] hover:bg-[#7a5f22]'
|
||||
}`}
|
||||
>
|
||||
{modalMode === 'edit' ? 'Update' : 'Add'}
|
||||
{modalMode === 'add' ? 'Add' : 'Save Changes'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
{showDeleteConfirm && (
|
||||
<div className="fixed inset-0 z-50">
|
||||
<div className="absolute inset-0 bg-black/40" onClick={closeDeleteConfirm} />
|
||||
@ -336,47 +755,41 @@ const IsicHsCodes = () => {
|
||||
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-lg bg-white border"
|
||||
style={{
|
||||
width: '480px',
|
||||
maxWidth: '92vw',
|
||||
borderColor: '#CBD5E1',
|
||||
boxShadow: '0 20px 45px rgba(0,0,0,0.12)',
|
||||
}}
|
||||
>
|
||||
<div className="px-8 py-6 space-y-6">
|
||||
<div className="flex items-start gap-3">
|
||||
<span
|
||||
className="flex items-center justify-center"
|
||||
style={{ width: '32px', height: '36px' }}
|
||||
>
|
||||
<img src={deleteIconSrc} alt="Delete" className="h-6 w-6" />
|
||||
</span>
|
||||
<img src={deleteIconSrc} alt="Delete" className="h-6 w-6 mt-1" />
|
||||
<div className="flex-1">
|
||||
<h3 className="text-[18px] font-semibold text-[#232528]">Delete HS Codes</h3>
|
||||
<p className="mt-1 text-sm leading-5 text-[#4B5563]">
|
||||
<h3 className="text-[18px] font-semibold text-[#232528]">
|
||||
Delete HS Codes
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-[#4B5563]">
|
||||
Are you sure you want to delete{' '}
|
||||
{deletingCode && (
|
||||
<span className="font-semibold text-[#232528]">
|
||||
{`${deletingCode.code} - ${deletingCode.product}`}
|
||||
</span>
|
||||
)}
|
||||
<span className="font-semibold text-[#232528]">
|
||||
{deletingCode && `${deletingCode.code} - ${deletingCode.product}`}
|
||||
</span>
|
||||
?
|
||||
</p>
|
||||
<p className="mt-2 text-sm leading-5 text-[#4B5563]">
|
||||
<p className="mt-2 text-sm text-[#4B5563]">
|
||||
This action will permanently delete the data and cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-4">
|
||||
<div className="flex justify-end gap-4">
|
||||
<button
|
||||
type="button"
|
||||
className="h-10 px-6 rounded-md border border-[#92722A] text-sm font-medium text-[#92722A] bg-white"
|
||||
onClick={closeDeleteConfirm}
|
||||
onClick={handleDeleteConfirm}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<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"
|
||||
onClick={handleDeleteConfirm}
|
||||
>
|
||||
Delete
|
||||
@ -386,6 +799,7 @@ const IsicHsCodes = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -6,7 +6,7 @@ import StatusBadge from '@/components/common/StatusBadge';
|
||||
const downloadIconSrc = '/assets/images/DownloadSimple.svg';
|
||||
const addIconSrc = '/assets/images/ic_baseline-plus.svg';
|
||||
const pencilActiveSrc = '/assets/images/PencilSimple.svg';
|
||||
const pencilInactiveSrc = '/assets/images/PencilSimple-inactive.svg';
|
||||
const pencilInactiveSrc = '/assets/images/pencilsimple-inactive.svg';
|
||||
const trashActiveSrc = '/assets/images/Trash - active.svg';
|
||||
const trashInactiveSrc = '/assets/images/Trash - Inactive.svg';
|
||||
const deleteIconSrc = '/assets/images/delete.svg';
|
||||
|
||||
@ -2,19 +2,23 @@ import React from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { login } from '@/services/auth/authService.js';
|
||||
import { RefreshCw } from "lucide-react";
|
||||
|
||||
const logoSrc = '/assets/images/FCSCLogo.svg';
|
||||
|
||||
const Login = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const [email, setEmail] = React.useState('');
|
||||
const [password, setPassword] = React.useState('');
|
||||
const [showPassword, setShowPassword] = React.useState(false);
|
||||
const [rememberMe, setRememberMe] = React.useState(false);
|
||||
const [rememberMe, setRememberMe] = React.useState(true);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [error, setError] = React.useState('');
|
||||
const [toast, setToast] = React.useState(null);
|
||||
|
||||
const [emailError, setEmailError] = React.useState('');
|
||||
const [passwordError, setPasswordError] = React.useState('');
|
||||
|
||||
const [captcha, setCaptcha] = React.useState('');
|
||||
const [userCaptcha, setUserCaptcha] = React.useState('');
|
||||
const [captchaError, setCaptchaError] = React.useState('');
|
||||
@ -22,6 +26,7 @@ const Login = () => {
|
||||
const toastTimeoutRef = React.useRef(null);
|
||||
const navigateTimeoutRef = React.useRef(null);
|
||||
|
||||
// Generate CAPTCHA
|
||||
const generateCaptcha = React.useCallback(() => {
|
||||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||
let code = '';
|
||||
@ -31,9 +36,11 @@ const Login = () => {
|
||||
setCaptcha(code);
|
||||
}, []);
|
||||
|
||||
|
||||
// Toast helpers
|
||||
const closeToast = React.useCallback(() => {
|
||||
if (toastTimeoutRef.current) {
|
||||
window.clearTimeout(toastTimeoutRef.current);
|
||||
clearTimeout(toastTimeoutRef.current);
|
||||
toastTimeoutRef.current = null;
|
||||
}
|
||||
setToast(null);
|
||||
@ -42,24 +49,25 @@ const Login = () => {
|
||||
const showToast = React.useCallback((type, message) => {
|
||||
if (!message) return;
|
||||
if (toastTimeoutRef.current) {
|
||||
window.clearTimeout(toastTimeoutRef.current);
|
||||
clearTimeout(toastTimeoutRef.current);
|
||||
toastTimeoutRef.current = null;
|
||||
}
|
||||
setToast({ type, message });
|
||||
toastTimeoutRef.current = window.setTimeout(() => {
|
||||
toastTimeoutRef.current = setTimeout(() => {
|
||||
setToast(null);
|
||||
toastTimeoutRef.current = null;
|
||||
}, 4000);
|
||||
}, []);
|
||||
|
||||
// Navigate delay
|
||||
const scheduleNavigate = React.useCallback(
|
||||
(path) => {
|
||||
if (!path) return;
|
||||
if (navigateTimeoutRef.current) {
|
||||
window.clearTimeout(navigateTimeoutRef.current);
|
||||
clearTimeout(navigateTimeoutRef.current);
|
||||
navigateTimeoutRef.current = null;
|
||||
}
|
||||
navigateTimeoutRef.current = window.setTimeout(() => {
|
||||
navigateTimeoutRef.current = setTimeout(() => {
|
||||
navigate(path);
|
||||
navigateTimeoutRef.current = null;
|
||||
}, 500);
|
||||
@ -74,45 +82,74 @@ const Login = () => {
|
||||
React.useEffect(() => {
|
||||
if (location.state?.showSuccessToast && location.state?.successMessage) {
|
||||
showToast('success', location.state.successMessage);
|
||||
|
||||
window.history.replaceState({}, document.title);
|
||||
}
|
||||
}, [location.state, showToast]);
|
||||
|
||||
React.useEffect(
|
||||
() => () => {
|
||||
if (toastTimeoutRef.current) {
|
||||
window.clearTimeout(toastTimeoutRef.current);
|
||||
}
|
||||
if (navigateTimeoutRef.current) {
|
||||
window.clearTimeout(navigateTimeoutRef.current);
|
||||
}
|
||||
if (toastTimeoutRef.current) clearTimeout(toastTimeoutRef.current);
|
||||
if (navigateTimeoutRef.current) clearTimeout(navigateTimeoutRef.current);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleCaptchaChange = (e) => {
|
||||
setUserCaptcha(e.target.value.toUpperCase());
|
||||
if (captchaError) setCaptchaError('');
|
||||
};
|
||||
|
||||
const refreshCaptcha = () => {
|
||||
generateCaptcha();
|
||||
setUserCaptcha('');
|
||||
setCaptchaError('');
|
||||
};
|
||||
|
||||
// Email format check
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
const validateForm = () => {
|
||||
let valid = true;
|
||||
setEmailError('');
|
||||
setPasswordError('');
|
||||
setCaptchaError('');
|
||||
|
||||
if (!email.trim()) {
|
||||
setEmailError('Enter your email address.');
|
||||
valid = false;
|
||||
} else if (!emailRegex.test(email)) {
|
||||
setEmailError('Enter a valid email address.');
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (!password.trim()) {
|
||||
setPasswordError('Enter your password.');
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (!userCaptcha.trim()) {
|
||||
setCaptchaError('Please enter the CAPTCHA.');
|
||||
valid = false;
|
||||
} else if (userCaptcha !== captcha) {
|
||||
setCaptchaError('Invalid. Try again or refresh for a new code.');
|
||||
valid = false;
|
||||
}
|
||||
|
||||
return valid;
|
||||
};
|
||||
|
||||
const submit = async (e) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setCaptchaError('');
|
||||
setLoading(true);
|
||||
|
||||
if (userCaptcha !== captcha) {
|
||||
setCaptchaError('Incorrect CAPTCHA. Please try again.');
|
||||
generateCaptcha();
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
if (!validateForm()) return;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const response = await login({ email, password });
|
||||
|
||||
if (response?.status !== 'success' || !response?.data) {
|
||||
throw new Error(response?.message || 'Invalid response');
|
||||
throw new Error('Email or password is incorrect.');
|
||||
}
|
||||
|
||||
const token = response.data;
|
||||
@ -131,6 +168,7 @@ const Login = () => {
|
||||
if (role) sessionStorage.setItem('user_role', role);
|
||||
|
||||
const profile = {
|
||||
id: payload?.id || payload?.user_id || '',
|
||||
name: payload?.name || payload?.username || '',
|
||||
email: payload?.email || '',
|
||||
};
|
||||
@ -163,13 +201,11 @@ const Login = () => {
|
||||
showToast('success', successMessage);
|
||||
scheduleNavigate('/dashboard');
|
||||
} else {
|
||||
setError('No establishment associated with this account.');
|
||||
showToast('error', 'Email or password is incorrect.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Login failed', err);
|
||||
const apiMessage = err.response?.data?.message || err.response?.data?.error;
|
||||
const fallbackMessage = err.message || 'Unable to sign in. Please try again.';
|
||||
setError(apiMessage || fallbackMessage);
|
||||
showToast('error', 'Email or password is incorrect.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@ -208,30 +244,41 @@ const Login = () => {
|
||||
|
||||
<div className="w-full max-w-md">
|
||||
<div className="bg-white rounded-lg shadow-sm ring-1 ring-gray-200 overflow-hidden">
|
||||
<div className="px-7 py-8 border-b border-gray-200 bg-[#F2ECCF] text-center">
|
||||
<img src={logoSrc} alt="FCSC Logo" className="mx-auto h-16 w-auto" />
|
||||
<h1 className="text-base font-semibold text-[#92722A]">IPI Survey Platform</h1>
|
||||
</div>
|
||||
|
||||
<form onSubmit={submit} className="px-7 py-8 space-y-4">
|
||||
{error && (
|
||||
<div className="text-sm text-red-700 bg-red-50 border border-red-200 rounded px-3 py-2">
|
||||
{error}
|
||||
<div className="px-7 py-6 border-b border-gray-200 bg-[#F2ECCF] text-center">
|
||||
<div className="mb-2">
|
||||
<img
|
||||
src={logoSrc}
|
||||
alt="FCSC Logo"
|
||||
className="mx-auto h-16 w-auto"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="Enter Email"
|
||||
className="block w-full h-10 rounded-md border-2 border-[#92722A] focus:border-[#92722A] focus:ring-0 px-4 text-sm bg-white"
|
||||
required
|
||||
/>
|
||||
<h1 className="text-[17px] font-semibold text-[#1F2937] mt-0">
|
||||
Industrial Production Index (IPI) Survey Portal
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<form onSubmit={submit} className="px-7 py-8 space-y-4">
|
||||
{/* Email */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Email address</label>
|
||||
<input
|
||||
type="text"
|
||||
value={email}
|
||||
onChange={(e) => {
|
||||
setEmail(e.target.value);
|
||||
if (emailError) setEmailError(''); // 👈 clears error while typing
|
||||
}}
|
||||
placeholder="name@company.com"
|
||||
className={`block w-full h-10 rounded-md border-2 px-4 text-sm focus:ring-0 ${
|
||||
emailError ? 'border-red-500 bg-red-50' : 'border-[#92722A] bg-white'
|
||||
}`}
|
||||
/>
|
||||
{emailError && <p className="text-xs text-red-600 mt-1">{emailError}</p>}
|
||||
</div>
|
||||
|
||||
{/* Password */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Password</label>
|
||||
<div className="relative">
|
||||
@ -239,9 +286,9 @@ const Login = () => {
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Enter password"
|
||||
className="block w-full h-10 rounded-md border-2 border-[#92722A] focus:border-[#92722A] focus:ring-0 px-4 pr-11 text-sm bg-white"
|
||||
required
|
||||
className={`block w-full h-10 rounded-md border-2 px-4 pr-11 text-sm focus:ring-0 ${
|
||||
passwordError ? 'border-red-500 bg-red-50' : 'border-[#92722A] bg-white'
|
||||
}`}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@ -251,18 +298,8 @@ const Login = () => {
|
||||
>
|
||||
{showPassword ? (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-[#92722A]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M13.875 18.825A10.05 10.05 0 0 1 12 19c-5.523 0-10-4.477-10-10a9.96 9.96 0 0 1 2.122-6.21m3.086-1.955A9.953 9.953 0 0 1 12 3c5.523 0 10 4.477 10 10 0 1.591-.372 3.093-1.034 4.432M9.88 9.88a3 3 0 1 0 4.24 4.24"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="m3 3 18 18"
|
||||
/>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13.875 18.825A10.05 10.05 0 0 1 12 19c-5.523 0-10-4.477-10-10a9.96 9.96 0 0 1 2.122-6.21m3.086-1.955A9.953 9.953 0 0 1 12 3c5.523 0 10 4.477 10 10 0 1.591-.372 3.093-1.034 4.432M9.88 9.88a3 3 0 1 0 4.24 4.24" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="m3 3 18 18" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-[#92722A]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
@ -272,48 +309,55 @@ const Login = () => {
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{passwordError && <p className="text-xs text-red-600 mt-1">{passwordError}</p>}
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
CAPTCHA Verification <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className="flex justify-center items-center font-mono text-lg tracking-widest bg-gray-100 border-2 border-[#92722A] rounded-md px-4 py-2 select-none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{captcha}
|
||||
</div>
|
||||
{/* CAPTCHA */}
|
||||
<div className="mt-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
CAPTCHA Verification <span className="text-red-500">*</span>
|
||||
</label>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* CAPTCHA text box */}
|
||||
<div className="flex justify-center items-center font-mono text-lg tracking-widest bg-gray-100 border-2 border-[#92722A] rounded-md px-4 py-2 select-none h-10">
|
||||
{captcha}
|
||||
</div>
|
||||
|
||||
{/* Refresh icon + text */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={generateCaptcha}
|
||||
className="flex items-center gap-1 text-sm text-[#92722A] hover:underline"
|
||||
onClick={refreshCaptcha}
|
||||
className="flex items-center gap-1 text-[#92722A] hover:text-[#7b5c1f]"
|
||||
title="Refresh CAPTCHA"
|
||||
>
|
||||
<RefreshCw size={14} className="text-[#92722A]" />
|
||||
Refresh
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
<span className="text-sm leading-none">Refresh</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Info text */}
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
If you cannot read the code, click "Refresh" or type:{" "}
|
||||
<span className="font-mono">{captcha}</span>
|
||||
Enter the characters in the image. Can't read it? Refresh for a new code.
|
||||
</p>
|
||||
{/* </div> */}
|
||||
|
||||
|
||||
<input
|
||||
type="text"
|
||||
name="captcha"
|
||||
id="captcha"
|
||||
value={userCaptcha}
|
||||
|
||||
onChange={handleCaptchaChange}
|
||||
placeholder="Enter the code shown above"
|
||||
className={`mt-2 block w-full h-10 rounded-md border-2 ${
|
||||
captchaError ? 'border-red-500' : 'border-[#92722A]'
|
||||
} focus:border-[#92722A] focus:ring-0 px-4 text-sm bg-white`}
|
||||
placeholder="Type the characters"
|
||||
className={`mt-2 block w-full h-10 rounded-md border-2 px-4 text-sm focus:ring-0 ${
|
||||
captchaError ? 'border-red-500 bg-red-50' : 'border-[#92722A] bg-white'
|
||||
}`}
|
||||
/>
|
||||
{captchaError && (
|
||||
<p className="text-xs text-red-600 mt-1">{captchaError}</p>
|
||||
)}
|
||||
{captchaError && <p className="text-xs text-red-600 mt-1">{captchaError}</p>}
|
||||
</div>
|
||||
|
||||
{/* Remember me */}
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<label className="inline-flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
@ -322,7 +366,7 @@ const Login = () => {
|
||||
checked={rememberMe}
|
||||
onChange={(e) => setRememberMe(e.target.checked)}
|
||||
/>
|
||||
<span className="text-gray-700">Remember me</span>
|
||||
<span className="text-gray-700">Keep me signed in on this device</span>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
@ -333,11 +377,12 @@ const Login = () => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Submit */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !userCaptcha}
|
||||
disabled={loading || !userCaptcha.trim()}
|
||||
className={`inline-flex items-center justify-center w-full h-12 rounded-lg text-white ${
|
||||
loading || !userCaptcha
|
||||
loading || !userCaptcha.trim()
|
||||
? 'bg-[#bfa06a] cursor-not-allowed'
|
||||
: 'bg-[#92722A] hover:bg-[#7b5c1f]'
|
||||
} transition-colors`}
|
||||
@ -351,4 +396,4 @@ const Login = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
export default Login;
|
||||
|
||||
@ -0,0 +1,84 @@
|
||||
import { getRequest, postRequest, putRequest ,deleteRequest} from '@/services/api/CommonService';
|
||||
|
||||
export const productService = {
|
||||
// Get all products
|
||||
getProducts: async () => {
|
||||
try {
|
||||
const response = await getRequest('/products');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error in productService.getProducts:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Get single product by ID
|
||||
getProductById: async (id) => {
|
||||
try {
|
||||
const response = await getRequest(`/products/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(`Error in productService.getProductById for ID ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Create new product
|
||||
createProduct: async (productData) => {
|
||||
try {
|
||||
const response = await postRequest('/products', productData);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error in productService.createProduct:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Update existing product
|
||||
updateProduct: async (id, productData) => {
|
||||
try {
|
||||
// Use the full URL to ensure correct request routing
|
||||
const response = await putRequest(`products/${id}`, productData);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(`Error in productService.updateProduct for ID ${id}:`, error);
|
||||
// Log the full error details for debugging
|
||||
if (error.response) {
|
||||
console.error('Error response data:', error.response.data);
|
||||
console.error('Error status:', error.response.status);
|
||||
console.error('Error headers:', error.response.headers);
|
||||
} else if (error.request) {
|
||||
console.error('No response received:', error.request);
|
||||
} else {
|
||||
console.error('Error message:', error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Delete product
|
||||
deleteProduct: async (id) => {
|
||||
try {
|
||||
console.log(`Initiating delete for product ID: ${id}`);
|
||||
// Use relative path since baseURL is already configured in apiClient
|
||||
const response = await deleteRequest(`/products/${id}`);
|
||||
console.log('Delete response:', response);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(`Error in productService.deleteProduct for ID ${id}:`, {
|
||||
error: error.message,
|
||||
response: error.response?.data,
|
||||
status: error.response?.status,
|
||||
config: error.config
|
||||
});
|
||||
|
||||
// Enhance error message for better debugging
|
||||
const errorMessage = error.response?.data?.message || error.message || 'Failed to delete product';
|
||||
const enhancedError = new Error(errorMessage);
|
||||
enhancedError.response = error.response;
|
||||
throw enhancedError;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default productService;
|
||||
@ -0,0 +1,41 @@
|
||||
import { getRequest, postRequest, putRequest, deleteRequest } from '@/services/api/CommonService';
|
||||
|
||||
export const getQuarterlyWindows = async () => {
|
||||
try {
|
||||
const response = await getRequest('/quarterly_windows');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching quarterly windows:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const createQuarterlyWindow = async (data) => {
|
||||
try {
|
||||
const response = await postRequest('/quarterly_windows', data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error creating quarterly window:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateQuarterlyWindow = async (id, data) => {
|
||||
try {
|
||||
const response = await putRequest(`/quarterly_windows/${id}`, data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error updating quarterly window:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteQuarterlyWindow = async (id) => {
|
||||
try {
|
||||
const response = await deleteRequest(`/quarterly_windows/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error deleting quarterly window:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,44 @@
|
||||
// src/services/configuration/unitService.js
|
||||
import { getRequest, postRequest, putRequest, deleteRequest } from '@/services/api/CommonService';
|
||||
|
||||
const UNIT_MASTER_ENDPOINT = '/unit_master';
|
||||
|
||||
export const getUnits = async () => {
|
||||
try {
|
||||
const response = await getRequest(UNIT_MASTER_ENDPOINT);
|
||||
return response.data || [];
|
||||
} catch (error) {
|
||||
console.error('Error fetching units:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const createUnit = async (unitData) => {
|
||||
try {
|
||||
const response = await postRequest(UNIT_MASTER_ENDPOINT, unitData);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error creating unit:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateUnit = async (id, unitData) => {
|
||||
try {
|
||||
const response = await putRequest(`${UNIT_MASTER_ENDPOINT}/${id}`, unitData);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error updating unit:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteUnit = async (id) => {
|
||||
try {
|
||||
const response = await deleteRequest(`${UNIT_MASTER_ENDPOINT}/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error deleting unit:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@ -45,7 +45,7 @@ export const fetchSubmissionDetail = async (submissionId, config = {}) => {
|
||||
if (!submissionId) {
|
||||
throw new Error('Submission ID is required to fetch submission detail.');
|
||||
}
|
||||
const url = `${endpoint}/${submissionId}`;
|
||||
const url = `${endpoint}/view/${submissionId}`;
|
||||
const response = await getRequest(url, config);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user