bug fixed in admin dashboard

This commit is contained in:
Malini 2025-11-07 19:02:38 +05:30
parent d0d8bd13f3
commit 9986f7c16b
13 changed files with 458 additions and 174 deletions

View File

@ -84,10 +84,16 @@ const SubmissionTable = ({ selectedQuarter, selectedYear }) => {
Rejected
</span>
);
} else if (normalized === 'pending') {
} else if (normalized === 'submitted') {
return (
<span className="px-3 py-1 rounded-md text-blue-600 bg-blue-50 font-medium text-sm whitespace-nowrap">
Pending
Submitted
</span>
);
} else if (normalized === 'resubmitted') {
return (
<span className="px-3 py-1 rounded-md text-[#7C5E24] bg-[#F9F7ED] font-medium text-sm whitespace-nowrap">
Resubmitted
</span>
);
} else {
@ -133,12 +139,14 @@ const SubmissionTable = ({ selectedQuarter, selectedYear }) => {
return (
<div
className="border border-[#E5E7EB] bg-white shadow-[0_16px_32px_rgba(15,23,42,0.06)] rounded-[8px]"
style={{
width: '1250px',
minHeight: filtered.length === 0 || loading ? 'auto' : '780px',
}}
>
className="border border-[#E5E7EB] bg-white shadow-[0_8px_16px_rgba(15,23,42,0.05)] rounded-[6px] w-full overflow-hidden py-2"
style={{
minHeight: filtered.length === 0 || loading ? 'auto' : '200px',
}}
>
<h2 className="px-4 pr-100 py-2 mt-3 text-[18px] leading-[28px] font-medium text-[#232528]">
{titleText}
</h2>
<div className="space-y-[1px] overflow-x-auto">
{/* <div className="px-6 pt-6 pb-4">
<h2 className="text-[18px] leading-[28px] font-medium text-[#232528]">
@ -155,7 +163,7 @@ const SubmissionTable = ({ selectedQuarter, selectedYear }) => {
No Submission Found
</div>
) : (
<div className="min-w-[1200px]">
<div className="w-full overflow-x-auto pt-5 h-[550px]">
<Table
headers={headers}
rows={tableRows}
@ -235,7 +243,7 @@ const SubmissionTable = ({ selectedQuarter, selectedYear }) => {
<button
type="button"
onClick={() => navigate('/admin/validations')}
className="inline-flex h-[44px] w-[190px] items-center justify-center gap-2 rounded-md bg-[#9E792B] text-sm font-medium text-white shadow-[0_8px_18px_rgba(146,114,42,0.35)] hover:bg-[#846522]"
className="inline-flex h-[44px] w-[190px] items-center justify-center gap-2 rounded-md bg-[#9E792B] text-sm font-medium text-white shadow-[0_8px_18px_rgba(146,114,42,0.35)] hover:bg-[#846522] py-6"
>
View All Submissions
</button>

View File

@ -3,12 +3,14 @@ import React from 'react';
const toneClasses = {
green: 'bg-[#F3FAF4] text-[#2F663C] ring-1 ring-[#C4E5CC]',
gray: 'bg-[#E9EDF3] text-[#232528] ring-1 ring-[#D6DBE4]',
resubmitted: 'bg-[#F9F7ED] text-[#8B8000] ring-1 ring-[#E5DFC5]',
};
const normalizeTone = (tone, status) => {
if (tone) return tone;
const normalizedStatus = String(status || '').toLowerCase();
if (normalizedStatus === 'active' || normalizedStatus === 'open') return 'green';
if (normalizedStatus === 'resubmitted') return 'resubmitted';
return 'gray';
};

View File

@ -30,16 +30,28 @@ const getStatusBadge = (status) => {
Approved
</span>
);
} else if (normalized === 'submitted') {
return (
<span className="px-3 py-1 rounded-md text-green-700 bg-green-50 font-medium text-sm whitespace-nowrap">
Submitted
</span>
);
} else if (normalized === 'resubmitted') {
return (
<span className="px-3 py-1 rounded-md text-[#7C5E24] bg-[#F9F7ED] font-medium text-sm whitespace-nowrap">
Resubmitted
</span>
);
} else if (['pending', 'under review', 'in-progress'].includes(normalized)) {
return (
<span className="px-3 py-1 rounded-md text-blue-600 bg-blue-50 font-medium text-sm whitespace-nowrap">
Under Review
</span>
);
} else if (['rejected', 'returned'].includes(normalized)) {
} else if (normalized === 'rejected') {
return (
<span className="px-3 py-1 rounded-md text-red-600 bg-red-50 font-medium text-sm whitespace-nowrap">
Returned
Rejected
</span>
);
} else {
@ -63,7 +75,7 @@ const downloadCsv = (data) => {
const normalized = String(item.status || '').toLowerCase();
if (normalized === 'approved') return 'Approved';
if (['pending', 'under review', 'in-progress'].includes(normalized)) return 'Under Review';
if (['rejected', 'returned'].includes(normalized)) return 'Returned';
if (['rejected', 'rejected'].includes(normalized)) return 'Returned';
return item.status || '—';
})();

View File

@ -30,7 +30,7 @@ const SurveyCarousel = ({
setCurrentIndex((prev) => (prev === surveys.length - 1 ? 0 : prev + 1));
return (
<div className="bg-black rounded-lg shadow-sm ring-1 ring-gray-200 mb-8 ml-2 ">
<div className="bg-black rounded-lg shadow-sm ring-1 w-[1275px] ring-gray-200 h-[148px] mb-8 ml-2 ">
<div className="rounded-none bg-[#F2ECCF] px-8 py-8 min-h-[148px] flex items-center justify-between transition-all duration-500">
<div className="flex-1">
<h3 className="text-[18px] leading-[28px] font-medium text-[#232528]">

View File

@ -44,7 +44,8 @@ export const DetailedOverview = ({ submission, onBack }) => {
'Approved': 'bg-[#F3FAF4] text-[#2F663C]',
'Submitted': 'bg-[#E7F5FF] text-[#003CFF]',
'Pending': 'bg-[#F9F7ED] text-[#7C5E24]',
'Rejected': 'bg-[#FEF2F2] text-[#B52520]'
'Rejected': 'bg-[#FEF2F2] text-[#B52520]',
'Resubmitted': 'bg-[#F9F7ED] text-[#7C5E24]'
};
const statusClass = statusMap[status] || 'bg-gray-100 text-gray-800';
@ -172,7 +173,7 @@ export const DetailedOverview = ({ submission, onBack }) => {
product.current_cost || '0',
product.forecast_quantity || '0',
product.forecast_cost || '0',
product.status || 'Pending'
product.status || 'Submitted'
].map((value) => `"${String(value).replace(/"/g, '""')}"`).join(','));
});
@ -231,8 +232,8 @@ export const DetailedOverview = ({ submission, onBack }) => {
productData.hs_code || '—',
productData.product_name || '—',
unit.uom || 'N/A',
product.previous_quantity || '0',
`AED ${product.previous_cost || '0'}`,
product.current_quantity || '0',
`AED ${product.current_cost || '0'}`,
// <span
// key={`status-${index}`}
// className={`inline-flex items-center justify-center rounded-md px-3 py-1 text-xs font-medium ${

View File

@ -50,6 +50,13 @@ const ProductDetails = ({ product, onBack, showToast: shouldShowToast, onToastSh
textColor: 'text-[#043DFF]',
iconColor: 'text-[#286CFF]',
message: 'Your survey was successfully submitted and is waiting for approval.'
},
resubmitted: {
type: 'resubmitted',
bgColor: 'bg-[#E7F5FF]',
textColor: 'text-[#043DFF]',
iconColor: 'text-[#286CFF]',
message: 'Your survey was successfully Resubmitted and is waiting for approval.'
}
};
@ -182,9 +189,11 @@ const ProductDetails = ({ product, onBack, showToast: shouldShowToast, onToastSh
industry_code: product.industry_code,
license_number: product.license_number,
isic_code: product.isic_code,
emirate: product.emirate
emirate: product.emirate,
status: product.status // Include status for reference
},
isResubmit: true
isResubmit: true,
submissionId: product.submission_id // Add submissionId for resubmission
}
});
}}

View File

@ -252,6 +252,7 @@ const ReviewSubmit = ({
onToastClose = () => {},
isSubmitting = false,
hasSubmitted = false,
submitButtonText = 'Submit',
}) => {
const [confirm, setConfirm] = React.useState(false);
const [remarks, setRemarks] = React.useState(() => {
@ -734,11 +735,10 @@ const ReviewSubmit = ({
<h3 className="text-base font-semibold text-gray-900 mb-4">Remarks</h3>
<div className="space-y-2">
<textarea
placeholder="Enter your remarks here..."
readOnly
value={remarks}
onChange={(e) => setRemarks(e.target.value)}
className="w-full min-h-[100px] p-3 border border-#CBA344 rounded-md focus:ring-2 focus:ring-[#CBA344] focus:border-transparent"
value={remarks || ''}
placeholder="No remarks provided"
className="w-full min-h-[100px] p-3 border border-gray-200 rounded-md bg-gray-50 text-gray-700 resize-none focus:outline-none focus:ring-0 focus:border-gray-200"
/>
</div>
</div>
@ -792,7 +792,7 @@ const ReviewSubmit = ({
: 'bg-[#92722A] text-white hover:bg-[#7b5c1f]'
}`}
>
<span>{submitLabel}</span>
<span>{submitButtonText}</span>
</button>
</div>
</div>

View File

@ -33,6 +33,7 @@ const ValidationReview = () => {
const [actionLoading, setActionLoading] = React.useState(false);
const [error, setError] = React.useState(null);
const [submissionData, setSubmissionData] = React.useState(null);
const [quarterPeriods, setQuarterPeriods] = React.useState(null);
React.useEffect(() => {
const fetchSubmission = async () => {
@ -49,10 +50,16 @@ const ValidationReview = () => {
const result = await getSubmissionById(id);
let data = null;
if (result?.status === 'success' && result?.data) {
if (result?.status === 'success') {
data = result.data;
if (result.quarter_periods) {
setQuarterPeriods(result.quarter_periods);
}
} else if (result?.data) {
data = result.data;
if (result.quarter_periods) {
setQuarterPeriods(result.quarter_periods);
}
} else {
data = result;
}
@ -239,10 +246,9 @@ const ValidationReview = () => {
},
];
const quarterPeriods = submissionData?.quarter_periods || {};
const previousQuarter = quarterPeriods.previous_quarter || 'Q1';
const currentQuarter = quarterPeriods.current_quarter || 'Q2';
const forecastQuarter = quarterPeriods.forecast_quarter || 'Q3';
const previousQuarter = quarterPeriods?.previous_quarter;
const currentQuarter = quarterPeriods?.current_quarter;
const forecastQuarter = quarterPeriods?.forecast_quarter;
const year = submissionData?.year || new Date().getFullYear();
const products = submissionData?.products || [];
@ -380,13 +386,7 @@ const ValidationReview = () => {
</section>
{productDetails.length > 0 ? (
<div
className={`grid gap-4 ${
productDetails.length === 1
? 'grid-cols-1'
: 'grid-cols-1 lg:grid-cols-2'
}`}
>
<div className={`grid gap-4 ${productDetails.length === 1 ? 'grid-cols-1' : 'grid-cols-1 lg:grid-cols-2'}`}>
{productDetails.map((product, idx) => (
<section
key={product.code + idx}
@ -420,113 +420,180 @@ const ValidationReview = () => {
Metric
</th>
<th colSpan="3" className="bg-[#F2ECCF] text-[#92722A] px-4 py-2 text-xs font-semibold uppercase text-center">
Q4-2024 (Previous Quarter)
{quarterPeriods?.previous_quarter}-{quarterPeriods?.previous_year} (Previous Quarter)
</th>
<th colSpan="3" className="bg-[#F2ECCF] text-[#92722A] px-4 py-2 text-xs font-semibold uppercase text-center">
Q1-2025 (Current Quarter)
{quarterPeriods?.current_quarter}-{quarterPeriods?.current_year} (Current Quarter)
</th>
<th colSpan="3" className="bg-[#F2ECCF] text-[#92722A] px-4 py-2 text-xs font-semibold uppercase text-center">
Q2-2025 (Next Quarter)
{quarterPeriods?.forecast_quarter}-{quarterPeriods?.forecast_year} (Next Quarter)
</th>
</tr>
<tr>
<th className="bg-[#F4F8FF] px-4 py-2 font-medium text-[#286CFF]">Oct</th>
<th className="bg-[#F4F8FF] px-4 py-2 font-medium text-[#286CFF]">Nov</th>
<th className="bg-[#F4F8FF] px-4 py-2 font-medium text-[#286CFF]">Dec</th>
<th className="bg-[#F4F8FF] px-4 py-2 font-medium text-[#286CFF]">{quarterPeriods?.current_month?.current_period_one}</th>
<th className="bg-[#F4F8FF] px-4 py-2 font-medium text-[#286CFF]">{quarterPeriods?.current_month?.current_period_two}</th>
<th className="bg-[#F4F8FF] px-4 py-2 font-medium text-[#286CFF]">{quarterPeriods?.current_month?.current_period_three}</th>
<th className="bg-[#F3FAF4] px-4 py-2 font-medium text-[#2F663C]">Jan</th>
<th className="bg-[#F3FAF4] px-4 py-2 font-medium text-[#2F663C]">Feb</th>
<th className="bg-[#F3FAF4] px-4 py-2 font-medium text-[#2F663C]">Mar</th>
<th className="bg-[#F3FAF4] px-4 py-2 font-medium text-[#2F663C]">{quarterPeriods?.forecast_month?.forecast_period_one}</th>
<th className="bg-[#F3FAF4] px-4 py-2 font-medium text-[#2F663C]">{quarterPeriods?.forecast_month?.forecast_period_two}</th>
<th className="bg-[#F3FAF4] px-4 py-2 font-medium text-[#2F663C]">{quarterPeriods?.forecast_month?.forecast_period_three}</th>
<th className="bg-[#FFF6EC] px-4 py-2 font-medium text-[#F29F0E]">Apr</th>
<th className="bg-[#FFF6EC] px-4 py-2 font-medium text-[#F29F0E]">May</th>
<th className="bg-[#FFF6EC] px-4 py-2 font-medium text-[#F29F0E]">Jun</th>
<th className="bg-[#FFF6EC] px-4 py-2 font-medium text-[#F29F0E]">{quarterPeriods?.previous_month?.previous_period_one}</th>
<th className="bg-[#FFF6EC] px-4 py-2 font-medium text-[#F29F0E]">{quarterPeriods?.previous_month?.previous_period_two}</th>
<th className="bg-[#FFF6EC] px-4 py-2 font-medium text-[#F29F0E]">{quarterPeriods?.previous_month?.previous_period_three}</th>
</tr>
</thead>
<tbody>
<tr className="border-t border-[#E5E7EB]">
<td className="bg-[#FFFCF2] px-4 py-3 font-medium text-[#232528]">
Quantity (liters)
</td>
<td className="px-4 py-3 text-[#232528]">1200</td>
<td className="px-4 py-3 text-red-600">1180 </td>
<td className="px-4 py-3 text-green-600">1250 </td>
{submissionData?.products?.map((product, idx) => (
<React.Fragment key={idx}>
{/* Quantity Row */}
<tr className="border-t border-[#E5E7EB]">
<td className="bg-[#FFFCF2] px-4 py-3 font-medium text-[#232528]">
Quantity ({product.unit?.uom || 'units'})
</td>
{/* Previous Quarter Quantities */}
<td className="px-4 py-3 text-[#232528]">
{product.previous_quantity_period_one || '0'}
</td>
<td className="px-4 py-3 text-[#232528]">
{product.previous_quantity_period_two || '0'}
</td>
<td className="px-4 py-3 text-[#232528]">
{product.previous_quantity_period_three || '0'}
</td>
{/* Current Quarter Quantities */}
<td className="px-4 py-3 text-[#232528]">
{product.current_quantity_period_one || '0'}
</td>
<td className="px-4 py-3 text-[#232528]">
{product.current_quantity_period_two || '0'}
</td>
<td className="px-4 py-3 text-[#232528]">
{product.current_quantity_period_three || '0'}
</td>
{/* Forecast Quarter Quantities */}
<td className="px-4 py-3 text-[#232528]">
{product.forecast_quantity_period_one || '0'}
</td>
<td className="px-4 py-3 text-[#232528]">
{product.forecast_quantity_period_two || '0'}
</td>
<td className="px-4 py-3 text-[#232528]">
{product.forecast_quantity_period_three || '0'}
</td>
</tr>
<td className="px-4 py-3 text-[#232528]">1280</td>
<td className="px-4 py-3 text-[#232528]">1340</td>
<td className="px-4 py-3 text-[#232528]">1375</td>
{/* Cost Row */}
<tr className="border-t border-[#E5E7EB]">
<td className="bg-[#FFFCF2] px-4 py-3 font-medium text-[#232528]">
Cost (AED)
</td>
{/* Previous Quarter Costs */}
<td className="px-4 py-3 text-[#232528]">
{product.previous_cost_period_one || '0'}
</td>
<td className="px-4 py-3 text-[#232528]">
{product.previous_cost_period_two || '0'}
</td>
<td className="px-4 py-3 text-[#232528]">
{product.previous_cost_period_three || '0'}
</td>
{/* Current Quarter Costs */}
<td className="px-4 py-3 text-[#232528]">
{product.current_cost_period_one || '0'}
</td>
<td className="px-4 py-3 text-[#232528]">
{product.current_cost_period_two || '0'}
</td>
<td className="px-4 py-3 text-[#232528]">
{product.current_cost_period_three || '0'}
</td>
{/* Forecast Quarter Costs */}
<td className="px-4 py-3 text-[#232528]">
{product.forecast_cost_period_one || '0'}
</td>
<td className="px-4 py-3 text-[#232528]">
{product.forecast_cost_period_two || '0'}
</td>
<td className="px-4 py-3 text-[#232528]">
{product.forecast_cost_period_three || '0'}
</td>
</tr>
{/* Remarks Row (if exists) */}
{product.remarks && (
<tr className="border-t border-[#E5E7EB] bg-gray-100">
<td className="px-4 py-3 font-medium text-[#232528]">
Remarks
</td>
<td colSpan="9" className="px-4 py-3 text-[#232528]">
{product.remarks}
</td>
</tr>
)}
{/* Current Quarter Reason Row */}
<tr className="border-t border-[#E5E7EB]">
<td className="bg-[#FFFCF2] px-4 py-3 font-medium text-[#232528]">
Reason (Current)
</td>
<td colSpan="3" className="px-4 py-3 text-center text-[#6B7280]">--</td>
<td colSpan="3" className="px-4 py-3 text-center bg-[#E9F6EC] text-[#1E7C34] font-medium rounded-md">
{product.variation_reason?.reason || 'No reason provided'}
{product.other_variation_reason && (
<div className="text-xs mt-1">Note: {product.other_variation_reason}</div>
)}
</td>
<td colSpan="3" className="px-4 py-3 text-center text-[#6B7280]">--</td>
</tr>
<td className="px-4 py-3 text-[#232528]">1400</td>
<td className="px-4 py-3 text-[#232528]">1450</td>
<td className="px-4 py-3 text-[#232528]">1500</td>
</tr>
<tr className="border-t border-[#E5E7EB]">
<td className="bg-[#FFFCF2] px-4 py-3 font-medium text-[#232528]">
Cost (AED)
</td>
<td className="px-4 py-3 text-[#232528]">39,000</td>
<td className="px-4 py-3 text-red-600">37,500 </td>
<td className="px-4 py-3 text-green-600">41,000 </td>
<td className="px-4 py-3 text-[#232528]">40,000</td>
<td className="px-4 py-3 text-[#232528]">42,000</td>
<td className="px-4 py-3 text-[#232528]">43,500</td>
<td className="px-4 py-3 text-[#232528]">44,000</td>
<td className="px-4 py-3 text-[#232528]">45,000</td>
<td className="px-4 py-3 text-[#232528]">46,000</td>
</tr>
<tr className="border-t border-[#E5E7EB]">
<td className="bg-[#FFFCF2] px-4 py-3 font-medium text-[#232528]">
Reason (Current)
</td>
<td colSpan="3" className="px-4 py-3 text-center text-[#6B7280]">--</td>
<td colSpan="3" className="px-4 py-3 text-center bg-[#E9F6EC] text-[#1E7C34] font-medium rounded-md">
Seasonal Increase
</td>
<td colSpan="3" className="px-4 py-3 text-center text-[#6B7280]">--</td>
</tr>
<tr className="border-t border-[#E5E7EB]">
<td className="bg-[#FFFCF2] px-4 py-3 font-medium text-[#232528]">
Reason (Forecast)
</td>
<td colSpan="3" className="px-4 py-3 text-center text-[#6B7280]">--</td>
<td colSpan="3" className="px-4 py-3 text-center text-[#6B7280]">--</td>
<td colSpan="3" className="px-4 py-3 text-center bg-[#FFF2E0] text-[#D97706] font-medium rounded-md">
Projected demand
</td>
</tr>
{/* Forecast Quarter Reason Row */}
<tr className="border-t border-[#E5E7EB]">
<td className="bg-[#FFFCF2] px-4 py-3 font-medium text-[#232528]">
Reason (Forecast)
</td>
<td colSpan="3" className="px-4 py-3 text-center text-[#6B7280]">--</td>
<td colSpan="3" className="px-4 py-3 text-center text-[#6B7280]">--</td>
<td colSpan="3" className="px-4 py-3 text-center bg-[#FFF2E0] text-[#D97706] font-medium rounded-md">
{product.zero_target_reason?.reason || 'No reason provided'}
{product.other_zero_target_reason && (
<div className="text-xs mt-1">Note: {product.other_zero_target_reason}</div>
)}
</td>
</tr>
</React.Fragment>
))}
</tbody>
</table>
</div>
<div className="text-xs text-[#4B5563] mt-2">
<p>
<span className="text-[#16A34A] font-semibold">Green </span> indicates upward trend;{' '}
<span className="text-[#DC2626] font-semibold">Red </span> indicates downward trend compared to previous month or quarter
</p>
<p className="mt-3 text-sm text-[#4B5563]">
<strong className="font-semibold text-[#232528]">Remarks:</strong> NA
</p>
</div>
<div className="mt-3">
<label className="block text-sm font-semibold mb-1 text-[#232528]">Admin Remarks</label>
<textarea
rows="2"
className="w-full rounded-[10px] border border-[#D1D5DB] bg-[#FFFCF2] px-4 py-2 text-sm text-[#232528] focus:ring-2 focus:ring-[#92722A] focus:outline-none"
placeholder="Add short note..."
></textarea>
</div>
</section>
))}
<div className="mt-4">
{/* <p>
<span className="text-[#16A34A] font-semibold">Green </span> indicates upward trend;{' '}
<span className="text-[#DC2626] font-semibold">Red </span> indicates downward trend compared to previous month or quarter
</p> */}
<p className="mt-3 text-sm text-[#4B5563]">
<strong className="font-semibold text-[#232528]">Remarks:</strong> NA
</p>
</div>
<div className="mt-3">
<label className="block text-sm font-semibold mb-1 text-[#232528]">Admin Remarks</label>
<textarea
rows="2"
className="w-full rounded-[10px] border border-[#D1D5DB] bg-[#FFFCF2] px-4 py-2 text-sm text-[#232528] focus:ring-2 focus:ring-[#92722A] focus:outline-none"
placeholder="Add short note..."
></textarea>
</div>
</div>
</section>
))}
</div>
) : (
<div className="rounded-lg bg-amber-50 border border-amber-200 p-6 text-center">
<p className="text-amber-800">No products found for this submission.</p>

View File

@ -13,6 +13,8 @@ const STATUS_VARIANTS = {
Approved: { background: '#F3FAF4', text: '#2F663C', border: '#F3FAF4' },
Pending: { background: '#E7F5FF', text: '#286CFF', border: '#E7F5FF' },
Rejected: { background: '#FEF2F2', text: '#B52520', border: '#FEF2F2' },
Resubmitted: { background: '#F9F7ED', text: '#7C5E24', border: '#FEF2F2' },
Submitted: { background: '#E7F5FF', text: '#003CFF', border: '#FEF2F2' },
};
const SUMMARY_VARIANTS = {
@ -20,8 +22,12 @@ const SUMMARY_VARIANTS = {
approved: { background: '#F3FAF4', label: '#2F663C', value: '#232528' },
pending: { background: '#E7F5FF', label: '#003CFF', value: '#232528' },
rejected: { background: '#FEF2F2', label: '#B52520', value: '#232528' },
resubmitted: { background: '#F9F7ED', text: '#7C5E24', border: '#FEF2F2' },
submitted: { background: '#E7F5FF', text: '#003CFF', border: '#FEF2F2' },
};
const formatDateTime = (value) => {
if (!value) return '—';
const date = new Date(value);

View File

@ -1947,6 +1947,7 @@ const requiredFields = [
factory_name: form.establishmentName || form.contactName || '',
permanent_factory_code: form.permanentFactoryCode || '',
industry_code: form.industryCodeBusiness || form.industryCodeCurrent || '',
industry_code_production: form.industryCodeBusiness || form.industryCodeCurrent || '',
license_number: form.uniqueLicenseNumber || '',
isic_code: form.isicCode || form.industryCodeBusiness || '',
description: form.industryDescription || '',
@ -2025,6 +2026,7 @@ const requiredFields = [
factory_name: form.establishmentName || form.contactName || '',
permanent_factory_code: form.permanentFactoryCode || '',
industry_code: form.industryCodeBusiness || form.industryCodeCurrent || '',
industry_code_production: form.industryCodeBusiness || form.industryCodeCurrent || '',
license_number: form.uniqueLicenseNumber || '',
isic_code: form.isicCode || form.industryCodeBusiness || '',
description: form.industryDescription || '',

View File

@ -11,15 +11,40 @@ const BreadcrumbVector = '/assets/images/Breadcrumb-vector.svg';
const caretDownSrc = '/assets/images/CaretDown.svg';
const Badge = ({ variant = 'default', children }) => {
// Define all possible status variants and their styles
const styles = {
approved: 'bg-[#F3FAF4] text-[#2F663C]',
submitted: 'bg-[#E7F5FF] text-[#003CFF]',
pending: 'bg-[#F9F7ED] text-[#7C5E24]',
rejected: 'bg-[#FEF2F2] text-[#B52520]',
default: 'bg-gray-50 text-gray-700',
// Approved status
approved: 'text-green-700 bg-green-50',
// Submitted status
submitted: 'text-green-700 bg-green-50',
// Under Review status (includes pending and in-progress)
'under review': 'text-blue-600 bg-blue-50',
pending: 'text-blue-600 bg-blue-50',
'in-progress': 'text-blue-600 bg-blue-50',
// Rejected status (includes returned)
rejected: 'text-red-600 bg-red-50',
returned: 'text-red-600 bg-red-50',
// Resubmitted status
resubmitted: 'text-[#7C5E24] bg-[#F9F7ED]',
// Default fallback
default: 'text-gray-600 bg-gray-50',
};
// Normalize the variant key
const normalizedVariant = String(variant || '').toLowerCase().trim();
// Get the appropriate style, fallback to default if not found
const style = styles[normalizedVariant] || styles.default;
return (
<span className={`inline-flex items-center justify-center rounded-full text-xs font-medium px-3 py-1 ${styles[variant.toLowerCase()] || styles.default}`}>
<span
className={`inline-flex items-center justify-center rounded-md px-3 py-1 text-sm font-medium whitespace-nowrap ${style}`}
>
{children}
</span>
);
@ -180,8 +205,9 @@ const formatExportDateTime = (value) => {
const text = String(status || '').toLowerCase();
if (text === 'approved') return 'approved';
if (text === 'submitted') return 'submitted';
if (text === 'rejected') return 'rejected';
if (text === 'pending' || text === 'in-progress' || text === 'under review') return 'pending';
if (text === 'resubmitted') return 'resubmitted';
if (text === 'rejected' || text === 'returned') return 'rejected';
if (text === 'pending' || text === 'in-progress' || text === 'under review') return 'under review';
return 'default';
}, []);
@ -375,7 +401,7 @@ const filteredRows = useMemo(() => {
'Quarter',
'HS Code',
'Product',
'Status',
{ name: 'Status', className: 'text-center' },
'Actors',
'Details'
];
@ -850,14 +876,24 @@ const tableRows = auditHistory.map((entry, index) => {
loading={loading}
error={error}
className="mt-4"
pagination={{
currentPage: pagination.currentPage,
onPageChange: handlePageChange,
pageSize: pagination.pageSize,
totalItems: pagination.totalItems,
pageSizeOptions: [10, 25, 50, 100],
onPageSizeChange: handlePageSizeChange,
}}
columnWidths={[
{ width: 180, style: { whiteSpace: 'normal', wordWrap: 'break-word' } }, // Submission Date
{ width: 80, style: { whiteSpace: 'normal', wordWrap: 'break-word' } }, // Year
{ width: 80, style: { whiteSpace: 'normal', wordWrap: 'break-word' } }, // Quarter
{ width: 120, style: { whiteSpace: 'normal', wordWrap: 'break-word' } }, // HS Code
{ width: 200, style: { whiteSpace: 'normal', wordWrap: 'break-word' } }, // Product
{ width: 130, align: 'center', style: { whiteSpace: 'normal', wordWrap: 'break-word' } }, // Status
{ width: 140, style: { whiteSpace: 'normal', wordWrap: 'break-word' } }, // Actors
{ width: 270, style: { whiteSpace: 'normal', wordWrap: 'break-word' } } // Details
]}
pagination={{
currentPage: pagination.currentPage,
onPageChange: handlePageChange,
pageSize: pagination.pageSize,
totalItems: pagination.totalItems,
pageSizeOptions: [10, 25, 50, 100],
onPageSizeChange: handlePageSizeChange,
}}
renderCell={(value, ri, ci) => {
if (ci === 5) { // Status column
return <Badge variant={toVariant(filteredRows[ri]?.status)}>{formatStatus(filteredRows[ri]?.status)}</Badge>;

View File

@ -14,6 +14,7 @@ const statusStyles = {
submitted: 'text-[#003CFF] bg-[#E7F5FF]',
pending: 'text-[#7C5E24] bg-[#F9F7ED]',
rejected: 'text-[#B52520] bg-[#FEF2F2]',
resubmitted: 'text-[#7C5E24] bg-[#F9F7ED]',
};
const formatDate = (dateString) => {

View File

@ -1,6 +1,6 @@
import React from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { submitSurvey, fetchSubmissionDetail } from '@/services/submissions/submissionService';
import { submitSurvey, fetchSubmissionDetail, resubmitSurvey } from '@/services/submissions/submissionService';
import { fetchEstablishmentDetail, fetchEstablishmentDashboard } from '@/services/establishments/establishmentService';
import EstablishmentInfo from '@/components/survey/EstablishmentInfo/EstablishmentInfo';
import HeaderBar from '@/components/layout/HeaderBar';
@ -71,6 +71,8 @@ const Survey = () => {
const [establishmentLoading, setEstablishmentLoading] = React.useState(false);
const [establishmentError, setEstablishmentError] = React.useState('');
const isViewMode = React.useMemo(() => Boolean(locationState.submission), [locationState.submission]);
const isResubmit = React.useMemo(() => locationState?.isResubmit === true, [locationState?.isResubmit]);
const submissionId = React.useMemo(() => locationState?.submissionId || locationState?.submission?.id, [locationState]);
const toastTimeoutRef = React.useRef(null);
React.useEffect(() => {
@ -98,8 +100,18 @@ const Survey = () => {
const initializeSurveyData = async () => {
try {
// If this is a resubmission, use the quarter and year from the submission
if (isResubmit && locationState?.submission) {
if (!cancelled) {
setEstablishmentData(prev => ({
...prev,
quarter: locationState.submission.quarter || '',
year: locationState.submission.year?.toString() || ''
}));
}
}
// If quarter and year are not provided in location state, fetch them
if (!locationState?.currentQuarter || !locationState?.currentYear) {
else if (!locationState?.currentQuarter || !locationState?.currentYear) {
const { quarter, year } = await fetchCurrentQuarterAndYear();
if (!cancelled) {
setEstablishmentData(prev => ({
@ -128,7 +140,7 @@ const Survey = () => {
return () => {
cancelled = true;
};
}, [locationState?.currentQuarter, locationState?.currentYear]); // Only depend on these values
}, [locationState?.currentQuarter, locationState?.currentYear]);
React.useEffect(() => {
let cancelled = false;
@ -204,19 +216,39 @@ const Survey = () => {
const mergeEstablishmentData = (detail) => {
const summary = locationState.submission || {};
const establishment = detail?.establishment || {};
return {
quarter: ensureString(detail?.quarter) || ensureString(summary.quarter) || '',
year: ensureString(detail?.year) || ensureString(summary.year) || '',
establishmentName: ensureString(summary.establishment_name) || establishmentData.establishmentName || '',
permanentFactoryCode:
ensureString(summary.permanent_factory_code) ||
ensureString(detail?.establishment_id) ||
establishmentData.permanentFactoryCode || '',
industryCode: ensureString(summary.industry_code) || establishmentData.industryCode || '',
licenseNumber: ensureString(summary.license_number) || establishmentData.licenseNumber || '',
isicCode: ensureString(summary.isic_code) || establishmentData.isicCode || '',
emirate: ensureString(summary.emirate) || establishmentData.emirate || '',
employeeInfo: { ...establishmentData.employeeInfo },
establishmentName: ensureString(establishment.factory_name) ||
ensureString(summary.establishment_name) ||
establishmentData.establishmentName || '',
permanentFactoryCode: ensureString(establishment.permanent_factory_code) ||
ensureString(summary.permanent_factory_code) ||
ensureString(detail?.establishment_id) ||
establishmentData.permanentFactoryCode || '',
industryCode: ensureString(establishment.industry_code) ||
ensureString(summary.industry_code) ||
establishmentData.industryCode || '',
licenseNumber: ensureString(establishment.license_number) ||
ensureString(summary.license_number) ||
establishmentData.licenseNumber || '',
isicCode: ensureString(establishment.isic_code) ||
ensureString(summary.isic_code) ||
establishmentData.isicCode || '',
emirate: ensureString(establishment.establishment_emirate_id) ||
ensureString(establishment.emirate) ||
ensureString(summary.emirate) ||
establishmentData.emirate || '',
employeeInfo: {
emiratiMale: ensureString(establishment.emirati_male) || establishmentData.employeeInfo.emiratiMale || '',
emiratiFemale: ensureString(establishment.emirati_female) || establishmentData.employeeInfo.emiratiFemale || '',
nonEmiratiMale: ensureString(establishment.non_emirati_male) || establishmentData.employeeInfo.nonEmiratiMale || '',
nonEmiratiFemale: ensureString(establishment.non_emirati_female) || establishmentData.employeeInfo.nonEmiratiFemale || '',
totalEmirati: ensureString(establishment.total_emirati) || establishmentData.employeeInfo.totalEmirati || '',
totalEmployees: ensureString(establishment.total_employees) || establishmentData.employeeInfo.totalEmployees || '',
},
};
};
@ -224,20 +256,50 @@ const Survey = () => {
if (!Array.isArray(detailProducts)) return [];
return detailProducts.map((product, index) => ({
id: product?.id ?? index + 1,
productId: product?.product_id || product?.product?.id || '',
product: ensureString(product?.product?.product_name) || ensureString(product?.product_id) || '',
unit: ensureString(product?.unit?.name) || ensureString(product?.unit_id) || '',
unit: ensureString(product?.unit?.uom) || ensureString(product?.unit?.name) || ensureString(product?.unit_id) || '',
unitId: product?.unit_id || product?.unit?.id || '',
capacity: ensureString(product?.annual_installed_capacity),
previousQuantity: ensureString(product?.previous_quantity),
previousCost: ensureString(product?.previous_cost),
currentQuantity: ensureString(product?.current_quantity),
currentCost: ensureString(product?.current_cost),
forecastQuantity: ensureString(product?.forecast_quantity),
forecastCost: ensureString(product?.forecast_cost),
variationReason: ensureString(product?.variation_reason_master_id),
otherVariationReason: ensureString(product?.other_variation_reason),
zeroTargetReason: ensureString(product?.zero_target_reason_master_id),
otherZeroTargetReason: ensureString(product?.other_zero_target_reason),
remarks: ensureString(product?.remarks),
// Previous Quarter (Q4) - October, November, December
octQuantity: ensureString(product?.previous_quantity_period_one || ''),
novQuantity: ensureString(product?.previous_quantity_period_two || ''),
decQuantity: ensureString(product?.previous_quantity_period_three || ''),
octCost: ensureString(product?.previous_cost_period_one || ''),
novCost: ensureString(product?.previous_cost_period_two || ''),
decCost: ensureString(product?.previous_cost_period_three || ''),
// Current Quarter (Q1) - January, February, March
janQuantity: ensureString(product?.current_quantity_period_one || ''),
febQuantity: ensureString(product?.current_quantity_period_two || ''),
marQuantity: ensureString(product?.current_quantity_period_three || ''),
janCost: ensureString(product?.current_cost_period_one || ''),
febCost: ensureString(product?.current_cost_period_two || ''),
marCost: ensureString(product?.current_cost_period_three || ''),
// Forecast Quarter (Q2) - April, May, June
aprQuantity: ensureString(product?.forecast_quantity_period_one || ''),
mayQuantity: ensureString(product?.forecast_quantity_period_two || ''),
junQuantity: ensureString(product?.forecast_quantity_period_three || ''),
aprCost: ensureString(product?.forecast_cost_period_one || ''),
mayCost: ensureString(product?.forecast_cost_period_two || ''),
junCost: ensureString(product?.forecast_cost_period_three || ''),
// Totals (for backward compatibility)
previousQuantity: ensureString(product?.previous_quantity || ''),
previousCost: ensureString(product?.previous_cost || ''),
currentQuantity: ensureString(product?.current_quantity || ''),
currentCost: ensureString(product?.current_cost || ''),
forecastQuantity: ensureString(product?.forecast_quantity || ''),
forecastCost: ensureString(product?.forecast_cost || ''),
// Reasons and remarks
variationReason: ensureString(product?.variation_reason_master_id || ''),
otherVariationReason: ensureString(product?.other_variation_reason || ''),
zeroTargetReason: ensureString(product?.zero_target_reason_master_id || ''),
otherZeroTargetReason: ensureString(product?.other_zero_target_reason || ''),
remarks: ensureString(product?.remarks || '')
}));
};
@ -309,7 +371,7 @@ const Survey = () => {
return String(value);
}, []);
const buildSubmissionPayload = React.useCallback(() => {
const buildSubmissionPayload = React.useCallback((isResubmitFlow = false) => {
const info = establishmentData?.employeeInfo ?? {};
return {
quarter: establishmentData?.quarter ?? '',
@ -324,11 +386,13 @@ const Survey = () => {
products: productsData.map((product) => {
const variationId = parseNumber(product.variationReason, null);
const zeroTargetId = parseNumber(product.zeroTargetReason, null);
const productId = parseNumber(product.id);
// Map monthly data to period-specific fields
return {
...(isResubmitFlow && productId ? { id: productId } : {}), // Only include ID during resubmit
product_id: parseNumber(product.productId) || parseNumber(product.product),
unit_id: parseNumber(product.unit),
unit_id: parseNumber(product.unitId) || parseNumber(product.unit),
annual_installed_capacity: stringify(product.capacity),
// Previous Quarter (Q4) - October, November, December
@ -412,7 +476,31 @@ const Survey = () => {
}
};
const handleSubmit = React.useCallback(async () => {
const handleSubmit = React.useCallback(async (e) => {
if (e) e.preventDefault();
if (isSubmitting) return;
// Validate current step before proceeding
if (step === 1) {
const establishmentError = validateEstablishmentInfo();
if (establishmentError) {
setError(establishmentError);
return;
}
} else if (step === 2) {
const productError = validateProductData();
if (productError) {
setError(productError);
return;
}
}
// If this is the final step (step 3) and it's a resubmission, include the submission ID
if (step === 3 && isResubmit && submissionId) {
await handleResubmit();
return;
}
setIsSubmitting(true);
try {
const payload = buildSubmissionPayload();
@ -432,6 +520,57 @@ const Survey = () => {
}
}, [buildSubmissionPayload, showToast]);
const handleResubmit = async () => {
setIsSubmitting(true);
setError('');
try {
const payload = {
...buildSubmissionPayload(true), // Pass true to indicate resubmission flow
status: 'Resubmitted', // Set status to Resubmitted for resubmissions
id: submissionId // Include submission ID in the payload
};
// Use the resubmitSurvey API with the submission ID and payload
await resubmitSurvey(submissionId, payload);
setShowSuccessPopup(true);
setHasSubmitted(true);
showToast('success', 'Survey resubmitted successfully!');
} catch (error) {
console.error('Resubmission failed:', error);
const message = error?.response?.data?.message || error?.message || 'Failed to resubmit survey. Please try again.';
setError(message);
showToast('error', message);
} finally {
setIsSubmitting(false);
}
};
const handleFinalSubmit = async () => {
if (isResubmit && submissionId) {
await handleResubmit();
return;
}
setIsSubmitting(true);
setError('');
try {
const payload = buildSubmissionPayload();
await submitSurvey(payload);
setShowSuccessPopup(true);
setHasSubmitted(true);
showToast('success', 'Survey submitted successfully!');
} catch (error) {
const message = error?.response?.data?.message || error?.message || 'Failed to submit survey. Please try again.';
setError(message);
showToast('error', message);
} finally {
setIsSubmitting(false);
}
};
const StepBadge = ({ state, number }) => {
if (state === 'active') {
return (
@ -634,7 +773,8 @@ const Survey = () => {
establishment={establishmentData}
products={productsData}
onBack={() => setStep(2)}
onSubmit={handleSubmit}
onSubmit={handleFinalSubmit}
submitButtonText={isResubmit ? 'Resubmit' : 'Submit'}
toast={toast}
onToastClose={() => {
if (toastTimeoutRef.current) {
@ -644,7 +784,7 @@ const Survey = () => {
setToast(null);
}}
isSubmitting={isSubmitting}
hasSubmitted={hasSubmitted}
hasSubmitted={false}
/>
)}
</div>
@ -674,12 +814,12 @@ const Survey = () => {
{/* Title */}
<h3 className="text-lg font-semibold text-gray-900">
Submission successful
{isResubmit ? 'Resubmission successful' : 'Submission successful'}
</h3>
{/* Message */}
<p className="text-sm text-gray-700">
Thank you. Your IP Quarterly Survey for <span className="font-semibold text-gray-900">{establishmentData?.quarter} {establishmentData?.year}</span> has been submitted.
Thank you. Your IP Quarterly Survey for <span className="font-semibold text-gray-900">{establishmentData?.quarter} {establishmentData?.year}</span> has been {isResubmit ? 'resubmitted' : 'submitted'}.
</p>
{/* Button */}