Bug fixed

This commit is contained in:
Malini 2025-11-08 11:36:07 +05:30
parent 9986f7c16b
commit 6dfa71a5b4
15 changed files with 1962 additions and 1199 deletions

View File

@ -12,11 +12,20 @@ const SubmissionTable = ({ selectedQuarter, selectedYear }) => {
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => { const fetchDashboardData = async (quarter, year) => {
const fetchDashboardData = async () => {
try { try {
setLoading(true); setLoading(true);
const response = await apiClient.get('/admin_dashboard'); const params = new URLSearchParams();
if (quarter && quarter !== 'All') {
params.append('quarter', quarter);
}
if (year && year !== 'All') {
params.append('year', year);
}
const response = await apiClient.get(`/admin_dashboard?${params.toString()}`);
const result = response?.data; const result = response?.data;
if (result?.status === 'success') { if (result?.status === 'success') {
setData(result.recent_submissions || []); setData(result.recent_submissions || []);
@ -29,8 +38,10 @@ const SubmissionTable = ({ selectedQuarter, selectedYear }) => {
setLoading(false); setLoading(false);
} }
}; };
fetchDashboardData();
}, []); useEffect(() => {
fetchDashboardData(selectedQuarter, selectedYear);
}, [selectedQuarter, selectedYear]);
const filtered = useMemo(() => { const filtered = useMemo(() => {
let list = [...data]; let list = [...data];

View File

@ -32,7 +32,7 @@ const getStatusBadge = (status) => {
); );
} else if (normalized === 'submitted') { } else if (normalized === 'submitted') {
return ( return (
<span className="px-3 py-1 rounded-md text-green-700 bg-green-50 font-medium text-sm whitespace-nowrap"> <span className="px-3 py-1 rounded-md text-[#003CFF] bg-[#E7F5FF] font-medium text-sm whitespace-nowrap">
Submitted Submitted
</span> </span>
); );
@ -152,7 +152,7 @@ export const SubmissionHistory = ({ history = [], loading = false, error = '' })
<span className="whitespace-nowrap">{item.submission_on}</span>, <span className="whitespace-nowrap">{item.submission_on}</span>,
productsLabel, productsLabel,
<button <button
onClick={() => setSelectedSubmission(item)} onClick={() => navigate(`/submission/${item.id}`)}
className="text-[#9E792B] hover:text-[#7b5f22] font-medium" className="text-[#9E792B] hover:text-[#7b5f22] font-medium"
> >
View Details View Details
@ -160,20 +160,6 @@ export const SubmissionHistory = ({ history = [], loading = false, error = '' })
]; ];
}); });
if (selectedSubmission) {
return (
<div className="min-h-screen bg-[#F8FAFC]">
<HeaderBar />
<main className="max-w-[1280px] mx-auto px-4 py-6">
<DetailedOverview
submission={selectedSubmission}
onBack={() => setSelectedSubmission(null)}
/>
</main>
</div>
);
}
return ( return (
<> <>
<div className="max-w-[1280px] mx-auto bg-white border border-[#E5E7EB] shadow-[0_16px_32px_rgba(15,23,42,0.06)] rounded-[8px] mt-8 w-full overflow-hidden"> <div className="max-w-[1280px] mx-auto bg-white border border-[#E5E7EB] shadow-[0_16px_32px_rgba(15,23,42,0.06)] rounded-[8px] mt-8 w-full overflow-hidden">

View File

@ -1,6 +1,7 @@
import React from 'react'; import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import SurveyCarousel from './SurveyCarousel'; import SurveyCarousel from './SurveyCarousel';
import { fetchEstablishmentDashboard } from '@/services/establishments/establishmentService.js';
const formatDate = (value) => { const formatDate = (value) => {
if (!value) return '—'; if (!value) return '—';
@ -55,44 +56,69 @@ const getQuarterData = () => {
}; };
}; };
export const SurveyStatus = ({ data = null, loading = false, error = '' }) => { export const SurveyStatus = () => {
const [dashboardData, setDashboardData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const navigate = useNavigate(); const navigate = useNavigate();
const handleStartSurvey = (survey) => {
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await fetchEstablishmentDashboard();
if (response.status === 'success') {
setDashboardData(response.data);
} else {
setError('Failed to load dashboard data');
}
} catch (err) {
setError('An error occurred while fetching data');
console.error('Error fetching dashboard data:', err);
} finally {
setLoading(false);
}
}; };
const { previous, current, next } = getQuarterData(); fetchData();
}, []);
const surveyData = data || {}; const handleStartSurvey = (survey) => {
const nextDeadline = surveyData?.next_deadline; // The survey object contains all the data from survey_ready
console.log('Survey data being passed to survey screen:', {
quarter: survey.quarter,
year: survey.year,
startDate: survey.startDate,
endDate: survey.endDate,
title: survey.title
});
// You can add any additional processing here before navigation
// For example, you might want to store the selected survey in state
// or make an API call to initialize the survey
};
const nextDeadline = dashboardData?.next_deadline;
const formattedDueDate = formatDate(nextDeadline); const formattedDueDate = formatDate(nextDeadline);
const relativeDue = formatRelativeDays(nextDeadline); const relativeDue = formatRelativeDays(nextDeadline);
const surveys = [ const surveys = dashboardData?.survey_ready?.map(survey => {
{ // Get the quarter and year from the survey_ready item
title: `${previous.quarter} ${previous.year} Survey Ready`, const { quarter, year, start_date, end_date } = survey;
return {
title: `${quarter} ${year} Survey`,
subtitle: 'Complete your quarterly Industrial Production Index (IPI) data submission', subtitle: 'Complete your quarterly Industrial Production Index (IPI) data submission',
dueDate: formattedDueDate, dueDate: formatDate(end_date),
relativeDue, relativeDue: formatRelativeDays(end_date),
estTime: '1520 minutes', estTime: '1520 minutes',
status: 'Pending', status: 'Pending',
}, startDate: start_date,
{ endDate: end_date,
title: `${current.quarter} ${current.year} Survey Ready`, quarter: quarter, // This will be used in the navigation
subtitle: 'Complete your quarterly Industrial Production Index (IPI) data submission', year: year // This will be used in the navigation
dueDate: formattedDueDate, };
relativeDue, }) || [];
estTime: '1520 minutes',
status: 'Pending',
},
{
title: `${next.quarter} ${next.year} Survey Ready`,
subtitle: 'Complete your quarterly Industrial Production Index (IPI) data submission',
dueDate: formattedDueDate,
relativeDue,
estTime: '1520 minutes',
status: 'Pending',
},
];
const heroTitle = loading const heroTitle = loading
? 'Loading survey status…' ? 'Loading survey status…'

View File

@ -45,14 +45,19 @@ export const DetailedOverview = ({ submission, onBack }) => {
'Submitted': 'bg-[#E7F5FF] text-[#003CFF]', 'Submitted': 'bg-[#E7F5FF] text-[#003CFF]',
'Pending': 'bg-[#F9F7ED] text-[#7C5E24]', 'Pending': 'bg-[#F9F7ED] text-[#7C5E24]',
'Rejected': 'bg-[#FEF2F2] text-[#B52520]', 'Rejected': 'bg-[#FEF2F2] text-[#B52520]',
'Resubmitted': 'bg-[#F9F7ED] text-[#7C5E24]' 'Resubmitted': 'bg-[#F9F7ED] text-[#7C5E24]', // Added this line
}; };
const statusClass = statusMap[status] || 'bg-gray-100 text-gray-800'; // Normalize the status (trim and capitalize first letter)
const normalizedStatus = status
? status.trim().charAt(0).toUpperCase() + status.trim().slice(1).toLowerCase()
: 'Pending';
const statusClass = statusMap[normalizedStatus] || 'bg-gray-100 text-gray-800';
return ( return (
<span className={`inline-flex items-center rounded-md px-3 py-1 text-xs font-medium ${statusClass}`}> <span className={`inline-flex items-center rounded-md px-3 py-1 text-xs font-medium ${statusClass}`}>
{status || 'N/A'} {normalizedStatus}
</span> </span>
); );
}; };

View File

