Bug fixed
This commit is contained in:
parent
9986f7c16b
commit
6dfa71a5b4
@ -12,25 +12,36 @@ 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 params = new URLSearchParams();
|
||||||
const response = await apiClient.get('/admin_dashboard');
|
|
||||||
const result = response?.data;
|
if (quarter && quarter !== 'All') {
|
||||||
if (result?.status === 'success') {
|
params.append('quarter', quarter);
|
||||||
setData(result.recent_submissions || []);
|
|
||||||
} else {
|
|
||||||
console.error('Error: Invalid response', result);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching recent submissions:', error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
fetchDashboardData();
|
if (year && year !== 'All') {
|
||||||
}, []);
|
params.append('year', year);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await apiClient.get(`/admin_dashboard?${params.toString()}`);
|
||||||
|
const result = response?.data;
|
||||||
|
if (result?.status === 'success') {
|
||||||
|
setData(result.recent_submissions || []);
|
||||||
|
} else {
|
||||||
|
console.error('Error: Invalid response', result);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching recent submissions:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchDashboardData(selectedQuarter, selectedYear);
|
||||||
|
}, [selectedQuarter, selectedYear]);
|
||||||
|
|
||||||
const filtered = useMemo(() => {
|
const filtered = useMemo(() => {
|
||||||
let list = [...data];
|
let list = [...data];
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleStartSurvey = (survey) => {
|
const handleStartSurvey = (survey) => {
|
||||||
|
// 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 { previous, current, next } = getQuarterData();
|
const nextDeadline = dashboardData?.next_deadline;
|
||||||
|
|
||||||
const surveyData = data || {};
|
|
||||||
const nextDeadline = surveyData?.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: '15–20 minutes',
|
estTime: '15–20 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: '15–20 minutes',
|
|
||||||
status: 'Pending',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: `${next.quarter} ${next.year} Survey Ready`,
|
|
||||||
subtitle: 'Complete your quarterly Industrial Production Index (IPI) data submission',
|
|
||||||
dueDate: formattedDueDate,
|
|
||||||
relativeDue,
|
|
||||||
estTime: '15–20 minutes',
|
|
||||||
status: 'Pending',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const heroTitle = loading
|
const heroTitle = loading
|
||||||
? 'Loading survey status…'
|
? 'Loading survey status…'
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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}
|
||||||
|
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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,40 +21,62 @@ 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 params = new URLSearchParams();
|
||||||
const response = await apiClient.get('/admin_dashboard');
|
|
||||||
|
if (quarter && quarter !== 'All') {
|
||||||
const data = response?.data?.summary || {};
|
params.append('quarter', quarter);
|
||||||
const selectedQuarterFromApi = response?.data?.selected_quarter || 'All';
|
|
||||||
const selectedYearFromApi = response?.data?.selected_year || 'All';
|
|
||||||
|
|
||||||
setSummary({
|
|
||||||
total_establishments: data.total_establishments || 0,
|
|
||||||
submitted: data.submitted || 0,
|
|
||||||
approved: data.approved || 0,
|
|
||||||
rejected: data.rejected || 0,
|
|
||||||
not_started: data.not_started || 0,
|
|
||||||
pending: data.pending || 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
setSelectedQuarter(selectedQuarterFromApi);
|
|
||||||
setSelectedYear(selectedYearFromApi);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching dashboard summary:', error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
if (year && year !== 'All') {
|
||||||
|
params.append('year', year);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await apiClient.get(`/admin_dashboard?${params.toString()}`);
|
||||||
|
|
||||||
fetchDashboardData();
|
const data = response?.data?.summary || {};
|
||||||
}, []);
|
const selectedQuarterFromApi = response?.data?.selected_quarter || quarter || 'All';
|
||||||
|
const selectedYearFromApi = response?.data?.selected_year || year || 'All';
|
||||||
|
const quarterlyWindowsData = response?.data?.quarterly_windows || {};
|
||||||
|
|
||||||
|
setSummary({
|
||||||
|
total_establishments: data.total_establishments || 0,
|
||||||
|
submitted: data.submitted || 0,
|
||||||
|
approved: data.approved || 0,
|
||||||
|
rejected: data.rejected || 0,
|
||||||
|
not_started: data.not_started || 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);
|
||||||
|
setSelectedYear(selectedYearFromApi);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching dashboard summary:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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>
|
||||||
|
|||||||
@ -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,
|
||||||
@ -209,52 +213,50 @@ const [confirmPassword, setConfirmPassword] = useState('');
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateUser = async () => {
|
const handleUpdateUser = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
const success =
|
const success =
|
||||||
res?.status === "success" ||
|
res?.status === "success" ||
|
||||||
res?.code === 200 ||
|
res?.code === 200 ||
|
||||||
res?.data?.status === "success" ||
|
res?.data?.status === "success" ||
|
||||||
(!res.error && res);
|
(!res.error && res);
|
||||||
|
|
||||||
if (!success) throw new Error(res?.message || "Failed to update user");
|
if (!success) throw new Error(res?.message || "Failed to update user");
|
||||||
|
|
||||||
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
|
? {
|
||||||
? {
|
...u,
|
||||||
...u,
|
name: formData.name,
|
||||||
name: formData.name,
|
email: formData.email,
|
||||||
email: formData.email,
|
status: formData.status,
|
||||||
status: formData.status,
|
is_active: formData.status === "Active",
|
||||||
is_active: formData.status === "Active",
|
}
|
||||||
}
|
: u
|
||||||
: u
|
)
|
||||||
)
|
);
|
||||||
);
|
|
||||||
|
|
||||||
setCurrentUser(null);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error updating user:", error);
|
|
||||||
showToast(error?.response?.data?.message || "Failed to update user", "error");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
setCurrentUser(null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating user:", error);
|
||||||
|
showToast(error?.response?.data?.message || "Failed to update user", "error");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const validateForm = () => {
|
const validateForm = () => {
|
||||||
const newErrors = {};
|
const newErrors = {};
|
||||||
@ -267,87 +269,182 @@ const [confirmPassword, setConfirmPassword] = useState('');
|
|||||||
setSelectedUser(user);
|
setSelectedUser(user);
|
||||||
setShowDeleteModal(true);
|
setShowDeleteModal(true);
|
||||||
};
|
};
|
||||||
const handleConfirmDelete = async () => {
|
|
||||||
try {
|
|
||||||
if (!selectedUser) return;
|
|
||||||
setDeletingRow(selectedUser.id);
|
|
||||||
await deleteAdminUser(selectedUser.id); // 🔥 API call
|
|
||||||
|
|
||||||
showToast("User deleted successfully!", "success"); // ✅ use showToast()
|
const handleConfirmDelete = async () => {
|
||||||
|
try {
|
||||||
|
if (!selectedUser) return;
|
||||||
|
setDeletingRow(selectedUser.id);
|
||||||
|
await deleteAdminUser(selectedUser.id);
|
||||||
|
|
||||||
setUsers((prev) => prev.filter((u) => u.id !== selectedUser.id));
|
showToast("User deleted successfully!", "success");
|
||||||
} catch (error) {
|
|
||||||
showToast("Failed to delete user!", "error"); // ✅ same toast handler for error
|
|
||||||
} finally {
|
|
||||||
setDeletingRow(null);
|
|
||||||
setShowDeleteModal(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEdit = async (user) => {
|
setUsers((prev) => prev.filter((u) => u.id !== selectedUser.id));
|
||||||
if (!user || !user.id) {
|
} catch (error) {
|
||||||
showToast('Invalid user selected', 'error');
|
showToast("Failed to delete user!", "error");
|
||||||
return;
|
} finally {
|
||||||
}
|
setDeletingRow(null);
|
||||||
|
setShowDeleteModal(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
setEditingRow(user); // store which row is being edited
|
const handleEdit = async (user) => {
|
||||||
setLoading(true);
|
if (!user || !user.id) {
|
||||||
|
showToast('Invalid user selected', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
setEditingRow(user);
|
||||||
const res = await getAdminUserById(user.id);
|
setLoading(true);
|
||||||
const payload = res?.data ?? res;
|
|
||||||
if (!payload) throw new Error('No user data returned from API');
|
|
||||||
|
|
||||||
const name = payload.name ?? payload.fullName ?? user.name ?? '';
|
try {
|
||||||
const email = payload.email ?? user.email ?? '';
|
const res = await getAdminUserById(user.id);
|
||||||
const is_active =
|
const payload = res?.data ?? res;
|
||||||
payload.is_active !== undefined ? !!payload.is_active : !!user.is_active;
|
if (!payload) throw new Error('No user data returned from API');
|
||||||
|
|
||||||
setCurrentUser({
|
const name = payload.name ?? payload.fullName ?? user.name ?? '';
|
||||||
...payload,
|
const email = payload.email ?? user.email ?? '';
|
||||||
id: user.id,
|
const is_active =
|
||||||
name,
|
payload.is_active !== undefined ? !!payload.is_active : !!user.is_active;
|
||||||
email,
|
|
||||||
is_active,
|
|
||||||
});
|
|
||||||
|
|
||||||
setFormData({
|
setCurrentUser({
|
||||||
name,
|
...payload,
|
||||||
email,
|
id: user.id,
|
||||||
status: is_active ? 'Active' : 'Inactive',
|
name,
|
||||||
});
|
email,
|
||||||
|
is_active,
|
||||||
|
});
|
||||||
|
|
||||||
setErrors({});
|
setFormData({
|
||||||
setIsEditModalOpen(true); // ✅ opens your modal
|
name,
|
||||||
} catch (error) {
|
email,
|
||||||
console.error('Error fetching user data:', error);
|
status: is_active ? 'Active' : 'Inactive',
|
||||||
showToast(error?.response?.data?.message || 'Failed to load user data', 'error');
|
});
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
setErrors({});
|
||||||
|
setIsEditModalOpen(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching user data:', error);
|
||||||
|
showToast(error?.response?.data?.message || 'Failed to load user data', 'error');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleOpenResetModal = (user) => {
|
const handleOpenResetModal = (user) => {
|
||||||
setResetUser(user);
|
setResetUser(user);
|
||||||
setNewPassword('');
|
setNewPassword('');
|
||||||
setConfirmPassword('');
|
setConfirmPassword('');
|
||||||
setIsResetModalOpen(true);
|
setResetErrors({});
|
||||||
};
|
setIsResetModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
const handleCloseResetModal = () => {
|
const handleCloseResetModal = () => {
|
||||||
setResetUser(null);
|
setResetUser(null);
|
||||||
setIsResetModalOpen(false);
|
setNewPassword('');
|
||||||
};
|
setConfirmPassword('');
|
||||||
|
setResetErrors({});
|
||||||
|
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();
|
||||||
@ -424,47 +521,42 @@ const handleCloseResetModal = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 sm:justify-end">
|
<div className="flex items-center gap-2 sm:justify-end">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
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');
|
||||||
link.setAttribute('download', 'admin-users.csv'); // 👈 No date in filename
|
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);
|
}}
|
||||||
}}
|
className={`h-9 px-3 rounded-md border text-sm inline-flex items-center gap-2 ${
|
||||||
className={`h-9 px-3 rounded-md border text-sm inline-flex items-center gap-2 ${
|
users.length
|
||||||
users.length
|
? 'bg-[#F7F7F7] border-[#C3C6CB] text-[#232528]'
|
||||||
? 'bg-[#F7F7F7] border-[#C3C6CB] text-[#232528]'
|
: '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)}
|
||||||
@ -508,130 +600,46 @@ const handleCloseResetModal = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<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="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
|
|
||||||
type="button"
|
|
||||||
className="h-8 w-8 grid place-items-center rounded hover:bg-gray-50 cursor-pointer"
|
|
||||||
title="Delete"
|
|
||||||
onClick={() => handleDelete(rowIndex)}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={deletingRow === rowIndex ? trashActiveSrc : trashInactiveSrc}
|
|
||||||
alt="Delete"
|
|
||||||
className="h-4 w-4"
|
|
||||||
/>
|
|
||||||
</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"
|
||||||
isActive ? '' : 'opacity-60'
|
title="Delete"
|
||||||
}`}
|
onClick={() => handleDelete(rowIndex)}
|
||||||
title={isActive ? 'Reset password' : 'Reset disabled for inactive user'}
|
>
|
||||||
disabled={!isActive}
|
<img
|
||||||
onClick={() => handleOpenResetModal(user)}
|
src={deletingRow === rowIndex ? trashActiveSrc : trashInactiveSrc}
|
||||||
>
|
alt="Delete"
|
||||||
<img
|
className="h-4 w-4"
|
||||||
src={isActive ? resetPasswordIconSrc : resetPasswordInactiveIconSrc}
|
/>
|
||||||
alt="Reset password"
|
</button>
|
||||||
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>
|
|
||||||
|
|
||||||
{/* 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}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`h-8 w-8 grid place-items-center rounded hover:bg-gray-50 cursor-pointer ${
|
||||||
|
isActive ? '' : 'opacity-60'
|
||||||
|
}`}
|
||||||
|
title={isActive ? 'Reset password' : 'Reset disabled for inactive user'}
|
||||||
|
disabled={!isActive}
|
||||||
|
onClick={() => handleOpenResetModal(user)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={isActive ? resetPasswordIconSrc : resetPasswordInactiveIconSrc}
|
||||||
|
alt="Reset password"
|
||||||
|
className="h-4 w-4"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
</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">
|
||||||
@ -677,4 +718,4 @@ const handleCloseResetModal = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AdminUsers;
|
export default AdminUsers;
|
||||||
@ -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;
|
||||||
File diff suppressed because it is too large
Load Diff
@ -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,93 +82,80 @@ 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);
|
||||||
const response = await getUnits();
|
const response = await getUnits();
|
||||||
const unitsData = response.data || [];
|
const unitsData = response.data || [];
|
||||||
|
|
||||||
// ✅ Sort by latest created_at date (newest first)
|
// ✅ Sort by latest created_at date (newest first)
|
||||||
const sortedUnits = [...unitsData].sort(
|
const sortedUnits = [...unitsData].sort(
|
||||||
(a, b) => new Date(b.created_at) - new Date(a.created_at)
|
(a, b) => new Date(b.created_at) - new Date(a.created_at)
|
||||||
);
|
);
|
||||||
|
|
||||||
setUnits(sortedUnits);
|
setUnits(sortedUnits);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching units:', error);
|
console.error('Error fetching units:', error);
|
||||||
} 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 = [
|
|
||||||
'Unit Name',
|
|
||||||
'Description',
|
|
||||||
'Mapped Products',
|
|
||||||
'Created By',
|
|
||||||
'Created On',
|
|
||||||
'Last Updated', // ✅ New column
|
|
||||||
'Status',
|
|
||||||
'Actions',
|
|
||||||
];
|
|
||||||
|
|
||||||
|
const headers = [
|
||||||
|
'Unit Name',
|
||||||
|
'Description',
|
||||||
|
'Mapped Products',
|
||||||
|
'Created By',
|
||||||
|
'Created On',
|
||||||
|
'Last Updated',
|
||||||
|
'Status',
|
||||||
|
'Actions',
|
||||||
|
];
|
||||||
|
|
||||||
const rows = useMemo(() => {
|
const rows = useMemo(() => {
|
||||||
return units.map((item) => {
|
return units.map((item) => {
|
||||||
const createdDate = item.created_at
|
const createdDate = item.created_at
|
||||||
? new Date(item.created_at).toLocaleDateString('en-GB')
|
? new Date(item.created_at).toLocaleDateString('en-GB')
|
||||||
: '-';
|
|
||||||
const updatedDate =
|
|
||||||
item.updated_at && item.updated_at !== item.created_at
|
|
||||||
? new Date(item.updated_at).toLocaleDateString('en-GB')
|
|
||||||
: '-';
|
: '-';
|
||||||
const currentUserName = currentUser?.name || '';
|
const updatedDate =
|
||||||
|
item.updated_at && item.updated_at !== item.created_at
|
||||||
return [
|
? new Date(item.updated_at).toLocaleDateString('en-GB')
|
||||||
item.uom || item.unitName, // Unit Name
|
: '-';
|
||||||
item.uom_short_name || item.description, // Description
|
const currentUserName = currentUser?.name || '';
|
||||||
item.mapped_products_count !== undefined
|
|
||||||
? item.mapped_products_count
|
|
||||||
: (Array.isArray(item.productsMapped) ? item.productsMapped.length : 0),
|
|
||||||
item.created_by_name || currentUserName || '-', // Created By
|
|
||||||
createdDate, // Created On
|
|
||||||
updatedDate, // ✅ Last Update (moved here)
|
|
||||||
<StatusBadge
|
|
||||||
status={item.is_active ? 'Active' : 'Inactive'}
|
|
||||||
tone={item.is_active ? 'green' : 'gray'}
|
|
||||||
/>, // Status
|
|
||||||
'actions', // Actions
|
|
||||||
];
|
|
||||||
});
|
|
||||||
}, [units, currentUser]);
|
|
||||||
|
|
||||||
|
return [
|
||||||
|
item.uom || item.unitName,
|
||||||
|
item.uom_short_name || item.description,
|
||||||
|
item.mapped_products_count !== undefined
|
||||||
|
? item.mapped_products_count
|
||||||
|
: (Array.isArray(item.productsMapped) ? item.productsMapped.length : 0),
|
||||||
|
item.created_by_name || currentUserName || '-',
|
||||||
|
createdDate,
|
||||||
|
updatedDate,
|
||||||
|
<StatusBadge
|
||||||
|
status={item.is_active ? 'Active' : 'Inactive'}
|
||||||
|
tone={item.is_active ? 'green' : 'gray'}
|
||||||
|
/>,
|
||||||
|
'actions',
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}, [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,170 +248,124 @@ const UnitMaster = () => {
|
|||||||
};
|
};
|
||||||
}, [isProductDropdownOpen]);
|
}, [isProductDropdownOpen]);
|
||||||
|
|
||||||
const getToday = () => {
|
const handleSave = async () => {
|
||||||
const now = new Date();
|
const selectedProducts = Array.isArray(form.productsMapped) ? form.productsMapped : [];
|
||||||
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 () => {
|
// ✅ Validation checks
|
||||||
// const selectedProducts = Array.isArray(form.productsMapped) ? form.productsMapped : [];
|
if (!form.unitName) {
|
||||||
// if (!form.unitName || !form.description || !form.status) {
|
setToastData({
|
||||||
// return;
|
message: 'Please enter Unit Name.',
|
||||||
// }
|
type: 'error'
|
||||||
|
|
||||||
// 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 : [];
|
|
||||||
|
|
||||||
// ✅ Validation checks
|
|
||||||
if (!form.unitName) {
|
|
||||||
toast.error('Please enter Unit Name.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!form.description) {
|
|
||||||
toast.error('Please enter Description.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!form.status) {
|
|
||||||
toast.error('Please select Status.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ Check for duplicate unit name
|
|
||||||
const nameExists = units.some(
|
|
||||||
unit => unit.uom?.toLowerCase().trim() === form.unitName.toLowerCase().trim()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Only check duplicates when adding (not editing)
|
|
||||||
if (modalMode !== 'edit' && nameExists) {
|
|
||||||
toast.error('Unit name already exists!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const unitData = {
|
|
||||||
uom: form.unitName,
|
|
||||||
uom_short_name: form.description,
|
|
||||||
is_active: form.status === 'Active',
|
|
||||||
productsMapped: selectedProducts,
|
|
||||||
};
|
|
||||||
|
|
||||||
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,
|
|
||||||
productsMapped: selectedProducts,
|
|
||||||
}
|
|
||||||
: item
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
toast.success('Unit updated successfully!');
|
|
||||||
} else {
|
|
||||||
const newUnit = await createUnit({
|
|
||||||
...unitData,
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
created_by: currentUser?.id || null,
|
|
||||||
created_by_name: currentUser?.name || '',
|
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
setUnits(prev => [
|
}
|
||||||
{
|
if (!form.description) {
|
||||||
...newUnit,
|
setToastData({
|
||||||
productsMapped: selectedProducts,
|
message: 'Please enter Description.',
|
||||||
},
|
type: 'error'
|
||||||
...prev,
|
});
|
||||||
]);
|
return;
|
||||||
|
}
|
||||||
toast.success('New unit added successfully!');
|
if (!form.status) {
|
||||||
|
setToastData({
|
||||||
|
message: 'Please select Status.',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await fetchUnits();
|
// ✅ Check for duplicate unit name
|
||||||
closeModal();
|
const nameExists = units.some(
|
||||||
} catch (error) {
|
unit => unit.uom?.toLowerCase().trim() === form.unitName.toLowerCase().trim()
|
||||||
console.error('Error saving unit:', error);
|
);
|
||||||
toast.error('Something went wrong while saving the unit.');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// Only check duplicates when adding (not editing)
|
||||||
|
if (modalMode !== 'edit' && nameExists) {
|
||||||
|
setToastData({
|
||||||
|
message: 'Unit name already exists!',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unitData = {
|
||||||
|
uom: form.unitName,
|
||||||
|
uom_short_name: form.description,
|
||||||
|
is_active: form.status === 'Active',
|
||||||
|
productsMapped: selectedProducts,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
if (modalMode === 'edit' && editingRow !== null) {
|
||||||
|
const updatedUnitData = {
|
||||||
|
...unitData,
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
updated_by: currentUser?.id || null,
|
||||||
|
};
|
||||||
|
|
||||||
|
await updateUnit(units[editingRow].id, updatedUnitData);
|
||||||
|
|
||||||
|
// ✅ Show success message for edit
|
||||||
|
setToastData({
|
||||||
|
message: 'Unit updated successfully!',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// ✅ Create new record
|
||||||
|
await createUnit({
|
||||||
|
...unitData,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
created_by: currentUser?.id || null,
|
||||||
|
created_by_name: currentUser?.name || '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ Show success message for add
|
||||||
|
setToastData({
|
||||||
|
message: 'New unit added successfully!',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await fetchUnits();
|
||||||
|
closeModal();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving unit:', error);
|
||||||
|
setToastData({
|
||||||
|
message: 'Something went wrong while saving the unit.',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
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,90 +374,68 @@ 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(',');
|
const csvRows = units.map((item) => {
|
||||||
|
const createdDate = item.created_at
|
||||||
|
? new Date(item.created_at).toLocaleDateString('en-GB')
|
||||||
|
: '-';
|
||||||
|
const updatedDate =
|
||||||
|
item.updated_at && item.updated_at !== item.created_at
|
||||||
|
? new Date(item.updated_at).toLocaleDateString('en-GB')
|
||||||
|
: '-';
|
||||||
|
const status = item.is_active ? 'Active' : 'Inactive';
|
||||||
|
const mappedProducts = Array.isArray(item.productsMapped)
|
||||||
|
? item.productsMapped.join('; ')
|
||||||
|
: '0';
|
||||||
|
|
||||||
// ✅ Map each unit to a CSV row
|
return [
|
||||||
const csvRows = units.map((item) => {
|
item.uom || item.unitName || '',
|
||||||
const createdDate = item.created_at
|
item.uom_short_name || item.description || '',
|
||||||
? new Date(item.created_at).toLocaleDateString('en-GB')
|
mappedProducts,
|
||||||
: '-';
|
item.created_by_name || currentUser?.name || '-',
|
||||||
const updatedDate =
|
createdDate,
|
||||||
item.updated_at && item.updated_at !== item.created_at
|
updatedDate,
|
||||||
? new Date(item.updated_at).toLocaleDateString('en-GB')
|
status,
|
||||||
: '-';
|
]
|
||||||
|
.map((value) => `"${String(value ?? '').replace(/"/g, '""')}"`)
|
||||||
const status = item.is_active ? 'Active' : 'Inactive';
|
.join(',');
|
||||||
const mappedProducts = Array.isArray(item.productsMapped)
|
});
|
||||||
? item.productsMapped.join('; ')
|
|
||||||
: '0';
|
|
||||||
|
|
||||||
return [
|
|
||||||
item.uom || item.unitName || '',
|
|
||||||
item.uom_short_name || item.description || '',
|
|
||||||
mappedProducts,
|
|
||||||
item.created_by_name || currentUser?.name || '-',
|
|
||||||
createdDate,
|
|
||||||
updatedDate,
|
|
||||||
status,
|
|
||||||
]
|
|
||||||
.map((value) => `"${String(value ?? '').replace(/"/g, '""')}"`)
|
|
||||||
.join(',');
|
|
||||||
});
|
|
||||||
|
|
||||||
// ✅ Combine header and rows into one CSV content
|
|
||||||
const csvContent = csvHeader + '\n' + csvRows.join('\n');
|
|
||||||
|
|
||||||
// ✅ Create a downloadable CSV file
|
|
||||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.setAttribute('download', 'unit-master.csv');
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}}
|
|
||||||
className={`h-10 px-4 rounded-[6px] border text-sm inline-flex items-center gap-2 ${
|
|
||||||
units.length
|
|
||||||
? 'bg-[#F7F7F7] border-[#C3C6CB] text-[#232528]'
|
|
||||||
: 'bg-[#F3F4F6] border-[#E5E7EB] text-[#9CA3AF] cursor-not-allowed'
|
|
||||||
}`}
|
|
||||||
disabled={!units.length}
|
|
||||||
>
|
|
||||||
<img src={downloadIconSrc} alt="Export" className="h-5 w-5" />
|
|
||||||
<span className="font-medium">Export CSV</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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', 'unit-master.csv');
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}}
|
||||||
|
className={`h-10 px-4 rounded-[6px] border text-sm inline-flex items-center gap-2 ${
|
||||||
|
units.length
|
||||||
|
? 'bg-[#F7F7F7] border-[#C3C6CB] text-[#232528]'
|
||||||
|
: 'bg-[#F3F4F6] border-[#E5E7EB] text-[#9CA3AF] cursor-not-allowed'
|
||||||
|
}`}
|
||||||
|
disabled={!units.length}
|
||||||
|
>
|
||||||
|
<img src={downloadIconSrc} alt="Export" className="h-5 w-5" />
|
||||||
|
<span className="font-medium">Export CSV</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -491,58 +447,60 @@ const handleSave = async () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{loading ? (
|
|
||||||
<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>
|
|
||||||
) : (
|
|
||||||
<Table
|
|
||||||
headers={headers}
|
|
||||||
rows={rows}
|
|
||||||
renderCell={(value, rowIndex, colIndex) => {
|
|
||||||
if (colIndex === headers.length - 1) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="h-8 w-8 grid place-items-center rounded hover:bg-gray-50 cursor-pointer"
|
|
||||||
title="Edit"
|
|
||||||
aria-label="Edit unit"
|
|
||||||
onClick={() => handleEdit(rowIndex)}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={editingRow === rowIndex ? pencilActiveSrc : pencilInactiveSrc}
|
|
||||||
alt="Edit"
|
|
||||||
className="h-4 w-4"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="h-8 w-8 grid place-items-center rounded hover:bg-gray-50 cursor-pointer"
|
|
||||||
title="Delete"
|
|
||||||
aria-label="Delete unit"
|
|
||||||
onClick={() => handleDelete(rowIndex)}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={deletingRow === rowIndex ? trashActiveSrc : trashInactiveSrc}
|
|
||||||
alt="Delete"
|
|
||||||
className="h-4 w-4"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
{loading ? (
|
||||||
}}
|
<div className="flex justify-center items-center h-64">
|
||||||
pagination={{
|
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-gray-900"></div>
|
||||||
currentPage,
|
</div>
|
||||||
onPageChange: setCurrentPage,
|
) : (
|
||||||
pageSize,
|
<Table
|
||||||
totalItems: filteredRows.length,
|
headers={headers}
|
||||||
}}
|
rows={rows}
|
||||||
/>
|
renderCell={(value, rowIndex, colIndex) => {
|
||||||
)}
|
if (colIndex === headers.length - 1) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="h-8 w-8 grid place-items-center rounded hover:bg-gray-50 cursor-pointer"
|
||||||
|
title="Edit"
|
||||||
|
aria-label="Edit unit"
|
||||||
|
onClick={() => handleEdit(rowIndex)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={editingRow === rowIndex ? pencilActiveSrc : pencilInactiveSrc}
|
||||||
|
alt="Edit"
|
||||||
|
className="h-4 w-4"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="h-8 w-8 grid place-items-center rounded hover:bg-gray-50 cursor-pointer"
|
||||||
|
title="Delete"
|
||||||
|
aria-label="Delete unit"
|
||||||
|
onClick={() => handleDelete(rowIndex)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={deletingRow === rowIndex ? trashActiveSrc : trashInactiveSrc}
|
||||||
|
alt="Delete"
|
||||||
|
className="h-4 w-4"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}}
|
||||||
|
pagination={{
|
||||||
|
currentPage,
|
||||||
|
onPageChange: setCurrentPage,
|
||||||
|
pageSize,
|
||||||
|
totalItems: filteredRows.length,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 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} />
|
||||||
@ -573,16 +531,16 @@ const handleSave = async () => {
|
|||||||
|
|
||||||
<div className="px-6 py-5">
|
<div className="px-6 py-5">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<TextField
|
<TextField
|
||||||
label={
|
label={
|
||||||
<>
|
<>
|
||||||
Unit Name <span className="text-red-500">*</span>
|
Unit Name <span className="text-red-500">*</span>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
value={form.unitName}
|
value={form.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,17 +595,17 @@ const handleSave = async () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SelectField
|
<SelectField
|
||||||
label={
|
label={
|
||||||
<>
|
<>
|
||||||
Status <span className="text-red-500">*</span>
|
Status <span className="text-red-500">*</span>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
value={form.status}
|
value={form.status}
|
||||||
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">
|
||||||
@ -658,27 +616,26 @@ const handleSave = async () => {
|
|||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="h-10 px-4 rounded-[6px] bg-[#92722A] text-white text-sm font-medium disabled:opacity-50"
|
className="h-10 px-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,8 +691,22 @@ 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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default UnitMaster;
|
export default UnitMaster;
|
||||||
@ -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(() => {
|
||||||
|
|||||||
@ -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');
|
||||||
|
|||||||
@ -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;
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
Loading…
Reference in New Issue
Block a user