Files
OpenSign/apps/OpenSign/src/pages/Report.jsx
T
2026-03-16 12:14:55 +00:00

364 lines
12 KiB
React

import { useEffect, useState, useRef } from "react";
import Parse from "parse";
import axios from "axios";
import reportJson, { extraCols } from "../json/ReportJson";
import { useParams } from "react-router";
import PageNotFound from "./PageNotFound";
import Loader from "../primitives/Loader";
import Contactbook from "../reports/contact/Contactbook";
import ColumnSelector from "../components/ColumnSelector";
import TemplatesReport from "../reports/template/TemplatesReport";
import DocumentsReport from "../reports/document/DocumentsReport";
import { templateReportTour } from "../json/ReportTour";
import { withSessionValidation } from "../utils";
const Report = () => {
const { id } = useParams();
const abortController = new AbortController();
const [list, setList] = useState([]);
const [isLoader, setIsLoader] = useState(true);
const [reportName, setReportName] = useState("");
const [reporthelp, setReportHelp] = useState("");
const [actions, setActions] = useState([]);
const [heading, setHeading] = useState([]);
const [isNextRecord, setIsNextRecord] = useState(false);
const [isMoreDocs, setIsMoreDocs] = useState(true);
const [tourData, setTourData] = useState([]);
const [searchTerm, setSearchTerm] = useState("");
const [signerStatusFilter, setSignerStatusFilter] = useState("all");
const [mobileSearchOpen, setMobileSearchOpen] = useState(false);
const [isSearchResult, setIsSearchResult] = useState(false);
const [allColumns, setAllColumns] = useState([]);
const [visibleColumns, setVisibleColumns] = useState([]);
const [columnLabels, setColumnLabels] = useState({});
const [defaultColumns, setDefaultColumns] = useState([]);
const [isColumnModal, setIsColumnModal] = useState(false);
const [searchLoader, setSearchLoader] = useState(false);
const debounceTimer = useRef(null);
const hasMountedSignerFilter = useRef(false);
const searchAbortRef = useRef(null); // holds AbortController
// Number of documents to display per page (should always be half of docLimit for proper pagination)
const docPerPage = 10;
// Number of documents to fetch per API call
const docLimit = 20;
// below useEffect is call when id param change
useEffect(() => {
setReportName("");
setList([]);
setSearchTerm("");
setSignerStatusFilter("all");
setMobileSearchOpen(false);
const saved = JSON.parse(localStorage.getItem("reportColumns") || "{}");
if (saved[id]) {
setVisibleColumns(saved[id].visible || saved[id]);
setColumnLabels(saved[id].labels || {});
} else {
setVisibleColumns([]);
setColumnLabels({});
}
getReportData(0, docLimit, "");
// Function returned from useEffect is called on unmount
return () => {
setIsLoader(true);
setList([]);
setIsNextRecord(false);
// Here it'll abort the fetch
abortController.abort();
if (searchAbortRef.current) {
searchAbortRef.current.abort();
searchAbortRef.current = null;
}
};
// eslint-disable-next-line
}, [id]);
// below useEffect call when isNextRecord state is true and fetch next record
useEffect(() => {
if (isNextRecord) {
getReportData(list?.length, docLimit, searchTerm);
}
// eslint-disable-next-line
}, [isNextRecord]);
useEffect(() => {
if (!hasMountedSignerFilter.current) {
hasMountedSignerFilter.current = true;
return;
}
setList([]);
setIsNextRecord(false);
setIsMoreDocs(true);
getDocumentbyUserStatus();
// eslint-disable-next-line
}, [signerStatusFilter]);
const getDocumentbyUserStatus = async () => {
setSearchLoader(true);
try {
await getReportData(0, docLimit, searchTerm, signerStatusFilter);
} catch (error) {
console.error("get report by user status error", error);
} finally {
setSearchLoader(false);
}
};
const handleSearchChange = withSessionValidation(async (e) => {
const term = e.target.value.toLowerCase();
setSearchTerm(term);
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
// 2) abort any in-flight request triggered by older debounce
if (searchAbortRef.current) {
searchAbortRef.current.abort();
searchAbortRef.current = null;
}
debounceTimer.current = setTimeout(async () => {
// create controller for THIS request
const controller = new AbortController();
searchAbortRef.current = controller;
setSearchLoader(true);
try {
const headers = {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
sessiontoken: localStorage.getItem("accesstoken")
};
const url = `${localStorage.getItem("baseUrl")}functions/getReport`;
const res = await axios.post(
url,
{
reportId: id,
searchTerm: term,
signerStatus: signerStatusFilter,
skip: 0,
limit: docPerPage
},
{ headers: headers, signal: controller.signal } // ✅ axios abort
);
// if you want to be extra safe (ignore late responses)
if (controller.signal.aborted) return;
const data = res.data?.result || [];
if (!data.error) {
setList(data);
setIsMoreDocs(data.length >= docPerPage);
setIsNextRecord(false);
setIsSearchResult(true);
setSearchLoader(false);
}
} catch (err) {
// ✅ ignore abort errors
const isAbort =
err?.name === "CanceledError" ||
err?.code === "ERR_CANCELED" ||
err?.message?.toLowerCase?.().includes("canceled");
if (!isAbort) console.error("Search error:", err);
setSearchLoader(false);
}
}, 300);
setIsSearchResult(false);
});
const handleSearchPaste = (e) => {
setTimeout(() => {
handleSearchChange({ target: { value: e.target.value } });
}, 0);
};
useEffect(() => {
return () => {
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
};
}, []);
const getReportData = withSessionValidation(
async (
skipUserRecord = 0,
limit = 20,
term = searchTerm,
signerStatus = signerStatusFilter
) => {
// setIsLoader(true);
const json = reportJson(id);
if (json) {
setActions(json.actions);
const savedCols = JSON.parse(
localStorage.getItem("reportColumns") || "{}"
);
const visible = savedCols[id]?.visible || json.heading;
const labels = savedCols[id]?.labels || {};
if (!savedCols[id] || id === "contacts") {
savedCols[id] = { visible: json.heading, labels: {} };
localStorage.setItem("reportColumns", JSON.stringify(savedCols));
}
setVisibleColumns(visible);
setColumnLabels(labels);
setHeading(visible);
setDefaultColumns(json.heading);
setReportName(json.reportName);
setReportHelp(json?.helpMsg);
const currentUser = Parse.User.current().id;
const headers = {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
sessiontoken: localStorage.getItem("accesstoken")
};
try {
const skipRecord = id === "4Hhwbp482K" ? 0 : skipUserRecord;
const limitRecord = id === "4Hhwbp482K" ? 200 : limit;
const params = { reportId: id, skip: skipRecord, limit: limitRecord };
if (term) {
params.searchTerm = term;
}
if (signerStatus && signerStatus !== "all") {
params.signerStatus = signerStatus;
}
const url = `${localStorage.getItem("baseUrl")}functions/getReport`;
const res = await axios.post(url, params, {
headers: headers,
signal: abortController.signal // is used to cancel fetch query
});
const extraHeads =
id === "4Hhwbp482K" ? [...extraCols, "Expiry Date"] : extraCols;
setAllColumns(Array.from(new Set([...json.heading, ...extraHeads])));
if (id === "6TeaPr321t") {
setTourData(templateReportTour);
}
if (id === "4Hhwbp482K") {
const listData = res.data?.result.filter(
(x) => x.Signers.length > 0
);
let arr = [];
for (const obj of listData) {
const isSigner = obj?.Signers?.some(
(item) => item.UserId.objectId === currentUser
);
if (isSigner) {
let isRecord;
if (obj?.AuditTrail && obj?.AuditTrail.length > 0) {
isRecord = obj?.AuditTrail.some(
(item) =>
item?.UserPtr?.UserId?.objectId === currentUser &&
item.Activity === "Signed"
);
} else {
isRecord = false;
}
if (isRecord === false) {
arr.push(obj);
}
}
}
if (arr.length === docPerPage) {
setIsMoreDocs(true);
} else {
setIsMoreDocs(false);
}
setList((prevRecord) =>
prevRecord.length > 0 ? [...prevRecord, ...arr] : arr
);
} else {
if (res.data.result.length >= docPerPage) {
setIsMoreDocs(true);
} else {
setIsMoreDocs(false);
}
if (!res.data.result.error) {
setIsNextRecord(false);
setList((prevRecord) =>
prevRecord.length > 0
? [...prevRecord, ...res.data.result]
: res.data.result
);
}
}
setIsLoader(false);
} catch (err) {
const isCancel = axios.isCancel(err);
if (!isCancel) {
console.error("getreport error", err);
setIsLoader(false);
}
}
} else {
setIsLoader(false);
}
}
);
const commonProps = {
ReportName: reportName,
List: list,
setList,
actions: actions,
heading: heading,
setIsNextRecord,
isMoreDocs,
docPerPage,
mobileSearchOpen,
setMobileSearchOpen,
searchTerm,
handleSearchChange,
handleSearchPaste,
isSearchResult,
columnLabels,
searchLoader,
signerStatusFilter,
handleSignerStatusFilter: setSignerStatusFilter,
openColumnModal: () => setIsColumnModal(true)
};
return (
<>
{isLoader ? (
<div className="h-[100vh] flex justify-center items-center">
<Loader />
</div>
) : (
<>
{id === "contacts" ? (
<Contactbook {...commonProps} />
) : id === "6TeaPr321t" ? (
<TemplatesReport
{...commonProps}
report_help={reporthelp}
tourData={tourData}
/>
) : reportName ? (
<DocumentsReport {...commonProps} report_help={reporthelp} />
) : (
<PageNotFound prefix={"Report"} />
)}
</>
)}
<ColumnSelector
isOpen={isColumnModal}
allColumns={allColumns}
visibleColumns={visibleColumns}
columnLabels={columnLabels}
defaultColumns={defaultColumns}
onApply={(cols, labels) => {
setVisibleColumns(cols);
setHeading(cols);
setColumnLabels(labels);
const saved = JSON.parse(
localStorage.getItem("reportColumns") || "{}"
);
saved[id] = { visible: cols, labels };
localStorage.setItem("reportColumns", JSON.stringify(saved));
}}
onClose={() => setIsColumnModal(false)}
/>
</>
);
};
export default Report;