bug fixed in establishemnet

This commit is contained in:
Malini 2025-11-06 18:50:56 +05:30
parent 74174618db
commit 0e65ba4d13
9 changed files with 498 additions and 205 deletions

View File

@ -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

View File

@ -77,8 +77,9 @@ const Table = ({
const safeCurrentPage = enablePagination ? Math.min(Math.max(currentPage, 1), totalPages) : 1; const safeCurrentPage = enablePagination ? Math.min(Math.max(currentPage, 1), totalPages) : 1;
const startIndex = enablePagination ? (safeCurrentPage - 1) * pageSize : 0; const startIndex = enablePagination ? (safeCurrentPage - 1) * pageSize : 0;
const endIndex = enablePagination ? startIndex + pageSize : rows.length; 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 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(() => { const displayedRows = React.useMemo(() => {
if (!enablePagination) return rows; if (!enablePagination) return rows;

View File

@ -489,6 +489,15 @@ export const DetailedOverview = ({ submission, onBack }) => {
pagination={tablePagination} pagination={tablePagination}
/> />
</section> </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> </div>
); );
}; };

View File

@ -37,10 +37,12 @@ const ProductDetails = ({ product, onBack, showToast: shouldShowToast, onToastSh
rejected: { rejected: {
type: 'rejected', type: 'rejected',
bgColor: 'bg-red-50', bgColor: 'bg-red-50',
textColor: '#D83731', textColor: 'text-red-600',
iconColor: 'text-red-500', iconColor: 'text-red-500',
message: 'Your survey has been rejected. Please review and resubmit.', 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: { submitted: {
type: 'submitted', type: 'submitted',
@ -146,18 +148,11 @@ const ProductDetails = ({ product, onBack, showToast: shouldShowToast, onToastSh
/> />
</svg> </svg>
) : toast.type === 'rejected' ? ( ) : toast.type === 'rejected' ? (
<svg <img
xmlns="http://www.w3.org/2000/svg" src={toast.icon}
viewBox="0 0 20 20" alt={toast.iconAlt || 'Rejected'}
fill="currentColor" className={toast.iconClassName || 'w-5 h-5 mr-2'}
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>
) : ( ) : (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -205,7 +200,7 @@ const ProductDetails = ({ product, onBack, showToast: shouldShowToast, onToastSh
</div> </div>
</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 */} {/* Basic Information */}
<div className="mb-6"> <div className="mb-6">
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-4"> <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} {formattedProduct.remarks}
</div> </div>
</div> </div>
{/* Submission Info */} {/* Submission Info */}
{/* <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">
@ -380,6 +375,15 @@ const ProductDetails = ({ product, onBack, showToast: shouldShowToast, onToastSh
</div> </div>
</div> */} </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> </div>
); );
}; };

View File

@ -178,12 +178,25 @@ const ProductData = ({
const [unitsError, setUnitsError] = React.useState(''); const [unitsError, setUnitsError] = React.useState('');
const [activeTab, setActiveTab] = React.useState('current'); const [activeTab, setActiveTab] = React.useState('current');
const [isInitialLoad, setIsInitialLoad] = React.useState(true); 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 [formErrors, setFormErrors] = React.useState({});
const [quarterPeriods, setQuarterPeriods] = useState(null); const [quarterPeriods, setQuarterPeriods] = useState(null);
const [isLoadingPeriods, setIsLoadingPeriods] = useState(false); 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(() => { useEffect(() => {
const fetchQuarterPeriods = async () => { const fetchQuarterPeriods = async () => {
if (!quarter || !year) return; if (!quarter || !year) return;
@ -1239,7 +1252,7 @@ const ProductData = ({
<Textarea <Textarea
placeholder="Enter your remarks here..." placeholder="Enter your remarks here..."
value={remarks} value={remarks}
onChange={(e) => setRemarks(e.target.value)} onChange={handleRemarksChange}
className="min-h-[60px]" className="min-h-[60px]"
/> />
</div> </div>

View File

@ -254,7 +254,13 @@ const ReviewSubmit = ({
hasSubmitted = false, hasSubmitted = false,
}) => { }) => {
const [confirm, setConfirm] = React.useState(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 establishmentDetails = React.useMemo(() => buildEstablishmentDetails(establishment), [establishment]);
const employmentDetails = React.useMemo(() => buildEmploymentDetails(establishment), [establishment]); const employmentDetails = React.useMemo(() => buildEmploymentDetails(establishment), [establishment]);
const productRows = React.useMemo(() => buildProductRows(products), [products]); const productRows = React.useMemo(() => buildProductRows(products), [products]);
@ -598,6 +604,15 @@ const ReviewSubmit = ({
</div> </div>
</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 */} {/* Table */}
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="min-w-full border border-gray-300 text-sm text-center"> <table className="min-w-full border border-gray-300 text-sm text-center">

View File

@ -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 { useNavigate } from 'react-router-dom';
import Table from '@/components/common/Table'; import Table from '@/components/common/Table';
import HeaderBar from '@/components/layout/HeaderBar'; 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 { fetchProducts } from '@/services/masters/masterService';
import { fetchSubmissionHistory, getSubmissionAuditHistory } from '@/services/submissions/submissionService';
const searchIconSrc = '/assets/images/material-symbols_search-rounded.svg'; const searchIconSrc = '/assets/images/material-symbols_search-rounded.svg';
const BreadcrumbVector = '/assets/images/Breadcrumb-vector.svg'; const BreadcrumbVector = '/assets/images/Breadcrumb-vector.svg';
@ -35,6 +36,11 @@ const History = () => {
const [hscodeFilter, setHscodeFilter] = React.useState(''); const [hscodeFilter, setHscodeFilter] = React.useState('');
const [hscodeOptions, setHscodeOptions] = React.useState([]); const [hscodeOptions, setHscodeOptions] = React.useState([]);
const [isLoadingHscodes, setIsLoadingHscodes] = React.useState(false); 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({ const [pagination, setPagination] = React.useState({
currentPage: 1, currentPage: 1,
pageSize: 10, pageSize: 10,
@ -59,6 +65,105 @@ const History = () => {
loadHscodeOptions(); 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 toVariant = React.useCallback((status) => {
const text = String(status || '').toLowerCase(); const text = String(status || '').toLowerCase();
if (text === 'approved') return 'approved'; if (text === 'approved') return 'approved';
@ -169,66 +274,45 @@ const History = () => {
const statusOptions = [ const statusOptions = [
{ value: 'all', label: 'All Status' }, { value: 'all', label: 'All Status' },
{ value: 'approved', label: 'Approved' }, { value: 'approved', label: 'Approved' },
{ value: 'pending', label: 'Pending' },
{ value: 'rejected', label: 'Rejected' }, { value: 'rejected', label: 'Rejected' },
{ value: 'submitted', label: 'Submitted' }, { value: 'submitted', label: 'Submitted' },
]; ];
const filteredRows = React.useMemo(() => { const filteredRows = useMemo(() => {
const term = searchTerm.trim().toLowerCase(); const term = searchTerm.trim().toLowerCase();
const statusValue = statusFilter.toLowerCase();
const filtered = auditHistory.filter((entry) => {
const filtered = rows.filter((entry) => { const matchesSearch =
// Status filter !term ||
const matchesStatus = Object.values(entry)
statusValue === 'all' || .join(' ')
toVariant(entry.status) === statusValue || .toLowerCase()
formatStatus(entry.status).toLowerCase() === statusValue; .includes(term);
// Year filter const matchesYear = !yearFilter || String(entry.year) === yearFilter;
const matchesYear = !yearFilter || String(entry.year) === yearFilter; const matchesQuarter = !quarterFilter || entry.quarter?.toUpperCase() === quarterFilter.toUpperCase();
// Quarter filter const matchesHscode = (() => {
const matchesQuarter = !quarterFilter || if (!hscodeFilter) return true;
(entry.quarter && entry.quarter.toUpperCase() === quarterFilter.toUpperCase()); const productName = hscodeFilter.includes(' - ')
? hscodeFilter.split(' - ')[1].trim()
// HS Code/Product filter : hscodeFilter.trim();
const matchesHscode = !hscodeFilter || return entry.product_name?.toLowerCase().includes(productName.toLowerCase());
(entry.hs_code && entry.hs_code.toString() === hscodeFilter) || })();
(entry.product_id && entry.product_id.toString() === hscodeFilter);
return matchesSearch && matchesYear && matchesQuarter && matchesHscode;
// Search term filter });
if (term) {
const haystack = [ setPagination((prev) => ({
entry.year, ...prev,
entry.quarter, totalItems: filtered.length,
entry.submission_window, currentPage: 1,
entry.total_products, }));
entry.products_submitted,
entry.total_cost, return filtered;
formatStatus(entry.status), }, [auditHistory, searchTerm, yearFilter, quarterFilter, hscodeFilter]);
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 formatNumber = (num) => { const formatNumber = (num) => {
if (num === null || num === undefined) return '—'; if (num === null || num === undefined) return '—';
@ -242,19 +326,29 @@ const History = () => {
}; };
// 🟩 Updated tableRows with expandable details // 🟩 Updated tableRows with expandable details
const handlePageChange = (newPage) => { const handlePageChange = React.useCallback((page) => {
setPagination(prev => ({ setPagination(prev => ({
...prev, ...prev,
currentPage: newPage, currentPage: page,
})); }));
}; }, []);
const handlePageSizeChange = (newSize) => { const handlePageSizeChange = React.useCallback((newSize) => {
setPagination(prev => ({ setPagination(prev => ({
...prev, ...prev,
pageSize: newSize, pageSize: newSize,
currentPage: 1, // Reset to first page when page size changes 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 = () => { const downloadCsv = () => {
@ -281,18 +375,76 @@ const History = () => {
// Convert data to CSV rows // Convert data to CSV rows
const csvRows = [ const csvRows = [
headers.join(','), headers.join(','),
...filteredRows.map(row => ...filteredRows.map(row => {
[ // Get actor(s) - handle both array and single value
getFieldValue(formatDateTime(row.created_at)), let actors = '';
getFieldValue(row.year), if (Array.isArray(row.actor)) {
getFieldValue(row.quarter), actors = row.actor.filter(a => a && a.trim() !== '').join(', ');
getFieldValue(row.hs_code), } else if (row.actor) {
getFieldValue(row.product_name), actors = row.actor;
getFieldValue(formatStatus(row.status)), } else {
(row.actors && row.actors.length ? row.actors.join(', ') : '-').replace(/"/g, '""'), actors = '-';
'Quantity and cost updates' // This is a placeholder for the details column }
].map(field => `"${field}"`).join(',')
) // 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 // Create CSV content
@ -312,66 +464,98 @@ const History = () => {
document.body.removeChild(link); document.body.removeChild(link);
}; };
const tableRows = filteredRows.map((entry, index) => { const tableRows = auditHistory.map((entry, index) => {
const isExpanded = expandedRow === index; const isExpanded = expandedRow === index;
return [ const details = entry.details || [];
formatDateTime(entry.created_at),
entry.year || '—', // Check if there are quantity or cost changes
entry.quarter || '—', const hasQuantityChanges = details.some(d => d.column.includes('quantity'));
entry.hs_code || '—', const hasCostChanges = details.some(d => d.column.includes('cost'));
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>
{isExpanded && ( return [
<div className="mt-2 ml-2 border border-gray-200 rounded p-2 bg-gray-50 text-sm text-gray-700"> formatDateTime(entry.submission_date || entry.created_at),
<div className="flex gap-6 mb-2"> entry.year || '—',
<div> entry.quarter || '—',
<div className="font-medium">Quantity Updated:</div> entry.hs_code || '—',
<div>-</div> entry.product_name || '—',
</div> <div className="flex justify-center">
<div> <Badge variant={toVariant(entry.status)}>
<div className="font-medium">Cost Updated:</div> {formatStatus(entry.status)}
<div>-</div> </Badge>
</div> </div>,
</div> entry.actor || '—',
<div> <div className="flex flex-col w-full">
<div className="font-medium">Reason for variations:</div> <div className="flex items-center justify-between text-sm pt-2 text-gray-800">
<div>-</div> <span>
</div> {hasQuantityChanges ? 'Quantity updated' : ''}
</div> {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> </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 = ( 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">
@ -386,11 +570,16 @@ const History = () => {
<div className="relative w-full md:w-56"> <div className="relative w-full md:w-56">
<input <input
type="text" type="text"
placeholder="Search..."
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => {
placeholder="Search submissions" const value = e.target.value;
className="w-full h-10 rounded-md border border-[#E5E7EB] pl-10 pr-3 text-sm focus:outline-none" 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" /> <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>
<div className="relative w-full md:w-40"> <div className="relative w-full md:w-40">
@ -512,35 +701,60 @@ const History = () => {
<polyline points="6 9 12 15 18 9" /> <polyline points="6 9 12 15 18 9" />
</svg> </svg>
</div> </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"> <div className="relative">
<select <select
value={hscodeFilter} value={selectedActor}
onChange={(e) => setHscodeFilter(e.target.value)} onChange={(e) => setSelectedActor(e.target.value)}
disabled={isLoadingHscodes} 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"
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="">Actors</option>
<option value="">HS Code/Product</option> {actorOptions.map((actor) => (
{hscodeOptions.map((option) => ( <option key={actor.value} value={actor.value}>
<option key={option.id || option.value} value={option.id || option.value}> {actor.label}
{option.hs_code ? `${option.hs_code} - ${option.product_name || ''}` : option.label} </option>
</option> ))}
))} </select>
</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>
<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>
@ -553,7 +767,6 @@ const History = () => {
> >
<option value="all">Status</option> <option value="all">Status</option>
<option value="approved">Approved</option> <option value="approved">Approved</option>
<option value="pending">Pending</option>
<option value="rejected">Rejected</option> <option value="rejected">Rejected</option>
<option value="submitted">Submitted</option> <option value="submitted">Submitted</option>
</select> </select>
@ -576,7 +789,7 @@ const History = () => {
type="text" type="text"
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} 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" className="w-full h-10 rounded-md border border-[#E2E8F0] pl-10 pr-3 text-sm text-[#232528] focus:outline-none"
/> />
<img <img
@ -599,10 +812,23 @@ const History = () => {
</div> </div>
</div> </div>
{filteredRows.length === 0 && !loading ? ( {filteredRows.length === 0 && !loading ? (
<div className="flex flex-col items-center justify-center py-12"> <div className="w-full flex flex-col h-5items-center justify-start pt-1 pb-7 text-center">
<p className="text-gray-500 text-lg">No details found</p> <div className="w-full max-w-md mx-auto">
{/* <p className="text-gray-400 text-sm mt-1">Try adjusting your search or filter to find what you're looking for.</p> */} {/* <svg
</div> 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 <Table
headers={headers} headers={headers}
@ -622,20 +848,20 @@ const History = () => {
if (ci === 5) { // Status column if (ci === 5) { // Status column
return <Badge variant={toVariant(filteredRows[ri]?.status)}>{formatStatus(filteredRows[ri]?.status)}</Badge>; return <Badge variant={toVariant(filteredRows[ri]?.status)}>{formatStatus(filteredRows[ri]?.status)}</Badge>;
} }
if (ci === 6) { // if (ci === 6) {
const submission = filteredRows[ri]; // const submission = filteredRows[ri];
const disabled = !submission; // const disabled = !submission;
return ( // return (
<button // <button
className={`text-[#92722A] hover:underline ${disabled ? 'cursor-not-allowed opacity-50 hover:no-underline' : ''}`} // className={`text-[#92722A] hover:underline ${disabled ? 'cursor-not-allowed opacity-50 hover:no-underline' : ''}`}
type="button" // type="button"
disabled={disabled} // disabled={disabled}
onClick={() => handleViewDetails(submission)} // onClick={() => handleViewDetails(submission)}
> // >
{String(value)} // {String(value)}
</button> // </button>
); // );
} // }
return value; return value;
}} }}
/> />

View File

@ -314,6 +314,7 @@ const Survey = () => {
return { return {
quarter: establishmentData?.quarter ?? '', quarter: establishmentData?.quarter ?? '',
year: parseNumber(establishmentData?.year), year: parseNumber(establishmentData?.year),
status: 'Submitted',
emirati_male: parseNumber(info.emiratiMale), emirati_male: parseNumber(info.emiratiMale),
emirati_female: parseNumber(info.emiratiFemale), emirati_female: parseNumber(info.emiratiFemale),
non_emirati_male: parseNumber(info.nonEmiratiMale), non_emirati_male: parseNumber(info.nonEmiratiMale),

View File

@ -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 { export default {
getSubmissions, getSubmissions,
submitSurvey, submitSurvey,
@ -110,5 +130,6 @@ export default {
fetchSubmissionDetail, fetchSubmissionDetail,
getQuarterPeriods, getQuarterPeriods,
getPreviousForecastData, getPreviousForecastData,
getSubmissionHistoryByEstablishment getSubmissionHistoryByEstablishment,
getSubmissionAuditHistory
}; };