From 8daf9d9a114490768b9cf2c9a40f6d5ff1160ad6 Mon Sep 17 00:00:00 2001 From: =bhagwanSingh <=bhagwan.r@hicglobalsolutions.com> Date: Tue, 15 Jul 2025 18:39:56 +0530 Subject: [PATCH] 'a' --- src/App.tsx | 9 +- src/components/client/SignDocument.tsx | 117 +++- .../process/processStepOne/ProcessStepOne.tsx | 29 +- .../process/processStepOne/RecipientCards.tsx | 25 +- .../processStepOne/SelectedDocuments.tsx | 16 +- src/components/signature/SignCon.tsx | 612 +++++++++++++++--- src/components/templates/Templates.tsx | 169 ++--- src/store/slices/documentSlice.ts | 315 ++++++--- src/store/slices/selectedRecipientsSlice.tsx | 533 ++++++++++++--- 9 files changed, 1451 insertions(+), 374 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index b204b47..7f24124 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -47,15 +47,18 @@ const App = () => { } /> - - - } /> + + + + } /> + diff --git a/src/components/client/SignDocument.tsx b/src/components/client/SignDocument.tsx index bb62107..8edc3c1 100644 --- a/src/components/client/SignDocument.tsx +++ b/src/components/client/SignDocument.tsx @@ -11,7 +11,7 @@ import { Bounce, toast } from "react-toastify"; import { useDispatch, useSelector } from "react-redux"; import { hideLoader, showLoader } from "../../store/slices/loaderSlice"; -import { FaRegImage, FaFileAlt, FaSignature, FaStamp } from 'react-icons/fa' +import { FaRegImage, FaFileAlt, FaSignature, FaStamp, FaCheck } from 'react-icons/fa' import { IoIosDocument } from 'react-icons/io' import { MdKeyboardArrowDown } from 'react-icons/md' import { TbChevronsDownLeft, TbSignature } from "react-icons/tb"; @@ -100,6 +100,30 @@ export interface StampTypeObj { blob: string; } +interface FIT { + label: string, + value: number | null +} + +const fitObj = [ + { + label: "Fit", + value: "FIT" + }, + { + label: "50%", + value: "50" + }, + { + label: "150%", + value: "150" + }, + { + label: "200%", + value: "200" + }, +] + // Functions - LIST // handleSignClick - Handle when sign || Initial tag got clicked // fetchData - Fetch document images and tags data @@ -126,6 +150,9 @@ const SignDocument = () => { // Show is fields - Showing all fields There const [isFields, setIsFields] = useState(true); + const [isFitDd, setIsFitDd] = useState(false) + const [selFitIndx, setSelFitIndx] = useState(0); + const globalLoader = useSelector((state: RootState) => state.loader) const [openSign, setOpenSign] = useState(false); @@ -146,6 +173,21 @@ const SignDocument = () => { const [signConfObj, setSignConfObj] = useState({}); + const zoom = (percentage: number, contentClass: string) => { + const content = document.querySelector(`.${contentClass}`); + if (content) { + (content as HTMLElement).style.zoom = `${percentage}%`; + } + } + + useEffect(() => { + let indx = fitObj[selFitIndx].value + if (indx === "FIT") { + indx = "100"; + } + zoom(Number(indx), 'zoom-con') + }, [selFitIndx]) + useEffect(() => { dispatch(showLoader("Loading ...")); fetchData(); @@ -162,9 +204,10 @@ const SignDocument = () => { console.log(saved, "Line 162") useEffect(() => { - if (recSignStatus === "COMPLETE") { + if (recSignStatus === "COMPLETED") { console.log("SETTING HIDE TAGS to TRUE LIne 161") - setHideTags(true); + // setHideTags(true); + setTransparentTags(true); } else { console.log("SETTING HIDE TAGS to TRUE LIne 161") setHideTags(false); @@ -447,13 +490,31 @@ const SignDocument = () => { // } // } + const fetchSignatureData = async (signatureId: string) => { + try { + const response = await axios.get(`${import.meta.env.VITE_BASE_URI}/api/sign/signatures/${signatureId}`, { + headers: { "ngrok-skip-browser-warning": "69420" } + }); + if (response.status === 200) { + return response.data.signature; + } + } catch (error) { + console.error("Error fetching signature data:", error); + } + return null; + } useEffect(() => { - if (tagJson) { - for (let i = 0; i < tagJson?.length; i++) { - + (async () => { + if (tagJson) { + for (const tag of tagJson) { + if (tag.type === "SIGNATURE" && "value" in tag) { + const signatureData = await fetchSignatureData(tag.value); + console.log(signatureData); + } + } } - } + })() }, [tagJson]) @@ -498,6 +559,40 @@ const SignDocument = () => {

Sign Made Easy

+ + + {/* FIT - START */} +
setIsFitDd(false)} + > +
setIsFitDd(!isFitDd)} + className='flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'> +

Fit

