bug fixed in establishemnet
This commit is contained in:
parent
74174618db
commit
0e65ba4d13
@ -0,0 +1,3 @@
|
||||
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.125 0C6.51803 0 4.94714 0.476523 3.611 1.36931C2.27485 2.2621 1.23344 3.53105 0.618482 5.0157C0.00352044 6.50035 -0.157382 8.13401 0.156123 9.71011C0.469628 11.2862 1.24346 12.7339 2.37976 13.8702C3.51606 15.0065 4.9638 15.7804 6.5399 16.0939C8.11599 16.4074 9.74966 16.2465 11.2343 15.6315C12.719 15.0166 13.9879 13.9752 14.8807 12.639C15.7735 11.3029 16.25 9.73197 16.25 8.125C16.2477 5.97081 15.391 3.90551 13.8677 2.38227C12.3445 0.85903 10.2792 0.00227486 8.125 0ZM7.5 4.375C7.5 4.20924 7.56585 4.05027 7.68306 3.93306C7.80027 3.81585 7.95924 3.75 8.125 3.75C8.29076 3.75 8.44974 3.81585 8.56695 3.93306C8.68416 4.05027 8.75 4.20924 8.75 4.375V8.75C8.75 8.91576 8.68416 9.07473 8.56695 9.19194C8.44974 9.30915 8.29076 9.375 8.125 9.375C7.95924 9.375 7.80027 9.30915 7.68306 9.19194C7.56585 9.07473 7.5 8.91576 7.5 8.75V4.375ZM8.125 12.5C7.93958 12.5 7.75833 12.445 7.60416 12.342C7.44999 12.239 7.32982 12.0926 7.25887 11.9213C7.18791 11.75 7.16934 11.5615 7.20552 11.3796C7.24169 11.1977 7.33098 11.0307 7.46209 10.8996C7.5932 10.7685 7.76025 10.6792 7.94211 10.643C8.12396 10.6068 8.31246 10.6254 8.48377 10.6964C8.65507 10.7673 8.80149 10.8875 8.90451 11.0417C9.00752 11.1958 9.0625 11.3771 9.0625 11.5625C9.0625 11.8111 8.96373 12.0496 8.78792 12.2254C8.6121 12.4012 8.37364 12.5 8.125 12.5Z" fill="#EA4F49"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@ -77,8 +77,9 @@ const Table = ({
|
||||
const safeCurrentPage = enablePagination ? Math.min(Math.max(currentPage, 1), totalPages) : 1;
|
||||
const startIndex = enablePagination ? (safeCurrentPage - 1) * pageSize : 0;
|
||||
const endIndex = enablePagination ? startIndex + pageSize : rows.length;
|
||||
// Calculate range start and end, handling the case when there are no items
|
||||
const rangeStart = totalItems === 0 ? 0 : startIndex + 1;
|
||||
const rangeEnd = enablePagination ? Math.min(endIndex, totalItems) : rows.length;
|
||||
const rangeEnd = totalItems === 0 ? 0 : (enablePagination ? Math.min(endIndex, totalItems) : rows.length);
|
||||
|
||||
const displayedRows = React.useMemo(() => {
|
||||
if (!enablePagination) return rows;
|
||||
|
||||
@ -489,6 +489,15 @@ export const DetailedOverview = ({ submission, onBack }) => {
|
||||
pagination={tablePagination}
|
||||
/>
|
||||
</section>
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="inline-flex items-center gap-2 h-9 px-4 rounded-md border border-[#92722A] text-[#92722A] bg-transparent hover:bg-[#92722A]/10"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<span>Back</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -37,10 +37,12 @@ const ProductDetails = ({ product, onBack, showToast: shouldShowToast, onToastSh
|
||||
rejected: {
|
||||
type: 'rejected',
|
||||
bgColor: 'bg-red-50',
|
||||
textColor: '#D83731',
|
||||
textColor: 'text-red-600',
|
||||
iconColor: 'text-red-500',
|
||||
message: 'Your survey has been rejected. Please review and resubmit.',
|
||||
// icon: 'warning-circle-fill-1.svg'
|
||||
icon: '/assets/images/Vector-rejected.svg',
|
||||
iconAlt: 'Rejected',
|
||||
iconClassName: 'mr-2 h-5 w-5 flex-shrink-0'
|
||||
},
|
||||
submitted: {
|
||||
type: 'submitted',
|
||||
@ -146,18 +148,11 @@ const ProductDetails = ({ product, onBack, showToast: shouldShowToast, onToastSh
|
||||
/>
|
||||
</svg>
|
||||
) : toast.type === 'rejected' ? (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
className={`w-5 h-5 mr-2 ${toast.iconColor}`}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<img
|
||||
src={toast.icon}
|
||||
alt={toast.iconAlt || 'Rejected'}
|
||||
className={toast.iconClassName || 'w-5 h-5 mr-2'}
|
||||
/>
|
||||
) : (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -205,7 +200,7 @@ const ProductDetails = ({ product, onBack, showToast: shouldShowToast, onToastSh
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow-sm border border-[#E2E8F0] p-6">
|
||||
<div className="bg-white rounded-lg shadow-sm border border-[#E2E8F0] p-6 mt-3">
|
||||
{/* Basic Information */}
|
||||
<div className="mb-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-4">
|
||||
@ -353,7 +348,7 @@ const ProductDetails = ({ product, onBack, showToast: shouldShowToast, onToastSh
|
||||
{formattedProduct.remarks}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Submission Info */}
|
||||
{/* <div className="mt-6 pt-4 border-t border-[#E2E8F0] text-sm">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@ -380,6 +375,15 @@ const ProductDetails = ({ product, onBack, showToast: shouldShowToast, onToastSh
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="inline-flex items-center gap-2 h-9 px-4 mt-8 rounded-md border border-[#92722A] text-[#92722A] bg-transparent hover:bg-[#92722A]/10"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<span>Back</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -178,12 +178,25 @@ const ProductData = ({
|
||||
const [unitsError, setUnitsError] = React.useState('');
|
||||
const [activeTab, setActiveTab] = React.useState('current');
|
||||
const [isInitialLoad, setIsInitialLoad] = React.useState(true);
|
||||
const [remarks, setRemarks] = React.useState('');
|
||||
const [remarks, setRemarks] = React.useState(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const savedRemarks = localStorage.getItem('surveyRemarks');
|
||||
return savedRemarks || '';
|
||||
}
|
||||
return '';
|
||||
});
|
||||
const [formErrors, setFormErrors] = React.useState({});
|
||||
const [quarterPeriods, setQuarterPeriods] = useState(null);
|
||||
const [isLoadingPeriods, setIsLoadingPeriods] = useState(false);
|
||||
|
||||
// Fetch quarter periods when component mounts or quarter/year changes
|
||||
const handleRemarksChange = (e) => {
|
||||
const newRemarks = e.target.value;
|
||||
setRemarks(newRemarks);
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('surveyRemarks', newRemarks);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchQuarterPeriods = async () => {
|
||||
if (!quarter || !year) return;
|
||||
@ -1239,7 +1252,7 @@ const ProductData = ({
|
||||
<Textarea
|
||||
placeholder="Enter your remarks here..."
|
||||
value={remarks}
|
||||
onChange={(e) => setRemarks(e.target.value)}
|
||||
onChange={handleRemarksChange}
|
||||
className="min-h-[60px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -254,7 +254,13 @@ const ReviewSubmit = ({
|
||||
hasSubmitted = false,
|
||||
}) => {
|
||||
const [confirm, setConfirm] = React.useState(false);
|
||||
const [remarks, setRemarks] = React.useState('');
|
||||
const [remarks, setRemarks] = React.useState(() => {
|
||||
// Load remarks from localStorage on component mount
|
||||
if (typeof window !== 'undefined') {
|
||||
return localStorage.getItem('surveyRemarks') || '';
|
||||
}
|
||||
return '';
|
||||
});
|
||||
const establishmentDetails = React.useMemo(() => buildEstablishmentDetails(establishment), [establishment]);
|
||||
const employmentDetails = React.useMemo(() => buildEmploymentDetails(establishment), [establishment]);
|
||||
const productRows = React.useMemo(() => buildProductRows(products), [products]);
|
||||
@ -598,6 +604,15 @@ const ReviewSubmit = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Remarks Section */}
|
||||
{/* {remarks && (
|
||||
<Card title="Remarks" className="mb-6">
|
||||
<div className="p-4 bg-gray-50 rounded-md">
|
||||
<p className="text-gray-700 whitespace-pre-line">{remarks}</p>
|
||||
</div>
|
||||
</Card>
|
||||
)} */}
|
||||
|
||||
{/* Table */}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full border border-gray-300 text-sm text-center">
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import React, { useEffect, useState, useMemo,useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import Table from '@/components/common/Table';
|
||||
import HeaderBar from '@/components/layout/HeaderBar';
|
||||
import { fetchSubmissionHistory } from '@/services/submissions/submissionService.js';
|
||||
// import { fetchSubmissionHistory } from '@/services/submissions/submissionService.js';
|
||||
import { fetchProducts } from '@/services/masters/masterService';
|
||||
import { fetchSubmissionHistory, getSubmissionAuditHistory } from '@/services/submissions/submissionService';
|
||||
|
||||
const searchIconSrc = '/assets/images/material-symbols_search-rounded.svg';
|
||||
const BreadcrumbVector = '/assets/images/Breadcrumb-vector.svg';
|
||||
@ -35,6 +36,11 @@ const History = () => {
|
||||
const [hscodeFilter, setHscodeFilter] = React.useState('');
|
||||
const [hscodeOptions, setHscodeOptions] = React.useState([]);
|
||||
const [isLoadingHscodes, setIsLoadingHscodes] = React.useState(false);
|
||||
const [auditHistory, setAuditHistory] = useState([]);
|
||||
const [isLoadingAudit, setIsLoadingAudit] = useState(false);
|
||||
const [actorOptions, setActorOptions] = useState([]);
|
||||
const [selectedActor, setSelectedActor] = useState('');
|
||||
|
||||
const [pagination, setPagination] = React.useState({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
@ -59,6 +65,105 @@ const History = () => {
|
||||
loadHscodeOptions();
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
// useEffect(() => {
|
||||
// const loadAuditHistory = async () => {
|
||||
// setIsLoadingAudit(true);
|
||||
// try {
|
||||
// const establishmentId = 41; // Replace with dynamic ID if available
|
||||
// const response = await getSubmissionAuditHistory(establishmentId, {
|
||||
// year: yearFilter || undefined,
|
||||
// quarter: quarterFilter || undefined,
|
||||
// product_id: hscodeFilter || undefined
|
||||
// });
|
||||
// setAuditHistory(response?.data || []);
|
||||
// setPagination(prev => ({
|
||||
// ...prev,
|
||||
// totalItems: response?.data?.length,
|
||||
// currentPage: 1,
|
||||
// }));
|
||||
// } catch (error) {
|
||||
// console.error('Error loading audit history:', error);
|
||||
// setAuditHistory([]);
|
||||
// } finally {
|
||||
// setIsLoadingAudit(false);
|
||||
// }
|
||||
// };
|
||||
|
||||
// loadAuditHistory();
|
||||
// }, [yearFilter, quarterFilter, hscodeFilter]);
|
||||
useEffect(() => {
|
||||
const loadAuditHistory = async () => {
|
||||
setIsLoadingAudit(true);
|
||||
try {
|
||||
const establishmentId = 41; // Replace with dynamic ID if available
|
||||
|
||||
// Extract product_name from selected value ("HS - Product" → "Product")
|
||||
const productName = hscodeFilter.includes(" - ")
|
||||
? hscodeFilter.split(" - ")[1].trim()
|
||||
: hscodeFilter.trim();
|
||||
|
||||
console.log("📦 Loading audit history with:", {
|
||||
yearFilter,
|
||||
quarterFilter,
|
||||
productName,
|
||||
});
|
||||
|
||||
const params = {
|
||||
year: yearFilter || undefined,
|
||||
quarter: quarterFilter || undefined,
|
||||
product_name: productName || undefined,
|
||||
};
|
||||
|
||||
const response = await getSubmissionAuditHistory(establishmentId, params);
|
||||
|
||||
const data = response?.data || [];
|
||||
setAuditHistory(data);
|
||||
|
||||
const actors = [...new Set(data.map(item => item.actor).filter(Boolean))];
|
||||
setActorOptions(actors.map(actor => ({
|
||||
value: actor,
|
||||
label: actor
|
||||
})));
|
||||
|
||||
setPagination((prev) => ({
|
||||
...prev,
|
||||
totalItems: data.length,
|
||||
currentPage: 1,
|
||||
}));
|
||||
|
||||
console.log("✅ Loaded audit history rows:", data.length);
|
||||
} catch (error) {
|
||||
console.error(" Error loading audit history:", error);
|
||||
setAuditHistory([]);
|
||||
} finally {
|
||||
setIsLoadingAudit(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadAuditHistory();
|
||||
}, [yearFilter, quarterFilter, hscodeFilter , searchTerm , selectedActor]);
|
||||
|
||||
|
||||
const formatExportDateTime = (value) => {
|
||||
if (!value) return '-';
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return '-';
|
||||
|
||||
// Format as DD/MM/YYYY, HH:MM AM/PM
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const year = date.getFullYear();
|
||||
let hours = date.getHours();
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const ampm = hours >= 12 ? 'PM' : 'AM';
|
||||
hours = hours % 12;
|
||||
hours = hours ? hours : 12; // the hour '0' should be '12'
|
||||
|
||||
return `${day}/${month}/${year}, ${hours}:${minutes} ${ampm}`;
|
||||
};
|
||||
|
||||
const toVariant = React.useCallback((status) => {
|
||||
const text = String(status || '').toLowerCase();
|
||||
if (text === 'approved') return 'approved';
|
||||
@ -169,66 +274,45 @@ const History = () => {
|
||||
const statusOptions = [
|
||||
{ value: 'all', label: 'All Status' },
|
||||
{ value: 'approved', label: 'Approved' },
|
||||
{ value: 'pending', label: 'Pending' },
|
||||
{ value: 'rejected', label: 'Rejected' },
|
||||
{ value: 'submitted', label: 'Submitted' },
|
||||
];
|
||||
|
||||
const filteredRows = React.useMemo(() => {
|
||||
const term = searchTerm.trim().toLowerCase();
|
||||
const statusValue = statusFilter.toLowerCase();
|
||||
|
||||
const filtered = rows.filter((entry) => {
|
||||
// Status filter
|
||||
const matchesStatus =
|
||||
statusValue === 'all' ||
|
||||
toVariant(entry.status) === statusValue ||
|
||||
formatStatus(entry.status).toLowerCase() === statusValue;
|
||||
|
||||
// Year filter
|
||||
const matchesYear = !yearFilter || String(entry.year) === yearFilter;
|
||||
|
||||
// Quarter filter
|
||||
const matchesQuarter = !quarterFilter ||
|
||||
(entry.quarter && entry.quarter.toUpperCase() === quarterFilter.toUpperCase());
|
||||
|
||||
// HS Code/Product filter
|
||||
const matchesHscode = !hscodeFilter ||
|
||||
(entry.hs_code && entry.hs_code.toString() === hscodeFilter) ||
|
||||
(entry.product_id && entry.product_id.toString() === hscodeFilter);
|
||||
|
||||
// Search term filter
|
||||
if (term) {
|
||||
const haystack = [
|
||||
entry.year,
|
||||
entry.quarter,
|
||||
entry.submission_window,
|
||||
entry.total_products,
|
||||
entry.products_submitted,
|
||||
entry.total_cost,
|
||||
formatStatus(entry.status),
|
||||
formatDate(entry.created_at),
|
||||
entry.hs_code,
|
||||
entry.product_name
|
||||
]
|
||||
.map((val) => String(val || '').toLowerCase())
|
||||
.join(' ');
|
||||
|
||||
return matchesStatus && matchesYear && matchesQuarter && matchesHscode && haystack.includes(term);
|
||||
}
|
||||
|
||||
return matchesStatus && matchesYear && matchesQuarter && matchesHscode;
|
||||
});
|
||||
|
||||
// Update total items when filtered rows change
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
totalItems: filtered.length,
|
||||
currentPage: 1, // Reset to first page when filter changes
|
||||
}));
|
||||
|
||||
return filtered;
|
||||
}, [rows, searchTerm, statusFilter, yearFilter, quarterFilter, hscodeFilter, toVariant, formatStatus, formatDate]);
|
||||
const filteredRows = useMemo(() => {
|
||||
const term = searchTerm.trim().toLowerCase();
|
||||
|
||||
const filtered = auditHistory.filter((entry) => {
|
||||
const matchesSearch =
|
||||
!term ||
|
||||
Object.values(entry)
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
.includes(term);
|
||||
|
||||
const matchesYear = !yearFilter || String(entry.year) === yearFilter;
|
||||
const matchesQuarter = !quarterFilter || entry.quarter?.toUpperCase() === quarterFilter.toUpperCase();
|
||||
|
||||
const matchesHscode = (() => {
|
||||
if (!hscodeFilter) return true;
|
||||
const productName = hscodeFilter.includes(' - ')
|
||||
? hscodeFilter.split(' - ')[1].trim()
|
||||
: hscodeFilter.trim();
|
||||
return entry.product_name?.toLowerCase().includes(productName.toLowerCase());
|
||||
})();
|
||||
|
||||
return matchesSearch && matchesYear && matchesQuarter && matchesHscode;
|
||||
});
|
||||
|
||||
setPagination((prev) => ({
|
||||
...prev,
|
||||
totalItems: filtered.length,
|
||||
currentPage: 1,
|
||||
}));
|
||||
|
||||
return filtered;
|
||||
}, [auditHistory, searchTerm, yearFilter, quarterFilter, hscodeFilter]);
|
||||
|
||||
|
||||
|
||||
const formatNumber = (num) => {
|
||||
if (num === null || num === undefined) return '—';
|
||||
@ -242,19 +326,29 @@ const History = () => {
|
||||
};
|
||||
|
||||
// 🟩 Updated tableRows with expandable details
|
||||
const handlePageChange = (newPage) => {
|
||||
const handlePageChange = React.useCallback((page) => {
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
currentPage: newPage,
|
||||
currentPage: page,
|
||||
}));
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handlePageSizeChange = (newSize) => {
|
||||
const handlePageSizeChange = React.useCallback((newSize) => {
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
pageSize: newSize,
|
||||
currentPage: 1, // Reset to first page when page size changes
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// Calculate pagination range text
|
||||
const getPaginationRangeText = () => {
|
||||
if (filteredRows.length === 0) {
|
||||
return '0-0 of 0';
|
||||
}
|
||||
const start = (pagination.currentPage - 1) * pagination.pageSize + 1;
|
||||
const end = Math.min(start + pagination.pageSize - 1, pagination.totalItems);
|
||||
return `${start}-${end} of ${pagination.totalItems}`;
|
||||
};
|
||||
|
||||
const downloadCsv = () => {
|
||||
@ -281,18 +375,76 @@ const History = () => {
|
||||
// Convert data to CSV rows
|
||||
const csvRows = [
|
||||
headers.join(','),
|
||||
...filteredRows.map(row =>
|
||||
[
|
||||
getFieldValue(formatDateTime(row.created_at)),
|
||||
getFieldValue(row.year),
|
||||
getFieldValue(row.quarter),
|
||||
getFieldValue(row.hs_code),
|
||||
getFieldValue(row.product_name),
|
||||
getFieldValue(formatStatus(row.status)),
|
||||
(row.actors && row.actors.length ? row.actors.join(', ') : '-').replace(/"/g, '""'),
|
||||
'Quantity and cost updates' // This is a placeholder for the details column
|
||||
].map(field => `"${field}"`).join(',')
|
||||
)
|
||||
...filteredRows.map(row => {
|
||||
// Get actor(s) - handle both array and single value
|
||||
let actors = '';
|
||||
if (Array.isArray(row.actor)) {
|
||||
actors = row.actor.filter(a => a && a.trim() !== '').join(', ');
|
||||
} else if (row.actor) {
|
||||
actors = row.actor;
|
||||
} else {
|
||||
actors = '-';
|
||||
}
|
||||
|
||||
// Calculate quantity and cost changes from details
|
||||
const details = row.details || [];
|
||||
const quantityChange = details
|
||||
.filter(d => d.column.includes('quantity'))
|
||||
.reduce((sum, d) => sum + (Number(d.new) - Number(d.old)), 0);
|
||||
|
||||
const costChange = details
|
||||
.filter(d => d.column.includes('cost'))
|
||||
.reduce((sum, d) => sum + (Number(d.new) - Number(d.old)), 0);
|
||||
|
||||
// Format the summary line with actual values
|
||||
const summaryParts = [];
|
||||
|
||||
if (details.some(d => d.column.includes('quantity'))) {
|
||||
const change = details
|
||||
.filter(d => d.column.includes('quantity'))
|
||||
.reduce((sum, d) => sum + (Number(d.new) - Number(d.old)), 0);
|
||||
|
||||
if (change !== 0) {
|
||||
summaryParts.push(`Quantity updated: ${change > 0 ? '+' : ''}${change.toLocaleString('en-IN')}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (details.some(d => d.column.includes('cost'))) {
|
||||
const change = details
|
||||
.filter(d => d.column.includes('cost'))
|
||||
.reduce((sum, d) => sum + (Number(d.new) - Number(d.old)), 0);
|
||||
|
||||
if (change !== 0) {
|
||||
const absChange = Math.abs(change);
|
||||
const sign = change > 0 ? '+' : '-';
|
||||
summaryParts.push(`Cost updated: ${sign}AED ${absChange.toLocaleString('en-IN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
})}`);
|
||||
}
|
||||
}
|
||||
|
||||
const displayText = summaryParts.length > 0
|
||||
? summaryParts.join('; ')
|
||||
: 'No changes';
|
||||
|
||||
// Create the row with proper escaping
|
||||
const rowData = [
|
||||
formatExportDateTime(row.created_at || row.submission_date) || '-',
|
||||
row.year || '-',
|
||||
row.quarter || '-',
|
||||
row.hs_code || '-',
|
||||
row.product_name || '-',
|
||||
formatStatus(row.status) || '-',
|
||||
actors,
|
||||
displayText // Use the summary line here instead of detailsText
|
||||
];
|
||||
|
||||
// Properly escape and quote CSV fields
|
||||
return rowData
|
||||
.map(field => `"${String(field || '').replace(/"/g, '""')}"`)
|
||||
.join(',');
|
||||
})
|
||||
];
|
||||
|
||||
// Create CSV content
|
||||
@ -312,66 +464,98 @@ const History = () => {
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
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 -; Cost updated -
|
||||
</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>
|
||||
const tableRows = auditHistory.map((entry, index) => {
|
||||
const isExpanded = expandedRow === index;
|
||||
const details = entry.details || [];
|
||||
|
||||
// Check if there are quantity or cost changes
|
||||
const hasQuantityChanges = details.some(d => d.column.includes('quantity'));
|
||||
const hasCostChanges = details.some(d => d.column.includes('cost'));
|
||||
|
||||
{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>-</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium">Cost Updated:</div>
|
||||
<div>-</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium">Reason for variations:</div>
|
||||
<div>-</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
return [
|
||||
formatDateTime(entry.submission_date || entry.created_at),
|
||||
entry.year || '—',
|
||||
entry.quarter || '—',
|
||||
entry.hs_code || '—',
|
||||
entry.product_name || '—',
|
||||
<div className="flex justify-center">
|
||||
<Badge variant={toVariant(entry.status)}>
|
||||
{formatStatus(entry.status)}
|
||||
</Badge>
|
||||
</div>,
|
||||
entry.actor || '—',
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="flex items-center justify-between text-sm pt-2 text-gray-800">
|
||||
<span>
|
||||
{hasQuantityChanges ? 'Quantity updated' : ''}
|
||||
{hasQuantityChanges && hasCostChanges ? '; ' : ''}
|
||||
{hasCostChanges ? 'Cost updated' : ''}
|
||||
{!hasQuantityChanges && !hasCostChanges ? 'No changes' : ''}
|
||||
</span>
|
||||
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
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 && details.length > 0 && (
|
||||
<div className="mt-2 ml-1 border border-gray-200 rounded p-2 bg-gray-50 text-sm text-gray-700">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div className="font-medium">Quantity Updated: </div>
|
||||
<div>
|
||||
{details
|
||||
.filter(d => d.column.includes('quantity'))
|
||||
.reduce((sum, d) => sum + (Number(d.new) - Number(d.old)), 0)
|
||||
.toLocaleString('en-IN')}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium">Cost Updated:</div>
|
||||
<div>
|
||||
{details
|
||||
.filter(d => d.column.includes('cost'))
|
||||
.reduce((sum, d) => sum + (Number(d.new) - Number(d.old)), 0)
|
||||
.toLocaleString('en-IN', {
|
||||
style: 'currency',
|
||||
currency: 'AED',
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium">Reason for variations: -</div>
|
||||
<div>
|
||||
</div>
|
||||
</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">
|
||||
@ -386,11 +570,16 @@ const History = () => {
|
||||
<div className="relative w-full md:w-56">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
placeholder="Search submissions"
|
||||
className="w-full h-10 rounded-md border border-[#E5E7EB] pl-10 pr-3 text-sm focus:outline-none"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setSearchTerm(value);
|
||||
console.log("🔍 Searching for:", value);
|
||||
}}
|
||||
className="w-64 h-10 border border-gray-300 rounded-md px-3 text-sm focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
/>
|
||||
|
||||
<img src={searchIconSrc} alt="Search" className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5" />
|
||||
</div>
|
||||
<div className="relative w-full md:w-40">
|
||||
@ -512,35 +701,60 @@ const History = () => {
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
</div>
|
||||
{/* Replace both HS Code and Product dropdowns with this */}
|
||||
<div className="relative">
|
||||
<select
|
||||
value={hscodeFilter}
|
||||
onChange={(e) => {
|
||||
console.log("HS filter selected:", e.target.value);
|
||||
setHscodeFilter(e.target.value);
|
||||
}}
|
||||
className="h-10 w-48 rounded-md border border-[#E2E8F0] bg-white pl-3 pr-8 text-sm text-[#232528] focus:outline-none cursor-pointer appearance-none"
|
||||
>
|
||||
<option value="">HS Codes & Products</option>
|
||||
{hscodeOptions.map((option) => (
|
||||
<option
|
||||
key={option.id}
|
||||
value={option.product_name} // ✅ this is the fix
|
||||
>
|
||||
{option.hs_code
|
||||
? `${option.hs_code} - ${option.product_name || ""}`.trim()
|
||||
: option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{isLoadingHscodes ? (
|
||||
<div className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 border-2 border-t-[#6B7280] border-r-transparent border-b-transparent border-l-transparent rounded-full animate-spin"></div>
|
||||
) : (
|
||||
<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
|
||||
value={hscodeFilter}
|
||||
onChange={(e) => setHscodeFilter(e.target.value)}
|
||||
disabled={isLoadingHscodes}
|
||||
className={`h-[40px] w-[170px] rounded-md border border-[#E2E8F0] bg-white pl-3 pr-8 text-sm text-[#232528] focus:outline-none cursor-pointer appearance-none ${isLoadingHscodes ? 'opacity-70' : ''}`}
|
||||
>
|
||||
<option value="">HS Code/Product</option>
|
||||
{hscodeOptions.map((option) => (
|
||||
<option key={option.id || option.value} value={option.id || option.value}>
|
||||
{option.hs_code ? `${option.hs_code} - ${option.product_name || ''}` : option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{isLoadingHscodes ? (
|
||||
<div className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 border-2 border-t-[#6B7280] border-r-transparent border-b-transparent border-l-transparent rounded-full animate-spin"></div>
|
||||
) : (
|
||||
<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"
|
||||
disabled
|
||||
>
|
||||
<option value="">Actor</option>
|
||||
</select>
|
||||
<select
|
||||
value={selectedActor}
|
||||
onChange={(e) => setSelectedActor(e.target.value)}
|
||||
className="h-10 rounded-md border border-[#E2E8F0] bg-white pl-3 pr-8 text-sm text-[#232528] focus:outline-none cursor-pointer appearance-none"
|
||||
>
|
||||
<option value="">Actors</option>
|
||||
{actorOptions.map((actor) => (
|
||||
<option key={actor.value} value={actor.value}>
|
||||
{actor.label}
|
||||
</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>
|
||||
@ -553,7 +767,6 @@ const History = () => {
|
||||
>
|
||||
<option value="all">Status</option>
|
||||
<option value="approved">Approved</option>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="rejected">Rejected</option>
|
||||
<option value="submitted">Submitted</option>
|
||||
</select>
|
||||
@ -576,7 +789,7 @@ const History = () => {
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
placeholder="Search establishment"
|
||||
placeholder="Search Product"
|
||||
className="w-full h-10 rounded-md border border-[#E2E8F0] pl-10 pr-3 text-sm text-[#232528] focus:outline-none"
|
||||
/>
|
||||
<img
|
||||
@ -599,10 +812,23 @@ const History = () => {
|
||||
</div>
|
||||
</div>
|
||||
{filteredRows.length === 0 && !loading ? (
|
||||
<div className="flex flex-col items-center justify-center py-12">
|
||||
<p className="text-gray-500 text-lg">No details found</p>
|
||||
{/* <p className="text-gray-400 text-sm mt-1">Try adjusting your search or filter to find what you're looking for.</p> */}
|
||||
</div>
|
||||
<div className="w-full flex flex-col h-5items-center justify-start pt-1 pb-7 text-center">
|
||||
<div className="w-full max-w-md mx-auto">
|
||||
{/* <svg
|
||||
className="w-16 h-16 text-gray-400 mx-auto mb-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
|
||||
</svg> */}
|
||||
<h3 className="text-lg font-medium text-[#92722A] pt-4 text-center">No submissions History found</h3>
|
||||
{/* <p className="mt-1 text-sm text-gray-500">
|
||||
No submission history is available for the current filters
|
||||
</p> */}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Table
|
||||
headers={headers}
|
||||
@ -622,20 +848,20 @@ const History = () => {
|
||||
if (ci === 5) { // Status column
|
||||
return <Badge variant={toVariant(filteredRows[ri]?.status)}>{formatStatus(filteredRows[ri]?.status)}</Badge>;
|
||||
}
|
||||
if (ci === 6) {
|
||||
const submission = filteredRows[ri];
|
||||
const disabled = !submission;
|
||||
return (
|
||||
<button
|
||||
className={`text-[#92722A] hover:underline ${disabled ? 'cursor-not-allowed opacity-50 hover:no-underline' : ''}`}
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => handleViewDetails(submission)}
|
||||
>
|
||||
{String(value)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
// if (ci === 6) {
|
||||
// const submission = filteredRows[ri];
|
||||
// const disabled = !submission;
|
||||
// return (
|
||||
// <button
|
||||
// className={`text-[#92722A] hover:underline ${disabled ? 'cursor-not-allowed opacity-50 hover:no-underline' : ''}`}
|
||||
// type="button"
|
||||
// disabled={disabled}
|
||||
// onClick={() => handleViewDetails(submission)}
|
||||
// >
|
||||
// {String(value)}
|
||||
// </button>
|
||||
// );
|
||||
// }
|
||||
return value;
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -314,6 +314,7 @@ const Survey = () => {
|
||||
return {
|
||||
quarter: establishmentData?.quarter ?? '',
|
||||
year: parseNumber(establishmentData?.year),
|
||||
status: 'Submitted',
|
||||
emirati_male: parseNumber(info.emiratiMale),
|
||||
emirati_female: parseNumber(info.emiratiFemale),
|
||||
non_emirati_male: parseNumber(info.nonEmiratiMale),
|
||||
|
||||
@ -103,6 +103,26 @@ export const getSubmissionHistoryByEstablishment = async (establishmentId, confi
|
||||
}
|
||||
};
|
||||
|
||||
export const getSubmissionAuditHistory = async (establishmentId, params = {}) => {
|
||||
try {
|
||||
if (!establishmentId) {
|
||||
throw new Error('Establishment ID is required');
|
||||
}
|
||||
|
||||
const response = await getRequest('/submission-audit-history', {
|
||||
params: {
|
||||
establishment_id: establishmentId,
|
||||
...params // Spread any additional parameters (year, quarter, product_id)
|
||||
}
|
||||
});
|
||||
|
||||
return response.data || response;
|
||||
} catch (error) {
|
||||
console.error('Error fetching submission audit history:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
getSubmissions,
|
||||
submitSurvey,
|
||||
@ -110,5 +130,6 @@ export default {
|
||||
fetchSubmissionDetail,
|
||||
getQuarterPeriods,
|
||||
getPreviousForecastData,
|
||||
getSubmissionHistoryByEstablishment
|
||||
getSubmissionHistoryByEstablishment,
|
||||
getSubmissionAuditHistory
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user