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 { 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" />
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|||||||
@ -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 (
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user