+ +
+ + {isFitDd &&
+ {fitObj.map((val, indx) => { + return (
setSelFitIndx(indx)} + className='flex z-50 items-center justify-between gap-2 w-[70px] p-2 text-xs cursor-default hover:bg-slate-100'> +
+

{val.label}

+ {selFitIndx === indx && } +
+
) + }) + } +
} +
+ {/* FIT - END */} + + @@ -613,7 +708,7 @@ const SignDocument = () => { {/* Main container */}
{ @@ -668,7 +763,7 @@ const SignDocument = () => {
@@ -967,7 +1062,7 @@ const SignDocument = () => { } {/* tag pagination Start */} - {!transparentTags && !hideTags &&
+ {/* {!transparentTags && !hideTags &&
handleTagNavClick("P")} >Prev
@@ -975,7 +1070,7 @@ const SignDocument = () => {
handleTagNavClick("N")} >Next
-
} +
} */}
diff --git a/src/components/process/processStepOne/ProcessStepOne.tsx b/src/components/process/processStepOne/ProcessStepOne.tsx index 48e1bc1..c6f8a0e 100644 --- a/src/components/process/processStepOne/ProcessStepOne.tsx +++ b/src/components/process/processStepOne/ProcessStepOne.tsx @@ -9,7 +9,7 @@ import { RootState } from "../../../store/store"; import { useEffect, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { setImages } from "../../../store/slices/selectedDocsSlice"; -import { addRecipient, addRecipientAsync, fetchRecipients, recipientType, resetRecipients, updateRecipientIds } from "../../../store/slices/selectedRecipientsSlice"; +import { addRecipient, addRecipientAsync, fetchRecipients, recipientType, resetRecipients, toggleToTemplates, TRANSACTION_TEMPLATE_TOGGLE, updateRecipientIds } from "../../../store/slices/selectedRecipientsSlice"; import { setTransactionId, TRANSACTION_STATUS_ENUMS, updateTransactionStatus } from "../../../store/slices/transactionSlice"; import { hideLoader, showLoader } from "../../../store/slices/loaderSlice"; import Loader from "../../dashboard/Loader"; @@ -22,15 +22,24 @@ const ProcessStepOne = () => { message: string; } - const { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>(); + let transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS; + const templateId = useParams<{ templateId: string }>().templateId; + let { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>(); + + const navigate = useNavigate(); + const dispatch = useDispatch(); + + if (!transactionId) { + transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES; + transactionId = templateId; + dispatch(toggleToTemplates()); + } + console.log("ProcessStepOne called with accountId:", accountId, "and transactionId:", transactionId); if (!accountId || !transactionId) { window.location.href = "/"; return null; } - const navigate = useNavigate(); - const dispatch = useDispatch(); - // const selectedDocuments = useSelector((state: RootState) => state.selectedDocuments.selectedDocuments) // const { selectedDocuments, signType } = useSelector((state: RootState) => state.selectedDocuments) @@ -99,9 +108,9 @@ const ProcessStepOne = () => { order: index, })); - await api.patch(`/api/accounts/${accountId}/transactions/${transactionId}/documents`, { "documents": documentsData }); + await api.patch(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}/documents`, { "documents": documentsData }); - await api.get(`/api/accounts/${accountId}/transactions/${transactionId}/documents?include_images=true`).then(res => { + await api.get(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}/documents?include_images=true`).then(res => { if (res.status === 200) { dispatch(setDocuments(res.data)); } @@ -112,7 +121,7 @@ const ProcessStepOne = () => { dispatch(showLoader("Processing Recipients...")); const recipientsData = selectedRecipients.map((recipient, index) => ({ - // index, + id: "id" in recipient ? recipient.id : null, routingOrder: "routingOrder" in recipient && recipient.routingOrder ? recipient.routingOrder : 1, type: recipient.type, firstName: recipient.firstName, @@ -133,14 +142,12 @@ const ProcessStepOne = () => { })); dispatch(addRecipientAsync({ recipientsData, accountId, transactionId })); - dispatch(showLoader("Processing Message...")); - await api.put(`/api/accounts/${accountId}/transactions/${transactionId}`, { subject: emailDtls.subject, message: emailDtls.message, status: "CREATED" }).then((res) => { + await api.put(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}`, { subject: emailDtls.subject, message: emailDtls.message, status: "CREATED" }).then((res) => { console.log(res); }) - dispatch(hideLoader()); if (isSendNow) { dispatch(updateTransactionStatus(TRANSACTION_STATUS_ENUMS.TRANSACTION_SEND_SUCCESS)); diff --git a/src/components/process/processStepOne/RecipientCards.tsx b/src/components/process/processStepOne/RecipientCards.tsx index 595f9f7..2e8bd3f 100644 --- a/src/components/process/processStepOne/RecipientCards.tsx +++ b/src/components/process/processStepOne/RecipientCards.tsx @@ -1,5 +1,6 @@ import { useDispatch, useSelector } from "react-redux"; import { + removeRecipientAsync, removeSelectedRecipient, // setSigningOrder, updateRecipientOrder, // 👈 You need to define this in your slice @@ -11,6 +12,7 @@ import { FaUsersLine } from "react-icons/fa6"; import { useState } from "react"; import SigningOrderDiagram from "../../../SigningOrderDiagram "; import { IoCloseOutline } from "react-icons/io5"; +import { useParams } from "react-router-dom"; const getInitials = (name: string) => { if (!name) return ""; @@ -22,7 +24,15 @@ const getInitials = (name: string) => { }; const RecipientCards = () => { - const dispatch = useDispatch(); + const templateId = useParams<{ templateId: string }>().templateId; + let { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>(); + + const dispatch = useDispatch(); + + if (!transactionId) { + transactionId = templateId; + } + const recipients = useSelector((state: RootState) => state.selectedRecipients.recipients); // const signingOrder = useSelector((state: RootState) => state.selectedRecipients.signingOrder); const signType = useSelector((state: RootState) => state.selectedDocuments.signType); @@ -34,7 +44,14 @@ const RecipientCards = () => { }; const handleRemoveRecipient = (recipient) => { - dispatch(removeSelectedRecipient(recipient)) + if (!accountId || !transactionId) return; + // dispatch(removeSelectedRecipient(recipient)) + dispatch(removeRecipientAsync({ + recipientId: recipient.id, + email: recipient.email, + accountId: accountId, + transactionId: transactionId, + })) if (recipients.length === 2) { let leftRecipient = recipients.filter((obj) => { if ("id" in obj && "id" in recipient) { @@ -152,8 +169,8 @@ const RecipientCards = () => {

Signing Order

setSigningOrderModal({...signingOrderModal, open: false})} - className="text-xl cursor-pointer hover:text-gray-200 transition-all"> + onClick={() => setSigningOrderModal({ ...signingOrderModal, open: false })} + className="text-xl cursor-pointer hover:text-gray-200 transition-all">
diff --git a/src/components/process/processStepOne/SelectedDocuments.tsx b/src/components/process/processStepOne/SelectedDocuments.tsx index 0738845..5d8509d 100644 --- a/src/components/process/processStepOne/SelectedDocuments.tsx +++ b/src/components/process/processStepOne/SelectedDocuments.tsx @@ -11,15 +11,19 @@ import { Bounce, toast } from "react-toastify"; import api from '../../../config/api'; import DocumentCard from './DocumentCard'; import { useParams } from 'react-router-dom'; +import { toggleToTemplates, TRANSACTION_TEMPLATE_TOGGLE } from '../../../store/slices/selectedRecipientsSlice'; const SelectedDocuments = () => { - const dispatch = useDispatch(); - const documents = useSelector((state: RootState) => state.documents.documents); + // let transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS; - const { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>(); - if (!accountId || !transactionId) { - window.location.href = "/"; - return null; + const documents = useSelector((state: RootState) => state.documents.documents); + const templateId = useParams<{ templateId: string }>().templateId; + let { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>(); + + const dispatch = useDispatch(); + + if (!transactionId) { + transactionId = templateId; } const [isModalOpen, setModalOpen] = useState(false); diff --git a/src/components/signature/SignCon.tsx b/src/components/signature/SignCon.tsx index 402a899..dde2de7 100644 --- a/src/components/signature/SignCon.tsx +++ b/src/components/signature/SignCon.tsx @@ -1,107 +1,549 @@ -import React, { useEffect, useState } from 'react' -import Draw from './Draw' -import TypedSign from './TypedSign'; -import SignUpload from './SignUpload'; -import { IoMdClose } from 'react-icons/io'; -import axios from 'axios'; -import { store } from '../../store/store'; -import { SaveType, SaveTypeEnum } from '../client/SignDocument'; +// import React from 'react' +// import { SaveType } from '../client/SignDocument'; -interface SignConProps { - setOpenSign: React.Dispatch>; - recipientId: string; - saved: SaveType; - setSaved: React.Dispatch>; - // save: () => void; +// interface SignConProps { +// setOpenSign: React.Dispatch>; +// recipientId: string; +// saved: SaveType; +// setSaved: React.Dispatch>; +// // save: () => void; +// } + +// const SignCon: React.FC = ({ setOpenSign, recipientId, saved, setSaved }) => { +// return ( +//
SignCon
+// ) +// } + +// export default SignCon + +import React, { useEffect, useState } from 'react'; +// import api from '../../config/api'; +import DrawSignatureCanvas from '../setting/Signature/DrawSignatureCanvas'; +import { AiOutlineUpload, AiOutlineEdit } from 'react-icons/ai'; +import { MdCloudUpload } from 'react-icons/md'; +import { FaSpinner } from "react-icons/fa"; +import { GrCheckboxSelected } from "react-icons/gr"; +import { Bounce, toast } from "react-toastify"; +import axios from 'axios'; + +interface Signature { + id: string; + email: string; + signatureType: string; + lastName: string; + firstName: string + font: string; + initialsBlob: string; + initialsStampedBlob: string; + initialsHash: string; + signatureHash: string; + signatureBlob: string; + signatureStampedBlob: string; + default: boolean } -const SignCon: React.FC = ({ setOpenSign, recipientId, saved, setSaved }) => { +interface AddSignatureModalProps { + setShowAddSignature?: (value: boolean) => void; + getAllSignatures: () => Promise; + editingSignature?: Signature | null; +} - const [activeSaveType, setActiveSaveType] = React.useState(SaveTypeEnum.DRAW); +interface Account { + id: number; + isDefault: boolean; + name: string; +} - const handleSaveSignature = () => { - if (!saved.save) return +interface UserData { + id: string; + firstName: string; + lastName: string; + email: string; + language: string; + account: Account; +} - console.log("signature:", saved.initial, "initials:", saved.signature, "subType:", saved.saveType, "MIC CHECK") - // return - - axios.post(`${import.meta.env.VITE_BASE_URI}/api/sign/signature?recipientId=${recipientId}`, - { - signature: saved.signature, - initials: saved.initial, - subType: saved.saveType - }, - { headers: { "ngrok-skip-browser-warning": "69420", } }) - .then((res) => { - console.log(res, "Signature saved response") - if (res.status === 200 || res.status === 201) { - setOpenSign(false) - setActiveSaveType(saved.saveType as SaveTypeEnum) +interface SelectStyle { + id: number, + style: string +} + +const AddSignatureModal: React.FC = ({ + setShowAddSignature, + getAllSignatures, + editingSignature +}) => { + const [activeTab, setActiveTab] = useState<'SELECT' | 'DRAW' | 'UPLOAD'>('SELECT'); + const [fullName, setFullName] = useState(''); + const [initials, setInitials] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + const [signatures, setSignatures] = useState<{ + fullSignature: string | null; + initials: string | null; + }>({ fullSignature: null, initials: null }); + + const signatureOptions = [ + { id: 1, name: 'print', initials: 'LA', style: 'print' }, + { id: 2, name: 'cursive', initials: 'LA', style: 'cursive' }, + { id: 3, name: 'script', initials: 'LA', style: 'script' }, + { id: 4, name: 'elegant', initials: 'LA', style: 'elegant' }, + { id: 5, name: 'modern', initials: 'LA', style: 'modern' }, + { id: 6, name: 'classic', initials: 'LA', style: 'classic' }, + ]; + + const [selectedSignature, setSelectedSignature] = useState( + signatureOptions.length > 0 ? { id: signatureOptions[0].id, style: signatureOptions[0].style } : { id: 0, style: "" } + ); + + // Store the actual file + const [file1, setFile1] = useState(null); + const [file2, setFile2] = useState(null); + + // Store the preview URL + const [preview1, setPreview1] = useState(null); + const [preview2, setPreview2] = useState(null); + + // User data + const [user, setUser] = useState({ + id: "", + firstName: "", + lastName: "", + email: "", + language: "", + account: { + id: 0, + isDefault: false, + name: "", + }, + }); + + const isEditMode = !!editingSignature; + + // Navigation items + const navigationItems = [ + { id: 'SELECT', label: 'SELECT', icon: }, + { id: 'DRAW', label: 'DRAW', icon: }, + { id: 'UPLOAD', label: 'UPLOAD', icon: }, + ]; + + // Initialize form with editing signature data + useEffect(() => { + if (editingSignature) { + setFullName(`${editingSignature.firstName} ${editingSignature.lastName}`); + setInitials(`${editingSignature.firstName.slice(0, 1)}${editingSignature.lastName.slice(0, 1)}`); + setActiveTab(editingSignature.signatureType as 'SELECT' | 'DRAW' | 'UPLOAD'); + + // Set font for SELECT tab + const fontOption = signatureOptions.find(option => option.style === editingSignature.font); + if (fontOption) { + setSelectedSignature({ id: fontOption.id, style: fontOption.style }); + } + + // Set signature blobs for all tabs + if (editingSignature.signatureBlob && editingSignature.initialsBlob) { + setSignatures({ + fullSignature: `data:image/png;base64,${editingSignature.signatureBlob}`, + initials: `data:image/png;base64,${editingSignature.initialsBlob}` + }); + + // Set preview URLs for UPLOAD tab + if (editingSignature.signatureType === 'UPLOAD') { + setPreview1(`data:image/png;base64,${editingSignature.signatureBlob}`); + setPreview2(`data:image/png;base64,${editingSignature.initialsBlob}`); } - }) - .catch((err) => { - console.error('Error saving signature:', err); - }) + } + } + }, [editingSignature]); + + const handleSignatureChange = (data: { fullSignature: string | null; initials: string | null }) => { + setSignatures(data); + }; + + // Cleanup preview URLs when component unmounts or image changes + useEffect(() => { + return () => { + if (preview1) URL.revokeObjectURL(preview1); + if (preview2) URL.revokeObjectURL(preview2); + }; + }, [preview1, preview2]); + + const handleImageChange = async ( + e: React.ChangeEvent, + setFile: React.Dispatch>, + setPreview: React.Dispatch>, + type: "fullSignature" | "initials" + ) => { + const file = e.target.files?.[0]; + if (file) { + setFile(file); + const previewUrl = URL.createObjectURL(file); + setPreview(previewUrl); + + // Convert to base64 + const base64 = await convertToBase64(file); + + // Update the appropriate signature + setSignatures((prev) => ({ + ...prev, + [type]: base64, + })); + } + }; + + const convertToBase64 = (file: File): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => resolve(reader.result as string); + reader.onerror = (error) => reject(error); + }); + }; + + const handleSubmit = async () => { + setIsSubmitting(true); + + if (!signatures.fullSignature || !signatures.initials) { + alert('Please provide both signature and initials'); + setIsSubmitting(false); + return; + } + + try { + const payload = { + signatureType: activeTab, + signatureBase64: signatures.fullSignature, + initialsBase64: signatures.initials, + firstName: fullName.split(' ')[0] || user.firstName, + lastName: fullName.split(' ')[1] || user.lastName, + font: selectedSignature.style + }; + + let res; + if (isEditMode && editingSignature) { + // Update existing signature + const updatePayload = { + ...payload, + signatureId: editingSignature.id + }; + res = await axios.put(`${import.meta.env.VITE_BASE_URI}/api/user/signatures`, updatePayload); + console.log("handleUpdate", res); + + toast.success("Signature Updated Successfully", { + position: "top-center", + autoClose: 500, + hideProgressBar: false, + closeOnClick: false, + pauseOnHover: true, + draggable: true, + progress: undefined, + theme: "light", + transition: Bounce, + }); + } else { + // Create new signature + res = await api.post(`/api/user/signatures`, payload); + console.log("handleSubmit", res); + + toast.success("Signature Created Successfully", { + position: "top-center", + autoClose: 500, + hideProgressBar: false, + closeOnClick: false, + pauseOnHover: true, + draggable: true, + progress: undefined, + theme: "light", + transition: Bounce, + }); + } + + if (res?.data) { + await getAllSignatures(); + setShowAddSignature?.(false); + } + } catch (error) { + console.error("Error submitting signature:", error); + toast.error(isEditMode ? "Failed to update signature" : "Failed to create signature", { + position: "top-center", + autoClose: 2000, + hideProgressBar: false, + closeOnClick: false, + pauseOnHover: true, + draggable: true, + progress: undefined, + theme: "light", + transition: Bounce, + }); + } finally { + setIsSubmitting(false); + } + }; + + const renderContent = () => { + switch (activeTab) { + case 'SELECT': + return ( +
+
+

Choose Signature Style

+

Select from pre-designed signature styles

+
+ +
+
+ {signatureOptions.map((option) => ( +
+ setSelectedSignature({ id: option.id, style: option.style })} + className="w-4 h-4 text-blue-600 focus:ring-blue-500" + /> +
+
+
Signed by:
+
+ {fullName || option.name} +
+
+ dca3e646-62cd-4cc... +
+
+
+
DS
+
+ {initials || option.initials} +
+
+
+
+ ))} +
+
+
+ ); + + case 'DRAW': + return ( + + ); + + case 'UPLOAD': + return ( +
+
+

Upload Signature

+
+
+
+
+ {/* Upload 1 */} +
+ + + handleImageChange(e, setFile1, setPreview1, "fullSignature")} + /> +
+ + {/* Upload 2 */} +
+ + + handleImageChange(e, setFile2, setPreview2, "initials")} + /> +
+
+
+
+
+ ); + + default: + return null; + } + }; + + const getUserDetails = async () => { + try { + let res = await api.get(`/api/user/details`); + console.log("getuser", res); + let currUser = res?.data; + setUser(currUser); + + // Only set default values if not in edit mode + if (!isEditMode) { + setFullName(currUser.firstName + " " + currUser.lastName); + setInitials(currUser.firstName.slice(0, 1) + currUser.lastName.slice(0, 1)); + } + } catch (error) { + console.error("Error fetching user details:", error); + } }; useEffect(() => { - handleSaveSignature(); - }, [saved]) + getUserDetails(); + }, []); return ( -
-
+
+
-
- setOpenSign(false)} - className='absolute right-3 text-slate-400 text-lg hover:text-[var(--primary)] cursor-pointer transition-all' /> - {/* Signature Save types Nav */} -
-
setActiveSaveType(SaveTypeEnum.DRAW)}> -

Draw

+ {/* Sidebar Navigation */} +
+ {/* Header */} +
+
+
+

Add Signature

+

Create your digital signature

+
+
-
setActiveSaveType(SaveTypeEnum.TYPE)}> -

