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

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;