SME-Web/src/components/templates/TemplateTable.tsx

284 lines
15 KiB
TypeScript

import { useEffect, useState } from 'react'
import { useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import logoRecord from '../../assets/record.svg';
import LoaderUi from '../../helper/LoaderUi';
import api from '../../config/api';
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
import { TemplateInterface } from '../../utlity/types';
import TemplateDeleteWarnModal from './TemplateDeleteWarnModal';
const TemplateTable = () => {
const [loading, setLoading] = useState(false)
const [templates, setTemplates] = useState<TemplateInterface[] | []>([])
const accountDetails = useSelector((state: any) => state.user.account);
const accountId = accountDetails.id || "";
const [showMenu, setShowMenu] = useState(null);
const [showTemplateDeleteWarnModal, setShowTemplateDeleteWarnModal] = useState<{ show: boolean, templateId: string }>({ show: false, templateId: "" });
const [activeAccountId, setActiveAccountId] = useState<string | null>(
JSON.parse(sessionStorage.getItem('activeAccountId') || 'null')
);
const navigate = useNavigate();
const getAllTemplates = async () => {
setLoading(true)
try {
let res = await api.get(`/api/v1/accounts/${activeAccountId}/templates?sortBy=lastModifiedAt desc`)
let templateData = res?.data
setTemplates(templateData)
} catch (error) {
console.log(error)
} finally {
setLoading(false)
}
}
useEffect(() => {
if (activeAccountId) {
getAllTemplates()
}
}, [activeAccountId])
// Listen for sessionStorage changes
useEffect(() => {
const handleStorageChange = (e: StorageEvent) => {
if (e.key === 'activeAccountId') {
const newActiveAccountId = e.newValue ? JSON.parse(e.newValue) : null;
setActiveAccountId(newActiveAccountId);
}
};
// Listen for storage events (works for changes from other tabs)
window.addEventListener('storage', handleStorageChange);
// Custom event listener for same-tab changes
const handleCustomStorageChange = (e: CustomEvent) => {
if (e.detail.key === 'activeAccountId') {
setActiveAccountId(e.detail.newValue);
}
};
window.addEventListener('sessionStorageChange', handleCustomStorageChange as EventListener);
return () => {
window.removeEventListener('storage', handleStorageChange);
window.removeEventListener('sessionStorageChange', handleCustomStorageChange as EventListener);
};
}, []);
const formatDate = (timestamp: number) => {
return new Date(timestamp * 1000).toLocaleString();
};
const handleUseClick = async (item: any) => {
try {
const payload = {
name: item?.name ?? "",
subject: item.subject,
message: item.message,
description: item.description,
expireAfter: item.expireAfter,
enableIdStamping: item.enableIdStamping,
allowViewAudit: item.allowViewAudit,
allowReassign: item.allowReassign,
enableSignatureIdStamping: item.enableSignatureIdStamping,
templateId: item.id,
};
let res = await api.post(`/api/v1/accounts/${activeAccountId}/transactions`, payload)
// /process-step-one/:accountId/:transactionId
navigate(`/process-step-one/${activeAccountId}/${res?.data?.id}?useTemplate=true`);
} catch (error) {
console.log(error)
}
}
const handleEditClick = (templateId: string) => {
setShowMenu(null);
navigate(`/process-step-one/${accountId}/template/${templateId}`);
}
const handleDeleteClick = async (templateId: string) => {
setShowMenu(null);
try {
await api.delete(`api/accounts/${activeAccountId}/templates/${templateId}`)
setTemplates((prev) => {
return prev.filter((temp) => temp.id !== Number(templateId));
});
setShowTemplateDeleteWarnModal({ show: false, templateId: "" })
} catch (e) { }
}
function formatTimestamp(isoString, options = { timeZone: 'local' }) {
// Parse the ISO string to a Date object
const date = new Date(isoString);
if (isNaN(date)) {
return 'Invalid date';
}
// Return formatted date string
if (options.timeZone === 'UTC') {
return date.toUTCString(); // e.g. "Mon, 04 Aug 2025 13:46:46 GMT"
} else {
return date.toLocaleString(); // e.g. "8/4/2025, 9:46:46 AM" (based on local time zone)
}
}
return (
<>
<div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
{/* Header */}
{location.pathname !== '/templates' &&
<div className="flex justify-between items-center px-6 py-4 border-b border-gray-100">
<h2 className="text-sm font-semibold text-gray-800">Recent Template</h2>
<span className=' text-[var(--primary)] text-xs hover:underline cursor-pointer font-semibold'>View all</span>
</div>}
{/* Scrollable Table Container */}
<div className={`overflow-auto no-scrollbar ${location.pathname === '/templates' ? "min-h-[76vh] max-h-[76vh]" : "min-h-[70vh] max-h-[70vh]"}`}>
<table className="w-full text-sm">
{/* Sticky Header */}
<thead className="bg-gray-50 sticky top-0 z-10">
<tr className="text-gray-600 text-xs uppercase tracking-wider border-b border-gray-200">
<th className="px-6 py-4 text-left font-semibold">Name</th>
{location.pathname === '/templates' && (
<>
<th className="px-6 py-4 text-center font-semibold">Created</th>
<th className="px-6 py-4 text-center font-semibold">Last Updated</th>
</>
)}
{/* <th className="px-6 py-4 text-center font-semibold">Use</th> */}
<th className="px-6 py-4 text-center font-semibold">Action</th>
</tr>
</thead>
{loading ? (
<tbody>
<tr>
<td colSpan="6" className="relative h-100">
<LoaderUi />
</td>
</tr>
</tbody>
) : templates.length > 0 ? (
<>
{/* Scrollable Body */}
<tbody className="bg-white divide-y divide-gray-100">
{templates.map((item, index) => (
<tr
key={index}
className="hover:bg-gray-50 transition-colors duration-150"
>
<td className="px-6 py-4 text-left">
<div className="text-gray-800 font-medium truncate max-w-xs">
{item.name || 'Untitled'}
</div>
</td>
{location.pathname === '/templates' && (
<>
<td className="px-6 py-4 text-center">
<span className="text-gray-600 whitespace-nowrap">
{formatTimestamp(item.createdAt)}
</span>
</td>
<td className="px-6 py-4 text-center">
<span className="text-gray-600 whitespace-nowrap">
{formatTimestamp(item.lastModifiedAt)}
</span>
</td>
</>
)}
{/* <td className="px-6 py-4 text-center">
<button
className="cursor-pointer bg-[var(--primary)] border-gray-300 text-white hover:bg-gray-100 px-3 py-2 rounded-lg font-semibold text-xs transition-colors duration-150"
onClick={() => handleUseClick(item)}
>
USE
</button>
</td> */}
<td className="px-6 py-4 text-center">
<div
className="relative inline-block"
tabIndex={0}
onBlur={(e) => {
if (!e.currentTarget.contains(e.relatedTarget)) {
setShowMenu(null);
}
}}
>
<button
className="text-gray-400 hover:text-gray-600 cursor-pointer transition-colors duration-150 p-1 rounded-md hover:bg-gray-100"
onClick={() =>
setShowMenu(showMenu === index ? null : index)
}
>
<PiDotsThreeOutlineVerticalFill className="text-lg" />
</button>
{showMenu === index && (
<div className="absolute top-7.5 right-0 bg-white border border-gray-200 rounded-xl shadow-lg z-20 min-w-[140px] overflow-hidden animate-fade-in">
<button
onClick={() => handleUseClick(item)}
className="w-full text-left px-4 py-2 text-sm cursor-pointer text-gray-700 hover:bg-blue-50 hover:text-blue-600 transition-colors"
>
Use
</button>
<button
onClick={() => handleEditClick(item.id)}
className="w-full text-left px-4 py-2 text-sm cursor-pointer text-gray-700 hover:bg-yellow-50 hover:text-yellow-600 transition-colors"
>
Edit
</button>
<button
onClick={() => {
setShowTemplateDeleteWarnModal({ show: true, templateId: item.id+"" })
}}
className="w-full text-left px-4 py-2 text-sm cursor-pointer text-gray-700 hover:bg-red-50 hover:text-red-600 transition-colors"
>
Delete
</button>
</div>
)}
</div>
</td>
</tr>
))}
</tbody>
</>
) : (
<tbody>
<tr>
<td colSpan="6" className="text-center py-10 text-gray-500">
<div className="flex flex-col items-center justify-center">
<img
src={logoRecord}
alt="Record Not Found"
className="w-70 h-70 mb-4 opacity-70"
/>
</div>
</td>
</tr>
</tbody>
)}
</table>
</div>
{
showTemplateDeleteWarnModal.show && <TemplateDeleteWarnModal
setShowTemplateDeleteWarnModal={setShowTemplateDeleteWarnModal}
onConfirm={() => handleDeleteClick(showTemplateDeleteWarnModal.templateId)}
/>
}
</div>
</>
)
}
export default TemplateTable