@ -6,7 +6,7 @@ const phoneIconSrc = '/assets/images/line-md_phone.svg';
const calendarIcon = '/assets/images/Duedate.svg'; const calendarIcon = '/assets/images/Duedate.svg';
const clockIcon = '/assets/images/mingcute_time-line.svg'; const clockIcon = '/assets/images/mingcute_time-line.svg';
const nextIcon = '/assets/images/mdi_page-next-outline.svg'; const nextIcon = '/assets/images/mdi_page-next-outline.svg';
import { useNavigate } from 'react-router-dom'; import { useNavigate, useLocation } from 'react-router-dom';
const Field = ({ label, placeholder = '', type = 'text', value = '', onChange = () => {}, disabled = true }) => ( const Field = ({ label, placeholder = '', type = 'text', value = '', onChange = () => {}, disabled = true }) => (
<div> <div>
@ -70,6 +70,21 @@ const defaultEmployeeInfo = {
totalEmirati: '', totalEmirati: '',
}; };
const getCurrentQuarterAndYear = () => {
const now = new Date();
const currentQuarter = Math.ceil((now.getMonth() + 1) / 3);
const currentYear = now.getFullYear();
// Calculate end of quarter date
const endOfQuarter = new Date(currentYear, currentQuarter * 3, 0); // Last day of the last month in quarter
return {
quarter: `Q${currentQuarter}`,
year: currentYear,
endDate: endOfQuarter.toISOString() // Return as ISO string for consistency
};
};
const EstablishmentInfo = ({ const EstablishmentInfo = ({
data = {}, data = {},
onChange = () => {}, onChange = () => {},
@ -78,22 +93,75 @@ const EstablishmentInfo = ({
loading = false, loading = false,
error = '', error = '',
isComplete = false, isComplete = false,
}) => { }) => {
const [showInfoCard, setShowInfoCard] = React.useState(true); const [showInfoCard, setShowInfoCard] = React.useState(true);
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation();
// Use a ref to persist the survey data across re-renders
const surveyDataRef = React.useRef(null);
const [surveyData, setSurveyData] = React.useState(() => {
// Initialize from location state if available, otherwise use defaults
if (location.state?.survey) {
const { quarter, year, endDate } = location.state.survey;
return {
quarter: quarter || '',
year: year || '',
endDate: endDate || ''
};
}
// Or use current quarter/year as fallback
const current = getCurrentQuarterAndYear();
return {
quarter: current.quarter,
year: current.year,
endDate: current.endDate
};
});
// Update the ref whenever surveyData changes
React.useEffect(() => {
surveyDataRef.current = surveyData;
}, [surveyData]);
React.useEffect(() => {
// Only update if we don't already have survey data
if (!surveyDataRef.current?.quarter && location.state?.survey) {
const { quarter, year, endDate } = location.state.survey;
console.log('Initial survey data:', { quarter, year, endDate });
const newSurveyData = {
quarter: quarter || '',
year: year || '',
endDate: endDate || ''
};
setSurveyData(newSurveyData);
// Update the parent component with the survey data
onChange({
...data,
quarter: newSurveyData.quarter,
year: newSurveyData.year,
end_date: newSurveyData.endDate
});
}
// Only run this effect when the component mounts or location.state changes
}, [location.state, data, onChange]);
const handleCloseInfo = () => { const handleCloseInfo = () => {
setShowInfoCard(false); setShowInfoCard(false);
}; };
// Debug log to see what data is being received // Debug log to see what data is being received
React.useEffect(() => { React.useEffect(() => {
console.log('Establishment data:', data);
}, [data]); }, [data]);
const info = React.useMemo( const info = React.useMemo(
() => ({ () => ({
quarter: '', quarter: surveyData.quarter || '',
year: '', year: surveyData.year || '',
establishmentName: '', establishmentName: '',
permanentFactoryCode: '', permanentFactoryCode: '',
industryCode: '', industryCode: '',
@ -151,7 +219,9 @@ const EstablishmentInfo = ({
<div className="h-12 w-12 animate-spin rounded-full border-[3px] border-[#92722A] border-t-transparent" /> <div className="h-12 w-12 animate-spin rounded-full border-[3px] border-[#92722A] border-t-transparent" />
</div> </div>
)} )}
<h2 className="text-xl font-semibold text-[#232528] mb-2">Industrial Production Index (IPI) Survey: {data.quarter} {data.year}</h2> <h2 className="text-xl font-semibold text-[#232528] mb-2">
Industrial Production Index (IPI) Survey: {surveyData.quarter} {surveyData.year}
</h2>
{/* Survey Information */} {/* Survey Information */}
{!showInfoCard && ( {!showInfoCard && (
@ -179,7 +249,7 @@ const EstablishmentInfo = ({
<div className="space-y-4"> <div className="space-y-4">
<p className="text-sm text-[#6C4527] mb-4"> <p className="text-sm text-[#6C4527] mb-4">
You're completing the <b>IPI Quarterly Survey</b> for <b>{data.quarter} {data.year}</b>. This step shows establishment details already registered with us. <b>Step 1 is read-only.</b> To make corrections, update them in <b>Profile Edit Profile</b>, then return to this survey. You're completing the <b>IPI Quarterly Survey</b> for <b>{surveyData.quarter} {surveyData.year}</b>. This step shows establishment details already registered with us. <b>Step 1 is read-only.</b> To make corrections, update them in <b>Profile Edit Profile</b>, then return to this survey.
</p> </p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
@ -189,7 +259,18 @@ const EstablishmentInfo = ({
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<img src={calendarIcon} alt="Calendar" className="w-4 h-4 flex-shrink-0" /> <img src={calendarIcon} alt="Calendar" className="w-4 h-4 flex-shrink-0" />
<span className="text-sm font-medium text-[#6C4527] whitespace-nowrap">Due:</span> <span className="text-sm font-medium text-[#6C4527] whitespace-nowrap">Due:</span>
<span className="text-sm text-[#6C4527] whitespace-nowrap">Jan 31, 2025</span> {/* <span className="text-sm text-[#6C4527] whitespace-nowrap">Jan 31, 2025</span> */}
<span className="text-sm text-[#6C4527] whitespace-nowrap">
{surveyData.endDate ? (
new Date(surveyData.endDate).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
})
) : (
<span className="text-amber-600">To be announced</span>
)}
</span>
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<img src={clockIcon} alt="Time" className="w-4 h-4 flex-shrink-0" /> <img src={clockIcon} alt="Time" className="w-4 h-4 flex-shrink-0" />
@ -279,7 +360,7 @@ const EstablishmentInfo = ({
<label className="block text-sm font-medium text-[#9EA2A9] mb-1">Quarter</label> <label className="block text-sm font-medium text-[#9EA2A9] mb-1">Quarter</label>
<div className="relative"> <div className="relative">
<select <select
value={info.quarter} value={surveyData.quarter}
onChange={handleFieldChange('quarter')} onChange={handleFieldChange('quarter')}
disabled={true} disabled={true}
required required
@ -301,7 +382,7 @@ const EstablishmentInfo = ({
<label className="block text-sm font-medium text-[#9EA2A9] mb-1">Year</label> <label className="block text-sm font-medium text-[#9EA2A9] mb-1">Year</label>
<div className="relative"> <div className="relative">
<select <select
value={info.year} value={surveyData.year}
onChange={handleFieldChange('year')} onChange={handleFieldChange('year')}
disabled={true} disabled={true}

View File

@ -1,4 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useState, useEffect } from 'react';
// import { useLocation } from 'react-router-dom';
import { getQuarterPeriods, getPreviousForecastData } from '@/services/submissions/submissionService'; import { getQuarterPeriods, getPreviousForecastData } from '@/services/submissions/submissionService';
import { import {
fetchVariationReasons, fetchVariationReasons,
@ -6,6 +8,7 @@ import {
fetchProducts, fetchProducts,
fetchUnits, fetchUnits,
} from '@/services/masters/masterService.js'; } from '@/services/masters/masterService.js';
import { useLocation } from 'react-router-dom';
const caretDownSrc = '/assets/images/CaretDown-black.svg'; const caretDownSrc = '/assets/images/CaretDown-black.svg';
const clockIcon = '/assets/images/mingcute_time-line.svg'; const clockIcon = '/assets/images/mingcute_time-line.svg';
@ -41,6 +44,8 @@ const Input = ({ placeholder = '', type = 'text', value = '', onChange = () => {
); );
}; };
const Select = ({ const Select = ({
placeholder = '', placeholder = '',
options = [], options = [],
@ -318,6 +323,35 @@ const ProductData = ({
} }
}, [products]); }, [products]);
const [surveyData, setSurveyData] = React.useState({
quarter: '',
year: '',
endDate: ''
});
const location = useLocation();
// Update surveyData when props or location changes
React.useEffect(() => {
// Get survey data from navigation state if available
if (location.state?.survey) {
const { quarter, year, endDate } = location.state.survey;
console.log('Received survey datassdd:', { quarter, year, endDate });
setSurveyData({
quarter: quarter || '',
year: year || '',
endDate: endDate || ''
});
} else {
// Fall back to props if no location state
setSurveyData(prev => ({
...prev,
quarter: quarter || prev.quarter,
year: year || prev.year
}));
}
}, [location.state, quarter, year]);
const requiredMessage = 'Required'; const requiredMessage = 'Required';
const validateForm = () => { const validateForm = () => {
const errors = {}; const errors = {};
@ -666,9 +700,9 @@ const ProductData = ({
)} )}
<div className="flex justify-between items-center mb-2"> <div className="flex justify-between items-center mb-2">
<h2 className="text-xl font-semibold text-[#232528]">Step 2: Product Data Monthly Output & Cost</h2> <h2 className="text-xl font-semibold text-[#232528]">Step 2: Product Data Monthly Output & Cost</h2>
{(quarter || year) && ( {(surveyData.quarter || surveyData.year) && (
<div className="text-sm font-medium text-gray-600"> <div className="text-sm font-medium text-gray-600">
Current Quarter: <span className="text-[#92722A]">{quarter}-{year}</span> Quarter: <span className="text-[#92722A]">{surveyData.quarter}-{surveyData.year}</span>
</div> </div>
)} )}
</div> </div>
@ -706,7 +740,15 @@ const ProductData = ({
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">
<img src={calendarIcon} alt="" className="h-4 w-4" /> <img src={calendarIcon} alt="" className="h-4 w-4" />
<span className="font-medium">Due:</span> <span className="font-medium">Due:</span>
<span>Jan 31, 2025</span> {surveyData.endDate ? (
new Date(surveyData.endDate).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
})
) : (
<span className="text-amber-600">To be announced</span>
)}
</div> </div>
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">

View File

@ -10,6 +10,7 @@ const approvedIconSrc = '/assets/images/check-circle-fill 1-approved.svg';
const rejectedIconSrc = '/assets/images/warning-circle-fill 1.svg'; const rejectedIconSrc = '/assets/images/warning-circle-fill 1.svg';
const overdueIconSrc = '/assets/images/warning-fill 1.svg'; const overdueIconSrc = '/assets/images/warning-fill 1.svg';
const pendingIconSrc = '/assets/images/info-fill 1.svg'; const pendingIconSrc = '/assets/images/info-fill 1.svg';
const dueDateIconSrc = '/assets/images/Duedate.svg';
const AdminDashboard = () => { const AdminDashboard = () => {
const [summary, setSummary] = useState({ const [summary, setSummary] = useState({
@ -20,19 +21,34 @@ const AdminDashboard = () => {
not_started: 0, not_started: 0,
pending: 0, pending: 0,
}); });
const [quarterlyWindows, setQuarterlyWindows] = useState({
end_date: null,
start_date: null,
grace_periods_days: 0
});
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [selectedQuarter, setSelectedQuarter] = useState('All'); const [selectedQuarter, setSelectedQuarter] = useState('All');
const [selectedYear, setSelectedYear] = useState('All'); const [selectedYear, setSelectedYear] = useState('All');
useEffect(() => { const fetchDashboardData = async (quarter, year) => {
const fetchDashboardData = async () => {
try { try {
setLoading(true); setLoading(true);
const response = await apiClient.get('/admin_dashboard'); const params = new URLSearchParams();
if (quarter && quarter !== 'All') {
params.append('quarter', quarter);
}
if (year && year !== 'All') {
params.append('year', year);
}
const response = await apiClient.get(`/admin_dashboard?${params.toString()}`);
const data = response?.data?.summary || {}; const data = response?.data?.summary || {};
const selectedQuarterFromApi = response?.data?.selected_quarter || 'All'; const selectedQuarterFromApi = response?.data?.selected_quarter || quarter || 'All';
const selectedYearFromApi = response?.data?.selected_year || 'All'; const selectedYearFromApi = response?.data?.selected_year || year || 'All';
const quarterlyWindowsData = response?.data?.quarterly_windows || {};
setSummary({ setSummary({
total_establishments: data.total_establishments || 0, total_establishments: data.total_establishments || 0,
@ -43,6 +59,12 @@ const AdminDashboard = () => {
pending: data.pending || 0, pending: data.pending || 0,
}); });
setQuarterlyWindows({
end_date: quarterlyWindowsData.end_date,
start_date: quarterlyWindowsData.start_date,
grace_periods_days: quarterlyWindowsData.grace_periods_days || 0
});
setSelectedQuarter(selectedQuarterFromApi); setSelectedQuarter(selectedQuarterFromApi);
setSelectedYear(selectedYearFromApi); setSelectedYear(selectedYearFromApi);
} catch (error) { } catch (error) {
@ -52,8 +74,9 @@ const AdminDashboard = () => {
} }
}; };
fetchDashboardData(); useEffect(() => {
}, []); fetchDashboardData(selectedQuarter, selectedYear);
}, [selectedQuarter, selectedYear]);
return ( return (
<div className="min-h-screen bg-[#F6F5F1]"> <div className="min-h-screen bg-[#F6F5F1]">
@ -88,6 +111,9 @@ const AdminDashboard = () => {
className="border border-[#D0D5DD] rounded-md h-8 px-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#92722A]" className="border border-[#D0D5DD] rounded-md h-8 px-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#92722A]"
> >
{/* <option>All</option> */} {/* <option>All</option> */}
<option>2028</option>
<option>2027</option>
<option>2026</option>
<option>2025</option> <option>2025</option>
<option>2024</option> <option>2024</option>
<option>2023</option> <option>2023</option>
@ -134,11 +160,13 @@ const AdminDashboard = () => {
tooltipText="Establishments past submission deadline." tooltipText="Establishments past submission deadline."
/> />
<StatCard <StatCard
label="Pending" label="Due Date"
value={loading ? '--' : summary.pending} labelClassName="text-sm"
value={loading ? '--' : quarterlyWindows.end_date ? new Date(quarterlyWindows.end_date).toLocaleDateString('en-GB') : '--'}
valueClassName="text-sm"
width={190} width={190}
icon={<img src={pendingIconSrc} alt="Pending" className="h-5 w-5" />} icon={<img src={dueDateIconSrc} alt="Due Date" className="h-5 w-5" />}
tooltipText="Establishments not yet submitted." tooltipText={`Submission deadline for ${selectedQuarter} ${selectedYear}`}
/> />
</div> </div>
</div> </div>

View File

@ -6,14 +6,15 @@ import {
getAdminUser, getAdminUser,
getAdminUserById, getAdminUserById,
updateAdminUser, updateAdminUser,
deleteAdminUser, // Added import deleteAdminUser,
changeAdminUserPassword,
} from '@/services/configuration/adminService'; } from '@/services/configuration/adminService';
import CustomToast from '@/components/common/CustomToast'; import CustomToast from '@/components/common/CustomToast';
import { X } from 'lucide-react';
import { TextField, SelectField } from '@/components/common/FormControls'; import { TextField, SelectField } from '@/components/common/FormControls';
import EditUserModal from './EditUserModal'; import EditUserModal from './EditUserModal';
import DeleteUserModal from './DeleteUserModal'; import DeleteUserModal from './DeleteUserModal';
import ResetPasswordModal from './ResetPasswordModal';
const searchIconSrc = '/assets/images/material-symbols_search-rounded.svg'; const searchIconSrc = '/assets/images/material-symbols_search-rounded.svg';
const exportIconSrc = '/assets/images/DownloadSimple.svg'; const exportIconSrc = '/assets/images/DownloadSimple.svg';
@ -72,8 +73,8 @@ const AdminUsers = () => {
const pageSize = 10; const pageSize = 10;
const [showDeleteModal, setShowDeleteModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false);
const [selectedUser, setSelectedUser] = useState(null); const [selectedUser, setSelectedUser] = useState(null);
const [deletingRow, setDeletingRow] = useState(null); const [deletingRow, setDeletingRow] = useState(null);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: '', name: '',
email: '', email: '',
@ -83,6 +84,14 @@ const [deletingRow, setDeletingRow] = useState(null);
const [toastData, setToastData] = useState(null); const [toastData, setToastData] = useState(null);
const navigate = useNavigate(); const navigate = useNavigate();
// Reset password modal state
const [isResetModalOpen, setIsResetModalOpen] = useState(false);
const [resetUser, setResetUser] = useState(null);
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [resettingPassword, setResettingPassword] = useState(false);
const [resetErrors, setResetErrors] = useState({});
// Helper: show toast // Helper: show toast
const showToast = (message, type = 'success') => { const showToast = (message, type = 'success') => {
setToastData({ message, type }); setToastData({ message, type });
@ -182,11 +191,6 @@ const [deletingRow, setDeletingRow] = useState(null);
const headers = ['Name', 'Email', 'Last Login', 'Status', 'Actions']; const headers = ['Name', 'Email', 'Last Login', 'Status', 'Actions'];
const columnWidths = [100, 130, 150, 100, 130]; const columnWidths = [100, 130, 150, 100, 130];
const [isResetModalOpen, setIsResetModalOpen] = useState(false);
const [resetUser, setResetUser] = useState(null);
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const rows = users.map((user) => [ const rows = users.map((user) => [
user.name, user.name,
@ -215,7 +219,7 @@ const [confirmPassword, setConfirmPassword] = useState('');
const updatedUser = { const updatedUser = {
name: formData.name, name: formData.name,
email: formData.email, email: formData.email,
is_active: formData.status === "Active", // status handled via is_active is_active: formData.status === "Active",
}; };
const res = await updateAdminUser(currentUser.id, updatedUser); const res = await updateAdminUser(currentUser.id, updatedUser);
@ -231,7 +235,6 @@ const [confirmPassword, setConfirmPassword] = useState('');
showToast("User updated successfully!", "success"); showToast("User updated successfully!", "success");
setIsEditModalOpen(false); setIsEditModalOpen(false);
// update UI
setUsers((prev) => setUsers((prev) =>
prev.map((u) => prev.map((u) =>
u.id === currentUser.id u.id === currentUser.id
@ -253,8 +256,7 @@ const [confirmPassword, setConfirmPassword] = useState('');
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const validateForm = () => { const validateForm = () => {
const newErrors = {}; const newErrors = {};
@ -267,22 +269,23 @@ const [confirmPassword, setConfirmPassword] = useState('');
setSelectedUser(user); setSelectedUser(user);
setShowDeleteModal(true); setShowDeleteModal(true);
}; };
const handleConfirmDelete = async () => { const handleConfirmDelete = async () => {
try { try {
if (!selectedUser) return; if (!selectedUser) return;
setDeletingRow(selectedUser.id); setDeletingRow(selectedUser.id);
await deleteAdminUser(selectedUser.id); // 🔥 API call await deleteAdminUser(selectedUser.id);
showToast("User deleted successfully!", "success"); // use showToast() showToast("User deleted successfully!", "success");
setUsers((prev) => prev.filter((u) => u.id !== selectedUser.id)); setUsers((prev) => prev.filter((u) => u.id !== selectedUser.id));
} catch (error) { } catch (error) {
showToast("Failed to delete user!", "error"); // same toast handler for error showToast("Failed to delete user!", "error");
} finally { } finally {
setDeletingRow(null); setDeletingRow(null);
setShowDeleteModal(false); setShowDeleteModal(false);
} }
}; };
const handleEdit = async (user) => { const handleEdit = async (user) => {
if (!user || !user.id) { if (!user || !user.id) {
@ -290,7 +293,7 @@ const [confirmPassword, setConfirmPassword] = useState('');
return; return;
} }
setEditingRow(user); // store which row is being edited setEditingRow(user);
setLoading(true); setLoading(true);
try { try {
@ -318,36 +321,130 @@ const [confirmPassword, setConfirmPassword] = useState('');
}); });
setErrors({}); setErrors({});
setIsEditModalOpen(true); // opens your modal setIsEditModalOpen(true);
} catch (error) { } catch (error) {
console.error('Error fetching user data:', error); console.error('Error fetching user data:', error);
showToast(error?.response?.data?.message || 'Failed to load user data', 'error'); showToast(error?.response?.data?.message || 'Failed to load user data', 'error');
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const handleOpenResetModal = (user) => { const handleOpenResetModal = (user) => {
setResetUser(user); setResetUser(user);
setNewPassword(''); setNewPassword('');
setConfirmPassword(''); setConfirmPassword('');
setResetErrors({});
setIsResetModalOpen(true); setIsResetModalOpen(true);
}; };
const handleCloseResetModal = () => { const handleCloseResetModal = () => {
setResetUser(null); setResetUser(null);
setNewPassword('');
setConfirmPassword('');
setResetErrors({});
setIsResetModalOpen(false); setIsResetModalOpen(false);
}; setResettingPassword(false);
};
// Validate reset password form
const validateResetForm = () => {
const newErrors = {};
if (!newPassword.trim()) {
newErrors.newPassword = 'Password is required';
} else if (newPassword.length < 6) {
newErrors.newPassword = 'Password must be at least 6 characters';
}
if (!confirmPassword.trim()) {
newErrors.confirmPassword = 'Please confirm your password';
} else if (newPassword !== confirmPassword) {
// Don't show error message, just prevent submission
newPassword !== confirmPassword;
}
return newErrors;
};
// Reset Password API Integration
const handleResetPassword = async () => {
if (!resetUser || !resetUser.id) {
showToast('No user selected for password reset', 'error');
return;
}
// Validate form
const formErrors = validateResetForm();
if (Object.keys(formErrors).length > 0) {
setResetErrors(formErrors);
return;
}
// Check if passwords match (without showing error message)
if (newPassword !== confirmPassword) {
// Just return without showing error
return;
}
try {
setResettingPassword(true);
const passwordData = {
newPassword: newPassword,
confirmPassword: confirmPassword
};
console.log('Initiating password reset for user:', resetUser.id);
const res = await changeAdminUserPassword(resetUser.id, passwordData);
console.log('Password reset response:', res);
// Check for different success patterns
const success =
res?.status === "success" ||
res?.status === 200 ||
res?.code === 200 ||
res?.data?.status === "success" ||
res?.message?.includes("success") ||
res?.message?.includes("updated") ||
(!res.error && res);
if (!success) {
throw new Error(res?.message || res?.data?.message || "Failed to reset password");
}
// No success toast message
handleCloseResetModal();
} catch (error) {
console.error('Error resetting password:', error);
// More detailed error handling
let errorMessage = 'Failed to reset password';
if (error.response?.data?.message) {
errorMessage = error.response.data.message;
} else if (error.response?.data?.errors) {
// Handle validation errors from backend
const errors = error.response.data.errors;
errorMessage = Object.values(errors).flat().join(', ');
} else if (error.message) {
errorMessage = error.message;
}
showToast(errorMessage, 'error');
} finally {
setResettingPassword(false);
}
};
// DELETE FUNCTIONALITY IMPLEMENTED HERE
const handleDelete = (rowIndex) => { const handleDelete = (rowIndex) => {
const user = users[rowIndex]; const user = users[rowIndex];
setSelectedUser(user); setSelectedUser(user);
setShowDeleteModal(true); setShowDeleteModal(true);
}; };
// END DELETE FUNCTIONALITY
const handleEditSubmit = async (e) => { const handleEditSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
@ -428,30 +525,25 @@ const handleCloseResetModal = () => {
onClick={() => { onClick={() => {
if (!users.length) return; if (!users.length) return;
// Define CSV headers (excluding date/time column logic)
const csvHeader = ['Name', 'Email', 'Status', 'Last Login'].join(','); const csvHeader = ['Name', 'Email', 'Status', 'Last Login'].join(',');
// Map user data into CSV rows
const csvRows = users.map((user) => const csvRows = users.map((user) =>
[user.name, user.email, user.status, user.lastLogin] [user.name, user.email, user.status, user.lastLogin]
.map((value) => `"${String(value ?? '').replace(/"/g, '""')}"`) .map((value) => `"${String(value ?? '').replace(/"/g, '""')}"`)
.join(',') .join(',')
); );
// Create the CSV file content
const blob = new Blob([csvHeader + '\n' + csvRows.join('\n')], { const blob = new Blob([csvHeader + '\n' + csvRows.join('\n')], {
type: 'text/csv;charset=utf-8;', type: 'text/csv;charset=utf-8;',
}); });
// Create download link
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const link = document.createElement('a'); const link = document.createElement('a');
link.href = url; link.href = url;
link.setAttribute('download', 'admin-users.csv'); // 👈 No date in filename link.setAttribute('download', 'admin-users.csv');
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
// Cleanup
document.body.removeChild(link); document.body.removeChild(link);
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
}} }}
@ -461,10 +553,10 @@ const handleCloseResetModal = () => {
: 'bg-[#F3F4F6] border-[#E5E7EB] text-[#9CA3AF] cursor-not-allowed' : 'bg-[#F3F4F6] border-[#E5E7EB] text-[#9CA3AF] cursor-not-allowed'
}`} }`}
disabled={!users.length} disabled={!users.length}
> >
<img src={exportIconSrc} alt="Export" className="h-4 w-4" /> <img src={exportIconSrc} alt="Export" className="h-4 w-4" />
<span>Export CSV</span> <span>Export CSV</span>
</button> </button>
<button <button
onClick={() => setIsAddUserModalOpen(true)} onClick={() => setIsAddUserModalOpen(true)}
@ -511,28 +603,27 @@ const handleCloseResetModal = () => {
type="button" type="button"
className="h-8 w-8 grid place-items-center rounded hover:bg-gray-50 cursor-pointer" className="h-8 w-8 grid place-items-center rounded hover:bg-gray-50 cursor-pointer"
title="Edit" title="Edit"
onClick={() => handleEdit(user)} // pass user directly instead of rowIndex onClick={() => handleEdit(user)}
> >
<img <img
src={editingRow?.id === user.id ? pencilActiveSrc : pencilInactiveSrc} src={editingRow?.id === user.id ? pencilActiveSrc : pencilInactiveSrc}
alt="Edit" alt="Edit"
className="h-4 w-4" className="h-4 w-4"
/> />
</button> </button>
<button <button
type="button" type="button"
className="h-8 w-8 grid place-items-center rounded hover:bg-gray-50 cursor-pointer" className="h-8 w-8 grid place-items-center rounded hover:bg-gray-50 cursor-pointer"
title="Delete" title="Delete"
onClick={() => handleDelete(rowIndex)} onClick={() => handleDelete(rowIndex)}
> >
<img <img
src={deletingRow === rowIndex ? trashActiveSrc : trashInactiveSrc} src={deletingRow === rowIndex ? trashActiveSrc : trashInactiveSrc}
alt="Delete" alt="Delete"
className="h-4 w-4" className="h-4 w-4"
/> />
</button> </button>
<button <button
type="button" type="button"
@ -542,96 +633,13 @@ const handleCloseResetModal = () => {
title={isActive ? 'Reset password' : 'Reset disabled for inactive user'} title={isActive ? 'Reset password' : 'Reset disabled for inactive user'}
disabled={!isActive} disabled={!isActive}
onClick={() => handleOpenResetModal(user)} onClick={() => handleOpenResetModal(user)}
> >
<img <img
src={isActive ? resetPasswordIconSrc : resetPasswordInactiveIconSrc} src={isActive ? resetPasswordIconSrc : resetPasswordInactiveIconSrc}
alt="Reset password" alt="Reset password"
className="h-4 w-4" className="h-4 w-4"
/> />
</button>
{isResetModalOpen && (
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/10">
<div className="bg-white rounded-lg w-[450px] shadow-lg p-6 relative">
{/* Close Button */}
<button
className="absolute top-3 right-3 text-gray-500 hover:text-gray-800 text-xl"
onClick={handleCloseResetModal}
>
</button> </button>
{/* Header */}
<h3 className="text-xl font-semibold text-gray-800 mb-6">Reset Password</h3>
{/* Input Fields */}
<div className="flex flex-col gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">New Password</label>
<input
type="password"
placeholder="Enter your new password"
className="w-full border border-[#D9C9A3] rounded-md px-3 py-2 focus:outline-none focus:ring-1 focus:ring-[#B68A35]"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Confirm Password</label>
<input
type="password"
placeholder="Re-enter your new password"
className="w-full border border-[#D9C9A3] rounded-md px-3 py-2 focus:outline-none focus:ring-1 focus:ring-[#B68A35]"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
</div>
</div>
{/* Buttons */}
<div className="flex justify-end gap-3 mt-6">
<button
className="px-5 py-2 rounded-md border border-gray-300 text-gray-700 hover:bg-gray-100"
onClick={handleCloseResetModal}
>
Cancel
</button>
<button
className="px-5 py-2 rounded-md bg-[#B68A35] text-white font-medium hover:bg-[#9c6f28]"
onClick={() => {
console.log({ newPassword, confirmPassword, user: resetUser });
handleCloseResetModal();
}}
>
Reset Password
</button>
</div>
</div>
</div>
)}
{/* Edit Modal */}
{isEditModalOpen && (
<EditUserModal
formData={formData}
setFormData={setFormData}
onClose={() => setIsEditModalOpen(false)}
onUpdate={handleUpdateUser}
/>
)}
{/* Delete Modal */}
{showDeleteModal && (
<DeleteUserModal
userName={selectedUser?.name}
onClose={() => setShowDeleteModal(false)}
onDelete={handleConfirmDelete}
/>
)}
</div> </div>
); );
} }
@ -661,6 +669,39 @@ const handleCloseResetModal = () => {
/> />
)} )}
{/* Edit Modal */}
{isEditModalOpen && (
<EditUserModal
formData={formData}
setFormData={setFormData}
onClose={() => setIsEditModalOpen(false)}
onUpdate={handleUpdateUser}
/>
)}
{/* Delete Modal */}
{showDeleteModal && (
<DeleteUserModal
userName={selectedUser?.name}
onClose={() => setShowDeleteModal(false)}
onDelete={handleConfirmDelete}
/>
)}
{/* Reset Password Modal */}
<ResetPasswordModal
isOpen={isResetModalOpen}
onClose={handleCloseResetModal}
newPassword={newPassword}
setNewPassword={setNewPassword}
confirmPassword={confirmPassword}
setConfirmPassword={setConfirmPassword}
onReset={handleResetPassword}
loading={resettingPassword}
errors={resetErrors}
setErrors={setResetErrors}
/>
{/* Toast */} {/* Toast */}
{toastData && ( {toastData && (
<div className="fixed inset-0 z-[9999] pointer-events-none"> <div className="fixed inset-0 z-[9999] pointer-events-none">

View File

@ -0,0 +1,182 @@
import React, { useState } from 'react';
const ResetPasswordModal = ({
isOpen,
onClose,
newPassword,
setNewPassword,
confirmPassword,
setConfirmPassword,
onReset,
loading = false
}) => {
const [errors, setErrors] = useState({});
const validateForm = () => {
const newErrors = {};
if (!newPassword.trim()) {
newErrors.newPassword = 'New password is required';
} else if (newPassword.length < 6) {
newErrors.newPassword = 'Password must be at least 6 characters';
}
if (!confirmPassword.trim()) {
newErrors.confirmPassword = 'Please confirm your password';
}
// Removed "Passwords do not match" error display
return newErrors;
};
const handleSubmit = () => {
const formErrors = validateForm();
// Check if passwords match (without showing error)
if (newPassword !== confirmPassword) {
return; // Just return without showing error
}
if (Object.keys(formErrors).length > 0) {
setErrors(formErrors);
return;
}
setErrors({});
onReset();
};
const handleInputChange = (field, value) => {
if (field === 'newPassword') setNewPassword(value);
if (field === 'confirmPassword') setConfirmPassword(value);
// Clear error when user starts typing
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: '' }));
}
};
const handleKeyPress = (e) => {
if (e.key === 'Enter' && !loading) {
handleSubmit();
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black bg-opacity-50 backdrop-blur-sm">
<div className="bg-white rounded-2xl w-[90%] max-w-md shadow-xl p-6 relative border border-gray-200">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-bold text-gray-900">Reset Password</h3>
<button
className="w-8 h-8 flex items-center justify-center rounded-lg hover:bg-gray-100 text-gray-500 hover:text-gray-700 transition-colors"
onClick={onClose}
disabled={loading}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Form */}
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
New Password
</label>
<input
type="password"
placeholder="Enter new password"
className={`w-full border rounded-lg px-4 py-3 text-sm focus:outline-none focus:ring-2 transition-all ${
errors.newPassword
? 'border-red-300 focus:ring-red-500 bg-red-50'
: 'border-gray-300 focus:border-[#B68A35] focus:ring-2 focus:ring-[#B68A35] focus:ring-opacity-20'
}`}
value={newPassword}
onChange={(e) => handleInputChange('newPassword', e.target.value)}
onKeyPress={handleKeyPress}
disabled={loading}
/>
{errors.newPassword && (
<p className="text-red-600 text-xs mt-2 flex items-center gap-1">
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
{errors.newPassword}
</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Confirm Password
</label>
<input
type="password"
placeholder="Confirm new password"
className={`w-full border rounded-lg px-4 py-3 text-sm focus:outline-none focus:ring-2 transition-all ${
errors.confirmPassword
? 'border-red-300 focus:ring-red-500 bg-red-50'
: 'border-gray-300 focus:border-[#B68A35] focus:ring-2 focus:ring-[#B68A35] focus:ring-opacity-20'
}`}
value={confirmPassword}
onChange={(e) => handleInputChange('confirmPassword', e.target.value)}
onKeyPress={handleKeyPress}
disabled={loading}
/>
{errors.confirmPassword && (
<p className="text-red-600 text-xs mt-2 flex items-center gap-1">
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
{errors.confirmPassword}
</p>
)}
</div>
</div>
{/* Note Section */}
<div className="mt-4 p-3 bg-blue-50 rounded-lg border border-blue-200">
<div className="flex items-start gap-2">
<svg className="w-4 h-4 text-blue-600 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
</svg>
<p className="text-sm text-blue-700">
<span className="font-medium">Note:</span> Password must be at least 6 characters long.
</p>
</div>
</div>
{/* Action Buttons */}
<div className="flex justify-end gap-3 mt-6 pt-4 border-t border-gray-200">
<button
className="px-6 py-2.5 rounded-lg border border-gray-300 text-gray-700 font-medium hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
onClick={onClose}
disabled={loading}
>
Cancel
</button>
<button
className="px-6 py-2.5 rounded-lg bg-[#B68A35] text-white font-medium hover:bg-[#9c6f28] disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 transition-colors"
onClick={handleSubmit}
disabled={loading}
>
{loading ? (
<>
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
Resetting...
</>
) : (
'Reset Password'
)}
</button>
</div>
</div>
</div>
);
};
export default ResetPasswordModal;

View File

@ -28,7 +28,7 @@ const createEmptyCodeForm = () => ({
}); });
// Toast notification component // Toast notification component
const Toast = ({ message, onClose }) => { const Toast = ({ message, type = 'success', onClose }) => {
React.useEffect(() => { React.useEffect(() => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
onClose(); onClose();
@ -36,15 +36,267 @@ const Toast = ({ message, onClose }) => {
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [onClose]); }, [onClose]);
const getToastStyles = () => {
switch (type) {
case 'success':
return 'bg-[#F3FAF4] text-[#3F8E50] border border-[#3F8E50]';
case 'error':
return 'bg-red-50 text-red-800 border border-red-200';
case 'warning':
return 'bg-yellow-50 text-yellow-800 border border-yellow-200';
default:
return 'bg-blue-50 text-blue-800 border border-blue-200';
}
};
return ( return (
<div className="fixed top-0 left-0 w-full flex justify-center z-50 mt-4"> <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"> <div className={`px-6 py-3 rounded-md shadow-lg font-medium ${getToastStyles()}`}>
{message} {message}
</div> </div>
</div> </div>
); );
}; };
// Import Modal Component
const ImportModal = ({ isOpen, onClose, onImport }) => {
const [file, setFile] = React.useState(null);
const [dragActive, setDragActive] = React.useState(false);
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState('');
const fileInputRef = React.useRef(null);
const handleDrag = (e) => {
e.preventDefault();
e.stopPropagation();
if (e.type === "dragenter" || e.type === "dragover") {
setDragActive(true);
} else if (e.type === "dragleave") {
setDragActive(false);
}
};
const handleDrop = (e) => {
e.preventDefault();
e.stopPropagation();
setDragActive(false);
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
const droppedFile = e.dataTransfer.files[0];
validateAndSetFile(droppedFile);
}
};
const handleFileChange = (e) => {
if (e.target.files && e.target.files[0]) {
const selectedFile = e.target.files[0];
validateAndSetFile(selectedFile);
}
};
const validateAndSetFile = (file) => {
setError('');
// Check file type
if (!file.name.toLowerCase().endsWith('.csv')) {
setError('Only .csv files are allowed.');
return;
}
// Check file size (10MB limit)
const maxSize = 10 * 1024 * 1024; // 10MB in bytes
if (file.size > maxSize) {
setError('File size exceeds limit (10MB)');
return;
}
setFile(file);
};
const handleImport = async () => {
if (!file) {
setError('Please select a file to import');
return;
}
try {
setLoading(true);
setError('');
// Simulate file upload - replace with actual API call
await new Promise(resolve => setTimeout(resolve, 2000));
// Call the onImport callback with the file
await onImport(file);
// Reset and close on success
setFile(null);
onClose();
} catch (err) {
setError('Failed to import file. Please try again.');
} finally {
setLoading(false);
}
};
const handleClose = () => {
setFile(null);
setError('');
setDragActive(false);
onClose();
};
const downloadSampleCSV = () => {
const sampleData = [
['HS Code', 'Product Name', 'Unit', 'Description', 'Status'],
['0101', 'Live Horses', 'kg', 'Live pure-bred breeding horses', 'Active'],
['0102', 'Live Bovine Animals', 'kg', 'Live bovine animals', 'Active'],
['0103', 'Live Swine', 'kg', 'Live swine', 'Active'],
['0104', 'Live Sheep And Goats', 'kg', 'Live sheep and goats', 'Active'],
['0105', 'Live Poultry', 'kg', 'Live poultry', 'Active']
];
const csvContent = sampleData.map(row =>
row.map(field => `"${field}"`).join(',')
).join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', 'hs-codes-sample.csv');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50">
<div className="absolute inset-0 bg-black/40" onClick={handleClose} />
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[520px] bg-white rounded-lg shadow-xl border border-[#E5E7EB]">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-[#E5E7EB]">
<h3 className="text-lg font-semibold text-[#111827]">Import HS Codes</h3>
<button
type="button"
className="text-gray-400 hover:text-gray-500"
onClick={handleClose}
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Content */}
<div className="p-6 space-y-4">
{/* File Upload Area */}
<div
className={`border-2 border-dashed rounded-lg p-6 text-center cursor-pointer transition-colors ${
dragActive
? 'border-[#92722A] bg-[#FDF8EA]'
: 'border-[#D1D5DB] hover:border-[#92722A] hover:bg-gray-50'
}`}
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()}
>
<div className="flex flex-col items-center justify-center space-y-3">
<div className="w-12 h-12 bg-[#FDF8EA] rounded-full flex items-center justify-center">
<img src={uploadImportIconSrc} alt="Upload" className="w-6 h-6" />
</div>
<div>
<p className="text-sm font-medium text-gray-900">
{file ? file.name : 'Drop your CSV file here or browse'}
</p>
<p className="text-sm text-gray-500 mt-1">
Supports .csv files only (Max 10MB)
</p>
</div>
<button
type="button"
className="px-4 py-2 text-sm font-medium text-[#92722A] bg-white border border-[#92722A] rounded-md hover:bg-[#FDF8EA]"
onClick={(e) => {
e.stopPropagation();
fileInputRef.current?.click();
}}
>
Browse Files
</button>
</div>
<input
ref={fileInputRef}
type="file"
accept=".csv"
onChange={handleFileChange}
className="hidden"
/>
</div>
{/* Error Message */}
{error && (
<div className="p-3 bg-red-50 border border-red-200 rounded-md">
<p className="text-sm text-red-800 flex items-center gap-2">
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
{error}
</p>
</div>
)}
{/* Sample CSV Link */}
<div className="text-center">
<button
type="button"
onClick={downloadSampleCSV}
className="text-sm text-[#92722A] hover:text-[#7a5f22] underline"
>
Download sample CSV template
</button>
</div>
</div>
{/* Footer */}
<div className="flex justify-end p-6 border-t border-[#E5E7EB] space-x-3">
<button
type="button"
onClick={handleClose}
className="h-10 px-6 text-sm font-medium text-[#374151] bg-white border border-[#D1D5DB] rounded-md hover:bg-gray-50"
>
Cancel
</button>
<button
type="button"
onClick={handleImport}
disabled={!file || loading}
className={`h-10 px-6 text-sm font-medium text-white rounded-md ${
!file || loading
? 'bg-gray-300 cursor-not-allowed'
: 'bg-[#92722A] hover:bg-[#7a5f22]'
}`}
>
{loading ? (
<div className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
Importing...
</div>
) : (
'Import'
)}
</button>
</div>
</div>
</div>
);
};
const IsicHsCodes = () => { const IsicHsCodes = () => {
const [rowsData, setRowsData] = React.useState([]); const [rowsData, setRowsData] = React.useState([]);
const [filteredData, setFilteredData] = React.useState([]); const [filteredData, setFilteredData] = React.useState([]);
@ -59,7 +311,8 @@ const IsicHsCodes = () => {
const [modalMode, setModalMode] = React.useState(null); const [modalMode, setModalMode] = React.useState(null);
const [form, setForm] = React.useState(createEmptyCodeForm()); const [form, setForm] = React.useState(createEmptyCodeForm());
const [unitOptions, setUnitOptions] = React.useState([]); const [unitOptions, setUnitOptions] = React.useState([]);
const [toast, setToast] = React.useState({ show: false, message: '' }); const [toast, setToast] = React.useState({ show: false, message: '', type: 'success' });
const [showImportModal, setShowImportModal] = React.useState(false);
// Get user profile from session // Get user profile from session
const userProfile = React.useMemo(() => { const userProfile = React.useMemo(() => {
@ -127,20 +380,13 @@ const IsicHsCodes = () => {
if (products.length > 0) { if (products.length > 0) {
const formattedData = await Promise.all(products.map(async (product) => { const formattedData = await Promise.all(products.map(async (product) => {
// Try to get creator's name if available
let createdByName = '-'; let createdByName = '-';
const createdById = product.created_by; const createdById = product.created_by;
// If still not available, try to fetch the creator's name by ID
// if (!createdByName && product.created_by) {
try { try {
const userProfileStr = sessionStorage.getItem('user_profile'); const userProfileStr = sessionStorage.getItem('user_profile');
if (userProfileStr) { if (userProfileStr) {
const userProfile = JSON.parse(userProfileStr); const userProfile = JSON.parse(userProfileStr);
// If the current user is the creator, use their name
if (createdById && createdById === userProfile.id) { if (createdById && createdById === userProfile.id) {
createdByName = userProfile.name || '-'; createdByName = userProfile.name || '-';
} }
@ -149,17 +395,16 @@ const IsicHsCodes = () => {
console.error('Error getting user from session:', error); console.error('Error getting user from session:', error);
} }
const createdDate = product.created_at const createdDate = product.created_at
? new Date(product.created_at).toLocaleDateString('en-GB') ? new Date(product.created_at).toLocaleDateString('en-GB')
: '-'; : '-';
const updatedDate = product.updated_at const updatedDate = product.updated_at
? new Date(product.updated_at).toLocaleDateString('en-GB') ? new Date(product.updated_at).toLocaleDateString('en-GB')
: createdDate; // Fallback to created date if updated_at is not available : createdDate;
if (product.created_by) { if (product.created_by) {
try { try {
// If the API returns the creator's name in the product data, use it
createdByName = product.created_by_user?.name || '-'; createdByName = product.created_by_user?.name || '-';
} catch (error) { } catch (error) {
console.error('Error fetching creator info:', error); console.error('Error fetching creator info:', error);
@ -175,12 +420,11 @@ const IsicHsCodes = () => {
createdBy: createdByName, createdBy: createdByName,
createdOn: createdDate || null, createdOn: createdDate || null,
updated: updatedDate, updated: updatedDate,
createdById: createdById, // Store the ID for reference createdById: createdById,
status: product.is_active ? 'Active' : 'Inactive', status: product.is_active ? 'Active' : 'Inactive',
description: product.hs_description || '', description: product.hs_description || '',
_createdAt: product.created_at, _createdAt: product.created_at,
_updatedAt: product.updated_at _updatedAt: product.updated_at
}; };
})); }));
setRowsData(formattedData); setRowsData(formattedData);
@ -229,7 +473,6 @@ const IsicHsCodes = () => {
}, [searchTerm, rowsData]); }, [searchTerm, rowsData]);
const rows = filteredData.map((item) => { const rows = filteredData.map((item) => {
// If we only have the ID and it matches the current user, use the current user's name
let displayName = item.createdBy; let displayName = item.createdBy;
if (displayName === '-' && item.createdById) { if (displayName === '-' && item.createdById) {
try { try {
@ -250,12 +493,12 @@ const IsicHsCodes = () => {
item.product, item.product,
item.unit, item.unit,
item.estimatedMapped, item.estimatedMapped,
displayName, // Use the resolved display name displayName,
item.updated || item.createdOn || '-', item.updated || item.createdOn || '-',
item.status, item.status,
'actions', 'actions',
]; ];
}); });
const handleView = async (index) => { const handleView = async (index) => {
const selected = rowsData[index]; const selected = rowsData[index];
@ -266,13 +509,11 @@ const IsicHsCodes = () => {
const productId = selected.id; const productId = selected.id;
console.log(`Fetching product details for ID: ${productId}`); console.log(`Fetching product details for ID: ${productId}`);
// Make API call to get product details
const response = await productService.getProductById(productId); const response = await productService.getProductById(productId);
console.log('Product details response:', response); console.log('Product details response:', response);
if (response && response.data) { if (response && response.data) {
const product = response.data; const product = response.data;
// Map API response to form fields
setForm({ setForm({
code: product.hs_code || '', code: product.hs_code || '',
product: product.product_name || '', product: product.product_name || '',
@ -286,11 +527,11 @@ const IsicHsCodes = () => {
setModalMode('view'); setModalMode('view');
} else { } else {
console.error('Invalid product data received from API'); console.error('Invalid product data received from API');
showToast('Failed to load product details'); showToast('Failed to load product details', 'error');
} }
} catch (error) { } catch (error) {
console.error('Error fetching product details:', error); console.error('Error fetching product details:', error);
showToast('Error loading product details'); showToast('Error loading product details', 'error');
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@ -305,13 +546,11 @@ const IsicHsCodes = () => {
const productId = selected.id; const productId = selected.id;
console.log(`Fetching product details for editing ID: ${productId}`); console.log(`Fetching product details for editing ID: ${productId}`);
// Fetch the latest product data by ID
const response = await productService.getProductById(productId); const response = await productService.getProductById(productId);
console.log('Product details for edit:', response); console.log('Product details for edit:', response);
if (response && response.data) { if (response && response.data) {
const product = response.data; const product = response.data;
// Map API response to form fields
setForm({ setForm({
code: product.hs_code || '', code: product.hs_code || '',
product: product.product_name || '', product: product.product_name || '',
@ -323,21 +562,19 @@ const IsicHsCodes = () => {
setModalMode('edit'); setModalMode('edit');
} else { } else {
console.error('Invalid product data received from API'); console.error('Invalid product data received from API');
showToast('Failed to load product details for editing'); showToast('Failed to load product details for editing', 'error');
} }
} catch (error) { } catch (error) {
console.error('Error fetching product details for edit:', error); console.error('Error fetching product details for edit:', error);
showToast('Error loading product details for editing'); showToast('Error loading product details for editing', 'error');
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}; };
// Store the row index when delete is clicked
const [deletingRowIndex, setDeletingRowIndex] = React.useState(null); const [deletingRowIndex, setDeletingRowIndex] = React.useState(null);
const handleDelete = async (paginationIndex) => { const handleDelete = async (paginationIndex) => {
// Convert pagination index to actual data index
const dataIndex = (currentPage - 1) * pageSize + paginationIndex; const dataIndex = (currentPage - 1) * pageSize + paginationIndex;
console.log('Delete button clicked, paginationIndex:', paginationIndex, 'dataIndex:', dataIndex); console.log('Delete button clicked, paginationIndex:', paginationIndex, 'dataIndex:', dataIndex);
@ -352,14 +589,12 @@ const IsicHsCodes = () => {
return; return;
} }
// Store the row index for later use
setDeletingRowIndex(dataIndex); setDeletingRowIndex(dataIndex);
try { try {
setIsLoading(true); setIsLoading(true);
console.log('Fetching product details before deletion, ID:', productId); console.log('Fetching product details before deletion, ID:', productId);
// Fetch the latest product data by ID
const response = await productService.getProductById(productId); const response = await productService.getProductById(productId);
console.log('Product details from API:', response); console.log('Product details from API:', response);
@ -367,7 +602,6 @@ const IsicHsCodes = () => {
throw new Error('Invalid product data received'); throw new Error('Invalid product data received');
} }
// Update the row data with the latest from the server
const updatedRowsData = [...rowsData]; const updatedRowsData = [...rowsData];
updatedRowsData[dataIndex] = { updatedRowsData[dataIndex] = {
...updatedRowsData[dataIndex], ...updatedRowsData[dataIndex],
@ -419,39 +653,24 @@ const IsicHsCodes = () => {
} }
console.log('Deleting product:', { productId, product }); console.log('Deleting product:', { productId, product });
console.log('Product ID to delete:', productId);
if (!productId) {
const errorMsg = 'Product ID not found for deletion';
console.error(errorMsg);
throw new Error(errorMsg);
}
// Call delete API
console.log(`Deleting product with ID: ${productId}`);
try { try {
await productService.deleteProduct(productId); await productService.deleteProduct(productId);
// If we get here, the delete was successful (204 No Content)
// Remove from local state
const newData = [...rowsData]; const newData = [...rowsData];
newData.splice(deletingRow, 1); newData.splice(deletingRowIndex, 1);
setRowsData(newData); setRowsData(newData);
showToast('Product deleted successfully!'); showToast('Product deleted successfully!', 'success');
} catch (apiError) { } catch (apiError) {
console.error('API Error:', 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) { if (apiError.response && apiError.response.status === 204) {
// Remove from local state
const newData = [...rowsData]; const newData = [...rowsData];
newData.splice(deletingRow, 1); newData.splice(deletingRowIndex, 1);
setRowsData(newData); setRowsData(newData);
showToast('Product deleted successfully!'); showToast('Product deleted successfully!', 'success');
} else { } else {
// Handle other errors
const errorMessage = apiError.response?.data?.message || 'Failed to delete product. Please try again.'; const errorMessage = apiError.response?.data?.message || 'Failed to delete product. Please try again.';
setError(errorMessage); setError(errorMessage);
showToast(errorMessage, 'error'); showToast(errorMessage, 'error');
@ -465,12 +684,10 @@ const IsicHsCodes = () => {
} finally { } finally {
setIsLoading(false); setIsLoading(false);
setShowDeleteConfirm(false); setShowDeleteConfirm(false);
setDeletingRow(null); setDeletingRowIndex(null);
} }
}; };
// Unit options are now fetched from the API and will be used in the dropdown
const statusOptions = [ const statusOptions = [
{ label: 'Active', value: 'Active' }, { label: 'Active', value: 'Active' },
{ label: 'Inactive', value: 'Inactive' }, { label: 'Inactive', value: 'Inactive' },
@ -488,8 +705,8 @@ const IsicHsCodes = () => {
setForm(createEmptyCodeForm()); setForm(createEmptyCodeForm());
}; };
const showToast = (message) => { const showToast = (message, type = 'success') => {
setToast({ show: true, message }); setToast({ show: true, message, type });
setTimeout(() => { setTimeout(() => {
setToast(prev => ({ ...prev, show: false })); setToast(prev => ({ ...prev, show: false }));
}, 3000); }, 3000);
@ -500,9 +717,55 @@ const IsicHsCodes = () => {
setForm((prev) => ({ ...prev, [field]: value })); setForm((prev) => ({ ...prev, [field]: value }));
}; };
const getToday = () => { const handleImportCSV = async (file) => {
const now = new Date(); try {
return now.toLocaleDateString('en-GB'); // Simulate CSV processing - replace with actual API call
console.log('Importing file:', file);
// Simulate successful import
await new Promise(resolve => setTimeout(resolve, 1500));
// Show success message
showToast('HS Codes imported successfully! New codes appear in the list.', 'success');
// In a real implementation, you would:
// 1. Call your import API endpoint
// 2. Refresh the data from the server
// 3. Update the rowsData state with the new data
} catch (error) {
showToast('Failed to import CSV file. Please try again.', 'error');
}
};
const handleExportCSV = () => {
const csvHeader = headers.slice(0, headers.length - 1).join(',');
const csvRows = rowsData.map((item) => {
return [
item.code,
item.product,
item.unit,
item.estimatedMapped,
item.createdBy,
item.updated || item.createdOn || '-',
item.status,
]
.map((value) => `"${String(value ?? '').replace(/"/g, '""')}"`)
.join(',');
});
const csvContent = csvHeader + '\n' + csvRows.join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', 'hs-codes-export.csv');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
showToast('CSV exported successfully!', 'success');
}; };
const handleSaveForm = async () => { const handleSaveForm = async () => {
@ -517,22 +780,20 @@ const IsicHsCodes = () => {
try { try {
setIsLoading(true); setIsLoading(true);
setError(null); // Clear previous errors setError(null);
// Always include these fields for both create and update
const productData = { const productData = {
product_name: form.product, product_name: form.product,
is_active: form.status === 'Active', is_active: form.status === 'Active',
hs_code: form.code, hs_code: form.code,
unit_id: form.unit ? parseInt(form.unit) : null, unit_id: form.unit ? parseInt(form.unit) : null,
created_by: userProfile?.id || null, // Add created_by with user ID created_by: userProfile?.id || null,
...(form.description && { hs_description: form.description }) ...(form.description && { hs_description: form.description })
}; };
console.log('Saving product data:', JSON.stringify(productData, null, 2)); console.log('Saving product data:', JSON.stringify(productData, null, 2));
if (modalMode === 'edit' && editingRow !== null) { if (modalMode === 'edit' && editingRow !== null) {
// Update existing product
const productId = rowsData[editingRow]?.id; const productId = rowsData[editingRow]?.id;
if (!productId) { if (!productId) {
throw new Error('Product ID not found for editing'); throw new Error('Product ID not found for editing');
@ -541,21 +802,12 @@ const IsicHsCodes = () => {
console.log(`Updating product with ID: ${productId}`); console.log(`Updating product with ID: ${productId}`);
try { try {
console.log('Sending update request with data:', {
productId,
productData,
formUnit: form.unit,
unitOptions: unitOptions
});
const response = await productService.updateProduct(productId, productData); const response = await productService.updateProduct(productId, productData);
console.log('Update response:', response); console.log('Update response:', response);
// Find the selected unit from unitOptions to get the uom
const selectedUnit = unitOptions.find(u => u.value === form.unit); const selectedUnit = unitOptions.find(u => u.value === form.unit);
console.log('Selected unit for update:', selectedUnit); console.log('Selected unit for update:', selectedUnit);
// Create the updated product object
const updatedProduct = { const updatedProduct = {
...rowsData[editingRow], ...rowsData[editingRow],
code: form.code, code: form.code,
@ -567,14 +819,12 @@ const IsicHsCodes = () => {
description: form.description || '' description: form.description || ''
}; };
// Update the product in the list while maintaining the same position
setRowsData(prev => { setRowsData(prev => {
const updated = [...prev]; const updated = [...prev];
updated[editingRow] = updatedProduct; updated[editingRow] = updatedProduct;
return updated; return updated;
}); });
// Also update filteredData if needed
setFilteredData(prev => { setFilteredData(prev => {
const updated = [...prev]; const updated = [...prev];
const index = updated.findIndex(item => item.id === updatedProduct.id); const index = updated.findIndex(item => item.id === updatedProduct.id);
@ -584,7 +834,7 @@ const IsicHsCodes = () => {
return updated; return updated;
}); });
showToast('Product updated successfully!'); showToast('Product updated successfully!', 'success');
closeModal(); closeModal();
return; return;
} catch (updateError) { } catch (updateError) {
@ -598,43 +848,34 @@ const IsicHsCodes = () => {
} }
} }
// If we get here, it's a create operation
console.log('Creating new product...'); console.log('Creating new product...');
try { try {
const response = await productService.createProduct(productData); const response = await productService.createProduct(productData);
console.log('Create response:', response); console.log('Create response:', response);
// Find the selected unit from unitOptions to get the uom
const selectedUnit = unitOptions.find(u => u.value === form.unit); const selectedUnit = unitOptions.find(u => u.value === form.unit);
const newProduct = { const newProduct = {
id: response.data?.id || Date.now(), // Fallback to timestamp if no ID in response id: response.data?.id || Date.now(),
code: form.code, code: form.code,
product: form.product, product: form.product,
unit: selectedUnit ? selectedUnit.label : 'N/A', unit: selectedUnit ? selectedUnit.label : 'N/A',
unit_id: form.unit ? parseInt(form.unit) : null, unit_id: form.unit ? parseInt(form.unit) : null,
estimatedMapped: 0, estimatedMapped: 0,
createdBy: userProfile?.name || 'System', // Use user's name or fallback to 'System' createdBy: userProfile?.name || 'System',
createdById: userProfile?.id || null, // Store user ID for reference createdById: userProfile?.id || null,
updated: new Date().toLocaleDateString('en-GB'), updated: new Date().toLocaleDateString('en-GB'),
status: form.status, status: form.status,
description: form.description || '' description: form.description || ''
}; };
// Add new product to the beginning of the array to show it at the top
setRowsData(prev => [newProduct, ...prev]); setRowsData(prev => [newProduct, ...prev]);
setFilteredData(prev => [newProduct, ...prev]); setFilteredData(prev => [newProduct, ...prev]);
console.log('Product created successfully'); console.log('Product created successfully');
showToast('Product created successfully!'); showToast('Product created successfully!', 'success');
closeModal(); closeModal();
} catch (createError) { } catch (createError) {
console.error('Error in product creation:', 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.'; let errorMessage = 'Failed to create product. Please try again.';
if (createError.response?.data?.message) { if (createError.response?.data?.message) {
errorMessage = createError.response.data.message; errorMessage = createError.response.data.message;
@ -646,9 +887,6 @@ const IsicHsCodes = () => {
let errorMessage = 'An unexpected error occurred. Please try again.'; let errorMessage = 'An unexpected error occurred. Please try again.';
if (error.response) { if (error.response) {
console.error('Response data:', error.response.data);
console.error('Response status:', error.response.status);
if (error.response.status === 401) { if (error.response.status === 401) {
errorMessage = 'Authentication required. Please log in again.'; errorMessage = 'Authentication required. Please log in again.';
} else if (error.response.status === 403) { } else if (error.response.status === 403) {
@ -661,10 +899,8 @@ const IsicHsCodes = () => {
errorMessage = error.response.data.message; errorMessage = error.response.data.message;
} }
} else if (error.request) { } else if (error.request) {
console.error('No response received:', error.request);
errorMessage = 'No response from server. Please check your network connection.'; errorMessage = 'No response from server. Please check your network connection.';
} else { } else {
console.error('Request setup error:', error.message);
errorMessage = error.message || 'Error setting up the request.'; errorMessage = error.message || 'Error setting up the request.';
} }
@ -679,9 +915,18 @@ const IsicHsCodes = () => {
{toast.show && ( {toast.show && (
<Toast <Toast
message={toast.message} message={toast.message}
type={toast.type}
onClose={() => setToast(prev => ({ ...prev, show: false }))} onClose={() => setToast(prev => ({ ...prev, show: false }))}
/> />
)} )}
{/* Import Modal */}
<ImportModal
isOpen={showImportModal}
onClose={() => setShowImportModal(false)}
onImport={handleImportCSV}
/>
<div className="bg-white w-full rounded-lg shadow-sm ring-1 ring-[#E5E7EB]"> <div className="bg-white w-full rounded-lg shadow-sm ring-1 ring-[#E5E7EB]">
{/* Header */} {/* Header */}
<div className="px-6 py-3 flex items-center justify-between"> <div className="px-6 py-3 flex items-center justify-between">
@ -699,17 +944,23 @@ const IsicHsCodes = () => {
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]" 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> </div>
<button className="h-10 px-4 rounded-[6px] bg-[#F7F7F7] border border-[#C3C6CB] text-sm inline-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 hover:bg-gray-50"
onClick={() => setShowImportModal(true)}
>
<img src={uploadImportIconSrc} alt="Import" className="h-5 w-5" /> <img src={uploadImportIconSrc} alt="Import" className="h-5 w-5" />
<span className="font-medium text-[#232528]">Import CSV</span> <span className="font-medium text-[#232528]">Import CSV</span>
</button> </button>
<button className="h-10 px-4 rounded-[6px] bg-[#F7F7F7] border border-[#C3C6CB] text-sm inline-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 hover:bg-gray-50"
onClick={handleExportCSV}
>
<img src={downloadIconSrc} alt="Export" className="h-5 w-5" /> <img src={downloadIconSrc} alt="Export" className="h-5 w-5" />
<span className="font-medium text-[#232528]">Export CSV</span> <span className="font-medium text-[#232528]">Export CSV</span>
</button> </button>
<button <button
type="button" type="button"
className="h-10 px-4 rounded-[6px] bg-[#92722A] text-white text-sm inline-flex items-center gap-2" className="h-10 px-4 rounded-[6px] bg-[#92722A] text-white text-sm inline-flex items-center gap-2 hover:bg-[#7a5f22]"
onClick={openAddModal} onClick={openAddModal}
> >
<img src={addIconSrc} alt="Add" className="h-5 w-5" /> <img src={addIconSrc} alt="Add" className="h-5 w-5" />
@ -723,7 +974,6 @@ const IsicHsCodes = () => {
headers={headers} headers={headers}
rows={rows} rows={rows}
renderCell={(value, rowIndex, colIndex) => { renderCell={(value, rowIndex, colIndex) => {
// STATUS COLUMN
if (colIndex === 6) { if (colIndex === 6) {
return ( return (
<StatusBadge <StatusBadge
@ -733,7 +983,6 @@ const IsicHsCodes = () => {
); );
} }
// ACTIONS COLUMN
if (colIndex === 7) { if (colIndex === 7) {
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -821,7 +1070,6 @@ const IsicHsCodes = () => {
{/* Form */} {/* Form */}
<div className="p-6 space-y-4"> <div className="p-6 space-y-4">
{/* HS Code */}
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="block text-sm font-medium text-[#374151] mb-1"> <label className="block text-sm font-medium text-[#374151] mb-1">
@ -852,7 +1100,6 @@ const IsicHsCodes = () => {
</div> </div>
</div> </div>
{/* Row 2: Unit + Status */}
<div className="grid grid-cols-2 gap-4 mt-4"> <div className="grid grid-cols-2 gap-4 mt-4">
<div> <div>
<label className="block text-sm font-medium text-[#374151] mb-1"> <label className="block text-sm font-medium text-[#374151] mb-1">

View File

@ -3,8 +3,7 @@ import Table from '@/components/common/Table';
import StatusBadge from '@/components/common/StatusBadge'; import StatusBadge from '@/components/common/StatusBadge';
import { TextField, SelectField } from '@/components/common/FormControls'; import { TextField, SelectField } from '@/components/common/FormControls';
import { getUnits, createUnit, updateUnit, deleteUnit } from '@/services/configuration/unitService'; import { getUnits, createUnit, updateUnit, deleteUnit } from '@/services/configuration/unitService';
import { toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
const downloadIconSrc = '/assets/images/DownloadSimple.svg'; const downloadIconSrc = '/assets/images/DownloadSimple.svg';
const addIconSrc = '/assets/images/ic_baseline-plus.svg'; const addIconSrc = '/assets/images/ic_baseline-plus.svg';
const pencilActiveSrc = '/assets/images/PencilSimple.svg'; const pencilActiveSrc = '/assets/images/PencilSimple.svg';
@ -16,6 +15,56 @@ const caretDownIconSrc = '/assets/images/caretdown-active.svg';
const rectangleIconSrc = '/assets/images/Rectangle.svg'; const rectangleIconSrc = '/assets/images/Rectangle.svg';
const checkboxIconSrc = '/assets/images/checkbox.svg'; const checkboxIconSrc = '/assets/images/checkbox.svg';
// Custom Toast Component
const CustomToast = ({ message, type, onClose }) => {
useEffect(() => {
const timer = setTimeout(() => {
onClose();
}, 3000);
return () => clearTimeout(timer);
}, [onClose]);
const getToastStyles = () => {
switch (type) {
case 'success':
return 'bg-green-50 border-green-200 text-green-800';
case 'error':
return 'bg-red-50 border-red-200 text-red-800';
case 'warning':
return 'bg-yellow-50 border-yellow-200 text-yellow-800';
default:
return 'bg-blue-50 border-blue-200 text-blue-800';
}
};
const getIcon = () => {
switch (type) {
case 'success':
return '✅';
case 'error':
return '❌';
case 'warning':
return '⚠️';
default:
return '';
}
};
return (
<div className={`flex items-center p-4 mb-4 rounded-lg border ${getToastStyles()} shadow-lg min-w-80 max-w-md`}>
<span className="text-lg mr-3">{getIcon()}</span>
<span className="flex-1 text-sm font-medium">{message}</span>
<button
onClick={onClose}
className="ml-4 text-gray-400 hover:text-gray-600 transition-colors"
>
</button>
</div>
);
};
const createEmptyUnitForm = () => ({ const createEmptyUnitForm = () => ({
unitName: '', unitName: '',
description: '', description: '',
@ -33,26 +82,15 @@ const UnitMaster = () => {
const [form, setForm] = useState(createEmptyUnitForm()); const [form, setForm] = useState(createEmptyUnitForm());
const [isProductDropdownOpen, setIsProductDropdownOpen] = useState(false); const [isProductDropdownOpen, setIsProductDropdownOpen] = useState(false);
const productDropdownRef = useRef(null); const productDropdownRef = useRef(null);
const pageSize = 10; // Add this line const pageSize = 10;
const [currentUser, setCurrentUser] = useState(null); const [currentUser, setCurrentUser] = useState(null);
const [toastData, setToastData] = useState(null);
// Fetch units on component mount // Fetch units on component mount
useEffect(() => { useEffect(() => {
fetchUnits(); fetchUnits();
}, []); }, []);
// const fetchUnits = async () => {
// try {
// setLoading(true);
// const response = await getUnits();
// setUnits(response.data || []);
// } catch (error) {
// console.error('Error fetching units:', error);
// } finally {
// setLoading(false);
// }
// };
const fetchUnits = async () => { const fetchUnits = async () => {
try { try {
setLoading(true); setLoading(true);
@ -70,26 +108,25 @@ const UnitMaster = () => {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
useEffect(() => { useEffect(() => {
const userProfile = sessionStorage.getItem('user_profile'); const userProfile = sessionStorage.getItem('user_profile');
if (userProfile) { if (userProfile) {
setCurrentUser(JSON.parse(userProfile)); setCurrentUser(JSON.parse(userProfile));
} }
}, []); }, []);
const headers = [ const headers = [
'Unit Name', 'Unit Name',
'Description', 'Description',
'Mapped Products', 'Mapped Products',
'Created By', 'Created By',
'Created On', 'Created On',
'Last Updated', // New column 'Last Updated',
'Status', 'Status',
'Actions', 'Actions',
]; ];
const rows = useMemo(() => { const rows = useMemo(() => {
return units.map((item) => { return units.map((item) => {
@ -103,23 +140,22 @@ const UnitMaster = () => {
const currentUserName = currentUser?.name || ''; const currentUserName = currentUser?.name || '';
return [ return [
item.uom || item.unitName, // Unit Name item.uom || item.unitName,
item.uom_short_name || item.description, // Description item.uom_short_name || item.description,
item.mapped_products_count !== undefined item.mapped_products_count !== undefined
? item.mapped_products_count ? item.mapped_products_count
: (Array.isArray(item.productsMapped) ? item.productsMapped.length : 0), : (Array.isArray(item.productsMapped) ? item.productsMapped.length : 0),
item.created_by_name || currentUserName || '-', // Created By item.created_by_name || currentUserName || '-',
createdDate, // Created On createdDate,
updatedDate, // Last Update (moved here) updatedDate,
<StatusBadge <StatusBadge
status={item.is_active ? 'Active' : 'Inactive'} status={item.is_active ? 'Active' : 'Inactive'}
tone={item.is_active ? 'green' : 'gray'} tone={item.is_active ? 'green' : 'gray'}
/>, // Status />,
'actions', // Actions 'actions',
]; ];
}); });
}, [units, currentUser]); }, [units, currentUser]);
const statusOptions = React.useMemo( const statusOptions = React.useMemo(
() => [ () => [
@ -166,23 +202,12 @@ const UnitMaster = () => {
}); });
setModalMode('edit'); setModalMode('edit');
setIsProductDropdownOpen(false); setIsProductDropdownOpen(false);
}; };
const handleDelete = (index) => { const handleDelete = (index) => {
setDeletingRow(index); setDeletingRow(index);
}; };
const confirmDelete = async () => {
if (deletingRow === null) return;
try {
await deleteUnit(units[deletingRow].id);
setUnits(prev => prev.filter((_, index) => index !== deletingRow));
setDeletingRow(null);
} catch (error) {
console.error('Error deleting unit:', error);
}
};
const closeModal = () => { const closeModal = () => {
setModalMode(null); setModalMode(null);
setForm(createEmptyUnitForm()); setForm(createEmptyUnitForm());
@ -206,7 +231,6 @@ const UnitMaster = () => {
}); });
}; };
React.useEffect(() => { React.useEffect(() => {
const handleClickOutside = (event) => { const handleClickOutside = (event) => {
if (!productDropdownRef.current) return; if (!productDropdownRef.current) return;
@ -224,86 +248,29 @@ const UnitMaster = () => {
}; };
}, [isProductDropdownOpen]); }, [isProductDropdownOpen]);
const getToday = () => { const handleSave = async () => {
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}`;
};
// const handleSaveForm = async () => {
// const selectedProducts = Array.isArray(form.productsMapped) ? form.productsMapped : [];
// if (!form.unitName || !form.description || !form.status) {
// return;
// }
// const unitData = {
// uom: form.unitName,
// uom_short_name: form.description,
// is_active: form.status === 'Active',
// };
// try {
// setLoading(true);
// if (modalMode === 'edit' && editingRow !== null) {
// const updatedUnitData = {
// ...unitData,
// updated_at: new Date().toISOString(),
// updated_by: currentUser?.id || null,
// };
// const updatedUnit = await updateUnit(units[editingRow].id, updatedUnitData);
// setUnits(prev =>
// prev.map((item, index) =>
// index === editingRow
// ? {
// ...item,
// ...updatedUnit,
// updated_at: updatedUnitData.updated_at,
// created_at: item.created_at,
// created_by: item.created_by,
// created_by_name: item.created_by_name,
// }
// : item
// )
// );
// } else {
// const newUnit = await createUnit({
// ...unitData,
// created_at: new Date().toISOString(),
// created_by: currentUser?.id || null,
// created_by_name: currentUser?.name || '',
// });
// setUnits(prev => [newUnit, ...prev]);
// }
// await fetchUnits();
// closeModal();
// } catch (error) {
// console.error('Error saving unit:', error);
// } finally {
// setLoading(false);
// }
// };
const handleSave = async () => {
const selectedProducts = Array.isArray(form.productsMapped) ? form.productsMapped : []; const selectedProducts = Array.isArray(form.productsMapped) ? form.productsMapped : [];
// Validation checks // Validation checks
if (!form.unitName) { if (!form.unitName) {
toast.error('Please enter Unit Name.'); setToastData({
message: 'Please enter Unit Name.',
type: 'error'
});
return; return;
} }
if (!form.description) { if (!form.description) {
toast.error('Please enter Description.'); setToastData({
message: 'Please enter Description.',
type: 'error'
});
return; return;
} }
if (!form.status) { if (!form.status) {
toast.error('Please select Status.'); setToastData({
message: 'Please select Status.',
type: 'error'
});
return; return;
} }
@ -314,7 +281,10 @@ const handleSave = async () => {
// Only check duplicates when adding (not editing) // Only check duplicates when adding (not editing)
if (modalMode !== 'edit' && nameExists) { if (modalMode !== 'edit' && nameExists) {
toast.error('Unit name already exists!'); setToastData({
message: 'Unit name already exists!',
type: 'error'
});
return; return;
} }
@ -335,59 +305,67 @@ const handleSave = async () => {
updated_by: currentUser?.id || null, updated_by: currentUser?.id || null,
}; };
const updatedUnit = await updateUnit(units[editingRow].id, updatedUnitData); await updateUnit(units[editingRow].id, updatedUnitData);
setUnits(prev => // Show success message for edit
prev.map((item, index) => setToastData({
index === editingRow message: 'Unit updated successfully!',
? { type: 'success'
...item, });
...updatedUnit,
updated_at: updatedUnitData.updated_at,
created_at: item.created_at,
created_by: item.created_by,
created_by_name: item.created_by_name,
productsMapped: selectedProducts,
}
: item
)
);
toast.success('Unit updated successfully!');
} else { } else {
const newUnit = await createUnit({ // Create new record
await createUnit({
...unitData, ...unitData,
created_at: new Date().toISOString(), created_at: new Date().toISOString(),
created_by: currentUser?.id || null, created_by: currentUser?.id || null,
created_by_name: currentUser?.name || '', created_by_name: currentUser?.name || '',
}); });
setUnits(prev => [ // Show success message for add
{ setToastData({
...newUnit, message: 'New unit added successfully!',
productsMapped: selectedProducts, type: 'success'
}, });
...prev,
]);
toast.success('New unit added successfully!');
} }
await fetchUnits(); await fetchUnits();
closeModal(); closeModal();
} catch (error) { } catch (error) {
console.error('Error saving unit:', error); console.error('Error saving unit:', error);
toast.error('Something went wrong while saving the unit.'); setToastData({
message: 'Something went wrong while saving the unit.',
type: 'error'
});
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const handleDeleteConfirm = async () => {
if (deletingRow === null) return;
try {
const unitToDelete = units[deletingRow];
await deleteUnit(unitToDelete.id);
setUnits(prev => prev.filter((_, index) => index !== deletingRow));
// Show success message for delete
setToastData({
message: 'Unit deleted successfully!',
type: 'success'
});
setDeletingRow(null);
} catch (error) {
console.error('Error deleting unit:', error);
setToastData({
message: 'Failed to delete unit. Please try again.',
type: 'error'
});
}
};
const closeDeleteConfirm = () => {
setDeletingRow(null);
};
const deletingUnit = React.useMemo(() => { const deletingUnit = React.useMemo(() => {
if (deletingRow === null || deletingRow < 0 || deletingRow >= units.length) { if (deletingRow === null || deletingRow < 0 || deletingRow >= units.length) {
@ -396,36 +374,21 @@ const handleSave = async () => {
return units[deletingRow]; return units[deletingRow];
}, [deletingRow, units]); }, [deletingRow, units]);
const closeDeleteConfirm = () => {
setDeletingRow(null);
};
const handleDeleteConfirm = async () => {
if (deletingRow === null) return;
try {
await deleteUnit(units[deletingRow].id);
setUnits(prev => prev.filter((_, index) => index !== deletingRow));
closeDeleteConfirm();
} catch (error) {
console.error('Error deleting unit:', error);
}
};
return ( return (
<div className="bg-white w-full rounded-lg shadow-sm ring-1 ring-[#E5E7EB]"> <div className="bg-white w-full rounded-lg shadow-sm ring-1 ring-[#E5E7EB]">
<div className="px-6 py-3 flex items-center justify-between border-b border-[#E5E7EB]"> <div className="px-6 py-3 flex items-center justify-between border-b border-[#E5E7EB]">
<h3 className="text-[16px] font-medium text-[#232528]">Unit Master</h3> <h3 className="text-[16px] font-medium text-[#232528]">Unit Master (
<span className="text-gray-600 text-sm font-medium pl-1 py-1 rounded-md">
{units.length} {units.length === 1 ? 'unit': ''}
</span>
)</h3>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
type="button" type="button"
onClick={() => { onClick={() => {
if (!units.length) return; if (!units.length) return;
// Prepare headers (excluding the last 'Actions' column)
const csvHeader = headers.slice(0, headers.length - 1).join(','); const csvHeader = headers.slice(0, headers.length - 1).join(',');
// Map each unit to a CSV row
const csvRows = units.map((item) => { const csvRows = units.map((item) => {
const createdDate = item.created_at const createdDate = item.created_at
? new Date(item.created_at).toLocaleDateString('en-GB') ? new Date(item.created_at).toLocaleDateString('en-GB')
@ -434,7 +397,6 @@ const handleSave = async () => {
item.updated_at && item.updated_at !== item.created_at item.updated_at && item.updated_at !== item.created_at
? new Date(item.updated_at).toLocaleDateString('en-GB') ? new Date(item.updated_at).toLocaleDateString('en-GB')
: '-'; : '-';
const status = item.is_active ? 'Active' : 'Inactive'; const status = item.is_active ? 'Active' : 'Inactive';
const mappedProducts = Array.isArray(item.productsMapped) const mappedProducts = Array.isArray(item.productsMapped)
? item.productsMapped.join('; ') ? item.productsMapped.join('; ')
@ -453,10 +415,7 @@ const handleSave = async () => {
.join(','); .join(',');
}); });
// Combine header and rows into one CSV content
const csvContent = csvHeader + '\n' + csvRows.join('\n'); const csvContent = csvHeader + '\n' + csvRows.join('\n');
// Create a downloadable CSV file
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const link = document.createElement('a'); const link = document.createElement('a');
@ -473,13 +432,10 @@ const handleSave = async () => {
: 'bg-[#F3F4F6] border-[#E5E7EB] text-[#9CA3AF] cursor-not-allowed' : 'bg-[#F3F4F6] border-[#E5E7EB] text-[#9CA3AF] cursor-not-allowed'
}`} }`}
disabled={!units.length} disabled={!units.length}
> >
<img src={downloadIconSrc} alt="Export" className="h-5 w-5" /> <img src={downloadIconSrc} alt="Export" className="h-5 w-5" />
<span className="font-medium">Export CSV</span> <span className="font-medium">Export CSV</span>
</button> </button>
<button <button
type="button" type="button"
@ -491,11 +447,12 @@ const handleSave = async () => {
</button> </button>
</div> </div>
</div> </div>
{loading ? ( {loading ? (
<div className="flex justify-center items-center h-64"> <div className="flex justify-center items-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-gray-900"></div> <div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-gray-900"></div>
</div> </div>
) : ( ) : (
<Table <Table
headers={headers} headers={headers}
rows={rows} rows={rows}
@ -532,7 +489,6 @@ const handleSave = async () => {
</div> </div>
); );
} }
return value; return value;
}} }}
pagination={{ pagination={{
@ -543,6 +499,8 @@ const handleSave = async () => {
}} }}
/> />
)} )}
{/* Add/Edit Modal */}
{modalMode && ( {modalMode && (
<div className="fixed inset-0 z-50"> <div className="fixed inset-0 z-50">
<div className="absolute inset-0 bg-black/40" onClick={closeModal} /> <div className="absolute inset-0 bg-black/40" onClick={closeModal} />
@ -582,7 +540,7 @@ const handleSave = async () => {
value={form.unitName} value={form.unitName}
onChange={handleFormChange('unitName')} onChange={handleFormChange('unitName')}
placeholder="Enter Unit Name" placeholder="Enter Unit Name"
/> />
<TextField <TextField
label="Description" label="Description"
value={form.description} value={form.description}
@ -637,7 +595,7 @@ const handleSave = async () => {
)} )}
</div> </div>
<SelectField <SelectField
label={ label={
<> <>
Status <span className="text-red-500">*</span> Status <span className="text-red-500">*</span>
@ -647,7 +605,7 @@ const handleSave = async () => {
onChange={handleFormChange('status')} onChange={handleFormChange('status')}
options={statusOptions} options={statusOptions}
placeholder="Select Status" placeholder="Select Status"
/> />
</div> </div>
<div className="mt-6 flex items-center justify-end gap-3"> <div className="mt-6 flex items-center justify-end gap-3">
@ -663,22 +621,21 @@ const handleSave = async () => {
className="h-10 px-4 rounded-[6px] bg-[#92722A] text-white text-sm font-medium disabled:opacity-50" className="h-10 px-4 rounded-[6px] bg-[#92722A] text-white text-sm font-medium disabled:opacity-50"
onClick={handleSave} onClick={handleSave}
disabled={loading} disabled={loading}
> >
{loading ? ( {loading ? (
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">
<div className="animate-spin rounded-full h-5 w-5 border-t-2 border-b-2 border-white mr-2"></div> <div className="animate-spin rounded-full h-5 w-5 border-t-2 border-b-2 border-white mr-2"></div>
{modalMode === 'edit' ? 'Updating...' : 'Saving...'} {modalMode === 'edit' ? 'Updating...' : 'Saving...'}
</div> </div>
) : modalMode === 'edit' ? 'Update' : 'Save'} ) : modalMode === 'edit' ? 'Update' : 'Save'}
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
)} )}
{/* Delete Confirmation Modal */}
{deletingUnit && ( {deletingUnit && (
<div className="fixed inset-0 z-50"> <div className="fixed inset-0 z-50">
<div className="absolute inset-0 bg-black/40" onClick={closeDeleteConfirm} /> <div className="absolute inset-0 bg-black/40" onClick={closeDeleteConfirm} />
@ -734,6 +691,20 @@ const handleSave = async () => {
</div> </div>
</div> </div>
)} )}
{/* Custom Toast Notification */}
{/* Custom Toast Notification - CENTERED */}
{toastData && (
<div className="fixed inset-0 z-[9999] pointer-events-none flex items-start justify-center pt-20">
<div className="pointer-events-auto">
<CustomToast
message={toastData.message}
type={toastData.type}
onClose={() => setToastData(null)}
/>
</div>
</div>
)}
</div> </div>
); );
}; };

View File

@ -314,6 +314,7 @@ const formatExportDateTime = (value) => {
{ value: 'approved', label: 'Approved' }, { value: 'approved', label: 'Approved' },
{ value: 'rejected', label: 'Rejected' }, { value: 'rejected', label: 'Rejected' },
{ value: 'submitted', label: 'Submitted' }, { value: 'submitted', label: 'Submitted' },
{ value: 'resubmitted', label: 'ReSubmitted' },
]; ];
const filteredRows = useMemo(() => { const filteredRows = useMemo(() => {

View File

@ -30,7 +30,7 @@ const Overview = () => {
const [submissions, setSubmissions] = useState([]); const [submissions, setSubmissions] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const establishmentId = 41; // You might want to get this from your auth context or props const establishmentId = sessionStorage.getItem('establishment_id');
const [pagination, setPagination] = useState({ const [pagination, setPagination] = useState({
currentPage: 1, currentPage: 1,
pageSize: 10, pageSize: 10,
@ -45,6 +45,7 @@ const Overview = () => {
setLoading(true); setLoading(true);
// Use getSubmissionHistoryByEstablishment instead of getSubmissions // Use getSubmissionHistoryByEstablishment instead of getSubmissions
const response = await getSubmissionHistoryByEstablishment(establishmentId); const response = await getSubmissionHistoryByEstablishment(establishmentId);
console.log("response",response)
setSubmissions(response.data || []); setSubmissions(response.data || []);
} catch (err) { } catch (err) {
setError('Failed to load submission history'); setError('Failed to load submission history');

View File

@ -0,0 +1,87 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { fetchSubmissionDetail } from '@/services/submissions/submissionService';
import { DetailedOverview } from '@/components/overview/DetailedOverview';
import HeaderBar from '@/components/layout/HeaderBar';
const SubmissionDetails = () => {
const { submissionId } = useParams();
const navigate = useNavigate();
const [submission, setSubmission] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadSubmission = async () => {
try {
const response = await fetchSubmissionDetail(submissionId);
const data = response.data;
console.log("data",data)
setSubmission({
id: submissionId,
survey_name: 'Industrial Production Survey',
year: data.year || '—',
quarter: data.quarter || '—',
status: data.status || '—',
submission_on: data.created_at || new Date().toISOString(),
products: data.products?.length || 0,
...data
});
} catch (error) {
console.error('Error loading submission:', error);
} finally {
setLoading(false);
}
};
if (submissionId) {
loadSubmission();
}
}, [submissionId]);
const handleBack = () => {
navigate('/dasboard');
};
if (loading) {
return (
<div className="min-h-screen bg-[#F8FAFC]">
<HeaderBar />
<main className="max-w-[1280px] mx-auto px-4 py-6">
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-[#92722A]"></div>
</div>
</main>
</div>
);
}
if (!submission) {
return (
<div className="min-h-screen bg-[#F8FAFC]">
<HeaderBar />
<main className="max-w-[1280px] mx-auto px-4 py-6">
<div className="text-center py-12">
<h2 className="text-xl font-medium text-gray-700">Submission not found</h2>
<button
onClick={handleBack}
className="mt-4 px-4 py-2 bg-[#92722A] text-white rounded-md hover:bg-[#7b5f22] transition-colors"
>
Back to History
</button>
</div>
</main>
</div>
);
}
return (
<div className="min-h-screen bg-[#F8FAFC]">
<HeaderBar />
<main className="max-w-[1280px] mx-auto px-4 py-6">
<DetailedOverview submission={submission} onBack={handleBack} />
</main>
</div>
);
};
export default SubmissionDetails;

View File

@ -51,6 +51,56 @@ export const getAdminUserById = async (id) => {
// throw error; // throw error;
// } // }
// }; // };
export const changeAdminUserPassword = async (id, passwordData) => {
try {
if (!id) throw new Error("Invalid user ID");
// Debug: see what fields the API expects
console.log("🔍 Debug: Making test request to discover required fields");
const testData = { test_field: "test_value" };
try {
await putRequest(`/admin/${id}/change-password`, testData);
} catch (testError) {
console.log(
"🔍 Debug: Test request failed with:",
testError.response?.data || testError.message
);
}
// Common Laravel-style field names
const requestData = {
password: passwordData.newPassword,
password_confirmation: passwordData.confirmPassword,
new_password: passwordData.newPassword,
new_password_confirmation: passwordData.confirmPassword,
};
console.log("🔍 Debug: Final request data:", requestData);
const response = await putRequest(`/admin/${id}/change-password`, requestData);
return response.data;
} catch (error) {
console.error("Error changing admin user password:", error);
if (error.response) {
console.error("🔍 Debug: Full error response:", {
status: error.response.status,
data: error.response.data,
headers: error.response.headers,
});
if (error.response.data.errors) {
console.error("🔍 Debug: Validation errors:", error.response.data.errors);
}
}
throw error;
}
};
export const deleteAdminUser = async (id) => { export const deleteAdminUser = async (id) => {
try { try {
@ -78,4 +128,8 @@ export const deleteAdminUser = async (id) => {
console.error("Error deleting admin user:", error); console.error("Error deleting admin user:", error);
throw error; throw error;
} }
}; };