Added Overview screen in Establishement users

This commit is contained in:
Malini 2025-10-31 09:54:39 +05:30
parent 02da3fedb0
commit 95528cb84c
8 changed files with 695 additions and 252 deletions

View File

@ -1,80 +1,133 @@
import React, { useState, useCallback, useMemo } from 'react'; import React, { useState, useCallback, useMemo, useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { fetchSubmissionDetail } from '@/services/submissions/submissionService';
import Table from '@/components/common/Table'; import Table from '@/components/common/Table';
import ProductDetails from './ProductDetails'; import ProductDetails from './ProductDetails';
const searchIconSrc = '/assets/images/material-symbols_search-rounded.svg'; const searchIconSrc = '/assets/images/material-symbols_search-rounded.svg';
const caretDownSrc = '/assets/images/CaretDown.svg'; const caretDownSrc = '/assets/images/CaretDown.svg';
const downloadIconSrc = '/assets/images/DownloadSimple.svg'; const downloadIconSrc = '/assets/images/DownloadSimple.svg';
export const DetailedOverview = ({ submission, onBack }) => { export const DetailedOverview = ({ submission, onBack }) => {
const navigate = useNavigate(); const navigate = useNavigate();
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [selectedStatus, setSelectedStatus] = useState(''); const [selectedStatus, setSelectedStatus] = useState('');
const [selectedProduct, setSelectedProduct] = useState(null); const [selectedProduct, setSelectedProduct] = useState(null);
const [loading, setLoading] = useState(true);
const [submissionData, setSubmissionData] = useState(null);
const [quarterPeriods, setQuarterPeriods] = useState(null);
const [loadingProduct, setLoadingProduct] = useState(false);
// Mock data - replace with actual data from props // Add state for tracking loading and error states
const productData = [ const [isLoading, setIsLoading] = useState(true);
{ const [error, setError] = useState(null);
id: 'PRD-2023-001',
hsCode: '1234.56.78', useEffect(() => {
name: 'Product 1', const fetchSubmissionDetails = async () => {
unit: 'PCS', try {
quantity: 1, setIsLoading(true);
cost: '1,000.00', setError(null);
status: 'Active', console.log('Fetching submission details for ID:', submission.id);
submissionDate: '30/10/2023 14:30', const response = await fetchSubmissionDetail(submission.id);
},
{ console.log('API Response:', response);
id: 'PRD-2023-002',
hsCode: '8765.43.21', if (response && response.data) {
name: 'Product 2', console.log('Setting submission data:', response.data);
unit: 'KG', setSubmissionData(response.data);
quantity: 5, setQuarterPeriods(response.quarter_periods);
cost: '2,000.00',
status: 'Pending', // Log the products array to verify it's being set correctly
submissionDate: '29/10/2023 10:15', if (response.data.products) {
}, console.log('Products in response:', response.data.products);
]; console.log('Number of products:', response.data.products.length);
}
} else {
console.warn('No data received in response');
}
} catch (error) {
console.error('Error fetching submission details:', error);
setError('Failed to load submission details. Please try again.');
} finally {
setIsLoading(false);
}
};
if (submission?.id) {
fetchSubmissionDetails();
} else {
console.error('No submission ID provided');
setError('No submission ID provided');
setIsLoading(false);
}
}, [submission]);
const filteredProducts = useMemo(() => { const filteredProducts = useMemo(() => {
console.log('Filtering products...');
console.log('Raw products data:', submissionData?.products);
if (!submissionData?.products || !Array.isArray(submissionData.products)) {
console.log('No products array found in submissionData');
return [];
}
const normalizedTerm = searchTerm.trim().toLowerCase(); const normalizedTerm = searchTerm.trim().toLowerCase();
return productData.filter((product) => { const filtered = submissionData.products.filter((product) => {
// Add null checks for product and product.product
if (!product || !product.product) return false;
const productName = product.product?.product_name || '';
const hsCode = product.product?.hs_code || '';
const status = product.status || '';
const matchesSearch = const matchesSearch =
!normalizedTerm || !normalizedTerm ||
product.name.toLowerCase().includes(normalizedTerm) || productName.toString().toLowerCase().includes(normalizedTerm) ||
product.hsCode.toLowerCase().includes(normalizedTerm) || hsCode.toString().toLowerCase().includes(normalizedTerm) ||
product.status.toLowerCase().includes(normalizedTerm); status.toLowerCase().includes(ormalizedTerm) ||
const matchesStatus = 'pending'.includes(normalizedTerm);
selectedStatus === '' ||
product.status.toLowerCase() === selectedStatus.toLowerCase(); const matchesStatus = selectedStatus === '' ||
(selectedStatus === 'pending' && !product.is_active) ||
(selectedStatus === 'approved' && product.is_active);
return matchesSearch && matchesStatus; return matchesSearch && matchesStatus;
}); });
}, [searchTerm, selectedStatus, productData]);
console.log('Filtered products:', filtered);
return filtered;
}, [searchTerm, selectedStatus, submissionData]);
const downloadCsv = useCallback(() => { const downloadCsv = useCallback(() => {
if (!filteredProducts.length) return; if (!filteredProducts.length || !submissionData) return;
const headers = [ const headers = [
'HS Code', 'HS Code',
'Product', 'Product',
'Unit', 'Unit',
'Quantity', 'Previous Quarter Quantity',
'Cost (AED)', 'Previous Quarter Cost (AED)',
'Status', 'Current Quarter Quantity',
'Submission Date & Time' 'Current Quarter Cost (AED)',
'Forecast Quarter Quantity',
'Forecast Quarter Cost (AED)',
'Status'
]; ];
const csvRows = [headers.join(',')]; const csvRows = [headers.join(',')];
filteredProducts.forEach((product) => { filteredProducts.forEach((product) => {
csvRows.push([ csvRows.push([
product.hsCode, product.product.hs_code,
product.name, product.product.product_name,
product.unit, product.unit?.uom || 'N/A',
product.quantity, product.previous_quantity || '0',
product.cost, product.previous_cost || '0',
product.status, product.current_quantity || '0',
product.submissionDate product.current_cost || '0',
product.forecast_quantity || '0',
product.forecast_cost || '0',
product.is_active ? 'Approved' : 'Pending'
].map((value) => `"${String(value).replace(/"/g, '""')}"`).join(',')); ].map((value) => `"${String(value).replace(/"/g, '""')}"`).join(','));
}); });
@ -89,26 +142,90 @@ export const DetailedOverview = ({ submission, onBack }) => {
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
}, [filteredProducts]); }, [filteredProducts]);
const rows = filteredProducts.map(product => [ const handleViewDetails = (product) => {
product.hsCode, // Use the product data we already have from the submission
product.name, setSelectedProduct(product);
product.unit, };
product.quantity,
`AED ${product.cost}`, const rows = useMemo(() => {
<span className={`inline-flex items-center justify-center rounded-md px-3 py-1 text-xs font-medium ${ if (!filteredProducts || !Array.isArray(filteredProducts)) {
product.status === 'Active' ? 'text-[#2F663C] bg-[#F3FAF4]' : console.log('No filtered products available');
'text-[#B45309] bg-[#FEF2F2]' return [];
}`}> }
{product.status}
</span>, console.log('Generating rows for filtered products:', filteredProducts);
product.submissionDate,
<button return filteredProducts.map((product, index) => {
onClick={() => setSelectedProduct(product)} // Add null checks for product and product properties
className="text-[#92722A] hover:underline text-sm font-medium" const productData = product.product || {};
const unit = product.unit || {};
return [
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'}`,
product.forecast_quantity || '0',
`AED ${product.forecast_cost || '0'}`,
<span
key={`status-${index}`}
className={`inline-flex items-center justify-center rounded-md px-3 py-1 text-xs font-medium ${
product.is_active ? 'text-[#2F663C] bg-[#F3FAF4]' :
'text-[#7C5E24] bg-[#F9F7ED]'
}`}
> >
View Details {product.is_active ? 'Approved' : 'Pending'}
</span>,
<button
key={`view-${index}`}
onClick={() => handleViewDetails(product)}
disabled={loadingProduct}
className="text-[#92722A] hover:underline text-sm font-medium disabled:opacity-50"
>
{loadingProduct && selectedProduct?.id === product.id ? 'Loading...' : 'View Details'}
</button> </button>
]); ];
});
}, [filteredProducts]);
// Show loading state for initial data load
if (isLoading) {
return (
<div className="flex flex-col 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>
<p className="mt-4 text-gray-600">Loading submission details...</p>
</div>
);
}
// Show error state
if (error) {
return (
<div className="p-4 bg-red-50 text-red-700 rounded-md">
<p className="font-medium">Error</p>
<p className="mt-1">{error}</p>
<button
onClick={() => window.location.reload()}
className="mt-3 px-4 py-2 bg-blue-100 text-blue-700 rounded-md hover:bg-blue-200"
>
Retry
</button>
</div>
);
}
// Show no data state
if (!submissionData) {
return (
<div className="text-center py-10">
<p className="text-gray-600">No submission data available</p>
</div>
);
}
const toolbar = ( const toolbar = (
@ -165,18 +282,29 @@ export const DetailedOverview = ({ submission, onBack }) => {
'HS Code', 'HS Code',
'Product', 'Product',
'Unit', 'Unit',
'Quantity', 'Previous Qty',
'Cost (AED)', 'Previous Cost (AED)',
'Current Qty',
'Current Cost (AED)',
'Forecast Qty',
'Forecast Cost (AED)',
'Status', 'Status',
'Submission Date & Time',
'Action' 'Action'
]; ];
// Calculate total products and average cost
const totalProducts = filteredProducts.length;
const totalCost = submissionData.products?.reduce((sum, product) => {
return sum + parseFloat(product.current_cost || 0);
}, 0);
const averageCost = totalProducts > 0 ? (totalCost / totalProducts).toFixed(2) : 0;
// If a product is selected, show the product details view // If a product is selected, show the product details view
if (selectedProduct) { if (selectedProduct) {
return ( return (
<ProductDetails <ProductDetails
product={selectedProduct} product={selectedProduct}
quarterPeriods={quarterPeriods}
onBack={() => setSelectedProduct(null)} onBack={() => setSelectedProduct(null)}
/> />
); );
@ -198,43 +326,38 @@ export const DetailedOverview = ({ submission, onBack }) => {
</svg> </svg>
</button> </button>
<div className="flex items-center space-x-2 pr-6"> <div className="flex items-center space-x-2 pr-6">
<p className="text-sm text-[#232528] font-medium">Establishment ID:</p> <p className="text-sm text-[#232528] font-medium">Establishment:</p>
<p className="text-sm text-[#232528]">Al Khazna Investment</p> <p className="text-sm text-[#232528]">{submissionData.establishment?.factory_name || 'N/A'}</p>
</div> </div>
<div className="flex items-center space-x-2 px-6"> <div className="flex items-center space-x-2 px-6">
<p className="text-sm text-[#232528] font-medium">Total Submitted Products:</p> <p className="text-sm text-[#232528] font-medium">Total Submitted Products:</p>
<p className="text-sm text-[#232528]">678</p> <p className="text-sm text-[#232528]">{totalProducts}</p>
</div> </div>
<div className="flex items-center space-x-2 pl-6"> <div className="flex items-center space-x-2 pl-6">
<p className="text-sm text-[#232528] font-medium">Average Cost:</p> <p className="text-sm text-[#232528] font-medium">Average Cost (AED):</p>
<p className="text-sm text-[#232528]">7889</p> <p className="text-sm text-[#232528]">{averageCost}</p>
</div> </div>
</div> </div>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<div className="relative"> <div className="relative">
<select className="h-10 w-32 rounded-md border border-[#E2E8F0] bg-white pl-3 pr-8 text-sm text-[#232528] focus:outline-none cursor-pointer appearance-none"> <select
<option>2023</option> className="h-10 w-32 rounded-md border border-[#E2E8F0] bg-white pl-3 pr-8 text-sm text-[#232528] focus:outline-none cursor-pointer appearance-none"
<option>2024</option> value={submissionData.year}
<option>2025</option> disabled
>
<option>{submissionData.year}</option>
</select> </select>
<svg className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[#6B7280]" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[#6B7280]" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="6 9 12 15 18 9" /> <polyline points="6 9 12 15 18 9" />
</svg> </svg>
</div> </div>
<div className="relative"> <div className="relative">
<select className="h-10 w-32 rounded-md border border-[#E2E8F0] bg-white pl-3 pr-8 text-sm text-[#232528] focus:outline-none cursor-pointer appearance-none"> <select
<option>January</option> className="h-10 w-32 rounded-md border border-[#E2E8F0] bg-white pl-3 pr-8 text-sm text-[#232528] focus:outline-none cursor-pointer appearance-none"
<option>February</option> value={submissionData.quarter}
<option>March</option> disabled
<option>April</option> >
<option>May</option> <option>{submissionData.quarter}</option>
<option>June</option>
<option>July</option>
<option>August</option>
<option>September</option>
<option>October</option>
<option>November</option>
<option>December</option>
</select> </select>
<svg className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[#6B7280]" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[#6B7280]" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="6 9 12 15 18 9" /> <polyline points="6 9 12 15 18 9" />

View File

@ -31,8 +31,37 @@ const ProductDetails = ({ product, onBack }) => {
if (!product) return null; if (!product) return null;
// Merge product data with quarterly data // Format the product data to match the component's expected structure
const productWithDetails = { ...product, ...quarterlyData }; const formatProductData = (product) => ({
...product,
name: product.product?.product_name || '—',
hsCode: product.product?.hs_code || '—',
unit: product.unit?.uom || 'PCS',
status: product.is_active ? 'Active' : 'Inactive',
q4_oct_quantity: product.previous_quantity_period_one || '—',
q4_nov_quantity: product.previous_quantity_period_two || '—',
q4_dec_quantity: product.previous_quantity_period_three || '—',
q1_jan_quantity: product.current_quantity_period_one || '—',
q1_feb_quantity: product.current_quantity_period_two || '—',
q1_mar_quantity: product.current_quantity_period_three || '—',
q2_apr_quantity: product.forecast_quantity_period_one || '—',
q2_may_quantity: product.forecast_quantity_period_two || '—',
q2_jun_quantity: product.forecast_quantity_period_three || '—',
q4_oct_cost: product.previous_cost_period_one || '—',
q4_nov_cost: product.previous_cost_period_two || '—',
q4_dec_cost: product.previous_cost_period_three || '—',
q1_jan_cost: product.current_cost_period_one || '—',
q1_feb_cost: product.current_cost_period_two || '—',
q1_mar_cost: product.current_cost_period_three || '—',
q2_apr_cost: product.forecast_cost_period_one || '—',
q2_may_cost: product.forecast_cost_period_two || '—',
q2_jun_cost: product.forecast_cost_period_three || '—',
variationReason: product.variation_reason?.reason || '—',
zeroTargetReason: product.zero_target_reason?.reason || '—',
remarks: product.remarks || 'No remarks provided for this product.'
});
const formattedProduct = formatProductData(product);
return ( return (
<div className="space-y-6 w-full"> <div className="space-y-6 w-full">
@ -47,7 +76,7 @@ const ProductDetails = ({ product, onBack }) => {
<path d="M12.5 15L7.5 10L12.5 5" stroke="#6B7280" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/> <path d="M12.5 15L7.5 10L12.5 5" stroke="#6B7280" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg> </svg>
</button> </button>
<h1 className="text-lg font-medium text-[#232528]">Product Details</h1> <h1 className="text-lg font-medium text-[#232528]">Product Details - {formattedProduct.name}</h1>
</div> </div>
</div> </div>
</div> </div>
@ -60,7 +89,7 @@ const ProductDetails = ({ product, onBack }) => {
<label className="block text-sm font-medium text-[#6B7280]">Product Name</label> <label className="block text-sm font-medium text-[#6B7280]">Product Name</label>
<div className="mt-1"> <div className="mt-1">
<p className="block w-full rounded-md border-0 py-1.5 text-gray-900 bg-white"> <p className="block w-full rounded-md border-0 py-1.5 text-gray-900 bg-white">
{product.name} {formattedProduct.name}
</p> </p>
</div> </div>
</div> </div>
@ -68,7 +97,7 @@ const ProductDetails = ({ product, onBack }) => {
<label className="block text-sm font-medium text-[#6B7280]">HS Code</label> <label className="block text-sm font-medium text-[#6B7280]">HS Code</label>
<div className="mt-1"> <div className="mt-1">
<p className="block w-full rounded-md border-0 py-1.5 text-gray-900 bg-white"> <p className="block w-full rounded-md border-0 py-1.5 text-gray-900 bg-white">
{product.hsCode || '—'} {formattedProduct.hsCode}
</p> </p>
</div> </div>
</div> </div>
@ -76,7 +105,7 @@ const ProductDetails = ({ product, onBack }) => {
<label className="block text-sm font-medium text-[#6B7280]">Unit</label> <label className="block text-sm font-medium text-[#6B7280]">Unit</label>
<div className="mt-1"> <div className="mt-1">
<p className="block w-full rounded-md border-0 py-1.5 text-gray-900 bg-white"> <p className="block w-full rounded-md border-0 py-1.5 text-gray-900 bg-white">
{product.unit || 'PCS'} {formattedProduct.unit}
</p> </p>
</div> </div>
</div> </div>
@ -84,11 +113,11 @@ const ProductDetails = ({ product, onBack }) => {
<label className="block text-sm font-medium text-[#6B7280]">Status</label> <label className="block text-sm font-medium text-[#6B7280]">Status</label>
<div className="mt-1"> <div className="mt-1">
<span className={`inline-flex items-center rounded-md px-3 py-1.5 text-sm font-medium ${ <span className={`inline-flex items-center rounded-md px-3 py-1.5 text-sm font-medium ${
product.status === 'Active' formattedProduct.status === 'Active'
? 'bg-green-50 text-green-700 ring-1 ring-inset ring-green-600/20' ? 'bg-green-50 text-green-700 ring-1 ring-inset ring-green-600/20'
: 'bg-yellow-50 text-yellow-700 ring-1 ring-inset ring-yellow-600/20' : 'bg-yellow-50 text-yellow-700 ring-1 ring-inset ring-yellow-600/20'
}`}> }`}>
{product.status || '—'} {formattedProduct.status}
</span> </span>
</div> </div>
</div> </div>
@ -132,17 +161,17 @@ const ProductDetails = ({ product, onBack }) => {
{/* Quantity Row */} {/* Quantity Row */}
<tr> <tr>
<td className="border border-gray-300 py-2 px-3 text-left font-medium"> <td className="border border-gray-300 py-2 px-3 text-left font-medium">
Quantity ({product.unit || 'units'}) Quantity ({formattedProduct.unit})
</td> </td>
<td className="border border-gray-300 py-2">{product.q4_oct_quantity || '—'}</td> <td className="border border-gray-300 py-2">{formattedProduct.q4_oct_quantity}</td>
<td className="border border-gray-300 py-2">{product.q4_nov_quantity || '—'}</td> <td className="border border-gray-300 py-2">{formattedProduct.q4_nov_quantity}</td>
<td className="border border-gray-300 py-2">{product.q4_dec_quantity || '—'}</td> <td className="border border-gray-300 py-2">{formattedProduct.q4_dec_quantity}</td>
<td className="border border-gray-300 py-2">{product.q1_jan_quantity || '—'}</td> <td className="border border-gray-300 py-2">{formattedProduct.q1_jan_quantity}</td>
<td className="border border-gray-300 py-2">{product.q1_feb_quantity || '—'}</td> <td className="border border-gray-300 py-2">{formattedProduct.q1_feb_quantity}</td>
<td className="border border-gray-300 py-2">{product.q1_mar_quantity || '—'}</td> <td className="border border-gray-300 py-2">{formattedProduct.q1_mar_quantity}</td>
<td className="border border-gray-300 py-2">{product.q2_apr_quantity || '—'}</td> <td className="border border-gray-300 py-2">{formattedProduct.q2_apr_quantity}</td>
<td className="border border-gray-300 py-2">{product.q2_may_quantity || '—'}</td> <td className="border border-gray-300 py-2">{formattedProduct.q2_may_quantity}</td>
<td className="border border-gray-300 py-2">{product.q2_jun_quantity || '—'}</td> <td className="border border-gray-300 py-2">{formattedProduct.q2_jun_quantity}</td>
</tr> </tr>
{/* Cost Row */} {/* Cost Row */}
@ -150,15 +179,15 @@ const ProductDetails = ({ product, onBack }) => {
<td className="border border-gray-300 py-2 px-3 text-left font-medium"> <td className="border border-gray-300 py-2 px-3 text-left font-medium">
Cost (AED) Cost (AED)
</td> </td>
<td className="border border-gray-300 py-2">{product.q4_oct_cost || '—'}</td> <td className="border border-gray-300 py-2">{formattedProduct.q4_oct_cost}</td>
<td className="border border-gray-300 py-2">{product.q4_nov_cost || '—'}</td> <td className="border border-gray-300 py-2">{formattedProduct.q4_nov_cost}</td>
<td className="border border-gray-300 py-2">{product.q4_dec_cost || '—'}</td> <td className="border border-gray-300 py-2">{formattedProduct.q4_dec_cost}</td>
<td className="border border-gray-300 py-2">{product.q1_jan_cost || '—'}</td> <td className="border border-gray-300 py-2">{formattedProduct.q1_jan_cost}</td>
<td className="border border-gray-300 py-2">{product.q1_feb_cost || '—'}</td> <td className="border border-gray-300 py-2">{formattedProduct.q1_feb_cost}</td>
<td className="border border-gray-300 py-2">{product.q1_mar_cost || '—'}</td> <td className="border border-gray-300 py-2">{formattedProduct.q1_mar_cost}</td>
<td className="border border-gray-300 py-2">{product.q2_apr_cost || '—'}</td> <td className="border border-gray-300 py-2">{formattedProduct.q2_apr_cost}</td>
<td className="border border-gray-300 py-2">{product.q2_may_cost || '—'}</td> <td className="border border-gray-300 py-2">{formattedProduct.q2_may_cost}</td>
<td className="border border-gray-300 py-2">{product.q2_jun_cost || '—'}</td> <td className="border border-gray-300 py-2">{formattedProduct.q2_jun_cost}</td>
</tr> </tr>
{/* Reason Current */} {/* Reason Current */}
@ -170,7 +199,7 @@ const ProductDetails = ({ product, onBack }) => {
-- -- -- --
</td> </td>
<td colSpan="3" className="border border-gray-300 py-2 text-center bg-[#F3FAF4] text-[#2F663C]"> <td colSpan="3" className="border border-gray-300 py-2 text-center bg-[#F3FAF4] text-[#2F663C]">
{product.variationReason || '—'} {formattedProduct.variationReason}
</td> </td>
<td colSpan="3" className="border border-gray-300 py-2 text-center"> <td colSpan="3" className="border border-gray-300 py-2 text-center">
-- -- -- --
@ -186,7 +215,7 @@ const ProductDetails = ({ product, onBack }) => {
-- -- -- --
</td> </td>
<td colSpan="3" className="border border-gray-300 py-2 text-center bg-[#FFF7E9] text-[#F29F0E]"> <td colSpan="3" className="border border-gray-300 py-2 text-center bg-[#FFF7E9] text-[#F29F0E]">
{product.zeroTargetReason || '—'} {formattedProduct.zeroTargetReason}
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -197,7 +226,7 @@ const ProductDetails = ({ product, onBack }) => {
<div className="mt-6"> <div className="mt-6">
<h3 className="text-sm font-medium text-[#232528] mb-2">Remarks</h3> <h3 className="text-sm font-medium text-[#232528] mb-2">Remarks</h3>
<div className="border border-[#F3B340] rounded-md p-3 text-sm bg-white"> <div className="border border-[#F3B340] rounded-md p-3 text-sm bg-white">
{product.remarks || 'No remarks provided for this product.'} {formattedProduct.remarks}
</div> </div>
</div> </div>
@ -205,16 +234,22 @@ const ProductDetails = ({ product, onBack }) => {
<div className="mt-6 pt-4 border-t border-[#E2E8F0] text-sm"> <div className="mt-6 pt-4 border-t border-[#E2E8F0] text-sm">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<p className="text-[#6B7280]">Submitted by: <span className="text-[#232528] font-medium">User Name</span></p> <p className="text-[#6B7280]">Submitted by: <span className="text-[#232528] font-medium">
<p className="text-[#6B7280]">Submission Date: <span className="text-[#232528] font-medium">{product.submissionDate || '—'}</span></p> {product.created_by || 'N/A'}
</span></p>
<p className="text-[#6B7280]">Submission Date: <span className="text-[#232528] font-medium">
{new Date(product.created_at).toLocaleDateString() || '—'}
</span></p>
</div> </div>
<div> <div>
<p className="text-[#6B7280]">Last Updated: <span className="text-[#232528] font-medium">{product.lastUpdated || product.submissionDate || '—'}</span></p> <p className="text-[#6B7280]">Last Updated: <span className="text-[#232528] font-medium">
{product.updated_at ? new Date(product.updated_at).toLocaleDateString() : '—'}
</span></p>
<p className="text-[#6B7280]">Status: <p className="text-[#6B7280]">Status:
<span className={`inline-flex items-center justify-center rounded-md px-2 py-0.5 text-xs font-medium ml-1 ${ <span className={`inline-flex items-center justify-center rounded-md px-2 py-0.5 text-xs font-medium ml-1 ${
product.status === 'Active' ? 'text-[#2F663C] bg-[#F3FAF4]' : 'text-[#B45309] bg-[#FEF2F2]' formattedProduct.status === 'Active' ? 'text-[#2F663C] bg-[#F3FAF4]' : 'text-[#B45309] bg-[#FEF2F2]'
}`}> }`}>
{product.status} {formattedProduct.status}
</span> </span>
</p> </p>
</div> </div>

View File

@ -290,11 +290,57 @@ const ProductData = ({
onProductsChange(products.filter((p) => p.id !== id)); onProductsChange(products.filter((p) => p.id !== id));
}; };
const updateProductField = (id, field, value) => { const updateProductField = (id, field, value, options = []) => {
onProductsChange( onProductsChange(
products.map((product) => products.map((product) => {
product.id === id ? { ...product, [field]: value } : product if (product.id !== id) return product;
)
// When product is selected, store both ID and name
if (field === 'product') {
const selectedProduct = options.find(p => p.value === value || p.id === value);
return {
...product,
product: value,
productName: selectedProduct?.label || selectedProduct?.name || '',
[field]: value
};
}
// When unit is selected, store both ID and name
if (field === 'unit') {
const selectedUnit = options.find(u => u.value === value || u.id === value);
return {
...product,
unit: value,
unitName: selectedUnit?.label || selectedUnit?.name || '',
[field]: value
};
}
// When variation reason is selected, store both ID and name
if (field === 'variationReason') {
const selectedReason = options.find(r => r.value === value || r.id === value);
return {
...product,
variationReason: value,
variationReasonName: selectedReason?.label || selectedReason?.name || '',
[field]: value
};
}
// When zero target reason is selected, store both ID and name
if (field === 'zeroTargetReason') {
const selectedReason = options.find(r => r.value === value || r.id === value);
return {
...product,
zeroTargetReason: value,
zeroTargetReasonName: selectedReason?.label || selectedReason?.name || '',
[field]: value
};
}
return { ...product, [field]: value };
})
); );
}; };
@ -467,7 +513,7 @@ const ProductData = ({
placeholder={isLoadingProducts ? 'Loading products...' : 'Select product HS code'} placeholder={isLoadingProducts ? 'Loading products...' : 'Select product HS code'}
options={productOptions} options={productOptions}
value={p.product || ''} value={p.product || ''}
onChange={(e) => updateProductField(p.id, 'product', e.target.value)} onChange={(e) => updateProductField(p.id, 'product', e.target.value, productOptions)}
/> />
{productsError && productOptions.length === 0 && ( {productsError && productOptions.length === 0 && (
<p className="mt-1 text-xs text-red-600">{productsError}</p> <p className="mt-1 text-xs text-red-600">{productsError}</p>
@ -479,7 +525,7 @@ const ProductData = ({
placeholder={isLoadingUnits ? 'Loading units...' : 'Select unit'} placeholder={isLoadingUnits ? 'Loading units...' : 'Select unit'}
options={unitOptions} options={unitOptions}
value={p.unit || ''} value={p.unit || ''}
onChange={(e) => updateProductField(p.id, 'unit', e.target.value)} onChange={(e) => updateProductField(p.id, 'unit', e.target.value, unitOptions)}
/> />
{unitsError && unitOptions.length === 0 && ( {unitsError && unitOptions.length === 0 && (
<p className="mt-1 text-xs text-red-600">{unitsError}</p> <p className="mt-1 text-xs text-red-600">{unitsError}</p>
@ -646,7 +692,7 @@ const ProductData = ({
placeholder={isLoadingReasons ? 'Loading reasons...' : 'Select reason'} placeholder={isLoadingReasons ? 'Loading reasons...' : 'Select reason'}
options={variationReasons} options={variationReasons}
value={p.variationReason || ''} value={p.variationReason || ''}
onChange={(e) => updateProductField(p.id, 'variationReason', e.target.value)} onChange={(e) => updateProductField(p.id, 'variationReason', e.target.value, variationReasons)}
/> />
{reasonsError && variationReasons.length === 0 && ( {reasonsError && variationReasons.length === 0 && (
<p className="mt-1 text-xs text-red-600">{reasonsError}</p> <p className="mt-1 text-xs text-red-600">{reasonsError}</p>
@ -676,6 +722,7 @@ const ProductData = ({
type="number" type="number"
required required
placeholder="Enter" placeholder="Enter"
value={p.mayQuantity || ''}
onChange={(e) => updateProductField(p.id, 'mayQuantity', e.target.value)} onChange={(e) => updateProductField(p.id, 'mayQuantity', e.target.value)}
/> />
</div> </div>
@ -686,6 +733,7 @@ const ProductData = ({
type="number" type="number"
required required
placeholder="Enter" placeholder="Enter"
value={p.junQuantity || ''}
onChange={(e) => updateProductField(p.id, 'junQuantity', e.target.value)} onChange={(e) => updateProductField(p.id, 'junQuantity', e.target.value)}
/> />
</div> </div>
@ -731,7 +779,7 @@ const ProductData = ({
placeholder={isLoadingZeroReasons ? 'Loading reasons...' : 'Select reason'} placeholder={isLoadingZeroReasons ? 'Loading reasons...' : 'Select reason'}
options={zeroTargetReasons} options={zeroTargetReasons}
value={p.zeroTargetReason || ''} value={p.zeroTargetReason || ''}
onChange={(e) => updateProductField(p.id, 'zeroTargetReason', e.target.value)} onChange={(e) => updateProductField(p.id, 'zeroTargetReason', e.target.value, zeroTargetReasons)}
/> />
{zeroReasonsError && zeroTargetReasons.length === 0 && ( {zeroReasonsError && zeroTargetReasons.length === 0 && (
<p className="mt-1 text-xs text-red-600">{zeroReasonsError}</p> <p className="mt-1 text-xs text-red-600">{zeroReasonsError}</p>

View File

@ -179,15 +179,16 @@ const buildProductRows = (products = [], options = {}) => {
console.log("productName",productName) console.log("productName",productName)
const productCode = fullProduct?.code || product.productCode || product.code || product.product?.code || product.id || `CODE-${index + 1}`; const productCode = fullProduct?.code || product.productCode || product.code || product.product?.code || product.id || `CODE-${index + 1}`;
console.log("productCode",productCode) console.log("productCode",productCode)
// Find unit display name // Use stored unitName if available, otherwise try to find it in unitOptions
const unitnames = findDisplayName(unitOptions) let unitName = product.unitName;
console.log("unitnames",unitnames) if (!unitName && product.unit) {
const unitName = findDisplayName(unitOptions, product.unit); unitName = findDisplayName(unitOptions, product.unit);
}
console.log("unitName", unitName) console.log("unitName", unitName)
// Find reason display names // Use stored reason names if available, otherwise try to find them in the options
const variationReasonName = findDisplayName(variationReasons, product.variationReason); const variationReasonName = product.variationReasonName || findDisplayName(variationReasons, product.variationReason);
const zeroTargetReasonName = findDisplayName(zeroTargetReasons, product.zeroTargetReason); const zeroTargetReasonName = product.zeroTargetReasonName || findDisplayName(zeroTargetReasons, product.zeroTargetReason);
const capacity = product.capacity !== undefined && product.capacity !== null ? formatNumber(product.capacity) : '—'; const capacity = product.capacity !== undefined && product.capacity !== null ? formatNumber(product.capacity) : '—';
@ -226,7 +227,7 @@ const buildProductRows = (products = [], options = {}) => {
// Reasons and remarks - use display names // Reasons and remarks - use display names
variationReason: variationReasonName, variationReason: variationReasonName,
zeroTargetReason: zeroTargetReasonName, zeroTargetReason: zeroTargetReasonName,
remarks: product.remarks || '', remarks: product.remarks || '',
// Keep the raw product data for debugging // Keep the raw product data for debugging
_raw: { ...product } _raw: { ...product }
@ -574,11 +575,11 @@ const ReviewSubmit = ({
<section key={product.id || index} className="border border-gray-200 rounded-md bg-white p-4 mb-6 shadow-sm"> <section key={product.id || index} className="border border-gray-200 rounded-md bg-white p-4 mb-6 shadow-sm">
{/* Header */} {/* Header */}
<div className="flex justify-between items-start mb-4"> <div className="flex justify-between items-start mb-4">
<h3 className="text-sm font-semibold text-[#2E2E2E]"> <h3 className="text-sm font-bold text-[#232528]">
<span className="text-gray-500">Product:</span>{' '} <span className="font-bold">Product:</span>{' '}
<span className="font-normal">{product.product}</span> <span className="font-normal">{product.product}</span>
{product.productCode && product.productCode !== product.product && ( {product.productCode && product.productCode !== product.product && (
<span className="text-gray-400 text-xs ml-2">(ID: {product.productCode})</span> <span className="text-gray-400 text-xs ml-2"></span>
)} )}
</h3> </h3>
<div className="flex gap-3"> <div className="flex gap-3">
@ -668,7 +669,7 @@ const ReviewSubmit = ({
-- -- -- --
</td> </td>
<td colSpan="3" className="border border-gray-300 py-2 text-center bg-[#F3FAF4] text-[#2F663C]"> <td colSpan="3" className="border border-gray-300 py-2 text-center bg-[#F3FAF4] text-[#2F663C]">
{product.variationReason || '—'} {product.variationReasonName || product.variationReason || '—'}
</td> </td>
<td colSpan="3" className="border border-gray-300 py-2 text-center"> <td colSpan="3" className="border border-gray-300 py-2 text-center">
-- -- -- --
@ -684,7 +685,7 @@ const ReviewSubmit = ({
-- -- -- --
</td> </td>
<td colSpan="3" className="border border-gray-300 py-2 text-center bg-[#FFF7E9] text-[#F29F0E]"> <td colSpan="3" className="border border-gray-300 py-2 text-center bg-[#FFF7E9] text-[#F29F0E]">
{product.zeroTargetReason || '—'} {product.zeroTargetReasonName || product.zeroTargetReason || '—'}
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -692,12 +693,17 @@ const ReviewSubmit = ({
</div> </div>
{/* Remarks */} {/* Remarks */}
<div className="mt-4 text-xs text-gray-600"> <div className="mt-4">
<p className="font-semibold mb-1">Remarks:</p> <p className="text-sm font-semibold text-[#2E2E2E] mb-2">Remarks</p>
<div className="border border-[#F3B340] rounded-md p-2 text-sm bg-white"> <div className="border border-[#CBA344] rounded-md p-3 text-sm w-[50%] min-h-[60px]">
{product.remarks || 'No remarks provided'} {product.remarks?.trim() ? (
product.remarks
) : (
<span className="text-gray-400 ">Add Short Note..</span>
)}
</div> </div>
</div> </div>
</section> </section>
))} ))}

View File

@ -58,6 +58,20 @@ const History = () => {
return date.toLocaleDateString('en-GB'); return date.toLocaleDateString('en-GB');
}, []); }, []);
const formatDateTime = React.useCallback((value) => {
if (!value) return '—';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return '—';
return date.toLocaleString('en-GB', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: true
});
}, []);
React.useEffect(() => { React.useEffect(() => {
let cancelled = false; let cancelled = false;
const loadHistory = async () => { const loadHistory = async () => {
@ -86,15 +100,14 @@ const History = () => {
}, []); }, []);
const headers = [ const headers = [
{ name: 'Year', className: 'text-left' }, { name: 'Submission Date & Time', className: 'text-left whitespace-nowrap w-[140px]' },
{ name: 'Quarter', className: 'text-left' }, { name: 'Year', className: 'text-left w-[60px]' },
{ name: 'Submission Window', className: 'text-left' }, { name: 'Quarter', className: 'text-left w-[70px]' },
{ name: 'Total Products', className: 'text-center' }, { name: 'HS Code', className: 'text-left w-[80px]' },
{ name: 'Products Submitted', className: 'text-center' }, { name: 'Product', className: 'text-left w-[120px]' },
{ name: 'Total Cost (AED)', className: 'text-right' }, { name: 'Status', className: 'text-center w-[100px]' },
{ name: 'Status', className: 'text-center' }, { name: 'Actors', className: 'text-left w-[150px]' },
{ name: 'Date Submitted', className: 'text-left' }, { name: 'Details', className: 'text-left flex-1 min-w-[200px]' }
{ name: 'Actions', className: 'text-center' }
]; ];
const statusOptions = React.useMemo( const statusOptions = React.useMemo(
@ -107,6 +120,7 @@ const History = () => {
], ],
[] []
); );
const handleViewDetails = React.useCallback((submission) => { const handleViewDetails = React.useCallback((submission) => {
if (!submission) return; if (!submission) return;
navigate('/survey', { state: { submission, initialStep: 3 } }); navigate('/survey', { state: { submission, initialStep: 3 } });
@ -135,22 +149,83 @@ const History = () => {
}); });
}, [rows, searchTerm, statusFilter, toVariant, formatStatus, formatDate]); }, [rows, searchTerm, statusFilter, toVariant, formatStatus, formatDate]);
const tableRows = filteredRows.map((entry) => [ const formatNumber = (num) => {
if (num === null || num === undefined) return '—';
return Number(num).toLocaleString('en-IN');
};
// 🟨 Added for expand/collapse logic
const [expandedRow, setExpandedRow] = React.useState(null);
const handleToggleDetails = (rowIndex) => {
setExpandedRow(expandedRow === rowIndex ? null : rowIndex);
};
// 🟩 Updated tableRows with expandable details
const tableRows = filteredRows.map((entry, index) => {
const isExpanded = expandedRow === index;
return [
formatDateTime(entry.created_at),
entry.year || '—', entry.year || '—',
entry.quarter || '—', entry.quarter || '—',
entry.submission_window || '—', entry.hs_code || '—',
Number.isFinite(Number(entry.total_products)) ? entry.total_products : '—', entry.product_name || '—',
Number.isFinite(Number(entry.products_submitted)) ? entry.products_submitted : '—',
entry.total_cost ? Intl.NumberFormat('en-US').format(entry.total_cost) : '—',
formatStatus(entry.status), formatStatus(entry.status),
formatDate(entry.created_at), entry.actors ? entry.actors.join(', ') : '—',
'View Details', <div className="flex flex-col w-full">
]); <div className="flex items-center justify-between text-sm text-gray-800">
<span>
Quantity updated 1,450 to 1,500; Cost updated 87,000 to 90,000
</span>
<button
onClick={() => handleToggleDetails(index)}
className="flex items-center justify-center w-5 h-5 border border-gray-300 rounded hover:bg-gray-100"
>
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={`transition-transform duration-200 ${isExpanded ? 'rotate-180' : ''}`}
>
<path
d="M6 9L12 15L18 9"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
</div>
{isExpanded && (
<div className="mt-2 ml-2 border border-gray-200 rounded p-2 bg-gray-50 text-sm text-gray-700">
<div className="flex gap-6 mb-2">
<div>
<div className="font-medium">Quantity Updated:</div>
<div>1,450 1,500</div>
</div>
<div>
<div className="font-medium">Cost Updated:</div>
<div>87,000 90,000</div>
</div>
</div>
<div>
<div className="font-medium">Reason for variations:</div>
<div>Seasonal Production Surge</div>
</div>
</div>
)}
</div>
];
});
const toolbar = ( const toolbar = (
<div className="px-6 py-4 flex flex-col gap-3 md:flex-row md:items-center md:justify-between"> <div className="px-6 py-4 flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<h2 className="text-[18px] leading-[28px] font-medium text-[#232528]">Quarter Overview</h2> <h2 className="text-[18px] leading-[28px] font-medium text-[#232528]">Submission History</h2>
<div className="flex gap-3 text-sm text-gray-500"> <div className="flex gap-3 text-sm text-gray-500">
{loading && <span>Loading</span>} {loading && <span>Loading</span>}
{error && <span className="text-red-600">{error}</span>} {error && <span className="text-red-600">{error}</span>}
@ -218,20 +293,121 @@ const History = () => {
</div> </div>
); );
// Calculate total products and average cost for the card
const totalProducts = rows.length;
const averageCost = rows.length > 0
? Math.round(rows.reduce((sum, row) => sum + (parseFloat(row.total_cost) || 0), 0) / rows.length)
: 0;
return ( return (
<div className="min-h-screen bg-[#F7F7F7]"> <div className="min-h-screen bg-[#F7F7F7]">
<HeaderBar /> <HeaderBar />
<div className="mx-auto max-w-7xl px-6 py-10"> <div className="mx-auto max-w-7xl px-6 py-10">
{/* Card Section */}
<div className="bg-white p-4 rounded-t-lg border-b border-[#E2E8F0] mb-6">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2 pr-6">
<p className="text-sm text-[#232528] font-medium">Establishment ID:</p>
<p className="text-sm text-[#232528]">{totalProducts}</p>
</div>
{/* <div className="flex items-center space-x-2 pl-6">
<p className="text-sm text-[#232528] font-medium">Average Cost (AED):</p>
<p className="text-sm text-[#232528]">{averageCost.toLocaleString('en-IN')}</p>
</div> */}
</div>
<div className="flex items-center space-x-4">
<div className="relative">
<select
className="h-10 w-32 rounded-md border border-[#E2E8F0] bg-white pl-3 pr-8 text-sm text-[#232528] focus:outline-none cursor-pointer appearance-none"
>
<option value="">Select Year</option>
<option value="2024">2024</option>
<option value="2023">2023</option>
<option value="2022">2022</option>
<option value="2021">2021</option>
</select>
<svg className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[#6B7280]" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="6 9 12 15 18 9" />
</svg>
</div>
<div className="relative">
<select
className="h-10 w-32 rounded-md border border-[#E2E8F0] bg-white pl-3 pr-8 text-sm text-[#232528] focus:outline-none cursor-pointer appearance-none"
>
<option value="">Select Quarter</option>
<option value="Q1">Q1</option>
<option value="Q2">Q2</option>
<option value="Q3">Q3</option>
<option value="Q4">Q4</option>
</select>
<svg className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[#6B7280]" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="6 9 12 15 18 9" />
</svg>
</div>
<div className="relative">
<select
className="h-10 w-32 rounded-md border border-[#E2E8F0] bg-white pl-3 pr-8 text-sm text-[#232528] focus:outline-none cursor-pointer appearance-none"
>
<option value="">Select HS Code</option>
<option value="code1">HS Code 1</option>
<option value="code2">HS Code 2</option>
</select>
<svg className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[#6B7280]" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="6 9 12 15 18 9" />
</svg>
</div>
<div className="relative">
<select
className="h-10 w-32 rounded-md border border-[#E2E8F0] bg-white pl-3 pr-8 text-sm text-[#232528] focus:outline-none cursor-pointer appearance-none"
>
<option value="">Select Product</option>
<option value="product1">Product 1</option>
<option value="product2">Product 2</option>
</select>
<svg className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[#6B7280]" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="6 9 12 15 18 9" />
</svg>
</div>
<div className="relative">
<select
className="h-10 w-32 rounded-md border border-[#E2E8F0] bg-white pl-3 pr-8 text-sm text-[#232528] focus:outline-none cursor-pointer appearance-none"
>
<option value="">Select Actor</option>
<option value="actor1">Actor 1</option>
<option value="actor2">Actor 2</option>
</select>
<svg className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[#6B7280]" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="6 9 12 15 18 9" />
</svg>
</div>
<div className="relative">
<select
className="h-10 w-32 rounded-md border border-[#E2E8F0] bg-white pl-3 pr-8 text-sm text-[#232528] focus:outline-none cursor-pointer appearance-none"
>
<option value="">Select Status</option>
<option value="approved">Approved</option>
<option value="pending">Pending</option>
<option value="rejected">Rejected</option>
</select>
<svg className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[#6B7280]" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="6 9 12 15 18 9" />
</svg>
</div>
</div>
</div>
</div>
<div className="bg-white w-full rounded-lg shadow-sm ring-1 ring-gray-200"> <div className="bg-white w-full rounded-lg shadow-sm ring-1 ring-gray-200">
<Table <Table
headers={headers} headers={headers}
rows={tableRows} rows={tableRows}
beforeHeader={toolbar} beforeHeader={toolbar}
renderCell={(value, ri, ci) => { renderCell={(value, ri, ci) => {
if (ci === 6) { if (ci === 3) {
return <Badge variant={toVariant(filteredRows[ri]?.status)}>{value}</Badge>; return <Badge variant={toVariant(filteredRows[ri]?.status)}>{value}</Badge>;
} }
if (ci === headers.length - 1) { if (ci === 5) {
const submission = filteredRows[ri]; const submission = filteredRows[ri];
const disabled = !submission; const disabled = !submission;
return ( return (

View File

@ -1,9 +1,10 @@
import React, { useState } from 'react'; // src/pages/Overview/Overview.jsx
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { getSubmissions } from '@/services/submissions/submissionService';
import HeaderBar from '@/components/layout/HeaderBar'; import HeaderBar from '@/components/layout/HeaderBar';
import Table from '@/components/common/Table'; import Table from '@/components/common/Table';
import DetailedOverview from '@/components/overview/DetailedOverview'; import DetailedOverview from '@/components/overview/DetailedOverview';
const searchIconSrc = '/assets/images/material-symbols_search-rounded.svg'; const searchIconSrc = '/assets/images/material-symbols_search-rounded.svg';
const caretDownSrc = '/assets/images/CaretDown.svg'; const caretDownSrc = '/assets/images/CaretDown.svg';
const downloadIconSrc = '/assets/images/DownloadSimple.svg'; const downloadIconSrc = '/assets/images/DownloadSimple.svg';
@ -11,109 +12,101 @@ const downloadIconSrc = '/assets/images/DownloadSimple.svg';
const statusStyles = { const statusStyles = {
approved: 'text-[#2F663C] bg-[#F3FAF4]', approved: 'text-[#2F663C] bg-[#F3FAF4]',
submitted: 'text-[#003CFF] bg-[#E7F5FF]', submitted: 'text-[#003CFF] bg-[#E7F5FF]',
pending: 'text-[#B45309] bg-[#FEF2F2]', pending: 'text-[#7C5E24] bg-[#F9F7ED]',
rejected: 'text-[#B52520] bg-[#FEF2F2]', rejected: 'text-[#B52520] bg-[#FEF2F2]',
}; };
const mockOverview = [ const formatDate = (dateString) => {
{ if (!dateString) return '-';
year: '2025', const date = new Date(dateString);
quarter: 'Q4', return date.toLocaleDateString('en-GB');
window: 'Oct-Dec 2025',
totalProducts: 10,
submittedProducts: 4,
totalCost: 195000,
status: 'Approved',
submittedOn: '18/10/2025',
},
{
year: '2025',
quarter: 'Q3',
window: 'Jul-Sep 2025',
totalProducts: 10,
submittedProducts: 6,
totalCost: 295000,
status: 'Submitted',
submittedOn: '28/08/2025',
},
{
year: '2025',
quarter: 'Q2',
window: 'Apr-Jun 2025',
totalProducts: 10,
submittedProducts: 6,
totalCost: 395000,
status: 'Rejected',
submittedOn: '24/05/2025',
},
{
year: '2025',
quarter: 'Q1',
window: 'Jan-Mar 2025',
totalProducts: 10,
submittedProducts: 6,
totalCost: 495000,
status: 'Rejected',
submittedOn: '21/03/2025',
},
];
const formatCurrency = (amount) => {
if (amount === null || amount === undefined) return '—';
return new Intl.NumberFormat('en-AE').format(amount);
}; };
const Overview = () => { const Overview = () => {
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [selectedYear, setSelectedYear] = useState('');
const [selectedQuarter, setSelectedQuarter] = useState('');
const [selectedStatus, setSelectedStatus] = useState(''); const [selectedStatus, setSelectedStatus] = useState('');
const [selectedSubmission, setSelectedSubmission] = useState(null); const [selectedSubmission, setSelectedSubmission] = useState(null);
const [submissions, setSubmissions] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [pagination, setPagination] = useState({
page: 1,
limit: 100,
total: 0,
totalPages: 1
});
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => {
const fetchSubmissions = async () => {
try {
setLoading(true);
const response = await getSubmissions(pagination.page, pagination.limit);
setSubmissions(response.data);
setPagination(response.pagination);
} catch (err) {
setError('Failed to load submissions');
console.error('Error:', err);
} finally {
setLoading(false);
}
};
fetchSubmissions();
}, [pagination.page, pagination.limit]);
const filteredRows = React.useMemo(() => { const filteredRows = React.useMemo(() => {
const normalizedTerm = searchTerm.trim().toLowerCase(); const normalizedTerm = searchTerm.trim().toLowerCase();
return mockOverview.filter((record) => { return submissions.filter((record) => {
const matchesSearch = const matchesSearch =
!normalizedTerm || !normalizedTerm ||
record.year.toLowerCase().includes(normalizedTerm) || String(record.year).toLowerCase().includes(normalizedTerm) ||
record.quarter.toLowerCase().includes(normalizedTerm) || record.quarter.toLowerCase().includes(normalizedTerm) ||
record.window.toLowerCase().includes(normalizedTerm) || `${record.quarter} ${record.year}`.toLowerCase().includes(normalizedTerm) ||
record.status.toLowerCase().includes(normalizedTerm); record.status.toLowerCase().includes(normalizedTerm) ||
record.establishment?.factory_name?.toLowerCase().includes(normalizedTerm);
const matchesStatus = const matchesStatus =
selectedStatus === '' || record.status.toLowerCase() === selectedStatus; !selectedStatus ||
(selectedStatus === 'approved' && record.status === 'Approved') ||
(selectedStatus === 'submitted' && record.status === 'Submitted') ||
(selectedStatus === 'rejected' && record.status === 'Rejected');
return matchesSearch && matchesStatus; return matchesSearch && matchesStatus;
}); });
}, [searchTerm, selectedStatus]); }, [searchTerm, selectedStatus, submissions]);
const rows = filteredRows.map((record) => [ const rows = filteredRows.map((record) => [
record.year, record.year,
record.quarter, record.quarter,
record.window, `${record.quarter} ${record.year}`,
record.totalProducts, 10, // Always show 10 for Total Products
record.submittedProducts, record.product_count || 0, // Show actual submitted products count
`AED ${formatCurrency(record.totalCost)}`, `-`, // Total cost not in API
record.status, record.status,
record.submittedOn, formatDate(record.created_at),
'View Details', 'View Details',
]); ]);
const downloadCsv = React.useCallback(() => { const downloadCsv = React.useCallback(() => {
if (!filteredRows.length) return; if (!filteredRows.length) return;
const headers = ['Year', 'Quarter', 'Submission Window', 'Total Products', 'Products Submitted', 'Total Cost (AED)', 'Status', 'Date Submitted']; const headers = ['Year', 'Quarter', 'Submission Window', 'Total Products', 'Products Submitted', 'Total Cost (AED)', 'Status', 'Date Submitted'];
const csvRows = [headers.join(',')]; const csvRows = [headers.join(',')];
filteredRows.forEach((record) => { filteredRows.forEach((record) => {
csvRows.push([ csvRows.push([
record.year, record.year,
record.quarter, record.quarter,
record.window, `${record.quarter} ${record.year}`,
record.totalProducts, 10, // Always show 10 for Total Products in CSV
record.submittedProducts, record.product_count || 0, // Show actual submitted products count
record.totalCost, '-', // Total cost not in API
record.status, record.status,
record.submittedOn, formatDate(record.created_at)
].map((value) => `"${String(value).replace(/"/g, '""')}"`).join(',')); ].map((value) => `"${String(value).replace(/"/g, '""')}"`).join(','));
}); });
const blob = new Blob([csvRows.join('\n')], { type: 'text/csv;charset=utf-8;' }); const blob = new Blob([csvRows.join('\n')], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const link = document.createElement('a'); const link = document.createElement('a');
@ -136,7 +129,7 @@ const Overview = () => {
type="text" type="text"
value={searchTerm} value={searchTerm}
onChange={(event) => setSearchTerm(event.target.value)} onChange={(event) => setSearchTerm(event.target.value)}
placeholder="Search establishment" placeholder="Search submissions..."
className="w-full h-10 rounded-md border border-[#E2E8F0] pl-10 pr-3 text-sm text-[#232528] focus:outline-none" className="w-full h-10 rounded-md border border-[#E2E8F0] pl-10 pr-3 text-sm text-[#232528] focus:outline-none"
/> />
<img <img
@ -166,7 +159,11 @@ const Overview = () => {
type="button" type="button"
onClick={downloadCsv} onClick={downloadCsv}
disabled={!filteredRows.length} disabled={!filteredRows.length}
className={`inline-flex items-center gap-2 h-10 rounded-md border px-4 text-sm font-medium ${filteredRows.length ? 'border-[#92722A] text-[#92722A] hover:bg-[#F2ECCF]' : 'border-gray-200 text-gray-400 cursor-not-allowed'}`} className={`inline-flex items-center gap-2 h-10 rounded-md border px-4 text-sm font-medium ${
filteredRows.length
? 'border-[#92722A] text-[#92722A] hover:bg-[#F2ECCF]'
: 'border-gray-200 text-gray-400 cursor-not-allowed'
}`}
> >
<img src={downloadIconSrc} alt="Export" className="h-4 w-4" /> <img src={downloadIconSrc} alt="Export" className="h-4 w-4" />
<span>Export CSV</span> <span>Export CSV</span>
@ -179,6 +176,41 @@ const Overview = () => {
setSelectedSubmission(filteredRows[rowIndex]); setSelectedSubmission(filteredRows[rowIndex]);
}; };
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 justify-center items-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 (error) {
return (
<div className="min-h-screen bg-[#F8FAFC]">
<HeaderBar />
<main className="max-w-[1280px] mx-auto px-4 py-6">
<div className="bg-red-50 border-l-4 border-red-400 p-4">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<p className="text-sm text-red-700">{error}</p>
</div>
</div>
</div>
</main>
</div>
);
}
if (selectedSubmission) { if (selectedSubmission) {
return ( return (
<div className="min-h-screen bg-[#F8FAFC]"> <div className="min-h-screen bg-[#F8FAFC]">
@ -209,7 +241,7 @@ const Overview = () => {
rowGapClass="border-spacing-y-2" rowGapClass="border-spacing-y-2"
className="w-full min-w-[1200px] [&_td]:whitespace-nowrap [&_th]:whitespace-nowrap" className="w-full min-w-[1200px] [&_td]:whitespace-nowrap [&_th]:whitespace-nowrap"
renderCell={(value, rowIndex, columnIndex) => { renderCell={(value, rowIndex, columnIndex) => {
if (columnIndex === 6) { if (columnIndex === 6) { // Status column
const normalized = String(value).toLowerCase(); const normalized = String(value).toLowerCase();
const style = statusStyles[normalized] || 'text-[#232528] bg-[#F8FAFC]'; const style = statusStyles[normalized] || 'text-[#232528] bg-[#F8FAFC]';
return ( return (
@ -218,7 +250,7 @@ const Overview = () => {
</span> </span>
); );
} }
if (columnIndex === 8) { if (columnIndex === 8) { // Action column
return ( return (
<button <button
className="text-[#92722A] hover:underline text-sm font-medium" className="text-[#92722A] hover:underline text-sm font-medium"
@ -228,7 +260,7 @@ const Overview = () => {
</button> </button>
); );
} }
return <span className="inline-block">{value}</span>; return value;
}} }}
/> />
</section> </section>

View File

@ -398,6 +398,9 @@ const Survey = () => {
try { try {
const payload = buildSubmissionPayload(); const payload = buildSubmissionPayload();
const response = await submitSurvey(payload); const response = await submitSurvey(payload);
// Navigate to overview page after successful submission
navigate('/overview');
console.log('Survey submission response:', response); console.log('Survey submission response:', response);
showToast('success', response?.message || 'You have successfully submitted the survey.'); showToast('success', response?.message || 'You have successfully submitted the survey.');
setHasSubmitted(true); setHasSubmitted(true);

View File

@ -3,6 +3,24 @@ import resolveEstablishmentId from '@/services/utils/establishment';
const endpoint = '/submissions'; const endpoint = '/submissions';
// Add this new function to fetch submissions with pagination
export const getSubmissions = async (page = 1, limit = 100, config = {}) => {
try {
const response = await getRequest(endpoint, {
...config,
params: {
page,
limit,
...(config.params || {}) // Preserve any existing params
}
});
return response.data;
} catch (error) {
console.error('Error fetching submissions:', error);
throw error;
}
};
export const submitSurvey = async (payload = {}, config = {}) => { export const submitSurvey = async (payload = {}, config = {}) => {
const establishmentId = resolveEstablishmentId(payload.establishment_id); const establishmentId = resolveEstablishmentId(payload.establishment_id);
const requestBody = { const requestBody = {
@ -32,7 +50,9 @@ export const fetchSubmissionDetail = async (submissionId, config = {}) => {
return response.data; return response.data;
}; };
// Add the new function to the default export
export default { export default {
getSubmissions, // Add this line
submitSurvey, submitSurvey,
fetchSubmissionHistory, fetchSubmissionHistory,
fetchSubmissionDetail, fetchSubmissionDetail,