mirror of
https://github.com/OpenSignLabs/OpenSign.git
synced 2026-08-17 21:25:54 +02:00
Merge pull request #772 from OpenSignLabs/feat_bulksend
Feat: quick send feature to create & send multiple documents at a time
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import axios from "axios";
|
||||
import SuggestionInput from "./shared/fields/SuggestionInput";
|
||||
|
||||
const BulkSendUi = (props) => {
|
||||
const [forms, setForms] = useState([]);
|
||||
const [formId, setFormId] = useState(2);
|
||||
const formRef = useRef(null);
|
||||
const [scrollOnNextUpdate, setScrollOnNextUpdate] = useState(false);
|
||||
const [isSubmit, setIsSubmit] = useState(false);
|
||||
const [allowedForm, setAllowedForm] = useState(0);
|
||||
const allowedSigners = 50;
|
||||
useEffect(() => {
|
||||
if (scrollOnNextUpdate && formRef.current) {
|
||||
formRef.current.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "end",
|
||||
inline: "nearest"
|
||||
});
|
||||
setScrollOnNextUpdate(false);
|
||||
}
|
||||
}, [forms, scrollOnNextUpdate]);
|
||||
|
||||
useEffect(() => {
|
||||
(() => {
|
||||
if (props?.Placeholders?.length > 0) {
|
||||
let users = [];
|
||||
props?.Placeholders?.forEach((element) => {
|
||||
if (!element.signerObjId) {
|
||||
users = [
|
||||
...users,
|
||||
{
|
||||
fieldId: element.Id,
|
||||
email: "",
|
||||
label: element.Role,
|
||||
signer: {}
|
||||
}
|
||||
];
|
||||
}
|
||||
});
|
||||
setForms((prevForms) => [...prevForms, { Id: 1, fields: users }]);
|
||||
const totalForms = Math.floor(allowedSigners / users?.length);
|
||||
setAllowedForm(totalForms);
|
||||
}
|
||||
})();
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
const handleInputChange = (index, signer, fieldIndex) => {
|
||||
const newForms = [...forms];
|
||||
newForms[index].fields[fieldIndex].email = signer?.Email
|
||||
? signer?.Email
|
||||
: signer || "";
|
||||
newForms[index].fields[fieldIndex].signer = signer?.objectId ? signer : "";
|
||||
setForms(newForms);
|
||||
};
|
||||
|
||||
const handleAddForm = (e) => {
|
||||
e.preventDefault();
|
||||
// Check if the quick send limit has been reached
|
||||
if (forms?.length < allowedForm) {
|
||||
if (props?.Placeholders.length > 0) {
|
||||
let newForm = [];
|
||||
props?.Placeholders?.forEach((element) => {
|
||||
if (!element.signerObjId) {
|
||||
newForm = [
|
||||
...newForm,
|
||||
{
|
||||
fieldId: element.Id,
|
||||
email: "",
|
||||
label: element.Role,
|
||||
signer: {}
|
||||
}
|
||||
];
|
||||
}
|
||||
});
|
||||
setForms([...forms, { Id: formId, fields: newForm }]);
|
||||
}
|
||||
setFormId(formId + 1);
|
||||
setScrollOnNextUpdate(true);
|
||||
} else {
|
||||
// If the limit has been reached, throw an error with the appropriate message
|
||||
alert("Quick send reached limit.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveForm = (index) => {
|
||||
const updatedForms = forms.filter((_, i) => i !== index);
|
||||
setForms(updatedForms);
|
||||
};
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsSubmit(true);
|
||||
|
||||
// Create a copy of Placeholders array from props.item
|
||||
let Placeholders = [...props.item.Placeholders];
|
||||
// Initialize an empty array to store updated documents
|
||||
let Documents = [];
|
||||
|
||||
// Loop through each form
|
||||
forms.forEach((form) => {
|
||||
// Map through the copied Placeholders array to update email values
|
||||
const updatedPlaceholders = Placeholders.map((placeholder) => {
|
||||
// Find the field in the current form that matches the placeholder Id
|
||||
const field = form.fields.find(
|
||||
(element) => parseInt(element.fieldId) === placeholder.Id
|
||||
);
|
||||
// If a matching field is found, update the email value in the placeholder
|
||||
const signer = field?.signer?.objectId ? field.signer : {};
|
||||
if (field) {
|
||||
return {
|
||||
...placeholder,
|
||||
email: field.email,
|
||||
signerObjId: field?.signer?.objectId || "",
|
||||
signerPtr: signer
|
||||
};
|
||||
}
|
||||
// If no matching field is found, keep the placeholder as is
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
// Push a new document object with updated Placeholders into the Documents array
|
||||
Documents.push({ ...props.item, Placeholders: updatedPlaceholders });
|
||||
});
|
||||
// console.log("Documents ", Documents);
|
||||
await batchQuery(Documents);
|
||||
};
|
||||
|
||||
const batchQuery = async (Documents) => {
|
||||
const serverUrl = localStorage.getItem("baseUrl");
|
||||
const functionsUrl = `${serverUrl}functions/batchdocuments`;
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
|
||||
sessionToken: localStorage.getItem("accesstoken")
|
||||
};
|
||||
const params = {
|
||||
Documents: JSON.stringify(Documents)
|
||||
};
|
||||
try {
|
||||
const res = await axios.post(functionsUrl, params, { headers: headers });
|
||||
// console.log("res ", res);
|
||||
if (res.data && res.data.result) {
|
||||
props.handleClose("success", Documents?.length);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("Err ", err);
|
||||
props.handleClose("error", 0);
|
||||
} finally {
|
||||
setIsSubmit(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isSubmit && (
|
||||
<div className="absolute z-[999] h-full w-full flex justify-center items-center bg-black bg-opacity-40">
|
||||
<div
|
||||
style={{
|
||||
fontSize: "45px",
|
||||
color: "#3dd3e0"
|
||||
}}
|
||||
className="loader-37 "
|
||||
></div>
|
||||
</div>
|
||||
)}
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className=" min-h-max max-h-[250px] overflow-y-auto">
|
||||
{forms?.map((form, index) => (
|
||||
<div
|
||||
key={form.Id}
|
||||
className="p-3 rounded-xl border-[1px] border-gray-400 m-4 bg-white text-black grid grid-cols-1 md:grid-cols-2 gap-2 relative"
|
||||
>
|
||||
{form?.fields?.map((field, fieldIndex) => (
|
||||
<div className="flex flex-col " key={field.fieldId}>
|
||||
<label>{field.label}</label>
|
||||
<SuggestionInput
|
||||
required
|
||||
type="email"
|
||||
value={field.value}
|
||||
index={fieldIndex}
|
||||
onChange={(signer) =>
|
||||
handleInputChange(index, signer, fieldIndex)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{index > 0 && (
|
||||
<button
|
||||
onClick={() => handleRemoveForm(index)}
|
||||
className="absolute right-3 top-1 border border-gray-300 rounded-lg px-2 py-1"
|
||||
>
|
||||
<i className="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
)}
|
||||
<div ref={formRef}></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col mx-4 mb-4 gap-3">
|
||||
<button
|
||||
onClick={handleAddForm}
|
||||
className="bg-[#32a3ac] p-2 text-white w-full rounded-full focus:outline-none"
|
||||
>
|
||||
<i className="fa-solid fa-plus"></i> <span>Add new</span>
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-[#32a3ac] p-2 text-white w-full rounded-full focus:outline-none"
|
||||
>
|
||||
<i className="fa-solid fa-paper-plane"></i> <span>Send</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BulkSendUi;
|
||||
@@ -0,0 +1,92 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { findContact } from "../../../constant/Utils";
|
||||
const SuggestionInput = (props) => {
|
||||
const [inputValue, setInputValue] = useState(props?.value || "");
|
||||
const [suggestions, setSuggestions] = useState([]);
|
||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||
const ref = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("mousedown", Clickout);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", Clickout);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const Clickout = (event) => {
|
||||
if (ref.current && !ref.current.contains(event.target)) {
|
||||
setShowSuggestions(false);
|
||||
}
|
||||
};
|
||||
// create debounce to avoid unnecessay api calls
|
||||
useEffect(() => {
|
||||
let timer;
|
||||
if (inputValue) {
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
(async () => {
|
||||
const res = await findContact(inputValue);
|
||||
if (res?.length > 0) {
|
||||
setSuggestions(res);
|
||||
setShowSuggestions(true);
|
||||
} else {
|
||||
setSuggestions(res);
|
||||
setShowSuggestions(false);
|
||||
}
|
||||
})();
|
||||
}, 1000);
|
||||
}
|
||||
return () => clearTimeout(timer);
|
||||
}, [inputValue]);
|
||||
|
||||
const handleInputChange = async (e) => {
|
||||
const value = e.target.value;
|
||||
setInputValue(value);
|
||||
if (props.onChange) {
|
||||
props.onChange(value);
|
||||
}
|
||||
|
||||
if (value.trim() === "") {
|
||||
setSuggestions([]);
|
||||
setShowSuggestions(false);
|
||||
return;
|
||||
}
|
||||
};
|
||||
const handleSuggestionClick = (suggestion) => {
|
||||
setInputValue(suggestion.Email);
|
||||
setSuggestions([]);
|
||||
setShowSuggestions(false);
|
||||
if (props.onChange) {
|
||||
props.onChange(suggestion);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="flex flex-col items-center relative">
|
||||
<input
|
||||
type={props?.type || "text"}
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Enter text..."
|
||||
className="w-full border-[1px] border-gray-400 p-2 text-black rounded"
|
||||
required={props.required}
|
||||
/>
|
||||
{showSuggestions && (
|
||||
<ul
|
||||
ref={ref}
|
||||
className="absolute z-50 left-0 top-[2.55rem] w-full max-h-[100px] overflow-auto bg-white border border-gray-300 rounded shadow-md"
|
||||
>
|
||||
{suggestions.map((suggestion, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="py-2 px-2 w-full text-sm cursor-pointer hover:bg-gray-100"
|
||||
onClick={() => handleSuggestionClick(suggestion)}
|
||||
>
|
||||
{suggestion.Email}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default SuggestionInput;
|
||||
@@ -2030,3 +2030,24 @@ export const handleSendOTP = async (email) => {
|
||||
alert(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
export async function findContact(value) {
|
||||
try {
|
||||
const currentUser = Parse.User.current();
|
||||
const contactbook = new Parse.Query("contracts_Contactbook");
|
||||
contactbook.equalTo(
|
||||
"CreatedBy",
|
||||
Parse.User.createWithoutData(currentUser.id)
|
||||
);
|
||||
contactbook.notEqualTo("IsDeleted", true);
|
||||
contactbook.matches("Email", new RegExp(value, "i"));
|
||||
|
||||
const contactRes = await contactbook.find();
|
||||
if (contactRes) {
|
||||
const res = JSON.parse(JSON.stringify(contactRes));
|
||||
return res;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching suggestions:", error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,6 +360,14 @@ export default function reportJson(id) {
|
||||
redirectUrl: "template",
|
||||
action: "redirect"
|
||||
},
|
||||
{
|
||||
btnId: "1631",
|
||||
btnLabel: "Quick send",
|
||||
hoverLabel: "Quick send",
|
||||
btnIcon: "fa-solid fa-envelope",
|
||||
redirectUrl: "",
|
||||
action: "bulksend"
|
||||
},
|
||||
{
|
||||
btnId: "1834",
|
||||
btnLabel: "Delete",
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import "../styles/loginPage.css";
|
||||
import loader from "../assets/images/loader2.gif";
|
||||
import axios from "axios";
|
||||
import { isEnableSubscription, themeColor } from "../constant/const";
|
||||
import {
|
||||
isEnableSubscription,
|
||||
modalSubmitBtnColor,
|
||||
themeColor
|
||||
} from "../constant/const";
|
||||
import { contractUsers, getAppLogo } from "../constant/Utils";
|
||||
import logo from "../assets/images/logo.png";
|
||||
import { appInfo } from "../constant/appinfo";
|
||||
import Parse from "parse";
|
||||
|
||||
function GuestLogin() {
|
||||
const { id, userMail, contactBookId, base64url } = useParams();
|
||||
let navigate = useNavigate();
|
||||
const navigate = useNavigate();
|
||||
const [email, setEmail] = useState(userMail);
|
||||
const [OTP, setOTP] = useState("");
|
||||
const [EnterOTP, setEnterOtp] = useState(false);
|
||||
@@ -20,9 +24,9 @@ function GuestLogin() {
|
||||
const [documentId, setDocumentId] = useState(id);
|
||||
const [contactId, setContactId] = useState(contactBookId);
|
||||
const [sendmail, setSendmail] = useState();
|
||||
const [contact, setContact] = useState({ name: "", phone: "", email: "" });
|
||||
useEffect(() => {
|
||||
handleServerUrl();
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
@@ -53,54 +57,51 @@ function GuestLogin() {
|
||||
//split url in array from '/'
|
||||
const checkSplit = decodebase64.split("/");
|
||||
setDocumentId(checkSplit[0]);
|
||||
setContact((prev) => ({ ...prev, email: checkSplit[1] }));
|
||||
setEmail(checkSplit[1]);
|
||||
setContactId(checkSplit[2]);
|
||||
const contactId = checkSplit?.[2];
|
||||
setSendmail(checkSplit[3]);
|
||||
if (!contactId) {
|
||||
const params = { email: checkSplit[1], docId: checkSplit[0] };
|
||||
try {
|
||||
const linkContactRes = await Parse.Cloud.run(
|
||||
"linkcontacttodoc",
|
||||
params
|
||||
);
|
||||
// console.log("linkContactRes ", linkContactRes);
|
||||
setContactId(linkContactRes?.contactId);
|
||||
} catch (err) {
|
||||
console.log("Err in link ext contact", err);
|
||||
}
|
||||
} else {
|
||||
setContactId(checkSplit[2]);
|
||||
}
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { value } = event.target;
|
||||
setOTP(value);
|
||||
};
|
||||
|
||||
//send email OTP function
|
||||
const SendOtp = async (e) => {
|
||||
const serverUrl =
|
||||
localStorage.getItem("baseUrl") && localStorage.getItem("baseUrl");
|
||||
const parseId =
|
||||
localStorage.getItem("parseAppId") && localStorage.getItem("parseAppId");
|
||||
if (serverUrl && localStorage) {
|
||||
setLoading(true);
|
||||
e.preventDefault();
|
||||
setEmail(email);
|
||||
|
||||
try {
|
||||
let url = `${serverUrl}functions/SendOTPMailV1/`;
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Parse-Application-Id": parseId
|
||||
};
|
||||
let body = {
|
||||
email: email.toString(),
|
||||
docId: documentId
|
||||
};
|
||||
let Otp = await axios.post(url, body, { headers: headers });
|
||||
|
||||
if (Otp) {
|
||||
setLoading(false);
|
||||
setEnterOtp(true);
|
||||
}
|
||||
} catch (error) {
|
||||
alert("something went wrong!");
|
||||
const SendOtp = async () => {
|
||||
setLoading(true);
|
||||
setEmail(email);
|
||||
try {
|
||||
const params = { email: email.toString(), docId: documentId };
|
||||
const Otp = await Parse.Cloud.run("SendOTPMailV1", params);
|
||||
if (Otp) {
|
||||
setLoading(false);
|
||||
setEnterOtp(true);
|
||||
}
|
||||
} else {
|
||||
} catch (error) {
|
||||
alert("something went wrong!");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendOTPBtn = async (e) => {
|
||||
e.preventDefault();
|
||||
await SendOtp();
|
||||
};
|
||||
|
||||
//verify OTP send on via email
|
||||
const VerifyOTP = async (e) => {
|
||||
e.preventDefault();
|
||||
@@ -164,156 +165,171 @@ function GuestLogin() {
|
||||
alert("Please Enter OTP!");
|
||||
}
|
||||
};
|
||||
|
||||
const handleUserData = async (e) => {
|
||||
e.preventDefault();
|
||||
const params = { ...contact, docId: documentId };
|
||||
try {
|
||||
const linkContactRes = await Parse.Cloud.run("linkcontacttodoc", params);
|
||||
// console.log("linkContactRes ", linkContactRes);
|
||||
setContactId(linkContactRes.contactId);
|
||||
setLoading(true);
|
||||
setEnterOtp(true);
|
||||
await SendOtp();
|
||||
} catch (err) {
|
||||
setLoading(false);
|
||||
alert("something went wron, please try agian later.");
|
||||
console.log("Err in link ext contact", err);
|
||||
}
|
||||
};
|
||||
const handleInputChange = (e) => {
|
||||
setContact((prev) => ({ ...prev, [e.target.name]: e.target.value }));
|
||||
};
|
||||
return (
|
||||
<div style={{ padding: "2rem", background: "white" }}>
|
||||
{isLoading ? (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: "100vh",
|
||||
flexDirection: "column"
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt="no img"
|
||||
src={loader}
|
||||
style={{ width: "80px", height: "80px" }}
|
||||
/>
|
||||
<div className="flex flex-col justify-center items-center h-[100vh]">
|
||||
<img className="w-[80px] h-[80px]" alt="loader" src={loader} />
|
||||
<span style={{ fontSize: "13px", color: "gray" }}>
|
||||
{isLoading.message}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
margin: "10px",
|
||||
border: "0.5px solid #c5c7c9",
|
||||
padding: "30px",
|
||||
boxShadow: "rgba(99, 99, 99, 0.2) 0px 2px 8px 0px"
|
||||
}}
|
||||
>
|
||||
<div className="main_head">
|
||||
<div className="w-[250px] h-[66px] inline-block overflow-hidden">
|
||||
{appLogo && (
|
||||
<img
|
||||
src={appLogo}
|
||||
className="object-contain h-full"
|
||||
alt="logo"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="m-1 md:m-2 border-[0.5px] border-[#c5c7c9] p-[30px] shadow-md">
|
||||
<div className="md:w-[250px] md:h-[66px] inline-block overflow-hidden mt-2 mb-11">
|
||||
{appLogo && (
|
||||
<img src={appLogo} className="object-contain h-full" alt="logo" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!EnterOTP ? (
|
||||
<div className="row">
|
||||
<div className="col-sm-6 KLO">
|
||||
<span className="welcomeText">Welcome Back !</span>
|
||||
<br />
|
||||
<span className="KNLO">
|
||||
Verification code is sent to your email
|
||||
</span>
|
||||
<div className="card card-box" style={{ borderRadius: "0px" }}>
|
||||
<div className="card-body">
|
||||
{contactId ? (
|
||||
<>
|
||||
{!EnterOTP ? (
|
||||
<div className="w-full md:w-[50%]">
|
||||
<h1 className="text-2xl md:text-[30px]">Welcome Back!</h1>
|
||||
<legend className="text-[12px] text-[#878787] mt-2">
|
||||
Verification code is sent to your email
|
||||
</legend>
|
||||
<div className="p-[20px] outline outline-1 outline-slate-300/50 my-2 rounded shadow-md">
|
||||
<input
|
||||
type="email"
|
||||
name="mobile"
|
||||
value={email}
|
||||
className="outline-none px-3 py-2 w-full border-[1px] border-gray-300 rounded text-xs disabled:bg-[#e0e5f5]"
|
||||
disabled
|
||||
className="loginInput"
|
||||
/>
|
||||
<br />
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<button
|
||||
className="w-[100px] text-white text-sm font-medium py-1 rounded-sm shadow-md hover:shadow-lg focus:outline-none"
|
||||
style={{ background: themeColor }}
|
||||
onClick={(e) => handleSendOTPBtn(e)}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "Loading..." : "Send OTP"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="btnContainer">
|
||||
{loading ? (
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
background: themeColor,
|
||||
color: "white"
|
||||
}}
|
||||
className="verifyBtn"
|
||||
disabled
|
||||
>
|
||||
<span
|
||||
className="spinner-border spinner-border-sm "
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Loading...
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="verifyBtn"
|
||||
style={{
|
||||
background: themeColor,
|
||||
color: "white",
|
||||
marginLeft: "0px !important"
|
||||
}}
|
||||
onClick={(e) => SendOtp(e)}
|
||||
>
|
||||
Send OTP
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="row">
|
||||
<div className="col-sm-6 KLO">
|
||||
<span className="welcomeText">Welcome Back !</span>
|
||||
<br />
|
||||
<span className="KNLO">You will get a OTP via Email</span>
|
||||
<div className="card card-box">
|
||||
<div className="card-body">
|
||||
<label>Enter Verification Code</label>
|
||||
) : (
|
||||
<form className="w-full md:w-[50%]" onSubmit={VerifyOTP}>
|
||||
<h1 className="text-2xl md:text-[30px]">Welcome Back!</h1>
|
||||
<legend className="text-[12px] text-[#878787] mt-2">
|
||||
You will get a OTP via Email
|
||||
</legend>
|
||||
<div className="p-[20px] pt-[15px] outline outline-1 outline-slate-300/50 my-2 rounded shadow-md">
|
||||
<p>Enter Verification Code</p>
|
||||
<input
|
||||
type="number"
|
||||
className="loginInput"
|
||||
className="mt-1 outline-none px-3 py-2 w-full border-[1px] border-gray-300 rounded text-xs bg-[#e0e5f5]"
|
||||
name="OTP"
|
||||
value={OTP}
|
||||
onChange={handleChange}
|
||||
onChange={(e) => setOTP(e.target.value)}
|
||||
/>
|
||||
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{loading ? (
|
||||
<div className="mt-4">
|
||||
<button
|
||||
style={{
|
||||
background: themeColor,
|
||||
color: "white"
|
||||
}}
|
||||
className="verifyBtn"
|
||||
type="button"
|
||||
disabled
|
||||
style={{ background: themeColor }}
|
||||
className="w-[100px] text-white text-sm font-medium py-1 rounded-sm shadow-md hover:shadow-lg focus:outline-none"
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
>
|
||||
<span
|
||||
className="spinner-border spinner-border-sm "
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Loading...
|
||||
{loading ? "Loading..." : "Verify"}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => VerifyOTP(e)}
|
||||
style={{
|
||||
background: themeColor,
|
||||
color: "white"
|
||||
}}
|
||||
className="verifyBtn"
|
||||
>
|
||||
Verify
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="w-full md:w-[50%]">
|
||||
<h1 className="text-2xl md:text-[30px]">Welcome</h1>
|
||||
<legend className="text-[12px] text-[#878787] mt-2">
|
||||
Provide your details
|
||||
</legend>
|
||||
<form
|
||||
className="p-[20px] pt-[15px] outline outline-1 outline-slate-300/50 my-2 rounded shadow-md"
|
||||
onSubmit={handleUserData}
|
||||
>
|
||||
<div className="mb-3">
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="block text-xs text-gray-700 font-semibold"
|
||||
>
|
||||
Name
|
||||
<span className="text-[red] text-[13px]"> *</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
value={contact.name}
|
||||
onChange={handleInputChange}
|
||||
className="px-3 py-2 w-full border-[1px] border-gray-300 rounded disabled:bg-[#e0e5f5] focus:outline-none text-xs"
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-xs text-gray-700 font-semibold"
|
||||
>
|
||||
Email
|
||||
<span className="text-[red] text-[13px]"> *</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
value={contact.email}
|
||||
onChange={handleInputChange}
|
||||
className="px-3 py-2 w-full border-[1px] border-gray-300 disabled:bg-[#e0e5f5] rounded focus:outline-none text-xs"
|
||||
required
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label
|
||||
htmlFor="phone"
|
||||
className="block text-xs text-gray-700 font-semibold"
|
||||
>
|
||||
Phone
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="phone"
|
||||
value={contact.phone}
|
||||
onChange={handleInputChange}
|
||||
className="px-3 py-2 w-full border-[1px] border-gray-300 rounded focus:outline-none text-xs"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 flex justify-start">
|
||||
<button
|
||||
type="submit"
|
||||
style={{ backgroundColor: modalSubmitBtnColor }}
|
||||
className="mr-2 px-[20px] py-1.5 text-white rounded shadow-md text-center focus:outline-none "
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "Loading..." : "Next"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -41,6 +41,7 @@ import DefaultSignature from "../components/pdf/DefaultSignature";
|
||||
import ModalUi from "../primitives/ModalUi";
|
||||
import VerifyEmail from "../components/pdf/VerifyEmail";
|
||||
import TourContentWithBtn from "../primitives/TourContentWithBtn";
|
||||
import { appInfo } from "../constant/appinfo";
|
||||
function useQuery() {
|
||||
return new URLSearchParams(useLocation().search);
|
||||
}
|
||||
@@ -460,7 +461,10 @@ function PdfRequestFiles() {
|
||||
} else {
|
||||
//else condition to check current user exist in contracts_Users class and check tour message status
|
||||
//if not then check user exist in contracts_Contactbook class and check tour message status
|
||||
const currentUser = JSON.parse(JSON.stringify(Parse.User.current()));
|
||||
const localuser = localStorage.getItem(
|
||||
`Parse/${appInfo.appId}/currentUser`
|
||||
);
|
||||
const currentUser = JSON.parse(JSON.stringify(localuser));
|
||||
const currentUserEmail = currentUser.email;
|
||||
const res = await contractUsers(currentUserEmail);
|
||||
if (res === "Error: Something went wrong!") {
|
||||
|
||||
@@ -18,6 +18,7 @@ import EditorToolbar, {
|
||||
} from "../components/pdf/EditorToolbar";
|
||||
import ReactQuill from "react-quill";
|
||||
import "react-quill/dist/quill.snow.css";
|
||||
import BulkSendUi from "../components/BulkSendUi";
|
||||
|
||||
const ReportTable = (props) => {
|
||||
const navigate = useNavigate();
|
||||
@@ -39,6 +40,11 @@ const ReportTable = (props) => {
|
||||
const [mail, setMail] = useState({ subject: "", body: "" });
|
||||
const [userDetails, setUserDetails] = useState({});
|
||||
const [isNextStep, setIsNextStep] = useState({});
|
||||
const [isBulkSend, setIsBulkSend] = useState({});
|
||||
const [templateDeatils, setTemplateDetails] = useState({});
|
||||
const [placeholders, setPlaceholders] = useState([]);
|
||||
const [isLoader, setIsLoader] = useState({});
|
||||
|
||||
const startIndex = (currentPage - 1) * props.docPerPage;
|
||||
const { isMoreDocs, setIsNextRecord } = props;
|
||||
// For loop is used to calculate page numbers visible below table
|
||||
@@ -223,6 +229,8 @@ const ReportTable = (props) => {
|
||||
setIsOption({ [item.objectId]: !isOption[item.objectId] });
|
||||
} else if (act.action === "resend") {
|
||||
setIsResendMail({ [item.objectId]: true });
|
||||
} else if (act.action === "bulksend") {
|
||||
handleBulkSend(item);
|
||||
}
|
||||
};
|
||||
// Get current list
|
||||
@@ -551,7 +559,6 @@ const ReportTable = (props) => {
|
||||
};
|
||||
const handleResendMail = async (e, doc, user) => {
|
||||
e.preventDefault();
|
||||
console.log("first");
|
||||
setActLoader({ [user.objectId]: true });
|
||||
const url = `${localStorage.getItem("baseUrl")}functions/sendmailv3`;
|
||||
const headers = {
|
||||
@@ -609,6 +616,75 @@ const ReportTable = (props) => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
// `handleQuickSendClose` is trigger when bulk send component trigger close event
|
||||
const handleQuickSendClose = (status, count) => {
|
||||
setIsBulkSend({});
|
||||
setIsAlert(true);
|
||||
if (status === "success") {
|
||||
if (count > 1) {
|
||||
setAlertMsg({
|
||||
type: "success",
|
||||
message: count + " Document sent successfully!"
|
||||
});
|
||||
setTimeout(() => setIsAlert(false), 1500);
|
||||
} else {
|
||||
setAlertMsg({
|
||||
type: "success",
|
||||
message: count + " Document sent successfully!"
|
||||
});
|
||||
setTimeout(() => setIsAlert(false), 1500);
|
||||
}
|
||||
} else {
|
||||
setAlertMsg({
|
||||
type: "danger",
|
||||
message: "Something went wrong, Please try again later!"
|
||||
});
|
||||
setTimeout(() => setIsAlert(false), 1500);
|
||||
}
|
||||
};
|
||||
|
||||
// `handleBulkSend` is used to open modal as well as fetch template
|
||||
// and show Ui on the basis template response
|
||||
const handleBulkSend = async (template) => {
|
||||
setIsBulkSend({ [template.objectId]: true });
|
||||
setIsLoader({ [template.objectId]: true });
|
||||
try {
|
||||
const params = {
|
||||
templateId: template.objectId,
|
||||
include: ["Placeholders.signerPtr"]
|
||||
};
|
||||
const axiosRes = 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")
|
||||
}
|
||||
}
|
||||
);
|
||||
const templateRes = axiosRes.data && axiosRes.data.result;
|
||||
if (templateRes?.Placeholders?.length > 0) {
|
||||
setPlaceholders(templateRes?.Placeholders);
|
||||
setTemplateDetails(templateRes);
|
||||
setIsLoader({});
|
||||
} else {
|
||||
setIsLoader(false);
|
||||
setIsDocErr(true);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("err in fetch template in bulk modal", err);
|
||||
setIsBulkSend({});
|
||||
setIsDocErr(false);
|
||||
setIsAlert(true);
|
||||
setAlertMsg({
|
||||
type: "danger",
|
||||
message: "Something went wrong, Please try again later!"
|
||||
});
|
||||
setTimeout(() => setIsAlert(false), 1500);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="relative">
|
||||
{Object.keys(actLoader)?.length > 0 && (
|
||||
@@ -793,11 +869,11 @@ const ReportTable = (props) => {
|
||||
)}
|
||||
{isOption[item.objectId] &&
|
||||
act.action === "option" && (
|
||||
<div className="absolute -right-2 top-5 bg-white rounded shadow z-[20] overflow-hidden">
|
||||
<div className="absolute -right-2 top-5 bg-white text-nowrap 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"
|
||||
className="hover:bg-gray-300 cursor-pointer px-2 py-1.5 flex justify-start items-center text-black"
|
||||
onClick={() =>
|
||||
handleActionBtn(subact, item)
|
||||
}
|
||||
@@ -849,6 +925,36 @@ const ReportTable = (props) => {
|
||||
</div>
|
||||
</ModalUi>
|
||||
)}
|
||||
{isBulkSend[item.objectId] && (
|
||||
<ModalUi
|
||||
isOpen
|
||||
title={"Quick send"}
|
||||
handleClose={() => setIsBulkSend({})}
|
||||
>
|
||||
{isLoader[item.objectId] ? (
|
||||
<div className="w-full h-[100px] md:h-[100px] rounded-b-md flex justify-center items-center bg-black bg-opacity-30 z-30">
|
||||
<div
|
||||
style={{ fontSize: "45px", color: "#3dd3e0" }}
|
||||
className="loader-37"
|
||||
></div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{isDocErr ? (
|
||||
<div className="text-black bg-white w-full h-[80px] md:h-[100px] text-sm md:text-xl flex justify-center items-center">
|
||||
Please add Signers or Roles in template
|
||||
</div>
|
||||
) : (
|
||||
<BulkSendUi
|
||||
Placeholders={placeholders}
|
||||
item={templateDeatils}
|
||||
handleClose={handleQuickSendClose}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ModalUi>
|
||||
)}
|
||||
{isShare[item.objectId] && (
|
||||
<ModalUi
|
||||
isOpen
|
||||
@@ -1096,16 +1202,6 @@ const ReportTable = (props) => {
|
||||
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>
|
||||
);
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
.loginInput {
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
font-size: 0.75rem;
|
||||
border: 1px solid #e2dddd;
|
||||
background-color: #e0e5f5;
|
||||
color: black;
|
||||
padding-left: 1rem !important;
|
||||
padding-right: 1rem !important;
|
||||
padding-top: 0.5rem !important;
|
||||
font-family: inherit;
|
||||
font-feature-settings: inherit;
|
||||
font-variation-settings: inherit;
|
||||
font-weight: inherit;
|
||||
overflow: visible;
|
||||
}
|
||||
.loginInput:focus {
|
||||
outline: none;
|
||||
border: none;
|
||||
}
|
||||
.verifyBtn {
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.18);
|
||||
padding: 3px 30px;
|
||||
color: white;
|
||||
font-weight: 500 !important;
|
||||
font-size: 14px !important;
|
||||
border: none;
|
||||
}
|
||||
.verifyBtn:focus {
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.verifyBtn:hover {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.18);
|
||||
color: white;
|
||||
}
|
||||
.fakeimg {
|
||||
height: 200px;
|
||||
background: #aaa;
|
||||
}
|
||||
|
||||
.GTRY {
|
||||
border: 1px solid #dee2e6;
|
||||
margin-top: 75px;
|
||||
padding-bottom: 20px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.main_head {
|
||||
width: 100%;
|
||||
height: 66px;
|
||||
background-color: #fff;
|
||||
margin-bottom: 52px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.main-logo {
|
||||
width: 250px;
|
||||
height: 66px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.card-box {
|
||||
background: #fff;
|
||||
min-height: 50px;
|
||||
box-shadow: 0 20px 20px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
transition: 0.5s;
|
||||
border: 1px solid #f2f2f2;
|
||||
border-radius: 7px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.LKJH {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.GTR {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.JUI {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
color: #fff;
|
||||
background-color: #15b4e9;
|
||||
border-color: #15b4e9;
|
||||
}
|
||||
|
||||
.btn-info:hover {
|
||||
color: #fff;
|
||||
background-color: #15b4e9;
|
||||
border-color: #15b4e9;
|
||||
}
|
||||
|
||||
.btn-reg {
|
||||
color: #15b4e9;
|
||||
background-color: #ffffff;
|
||||
border-color: #15b4e9;
|
||||
}
|
||||
|
||||
.btn-reg:hover {
|
||||
color: #15b4e9;
|
||||
background-color: #ffffff;
|
||||
border-color: #15b4e9;
|
||||
}
|
||||
|
||||
.SYTU {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.MLKI {
|
||||
float: right;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100% !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 667px) {
|
||||
.MLKI {
|
||||
float: inherit;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 210px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.MLKI a {
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.KNLO {
|
||||
color: #878787;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.form-check-label {
|
||||
margin-left: 17px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.welcomeText {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.KLO1 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.MLKI a {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.form-check-label {
|
||||
font-size: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.MKUY {
|
||||
text-align: center;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.main_head {
|
||||
margin-bottom: 12px;
|
||||
padding-top: 10px;
|
||||
border-bottom: 1px dashed #eeeeee;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 366px) {
|
||||
.main-logo {
|
||||
width: 150px;
|
||||
height: 46px;
|
||||
display: inline-block;
|
||||
}
|
||||
.welcomeText {
|
||||
font-size: 20px;
|
||||
}
|
||||
.KNLO {
|
||||
color: #878787;
|
||||
font-size: 10px;
|
||||
}
|
||||
.btnContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,27 @@ import saveSubscription from './parsefunction/saveSubscription.js';
|
||||
import VerifyEmail from './parsefunction/VerifyEmail.js';
|
||||
import encryptedpdf from './parsefunction/encryptedPdf.js';
|
||||
import { getSignedUrl } from './parsefunction/getSignedUrl.js';
|
||||
import createBatchDocs from './parsefunction/createBatchDocs.js';
|
||||
import linkContactToDoc from './parsefunction/linkContactToDoc.js';
|
||||
|
||||
// This afterSave function triggers after an object is added or updated in the specified class, allowing for post-processing logic.
|
||||
Parse.Cloud.afterSave('contracts_Document', DocumentAftersave);
|
||||
Parse.Cloud.afterSave('contracts_Contactbook', ContactbookAftersave);
|
||||
Parse.Cloud.afterSave('contracts_Users', ContractUsersAftersave);
|
||||
Parse.Cloud.afterSave('contracts_Template', TemplateAfterSave);
|
||||
|
||||
// This beforeSave function triggers before an object is added or updated in the specified class, allowing for validation or modification.
|
||||
Parse.Cloud.beforeSave('contracts_Document', DocumentBeforesave);
|
||||
Parse.Cloud.beforeSave('contracts_Template', TemplateBeforeSave);
|
||||
|
||||
// This afterFind function triggers after a query retrieves objects from the specified class, allowing for post-processing of the results.
|
||||
Parse.Cloud.afterFind(Parse.User, UserAfterFind);
|
||||
Parse.Cloud.afterFind('contracts_Document', DocumentBeforeFind);
|
||||
Parse.Cloud.afterFind('contracts_Template', TemplateAfterFind);
|
||||
Parse.Cloud.afterFind('contracts_Signature', SignatureAfterFind);
|
||||
Parse.Cloud.afterFind('partners_Tenant', TenantAterFind);
|
||||
|
||||
// This define function creates a custom Cloud Function that can be called from the client-side, enabling custom business logic on the server.
|
||||
Parse.Cloud.define('AddUserToRole', addUserToGroups);
|
||||
Parse.Cloud.define('UserGroups', getUserGroups);
|
||||
Parse.Cloud.define('signPdf', PDF);
|
||||
@@ -62,18 +82,9 @@ Parse.Cloud.define('freesubscription', SubscribeFree);
|
||||
Parse.Cloud.define('getinvoices', getInvoices);
|
||||
Parse.Cloud.define('getpayments', getPayments);
|
||||
Parse.Cloud.define('getsubscriptions', getSubscriptions);
|
||||
Parse.Cloud.afterSave('contracts_Document', DocumentAftersave);
|
||||
Parse.Cloud.afterSave('contracts_Contactbook', ContactbookAftersave);
|
||||
Parse.Cloud.afterSave('contracts_Users', ContractUsersAftersave);
|
||||
Parse.Cloud.afterSave('contracts_Template', TemplateAfterSave);
|
||||
Parse.Cloud.beforeSave('contracts_Document', DocumentBeforesave);
|
||||
Parse.Cloud.beforeSave('contracts_Template', TemplateBeforeSave);
|
||||
Parse.Cloud.afterFind(Parse.User, UserAfterFind);
|
||||
Parse.Cloud.afterFind('contracts_Document', DocumentBeforeFind);
|
||||
Parse.Cloud.afterFind('contracts_Template', TemplateAfterFind);
|
||||
Parse.Cloud.afterFind('contracts_Signature', SignatureAfterFind);
|
||||
Parse.Cloud.afterFind('partners_Tenant', TenantAterFind);
|
||||
Parse.Cloud.define('savesubscription', saveSubscription);
|
||||
Parse.Cloud.define('verifyemail', VerifyEmail);
|
||||
Parse.Cloud.define('encryptedpdf', encryptedpdf);
|
||||
Parse.Cloud.define('getsignedurl', getSignedUrl);
|
||||
Parse.Cloud.define('batchdocuments', createBatchDocs);
|
||||
Parse.Cloud.define('linkcontacttodoc', linkContactToDoc);
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const serverUrl = process.env.SERVER_URL;
|
||||
const appId = process.env.APP_ID;
|
||||
|
||||
async function sendMail(document, sessionToken) {
|
||||
const baseUrl = new URL(process.env.PUBLIC_URL);
|
||||
|
||||
// console.log("pdfDetails", pdfDetails);
|
||||
const timeToCompleteDays = document?.TimeToCompleteDays || 15;
|
||||
const ExpireDate = new Date(document.createdAt);
|
||||
ExpireDate.setDate(ExpireDate.getDate() + timeToCompleteDays);
|
||||
const newDate = ExpireDate;
|
||||
const localExpireDate = newDate.toLocaleDateString('en-US', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
});
|
||||
const sender = document.ExtUserPtr.Email;
|
||||
const signerMail = document.Placeholders;
|
||||
for (let i = 0; i < signerMail.length; i++) {
|
||||
try {
|
||||
const imgPng = 'https://qikinnovation.ams3.digitaloceanspaces.com/logo.png';
|
||||
let url = `${serverUrl}/functions/sendmailv3`;
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': appId,
|
||||
sessionToken: sessionToken,
|
||||
};
|
||||
const objectId = signerMail[i]?.signerObjId;
|
||||
const hostUrl = baseUrl.origin;
|
||||
let encodeBase64;
|
||||
if (objectId) {
|
||||
encodeBase64 = btoa(`${document.objectId}/${signerMail[i].signerPtr.Email}/${objectId}`);
|
||||
} else {
|
||||
encodeBase64 = btoa(`${document.objectId}/${signerMail[i].email}`);
|
||||
}
|
||||
let signPdf = `${hostUrl}/login/${encodeBase64}`;
|
||||
const openSignUrl = 'https://www.opensignlabs.com/';
|
||||
const orgName = document.ExtUserPtr.Company ? document.ExtUserPtr.Company : '';
|
||||
const themeBGcolor = '#47a3ad';
|
||||
let params = {
|
||||
extUserId: document.ExtUserPtr.objectId,
|
||||
recipient: objectId ? signerMail[i].signerPtr.Email : signerMail[i].email,
|
||||
subject: `${document.ExtUserPtr.Name} has requested you to sign ${document.Name}`,
|
||||
from: sender,
|
||||
html:
|
||||
"<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8' /> </head> <body> <div style='background-color: #f5f5f5; padding: 20px'> <div style='box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px;background: white;padding-bottom: 20px;'> <div style='padding:10px 10px 0 10px'><img src='" +
|
||||
imgPng +
|
||||
"' height='50' style='padding:20px; width:170px; height:40px;' /></div><div style='padding: 2px;font-family: system-ui;background-color:" +
|
||||
themeBGcolor +
|
||||
";'><p style='font-size: 20px;font-weight: 400;color: white;padding-left: 20px;' > Digital Signature Request</p></div><div><p style='padding: 20px;font-family: system-ui;font-size: 14px; margin-bottom: 10px;'> " +
|
||||
document.ExtUserPtr.Name +
|
||||
' has requested you to review and sign <strong> ' +
|
||||
document.Name +
|
||||
"</strong>.</p><div style='padding: 5px 0px 5px 25px;display: flex;flex-direction: row;justify-content: space-around;'><table> <tr> <td style='font-weight:bold;font-family:sans-serif;font-size:15px'>Sender</td> <td> </td> <td style='color:#626363;font-weight:bold'>" +
|
||||
sender +
|
||||
"</td></tr><tr><td style='font-weight:bold;font-family:sans-serif;font-size:15px'>Organization</td> <td> </td><td style='color:#626363;font-weight:bold'> " +
|
||||
orgName +
|
||||
"</td></tr> <tr> <td style='font-weight:bold;font-family:sans-serif;font-size:15px'>Expires on</td><td> </td> <td style='color:#626363;font-weight:bold'>" +
|
||||
localExpireDate +
|
||||
"</td></tr><tr> <td></td> <td> </td></tr></table> </div> <div style='margin-left:70px'><a href=" +
|
||||
signPdf +
|
||||
"> <button style='padding: 12px 12px 12px 12px;background-color: #d46b0f;color: white; border: 0px;box-shadow: rgba(0, 0, 0, 0.05) 0px 6px 24px 0px,rgba(0, 0, 0, 0.08) 0px 0px 0px 1px;font-weight:bold;margin-top:30px;'>Sign here</button></a> </div> <div style='display: flex; justify-content: center;margin-top: 10px;'> </div></div></div><div><p> This is an automated email from OpenSign™. For any queries regarding this email, please contact the sender " +
|
||||
sender +
|
||||
' directly.If you think this email is inappropriate or spam, you may file a complaint with OpenSign™ <a href=' +
|
||||
openSignUrl +
|
||||
' target=_blank>here</a>.</p> </div></div></body> </html>',
|
||||
};
|
||||
const sendMail = await axios.post(url, params, { headers: headers });
|
||||
// if (sendMail.data.result.status === 'success') {
|
||||
// console.log('batch login mail sent');
|
||||
// }
|
||||
} catch (error) {
|
||||
console.log('error', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
export default async function createBatchDocs(request) {
|
||||
const strDocuments = request.params.Documents;
|
||||
const sessionToken = request.headers['sessiontoken'];
|
||||
const Documents = JSON.parse(strDocuments);
|
||||
const Ip = request?.headers?.['x-real-ip'] || '';
|
||||
// console.log('Documents ', Documents);
|
||||
const parseConfig = {
|
||||
baseURL: serverUrl, //localStorage.getItem('baseUrl'),
|
||||
headers: {
|
||||
'X-Parse-Application-Id': appId,
|
||||
'X-Parse-Session-Token': sessionToken,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
try {
|
||||
const requests = Documents.map(x => {
|
||||
const Signers = x.Signers;
|
||||
const allSigner = x?.Placeholders?.map(
|
||||
item => Signers?.find(e => item?.signerPtr?.objectId === e?.objectId) || item?.signerPtr
|
||||
).filter(signer => Object.keys(signer).length > 0);
|
||||
const date = new Date();
|
||||
const isoDate = date.toISOString();
|
||||
let Acl = { [x.CreatedBy.objectId]: { read: true, write: true } };
|
||||
if (allSigner && allSigner.length > 0) {
|
||||
allSigner.forEach(x => {
|
||||
const obj = { [x.CreatedBy.objectId]: { read: true, write: true } };
|
||||
Acl = { ...Acl, ...obj };
|
||||
});
|
||||
}
|
||||
return {
|
||||
method: 'POST',
|
||||
path: '/app/classes/contracts_Document',
|
||||
body: {
|
||||
Name: x.Name,
|
||||
URL: x.URL,
|
||||
Note: x.Note,
|
||||
Description: x.Description,
|
||||
CreatedBy: x.CreatedBy,
|
||||
SendinOrder: x.SendinOrder || true,
|
||||
ExtUserPtr: {
|
||||
__type: 'Pointer',
|
||||
className: x.ExtUserPtr.className,
|
||||
objectId: x.ExtUserPtr.objectId,
|
||||
},
|
||||
Placeholders: x.Placeholders.map(y =>
|
||||
y?.signerPtr?.objectId
|
||||
? {
|
||||
...y,
|
||||
signerPtr: {
|
||||
__type: 'Pointer',
|
||||
className: 'contracts_Contactbook',
|
||||
objectId: y.signerPtr.objectId,
|
||||
},
|
||||
signerObjId: y.signerObjId,
|
||||
}
|
||||
: { ...y, signerPtr: {}, signerObjId: '' }
|
||||
),
|
||||
SignedUrl: x.URL || x.SignedUrl,
|
||||
Signers: allSigner?.map(y => ({
|
||||
__type: 'Pointer',
|
||||
className: 'contracts_Contactbook',
|
||||
objectId: y.objectId,
|
||||
})),
|
||||
ACL: Acl,
|
||||
RemindOnceInEvery: x.RemindOnceInEvery || 5,
|
||||
AutomaticReminders: x.AutomaticReminders || false,
|
||||
TimeToCompleteDays: x.TimeToCompleteDays || 15,
|
||||
OriginIp: Ip,
|
||||
DocSentAt: { __type: 'Date', iso: isoDate },
|
||||
},
|
||||
};
|
||||
});
|
||||
// console.log('requests ', requests);
|
||||
|
||||
const response = await axios.post('batch', { requests: requests }, parseConfig);
|
||||
// // Handle the batch query response
|
||||
// console.log('Batch query response:', response.data);
|
||||
if (response.data && response.data.length > 0) {
|
||||
const updateDocuments = Documents.map((x, i) => ({
|
||||
...x,
|
||||
objectId: response.data[i]?.success?.objectId,
|
||||
createdAt: response.data[i]?.success?.createdAt,
|
||||
}));
|
||||
for (let i = 0; i < updateDocuments.length; i++) {
|
||||
// console.log('updateDocuments ', updateDocuments);
|
||||
sendMail(updateDocuments[i], sessionToken);
|
||||
}
|
||||
return 'success';
|
||||
}
|
||||
|
||||
// Handle individual responses within response.data.results
|
||||
} catch (error) {
|
||||
console.error('Error performing batch query:', error);
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ export default async function getReport(request) {
|
||||
'X-Parse-Application-Id': appId,
|
||||
'X-Parse-Master-Key': process.env.MASTER_KEY,
|
||||
};
|
||||
const url = `${serverUrl}/classes/${clsName}?where=${strParams}&keys=${strKeys}&order=${orderBy}&skip=${skip}&limit=${limit}&include=AuditTrail.UserPtr`;
|
||||
const url = `${serverUrl}/classes/${clsName}?where=${strParams}&keys=${strKeys}&order=${orderBy}&skip=${skip}&limit=${limit}&include=AuditTrail.UserPtr,Placeholders.signerPtr`;
|
||||
const res = await axios.get(url, { headers: headers });
|
||||
if (res.data && res.data.results) {
|
||||
return res.data.results;
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
// `saveRoleContact` is used to save user in contracts_Guest role and create contact
|
||||
const saveRoleContact = async contact => {
|
||||
try {
|
||||
const Role = new Parse.Query(Parse.Role);
|
||||
const guestRole = await Role.equalTo('name', 'contracts_Guest').first();
|
||||
if (guestRole) {
|
||||
// Check if the user is already in the role
|
||||
const relation = guestRole.relation('users');
|
||||
const usersInRoleQuery = relation.query();
|
||||
usersInRoleQuery.equalTo('objectId', contact.UserId.objectId);
|
||||
const usersInRole = await usersInRoleQuery.find();
|
||||
if (usersInRole.length > 0) {
|
||||
console.log('User already added to Guest role.');
|
||||
} else {
|
||||
relation.add({ __type: 'Pointer', className: '_User', id: contact.UserId.objectId });
|
||||
await guestRole.save(null, { useMasterKey: true });
|
||||
// console.log('User added to Guest role successfully.');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('err in role save', err);
|
||||
}
|
||||
const contactQuery = new Parse.Object('contracts_Contactbook');
|
||||
contactQuery.set('Name', contact.Name);
|
||||
contactQuery.set('Email', contact.Email);
|
||||
if (contact?.Phone) {
|
||||
contactQuery.set('Phone', contact.Phone);
|
||||
}
|
||||
contactQuery.set('CreatedBy', contact.CreatedBy);
|
||||
contactQuery.set('UserId', contact.UserId);
|
||||
contactQuery.set('UserRole', 'contracts_Guest');
|
||||
contactQuery.set('TenantId', contact.TenantId);
|
||||
contactQuery.set('IsDeleted', false);
|
||||
const acl = new Parse.ACL();
|
||||
acl.setReadAccess(contact.CreatedBy.objectId, true);
|
||||
acl.setWriteAccess(contact.CreatedBy.objectId, true);
|
||||
acl.setReadAccess(contact.UserId.objectId, true);
|
||||
acl.setWriteAccess(contact.UserId.objectId, true);
|
||||
contactQuery.setACL(acl);
|
||||
const contactRes = await contactQuery.save();
|
||||
if (contactRes) {
|
||||
return contactRes;
|
||||
}
|
||||
};
|
||||
|
||||
// `linkContactToDoc` cloud function is used to create contact, add this contact in contracts_Guest role and
|
||||
// save contact pointer in placeholder, signers and ACL of Document
|
||||
export default async function linkContactToDoc(req) {
|
||||
const email = req.params.email;
|
||||
const docId = req.params.docId;
|
||||
const name = req.params.name;
|
||||
const phone = req.params.phone;
|
||||
try {
|
||||
if (docId) {
|
||||
// Execute the query to get the document with the specified 'docId'
|
||||
const docQuery = new Parse.Query('contracts_Document');
|
||||
docQuery.include('ExtUserPtr');
|
||||
const docRes = await docQuery.get(docId, { useMasterKey: true });
|
||||
// Check if the document was found; if not, throw an error indicating the document was not found
|
||||
if (!docRes) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Document not found.');
|
||||
}
|
||||
const _docRes = JSON.parse(JSON.stringify(docRes));
|
||||
const Placeholders = _docRes?.Placeholders || [];
|
||||
const index = Placeholders?.findIndex(x => x.email && x.email === email);
|
||||
if (index !== -1) {
|
||||
// `signerObjectId` holds the value of `signerObjId` from the `Placeholders` array at the current index.
|
||||
// This value is used to check if `signerObjId` is present or not.
|
||||
const signerObjectId = Placeholders[index]?.signerObjId;
|
||||
if (signerObjectId) {
|
||||
return { contactId: signerObjectId };
|
||||
}
|
||||
// Execute the query to check if a contact already exists in the 'contracts_Contactbook' class
|
||||
const contactCls = new Parse.Query('contracts_Contactbook');
|
||||
contactCls.equalTo('Email', email);
|
||||
contactCls.equalTo('CreatedBy', _docRes.CreatedBy);
|
||||
contactCls.notEqualTo('IsDeleted', true);
|
||||
const existContact = await contactCls.first({ useMasterKey: true });
|
||||
if (existContact) {
|
||||
//update contact in placeholder, signers and update ACl in provide document
|
||||
const updateDoc = new Parse.Object('contracts_Document');
|
||||
updateDoc.id = docId;
|
||||
const signers = _docRes?.Signers || [];
|
||||
signers.push({
|
||||
__type: 'Pointer',
|
||||
className: 'contracts_Contactbook',
|
||||
objectId: existContact.id,
|
||||
});
|
||||
updateDoc.set('Signers', signers);
|
||||
|
||||
Placeholders[index] = {
|
||||
...Placeholders[index],
|
||||
signerObjId: existContact.id,
|
||||
signerPtr: {
|
||||
__type: 'Pointer',
|
||||
className: 'contracts_Contactbook',
|
||||
objectId: existContact.id,
|
||||
},
|
||||
};
|
||||
updateDoc.set('Placeholders', Placeholders);
|
||||
const Acl = docRes.getACL();
|
||||
Acl.setReadAccess(existContact.get('CreatedBy').id, true);
|
||||
Acl.setWriteAccess(existContact.get('CreatedBy').id, true);
|
||||
updateDoc.setACL(Acl);
|
||||
// const parseData = JSON.parse(JSON.stringify(res));
|
||||
const resDoc = await updateDoc.save(null, { useMasterKey: true });
|
||||
if (resDoc) {
|
||||
return { contactId: existContact.id };
|
||||
}
|
||||
} else {
|
||||
// Execute the query to check if a user already exists in the 'contracts_Users' class
|
||||
const extUserQuery = new Parse.Query('contracts_Users');
|
||||
extUserQuery.equalTo('Email', email);
|
||||
const extUser = await extUserQuery.first({ useMasterKey: true });
|
||||
if (extUser) {
|
||||
const _extUser = JSON.parse(JSON.stringify(extUser));
|
||||
const contact = {
|
||||
UserId: _extUser.UserId,
|
||||
Name: _extUser.Name,
|
||||
Email: email,
|
||||
Phone: _extUser?.Phone ? _extUser.Phone : '',
|
||||
CreatedBy: _docRes.CreatedBy,
|
||||
TenantId: _docRes.ExtUserPtr.TenantId,
|
||||
};
|
||||
// if user present on platform create contact on the basis of extended user details
|
||||
const contactRes = await saveRoleContact(contact);
|
||||
//update contact in placeholder, signers and update ACl in provide document
|
||||
const updateDoc = new Parse.Object('contracts_Document');
|
||||
updateDoc.id = docId;
|
||||
const signers = _docRes?.Signers || [];
|
||||
signers.push({
|
||||
__type: 'Pointer',
|
||||
className: 'contracts_Contactbook',
|
||||
objectId: contactRes.id,
|
||||
});
|
||||
updateDoc.set('Signers', signers);
|
||||
|
||||
Placeholders[index] = {
|
||||
...Placeholders[index],
|
||||
signerObjId: contactRes.id,
|
||||
signerPtr: {
|
||||
__type: 'Pointer',
|
||||
className: 'contracts_Contactbook',
|
||||
objectId: contactRes.id,
|
||||
},
|
||||
};
|
||||
updateDoc.set('Placeholders', Placeholders);
|
||||
const Acl = docRes.getACL();
|
||||
Acl.setReadAccess(_extUser.UserId.objectId, true);
|
||||
Acl.setWriteAccess(_extUser.UserId.objectId, true);
|
||||
updateDoc.setACL(Acl);
|
||||
// const parseData = JSON.parse(JSON.stringify(res));
|
||||
const resDoc = await updateDoc.save(null, { useMasterKey: true });
|
||||
if (resDoc) {
|
||||
return { contactId: contactRes.id };
|
||||
}
|
||||
} else if (name) {
|
||||
try {
|
||||
// Execute the query to check if a user already exists in the '_User' class
|
||||
const userQuery = new Parse.Query(Parse.User);
|
||||
userQuery.equalTo('email', email);
|
||||
const userRes = await userQuery.first({ useMasterKey: true });
|
||||
if (userRes) {
|
||||
const contact = {
|
||||
UserId: { __type: 'Pointer', className: '_User', objectId: userRes.id },
|
||||
Name: name,
|
||||
Email: email,
|
||||
Phone: phone,
|
||||
CreatedBy: _docRes.CreatedBy,
|
||||
TenantId: _docRes.ExtUserPtr.TenantId,
|
||||
};
|
||||
// Create new contract on the basis provided contact details by user and userId from _User class
|
||||
const contactRes = await saveRoleContact(contact);
|
||||
//update contact in placeholder, signers and update ACl in provide document
|
||||
const updateDoc = new Parse.Object('contracts_Document');
|
||||
updateDoc.id = docId;
|
||||
const signers = _docRes?.Signers || [];
|
||||
signers.push({
|
||||
__type: 'Pointer',
|
||||
className: 'contracts_Contactbook',
|
||||
objectId: contactRes.id,
|
||||
});
|
||||
updateDoc.set('Signers', signers);
|
||||
|
||||
Placeholders[index] = {
|
||||
...Placeholders[index],
|
||||
signerObjId: contactRes.id,
|
||||
signerPtr: {
|
||||
__type: 'Pointer',
|
||||
className: 'contracts_Contactbook',
|
||||
objectId: contactRes.id,
|
||||
},
|
||||
};
|
||||
updateDoc.set('Placeholders', Placeholders);
|
||||
const Acl = docRes.getACL();
|
||||
Acl.setReadAccess(userRes.id, true);
|
||||
Acl.setWriteAccess(userRes.id, true);
|
||||
updateDoc.setACL(Acl);
|
||||
// const parseData = JSON.parse(JSON.stringify(res));
|
||||
const resDoc = await updateDoc.save(null, { useMasterKey: true });
|
||||
if (resDoc) {
|
||||
return { contactId: contactRes.id };
|
||||
}
|
||||
} else {
|
||||
// create new user in _User class on the basis of details provide by user
|
||||
const _users = Parse.Object.extend('User');
|
||||
const _user = new _users();
|
||||
_user.set('name', name);
|
||||
_user.set('username', email);
|
||||
_user.set('email', email);
|
||||
if (phone) {
|
||||
_user.set('phone', phone);
|
||||
_user.set('password', phone);
|
||||
} else {
|
||||
_user.set('password', email);
|
||||
}
|
||||
const newUserRes = await _user.save();
|
||||
const contact = {
|
||||
UserId: { __type: 'Pointer', className: '_User', objectId: newUserRes.id },
|
||||
Name: name,
|
||||
Email: email,
|
||||
Phone: phone,
|
||||
CreatedBy: _docRes.CreatedBy,
|
||||
TenantId: _docRes.ExtUserPtr.TenantId,
|
||||
};
|
||||
// Create new contract on the basis provided contact details by user and userId from _User class
|
||||
const contactRes = await saveRoleContact(contact);
|
||||
//update contact in placeholder, signers and update ACl in provide document
|
||||
const updateDoc = new Parse.Object('contracts_Document');
|
||||
updateDoc.id = docId;
|
||||
const signers = _docRes?.Signers || [];
|
||||
signers.push({
|
||||
__type: 'Pointer',
|
||||
className: 'contracts_Contactbook',
|
||||
objectId: contactRes.id,
|
||||
});
|
||||
updateDoc.set('Signers', signers);
|
||||
|
||||
Placeholders[index] = {
|
||||
...Placeholders[index],
|
||||
signerObjId: contactRes.id,
|
||||
signerPtr: {
|
||||
__type: 'Pointer',
|
||||
className: 'contracts_Contactbook',
|
||||
objectId: contactRes.id,
|
||||
},
|
||||
};
|
||||
updateDoc.set('Placeholders', Placeholders);
|
||||
const Acl = docRes.getACL();
|
||||
Acl.setReadAccess(newUserRes.id, true);
|
||||
Acl.setWriteAccess(newUserRes.id, true);
|
||||
updateDoc.setACL(Acl);
|
||||
// const parseData = JSON.parse(JSON.stringify(res));
|
||||
const resDoc = await updateDoc.save(null, { useMasterKey: true });
|
||||
if (resDoc) {
|
||||
return { contactId: contactRes.id };
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Err', err);
|
||||
}
|
||||
} else {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'User not found.');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'unauthorized');
|
||||
}
|
||||
} else {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Document not found.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('err in linkcontacttodoc', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -324,6 +324,7 @@ export default function reportJson(id, userId) {
|
||||
'Signers.Name',
|
||||
'Signers.Email',
|
||||
'Signers.Phone',
|
||||
'Placeholders',
|
||||
],
|
||||
};
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user