Skip to content

Commit c40a923

Browse files
Merge branch 'staging' into raktima-opensignlabs-patch-8
2 parents 08a9770 + 6a9408b commit c40a923

File tree

15 files changed

+1110
-389
lines changed

15 files changed

+1110
-389
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ We would like to thank all our contributors and users for their support and feed
136136
<td align="center" valign="top" width="14.28%"><a href="https://github.com/VikramNagwal"><img src="https://avatars.githubusercontent.com/u/123088024?v=4?s=100" width="100px;" alt="Vikram"/><br /><sub><b>Vikram</b></sub></a><br /><a href="#code-VikramNagwal" title="Code">💻</a></td>
137137
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ugoconsonni"><img src="https://avatars.githubusercontent.com/u/13661702?v=4?s=100" width="100px;" alt="ugoconsonni"/><br /><sub><b>ugoconsonni</b></sub></a><br /><a href="#code-ugoconsonni" title="Code">💻</a></td>
138138
<td align="center" valign="top" width="14.28%"><a href="https://github.com/daniel-mutwiri"><img src="https://avatars.githubusercontent.com/u/8936960?v=4?s=100" width="100px;" alt="Daniel Mutwiri"/><br /><sub><b>Daniel Mutwiri</b></sub></a><br /><a href="#code-daniel-mutwiri" title="Code">💻</a></td>
139-
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Zathiel"><img src="https://avatars.githubusercontent.com/u/26553418?v=4?s=100" width="100px;" alt="Zathiel"/><br /><sub><b>Zathiel</b></sub></a><br /><a href="#code-Zathiel" title="Code">💻</a></td>
139+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Zathiel"><img src="https://avatars.githubusercontent.com/u/26553418?v=4?s=100" width="100px;" alt="Zathiel"/><br /><sub><b>Zathiel</b></sub></a><br /><a href="#code-Zathiel" title="Code">💻</a><a href="#security-Zathiel" title="Security">🛡️</a></td>
140140
</tr>
141141
</tbody>
142142
</table>
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import React, { useState, useEffect, useRef } from "react";
2+
import axios from "axios";
3+
import SuggestionInput from "./shared/fields/SuggestionInput";
4+
5+
const BulkSendUi = (props) => {
6+
const [forms, setForms] = useState([]);
7+
const [formId, setFormId] = useState(2);
8+
const formRef = useRef(null);
9+
const [scrollOnNextUpdate, setScrollOnNextUpdate] = useState(false);
10+
const [isSubmit, setIsSubmit] = useState(false);
11+
const [allowedForm, setAllowedForm] = useState(0);
12+
const allowedSigners = 50;
13+
useEffect(() => {
14+
if (scrollOnNextUpdate && formRef.current) {
15+
formRef.current.scrollIntoView({
16+
behavior: "smooth",
17+
block: "end",
18+
inline: "nearest"
19+
});
20+
setScrollOnNextUpdate(false);
21+
}
22+
}, [forms, scrollOnNextUpdate]);
23+
24+
useEffect(() => {
25+
(() => {
26+
if (props?.Placeholders?.length > 0) {
27+
let users = [];
28+
props?.Placeholders?.forEach((element) => {
29+
if (!element.signerObjId) {
30+
users = [
31+
...users,
32+
{
33+
fieldId: element.Id,
34+
email: "",
35+
label: element.Role,
36+
signer: {}
37+
}
38+
];
39+
}
40+
});
41+
setForms((prevForms) => [...prevForms, { Id: 1, fields: users }]);
42+
const totalForms = Math.floor(allowedSigners / users?.length);
43+
setAllowedForm(totalForms);
44+
}
45+
})();
46+
// eslint-disable-next-line
47+
}, []);
48+
const handleInputChange = (index, signer, fieldIndex) => {
49+
const newForms = [...forms];
50+
newForms[index].fields[fieldIndex].email = signer?.Email
51+
? signer?.Email
52+
: signer || "";
53+
newForms[index].fields[fieldIndex].signer = signer?.objectId ? signer : "";
54+
setForms(newForms);
55+
};
56+
57+
const handleAddForm = (e) => {
58+
e.preventDefault();
59+
// Check if the quick send limit has been reached
60+
if (forms?.length < allowedForm) {
61+
if (props?.Placeholders.length > 0) {
62+
let newForm = [];
63+
props?.Placeholders?.forEach((element) => {
64+
if (!element.signerObjId) {
65+
newForm = [
66+
...newForm,
67+
{
68+
fieldId: element.Id,
69+
email: "",
70+
label: element.Role,
71+
signer: {}
72+
}
73+
];
74+
}
75+
});
76+
setForms([...forms, { Id: formId, fields: newForm }]);
77+
}
78+
setFormId(formId + 1);
79+
setScrollOnNextUpdate(true);
80+
} else {
81+
// If the limit has been reached, throw an error with the appropriate message
82+
alert("Quick send reached limit.");
83+
}
84+
};
85+
86+
const handleRemoveForm = (index) => {
87+
const updatedForms = forms.filter((_, i) => i !== index);
88+
setForms(updatedForms);
89+
};
90+
const handleSubmit = async (e) => {
91+
e.preventDefault();
92+
e.stopPropagation();
93+
setIsSubmit(true);
94+
95+
// Create a copy of Placeholders array from props.item
96+
let Placeholders = [...props.item.Placeholders];
97+
// Initialize an empty array to store updated documents
98+
let Documents = [];
99+
100+
// Loop through each form
101+
forms.forEach((form) => {
102+
// Map through the copied Placeholders array to update email values
103+
const updatedPlaceholders = Placeholders.map((placeholder) => {
104+
// Find the field in the current form that matches the placeholder Id
105+
const field = form.fields.find(
106+
(element) => parseInt(element.fieldId) === placeholder.Id
107+
);
108+
// If a matching field is found, update the email value in the placeholder
109+
const signer = field?.signer?.objectId ? field.signer : {};
110+
if (field) {
111+
return {
112+
...placeholder,
113+
email: field.email,
114+
signerObjId: field?.signer?.objectId || "",
115+
signerPtr: signer
116+
};
117+
}
118+
// If no matching field is found, keep the placeholder as is
119+
return placeholder;
120+
});
121+
122+
// Push a new document object with updated Placeholders into the Documents array
123+
Documents.push({ ...props.item, Placeholders: updatedPlaceholders });
124+
});
125+
// console.log("Documents ", Documents);
126+
await batchQuery(Documents);
127+
};
128+
129+
const batchQuery = async (Documents) => {
130+
const serverUrl = localStorage.getItem("baseUrl");
131+
const functionsUrl = `${serverUrl}functions/batchdocuments`;
132+
const headers = {
133+
"Content-Type": "application/json",
134+
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
135+
sessionToken: localStorage.getItem("accesstoken")
136+
};
137+
const params = {
138+
Documents: JSON.stringify(Documents)
139+
};
140+
try {
141+
const res = await axios.post(functionsUrl, params, { headers: headers });
142+
// console.log("res ", res);
143+
if (res.data && res.data.result) {
144+
props.handleClose("success", Documents?.length);
145+
}
146+
} catch (err) {
147+
console.log("Err ", err);
148+
props.handleClose("error", 0);
149+
} finally {
150+
setIsSubmit(false);
151+
}
152+
};
153+
154+
return (
155+
<>
156+
{isSubmit && (
157+
<div className="absolute z-[999] h-full w-full flex justify-center items-center bg-black bg-opacity-40">
158+
<div
159+
style={{
160+
fontSize: "45px",
161+
color: "#3dd3e0"
162+
}}
163+
className="loader-37 "
164+
></div>
165+
</div>
166+
)}
167+
<form onSubmit={handleSubmit}>
168+
<div className=" min-h-max max-h-[250px] overflow-y-auto">
169+
{forms?.map((form, index) => (
170+
<div
171+
key={form.Id}
172+
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"
173+
>
174+
{form?.fields?.map((field, fieldIndex) => (
175+
<div className="flex flex-col " key={field.fieldId}>
176+
<label>{field.label}</label>
177+
<SuggestionInput
178+
required
179+
type="email"
180+
value={field.value}
181+
index={fieldIndex}
182+
onChange={(signer) =>
183+
handleInputChange(index, signer, fieldIndex)
184+
}
185+
/>
186+
</div>
187+
))}
188+
{index > 0 && (
189+
<button
190+
onClick={() => handleRemoveForm(index)}
191+
className="absolute right-3 top-1 border border-gray-300 rounded-lg px-2 py-1"
192+
>
193+
<i className="fa-solid fa-trash"></i>
194+
</button>
195+
)}
196+
<div ref={formRef}></div>
197+
</div>
198+
))}
199+
</div>
200+
<div className="flex flex-col mx-4 mb-4 gap-3">
201+
<button
202+
onClick={handleAddForm}
203+
className="bg-[#32a3ac] p-2 text-white w-full rounded-full focus:outline-none"
204+
>
205+
<i className="fa-solid fa-plus"></i> <span>Add new</span>
206+
</button>
207+
<button
208+
type="submit"
209+
className="bg-[#32a3ac] p-2 text-white w-full rounded-full focus:outline-none"
210+
>
211+
<i className="fa-solid fa-paper-plane"></i> <span>Send</span>
212+
</button>
213+
</div>
214+
</form>
215+
</>
216+
);
217+
};
218+
219+
export default BulkSendUi;
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import React, { useState, useEffect, useRef } from "react";
2+
import { findContact } from "../../../constant/Utils";
3+
const SuggestionInput = (props) => {
4+
const [inputValue, setInputValue] = useState(props?.value || "");
5+
const [suggestions, setSuggestions] = useState([]);
6+
const [showSuggestions, setShowSuggestions] = useState(false);
7+
const ref = useRef(null);
8+
9+
useEffect(() => {
10+
document.addEventListener("mousedown", Clickout);
11+
return () => {
12+
document.removeEventListener("mousedown", Clickout);
13+
};
14+
}, []);
15+
16+
const Clickout = (event) => {
17+
if (ref.current && !ref.current.contains(event.target)) {
18+
setShowSuggestions(false);
19+
}
20+
};
21+
// create debounce to avoid unnecessay api calls
22+
useEffect(() => {
23+
let timer;
24+
if (inputValue) {
25+
if (timer) clearTimeout(timer);
26+
timer = setTimeout(() => {
27+
(async () => {
28+
const res = await findContact(inputValue);
29+
if (res?.length > 0) {
30+
setSuggestions(res);
31+
setShowSuggestions(true);
32+
} else {
33+
setSuggestions(res);
34+
setShowSuggestions(false);
35+
}
36+
})();
37+
}, 1000);
38+
}
39+
return () => clearTimeout(timer);
40+
}, [inputValue]);
41+
42+
const handleInputChange = async (e) => {
43+
const value = e.target.value;
44+
setInputValue(value);
45+
if (props.onChange) {
46+
props.onChange(value);
47+
}
48+
49+
if (value.trim() === "") {
50+
setSuggestions([]);
51+
setShowSuggestions(false);
52+
return;
53+
}
54+
};
55+
const handleSuggestionClick = (suggestion) => {
56+
setInputValue(suggestion.Email);
57+
setSuggestions([]);
58+
setShowSuggestions(false);
59+
if (props.onChange) {
60+
props.onChange(suggestion);
61+
}
62+
};
63+
return (
64+
<div className="flex flex-col items-center relative">
65+
<input
66+
type={props?.type || "text"}
67+
value={inputValue}
68+
onChange={handleInputChange}
69+
placeholder="Enter text..."
70+
className="w-full border-[1px] border-gray-400 p-2 text-black rounded"
71+
required={props.required}
72+
/>
73+
{showSuggestions && (
74+
<ul
75+
ref={ref}
76+
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"
77+
>
78+
{suggestions.map((suggestion, index) => (
79+
<li
80+
key={index}
81+
className="py-2 px-2 w-full text-sm cursor-pointer hover:bg-gray-100"
82+
onClick={() => handleSuggestionClick(suggestion)}
83+
>
84+
{suggestion.Email}
85+
</li>
86+
))}
87+
</ul>
88+
)}
89+
</div>
90+
);
91+
};
92+
export default SuggestionInput;

