478 lines
27 KiB
TypeScript
478 lines
27 KiB
TypeScript
import { useDispatch, useSelector } from "react-redux";
|
|
import {
|
|
removeRecipientAsync,
|
|
removeSelectedRecipient,
|
|
updateRecipientOrder,
|
|
TRANSACTION_TEMPLATE_TOGGLE,
|
|
updateRecipient,
|
|
recipientType,
|
|
} 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 { FiEdit } from "react-icons/fi";
|
|
import { FaUsersLine } from "react-icons/fa6";
|
|
|
|
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>();
|
|
|
|
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)}
|
|
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);
|
|
};
|
|
|
|
return (
|
|
<div className="w-full bg-white rounded-md shadow-sm border border-gray-200">
|
|
{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>
|
|
)}
|
|
|
|
<div className="flex flex-col divide-y divide-gray-200 max-h-[47vh] overflow-y-auto custom-scrollbar">
|
|
{recipients.map((recipient, idx) => (
|
|
<div
|
|
key={recipient.email + idx}
|
|
className={`flex flex-col lg:flex-row lg:items-center justify-between px-2 sm:px-3 lg:px-2 py-3 hover:bg-gray-50 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 */}
|
|
{recipients.length > 1 ? (
|
|
<div className="relative flex-shrink-0">
|
|
<input
|
|
type="number"
|
|
value={recipient.routingOrder || 1}
|
|
min={1}
|
|
onChange={(e) =>
|
|
handleOrderChange(
|
|
recipient.id || recipient.email,
|
|
Number(e.target.value)
|
|
)
|
|
}
|
|
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 bg-slate-100"
|
|
/>
|
|
</div>
|
|
) : (
|
|
<input
|
|
type="number"
|
|
disabled
|
|
value={recipient.routingOrder}
|
|
min={1}
|
|
onChange={(e) =>
|
|
handleOrderChange(
|
|
recipient.id || recipient.email,
|
|
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"
|
|
/>
|
|
)}
|
|
|
|
{/* 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 flex-1">
|
|
{/* Role Name and Permissions Row */}
|
|
<div className="flex flex-col lg:flex-row lg:items-center gap-2 mb-2">
|
|
<div className="flex-1 lg:w-1/2">
|
|
<p className="font-semibold text-gray-800 text-xs">
|
|
{renderEditableField(recipient, "roleName", "Role name")}
|
|
</p>
|
|
</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">
|
|
{/* Mobile: Stack vertically, Tablet+: Side by side */}
|
|
<div className="flex flex-col 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">
|
|
<p className="font-semibold text-gray-800 text-xs">
|
|
{renderEditableField(recipient, "firstName", "First Name")}
|
|
</p>
|
|
</div>
|
|
<div className="w-full sm:w-1/2">
|
|
<p className="font-semibold text-gray-800 text-xs">
|
|
{renderEditableField(recipient, "lastName", "Last Name")}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="w-full sm:flex-1">
|
|
<p className="font-semibold text-gray-800 text-xs">
|
|
{renderEditableField(recipient, "email", "Recipient Email", "", "email")}
|
|
</p>
|
|
</div>
|
|
</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">
|
|
<p className="font-semibold text-gray-800 text-xs">
|
|
{renderEditableField(recipient, "hostName", "Host Name")}
|
|
</p>
|
|
</div>
|
|
<div className="w-full sm:w-1/2">
|
|
<p className="font-semibold text-gray-800 text-xs">
|
|
{renderEditableField(recipient, "hostEmail", "Host Email", "", "email")}
|
|
</p>
|
|
</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>
|
|
|
|
{/* Action Buttons */}
|
|
{signType !== "self-sign" && (
|
|
<div className="flex justify-end lg:justify-center gap-2 items-center lg:ml-2">
|
|
<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>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</>
|
|
) : (
|
|
<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; |