297 lines
14 KiB
TypeScript
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;
|