Type

+ + {/* Form Inputs */} +
+
+
+ + setFullName(e.target.value)} + placeholder="Enter full name" + className="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +
+ +
+ + setInitials(e.target.value)} + placeholder="Enter initials" + className="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +
+
-
setActiveSaveType(SaveTypeEnum.UPLOAD)}> -

Upload

+ + {/* Navigation Menu */} +
+
+
- {activeSaveType === SaveTypeEnum.DRAW && } - {activeSaveType === SaveTypeEnum.TYPE && } - {activeSaveType === SaveTypeEnum.UPLOAD && } + {/* Main Content Area */} +
+ {/* Header with close button */} - {/*
- - -
*/} + {/* Content */} +
+ {/* */} + {renderContent()} +
+ + {/* Footer Buttons in Main Area */} +
+ + +
+ +
- ) -} + ); +}; -export default SignCon; \ No newline at end of file +export default AddSignatureModal; \ No newline at end of file diff --git a/src/components/templates/Templates.tsx b/src/components/templates/Templates.tsx index b32dc6a..15fd0ae 100644 --- a/src/components/templates/Templates.tsx +++ b/src/components/templates/Templates.tsx @@ -1,127 +1,96 @@ +import { useState } from "react"; import { PiDotsThreeOutlineVerticalFill } from "react-icons/pi"; -import { FaCheck, FaDatabase } from "react-icons/fa"; - -import AddAccountModal from "../accounts/AddAccountModal"; import { Bounce, toast } from "react-toastify"; import api from "../../config/api"; -import { useState } from "react"; +import { IoMdAdd } from "react-icons/io"; +import { useNavigate } from "react-router-dom"; +import { useSelector } from "react-redux"; const Templates = () => { - const [openMenuId, setOpenMenuId] = useState(null); + const [activeFilter, setActiveFilter] = useState("All"); + const accountDetails = useSelector((state: any) => state.user.account); + const accountId = accountDetails.id || ""; - const handleAction = async (accountId: string, action: 'accept' | 'reject') => { - try { - let response = await api.post(`/api/accounts/${accountId}/accept/invite`) - console.log(response) - if (response.data.status === 202) { - toast.success("Login Successful", { - position: "top-center", - autoClose: 5000, - hideProgressBar: false, - closeOnClick: false, - pauseOnHover: true, - draggable: true, - progress: undefined, - theme: "light", - transition: Bounce, - }); - } - } catch (error) { - console.error('Error posting data:', error); - } finally { - setOpenMenuId(null); // close menu - } - }; + const navigate = useNavigate(); + + const filters = ["All", "My Templates", "Shared"]; + + const handleAddTemplate = async () => { + console.log("handleAddTemplate called"); + const res = await api.post(`/api/accounts/${accountId}/templates`); + + navigate(`/process-step-one/${accountId}/template/${res.data}`) + } return ( - <> +
+ {/* Header */} +
+ {/* Filter as pill buttons */} +
+ {filters.map((filter) => ( + + ))} +
-
- - +
+ + +
+ + + {/* Table */} +
+
+ - - - - - - - + + + + + + - - - - + - - - - - - - - + + + + - -
NameOwnerCreated DateLast ChangeNameOwnerCreatedLast Updated
-
- {/* {selectedTransactions.includes(tx.id) && ( - - )} */} +
+
+ {/* */}
-
- {"test"} -
-
{"1"} - - {"role"} - - - - {"text"} - - - - + Test TemplateUser 12025-07-14Just now - - {/* Dropdown menu */} - {/* {openMenuId === "1" && ( -
e.stopPropagation()} // prevent row click closing - > - - -
- )} */}
- - +
); }; diff --git a/src/store/slices/documentSlice.ts b/src/store/slices/documentSlice.ts index b1a67c3..ae655b7 100644 --- a/src/store/slices/documentSlice.ts +++ b/src/store/slices/documentSlice.ts @@ -1,27 +1,188 @@ -import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit"; -// import { v4 as uuidv4 } from "uuid"; +// import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit"; +// // import { v4 as uuidv4 } from "uuid"; -import { hideLoader, showLoader } from "./loaderSlice"; -// import { showModal } from "./modalSlice"; -// import { updateSelectedDocuments } from "./selectedDocsSlice"; +// import { hideLoader, showLoader } from "./loaderSlice"; +// // import { showModal } from "./modalSlice"; +// // import { updateSelectedDocuments } from "./selectedDocsSlice"; -import api from "../../config/api"; -import { Bounce, toast } from "react-toastify"; -// import { createAxiosInstance } from "../../config/axiosInstance"; +// import api from "../../config/api"; +// import { Bounce, toast } from "react-toastify"; +// // import { createAxiosInstance } from "../../config/axiosInstance"; -// const axiosInstance = createAxiosInstance(); +// // const axiosInstance = createAxiosInstance(); +// export interface Document { +// id: string; +// name: string; +// extension: string; +// original?: boolean; +// corrupted?: boolean; +// isCompleted: boolean; +// status: string; +// } + +// export interface DocumentState { +// documents: Document[]; +// loading: boolean; +// error: string | null; +// } + +// const initialState: DocumentState = { +// documents: [], +// loading: false, +// error: null, +// }; + +// interface uploadArgs { +// formData: FormData; +// accountId: string; +// transactionId: string; +// } + + +// // Fetch documents from API +// export const fetchDocuments = createAsyncThunk( +// "documents/fetchDocuments", +// async ({ accountId, transactionId }, { rejectWithValue }) => { + +// try { +// const response = await api.get(`/api/accounts/${accountId}/transactions/${transactionId}/documents?include_images=true`); +// return response.data as Document[]; +// } catch (error: any) { +// // if(error.status === 401) {} +// return rejectWithValue(error.response?.data || "Failed to fetch documents"); +// } +// } +// ); + +// // Upload multiple documents using FormData (Key: "document") +// export const uploadDocuments = createAsyncThunk( +// "documents/uploadDocuments", +// async ({ formData, accountId, transactionId }, { dispatch, rejectWithValue }) => { +// try { +// dispatch(showLoader("Uploading documents...")); +// console.log("Form Data", formData.getAll("document")) + +// const response = await api.post(`/api/accounts/${accountId}/transactions/${transactionId}/documents`, formData, { +// headers: { +// "Content-Type": "multipart/form-data" +// }, +// }); + +// console.log("Form Uploaded Response DocumentSlice Line 185", response.data) + +// dispatch(hideLoader()); +// return [response.data]; +// } catch (error: any) { +// dispatch(hideLoader()); +// toast.error("Error Uploading Document", { +// position: "bottom-right", +// autoClose: 3000, +// hideProgressBar: false, +// closeOnClick: false, +// pauseOnHover: false, +// draggable: false, +// progress: undefined, +// theme: "light", +// transition: Bounce, +// }); + +// console.log("Failed Uploading Documents") +// throw new Error("Failed Uploading Documents") + +// // Mock data in case of failure +// // const failedDocuments = formData.getAll("document").map((file: any) => ({ +// // id: uuidv4(), +// // name: file.name, +// // extension: file.name.split(".").pop() || "unknown", +// // original: Math.random() < 0.5, +// // corrupted: Math.random() < 0.5, +// // })); + +// // dispatch(updateSelectedDocuments({ documents: failedDocuments })); +// // return failedDocuments; // Instead of rejecting, return mock data +// } +// } +// ); + +// const documentSlice = createSlice({ +// name: "documents", +// initialState, +// reducers: { +// setDocuments: (state, action: PayloadAction) => { +// state.documents = action.payload; +// }, +// setDocProgComplete: (state, action: PayloadAction) => { +// const docIndex = state.documents.findIndex(doc => doc.id === action.payload); +// if (docIndex !== -1) { +// state.documents[docIndex].isCompleted = true; +// } +// }, +// resetDocuments: (state) => { +// state.documents = []; +// }, +// deleteDocument: (state, action: PayloadAction) => { +// state.documents = state.documents.filter(doc => doc.id !== action.payload); +// }, +// }, +// extraReducers: (builder) => { +// builder +// // Fetch documents +// .addCase(fetchDocuments.pending, (state) => { +// state.loading = true; +// state.error = null; +// }) +// .addCase(fetchDocuments.fulfilled, (state, action: PayloadAction) => { +// state.loading = false; +// state.documents = action.payload; +// }) +// .addCase(fetchDocuments.rejected, (state, action) => { +// state.loading = false; +// state.error = action.payload as string; +// }) + +// // Upload multiple documents +// .addCase(uploadDocuments.pending, (state) => { +// state.loading = true; +// state.error = null; +// }) +// .addCase(uploadDocuments.fulfilled, (state, action: PayloadAction) => { +// state.loading = false; +// state.documents = [...action.payload, ...state.documents]; +// }) +// .addCase(uploadDocuments.rejected, (state, action) => { +// state.loading = false; +// state.error = action.payload as string; +// }); +// }, +// }); + +// export const { resetDocuments, setDocuments, setDocProgComplete, deleteDocument } = documentSlice.actions; +// export default documentSlice.reducer; + + +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import { toast, Bounce } from 'react-toastify'; +import api from '../../config/api'; +import { hideLoader, showLoader } from './loaderSlice'; +// import { showLoader, hideLoader } from '../loader/loaderSlice'; + +// ----------------------- +// Document Type +// ----------------------- export interface Document { - id: string; + id?: string; name: string; - extension: string; - original?: boolean; - corrupted?: boolean; - isCompleted: boolean; - status: string; + url: string; + type: string; + pageCount?: number; + [key: string]: any; } -export interface DocumentState { +// ----------------------- +// Initial State +// ----------------------- +interface DocumentState { documents: Document[]; loading: boolean; error: string | null; @@ -33,80 +194,81 @@ const initialState: DocumentState = { error: null, }; -interface uploadArgs { - formData: FormData; - accountId: string; - transactionId: string; -} - - -// Fetch documents from API -export const fetchDocuments = createAsyncThunk( - "documents/fetchDocuments", - async ({ accountId, transactionId }, { rejectWithValue }) => { - +// ----------------------- +// Thunk: Fetch Documents +// ----------------------- +export const fetchDocuments = createAsyncThunk< + Document[], + { accountId: string; transactionId: string } +>( + 'documents/fetchDocuments', + async ({ accountId, transactionId }, { rejectWithValue, getState }) => { try { - const response = await api.get(`/api/accounts/${accountId}/transactions/${transactionId}/documents?include_images=true`); + const state = getState() as { + selectedRecipients: { transactionTemplateToggle: string }; + }; + const toggle = state.selectedRecipients.transactionTemplateToggle; + + const response = await api.get( + `/api/accounts/${accountId}/${toggle}/${transactionId}/documents?include_images=true` + ); + return response.data as Document[]; } catch (error: any) { - // if(error.status === 401) {} - return rejectWithValue(error.response?.data || "Failed to fetch documents"); + return rejectWithValue(error.response?.data || 'Failed to fetch documents'); } } ); -// Upload multiple documents using FormData (Key: "document") -export const uploadDocuments = createAsyncThunk( - "documents/uploadDocuments", - async ({ formData, accountId, transactionId }, { dispatch, rejectWithValue }) => { +// ----------------------- +// Thunk: Upload Documents +// ----------------------- +export const uploadDocuments = createAsyncThunk< + Document[], + { formData: FormData; accountId: string; transactionId: string } +>( + 'documents/uploadDocuments', + async ({ formData, accountId, transactionId }, { dispatch, rejectWithValue, getState }) => { try { - dispatch(showLoader("Uploading documents...")); - console.log("Form Data", formData.getAll("document")) + dispatch(showLoader('Uploading documents...')); - const response = await api.post(`/api/accounts/${accountId}/transactions/${transactionId}/documents`, formData, { - headers: { - "Content-Type": "multipart/form-data" - }, - }); + const state = getState() as { + selectedRecipients: { transactionTemplateToggle: string }; + }; + const toggle = state.selectedRecipients.transactionTemplateToggle; - console.log("Form Uploaded Response DocumentSlice Line 185", response.data) + const response = await api.post( + `/api/accounts/${accountId}/${toggle}/${transactionId}/documents`, + formData, + { + headers: { + 'Content-Type': 'multipart/form-data', + }, + } + ); dispatch(hideLoader()); return [response.data]; } catch (error: any) { dispatch(hideLoader()); - toast.error("Error Uploading Document", { - position: "bottom-right", + + toast.error('Error Uploading Document', { + position: 'bottom-right', autoClose: 3000, - hideProgressBar: false, - closeOnClick: false, - pauseOnHover: false, - draggable: false, - progress: undefined, - theme: "light", + theme: 'light', transition: Bounce, }); - console.log("Failed Uploading Documents") - throw new Error("Failed Uploading Documents") - - // Mock data in case of failure - // const failedDocuments = formData.getAll("document").map((file: any) => ({ - // id: uuidv4(), - // name: file.name, - // extension: file.name.split(".").pop() || "unknown", - // original: Math.random() < 0.5, - // corrupted: Math.random() < 0.5, - // })); - - // dispatch(updateSelectedDocuments({ documents: failedDocuments })); - // return failedDocuments; // Instead of rejecting, return mock data + return rejectWithValue('Failed Uploading Documents'); } } ); -const documentSlice = createSlice({ - name: "documents", +// ----------------------- +// Slice +// ----------------------- +const documentsSlice = createSlice({ + name: 'documents', initialState, reducers: { setDocuments: (state, action: PayloadAction) => { @@ -127,7 +289,7 @@ const documentSlice = createSlice({ }, extraReducers: (builder) => { builder - // Fetch documents + // fetchDocuments .addCase(fetchDocuments.pending, (state) => { state.loading = true; state.error = null; @@ -139,16 +301,22 @@ const documentSlice = createSlice({ .addCase(fetchDocuments.rejected, (state, action) => { state.loading = false; state.error = action.payload as string; + toast.error('Failed to fetch documents', { + position: 'bottom-right', + autoClose: 3000, + theme: 'light', + transition: Bounce, + }); }) - // Upload multiple documents + // uploadDocuments .addCase(uploadDocuments.pending, (state) => { state.loading = true; state.error = null; }) .addCase(uploadDocuments.fulfilled, (state, action: PayloadAction) => { state.loading = false; - state.documents = [...action.payload, ...state.documents]; + state.documents.push(...action.payload); }) .addCase(uploadDocuments.rejected, (state, action) => { state.loading = false; @@ -157,5 +325,8 @@ const documentSlice = createSlice({ }, }); -export const { resetDocuments, setDocuments, setDocProgComplete, deleteDocument } = documentSlice.actions; -export default documentSlice.reducer; \ No newline at end of file +// ----------------------- +// Exports +// ----------------------- +export const { deleteDocument, resetDocuments, setDocuments, setDocProgComplete } = documentsSlice.actions; +export default documentsSlice.reducer; diff --git a/src/store/slices/selectedRecipientsSlice.tsx b/src/store/slices/selectedRecipientsSlice.tsx index 9feaa2c..85c610c 100644 --- a/src/store/slices/selectedRecipientsSlice.tsx +++ b/src/store/slices/selectedRecipientsSlice.tsx @@ -1,11 +1,340 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { createAsyncThunk } from '@reduxjs/toolkit'; +// import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +// import { createAsyncThunk } from '@reduxjs/toolkit'; +// import { toast } from 'react-toastify'; +// import colors from '../../utlity/colorsArr'; +// import api from '../../config/api'; + + + +// export enum recipientType { +// SIGNER = "Signer", +// INPERSONSIGNER = "InPersonSigner", +// CC = "CC" +// } + +// export interface Recipient { +// id?: string; +// firstName: string; +// lastName: string; +// routingOrder: number; +// email: string; +// hostName: string; +// hostEmail: string; +// type: recipientType; +// color?: string; +// } + +// export interface SelectedRecipientInt { +// recipients: Recipient[], +// signingOrder: boolean, +// transactionTemplateToggle: TRANSACTION_TEMPLATE_TOGGLE +// } + +// interface AddRecipientArgs { +// recipientsData: Recipient[]; +// accountId: string; +// transactionId: string; +// } + +// export enum TRANSACTION_TEMPLATE_TOGGLE { +// TRANSACTIONS = "transactions", +// TEMPLATES = "templates" +// } + +// const initialState: SelectedRecipientInt = { +// recipients: [], +// signingOrder: false, +// transactionTemplateToggle: TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS +// }; + +// export const addRecipientAsync = createAsyncThunk( +// 'selectedRecipients/addRecipientAsync', +// async ({ recipientsData, accountId, transactionId }, { rejectWithValue }) => { +// console.log(recipientsData, accountId, transactionId, "recipientsData Line 46"); +// try { +// console.log(recipientsData, "recipientsData --- Line 48"); + +// const updateRecipientsData = recipientsData.filter((recipient, idx) => recipient.id !== null); +// const createRecipientsData = recipientsData.filter((recipient, idx) => recipient.id == null); + +// console.log(updateRecipientsData, "updateRecipientsData Line 52"); +// console.log(createRecipientsData, "createRecipientsData Line 53"); + +// const resUpdate = await api.put( +// `/api/accounts/${accountId}/transactions/${transactionId}/recipients`, +// updateRecipientsData +// ); + +// const resCreate = await api.post( +// `/api/accounts/${accountId}/transactions/${transactionId}/recipients`, +// createRecipientsData +// ); + +// if (createRecipientsData.length === 0) resCreate.data = []; +// if (updateRecipientsData.length === 0) resUpdate.data = []; + +// console.log(resCreate, "resCreate"); +// console.log(resUpdate, "resUpdate"); + +// return [...resCreate.data, ...resUpdate.data] as Recipient[]; +// } catch (err: any) { +// return rejectWithValue(err.response?.data || "Failed to add recipient"); +// } +// } +// ); + +// interface RemoveRecipientAsyncInterfaceObj { +// id?: string; +// email?: string; +// } + +// interface RemoveRecipientAsyncInterface { +// recipientId: number | null; +// accountId: string; +// transactionId: string; +// email: string; +// } + +// export const removeRecipientAsync = createAsyncThunk( +// 'selectedRecipients/removeRecipientAsync', +// async (obj, { rejectWithValue }) => { +// try { +// if (obj.recipientId) { +// const res = await api.delete(`/api/accounts/${obj.accountId}/transactions/${obj.transactionId}/recipients/${obj.recipientId}`); +// console.log(res, "Line 77") +// return obj.recipientId; +// } +// return obj.email +// } catch (err: any) { +// return rejectWithValue("Failed to remove recipient"); +// } +// } +// ); + +// export const fetchRecipients = createAsyncThunk( +// 'selectedRecipients/fetchRecipients', +// async ({ accountId, transactionId }, { rejectWithValue }) => { +// try { +// const response = await api.get(`/api/accounts/${accountId}/transactions/${transactionId}/recipients`); +// return response.data as Recipient[]; +// } catch (err: any) { +// return rejectWithValue(err.response?.data || "Failed to fetch recipients"); +// } +// } +// ); + +// const selectedRecipientsSlice = createSlice({ +// name: 'selectedRecipients', +// initialState, +// reducers: { +// // Add a single recipient +// addRecipient: (state, action: PayloadAction) => { +// for (const recipient of state.recipients) { +// if (recipient.email === action.payload.email) { +// toast.dismiss(); +// toast.error("Email already exists !", { +// position: "bottom-right", +// autoClose: 5000, +// hideProgressBar: false, +// closeOnClick: false, +// pauseOnHover: true, +// draggable: true, +// progress: undefined, +// theme: "light", +// }); +// return state; +// } +// } + +// // Adding Recipient on top if type is inPersonSigner +// console.log("Getting Type ", action.payload.type) +// if (action.payload.type === recipientType.INPERSONSIGNER) { +// state.signingOrder = true +// state.recipients = [action.payload, ...state.recipients] +// } else { +// state.recipients.push(action.payload); +// } + +// }, +// resetRecipients: (state) => { +// state.recipients = []; +// }, +// toggleToTemplates: (state) => { +// state.transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES; +// }, +// // Replace the entire selectedRecipients array +// setSelectedRecipients: (state, action: PayloadAction) => { +// for (const [index, recipient] of action.payload.entries()) { +// if ((recipient.type === recipientType.INPERSONSIGNER && index > 0) && action.payload[index - 1].type !== recipientType.INPERSONSIGNER) { +// return state; +// } +// } + +// state.recipients = action.payload; +// }, +// setSigningOrder: (state, action: PayloadAction) => { + +// for (const recipient of state.recipients) { +// if (recipient.type === recipientType.INPERSONSIGNER) { +// if (state.signingOrder) return state; +// state.signingOrder = true; +// return state +// } +// } + +// state.signingOrder = action.payload; +// }, +// // Remove Recipients +// removeSelectedRecipient: (state, action: PayloadAction) => { +// console.log("removeSelectedRecipient Called") +// state.recipients = state.recipients.filter((recipient) => recipient.email !== action.payload.email); +// }, + +// // Add Color in Recipient +// addColorsToRecipient: (state) => { +// // const colors = generateUniqueColors(state.recipients.length); +// for (const [index, recipient] of state.recipients.entries()) { +// console.log(state) +// recipient.color = colors[index] +// } +// return state; +// }, +// updateRecipientOrder: (state, action: PayloadAction<{ id: string; routingOrder: number }>) => { +// console.log("recipient order called Line 159") +// const { id, routingOrder } = action.payload; +// const recipient = state.recipients.find((r) => r.id === id || r.email === id); +// if (recipient) { +// recipient.routingOrder = routingOrder; +// } +// }, +// updateRecipientIds: (state, action: PayloadAction) => { +// console.log("updateRecipientIds", console.table(action.payload)); +// for (const recObj of state.recipients) { +// const email = recObj.email +// for (const recObj2 of action.payload) { +// recObj.id = recObj2.id; +// } +// } +// }, +// resetSelRecipient: (state) => { +// state = initialState; +// return state; +// } +// }, +// extraReducers: (builder) => { +// builder +// // Fetch Recipient - START +// .addCase(fetchRecipients.pending, (state) => { +// }) +// .addCase(fetchRecipients.fulfilled, (state, action: PayloadAction) => { +// // console.log("Fetched recipients successfully:", action.payload); + +// // Assign colors to recipients +// const assignedRecipients = action.payload.map((recipient, index) => ({ +// ...recipient, +// color: colors[index], // Cycle if more recipients than colors +// })); + +// state.recipients = assignedRecipients; +// }) +// .addCase(fetchRecipients.rejected, (state, action) => { +// console.error("Failed to fetch recipients:", action.payload); +// toast.error("Failed to fetch recipients", { +// position: "bottom-right", +// autoClose: 5000, +// hideProgressBar: false, +// closeOnClick: false, +// pauseOnHover: true, +// draggable: true, +// progress: undefined, +// theme: "light", +// }); +// }) +// // Fetch Recipient - END + +// // Add Recipient - START +// .addCase(addRecipientAsync.pending, (state) => { + +// }) + + +// .addCase(addRecipientAsync.fulfilled, (state, action: PayloadAction) => { +// // Optionally avoid duplicates if needed + +// console.log("addRecipientAsync Called Line 258", action.payload); +// // const newRecipients = action.payload.filter(newRec => { +// // return !state.recipients.some(existing => existing existing.id === newRec.id); +// // }); + +// // const newRecipients = action.payload.filter(newRec => { +// // return !state.recipients.some(existing => "id" in existing && existing.id === newRec.id); +// // }); + +// // // Add new recipients and assign colors +// // const startIndex = state.recipients.length; +// // newRecipients.forEach((rec, index) => { +// // rec.color = colors[startIndex + index]; +// // state.recipients.push(rec); +// // }); + +// state.recipients = action.payload.map((recipient, index) => ({ +// ...recipient, +// color: colors[index % colors.length], // Cycle through colors +// })); + +// }) + + +// .addCase(addRecipientAsync.rejected, (state, action) => { +// console.error("Failed to fetch recipients:", action.payload); +// toast.error("Failed to fetch recipients", { +// position: "bottom-right", +// autoClose: 5000, +// hideProgressBar: false, +// closeOnClick: false, +// pauseOnHover: true, +// draggable: true, +// progress: undefined, +// theme: "light", +// }); +// }) +// // Add Recipient - END + +// // Fetch Recipient - START +// .addCase(removeRecipientAsync.pending, (state) => { +// }) +// .addCase(removeRecipientAsync.fulfilled, (state, action: PayloadAction) => { +// state.recipients = state.recipients.filter( +// (recipient) => recipient.id !== action.payload && recipient.email !== action.payload +// ); +// }) +// .addCase(removeRecipientAsync.rejected, (state, action) => { +// console.error("Failed to Delete recipients:", action.payload); +// toast.error("Failed to Delete recipients", { +// position: "bottom-right", +// autoClose: 5000, +// hideProgressBar: false, +// closeOnClick: false, +// pauseOnHover: true, +// draggable: true, +// progress: undefined, +// theme: "light", +// }); +// }) +// // Fetch Recipient - END +// } +// }); + +// export const { addRecipient, setSelectedRecipients, toggleToTemplates, removeSelectedRecipient, setSigningOrder, addColorsToRecipient, updateRecipientIds, resetSelRecipient, updateRecipientOrder, resetRecipients } = selectedRecipientsSlice.actions; +// export default selectedRecipientsSlice.reducer; + + + +import { createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit'; import { toast } from 'react-toastify'; import colors from '../../utlity/colorsArr'; import api from '../../config/api'; - - export enum recipientType { SIGNER = "Signer", INPERSONSIGNER = "InPersonSigner", @@ -26,54 +355,106 @@ export interface Recipient { export interface SelectedRecipientInt { recipients: Recipient[], - signingOrder: boolean + signingOrder: boolean, + transactionTemplateToggle: TRANSACTION_TEMPLATE_TOGGLE } interface AddRecipientArgs { recipientsData: Recipient[]; accountId: string; transactionId: string; + transactionTemplateToggle: TRANSACTION_TEMPLATE_TOGGLE; +} + +interface RemoveRecipientAsyncInterface { + recipientId: number | null; + accountId: string; + transactionId: string; + email: string; + transactionTemplateToggle: TRANSACTION_TEMPLATE_TOGGLE; +} + +export enum TRANSACTION_TEMPLATE_TOGGLE { + TRANSACTIONS = "transactions", + TEMPLATES = "templates" } const initialState: SelectedRecipientInt = { recipients: [], - signingOrder: false + signingOrder: false, + transactionTemplateToggle: TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS }; -export const addRecipientAsync = createAsyncThunk( +// 1. Add Recipient Async +export const addRecipientAsync = createAsyncThunk< + Recipient[], + { recipientsData: Recipient[]; accountId: string; transactionId: string } +>( 'selectedRecipients/addRecipientAsync', - async ({ recipientsData, accountId, transactionId }, { rejectWithValue }) => { - console.log(recipientsData, accountId, transactionId, "recipientsData Line 46"); + async ({ recipientsData, accountId, transactionId }, { rejectWithValue, getState }) => { try { - const response = await api.post( - `/api/accounts/${accountId}/transactions/${transactionId}/recipients`, - recipientsData - ); - console.log(response) - return response.data as Recipient[]; + const state = getState() as { selectedRecipients: SelectedRecipientInt }; + const toggle = state.selectedRecipients.transactionTemplateToggle; + + const updateRecipientsData = recipientsData.filter(recipient => recipient.id !== null); + const createRecipientsData = recipientsData.filter(recipient => recipient.id == null); + + const basePath = `/api/accounts/${accountId}/${toggle}/${transactionId}/recipients`; + + const resUpdate = updateRecipientsData.length + ? await api.put(basePath, updateRecipientsData) + : { data: [] }; + + const resCreate = createRecipientsData.length + ? await api.post(basePath, createRecipientsData) + : { data: [] }; + + return [...resCreate.data, ...resUpdate.data] as Recipient[]; } catch (err: any) { return rejectWithValue(err.response?.data || "Failed to add recipient"); } } ); -export const removeRecipientAsync = createAsyncThunk( +// 2. Remove Recipient Async +export const removeRecipientAsync = createAsyncThunk< + string | number, + { recipientId: number | null; accountId: string; transactionId: string; email: string } +>( 'selectedRecipients/removeRecipientAsync', - async (email, { rejectWithValue }) => { + async ({ recipientId, accountId, transactionId, email }, { rejectWithValue, getState }) => { try { - await api.delete(`/api/recipients`, { data: { email } }); - return email; // return email so we can filter from state + const state = getState() as { selectedRecipients: SelectedRecipientInt }; + const toggle = state.selectedRecipients.transactionTemplateToggle; + + const basePath = `/api/accounts/${accountId}/${toggle}/${transactionId}/recipients`; + + if (recipientId) { + await api.delete(`${basePath}/${recipientId}`); + return recipientId; + } + + return email; } catch (err: any) { - return rejectWithValue(err.response?.data || "Failed to remove recipient"); + return rejectWithValue("Failed to remove recipient"); } } ); -export const fetchRecipients = createAsyncThunk( +// 3. Fetch Recipients Async +export const fetchRecipients = createAsyncThunk< + Recipient[], + { accountId: string; transactionId: string } +>( 'selectedRecipients/fetchRecipients', - async ({ accountId, transactionId }, { rejectWithValue }) => { + async ({ accountId, transactionId }, { rejectWithValue, getState }) => { try { - const response = await api.get(`/api/accounts/${accountId}/transactions/${transactionId}/recipients`); + const state = getState() as { selectedRecipients: SelectedRecipientInt }; + const toggle = state.selectedRecipients.transactionTemplateToggle; + + const basePath = `/api/accounts/${accountId}/${toggle}/${transactionId}/recipients`; + const response = await api.get(basePath); + return response.data as Recipient[]; } catch (err: any) { return rejectWithValue(err.response?.data || "Failed to fetch recipients"); @@ -85,7 +466,6 @@ const selectedRecipientsSlice = createSlice({ name: 'selectedRecipients', initialState, reducers: { - // Add a single recipient addRecipient: (state, action: PayloadAction) => { for (const recipient of state.recipients) { if (recipient.email === action.payload.email) { @@ -93,34 +473,29 @@ const selectedRecipientsSlice = createSlice({ toast.error("Email already exists !", { position: "bottom-right", autoClose: 5000, - hideProgressBar: false, - closeOnClick: false, - pauseOnHover: true, - draggable: true, - progress: undefined, theme: "light", }); return state; } } - // Adding Recipient on top if type is inPersonSigner - console.log("Getting Type ", action.payload.type) if (action.payload.type === recipientType.INPERSONSIGNER) { - state.signingOrder = true - state.recipients = [action.payload, ...state.recipients] + state.signingOrder = true; + state.recipients = [action.payload, ...state.recipients]; } else { state.recipients.push(action.payload); } - }, resetRecipients: (state) => { state.recipients = []; }, - // Replace the entire selectedRecipients array + toggleToTemplates: (state) => { + state.transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES; + }, setSelectedRecipients: (state, action: PayloadAction) => { for (const [index, recipient] of action.payload.entries()) { - if ((recipient.type === recipientType.INPERSONSIGNER && index > 0) && action.payload[index - 1].type !== recipientType.INPERSONSIGNER) { + if ((recipient.type === recipientType.INPERSONSIGNER && index > 0) + && action.payload[index - 1].type !== recipientType.INPERSONSIGNER) { return state; } } @@ -128,35 +503,26 @@ const selectedRecipientsSlice = createSlice({ state.recipients = action.payload; }, setSigningOrder: (state, action: PayloadAction) => { - for (const recipient of state.recipients) { if (recipient.type === recipientType.INPERSONSIGNER) { if (state.signingOrder) return state; state.signingOrder = true; - return state + return state; } } state.signingOrder = action.payload; }, - // Remove Recipients removeSelectedRecipient: (state, action: PayloadAction) => { - console.log("removeSelectedRecipient Called") state.recipients = state.recipients.filter((recipient) => recipient.email !== action.payload.email); }, - - // Add Color in Recipient addColorsToRecipient: (state) => { - // const colors = generateUniqueColors(state.recipients.length); for (const [index, recipient] of state.recipients.entries()) { - console.log(state) - debugger - recipient.color = colors[index] + recipient.color = colors[index]; } return state; }, updateRecipientOrder: (state, action: PayloadAction<{ id: string; routingOrder: number }>) => { - console.log("recipient order called Line 159") const { id, routingOrder } = action.payload; const recipient = state.recipients.find((r) => r.id === id || r.email === id); if (recipient) { @@ -164,32 +530,22 @@ const selectedRecipientsSlice = createSlice({ } }, updateRecipientIds: (state, action: PayloadAction) => { - console.log("updateRecipientIds", console.table(action.payload)); for (const recObj of state.recipients) { - const email = recObj.email for (const recObj2 of action.payload) { recObj.id = recObj2.id; } } }, - resetSelRecipient: (state) => { - state = initialState; - return state; - } + resetSelRecipient: () => initialState }, extraReducers: (builder) => { builder - .addCase(fetchRecipients.pending, (state) => { - }) + .addCase(fetchRecipients.pending, () => { }) .addCase(fetchRecipients.fulfilled, (state, action: PayloadAction) => { - // console.log("Fetched recipients successfully:", action.payload); - - // Assign colors to recipients const assignedRecipients = action.payload.map((recipient, index) => ({ ...recipient, - color: colors[index], // Cycle if more recipients than colors + color: colors[index], })); - state.recipients = assignedRecipients; }) .addCase(fetchRecipients.rejected, (state, action) => { @@ -197,39 +553,52 @@ const selectedRecipientsSlice = createSlice({ toast.error("Failed to fetch recipients", { position: "bottom-right", autoClose: 5000, - hideProgressBar: false, - closeOnClick: false, - pauseOnHover: true, - draggable: true, - progress: undefined, theme: "light", }); }) - - - .addCase(addRecipientAsync.pending, (state) => { - - }) - .addCase(addRecipientAsync.fulfilled, (state, action: PayloadAction) => { - console.log("Added recipient successfully:", action.payload); - // state.recipients.push(action.payload); + .addCase(addRecipientAsync.pending, () => { }) + .addCase(addRecipientAsync.fulfilled, (state, action: PayloadAction) => { + state.recipients = action.payload.map((recipient, index) => ({ + ...recipient, + color: colors[index % colors.length], + })); }) .addCase(addRecipientAsync.rejected, (state, action) => { - console.error("Failed to fetch recipients:", action.payload); - toast.error("Failed to fetch recipients", { + console.error("Failed to add recipients:", action.payload); + toast.error("Failed to add recipients", { + position: "bottom-right", + autoClose: 5000, + theme: "light", + }); + }) + .addCase(removeRecipientAsync.pending, () => { }) + .addCase(removeRecipientAsync.fulfilled, (state, action: PayloadAction) => { + state.recipients = state.recipients.filter( + (recipient) => recipient.id !== action.payload && recipient.email !== action.payload + ); + }) + .addCase(removeRecipientAsync.rejected, (state, action) => { + console.error("Failed to delete recipient:", action.payload); + toast.error("Failed to delete recipient", { position: "bottom-right", autoClose: 5000, - hideProgressBar: false, - closeOnClick: false, - pauseOnHover: true, - draggable: true, - progress: undefined, theme: "light", }); }); - } }); -export const { addRecipient, setSelectedRecipients, removeSelectedRecipient, setSigningOrder, addColorsToRecipient, updateRecipientIds, resetSelRecipient, updateRecipientOrder, resetRecipients } = selectedRecipientsSlice.actions; +export const { + addRecipient, + setSelectedRecipients, + toggleToTemplates, + removeSelectedRecipient, + setSigningOrder, + addColorsToRecipient, + updateRecipientIds, + resetSelRecipient, + updateRecipientOrder, + resetRecipients +} = selectedRecipientsSlice.actions; + export default selectedRecipientsSlice.reducer;