Added Overview screen in Establishement users
This commit is contained in:
parent
02da3fedb0
commit
95528cb84c
@ -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 { fetchSubmissionDetail } from '@/services/submissions/submissionService';
|
||||
import Table from '@/components/common/Table';
|
||||
import ProductDetails from './ProductDetails';
|
||||
|
||||
const searchIconSrc = '/assets/images/material-symbols_search-rounded.svg';
|
||||
const caretDownSrc = '/assets/images/CaretDown.svg';
|
||||
const downloadIconSrc = '/assets/images/DownloadSimple.svg';
|
||||
|
||||
export const DetailedOverview = ({ submission, onBack }) => {
|
||||
const navigate = useNavigate();
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedStatus, setSelectedStatus] = useState('');
|
||||
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
|
||||
const productData = [
|
||||
{
|
||||
id: 'PRD-2023-001',
|
||||
hsCode: '1234.56.78',
|
||||
name: 'Product 1',
|
||||
unit: 'PCS',
|
||||
quantity: 1,
|
||||
cost: '1,000.00',
|
||||
status: 'Active',
|
||||
submissionDate: '30/10/2023 14:30',
|
||||
},
|
||||
{
|
||||
id: 'PRD-2023-002',
|
||||
hsCode: '8765.43.21',
|
||||
name: 'Product 2',
|
||||
unit: 'KG',
|
||||
quantity: 5,
|
||||
cost: '2,000.00',
|
||||
status: 'Pending',
|
||||
submissionDate: '29/10/2023 10:15',
|
||||
},
|
||||
];
|
||||
// Add state for tracking loading and error states
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSubmissionDetails = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
console.log('Fetching submission details for ID:', submission.id);
|
||||
const response = await fetchSubmissionDetail(submission.id);
|
||||
|
||||
console.log('API Response:', response);
|
||||
|
||||
if (response && response.data) {
|
||||
console.log('Setting submission data:', response.data);
|
||||
setSubmissionData(response.data);
|
||||
setQuarterPeriods(response.quarter_periods);
|
||||
|
||||
// Log the products array to verify it's being set correctly
|
||||
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(() => {
|
||||
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();
|
||||
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 =
|
||||
!normalizedTerm ||
|
||||
product.name.toLowerCase().includes(normalizedTerm) ||
|
||||
product.hsCode.toLowerCase().includes(normalizedTerm) ||
|
||||
product.status.toLowerCase().includes(normalizedTerm);
|
||||
const matchesStatus =
|
||||
selectedStatus === '' ||
|
||||
product.status.toLowerCase() === selectedStatus.toLowerCase();
|
||||
productName.toString().toLowerCase().includes(normalizedTerm) ||
|
||||
hsCode.toString().toLowerCase().includes(normalizedTerm) ||
|
||||
status.toLowerCase().includes(ormalizedTerm) ||
|
||||
'pending'.includes(normalizedTerm);
|
||||
|
||||
const matchesStatus = selectedStatus === '' ||
|
||||
(selectedStatus === 'pending' && !product.is_active) ||
|
||||
(selectedStatus === 'approved' && product.is_active);
|
||||
|
||||
return matchesSearch && matchesStatus;
|
||||
});
|
||||
}, [searchTerm, selectedStatus, productData]);
|
||||
|
||||
console.log('Filtered products:', filtered);
|
||||
return filtered;
|
||||
}, [searchTerm, selectedStatus, submissionData]);
|
||||
|
||||
const downloadCsv = useCallback(() => {
|
||||
if (!filteredProducts.length) return;
|
||||
if (!filteredProducts.length || !submissionData) return;
|
||||
|
||||
const headers = [
|
||||
'HS Code',
|
||||
'Product',
|
||||
'Unit',
|
||||
'Quantity',
|
||||
'Cost (AED)',
|
||||
'Status',
|
||||
'Submission Date & Time'
|
||||
'Previous Quarter Quantity',
|
||||
'Previous Quarter Cost (AED)',
|
||||
'Current Quarter Quantity',
|
||||
'Current Quarter Cost (AED)',
|
||||
'Forecast Quarter Quantity',
|
||||
'Forecast Quarter Cost (AED)',
|
||||
'Status'
|
||||
];
|
||||
|
||||
const csvRows = [headers.join(',')];
|
||||
|
||||
filteredProducts.forEach((product) => {
|
||||
csvRows.push([
|
||||
product.hsCode,
|
||||
product.name,
|
||||
product.unit,
|
||||
product.quantity,
|
||||
product.cost,
|
||||
product.status,
|
||||
product.submissionDate
|
||||
product.product.hs_code,
|
||||
product.product.product_name,
|
||||
product.unit?.uom || 'N/A',
|
||||
product.previous_quantity || '0',
|
||||
product.previous_cost || '0',
|
||||
product.current_quantity || '0',
|
||||
product.current_cost || '0',
|
||||
product.forecast_quantity || '0',
|
||||
product.forecast_cost || '0',
|
||||
product.is_active ? 'Approved' : 'Pending'
|
||||
].map((value) => `"${String(value).replace(/"/g, '""')}"`).join(','));
|
||||
});
|
||||
|
||||
@ -89,26 +142,90 @@ export const DetailedOverview = ({ submission, onBack }) => {
|
||||
URL.revokeObjectURL(url);
|
||||
}, [filteredProducts]);
|
||||
|
||||
const rows = filteredProducts.map(product => [
|
||||
product.hsCode,
|
||||
product.name,
|
||||
product.unit,
|
||||
product.quantity,
|
||||
`AED ${product.cost}`,
|
||||
<span className={`inline-flex items-center justify-center rounded-md px-3 py-1 text-xs font-medium ${
|
||||
product.status === 'Active' ? 'text-[#2F663C] bg-[#F3FAF4]' :
|
||||
'text-[#B45309] bg-[#FEF2F2]'
|
||||
}`}>
|
||||
{product.status}
|
||||
</span>,
|
||||
product.submissionDate,
|
||||
<button
|
||||
onClick={() => setSelectedProduct(product)}
|
||||
className="text-[#92722A] hover:underline text-sm font-medium"
|
||||
>
|
||||
View Details
|
||||
</button>
|
||||
]);
|
||||
const handleViewDetails = (product) => {
|
||||
// Use the product data we already have from the submission
|
||||
setSelectedProduct(product);
|
||||
};
|
||||
|
||||
const rows = useMemo(() => {
|
||||
if (!filteredProducts || !Array.isArray(filteredProducts)) {
|
||||
console.log('No filtered products available');
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log('Generating rows for filtered products:', filteredProducts);
|
||||
|
||||
return filteredProducts.map((product, index) => {
|
||||
// Add null checks for product and product properties
|
||||
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]'
|
||||
}`}
|
||||
>
|
||||
{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>
|
||||
];
|
||||
});
|
||||
}, [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 = (
|
||||
|
||||
@ -165,18 +282,29 @@ export const DetailedOverview = ({ submission, onBack }) => {
|
||||
'HS Code',
|
||||
'Product',
|
||||
'Unit',
|
||||
'Quantity',
|
||||
'Cost (AED)',
|
||||
'Previous Qty',
|
||||
'Previous Cost (AED)',
|
||||
'Current Qty',
|
||||
'Current Cost (AED)',
|
||||
'Forecast Qty',
|
||||
'Forecast Cost (AED)',
|
||||
'Status',
|
||||
'Submission Date & Time',
|
||||
'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 (selectedProduct) {
|
||||
return (
|
||||
<ProductDetails
|
||||
product={selectedProduct}
|
||||
quarterPeriods={quarterPeriods}
|
||||
onBack={() => setSelectedProduct(null)}
|
||||
/>
|
||||
);
|
||||
@ -198,43 +326,38 @@ export const DetailedOverview = ({ submission, onBack }) => {
|
||||
</svg>
|
||||
</button>
|
||||
<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]">Al Khazna Investment</p>
|
||||
<p className="text-sm text-[#232528] font-medium">Establishment:</p>
|
||||
<p className="text-sm text-[#232528]">{submissionData.establishment?.factory_name || 'N/A'}</p>
|
||||
</div>
|
||||
<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]">678</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:</p>
|
||||
<p className="text-sm text-[#232528]">7889</p>
|
||||
<p className="text-sm text-[#232528] font-medium">Average Cost (AED):</p>
|
||||
<p className="text-sm text-[#232528]">{averageCost}</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>2023</option>
|
||||
<option>2024</option>
|
||||
<option>2025</option>
|
||||
<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"
|
||||
value={submissionData.year}
|
||||
disabled
|
||||
>
|
||||
<option>{submissionData.year}</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>January</option>
|
||||
<option>February</option>
|
||||
<option>March</option>
|
||||
<option>April</option>
|
||||
<option>May</option>
|
||||
<option>June</option>
|
||||
<option>July</option>
|
||||
<option>August</option>
|
||||
<option>September</option>
|
||||
<option>October</option>
|
||||
<option>November</option>
|
||||
<option>December</option>
|
||||
<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"
|
||||
value={submissionData.quarter}
|
||||
disabled
|
||||
>
|
||||
<option>{submissionData.quarter}</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" />
|
||||
|
||||
@ -31,8 +31,37 @@ const ProductDetails = ({ product, onBack }) => {
|
||||
|
||||
if (!product) return null;
|
||||
|
||||
// Merge product data with quarterly data
|
||||
const productWithDetails = { ...product, ...quarterlyData };
|
||||
// Format the product data to match the component's expected structure
|
||||
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 (
|
||||
<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"/>
|
||||
</svg>
|
||||
</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>
|
||||
@ -60,7 +89,7 @@ const ProductDetails = ({ product, onBack }) => {
|
||||
<label className="block text-sm font-medium text-[#6B7280]">Product Name</label>
|
||||
<div className="mt-1">
|
||||
<p className="block w-full rounded-md border-0 py-1.5 text-gray-900 bg-white">
|
||||
{product.name}
|
||||
{formattedProduct.name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -68,7 +97,7 @@ const ProductDetails = ({ product, onBack }) => {
|
||||
<label className="block text-sm font-medium text-[#6B7280]">HS Code</label>
|
||||
<div className="mt-1">
|
||||
<p className="block w-full rounded-md border-0 py-1.5 text-gray-900 bg-white">
|
||||
{product.hsCode || '—'}
|
||||
{formattedProduct.hsCode}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -76,7 +105,7 @@ const ProductDetails = ({ product, onBack }) => {
|
||||
<label className="block text-sm font-medium text-[#6B7280]">Unit</label>
|
||||
<div className="mt-1">
|
||||
<p className="block w-full rounded-md border-0 py-1.5 text-gray-900 bg-white">
|
||||
{product.unit || 'PCS'}
|
||||
{formattedProduct.unit}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -84,11 +113,11 @@ const ProductDetails = ({ product, onBack }) => {
|
||||
<label className="block text-sm font-medium text-[#6B7280]">Status</label>
|
||||
<div className="mt-1">
|
||||
<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-yellow-50 text-yellow-700 ring-1 ring-inset ring-yellow-600/20'
|
||||
}`}>
|
||||
{product.status || '—'}
|
||||
{formattedProduct.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -132,17 +161,17 @@ const ProductDetails = ({ product, onBack }) => {
|
||||
{/* Quantity Row */}
|
||||
<tr>
|
||||
<td className="border border-gray-300 py-2 px-3 text-left font-medium">
|
||||
Quantity ({product.unit || 'units'})
|
||||
Quantity ({formattedProduct.unit})
|
||||
</td>
|
||||
<td className="border border-gray-300 py-2">{product.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">{product.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">{product.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">{product.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">{product.q2_jun_quantity || '—'}</td>
|
||||
<td className="border border-gray-300 py-2">{formattedProduct.q4_oct_quantity}</td>
|
||||
<td className="border border-gray-300 py-2">{formattedProduct.q4_nov_quantity}</td>
|
||||
<td className="border border-gray-300 py-2">{formattedProduct.q4_dec_quantity}</td>
|
||||
<td className="border border-gray-300 py-2">{formattedProduct.q1_jan_quantity}</td>
|
||||
<td className="border border-gray-300 py-2">{formattedProduct.q1_feb_quantity}</td>
|
||||
<td className="border border-gray-300 py-2">{formattedProduct.q1_mar_quantity}</td>
|
||||
<td className="border border-gray-300 py-2">{formattedProduct.q2_apr_quantity}</td>
|
||||
<td className="border border-gray-300 py-2">{formattedProduct.q2_may_quantity}</td>
|
||||
<td className="border border-gray-300 py-2">{formattedProduct.q2_jun_quantity}</td>
|
||||
</tr>
|
||||
|
||||
{/* Cost Row */}
|
||||
@ -150,15 +179,15 @@ const ProductDetails = ({ product, onBack }) => {
|
||||
<td className="border border-gray-300 py-2 px-3 text-left font-medium">
|
||||
Cost (AED)
|
||||
</td>
|
||||
<td className="border border-gray-300 py-2">{product.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">{product.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">{product.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">{product.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">{product.q2_jun_cost || '—'}</td>
|
||||
<td className="border border-gray-300 py-2">{formattedProduct.q4_oct_cost}</td>
|
||||
<td className="border border-gray-300 py-2">{formattedProduct.q4_nov_cost}</td>
|
||||
<td className="border border-gray-300 py-2">{formattedProduct.q4_dec_cost}</td>
|
||||
<td className="border border-gray-300 py-2">{formattedProduct.q1_jan_cost}</td>
|
||||
<td className="border border-gray-300 py-2">{formattedProduct.q1_feb_cost}</td>
|
||||
<td className="border border-gray-300 py-2">{formattedProduct.q1_mar_cost}</td>
|
||||
<td className="border border-gray-300 py-2">{formattedProduct.q2_apr_cost}</td>
|
||||
<td className="border border-gray-300 py-2">{formattedProduct.q2_may_cost}</td>
|
||||
<td className="border border-gray-300 py-2">{formattedProduct.q2_jun_cost}</td>
|
||||
</tr>
|
||||
|
||||
{/* Reason Current */}
|
||||
@ -170,7 +199,7 @@ const ProductDetails = ({ product, onBack }) => {
|
||||
-- --
|
||||
</td>
|
||||
<td colSpan="3" className="border border-gray-300 py-2 text-center bg-[#F3FAF4] text-[#2F663C]">
|
||||
{product.variationReason || '—'}
|
||||
{formattedProduct.variationReason}
|
||||
</td>
|
||||
<td colSpan="3" className="border border-gray-300 py-2 text-center">
|
||||
-- --
|
||||
@ -186,7 +215,7 @@ const ProductDetails = ({ product, onBack }) => {
|
||||
-- --
|
||||
</td>
|
||||
<td colSpan="3" className="border border-gray-300 py-2 text-center bg-[#FFF7E9] text-[#F29F0E]">
|
||||
{product.zeroTargetReason || '—'}
|
||||
{formattedProduct.zeroTargetReason}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@ -197,7 +226,7 @@ const ProductDetails = ({ product, onBack }) => {
|
||||
<div className="mt-6">
|
||||
<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">
|
||||
{product.remarks || 'No remarks provided for this product.'}
|
||||
{formattedProduct.remarks}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -205,16 +234,22 @@ const ProductDetails = ({ product, onBack }) => {
|
||||
<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>
|
||||
<p className="text-[#6B7280]">Submitted by: <span className="text-[#232528] font-medium">User Name</span></p>
|
||||
<p className="text-[#6B7280]">Submission Date: <span className="text-[#232528] font-medium">{product.submissionDate || '—'}</span></p>
|
||||
<p className="text-[#6B7280]">Submitted by: <span className="text-[#232528] font-medium">
|
||||
{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>
|
||||
<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:
|
||||
<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>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -290,11 +290,57 @@ const ProductData = ({
|
||||
onProductsChange(products.filter((p) => p.id !== id));
|
||||
};
|
||||
|
||||
const updateProductField = (id, field, value) => {
|
||||
const updateProductField = (id, field, value, options = []) => {
|
||||
onProductsChange(
|
||||
products.map((product) =>
|
||||
product.id === id ? { ...product, [field]: value } : product
|
||||
)
|
||||
products.map((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'}
|
||||
options={productOptions}
|
||||
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 && (
|
||||
<p className="mt-1 text-xs text-red-600">{productsError}</p>
|
||||
@ -479,7 +525,7 @@ const ProductData = ({
|
||||
placeholder={isLoadingUnits ? 'Loading units...' : 'Select unit'}
|
||||
options={unitOptions}
|
||||
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 && (
|
||||
<p className="mt-1 text-xs text-red-600">{unitsError}</p>
|
||||
@ -646,7 +692,7 @@ const ProductData = ({
|
||||
placeholder={isLoadingReasons ? 'Loading reasons...' : 'Select reason'}
|
||||
options={variationReasons}
|
||||
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 && (
|
||||
<p className="mt-1 text-xs text-red-600">{reasonsError}</p>
|
||||
@ -676,6 +722,7 @@ const ProductData = ({
|
||||
type="number"
|
||||
required
|
||||
placeholder="Enter"
|
||||
value={p.mayQuantity || ''}
|
||||
onChange={(e) => updateProductField(p.id, 'mayQuantity', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
@ -686,6 +733,7 @@ const ProductData = ({
|
||||
type="number"
|
||||
required
|
||||
placeholder="Enter"
|
||||
value={p.junQuantity || ''}
|
||||
onChange={(e) => updateProductField(p.id, 'junQuantity', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
@ -731,7 +779,7 @@ const ProductData = ({
|
||||
placeholder={isLoadingZeroReasons ? 'Loading reasons...' : 'Select reason'}
|
||||
options={zeroTargetReasons}
|
||||
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 && (
|
||||
<p className="mt-1 text-xs text-red-600">{zeroReasonsError}</p>
|
||||
|
||||
@ -179,15 +179,16 @@ const buildProductRows = (products = [], options = {}) => {
|
||||
console.log("productName",productName)
|
||||
const productCode = fullProduct?.code || product.productCode || product.code || product.product?.code || product.id || `CODE-${index + 1}`;
|
||||
console.log("productCode",productCode)
|
||||
// Find unit display name
|
||||
const unitnames = findDisplayName(unitOptions)
|
||||
console.log("unitnames",unitnames)
|
||||
const unitName = findDisplayName(unitOptions, product.unit);
|
||||
console.log("unitName",unitName)
|
||||
// Use stored unitName if available, otherwise try to find it in unitOptions
|
||||
let unitName = product.unitName;
|
||||
if (!unitName && product.unit) {
|
||||
unitName = findDisplayName(unitOptions, product.unit);
|
||||
}
|
||||
console.log("unitName", unitName)
|
||||
|
||||
// Find reason display names
|
||||
const variationReasonName = findDisplayName(variationReasons, product.variationReason);
|
||||
const zeroTargetReasonName = findDisplayName(zeroTargetReasons, product.zeroTargetReason);
|
||||
// Use stored reason names if available, otherwise try to find them in the options
|
||||
const variationReasonName = product.variationReasonName || findDisplayName(variationReasons, product.variationReason);
|
||||
const zeroTargetReasonName = product.zeroTargetReasonName || findDisplayName(zeroTargetReasons, product.zeroTargetReason);
|
||||
|
||||
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
|
||||
variationReason: variationReasonName,
|
||||
zeroTargetReason: zeroTargetReasonName,
|
||||
remarks: product.remarks || '—',
|
||||
remarks: product.remarks || '',
|
||||
|
||||
// Keep the raw product data for debugging
|
||||
_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">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<h3 className="text-sm font-semibold text-[#2E2E2E]">
|
||||
<span className="text-gray-500">Product:</span>{' '}
|
||||
<h3 className="text-sm font-bold text-[#232528]">
|
||||
<span className="font-bold">Product:</span>{' '}
|
||||
<span className="font-normal">{product.product}</span>
|
||||
{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>
|
||||
<div className="flex gap-3">
|
||||
@ -668,7 +669,7 @@ const ReviewSubmit = ({
|
||||
-- --
|
||||
</td>
|
||||
<td colSpan="3" className="border border-gray-300 py-2 text-center bg-[#F3FAF4] text-[#2F663C]">
|
||||
{product.variationReason || '—'}
|
||||
{product.variationReasonName || product.variationReason || '—'}
|
||||
</td>
|
||||
<td colSpan="3" className="border border-gray-300 py-2 text-center">
|
||||
-- --
|
||||
@ -684,20 +685,25 @@ const ReviewSubmit = ({
|
||||
-- --
|
||||
</td>
|
||||
<td colSpan="3" className="border border-gray-300 py-2 text-center bg-[#FFF7E9] text-[#F29F0E]">
|
||||
{product.zeroTargetReason || '—'}
|
||||
{product.zeroTargetReasonName || product.zeroTargetReason || '—'}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Remarks */}
|
||||
<div className="mt-4 text-xs text-gray-600">
|
||||
<p className="font-semibold mb-1">Remarks:</p>
|
||||
<div className="border border-[#F3B340] rounded-md p-2 text-sm bg-white">
|
||||
{product.remarks || 'No remarks provided'}
|
||||
</div>
|
||||
</div>
|
||||
{/* Remarks */}
|
||||
<div className="mt-4">
|
||||
<p className="text-sm font-semibold text-[#2E2E2E] mb-2">Remarks</p>
|
||||
<div className="border border-[#CBA344] rounded-md p-3 text-sm w-[50%] min-h-[60px]">
|
||||
{product.remarks?.trim() ? (
|
||||
product.remarks
|
||||
) : (
|
||||
<span className="text-gray-400 ">Add Short Note..</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
))}
|
||||
|
||||
|
||||
@ -58,6 +58,20 @@ const History = () => {
|
||||
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(() => {
|
||||
let cancelled = false;
|
||||
const loadHistory = async () => {
|
||||
@ -86,15 +100,14 @@ const History = () => {
|
||||
}, []);
|
||||
|
||||
const headers = [
|
||||
{ name: 'Year', className: 'text-left' },
|
||||
{ name: 'Quarter', className: 'text-left' },
|
||||
{ name: 'Submission Window', className: 'text-left' },
|
||||
{ name: 'Total Products', className: 'text-center' },
|
||||
{ name: 'Products Submitted', className: 'text-center' },
|
||||
{ name: 'Total Cost (AED)', className: 'text-right' },
|
||||
{ name: 'Status', className: 'text-center' },
|
||||
{ name: 'Date Submitted', className: 'text-left' },
|
||||
{ name: 'Actions', className: 'text-center' }
|
||||
{ name: 'Submission Date & Time', className: 'text-left whitespace-nowrap w-[140px]' },
|
||||
{ name: 'Year', className: 'text-left w-[60px]' },
|
||||
{ name: 'Quarter', className: 'text-left w-[70px]' },
|
||||
{ name: 'HS Code', className: 'text-left w-[80px]' },
|
||||
{ name: 'Product', className: 'text-left w-[120px]' },
|
||||
{ name: 'Status', className: 'text-center w-[100px]' },
|
||||
{ name: 'Actors', className: 'text-left w-[150px]' },
|
||||
{ name: 'Details', className: 'text-left flex-1 min-w-[200px]' }
|
||||
];
|
||||
|
||||
const statusOptions = React.useMemo(
|
||||
@ -107,6 +120,7 @@ const History = () => {
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const handleViewDetails = React.useCallback((submission) => {
|
||||
if (!submission) return;
|
||||
navigate('/survey', { state: { submission, initialStep: 3 } });
|
||||
@ -135,22 +149,83 @@ const History = () => {
|
||||
});
|
||||
}, [rows, searchTerm, statusFilter, toVariant, formatStatus, formatDate]);
|
||||
|
||||
const tableRows = filteredRows.map((entry) => [
|
||||
entry.year || '—',
|
||||
entry.quarter || '—',
|
||||
entry.submission_window || '—',
|
||||
Number.isFinite(Number(entry.total_products)) ? entry.total_products : '—',
|
||||
Number.isFinite(Number(entry.products_submitted)) ? entry.products_submitted : '—',
|
||||
entry.total_cost ? Intl.NumberFormat('en-US').format(entry.total_cost) : '—',
|
||||
formatStatus(entry.status),
|
||||
formatDate(entry.created_at),
|
||||
'View Details',
|
||||
]);
|
||||
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.quarter || '—',
|
||||
entry.hs_code || '—',
|
||||
entry.product_name || '—',
|
||||
formatStatus(entry.status),
|
||||
entry.actors ? entry.actors.join(', ') : '—',
|
||||
<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 = (
|
||||
<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">
|
||||
<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">
|
||||
{loading && <span>Loading…</span>}
|
||||
{error && <span className="text-red-600">{error}</span>}
|
||||
@ -218,20 +293,121 @@ const History = () => {
|
||||
</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 (
|
||||
<div className="min-h-screen bg-[#F7F7F7]">
|
||||
<HeaderBar />
|
||||
<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">
|
||||
<Table
|
||||
headers={headers}
|
||||
rows={tableRows}
|
||||
beforeHeader={toolbar}
|
||||
renderCell={(value, ri, ci) => {
|
||||
if (ci === 6) {
|
||||
if (ci === 3) {
|
||||
return <Badge variant={toVariant(filteredRows[ri]?.status)}>{value}</Badge>;
|
||||
}
|
||||
if (ci === headers.length - 1) {
|
||||
if (ci === 5) {
|
||||
const submission = filteredRows[ri];
|
||||
const disabled = !submission;
|
||||
return (
|
||||
|
||||
@ -1,119 +1,112 @@
|
||||
import React, { useState } from 'react';
|
||||
// src/pages/Overview/Overview.jsx
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { getSubmissions } from '@/services/submissions/submissionService';
|
||||
import HeaderBar from '@/components/layout/HeaderBar';
|
||||
import Table from '@/components/common/Table';
|
||||
import DetailedOverview from '@/components/overview/DetailedOverview';
|
||||
|
||||
const searchIconSrc = '/assets/images/material-symbols_search-rounded.svg';
|
||||
const caretDownSrc = '/assets/images/CaretDown.svg';
|
||||
const downloadIconSrc = '/assets/images/DownloadSimple.svg';
|
||||
|
||||
const statusStyles = {
|
||||
approved: 'text-[#2F663C] bg-[#F3FAF4] ',
|
||||
approved: 'text-[#2F663C] bg-[#F3FAF4]',
|
||||
submitted: 'text-[#003CFF] bg-[#E7F5FF]',
|
||||
pending: 'text-[#B45309] bg-[#FEF2F2]',
|
||||
pending: 'text-[#7C5E24] bg-[#F9F7ED]',
|
||||
rejected: 'text-[#B52520] bg-[#FEF2F2]',
|
||||
};
|
||||
|
||||
const mockOverview = [
|
||||
{
|
||||
year: '2025',
|
||||
quarter: 'Q4',
|
||||
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 formatDate = (dateString) => {
|
||||
if (!dateString) return '-';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-GB');
|
||||
};
|
||||
|
||||
const Overview = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedYear, setSelectedYear] = useState('');
|
||||
const [selectedQuarter, setSelectedQuarter] = useState('');
|
||||
const [selectedStatus, setSelectedStatus] = useState('');
|
||||
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();
|
||||
|
||||
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 normalizedTerm = searchTerm.trim().toLowerCase();
|
||||
return mockOverview.filter((record) => {
|
||||
return submissions.filter((record) => {
|
||||
const matchesSearch =
|
||||
!normalizedTerm ||
|
||||
record.year.toLowerCase().includes(normalizedTerm) ||
|
||||
String(record.year).toLowerCase().includes(normalizedTerm) ||
|
||||
record.quarter.toLowerCase().includes(normalizedTerm) ||
|
||||
record.window.toLowerCase().includes(normalizedTerm) ||
|
||||
record.status.toLowerCase().includes(normalizedTerm);
|
||||
`${record.quarter} ${record.year}`.toLowerCase().includes(normalizedTerm) ||
|
||||
record.status.toLowerCase().includes(normalizedTerm) ||
|
||||
record.establishment?.factory_name?.toLowerCase().includes(normalizedTerm);
|
||||
|
||||
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;
|
||||
});
|
||||
}, [searchTerm, selectedStatus]);
|
||||
}, [searchTerm, selectedStatus, submissions]);
|
||||
|
||||
const rows = filteredRows.map((record) => [
|
||||
record.year,
|
||||
record.quarter,
|
||||
record.window,
|
||||
record.totalProducts,
|
||||
record.submittedProducts,
|
||||
`AED ${formatCurrency(record.totalCost)}`,
|
||||
`${record.quarter} ${record.year}`,
|
||||
10, // Always show 10 for Total Products
|
||||
record.product_count || 0, // Show actual submitted products count
|
||||
`-`, // Total cost not in API
|
||||
record.status,
|
||||
record.submittedOn,
|
||||
formatDate(record.created_at),
|
||||
'View Details',
|
||||
]);
|
||||
|
||||
const downloadCsv = React.useCallback(() => {
|
||||
if (!filteredRows.length) return;
|
||||
|
||||
const headers = ['Year', 'Quarter', 'Submission Window', 'Total Products', 'Products Submitted', 'Total Cost (AED)', 'Status', 'Date Submitted'];
|
||||
const csvRows = [headers.join(',')];
|
||||
|
||||
filteredRows.forEach((record) => {
|
||||
csvRows.push([
|
||||
record.year,
|
||||
record.quarter,
|
||||
record.window,
|
||||
record.totalProducts,
|
||||
record.submittedProducts,
|
||||
record.totalCost,
|
||||
`${record.quarter} ${record.year}`,
|
||||
10, // Always show 10 for Total Products in CSV
|
||||
record.product_count || 0, // Show actual submitted products count
|
||||
'-', // Total cost not in API
|
||||
record.status,
|
||||
record.submittedOn,
|
||||
formatDate(record.created_at)
|
||||
].map((value) => `"${String(value).replace(/"/g, '""')}"`).join(','));
|
||||
});
|
||||
|
||||
const blob = new Blob([csvRows.join('\n')], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
@ -136,7 +129,7 @@ const Overview = () => {
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
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"
|
||||
/>
|
||||
<img
|
||||
@ -166,7 +159,11 @@ const Overview = () => {
|
||||
type="button"
|
||||
onClick={downloadCsv}
|
||||
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" />
|
||||
<span>Export CSV</span>
|
||||
@ -179,6 +176,41 @@ const Overview = () => {
|
||||
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) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8FAFC]">
|
||||
@ -209,16 +241,16 @@ const Overview = () => {
|
||||
rowGapClass="border-spacing-y-2"
|
||||
className="w-full min-w-[1200px] [&_td]:whitespace-nowrap [&_th]:whitespace-nowrap"
|
||||
renderCell={(value, rowIndex, columnIndex) => {
|
||||
if (columnIndex === 6) {
|
||||
if (columnIndex === 6) { // Status column
|
||||
const normalized = String(value).toLowerCase();
|
||||
const style = statusStyles[normalized] || 'text-[#232528] bg-[#F8FAFC] ';
|
||||
const style = statusStyles[normalized] || 'text-[#232528] bg-[#F8FAFC]';
|
||||
return (
|
||||
<span className={`inline-flex items-center justify-center rounded-md px-3 py-1 text-xs font-medium ${style}`}>
|
||||
{value}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (columnIndex === 8) {
|
||||
if (columnIndex === 8) { // Action column
|
||||
return (
|
||||
<button
|
||||
className="text-[#92722A] hover:underline text-sm font-medium"
|
||||
@ -228,7 +260,7 @@ const Overview = () => {
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return <span className="inline-block">{value}</span>;
|
||||
return value;
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
@ -237,4 +269,4 @@ const Overview = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Overview;
|
||||
export default Overview;
|
||||
@ -398,6 +398,9 @@ const Survey = () => {
|
||||
try {
|
||||
const payload = buildSubmissionPayload();
|
||||
const response = await submitSurvey(payload);
|
||||
|
||||
// Navigate to overview page after successful submission
|
||||
navigate('/overview');
|
||||
console.log('Survey submission response:', response);
|
||||
showToast('success', response?.message || 'You have successfully submitted the survey.');
|
||||
setHasSubmitted(true);
|
||||
|
||||
@ -3,6 +3,24 @@ import resolveEstablishmentId from '@/services/utils/establishment';
|
||||
|
||||
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 = {}) => {
|
||||
const establishmentId = resolveEstablishmentId(payload.establishment_id);
|
||||
const requestBody = {
|
||||
@ -32,8 +50,10 @@ export const fetchSubmissionDetail = async (submissionId, config = {}) => {
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// Add the new function to the default export
|
||||
export default {
|
||||
getSubmissions, // Add this line
|
||||
submitSurvey,
|
||||
fetchSubmissionHistory,
|
||||
fetchSubmissionDetail,
|
||||
};
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user