changes in survey page and submission history

This commit is contained in:
Malini 2025-11-01 12:51:42 +05:30
parent 2bbce71b96
commit 492931d488
15 changed files with 1082 additions and 331 deletions

View File

@ -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

View File

@ -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={(

View File

@ -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

View File

@ -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"

View File

@ -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"

View File

@ -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]">

View File

@ -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">

View File

@ -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 = () => {

View File

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

View File

@ -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';

View File

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

View File

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

View File

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

View File

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

View File

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