Files
OpenSign/apps/OpenSign/src/primitives/GetReportDisplay.js
T

840 lines
33 KiB
JavaScript

import React, { useState, useEffect, useMemo } from "react";
import pad from "../assets/images/pad.svg";
import { useNavigate } from "react-router-dom";
import axios from "axios";
import ModalUi from "./ModalUi";
import AddSigner from "../components/AddSigner";
import { modalSubmitBtnColor, modalCancelBtnColor } from "../constant/const";
import Alert from "./Alert";
import Tooltip from "./Tooltip";
import { RWebShare } from "react-web-share";
import Tour from "reactour";
import Parse from "parse";
import { saveAs } from "file-saver";
const ReportTable = (props) => {
const navigate = useNavigate();
const [currentPage, setCurrentPage] = useState(1);
const [actLoader, setActLoader] = useState({});
const [isAlert, setIsAlert] = useState(false);
const [isDocErr, setIsDocErr] = useState(false);
const [isContactform, setIsContactform] = useState(false);
const [isDeleteModal, setIsDeleteModal] = useState({});
const [isRevoke, setIsRevoke] = useState({});
const [isShare, setIsShare] = useState({});
const [shareUrls, setShareUrls] = useState([]);
const [copied, setCopied] = useState(false);
const [isOption, setIsOption] = useState({});
const [alertMsg, setAlertMsg] = useState({ type: "success", message: "" });
const [isTour, setIsTour] = useState(false);
const [tourStatusArr, setTourStatusArr] = useState([]);
const startIndex = (currentPage - 1) * props.docPerPage;
const { isMoreDocs, setIsNextRecord } = props;
// For loop is used to calculate page numbers visible below table
// Initialize pageNumbers using useMemo to avoid unnecessary re-creation
const pageNumbers = useMemo(() => {
const calculatedPageNumbers = [];
for (let i = 1; i <= Math.ceil(props.List.length / props.docPerPage); i++) {
calculatedPageNumbers.push(i);
}
return calculatedPageNumbers;
}, [props.List, props.docPerPage]);
// below useEffect reset currenpage to 1 if user change route
useEffect(() => {
checkTourStatus();
return () => setCurrentPage(1);
}, []);
// `formatRow` is used to show data in poper manner like
// if data is of array type then it will join array items with ","
// if data is of object type then it Name values will be show in row
// if no data available it will show hyphen "-"
const formatRow = (row) => {
if (Array.isArray(row)) {
let updateArr = row.map((x) => x.Name);
return updateArr.join(", ");
} else if (typeof row === "object" && row !== null) {
return row?.Name || "-";
} else {
return "-";
}
};
// below useEffect is used to render next record if IsMoreDoc is true
// second last value of pageNumber array is same as currentPage
useEffect(() => {
if (isMoreDocs && pageNumbers[pageNumbers.length - 1] === currentPage) {
setIsNextRecord(true);
}
}, [isMoreDocs, pageNumbers, currentPage, setIsNextRecord]);
// `handleURL` is used to open microapp
const handleURL = async (item, act) => {
if (props.ReportName === "Templates") {
if (act.hoverLabel === "Edit") {
navigate(`/${act.redirectUrl}/${item.objectId}`);
} else {
setActLoader({ [`${item.objectId}_${act.btnId}`]: true });
try {
const params = {
templateId: item.objectId
};
const templateDeatils = await axios.post(
`${localStorage.getItem("baseUrl")}functions/getTemplate`,
params,
{
headers: {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
sessionToken: localStorage.getItem("accesstoken")
}
}
);
// console.log("templateDeatils.data ", templateDeatils.data);
const templateData =
templateDeatils.data && templateDeatils.data.result;
if (!templateData.error) {
const Doc = templateData;
let signers = [];
if (Doc.Signers?.length > 0) {
Doc.Signers?.forEach((x) => {
if (x.objectId) {
const obj = {
__type: "Pointer",
className: "contracts_Contactbook",
objectId: x.objectId
};
signers.push(obj);
}
});
}
let placeholdersArr = [];
if (Doc.Placeholders?.length > 0) {
placeholdersArr = Doc.Placeholders;
const data = {
Name: Doc.Name,
URL: Doc.URL,
SignedUrl: Doc.SignedUrl,
Description: Doc.Description,
Note: Doc.Note,
Placeholders: placeholdersArr,
ExtUserPtr: {
__type: "Pointer",
className: "contracts_Users",
objectId: Doc.ExtUserPtr.objectId
},
CreatedBy: {
__type: "Pointer",
className: "_User",
objectId: Doc.CreatedBy.objectId
},
Signers: signers
};
try {
const res = await axios.post(
`${localStorage.getItem(
"baseUrl"
)}classes/${localStorage.getItem("_appName")}_Document`,
data,
{
headers: {
"Content-Type": "application/json",
"X-Parse-Application-Id":
localStorage.getItem("parseAppId"),
"X-Parse-Session-Token":
localStorage.getItem("accesstoken")
}
}
);
// console.log("Res ", res.data);
if (res.data && res.data.objectId) {
setActLoader({});
setIsAlert(true);
setTimeout(() => setIsAlert(false), 1500);
navigate(`/${act.redirectUrl}/${res.data.objectId}`, {
state: { title: "Use Template" }
});
}
} catch (err) {
console.log("Err", err);
setIsAlert(true);
setAlertMsg({
type: "danger",
message: "Something went wrong, Please try again later!"
});
setTimeout(() => setIsAlert(false), 1500);
setActLoader({});
}
} else {
setIsDocErr(true);
setActLoader({});
}
} else {
setIsAlert(true);
setAlertMsg({
type: "danger",
message: "Something went wrong, Please try again later!"
});
setTimeout(() => setIsAlert(false), 1500);
setActLoader({});
}
} catch (err) {
console.log("err", err);
setIsAlert(true);
setAlertMsg({
type: "danger",
message: "Something went wrong, Please try again later!"
});
setTimeout(() => setIsAlert(false), 1500);
setActLoader({});
}
}
} else {
localStorage.removeItem("rowlevel");
navigate(`/${act.redirectUrl}`);
localStorage.setItem("rowlevel", JSON.stringify(item));
}
};
const handleActionBtn = (act, item) => {
if (act.action === "redirect") {
handleURL(item, act);
} else if (act.action === "delete") {
setIsDeleteModal({ [item.objectId]: true });
} else if (act.action === "share") {
handleShare(item);
} else if (act.action === "revoke") {
setIsRevoke({ [item.objectId]: true });
} else if (act.action === "option") {
setIsOption({ [item.objectId]: !isOption[item.objectId] });
}
};
// Get current list
const indexOfLastDoc = currentPage * props.docPerPage;
const indexOfFirstDoc = indexOfLastDoc - props.docPerPage;
// `currentLists` is total record render on current page
const currentLists = props.List?.slice(indexOfFirstDoc, indexOfLastDoc);
// Change page
const paginateFront = () => setCurrentPage(currentPage + 1);
const paginateBack = () => setCurrentPage(currentPage - 1);
const handleContactFormModal = () => {
setIsContactform(!isContactform);
};
const handleUserData = (data) => {
props.setList((prevData) => [data, ...prevData]);
};
const handleDelete = async (item) => {
setIsDeleteModal({});
setActLoader({ [`${item.objectId}`]: true });
const clsObj = {
Contactbook: "contracts_Contactbook",
Templates: "contracts_Template"
};
try {
const serverUrl = process.env.REACT_APP_SERVERURL
? process.env.REACT_APP_SERVERURL
: window.location.origin + "/api/app";
const cls = clsObj[props.ReportName] || "contracts_Document";
const url = serverUrl + `/classes/${cls}/`;
const body =
props.ReportName === "Contactbook"
? { IsDeleted: true }
: { IsArchive: true };
const res = await axios.put(url + item.objectId, body, {
headers: {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
}
});
// console.log("Res ", res.data);
if (res.data && res.data.updatedAt) {
setActLoader({});
setIsAlert(true);
setAlertMsg({
type: "success",
message: "Record deleted successfully!"
});
setTimeout(() => setIsAlert(false), 1500);
const upldatedList = props.List.filter(
(x) => x.objectId !== item.objectId
);
props.setList(upldatedList);
}
} catch (err) {
console.log("err", err);
setIsAlert(true);
setAlertMsg({
type: "danger",
message: "Something went wrong, Please try again later!"
});
setTimeout(() => setIsAlert(false), 1500);
setActLoader({});
}
};
const handleClose = () => {
setIsRevoke({});
setIsDeleteModal({});
};
const handleShare = (item) => {
setActLoader({ [item.objectId]: true });
const host = window.location.origin;
const getUrl = (x) => {
//encode this url value `${item.objectId}/${x.Email}/${x.objectId}` to base64 using `btoa` function
const encodeBase64 = btoa(`${item.objectId}/${x.Email}/${x.objectId}`);
return `${host}/login/${encodeBase64}`;
};
const urls = item.Signers.map((x) => ({
email: x.Email,
url: getUrl(x)
}));
setShareUrls(urls);
setIsShare({ [item.objectId]: true });
};
const copytoclipboard = (share) => {
navigator.clipboard.writeText(share.url);
setCopied({ ...copied, [share.email]: true });
};
//function to handle revoke/decline docment
const handleRevoke = async (item) => {
setIsRevoke({});
setActLoader({ [`${item.objectId}`]: true });
const data = {
IsDeclined: true
};
await axios
.put(
`${localStorage.getItem("baseUrl")}classes/${localStorage.getItem(
"_appName"
)}_Document/${item.objectId}`,
data,
{
headers: {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
}
}
)
.then(async (result) => {
const res = result.data;
if (res) {
setActLoader({});
setIsAlert(true);
setAlertMsg({
type: "success",
message: "Record revoked successfully!"
});
setTimeout(() => setIsAlert(false), 1500);
const upldatedList = props.List.filter(
(x) => x.objectId !== item.objectId
);
props.setList(upldatedList);
}
})
.catch((err) => {
console.log("err", err);
setIsAlert(true);
setAlertMsg({
type: "danger",
message: "Something went wrong, Please try again later!"
});
setTimeout(() => setIsAlert(false), 1500);
setActLoader({});
});
};
async function checkTourStatus() {
const currentUser = Parse.User.current();
const cloudRes = await Parse.Cloud.run("getUserDetails", {
email: currentUser.get("email")
});
const res = { data: cloudRes.toJSON() };
if (res.data && res.data.TourStatus && res.data.TourStatus.length > 0) {
const tourStatus = res.data.TourStatus;
// console.log("res ", res.data.TourStatus);
setTourStatusArr(tourStatus);
const filteredtourStatus = tourStatus.filter(
(obj) => obj["templateTour"]
);
if (filteredtourStatus.length > 0) {
const templateTour = filteredtourStatus[0]["templateTour"];
if (templateTour) {
setIsTour(false);
} else {
setIsTour(true);
}
} else {
setIsTour(true);
}
} else {
setIsTour(true);
}
}
const closeTour = async () => {
// console.log("closeTour");
setIsTour(false);
if (props.isDontShow) {
const serverUrl = localStorage.getItem("baseUrl");
const appId = localStorage.getItem("parseAppId");
const extUserClass = localStorage.getItem("extended_class");
const json = JSON.parse(localStorage.getItem("Extand_Class"));
const extUserId = json && json.length > 0 && json[0].objectId;
// console.log("extUserId ", extUserId)
let updatedTourStatus = [];
if (tourStatusArr.length > 0) {
updatedTourStatus = [...tourStatusArr];
const templateTourIndex = tourStatusArr.findIndex(
(obj) => obj["templateTour"] === false || obj["templateTour"] === true
);
if (templateTourIndex !== -1) {
updatedTourStatus[templateTourIndex] = { templateTour: true };
} else {
updatedTourStatus.push({ templateTour: true });
}
} else {
updatedTourStatus = [{ templateTour: true }];
}
await axios.put(
serverUrl + "classes/" + extUserClass + "/" + extUserId,
{
TourStatus: updatedTourStatus
},
{
headers: {
"X-Parse-Application-Id": appId
}
}
);
}
};
// `handleDownload` is used to get valid doc url available in completed report
const handleDownload = async (item) => {
const url = item?.SignedUrl || item?.URL || "";
if (url) {
try {
const signedUrl = await Parse.Cloud.run("getsignedurl", { url: url });
saveAs(signedUrl);
} catch (err) {
console.log("err in getsignedurl", err);
alert("something went wrong, please try again later.");
}
}
};
return (
<div className="relative">
{Object.keys(actLoader)?.length > 0 && (
<div className="absolute w-full h-full rounded-md flex justify-center items-center bg-black bg-opacity-30 z-30">
<div
style={{ fontSize: "45px", color: "#3dd3e0" }}
className="loader-37"
></div>
</div>
)}
<div className="p-2 overflow-x-scroll w-full bg-white rounded-md">
{isAlert && <Alert type={alertMsg.type}>{alertMsg.message}</Alert>}
{props.tourData && props.ReportName === "Templates" && (
<Tour
onRequestClose={closeTour}
steps={props.tourData}
isOpen={isTour}
// rounded={5}
closeWithMask={false}
/>
)}
<div className="flex flex-row items-center justify-between my-2 mx-3 text-[20px] md:text-[23px]">
<div className="font-light">
{props.ReportName}{" "}
{props.report_help && (
<span className="text-xs md:text-[13px] font-normal">
<Tooltip message={props.report_help} />
</span>
)}
</div>
{props.ReportName === "Templates" && (
<i
data-tut="reactourFirst"
onClick={() => navigate("/form/template")}
className="fa-solid fa-square-plus text-sky-400 text-[25px]"
></i>
)}
{props.form && (
<div
className="cursor-pointer"
onClick={() => handleContactFormModal()}
>
<i className="fa-solid fa-square-plus text-sky-400 text-[25px]"></i>
</div>
)}
</div>
<table className="table-auto w-full border-collapse">
<thead className="text-[14px]">
<tr className="border-y-[1px]">
{props.heading?.map((item, index) => (
<React.Fragment key={index}>
<th className="px-4 py-2 font-thin">{item}</th>
</React.Fragment>
))}
{props.actions?.length > 0 && (
<th className="px-4 py-2 font-thin">Action</th>
)}
</tr>
</thead>
<tbody className="text-[12px]">
{props.List?.length > 0 && (
<>
{currentLists.map((item, index) =>
props.ReportName === "Contactbook" ? (
<tr className="border-y-[1px]" key={index}>
{props.heading.includes("Sr.No") && (
<td className="px-4 py-2">{startIndex + index + 1}</td>
)}
<td className="px-4 py-2 font-semibold">{item?.Name} </td>
<td className="px-4 py-2">{item?.Email || "-"}</td>
<td className="px-4 py-2">{item?.Phone || "-"}</td>
<td className="px-3 py-2 text-white grid grid-cols-2">
{props.actions?.length > 0 &&
props.actions.map((act, index) => (
<button
key={index}
onClick={() => handleActionBtn(act, item)}
className={`mb-1 flex justify-center items-center gap-1 px-2 py-1 rounded shadow`}
title={act.hoverLabel}
style={{
backgroundColor: act.btnColor
? act.btnColor
: "#3ac9d6",
color: act?.textColor ? act?.textColor : "white"
}}
>
<i className={act.btnIcon}></i>
</button>
))}
{isDeleteModal[item.objectId] && (
<ModalUi
isOpen
title={"Delete Contact"}
handleClose={handleClose}
>
<div className="m-[20px]">
<div className="text-lg font-normal text-black">
Are you sure you want to delete this contact?
</div>
<hr className="bg-[#ccc] mt-4 " />
<div className="flex items-center mt-3 gap-2 text-white">
<button
onClick={() => handleDelete(item)}
className="px-4 py-1.5 text-white rounded shadow-md text-center focus:outline-none "
style={{
backgroundColor: modalSubmitBtnColor
}}
>
Yes
</button>
<button
onClick={handleClose}
className="px-4 py-1.5 text-black border-[1px] border-[#ccc] shadow-md rounded focus:outline-none"
style={{
backgroundColor: modalCancelBtnColor
}}
>
No
</button>
</div>
</div>
</ModalUi>
)}
</td>
</tr>
) : (
<tr className="border-y-[1px]" key={index}>
{props.heading.includes("Sr.No") && (
<td className="px-4 py-2">{startIndex + index + 1}</td>
)}
<td className="px-4 py-2 font-semibold w-56">
{item?.Name}{" "}
</td>
{props.heading.includes("Note") && (
<td className="px-4 py-2">{item?.Note || "-"}</td>
)}
{props.heading.includes("Folder") && (
<td className="px-4 py-2">
{item?.Folder?.Name || "OpenSign™ Drive"}
</td>
)}
<td className="px-4 py-2">
<button
onClick={() => handleDownload(item)}
className="text-[blue] hover:text-[blue] hover:underline focus:outline-none"
title={"Download"}
>
{item?.URL ? "Download" : "-"}
</button>
</td>
<td className="px-4 py-2">
{formatRow(item?.ExtUserPtr)}
</td>
<td className="px-4 py-2">
{item?.Signers ? formatRow(item?.Signers) : "-"}
</td>
<td className="px-2 py-2 text-white flex flex-row gap-x-2 gap-y-1 justify-center items-center">
{props.actions?.length > 0 &&
props.actions.map((act, index) => (
<button
data-tut={act?.selector}
key={index}
onClick={() => handleActionBtn(act, item)}
className={`${
act.action === "option" ? "" : "rounded shadow"
} ${
act.btnLabel ? "py-[3px] px-1.5" : "py-1.5 px-2"
} relative text-center flex items-center justify-center focus:outline-none`}
title={act.hoverLabel}
style={{
backgroundColor: act.btnColor
? act.btnColor
: "#3ac9d6",
color: act?.textColor ? act?.textColor : "white"
}}
>
<i className={act.btnIcon}></i>
{act.btnLabel && (
<span className="ml-[4px] uppercase font-medium">
{act.btnLabel}
</span>
)}
{isOption[item.objectId] &&
act.action === "option" && (
<div className="absolute -right-2 top-5 bg-white rounded shadow z-[20] overflow-hidden">
{act.subaction?.map((subact) => (
<div
key={subact.btnId}
className="hover:bg-gray-300 cursor-pointer px-2 py-1.5 flex justify-start items-center text-black"
onClick={() =>
handleActionBtn(subact, item)
}
title={subact.hoverLabel}
>
<i className={subact.btnIcon}></i>
{subact.btnLabel && (
<span className="ml-[4px] text-xs capitalize">
{subact.btnLabel}
</span>
)}
</div>
))}
</div>
)}
</button>
))}
{isDeleteModal[item.objectId] && (
<ModalUi
isOpen
title={"Delete Document"}
handleClose={handleClose}
>
<div className="m-[20px]">
<div className="text-lg font-normal text-black">
Are you sure you want to delete this document?
</div>
<hr className="bg-[#ccc] mt-4" />
<div className="flex items-center mt-3 gap-2 text-white">
<button
onClick={() => handleDelete(item)}
className="px-4 py-1.5 text-white rounded shadow-md text-center focus:outline-none "
style={{
backgroundColor: modalSubmitBtnColor
}}
>
Yes
</button>
<button
onClick={handleClose}
className="px-4 py-1.5 text-black border-[1px] border-[#ccc] shadow-md rounded focus:outline-none"
style={{
backgroundColor: modalCancelBtnColor
}}
>
No
</button>
</div>
</div>
</ModalUi>
)}
{isShare[item.objectId] && (
<ModalUi
isOpen
title={"Copy link"}
handleClose={() => {
setIsShare({});
setActLoader({});
}}
>
<div className="m-[20px]">
{shareUrls.map((share, i) => (
<div
key={i}
className="text-sm font-normal text-black flex my-2 justify-between items-center"
>
<span className="text-sm font-semibold">
{share.email}
</span>
<div>
<RWebShare
data={{
url: share.url,
title: "Sign url"
}}
>
<button className="bg-[#002864] text-white rounded w-[32px] h-[30px] focus:outline-none">
<i className="fa-solid fa-share-from-square"></i>{" "}
</button>
</RWebShare>
<button
className="ml-2 bg-[#002864] text-white rounded w-[100px] h-[30px] focus:outline-none"
onClick={() => copytoclipboard(share)}
>
<i className="fa-solid fa-link"></i>{" "}
{copied[share.email] ? "Copied" : "Copy"}
</button>
</div>
</div>
))}
</div>
</ModalUi>
)}
{isRevoke[item.objectId] && (
<ModalUi
isOpen
title={"Revoke document"}
handleClose={handleClose}
>
<div className="m-[20px]">
<div className="text-lg font-normal text-black">
Are you sure you want to revoke this document?
</div>
<hr className="bg-[#ccc] mt-4 " />
<div className="flex items-center mt-3 gap-2 text-white">
<button
onClick={() => handleRevoke(item)}
className="px-4 py-1.5 text-white rounded shadow-md text-center focus:outline-none "
style={{
backgroundColor: modalSubmitBtnColor
}}
>
Yes
</button>
<button
onClick={handleClose}
className="px-4 py-1.5 text-black border-[1px] border-[#ccc] shadow-md rounded focus:outline-none"
style={{
backgroundColor: modalCancelBtnColor
}}
>
No
</button>
</div>
</div>
</ModalUi>
)}
</td>
</tr>
)
)}
</>
)}
</tbody>
</table>
<div className="flex flex-wrap items-center gap-2 p-2 ">
{props.List.length > props.docPerPage && (
<>
{currentPage > 1 && (
<button
onClick={() => paginateBack()}
className="bg-blue-400 text-white rounded px-3 py-2 focus:outline-none"
>
Prev
</button>
)}
</>
)}
{pageNumbers.map((x) => (
<button
key={x}
onClick={() => setCurrentPage(x)}
className=" bg-sky-400 text-white rounded px-3 py-2 focus:outline-none"
>
{x}
</button>
))}
{isMoreDocs && (
<button className="text-black rounded px-1 py-2">...</button>
)}
{props.List.length > props.docPerPage && (
<>
{pageNumbers.includes(currentPage + 1) && (
<button
onClick={() => paginateFront()}
className="bg-blue-400 text-white rounded px-3 py-2 focus:outline-none"
>
Next
</button>
)}
</>
)}
</div>
{props.List?.length <= 0 && (
<div className="flex flex-col items-center justify-center w-full bg-white rounded py-4">
<div className="w-[60px] h-[60px] overflow-hidden">
<img
className="w-full h-full object-contain"
src={pad}
alt="img"
/>
</div>
<div className="text-sm font-semibold">No Data Available</div>
</div>
)}
<ModalUi
title={"Add Contact"}
isOpen={isContactform}
handleClose={handleContactFormModal}
>
<AddSigner
handleUserData={handleUserData}
closePopup={handleContactFormModal}
/>
</ModalUi>
<ModalUi
headColor={"#dc3545"}
isOpen={isDocErr}
title={"Receipent required"}
handleClose={() => setIsDocErr(false)}
>
<div style={{ height: "100%", padding: 20 }}>
<p>Please add receipent in template!</p>
</div>
</ModalUi>
</div>
</div>
);
};
export default ReportTable;