SME-Web/src/components/process/processStepOne/RecipientCards.tsx

756 lines
40 KiB
TypeScript

import { useDispatch, useSelector } from "react-redux";
import {
removeRecipientAsync,
removeSelectedRecipient,
updateRecipientOrder,
TRANSACTION_TEMPLATE_TOGGLE,
updateRecipient,
recipientType,
setSelectedRecipients,
reorderAllRecipientAscOrder,
updateRecipientByIndxAndRo,
} from "../../../store/slices/selectedRecipientsSlice";
import { AppDispatch, RootState } from "../../../store/store";
import { ImBin } from "react-icons/im";
import { useState } from "react";
import SigningOrderDiagram from "../../../SigningOrderDiagram ";
import { IoCloseOutline } from "react-icons/io5";
import { useParams } from "react-router-dom";
import { FaUsersLine } from "react-icons/fa6";
import { BiSolidHelpCircle } from "react-icons/bi";
import {
DndContext,
closestCenter,
DragEndEvent,
} from "@dnd-kit/core";
import {
arrayMove,
SortableContext,
useSortable,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import SortableRecipient from "./SortableRecipient";
const getInitials = (name: string) => {
if (!name) return "";
return name
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase();
};
type CardsData = {
userEmail: string;
// getMinRoutingOrder: () => number;
editRecipient: any;
setEditRecipient: (recipient: any) => void;
setEditCase: (string: any) => void;
transactionTemplateToggle: string;
editRecipientCurrentEmail: string;
setEditRecipientCurrentEmail: (email: string) => void;
};
const RecipientCards = ({
setEditRecipient,
setEditCase,
transactionTemplateToggle,
editRecipient,
editRecipientCurrentEmail,
setEditRecipientCurrentEmail,
}: CardsData) => {
const templateId = useParams<{ templateId: string }>().templateId;
let { accountId, transactionId } = useParams<{ accountId: string; transactionId: string }>();
const dispatch = useDispatch<AppDispatch>();
const signingOrder = useSelector((state: RootState) => state.selectedRecipients.signingOrder)
if (!transactionId) {
transactionId = templateId;
}
const recipients = useSelector((state: RootState) => state.selectedRecipients.recipients);
const signType = useSelector((state: RootState) => state.selectedDocuments.signType);
const [signingOrderModal, setSigningOrderModal] = useState({ open: false });
const [editingField, setEditingField] = useState<{ recipientId: string; fieldName: string } | null>(null);
const [localChanges, setLocalChanges] = useState<Record<string, any>>({});
const [errors, setErrors] = useState<Record<string, { hostName: boolean; hostEmail: boolean }>>({});
const getRecipientIdentifier = (recipient: any) => {
return recipient.id || recipient.uuid;
};
const isEditable = (recipient: any) => {
return transactionTemplateToggle == TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES
? true
: recipient.newOne || recipient.allowSenderToEdit;
};
const validateInPersonSignerFields = (recipient: any, updatedFields: any) => {
if (recipient.type != recipientType.INPERSONSIGNER) return true;
const hostName = updatedFields.hostName ?? getCurrentValue(recipient, "hostName");
const hostEmail = updatedFields.hostEmail ?? getCurrentValue(recipient, "hostEmail");
const identifier = getRecipientIdentifier(recipient);
const newErrors = { hostName: !hostName, hostEmail: !hostEmail };
setErrors((prev) => ({ ...prev, [identifier]: newErrors }));
return !newErrors.hostName && !newErrors.hostEmail;
};
const handleFieldFocus = (recipientId: string, fieldName: string) => {
setEditingField({ recipientId, fieldName });
};
const handleFieldBlur = (recipient: any, fieldName: string, value: any) => {
const identifier = getRecipientIdentifier(recipient);
const updatedRecipient = { ...recipient, [fieldName]: value, uuid: identifier };
if (recipient.type == recipientType.INPERSONSIGNER && (fieldName == "hostName" || fieldName == "hostEmail")) {
if (!validateInPersonSignerFields(recipient, { [fieldName]: value })) {
return;
}
}
dispatch(updateRecipient(updatedRecipient));
setLocalChanges((prev) => ({
...prev,
[`${identifier}_${fieldName}`]: undefined,
}));
setEditingField(null);
setErrors((prev) => ({ ...prev, [identifier]: { hostName: false, hostEmail: false } }));
};
const handleFieldChange = (recipient: any, fieldName: string, value: any) => {
const identifier = getRecipientIdentifier(recipient);
setLocalChanges((prev) => ({
...prev,
[`${identifier}_${fieldName}`]: value,
}));
};
const getCurrentValue = (recipient: any, fieldName: string) => {
const identifier = getRecipientIdentifier(recipient);
const localValue = localChanges[`${identifier}_${fieldName}`];
return localValue != undefined ? localValue : recipient[fieldName];
};
const hasValue = (recipient: any, fieldName: string) => {
const currentValue = getCurrentValue(recipient, fieldName);
return currentValue && currentValue.toString().trim() != "";
};
const renderEditableField = (
recipient: any,
fieldName: string,
placeholder: string,
className = "",
type = "text"
) => {
const identifier = getRecipientIdentifier(recipient);
const currentValue = getCurrentValue(recipient, fieldName);
const error = errors[identifier]?.[fieldName];
const editable = isEditable(recipient);
return (
<div className="relative inline-block w-full sm:mx-1 lg:mx-3">
<input
type={type}
value={currentValue || ""}
placeholder={placeholder}
onFocus={() => handleFieldFocus(identifier, fieldName)}
onBlur={(e) => handleFieldBlur(recipient, fieldName, e.target.value)}
// onChange={(e) => handleFieldChange(recipient, fieldName, e.target.value)}
onInput={(e) => handleFieldChange(recipient, fieldName, (e.target as HTMLInputElement).value)}
onKeyPress={(e) => {
if (e.key == "Enter") {
e.currentTarget.blur();
}
}}
disabled={!editable}
className={`w-full px-2 sm:px-3 lg:px-4 py-1.5 sm:py-2 font-normal border focus:outline-none focus:ring-2 focus:ring-[var(--primary)] transition ${error ? "border-red-500 placeholder-red-500 text-xs" : "border-gray-300"} rounded-lg text-xs sm:text-sm ${className} ${!editable ? "bg-gray-100 cursor-not-allowed" : ""}`}
title={editable ? "Edit field" : "Editing disabled"}
/>
</div>
);
};
const handleOrderChange = (id: string, value: number) => {
dispatch(updateRecipientOrder({ id, routingOrder: value }));
};
const handleEditRecipient = (recipient: any) => {
setEditCase("Edit");
setEditRecipient(recipient);
setEditRecipientCurrentEmail(recipient.id || recipient.uuid);
};
const handleRemoveRecipient = (recipient: any) => {
if (!accountId || !transactionId) return;
if (recipient.uuid) {
dispatch(removeSelectedRecipient(recipient));
} else {
dispatch(
removeRecipientAsync({
recipientId: recipient.id,
email: recipient.email,
accountId: accountId,
transactionId: transactionId,
})
);
}
if (recipients.length == 2) {
const leftRecipient = recipients.filter((obj) => {
if ("id" in obj && "id" in recipient) {
return obj.id != recipient.id;
}
if ("email" in obj && "email" in recipient) {
return obj.email != recipient.email;
}
return false;
});
dispatch(
updateRecipientOrder({ id: leftRecipient[0].id || leftRecipient[0].email, routingOrder: 1 })
);
}
};
const handleTypeChange = (recipient: any, type: string) => {
const identifier = getRecipientIdentifier(recipient);
const updatedRecipient = { ...recipient, type, uuid: identifier };
if (type == recipientType.SIGNER || type == recipientType.CC) {
updatedRecipient.hostName = "";
updatedRecipient.hostEmail = "";
setErrors((prev) => ({ ...prev, [identifier]: { hostName: false, hostEmail: false } }));
} else if (type == recipientType.INPERSONSIGNER) {
if (!validateInPersonSignerFields(recipient, updatedRecipient)) {
return;
}
}
dispatch(updateRecipient(updatedRecipient));
setEditingField(null);
};
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (!over || active.id == over.id) return;
console.log(over, "Over")
console.log(active, "Active")
const activeIndex = recipients.findIndex((r) => (r.id || r.uuid) == active.id);
const overIndex = recipients.findIndex((r) => (r.id || r.uuid) == over.id);
const activeRec = recipients[activeIndex]
const overRec = recipients[overIndex]
const activeRecRo = Number(activeRec.routingOrder) == 0 || !activeRec.routingOrder ? 1 : activeRec.routingOrder
const overRecRo = Number(overRec.routingOrder) == 0 || !overRec.routingOrder ? 1 : overRec.routingOrder
let newActiveRecRo = 1
let newOverRecRo = 1
// if (activeRec.routingOrder >= overRecRo && activeIndex > overIndex) {
// newActiveRecRo = activeRecRo > 1 ? activeRecRo - 1 : 1
// newOverRecRo = activeRecRo > 1 ? activeRecRo + 1 : 2
// }
// if (activeRec.routingOrder >= overRecRo && activeIndex < overIndex) {
// newActiveRecRo = overRecRo > 1 ? overRecRo + 1 : 2
// newOverRecRo = overRecRo > 1 ? overRecRo - 1 : 1
// }
// if (activeRec.routingOrder <= overRecRo && activeIndex > overIndex) {
// newActiveRecRo = activeRecRo > 1 ? activeRecRo - 1 : 1
// newOverRecRo = activeRecRo > 1 ? activeRecRo + 1 : 2
// }
// if (activeRec.routingOrder <= overRecRo && activeIndex < overIndex) {
// newActiveRecRo = overRecRo > 1 ? overRecRo + 1 : 2
// newOverRecRo = overRecRo > 1 ? overRecRo - 1 : 1
// }
// if (activeRec.routingOrder > overRecRo && activeIndex < overIndex) {
// console.log(activeIndex, overIndex, "283 2")
// newActiveRecRo = overRecRo > 1 ? overRecRo : 2
// newOverRecRo = activeRecRo > 1 ? activeRecRo : 1
// }
// if (activeRec.routingOrder < overRecRo && activeIndex > overIndex) {
// console.log(activeIndex, overIndex, "283 3")
// newActiveRecRo = overRecRo > 1 ? overRecRo : 1
// newOverRecRo = activeRecRo > 1 ? activeRecRo : 2
// }
if (activeRecRo > overRecRo && activeIndex > overIndex) {
console.log(activeIndex, overIndex, "283 1")
newActiveRecRo = activeRecRo > 1 ? overRecRo : 1
newOverRecRo = overRecRo > 1 ? activeRecRo : 2
}
if (activeRecRo > overRecRo && activeIndex < overIndex) {
console.log(activeIndex, overIndex, "283 kam")
newActiveRecRo = activeRecRo > 1 ? activeRecRo : 2
newOverRecRo = overRecRo > 1 ? overRecRo : 1
}
if (activeRecRo < overRecRo && activeIndex < overIndex) {
console.log(activeIndex, overIndex, "283 4")
newActiveRecRo = overRecRo > 1 ? overRecRo : 2
newOverRecRo = activeRecRo > 1 ? activeRecRo : 1
}
if (activeRecRo < overRecRo && activeIndex > overIndex) {
console.log(activeIndex, overIndex, "283 mak")
newActiveRecRo = activeRecRo > 1 ? activeRecRo : 1
newOverRecRo = overRecRo > 1 ? overRecRo : 1
}
if (activeRecRo == activeRecRo && activeIndex < overIndex) {
newActiveRecRo = activeRecRo > 1 ? activeRecRo + 1 : 2
newOverRecRo = overRecRo > 1 ? overRecRo : 1
}
if (activeRecRo == overRecRo && activeIndex > overIndex) {
newActiveRecRo = activeRecRo > 1 ? activeRecRo : 1
newOverRecRo = overRecRo > 1 ? overRecRo + 1 : 2
}
// let reordered = recipients.map((r, idx) => {
// if (idx == activeIndex) {
// return (
// {
// ...r, routingOrder: newActiveRecRo
// }
// )
// }
// if (idx == overRecRo) {
// return (
// {
// ...r, routingOrder: newOverRecRo
// }
// )
// }
// });
// dispatch(updateRecipientOrder({ id: activeRec.id || activeRec.uuid, routingOrder: newActiveRecRo }));
// dispatch(updateRecipientOrder({ id: overRec.id || overRec.uuid, routingOrder: newOverRecRo }));
dispatch(updateRecipientByIndxAndRo({ indx1: activeIndex, ro1: newActiveRecRo, indx2: overIndex, ro2: newOverRecRo, }));
// dispatch(reorderAllRecipientAscOrder());
};
return (
<div className="w-full rounded-md">
{recipients.length > 0 ? (
<>
{/* {recipients.length > 1 && (
<div className="flex sm:flex-row sm:items-center justify-between px-3 sm:px-4 py-2 border-b border-gray-300 gap-2">
<h3 className="text-sm font-semibold text-gray-700">
Recipients ({recipients.length})
</h3>
<label
className="flex items-center gap-2 cursor-pointer select-none justify-center sm:justify-end"
onClick={() => setSigningOrderModal({ ...signingOrderModal, open: true })}
>
<span className="text-xs font-medium text-gray-600">Signing Order</span>
<FaUsersLine className="text-[var(--primary)] mt-0.5 text-lg" />
</label>
</div>
)} */}
<DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext
items={recipients.map((r) => r.id || r.uuid)}
strategy={verticalListSortingStrategy}
>
<div className="flex flex-col max-h-[47vh] overflow-y-auto custom-scrollbar">
{recipients.map((recipient, idx) => (
<SortableRecipient
key={recipient.id || recipient.uuid}
recipient={recipient}
idx={idx}
editRecipientCurrentEmail={editRecipientCurrentEmail}
>
<div
key={recipient.email + idx}
className={`relative flex flex-col lg:flex-row lg:items-center justify-between transition-colors gap-3 lg:gap-0 ${(recipient.uuid || recipient.id) == editRecipientCurrentEmail
? "bg-neutral-100 opacity-40"
: ""
}`}
>
<>
{/* Main Content Section */}
<div className="flex items-start sm:items-center gap-2 sm:gap-3 min-w-0 flex-1">
{/* Order Number */}
<div className="">
{recipients.length > 1 && signingOrder ? (
<div className="relative flex-shrink-0">
<input
type="text"
value={recipient.routingOrder == 0 ? "" : recipient.routingOrder}
onChange={(e) => {
const value = e.target.value;
if (/^\d*$/.test(value)) {
const num = Number(value);
handleOrderChange(recipient.id || recipient.uuid, num);
}
}}
onKeyDown={(e) => {
if (!/[0-9]/.test(e.key) && e.key != "Backspace" && e.key != "Tab") {
e.preventDefault();
}
}}
className="w-8 sm:w-10 h-6 text-center rounded text-sm text-gray-600 focus:outline-none focus:ring-[var(--primary)] border border-dashed border-gray-400"
/>
<>
{idx == 0 && recipients.length > 1 && <p className="group">
<>
{/* <BiSolidHelpCircle className="absolute top-[-12px] right-[-10px] text-gray-400 cursor-pointer" />
<span className="absolute top-[2px] left-[48px] hidden group-hover:block w-56 p-2 rounded-lg text-sm bg-white text-slate-600 shadow-lg z-10">
Signing order: Recipients receive emails one after another.
Same order means they all receive at once.
</span> */}
</>
</p>}
</>
</div>
) : (
<>{signingOrder &&
<input
type="number"
disabled
value={recipient.routingOrder}
min={1}
onChange={(e) =>
handleOrderChange(
recipient.id || recipient.uuid,
Number(e.target.value)
)
}
className="w-6 sm:w-8 text-center rounded text-sm text-gray-700 border-none focus:outline-none focus:ring-[var(--primary)] flex-shrink-0 border-2 border-slate-200"
/>}
</>
)}
</div>
{/* Avatar */}
{/* <div className="flex-shrink-0 w-7 h-7 sm:w-8 sm:h-8 rounded-full bg-[var(--primary)] text-white flex items-center justify-center font-semibold text-xs select-none">
{getInitials(`${recipient.firstName} ${recipient.lastName || ""}`)}
</div> */}
{/* Form Fields Section */}
<div className={`min-w-0 py-4 ${signingOrder ? "pl-8" : "pl-2"} flex-1 border-l-8 ${idx % 2 == 0 ? 'border-[#A5EDFC]' : 'border-[#84EFC3]'}`}>
{/* Role Name and Permissions Row */}
<div className="flex flex-col lg:flex-row lg:items-center gap-2 mb-2">
{<div className="flex lg:w-1/2">
{transactionTemplateToggle == TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES ?
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "roleName", "Role name")}
</div>
:
<div className="font-semibold text-gray-800 text-sm px-2">
{recipient.roleName}
</div>
}
</div>}
{/* Permissions - Mobile: Below role, Desktop: Right side */}
<div className="lg:w-1/2 lg:flex lg:items-center lg:justify-end">
{transactionTemplateToggle == TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && (
<div className="flex ps-2.5 sm:flex-row items-start sm:items-center gap-2 sm:gap-4">
<label className="flex items-center gap-1 text-xs sm:text-sm text-gray-700">
<input
type="checkbox"
checked={recipient.allowSenderToEdit}
onChange={() =>
dispatch(
updateRecipient({
...recipient,
allowSenderToEdit: !recipient.allowSenderToEdit,
uuid: getRecipientIdentifier(recipient),
})
)
}
className="accent-blue-500 w-3 h-3 sm:w-4 sm:h-4"
/>
<span className="whitespace-nowrap">Allow Edit</span>
</label>
<label className="flex items-center gap-1 text-xs sm:text-sm text-gray-700">
<input
type="checkbox"
checked={recipient.allowSenderToDelete}
onChange={() =>
dispatch(
updateRecipient({
...recipient,
allowSenderToDelete: !recipient.allowSenderToDelete,
uuid: getRecipientIdentifier(recipient),
})
)
}
className="accent-red-500 w-3 h-3 sm:w-4 sm:h-4"
/>
<span className="whitespace-nowrap">Allow Delete</span>
</label>
</div>
)}
</div>
</div>
{/* Name and Email Fields */}
<div className="mb-2 w-[100%]">
{/* Mobile: Stack vertically, Tablet+: Side by side */}
<div className="flex flex-col justify-center items-center sm:flex-row gap-1 sm:gap-2">
<div className="flex flex-col sm:flex-row gap-1 sm:gap-2 sm:flex-1">
<div className="w-full sm:w-1/2">
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "firstName", "First Name")}
</div>
</div>
<div className="w-full sm:w-1/2">
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "lastName", "Last Name")}
</div>
</div>
</div>
<div className="w-full sm:flex-1">
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "email", "Recipient Email", "", "email")}
</div>
</div>
{/* Action Buttons - START*/}
{signType != "self-sign" && (
<div className="ml-4">
<button
type="button"
title="Remove recipient"
aria-label={`Remove recipient ${recipient.firstName}`}
onClick={() => handleRemoveRecipient(recipient)}
disabled={
transactionTemplateToggle == TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES
? false
: !recipient.newOne && !recipient.allowSenderToDelete
}
className="text-gray-400 hover:text-red-600 transition-colors cursor-pointer disabled:opacity-50 p-1"
>
<ImBin size={14} className="sm:w-4 sm:h-4" />
</button>
</div>
)}
{/* Action Buttons - END*/}
</div>
</div>
{/* Host Fields (for In-Person Signers) */}
{recipient.type == recipientType.INPERSONSIGNER && (
<div className="mb-2">
<div className="flex flex-col sm:flex-row gap-1 sm:gap-2">
<div className="w-full sm:w-1/2">
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "hostName", "Host Name")}
</div>
</div>
<div className="w-full sm:w-1/2">
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "hostEmail", "Host Email", "", "email")}
</div>
</div>
</div>
</div>
)}
{/* Recipient Type Radio Buttons */}
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2 my-1.5 mx-1 sm:mx-2">
<label className="flex items-center gap-1">
<input
type="radio"
name={`recipient-type-${getRecipientIdentifier(recipient)}`}
value={recipientType.SIGNER}
checked={recipient.type == recipientType.SIGNER}
onChange={() => handleTypeChange(recipient, recipientType.SIGNER)}
onBlur={() => setEditingField(null)}
className="accent-[var(--primary)] w-3 h-3"
disabled={!isEditable(recipient)}
/>
<span className="text-xs sm:text-sm">Signer</span>
</label>
<label className="flex items-center gap-1">
<input
type="radio"
name={`recipient-type-${getRecipientIdentifier(recipient)}`}
value={recipientType.INPERSONSIGNER}
checked={recipient.type == recipientType.INPERSONSIGNER}
onChange={() => handleTypeChange(recipient, recipientType.INPERSONSIGNER)}
onBlur={() => setEditingField(null)}
className="accent-[var(--primary)] w-3 h-3"
disabled={!isEditable(recipient)}
/>
<span className="text-xs sm:text-sm whitespace-nowrap">In Person Signer</span>
</label>
<label className="flex items-center gap-1">
<input
type="radio"
name={`recipient-type-${getRecipientIdentifier(recipient)}`}
value={recipientType.CC}
checked={recipient.type == recipientType.CC}
onChange={() => handleTypeChange(recipient, recipientType.CC)}
onBlur={() => setEditingField(null)}
className="accent-[var(--primary)] w-3 h-3"
disabled={!isEditable(recipient)}
/>
<span className="text-xs sm:text-sm">CC</span>
</label>
</div>
</div>
</div>
</>
</div>
</SortableRecipient>
))}
</div>
</SortableContext>
</ DndContext>
</>
) : (
<div className="p-4 sm:p-6 text-center text-gray-500 font-medium text-sm">
No Recipients Added
</div>
)}
{/* Responsive Modal */}
{signingOrderModal.open && (
<div className="fixed top-0 left-0 inset-0 z-50 bg-black/50 backdrop-blur-sm flex justify-center items-center p-4">
<div className="flex-col relative justify-center items-center w-full max-w-xs sm:max-w-md md:max-w-lg lg:max-w-2xl xl:max-w-4xl max-h-[80vh] sm:max-h-[70vh] lg:max-h-[60vh] h-full overflow-auto">
<div className="p-3 sm:p-4 rounded-t-lg text-white flex justify-between items-center bg-[var(--primary)]">
<p className="font-semibold text-sm sm:text-base">Signing Order</p>
<div
onClick={() => setSigningOrderModal({ ...signingOrderModal, open: false })}
className="text-xl sm:text-2xl cursor-pointer hover:text-gray-200 transition-all p-1"
>
<IoCloseOutline />
</div>
</div>
<div className="bg-white rounded-b-lg">
<SigningOrderDiagram signers={recipients} />
</div>
</div>
</div>
)}
</div>
);
};
export default RecipientCards;
// <DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
// <SortableContext
// items={recipients.map((r) => r.id || r.uuid)}
// strategy={verticalListSortingStrategy}
// >
// <div className="flex flex-col divide-y divide-gray-200 max-h-[47vh] overflow-y-auto custom-scrollbar">
// {recipients.map((recipient, idx) => (
// <SortableRecipient
// key={recipient.id || recipient.uuid}
// recipient={recipient}
// idx={idx}
// editRecipientCurrentEmail={editRecipientCurrentEmail}
// >
// {/* 🔹 your existing recipient card content goes here */}
// </SortableRecipient>
// ))}
// </div>
// </SortableContext>
// </DndContext>