apps/OpenSign/src/constant/Utils.js

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,12 @@ export async function fetchSubscription(
2222
isGuestSign = false
2323
) {
2424
try {
25-
const extClass = localStorage.getItem("Extand_Class");
25+
const Extand_Class = localStorage.getItem("Extand_Class");
26+
const extClass = Extand_Class && JSON.parse(Extand_Class);
27+
// console.log("extClass ", extClass);
2628
let extUser;
2729
if (extClass && extClass.length > 0) {
28-
const jsonSender = JSON.parse(extClass);
29-
extUser = jsonSender[0].objectId;
30+
extUser = extClass[0].objectId;
3031
} else {
3132
extUser = extUserId;
3233
}
@@ -2032,7 +2033,6 @@ export const handleSendOTP = async (email) => {
20322033
alert(error.message);
20332034
}
20342035
};
2035-
20362036
//handle download signed pdf
20372037
export const handleDownloadPdf = async (pdfDetails, pdfUrl) => {
20382038
const pdfName = pdfDetails[0] && pdfDetails[0].Name;
@@ -2150,3 +2150,23 @@ export const handleDownloadCertificate = async (
21502150
}
21512151
}
21522152
};
2153+
export async function findContact(value) {
2154+
try {
2155+
const currentUser = Parse.User.current();
2156+
const contactbook = new Parse.Query("contracts_Contactbook");
2157+
contactbook.equalTo(
2158+
"CreatedBy",
2159+
Parse.User.createWithoutData(currentUser.id)
2160+
);
2161+
contactbook.notEqualTo("IsDeleted", true);
2162+
contactbook.matches("Email", new RegExp(value, "i"));
2163+
2164+
const contactRes = await contactbook.find();
2165+
if (contactRes) {
2166+
const res = JSON.parse(JSON.stringify(contactRes));
2167+
return res;
2168+
}
2169+
} catch (error) {
2170+
console.error("Error fetching suggestions:", error);
2171+
}
2172+
}

apps/OpenSign/src/json/ReportJson.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,14 @@ export default function reportJson(id) {
360360
redirectUrl: "template",
361361
action: "redirect"
362362
},
363+
{
364+
btnId: "1631",
365+
btnLabel: "Quick send",
366+
hoverLabel: "Quick send",
367+
btnIcon: "fa-solid fa-envelope",
368+
redirectUrl: "",
369+
action: "bulksend"
370+
},
363371
{
364372
btnId: "1834",
365373
btnLabel: "Delete",

apps/OpenSign/src/json/plansArr.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ const plans = [
5252
"API Access",
5353
"100 API signatures included",
5454
"DocumentId removal from signed docs",
55-
"Custom email templates"
55+
"Custom email templates",
56+
"Auto reminders"
5657
]
5758
},
5859
{

0 commit comments

Comments
 (0)