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

297 lines
14 KiB
TypeScript

import React, { useEffect, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { PiDotsThreeOutlineVerticalFill } from "react-icons/pi";
import logoRecord from "../../assets/record.svg";
import Loader from "../dashboard/Loader";
import api from "../../config/api";
import TemplateDeleteWarnModal from "./TemplateDeleteWarnModal";
import { resetRecipients } from "../../store/slices/selectedRecipientsSlice";
import { resetDocuments } from "../../store/slices/documentSlice";
import { AppDispatch } from "../../store/store";
import { useDispatch } from "react-redux";
interface Template {
id: number;
name: string | null;
description: string | null;
createdAt: string;
lastModifiedAt: string;
createdBy?: { name: string };
lastModifiedBy?: { name: string };
expireAfter?: number;
}
const formatDate = (isoString: string) => {
const date = new Date(isoString);
return isNaN(date.getTime()) ? "-" : date.toLocaleString();
};
const TemplateTable = () => {
const dispatch = useDispatch<AppDispatch>();
const [activeAccountId, setActiveAccountId] = useState<string | null>(
JSON.parse(sessionStorage.getItem('activeAccountId') || 'null')
);
const navigate = useNavigate();
const location = useLocation();
const [templates, setTemplates] = useState<Template[]>([]);
const [loading, setLoading] = useState(false);
// Pagination
const [page, setPage] = useState(0); // 0-based
const [size, setSize] = useState(10);
const [totalPages, setTotalPages] = useState(0);
// Menu state
const [showMenu, setShowMenu] = useState<{ open: boolean; templateId: number | null }>({
open: false,
templateId: null,
});
// Delete modal
const [showTemplateDeleteWarnModal, setShowTemplateDeleteWarnModal] = useState<{
show: boolean;
templateId: string;
}>({ show: false, templateId: "" });
const fetchTemplates = async () => {
if (!activeAccountId) return;
setLoading(true);
try {
const res = await api.get(
`/api/v1/accounts/${activeAccountId}/templates?sortBy=lastModifiedAt desc&page=${page}&size=${size}`
);
const { content = [], page: pageData = {} } = res.data;
setTemplates(content);
setTotalPages(pageData.totalPages || 0);
} catch (error) {
console.error("Error fetching templates:", error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchTemplates();
}, [activeAccountId, page, size]);
const handleUseClick = async (item: Template) => {
try {
await dispatch(resetRecipients())
await dispatch(resetDocuments())
const payload = {
name: item.name ?? "",
subject: item.description,
message: "",
expireAfter: item.expireAfter,
templateId: item.id,
};
const res = await api.post(`/api/v1/accounts/${activeAccountId}/transactions`, payload);
navigate(`/setup-transaction/${activeAccountId}/${res?.data?.id}?useTemplate=true`);
} catch (error) {
console.error(error);
}
};
const handleEditClick = (templateId: string) => {
setShowMenu({ open: false, templateId: null });
navigate(`/setup-transaction/${activeAccountId}/template/${templateId}`);
};
const handleDeleteClick = async (templateId: string) => {
setShowMenu({ open: false, templateId: null });
try {
await api.delete(`/api/v1/accounts/${activeAccountId}/templates/${templateId}`);
setTemplates((prev) => prev.filter((temp) => temp.id != templateId));
setShowTemplateDeleteWarnModal({ show: false, templateId: "" });
} catch (e) {
console.error(e);
}
};
return (
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden p-5">
{/* Header */}
{location.pathname != "/templates" && (
<div className="flex justify-between items-center pb-3">
<h2 className="text-lg font-semibold text-gray-800">Recent Templates</h2>
<span
onClick={() => navigate("/templates")}
className="text-[var(--primary)] text-sm hover:underline cursor-pointer font-medium"
>
View all
</span>
</div>
)}
{/* Table */}
<div
className={`overflow-auto rounded-lg ${location.pathname == "/templates"
? "min-h-[70vh] max-h-[70vh]"
: "min-h-[30vh] max-h-[50vh]"
}`}
>
<table className="w-full text-sm">
<thead className="sticky top-0 bg-gray-50 z-10">
<tr className="text-xs text-gray-600">
<th className="px-6 py-3 text-left font-semibold">Template Name</th>
<th className="px-6 py-3 text-left font-semibold">Created By</th>
<th className="px-6 py-3 text-left font-semibold">Created</th>
<th className="px-6 py-3 text-left font-semibold">Last Updated</th>
<th className="px-6 py-3 text-left font-semibold">Actions</th>
</tr>
</thead>
{loading ? (
<tbody>
<tr>
<td colSpan={6} className="py-10 text-center">
<Loader />
</td>
</tr>
</tbody>
) : templates.length > 0 ? (
<tbody>
{templates.map((template, idx) => (
<tr
key={template.id}
className={`${idx % 2 != 0 ? "bg-gray-50" : "bg-white"
} border-b border-gray-300 hover:bg-gray-100 transition`}
>
<td className="px-6 py-3 text-gray-800 font-medium truncate max-w-xs">
{template.name || "Untitled"}
</td>
<td className="px-6 py-3 text-gray-700">
{template.createdBy?.name || "-"}
</td>
<td className="px-6 py-3 text-gray-600">{formatDate(template.createdAt)}</td>
<td className="px-6 py-3 text-gray-600">
{formatDate(template.lastModifiedAt)}
</td>
<td className="px-6 py-3 text-gray-700">
<div className="relative inline-block text-left">
<button
onClick={() =>
setShowMenu({
open:
showMenu.open && showMenu.templateId == template.id ? false : true,
templateId: template.id,
})
}
className="text-gray-500 hover:text-gray-700 p-1 rounded-md hover:bg-gray-100"
>
<PiDotsThreeOutlineVerticalFill className="text-lg" />
</button>
{showMenu.open && showMenu.templateId == template.id && (
<div className="absolute right-0 mt-2 w-40 bg-white border border-gray-200 rounded-lg shadow-lg z-20 animate-fade-in">
<button
onClick={() => handleUseClick(template)}
className="w-full text-left px-4 py-2 text-sm hover:bg-blue-50 hover:text-blue-600"
>
Use
</button>
<button
onClick={() => handleEditClick(template.id + "")}
className="w-full text-left px-4 py-2 text-sm hover:bg-yellow-50 hover:text-yellow-600"
>
Edit
</button>
<button
onClick={() =>
setShowTemplateDeleteWarnModal({
show: true,
templateId: template.id + "",
})
}
className="w-full text-left px-4 py-2 text-sm hover:bg-red-50 hover:text-red-600"
>
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">
<img
src={logoRecord}
alt="No Templates"
className="w-32 h-32 mb-4 opacity-70"
/>
<p>No templates found</p>
</div>
</td>
</tr>
</tbody>
)}
</table>
</div>
{/* Pagination */}
{templates.length > 0 && (
<div className="flex justify-between items-center mt-4 text-sm text-gray-600">
<div>
Page {page + 1} of {totalPages}
</div>
<div className="flex items-center gap-3">
<select
value={size}
onChange={(e) => {
setSize(Math.min(Number(e.target.value), 30));
setPage(0);
}}
className="border rounded px-2 py-1 text-sm"
>
{[5, 10, 20, 30].map((s) => (
<option key={s} value={s}>
{s} / page
</option>
))}
</select>
<button
onClick={() => setPage((prev) => Math.max(prev - 1, 0))}
disabled={page == 0}
className={`px-3 py-1 rounded border ${page == 0
? "bg-gray-100 text-gray-400 cursor-not-allowed"
: "bg-white hover:bg-gray-50"
}`}
>
Prev
</button>
<button
onClick={() => setPage((prev) => prev + 1)}
disabled={page + 1 >= totalPages}
className={`px-3 py-1 rounded border ${page + 1 >= totalPages
? "bg-gray-100 text-gray-400 cursor-not-allowed"
: "bg-white hover:bg-gray-50"
}`}
>
Next
</button>
</div>
</div>
)}
{showTemplateDeleteWarnModal.show && (
<TemplateDeleteWarnModal
setShowTemplateDeleteWarnModal={setShowTemplateDeleteWarnModal}
onConfirm={() => handleDeleteClick(showTemplateDeleteWarnModal.templateId)}
/>
)}
</div>
);
};
export default TemplateTable;