jai #1
|
|
@ -1,63 +1,119 @@
|
||||||
import React, { useRef, useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
|
import { FaSpinner } from 'react-icons/fa';
|
||||||
|
|
||||||
export type ActionType = 'create' | 'invite';
|
export type ActionType = 'create' | 'invite';
|
||||||
|
|
||||||
type CreateData = { accountName: string };
|
type CreateData = { accountName: string };
|
||||||
type InviteData = { i_accountName: string; email: string; role: string };
|
type InviteData = { i_accountName: number; email: string; role: string };
|
||||||
|
|
||||||
type FormDataType = CreateData | InviteData;
|
export type FormDataType = CreateData | InviteData;
|
||||||
|
|
||||||
|
interface AccountOption {
|
||||||
|
userId: number;
|
||||||
|
userEmail: string;
|
||||||
|
accountId: number;
|
||||||
|
accountName: string;
|
||||||
|
role: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onSubmit: (data: FormDataType, action: ActionType) => void;
|
onSubmit: (data: FormDataType, action: ActionType) => void;
|
||||||
|
createAndInviteLoader: boolean;
|
||||||
|
accountOptions: AccountOption[];
|
||||||
|
accountOptionsLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CreateAndInvite = ({ onSubmit }: Props) => {
|
const CreateAndInvite = ({
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
onSubmit,
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
createAndInviteLoader,
|
||||||
|
accountOptions,
|
||||||
|
accountOptionsLoading
|
||||||
|
}: Props) => {
|
||||||
|
const [isRoleDropdownOpen, setIsRoleDropdownOpen] = useState(false);
|
||||||
|
const [isAccountDropdownOpen, setIsAccountDropdownOpen] = useState(false);
|
||||||
|
const roleDropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
const accountDropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const [createFormData, setCreateFormData] = useState<{ accountName: string }>({
|
const [createFormData, setCreateFormData] = useState<{ accountName: string }>({
|
||||||
accountName: '',
|
accountName: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [accountSearchTerm, setAccountSearchTerm] = useState('');
|
||||||
|
const [selectedAccount, setSelectedAccount] = useState<AccountOption | null>(null);
|
||||||
|
|
||||||
const [inviteFormData, setInviteFormData] = useState<{
|
const [inviteFormData, setInviteFormData] = useState<{
|
||||||
i_accountName: string;
|
i_accountName: number;
|
||||||
email: string;
|
email: string;
|
||||||
role: string;
|
role: string;
|
||||||
}>({
|
}>({
|
||||||
i_accountName: '',
|
i_accountName: 0,
|
||||||
email: '',
|
email: '',
|
||||||
role: '',
|
role: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
||||||
|
const roles = ['ADMIN', 'EDITOR', 'VIEWER'];
|
||||||
|
|
||||||
const roles = ['Owner', 'Client'];
|
// Handle role dropdown
|
||||||
|
const handleRoleDropdownFocus = () => setIsRoleDropdownOpen(true);
|
||||||
const handleFocus = () => setIsOpen(true);
|
|
||||||
|
const handleRoleDropdownBlur = (e: React.FocusEvent<HTMLDivElement>) => {
|
||||||
const handleBlur = (e: React.FocusEvent<HTMLDivElement>) => {
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const activeElement = document.activeElement as HTMLElement;
|
const activeElement = document.activeElement as HTMLElement;
|
||||||
if (dropdownRef.current && !dropdownRef.current.contains(activeElement)) {
|
if (roleDropdownRef.current && !roleDropdownRef.current.contains(activeElement)) {
|
||||||
setIsOpen(false);
|
setIsRoleDropdownOpen(false);
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelect = (role: string) => {
|
const handleRoleSelect = (role: string) => {
|
||||||
setInviteFormData((prev) => ({ ...prev, role }));
|
setInviteFormData((prev) => ({ ...prev, role }));
|
||||||
setIsOpen(false);
|
setIsRoleDropdownOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle account dropdown
|
||||||
|
const handleAccountDropdownFocus = () => setIsAccountDropdownOpen(true);
|
||||||
|
|
||||||
|
const handleAccountDropdownBlur = (e: React.FocusEvent<HTMLDivElement>) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
const activeElement = document.activeElement as HTMLElement;
|
||||||
|
if (accountDropdownRef.current && !accountDropdownRef.current.contains(activeElement)) {
|
||||||
|
setIsAccountDropdownOpen(false);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAccountSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setAccountSearchTerm(value);
|
||||||
|
|
||||||
|
// Clear selected account if user is typing
|
||||||
|
if (selectedAccount && value !== selectedAccount.accountName) {
|
||||||
|
setSelectedAccount(null);
|
||||||
|
setInviteFormData((prev) => ({ ...prev, i_accountName: 0 }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAccountSelect = (option: AccountOption) => {
|
||||||
|
setSelectedAccount(option);
|
||||||
|
setAccountSearchTerm(option.accountName);
|
||||||
|
setInviteFormData((prev) => ({ ...prev, i_accountName: option.accountId }));
|
||||||
|
setIsAccountDropdownOpen(false);
|
||||||
|
|
||||||
|
// Clear any existing account name error
|
||||||
|
if (errors.i_accountName) {
|
||||||
|
setErrors(prev => ({ ...prev, i_accountName: '' }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle create form
|
||||||
const handleCreateChange = (value: string) => {
|
const handleCreateChange = (value: string) => {
|
||||||
setCreateFormData({ accountName: value });
|
setCreateFormData({ accountName: value });
|
||||||
};
|
// Clear error when user starts typing
|
||||||
|
if (errors.accountName) {
|
||||||
const handleInviteChange = (
|
setErrors(prev => ({ ...prev, accountName: '' }));
|
||||||
field: keyof typeof inviteFormData,
|
}
|
||||||
value: string
|
|
||||||
) => {
|
|
||||||
setInviteFormData((prev) => ({ ...prev, [field]: value }));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateSubmit = () => {
|
const handleCreateSubmit = () => {
|
||||||
|
|
@ -69,11 +125,20 @@ const CreateAndInvite = ({ onSubmit }: Props) => {
|
||||||
onSubmit(createFormData, 'create');
|
onSubmit(createFormData, 'create');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle invite form
|
||||||
|
const handleInviteChange = (field: keyof typeof inviteFormData, value: string) => {
|
||||||
|
setInviteFormData((prev) => ({ ...prev, [field]: value }));
|
||||||
|
// Clear error when user starts typing
|
||||||
|
if (errors[field]) {
|
||||||
|
setErrors(prev => ({ ...prev, [field]: '' }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleInviteSubmit = () => {
|
const handleInviteSubmit = () => {
|
||||||
const { i_accountName, email, role } = inviteFormData;
|
const { i_accountName, email, role } = inviteFormData;
|
||||||
const errs: { [key: string]: string } = {};
|
const errs: { [key: string]: string } = {};
|
||||||
|
|
||||||
if (!i_accountName.trim()) errs.i_accountName = 'Account name is required';
|
if (!i_accountName) errs.i_accountName = 'Account name is required';
|
||||||
if (!email.trim()) errs.email = 'Email is required';
|
if (!email.trim()) errs.email = 'Email is required';
|
||||||
else if (!/\S+@\S+\.\S+/.test(email)) errs.email = 'Invalid email';
|
else if (!/\S+@\S+\.\S+/.test(email)) errs.email = 'Invalid email';
|
||||||
if (!role.trim()) errs.role = 'Role is required';
|
if (!role.trim()) errs.role = 'Role is required';
|
||||||
|
|
@ -87,9 +152,14 @@ const CreateAndInvite = ({ onSubmit }: Props) => {
|
||||||
onSubmit(inviteFormData, 'invite');
|
onSubmit(inviteFormData, 'invite');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Filter account options based on search term
|
||||||
|
const filteredAccountOptions = accountOptions.filter((option) =>
|
||||||
|
option.accountName?.toLowerCase().includes(accountSearchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-3 space-y-3">
|
<div className="ms-2 space-y-3">
|
||||||
{/* Create */}
|
{/* Create Section */}
|
||||||
<section className="border-gray-200 border-b">
|
<section className="border-gray-200 border-b">
|
||||||
<h2 className="text-lg font-medium mb-2 text-slate-500">Create Account</h2>
|
<h2 className="text-lg font-medium mb-2 text-slate-500">Create Account</h2>
|
||||||
<div className="flex flex-col mb-6">
|
<div className="flex flex-col mb-6">
|
||||||
|
|
@ -107,9 +177,17 @@ const CreateAndInvite = ({ onSubmit }: Props) => {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleCreateSubmit}
|
onClick={handleCreateSubmit}
|
||||||
className="ms-1 text-[14px] px-3 py-[1.1] bg-gray-300 text-slate-600 font-medium rounded-md border border-gray-300 hover:bg-opacity-90 transition cursor-pointer"
|
className="ms-1 flex items-center text-[14px] px-3 py-[1.1] bg-gray-300 text-slate-600 font-medium rounded-md border border-gray-300 hover:bg-opacity-90 transition cursor-pointer disabled:opacity-60 disabled:cursor-not-allowed"
|
||||||
|
disabled={createAndInviteLoader}
|
||||||
>
|
>
|
||||||
Create
|
{createAndInviteLoader ? (
|
||||||
|
<>
|
||||||
|
<FaSpinner className="animate-spin me-1" />
|
||||||
|
CREATING...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'CREATE'
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{errors.accountName && (
|
{errors.accountName && (
|
||||||
|
|
@ -118,22 +196,62 @@ const CreateAndInvite = ({ onSubmit }: Props) => {
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Invite */}
|
{/* Invite Section */}
|
||||||
<section className="border-gray-300">
|
<section className="border-gray-300">
|
||||||
<h2 className="text-lg font-medium mb-2 text-slate-500">Invite</h2>
|
<h2 className="text-lg font-medium mb-2 text-slate-500">Invite</h2>
|
||||||
<div className="grid grid-cols-2 gap-4 mb-6">
|
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||||
{/* Account Name */}
|
{/* Account Name Dropdown */}
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col relative">
|
||||||
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">
|
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">
|
||||||
Account Name
|
Account Name
|
||||||
</label>
|
</label>
|
||||||
<input
|
<div
|
||||||
type="text"
|
className="relative w-full"
|
||||||
placeholder="Enter account name"
|
onBlur={handleAccountDropdownBlur}
|
||||||
value={inviteFormData.i_accountName}
|
ref={accountDropdownRef}
|
||||||
onChange={(e) => handleInviteChange('i_accountName', e.target.value)}
|
tabIndex={-1}
|
||||||
className="w-full px-3 py-1.5 text-gray-600 rounded-md border border-gray-400 focus:outline-none focus:ring-1 focus:ring-[var(--primary)]"
|
>
|
||||||
/>
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={accountOptionsLoading ? "Loading accounts..." : "Select or search account"}
|
||||||
|
value={accountSearchTerm}
|
||||||
|
onChange={handleAccountSearchChange}
|
||||||
|
onFocus={handleAccountDropdownFocus}
|
||||||
|
disabled={accountOptionsLoading}
|
||||||
|
className="w-full px-3 py-1.5 text-gray-600 rounded-md border border-gray-400 focus:outline-none focus:ring-1 focus:ring-[var(--primary)] disabled:opacity-60 disabled:cursor-not-allowed"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Loading indicator */}
|
||||||
|
{accountOptionsLoading && (
|
||||||
|
<div className="absolute right-3 top-1/2 transform -translate-y-1/2">
|
||||||
|
<FaSpinner className="animate-spin text-gray-400 text-sm" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Account Dropdown */}
|
||||||
|
{isAccountDropdownOpen && !accountOptionsLoading && (
|
||||||
|
<div className="absolute w-full bg-white border border-gray-300 rounded-md mt-1 shadow-lg z-10">
|
||||||
|
<ul className="max-h-40 overflow-y-auto">
|
||||||
|
{filteredAccountOptions.length > 0 ? (
|
||||||
|
filteredAccountOptions.map((option) => (
|
||||||
|
<li
|
||||||
|
key={option.accountId}
|
||||||
|
onMouseDown={() => handleAccountSelect(option)}
|
||||||
|
className="px-3 py-2 text-[13px] text-gray-600 hover:bg-gray-100 cursor-pointer"
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span>{option.accountName}</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<li className="px-3 py-2 text-gray-500">No results found</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{errors.i_accountName && (
|
{errors.i_accountName && (
|
||||||
<span className="text-sm text-red-500 mt-1">{errors.i_accountName}</span>
|
<span className="text-sm text-red-500 mt-1">{errors.i_accountName}</span>
|
||||||
)}
|
)}
|
||||||
|
|
@ -163,8 +281,8 @@ const CreateAndInvite = ({ onSubmit }: Props) => {
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
className="relative w-full"
|
className="relative w-full"
|
||||||
onBlur={handleBlur}
|
onBlur={handleRoleDropdownBlur}
|
||||||
ref={dropdownRef}
|
ref={roleDropdownRef}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
|
@ -172,30 +290,30 @@ const CreateAndInvite = ({ onSubmit }: Props) => {
|
||||||
placeholder="Select role"
|
placeholder="Select role"
|
||||||
value={inviteFormData.role}
|
value={inviteFormData.role}
|
||||||
readOnly
|
readOnly
|
||||||
onFocus={handleFocus}
|
onFocus={handleRoleDropdownFocus}
|
||||||
className="w-full px-3 py-1.5 border text-gray-600 border-gray-400 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-[var(--primary)] transition"
|
className="w-full px-3 py-1.5 border text-gray-600 border-gray-400 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-[var(--primary)] transition cursor-pointer"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={`absolute z-10 w-full bg-white text-slate-500 font-semibold border border-gray-200 rounded-md shadow-lg mt-1 transition-all duration-200 ease-in-out ${
|
className={`absolute z-10 w-full bg-white text-slate-500 font-semibold border border-gray-200 rounded-md shadow-lg mt-1 transition-all duration-200 ease-in-out ${
|
||||||
isOpen
|
isRoleDropdownOpen
|
||||||
? 'opacity-100 scale-100'
|
? 'opacity-100 scale-100'
|
||||||
: 'opacity-0 scale-95 pointer-events-none'
|
: 'opacity-0 scale-95 pointer-events-none'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{roles.map((r) => (
|
{roles.map((role) => (
|
||||||
<div
|
<div
|
||||||
key={r}
|
key={role}
|
||||||
onMouseDown={() => handleSelect(r)}
|
onMouseDown={() => handleRoleSelect(role)}
|
||||||
className="px-4 py-2 text-[13px] hover:bg-gray-100 cursor-pointer"
|
className="px-4 py-1 text-[13px] hover:bg-gray-100 cursor-pointer"
|
||||||
>
|
>
|
||||||
{r}
|
{role}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{errors.role && (
|
|
||||||
<span className="text-sm text-red-500 mt-1">{errors.role}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
{errors.role && (
|
||||||
|
<span className="text-sm text-red-500 mt-1">{errors.role}</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -203,9 +321,17 @@ const CreateAndInvite = ({ onSubmit }: Props) => {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleInviteSubmit}
|
onClick={handleInviteSubmit}
|
||||||
className="px-4 py-2 text-sm font-medium rounded-md cursor-pointer hover:bg-opacity-90 transition text-[14px] bg-gray-300 text-slate-600"
|
className="px-4 py-2 text-sm font-medium rounded-md cursor-pointer hover:bg-opacity-90 transition text-[14px] bg-gray-300 text-slate-600 disabled:opacity-60 disabled:cursor-not-allowed"
|
||||||
|
disabled={createAndInviteLoader}
|
||||||
>
|
>
|
||||||
Invite
|
{createAndInviteLoader ? (
|
||||||
|
<>
|
||||||
|
<FaSpinner className="animate-spin me-1" />
|
||||||
|
INVITING...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'INVITE'
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
@ -213,4 +339,4 @@ const CreateAndInvite = ({ onSubmit }: Props) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CreateAndInvite;
|
export default CreateAndInvite;
|
||||||
|
|
@ -7,6 +7,7 @@ import Language from "./Language";
|
||||||
import CreateAndInvite, { ActionType, FormDataType } from './CreateAndInvite'
|
import CreateAndInvite, { ActionType, FormDataType } from './CreateAndInvite'
|
||||||
import AsideSection from "./AsideSection";
|
import AsideSection from "./AsideSection";
|
||||||
import Signature from "./Signature/Signature";
|
import Signature from "./Signature/Signature";
|
||||||
|
import { Bounce, toast } from "react-toastify";
|
||||||
|
|
||||||
// Define the shape of state object
|
// Define the shape of state object
|
||||||
interface Account {
|
interface Account {
|
||||||
|
|
@ -30,11 +31,20 @@ interface SubSection {
|
||||||
component?: React.ReactNode;
|
component?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AccountOption {
|
||||||
|
userId: number;
|
||||||
|
userEmail: string;
|
||||||
|
accountId: number;
|
||||||
|
accountName: string;
|
||||||
|
role: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
const Setting = () => {
|
const Setting = () => {
|
||||||
const { page1 } = useParams<{ page1: string }>();
|
const { page1 } = useParams<{ page1: string }>();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// Refs for subsections within each main tab
|
// Refs for subsections within each main tab
|
||||||
const subSectionRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
|
const subSectionRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
@ -54,6 +64,12 @@ const Setting = () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [createAndInviteLoader, setCreateAndInviteLoader] = useState(false);
|
||||||
|
|
||||||
|
// Dropdown data states
|
||||||
|
const [accountOptions, setAccountOptions] = useState<AccountOption[]>([]);
|
||||||
|
const [accountOptionsLoading, setAccountOptionsLoading] = useState(false);
|
||||||
|
|
||||||
// Define subsections for each main tab
|
// Define subsections for each main tab
|
||||||
const getSubSections = (tabId: string): SubSection[] => {
|
const getSubSections = (tabId: string): SubSection[] => {
|
||||||
switch (tabId) {
|
switch (tabId) {
|
||||||
|
|
@ -62,16 +78,6 @@ const Setting = () => {
|
||||||
{ id: "profile-info", label: "Profile Information" },
|
{ id: "profile-info", label: "Profile Information" },
|
||||||
{ id: "signature", label: "Signature" },
|
{ id: "signature", label: "Signature" },
|
||||||
];
|
];
|
||||||
// case "create and invite":
|
|
||||||
// return [
|
|
||||||
// { id: "create-account", label: "Create Account" },
|
|
||||||
// { id: "invite-users", label: "Invite Users" },
|
|
||||||
// { id: "manage-invitations", label: "Manage Invitations" }
|
|
||||||
// ];
|
|
||||||
// return [
|
|
||||||
// { id: "share-options", label: "Share Options" },
|
|
||||||
// { id: "privacy-settings", label: "Privacy Settings" }
|
|
||||||
// ];
|
|
||||||
default:
|
default:
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
@ -80,18 +86,18 @@ const Setting = () => {
|
||||||
// Function to scroll to specific subsection
|
// Function to scroll to specific subsection
|
||||||
const scrollToSubSection = (subSectionId: string) => {
|
const scrollToSubSection = (subSectionId: string) => {
|
||||||
const targetRef = subSectionRefs.current[subSectionId];
|
const targetRef = subSectionRefs.current[subSectionId];
|
||||||
|
|
||||||
if (targetRef && scrollContainerRef?.current) {
|
if (targetRef && scrollContainerRef?.current) {
|
||||||
const container = scrollContainerRef.current;
|
const container = scrollContainerRef.current;
|
||||||
const target = targetRef;
|
const target = targetRef;
|
||||||
|
|
||||||
// Calculate the position relative to the scroll container
|
// Calculate the position relative to the scroll container
|
||||||
const containerRect = container.getBoundingClientRect();
|
const containerRect = container.getBoundingClientRect();
|
||||||
const targetRect = target.getBoundingClientRect();
|
const targetRect = target.getBoundingClientRect();
|
||||||
|
|
||||||
// Calculate scroll position (accounting for some padding)
|
// Calculate scroll position (accounting for some padding)
|
||||||
const scrollTop = container.scrollTop + targetRect.top - containerRect.top - 20;
|
const scrollTop = container.scrollTop + targetRect.top - containerRect.top - 20;
|
||||||
|
|
||||||
// Smooth scroll to the target
|
// Smooth scroll to the target
|
||||||
container.scrollTo({
|
container.scrollTo({
|
||||||
top: scrollTop,
|
top: scrollTop,
|
||||||
|
|
@ -132,7 +138,7 @@ const Setting = () => {
|
||||||
// Check if the hash corresponds to a valid subsection
|
// Check if the hash corresponds to a valid subsection
|
||||||
const allSubSections = getSubSections(activeTab);
|
const allSubSections = getSubSections(activeTab);
|
||||||
const validSubSection = allSubSections.find(section => section.id === hash);
|
const validSubSection = allSubSections.find(section => section.id === hash);
|
||||||
|
|
||||||
if (validSubSection) {
|
if (validSubSection) {
|
||||||
setActiveSubSection(hash);
|
setActiveSubSection(hash);
|
||||||
// Delay scroll to ensure component is rendered
|
// Delay scroll to ensure component is rendered
|
||||||
|
|
@ -143,6 +149,21 @@ const Setting = () => {
|
||||||
}
|
}
|
||||||
}, [location.hash, activeTab]);
|
}, [location.hash, activeTab]);
|
||||||
|
|
||||||
|
// Fetch account list for dropdown
|
||||||
|
const getAccountList = async () => {
|
||||||
|
setAccountOptionsLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await api.get('/api/user/accounts');
|
||||||
|
if (res?.data) {
|
||||||
|
setAccountOptions(res.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching account options:', error);
|
||||||
|
} finally {
|
||||||
|
setAccountOptionsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Render subsection content based on active tab and subsection
|
// Render subsection content based on active tab and subsection
|
||||||
const renderSubSectionContent = (subSectionId: string) => {
|
const renderSubSectionContent = (subSectionId: string) => {
|
||||||
switch (activeTab) {
|
switch (activeTab) {
|
||||||
|
|
@ -152,7 +173,7 @@ const Setting = () => {
|
||||||
return <UserProfile userData={user} />;
|
return <UserProfile userData={user} />;
|
||||||
case "signature":
|
case "signature":
|
||||||
return (
|
return (
|
||||||
<Signature/>
|
<Signature />
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return;
|
return;
|
||||||
|
|
@ -169,7 +190,7 @@ const Setting = () => {
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
const currentSubSections = getSubSections(activeTab);
|
const currentSubSections = getSubSections(activeTab);
|
||||||
|
|
||||||
if (currentSubSections.length === 0) {
|
if (currentSubSections.length === 0) {
|
||||||
// If no subsections, render the main content directly
|
// If no subsections, render the main content directly
|
||||||
switch (activeTab) {
|
switch (activeTab) {
|
||||||
|
|
@ -178,7 +199,14 @@ const Setting = () => {
|
||||||
case "language":
|
case "language":
|
||||||
return <Language userData={user} />;
|
return <Language userData={user} />;
|
||||||
case "create and invite":
|
case "create and invite":
|
||||||
return <CreateAndInvite onSubmit={handleSubmit} />;
|
return (
|
||||||
|
<CreateAndInvite
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
createAndInviteLoader={createAndInviteLoader}
|
||||||
|
accountOptions={accountOptions}
|
||||||
|
accountOptionsLoading={accountOptionsLoading}
|
||||||
|
/>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return <div className="bg-white p-6 rounded-lg shadow-sm">Coming Soon...</div>;
|
return <div className="bg-white p-6 rounded-lg shadow-sm">Coming Soon...</div>;
|
||||||
}
|
}
|
||||||
|
|
@ -221,18 +249,20 @@ const Setting = () => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getUserDetails()
|
getUserDetails()
|
||||||
|
// Fetch account list when component mounts
|
||||||
|
getAccountList()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Set initial subsection when component mounts or activeTab changes
|
// Set initial subsection when component mounts or activeTab changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const subSections = getSubSections(activeTab);
|
const subSections = getSubSections(activeTab);
|
||||||
const hash = location.hash.replace('#', '');
|
const hash = location.hash.replace('#', '');
|
||||||
|
|
||||||
if (subSections.length > 0) {
|
if (subSections.length > 0) {
|
||||||
// If there's a hash and it's valid for current tab, use it
|
// If there's a hash and it's valid for current tab, use it
|
||||||
if (hash && subSections.find(s => s.id === hash)) {
|
if (hash && subSections.find(s => s.id === hash)) {
|
||||||
setActiveSubSection(hash);
|
setActiveSubSection(hash);
|
||||||
}
|
}
|
||||||
// If no valid hash or no current activeSubSection, use first subsection
|
// If no valid hash or no current activeSubSection, use first subsection
|
||||||
else if (!activeSubSection || !subSections.find(s => s.id === activeSubSection)) {
|
else if (!activeSubSection || !subSections.find(s => s.id === activeSubSection)) {
|
||||||
const firstSubSectionId = subSections[0].id;
|
const firstSubSectionId = subSections[0].id;
|
||||||
|
|
@ -255,14 +285,51 @@ const Setting = () => {
|
||||||
}, [activeSubSection]);
|
}, [activeSubSection]);
|
||||||
|
|
||||||
const handleSubmit = async (data: FormDataType, action: ActionType) => {
|
const handleSubmit = async (data: FormDataType, action: ActionType) => {
|
||||||
|
setCreateAndInviteLoader(true)
|
||||||
try {
|
try {
|
||||||
|
let res;
|
||||||
|
let toastMesaage ;
|
||||||
if (action === 'create') {
|
if (action === 'create') {
|
||||||
console.log('Creating account:', data);
|
console.log('Creating account:', data);
|
||||||
|
const payloadAccount = {
|
||||||
|
name: data.accountName
|
||||||
|
}
|
||||||
|
res = await api.post(`/api/user/accounts`, payloadAccount)
|
||||||
|
console.log("resAccountName", res)
|
||||||
|
if (res.status === 201) {
|
||||||
|
toastMesaage = `${action.toUpperCase()} Created Successfull...!`
|
||||||
|
}
|
||||||
} else if (action === 'invite') {
|
} else if (action === 'invite') {
|
||||||
console.log('Inviting user:', data);
|
console.log('Inviting user:', data);
|
||||||
|
const payloadInvite = {
|
||||||
|
email: data.email,
|
||||||
|
role: data.role
|
||||||
|
}
|
||||||
|
res = await api.post(`/api/accounts/${data.i_accountName}/invite`, payloadInvite)
|
||||||
|
console.log("InviteNAme",res)
|
||||||
|
toastMesaage = "Invite Successfull...!"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res?.data) {
|
||||||
|
toast.success(toastMesaage, {
|
||||||
|
position: "top-center",
|
||||||
|
autoClose: 1500,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: false,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
progress: undefined,
|
||||||
|
theme: "light",
|
||||||
|
transition: Bounce,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refetch account list after successful submission
|
||||||
|
await getAccountList();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Submission error:', err);
|
console.error('Submission error:', err);
|
||||||
|
} finally {
|
||||||
|
setCreateAndInviteLoader(false)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -279,11 +346,10 @@ const Setting = () => {
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
onClick={() => handleMainTabClick(tab.id)}
|
onClick={() => handleMainTabClick(tab.id)}
|
||||||
className={`cursor-pointer relative pb-3 text-sm font-medium transition-all duration-200
|
className={`cursor-pointer relative pb-3 text-sm font-medium transition-all duration-200
|
||||||
${
|
${activeTab === tab.id
|
||||||
activeTab === tab.id
|
|
||||||
? "text-blue-600 after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:bg-blue-600"
|
? "text-blue-600 after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:bg-blue-600"
|
||||||
: "text-gray-600 hover:text-blue-500"
|
: "text-gray-600 hover:text-blue-500"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -296,19 +362,19 @@ const Setting = () => {
|
||||||
<div className="flex mt-3 px-8 flex-1 min-h-0">
|
<div className="flex mt-3 px-8 flex-1 min-h-0">
|
||||||
{/* Fixed Aside Navigation - Shows subsections of active tab */}
|
{/* Fixed Aside Navigation - Shows subsections of active tab */}
|
||||||
{activeTab === 'userProfile' &&
|
{activeTab === 'userProfile' &&
|
||||||
<section className="flex-shrink-0 mr-6 w-50 relative">
|
<section className="flex-shrink-0 mr-6 w-50 relative">
|
||||||
<div className="sticky top-6 z-20">
|
<div className="sticky top-6 z-20">
|
||||||
<AsideSection
|
<AsideSection
|
||||||
activeSubSection={activeSubSection}
|
activeSubSection={activeSubSection}
|
||||||
onSubSectionClick={handleSubSectionClick}
|
onSubSectionClick={handleSubSectionClick}
|
||||||
subSections={getSubSections(activeTab)}
|
subSections={getSubSections(activeTab)}
|
||||||
mainTabTitle={tabs.find(t => t.id === activeTab)?.label || ""}
|
mainTabTitle={tabs.find(t => t.id === activeTab)?.label || ""}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
{/* Scrollable Main Content */}
|
{/* Scrollable Main Content */}
|
||||||
<div
|
<div
|
||||||
ref={scrollContainerRef}
|
ref={scrollContainerRef}
|
||||||
className="flex-1 min-w-0 overflow-y-auto no-scrollbar"
|
className="flex-1 min-w-0 overflow-y-auto no-scrollbar"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
// import { X, Upload, Edit3, Image, Cloud, HardDrive, Droplets } from 'lucide-react';
|
|
||||||
import { AiOutlineClose, AiOutlineUpload, AiOutlineEdit } from 'react-icons/ai';
|
import { AiOutlineClose, AiOutlineUpload, AiOutlineEdit } from 'react-icons/ai';
|
||||||
import { MdImage, MdCloudUpload } from 'react-icons/md';
|
import { MdImage, MdCloudUpload } from 'react-icons/md';
|
||||||
import api from '../../../config/api';
|
import api from '../../../config/api';
|
||||||
import DrawSignatureCanvas from './DrawSignatureCanvas';
|
import DrawSignatureCanvas from './DrawSignatureCanvas';
|
||||||
|
import { FaSpinner } from "react-icons/fa";
|
||||||
|
import { GrCheckboxSelected } from "react-icons/gr";
|
||||||
|
|
||||||
interface AddSignatureModalProps {
|
interface AddSignatureModalProps {
|
||||||
setShowAddSignature?: (value: boolean) => void;
|
setShowAddSignature?: (value: boolean) => void;
|
||||||
|
getAllSignatures: () => Promise<void>;
|
||||||
}
|
}
|
||||||
interface Account {
|
interface Account {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -24,23 +25,19 @@ interface UserData {
|
||||||
account: Account;
|
account: Account;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignature }) => {
|
interface SelectStyle {
|
||||||
const [activeTab, setActiveTab] = useState<'choose' | 'draw' | 'upload'>('choose');
|
id: number,
|
||||||
|
style: string
|
||||||
|
}
|
||||||
|
const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignature , getAllSignatures }) => {
|
||||||
|
const [activeTab, setActiveTab] = useState<'SELECT' | 'DRAW' | 'UPLOAD'>('SELECT');
|
||||||
const [fullName, setFullName] = useState('');
|
const [fullName, setFullName] = useState('');
|
||||||
const [initials, setInitials] = useState('');
|
const [initials, setInitials] = useState('');
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||||
const [signatures, setSignatures] = useState<{
|
const [signatures, setSignatures] = useState<{
|
||||||
fullSignature: string | null;
|
fullSignature: string | null;
|
||||||
initials: string | null;
|
initials: string | null;
|
||||||
}>({ fullSignature: null, initials: null });
|
}>({ fullSignature: null, initials: null });
|
||||||
|
|
||||||
const handleSignatureChange = (data: { fullSignature: string | null; initials: string | null }) => {
|
|
||||||
setSignatures(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Sample signature options for the CHOOSE tab
|
|
||||||
const signatureOptions = [
|
const signatureOptions = [
|
||||||
{ id: 1, name: 'print', initials: 'LA', style: 'print' },
|
{ id: 1, name: 'print', initials: 'LA', style: 'print' },
|
||||||
{ id: 2, name: 'cursive', initials: 'LA', style: 'cursive' },
|
{ id: 2, name: 'cursive', initials: 'LA', style: 'cursive' },
|
||||||
|
|
@ -49,10 +46,9 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignatu
|
||||||
{ id: 5, name: 'modern', initials: 'LA', style: 'modern' },
|
{ id: 5, name: 'modern', initials: 'LA', style: 'modern' },
|
||||||
{ id: 6, name: 'classic', initials: 'LA', style: 'classic' },
|
{ id: 6, name: 'classic', initials: 'LA', style: 'classic' },
|
||||||
];
|
];
|
||||||
|
const [selectedSignature, setSelectedSignature] = useState<SelectStyle>(
|
||||||
const [selectedSignature, setSelectedSignature] = useState<number>(1);
|
signatureOptions.length > 0 ? { id: signatureOptions[0].id, style: signatureOptions[0].style } : { id: 0, style: "" }
|
||||||
console.log(selectedSignature);
|
);
|
||||||
|
|
||||||
|
|
||||||
// Store the actual file
|
// Store the actual file
|
||||||
const [file1, setFile1] = useState<File | null>(null);
|
const [file1, setFile1] = useState<File | null>(null);
|
||||||
|
|
@ -75,13 +71,22 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignatu
|
||||||
name: "",
|
name: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Navigation items
|
// Navigation items
|
||||||
const navigationItems = [
|
const navigationItems = [
|
||||||
{ id: 'choose', label: 'Choose', icon: <MdImage className="w-5 h-5" /> },
|
{ id: 'SELECT', label: 'SELECT', icon: <GrCheckboxSelected className="w-4 h-4" /> },
|
||||||
{ id: 'draw', label: 'Draw', icon: <AiOutlineEdit className="w-5 h-5" /> },
|
{ id: 'DRAW', label: 'DRAW', icon: <AiOutlineEdit className="w-5 h-5" /> },
|
||||||
{ id: 'upload', label: 'Upload', icon: <AiOutlineUpload className="w-5 h-5" /> },
|
{ id: 'UPLOAD', label: 'UPLOAD', icon: <AiOutlineUpload className="w-5 h-5" /> },
|
||||||
];
|
];
|
||||||
|
console.log("signatures", signatures)
|
||||||
|
const handleSignatureChange = (data: { fullSignature: string | null; initials: string | null }) => {
|
||||||
|
setSignatures(data);
|
||||||
|
};
|
||||||
|
console.log("activeTab", activeTab)
|
||||||
|
console.log("selectedSignature", selectedSignature);
|
||||||
|
|
||||||
|
console.log('file1', file1)
|
||||||
|
console.log("file1", file2)
|
||||||
|
|
||||||
|
|
||||||
// Cleanup preview URLs when component unmounts or image changes
|
// Cleanup preview URLs when component unmounts or image changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -91,55 +96,73 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignatu
|
||||||
};
|
};
|
||||||
}, [preview1, preview2]);
|
}, [preview1, preview2]);
|
||||||
|
|
||||||
const handleImageChange = (
|
const handleImageChange = async (
|
||||||
e: React.ChangeEvent<HTMLInputElement>,
|
e: React.ChangeEvent<HTMLInputElement>,
|
||||||
setFile: React.Dispatch<React.SetStateAction<File | null>>,
|
setFile: React.Dispatch<React.SetStateAction<File | null>>,
|
||||||
setPreview: React.Dispatch<React.SetStateAction<string | null>>
|
setPreview: React.Dispatch<React.SetStateAction<string | null>>,
|
||||||
|
type: "fullSignature" | "initials"
|
||||||
) => {
|
) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
setFile(file);
|
setFile(file);
|
||||||
const previewUrl = URL.createObjectURL(file);
|
const previewUrl = URL.createObjectURL(file);
|
||||||
setPreview(previewUrl);
|
setPreview(previewUrl);
|
||||||
|
|
||||||
|
// Convert to base64
|
||||||
|
const base64 = await convertToBase64(file);
|
||||||
|
|
||||||
|
// Update the appropriate signature
|
||||||
|
setSignatures((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[type]: base64,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const convertToBase64 = (file: File): Promise<string> => {
|
||||||
|
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 () => {
|
const handleSubmit = async () => {
|
||||||
|
setIsSubmitting(true)
|
||||||
if (!signatures.fullSignature || !signatures.initials) {
|
if (!signatures.fullSignature || !signatures.initials) {
|
||||||
alert('Please provide both signature and initials');
|
alert('Please provide both signature and initials');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Example: Posting the base64 data
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
signatureType: "DRAW",
|
signatureType: activeTab,
|
||||||
// "signatureId": 0, this work on only the put req.
|
// "signatureId": 0, this work on only the put req.
|
||||||
signatureBase64: signatures.fullSignature,
|
signatureBase64: signatures.fullSignature,
|
||||||
initialsBase64: signatures.initials,
|
initialsBase64: signatures.initials,
|
||||||
firstName: user.firstName,
|
firstName: user.firstName,
|
||||||
lastName: user.lastName,
|
lastName: user.lastName,
|
||||||
font: ""
|
font: selectedSignature.style
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('Submitting Signature:', payload);
|
console.log('Submitting Signature:', payload);
|
||||||
let res = await api.post(`/api/user/signatures`,payload)
|
let res = await api.post(`/api/user/signatures`, payload)
|
||||||
console.log("handleSubmit", res)
|
console.log("handleSubmit", res)
|
||||||
|
if (res?.data) {
|
||||||
|
getAllSignatures()
|
||||||
|
setShowAddSignature?.(false)
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching user details:", error);
|
console.error("Error fetching user details:", error);
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// fetch('/api/signature', { method: 'POST', body: JSON.stringify(payload) })
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
switch (activeTab) {
|
switch (activeTab) {
|
||||||
case 'choose':
|
case 'SELECT':
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col ">
|
<div className="h-full flex flex-col ">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
|
|
@ -155,8 +178,8 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignatu
|
||||||
type="radio"
|
type="radio"
|
||||||
id={`signature-${option.id}`}
|
id={`signature-${option.id}`}
|
||||||
name="signature"
|
name="signature"
|
||||||
checked={selectedSignature === option.id}
|
checked={selectedSignature.id === option.id}
|
||||||
onChange={() => setSelectedSignature(option.id)}
|
onChange={() => setSelectedSignature({ id: option.id, style: option.style })}
|
||||||
className="w-4 h-4 text-blue-600 focus:ring-blue-500"
|
className="w-4 h-4 text-blue-600 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
<div className="flex-1 grid grid-cols-2 gap-2">
|
<div className="flex-1 grid grid-cols-2 gap-2">
|
||||||
|
|
@ -193,12 +216,12 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignatu
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'draw':
|
case 'DRAW':
|
||||||
return (
|
return (
|
||||||
<DrawSignatureCanvas onSignatureChange={handleSignatureChange} />
|
<DrawSignatureCanvas onSignatureChange={handleSignatureChange} />
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'upload':
|
case 'UPLOAD':
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
|
|
@ -210,7 +233,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignatu
|
||||||
{/* Upload 1 */}
|
{/* Upload 1 */}
|
||||||
<div className="col-span-8">
|
<div className="col-span-8">
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">Full Signature</label>
|
<label className="block text-sm font-medium text-gray-700 mb-2">Full Signature</label>
|
||||||
<label htmlFor="upload1" className=" border-2 border-dashed border-gray-300 rounded-lg h-48 flex items-center justify-center bg-gray-50 hover:bg-gray-100 cursor-pointer transition-colors">
|
<label htmlFor="upload1" className=" border-2 border-dashed border-gray-300 rounded-lg h-62 flex items-center justify-center bg-gray-50 hover:bg-gray-100 cursor-pointer transition-colors">
|
||||||
{preview1 ? (
|
{preview1 ? (
|
||||||
<img src={preview1} alt="Signature Preview" className="max-h-44 object-contain" />
|
<img src={preview1} alt="Signature Preview" className="max-h-44 object-contain" />
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -226,19 +249,19 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignatu
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
className="hidden"
|
className="hidden"
|
||||||
onChange={(e) => handleImageChange(e, setFile1, setPreview1)}
|
onChange={(e) => handleImageChange(e, setFile1, setPreview1, "fullSignature")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Upload 2 */}
|
{/* Upload 2 */}
|
||||||
<div className="col-span-4">
|
<div className="col-span-4">
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">Initials</label>
|
<label className="block text-sm font-medium text-gray-700 mb-2">Initials</label>
|
||||||
<label htmlFor="upload2" className=" border-2 border-dashed border-gray-300 rounded-lg h-48 flex items-center justify-center bg-gray-50 hover:bg-gray-100 cursor-pointer transition-colors">
|
<label htmlFor="upload2" className=" border-2 border-dashed border-gray-300 rounded-lg h-62 flex items-center justify-center bg-gray-50 hover:bg-gray-100 cursor-pointer transition-colors">
|
||||||
{preview2 ? (
|
{preview2 ? (
|
||||||
<img src={preview2} alt="Initials Preview" className="max-h-44 object-contain" />
|
<img src={preview2} alt="Initials Preview" className="max-h-44 object-contain" />
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<MdImage className="mx-auto h-8 w-8 text-gray-400 mb-2" />
|
<MdCloudUpload className="mx-auto h-8 w-8 text-gray-400 mb-2" />
|
||||||
<p className="text-xs text-gray-500">Upload initials</p>
|
<p className="text-xs text-gray-500">Upload initials</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -248,7 +271,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignatu
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
className="hidden"
|
className="hidden"
|
||||||
onChange={(e) => handleImageChange(e, setFile2, setPreview2)}
|
onChange={(e) => handleImageChange(e, setFile2, setPreview2, "initials")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -332,8 +355,8 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignatu
|
||||||
{navigationItems.map((item) => (
|
{navigationItems.map((item) => (
|
||||||
<button
|
<button
|
||||||
key={item.id}
|
key={item.id}
|
||||||
onClick={() => setActiveTab(item.id as 'choose' | 'draw' | 'upload')}
|
onClick={() => setActiveTab(item.id as 'SELECT' | 'DRAW' | 'UPLOAD')}
|
||||||
className={`w-full flex items-center space-x-3 px-2 py-2 text-sm font-medium rounded-lg transition-colors ${activeTab === item.id
|
className={`w-full flex items-center space-x-3 px-2 py-2 text-sm font-medium rounded-lg cursor-pointer transition-colors ${activeTab === item.id
|
||||||
? 'bg-blue-50 text-blue-700 border border-blue-200'
|
? 'bg-blue-50 text-blue-700 border border-blue-200'
|
||||||
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
|
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
|
||||||
}`}
|
}`}
|
||||||
|
|
@ -356,25 +379,37 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignatu
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="flex-1 py-5 px-5 overflow-hidden ">
|
<div className="flex-1 py-5 px-5 overflow-hidden ">
|
||||||
<button
|
{/* <button
|
||||||
onClick={() => setShowAddSignature?.(false)}
|
onClick={() => setShowAddSignature?.(false)}
|
||||||
className="absolute top-2 right-3 p-2 hover:bg-gray-100 rounded-full transition-colors z-20"
|
className="absolute top-2 right-3 p-2 hover:bg-gray-100 rounded-full transition-colors z-20"
|
||||||
>
|
>
|
||||||
<AiOutlineClose className="w-5 h-5 text-gray-500" />
|
<AiOutlineClose className="w-5 h-5 text-gray-500" />
|
||||||
</button>
|
</button> */}
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer Buttons in Main Area */}
|
{/* Footer Buttons in Main Area */}
|
||||||
<div className="p-4 border-t border-gray-200 flex justify-end gap-3">
|
<div className="p-4 border-t border-gray-200 flex justify-end gap-3">
|
||||||
<button
|
<button
|
||||||
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 text-gray-700 hover:bg-gray-50 transition-colors"
|
className="cursor-pointer px-4 py-2 text-sm font-medium rounded-md border border-gray-300 text-gray-700 hover:bg-gray-50 transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
|
||||||
onClick={() => setShowAddSignature?.(false)}
|
onClick={() => setShowAddSignature?.(false)}
|
||||||
|
disabled={isSubmitting}
|
||||||
>
|
>
|
||||||
CANCEL
|
CANCEL
|
||||||
</button>
|
</button>
|
||||||
<button className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700 transition-colors" onClick={handleSubmit}>
|
<button
|
||||||
CREATE SIGNATURE
|
className="cursor-pointer px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700 transition-colors flex items-center gap-2 disabled:opacity-60 disabled:cursor-not-allowed"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<>
|
||||||
|
<FaSpinner className="animate-spin" />
|
||||||
|
CREATING...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"CREATE SIGNATURE"
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import React, { useEffect, useState } from 'react';
|
||||||
import { FiEdit3, FiTrash2, FiPlus, FiMoreVertical } from 'react-icons/fi';
|
import { FiEdit3, FiTrash2, FiPlus, FiMoreVertical } from 'react-icons/fi';
|
||||||
import AddSignatureModal from './AddSignatureModal';
|
import AddSignatureModal from './AddSignatureModal';
|
||||||
import api from '../../../config/api';
|
import api from '../../../config/api';
|
||||||
|
import { Bounce, toast } from "react-toastify";
|
||||||
|
import { FaSpinner } from 'react-icons/fa';
|
||||||
|
|
||||||
interface Signature {
|
interface Signature {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -10,39 +12,23 @@ interface Signature {
|
||||||
lastName: string;
|
lastName: string;
|
||||||
firstName: string
|
firstName: string
|
||||||
font: string;
|
font: string;
|
||||||
initialsBlob:string;
|
initialsBlob: string;
|
||||||
initialsStampedBlob:string;
|
initialsStampedBlob: string;
|
||||||
initialsHash:string;
|
initialsHash: string;
|
||||||
signatureHash:string;
|
signatureHash: string;
|
||||||
signatureBlob:string;
|
signatureBlob: string;
|
||||||
signatureStampedBlob:string;
|
signatureStampedBlob: string;
|
||||||
default:boolean
|
default: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const SignatureManager: React.FC = () => {
|
const SignatureManager: React.FC = () => {
|
||||||
|
const [loading, setLoading] = useState<boolean>(false)
|
||||||
|
|
||||||
|
|
||||||
const [signatures, setSignatures] = useState<Signature[]>([]);
|
const [signatures, setSignatures] = useState<Signature[]>([]);
|
||||||
|
|
||||||
const [activeDropdown, setActiveDropdown] = useState<string | null>(null);
|
const [activeDropdown, setActiveDropdown] = useState<string | null>(null);
|
||||||
const [showAddSignature, setShowAddSignature] = useState<boolean>(false)
|
const [showAddSignature, setShowAddSignature] = useState<boolean>(false)
|
||||||
|
|
||||||
const handleEdit = (id: string) => {
|
|
||||||
console.log('Edit signature:', id);
|
|
||||||
setActiveDropdown(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = (id: string) => {
|
|
||||||
setSignatures(signatures.filter(sig => sig.id !== id));
|
|
||||||
setActiveDropdown(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleDropdown = (id: string) => {
|
|
||||||
setActiveDropdown(activeDropdown === id ? null : id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAllSignatures = async () => {
|
const getAllSignatures = async () => {
|
||||||
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
let res = await api.get("/api/user/signatures")
|
let res = await api.get("/api/user/signatures")
|
||||||
console.log("getAllSignatures", res.data)
|
console.log("getAllSignatures", res.data)
|
||||||
|
|
@ -50,6 +36,8 @@ const SignatureManager: React.FC = () => {
|
||||||
setSignatures(signatureData)
|
setSignatures(signatureData)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("getAllSignatures", error)
|
console.log("getAllSignatures", error)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,148 +45,198 @@ const SignatureManager: React.FC = () => {
|
||||||
getAllSignatures()
|
getAllSignatures()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const handleEdit = (id: string) => {
|
||||||
|
console.log('Edit signature:', id);
|
||||||
|
setActiveDropdown(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (id: string) => {
|
||||||
|
if (!id) return;
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
let res = await api.delete(`/api/user/signatures/${id}`)
|
||||||
|
console.log(res.data)
|
||||||
|
toast.success("Signature Delete Successfull", {
|
||||||
|
position: "top-center",
|
||||||
|
autoClose: 500,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: false,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
progress: undefined,
|
||||||
|
theme: "light",
|
||||||
|
transition: Bounce,
|
||||||
|
});
|
||||||
|
await getAllSignatures();
|
||||||
|
} catch (error) {
|
||||||
|
console.log("delete signature", error)
|
||||||
|
} finally {
|
||||||
|
setActiveDropdown(null);
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleDropdown = (id: string) => {
|
||||||
|
setActiveDropdown(activeDropdown === id ? null : id);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white p-3 rounded-md px-4 mb-6">
|
<>
|
||||||
<div className="max-w-[84vw] mx-auto">
|
{loading ? <div className=' flex items-center justify-center'>
|
||||||
{/* Header */}
|
<div className="p-12 text-center mt-8">
|
||||||
<div className="flex items-center justify-between mb-8">
|
<div className="text-gray-400 mb-4">
|
||||||
<div>
|
<FaSpinner className="w-20 h-20 mx-auto animate-spin" />
|
||||||
<h1 className="text-[17px] font-medium text-gray-500 mb-2">Signatures</h1>
|
|
||||||
<p className="text-gray-300 text-sm">Add or update your name and signature styles.</p>
|
|
||||||
</div>
|
</div>
|
||||||
<button className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-semibold text-sm text-[14px] flex items-center cursor-pointer transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5" onClick={() => setShowAddSignature((prev) => !prev)}>
|
<h3 className="text-xl font-semibold text-gray-700 mb-2">Loading...</h3>
|
||||||
<FiPlus className="" />
|
|
||||||
ADD
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div> :
|
||||||
|
<div className="min-h-screen bg-white p-3 rounded-md px-4 mb-6">
|
||||||
|
<div className="max-w-[84vw] mx-auto">
|
||||||
|
<>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between mb-8">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-[17px] font-medium text-gray-500 mb-2">Signatures</h1>
|
||||||
|
<p className="text-gray-300 text-sm">Add or update your name and signature styles.</p>
|
||||||
|
</div>
|
||||||
|
<button className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-semibold text-sm text-[14px] flex items-center cursor-pointer transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5" onClick={() => setShowAddSignature((prev) => !prev)}>
|
||||||
|
<FiPlus className="" />
|
||||||
|
ADD
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Table Card */}
|
{/* Table Card */}
|
||||||
<div className="bg-white rounded-md shadow-md overflow-hidden border border-gray-100">
|
<div className="bg-white rounded-md shadow-md overflow-hidden border border-gray-100">
|
||||||
{/* Table Header */}
|
{/* Table Header */}
|
||||||
<div className="bg-gray-50 border-b border-gray-200">
|
<div className="bg-gray-50 border-b border-gray-200">
|
||||||
<div className="grid grid-cols-12 gap-4 px-6 py-4">
|
<div className="grid grid-cols-12 gap-4 px-6 py-4">
|
||||||
<div className="col-span-5">
|
<div className="col-span-5">
|
||||||
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wider">Signature</h3>
|
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wider">Signature</h3>
|
||||||
</div>
|
|
||||||
<div className="col-span-5">
|
|
||||||
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wider">Initial</h3>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-2">
|
|
||||||
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wider text-right">Actions</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Table Body */}
|
|
||||||
<div className="divide-y divide-gray-100">
|
|
||||||
{signatures.map((signature, index) => (
|
|
||||||
<div key={signature.id} className="grid grid-cols-12 gap-4 px-6 py-6 hover:bg-gray-50 transition-colors duration-150">
|
|
||||||
{/* Signature Column */}
|
|
||||||
<div className="col-span-5">
|
|
||||||
<div className="flex flex-col space-y-2 relative ps-6 border-l-3 border-blue-600 rounded-bl-2xl rounded-tl-2xl">
|
|
||||||
{/* Label */}
|
|
||||||
<div className="text-sm text-gray-600 font-medium mb-1">
|
|
||||||
Signed by:
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="col-span-5">
|
||||||
{/* Signature Image */}
|
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wider">Initial</h3>
|
||||||
<div className="bg-white rounded-lg p-1 flex items-center justify-center ">
|
|
||||||
<img
|
|
||||||
src={`data:image/png;base64,${signature.signatureBlob}`}
|
|
||||||
className="h-[50px] w-[250px] object-contain"
|
|
||||||
alt="Signature"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="col-span-2">
|
||||||
{/* Signature ID */}
|
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wider text-right">Actions</h3>
|
||||||
<div className="text-xs text-gray-500 truncate">
|
|
||||||
{signature.signatureHash}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Table Body */}
|
||||||
|
<div className="divide-y divide-gray-100">
|
||||||
|
{signatures.map((signature, index) => (
|
||||||
|
<div key={signature.id} className="grid grid-cols-12 gap-4 px-6 py-9 hover:bg-gray-50 transition-colors duration-150">
|
||||||
|
{/* Signature Column */}
|
||||||
|
<div className="col-span-5">
|
||||||
|
<div className="flex flex-col space-y-2 relative ps-6 border-l-3 border-blue-600 rounded-bl-2xl rounded-tl-2xl">
|
||||||
|
{/* Label */}
|
||||||
|
<div className="text-sm text-gray-600 font-medium mb-1">
|
||||||
|
Signed by:
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Initial Column */}
|
{/* Signature Image */}
|
||||||
<div className="col-span-5">
|
<div className="bg-white rounded-lg p-1 flex items-center justify-center ">
|
||||||
<div className="flex flex-col space-y-2 relative ps-6 border-l-3 border-green-600 rounded-bl-2xl rounded-tl-2xl">
|
<img
|
||||||
{/* Label */}
|
src={`data:image/png;base64,${signature.signatureBlob}`}
|
||||||
<div className="text-sm text-gray-600 font-medium mb-1">
|
className="h-[50px] w-[250px] object-contain"
|
||||||
DS
|
alt="Signature"
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Signature Image */}
|
{/* Signature ID */}
|
||||||
<div className="bg-white rounded-lg p-1 flex items-center justify-center ">
|
<div className="text-xs text-gray-500 truncate">
|
||||||
<img
|
{signature.signatureHash}
|
||||||
src={`data:image/png;base64,${signature.initialsBlob}`}
|
</div>
|
||||||
className="h-[50px] object-contain"
|
</div>
|
||||||
alt="DS Signature"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
{/* Actions Column */}
|
|
||||||
<div className="col-span-2 flex justify-between items-start pt-9 ps-8 ">
|
|
||||||
<div className="relative ">
|
|
||||||
<button
|
|
||||||
onClick={() => toggleDropdown(signature.id)}
|
|
||||||
className="bg-gray-100 hover:bg-gray-200 text-gray-700 px-1.5 py-1.5 text-[13px] rounded-lg font-semibold flex items-center transition-all duration-200 border border-gray-300 "
|
|
||||||
>
|
|
||||||
ACTIONS
|
|
||||||
<FiMoreVertical />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Dropdown Menu */}
|
|
||||||
{activeDropdown === signature.id && (
|
|
||||||
<div className="absolute right-0 mt-0.5 w-28 bg-white rounded-lg shadow-xl border border-gray-200 z-10 py-1">
|
|
||||||
<button
|
|
||||||
onClick={() => handleEdit(signature.id)}
|
|
||||||
className="w-full px-4 py-1 text-left text-gray-500 hover:bg-blue-50 hover:text-blue-700 flex items-center gap-3 text-sm transition-colors duration-150"
|
|
||||||
>
|
|
||||||
<FiEdit3 />
|
|
||||||
Edit
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleDelete(signature.id)}
|
|
||||||
className="w-full px-4 py-1 text-left text-gray-500 hover:bg-red-50 hover:text-red-700 flex items-center gap-3 text-sm transition-colors duration-150"
|
|
||||||
>
|
|
||||||
<FiTrash2 />
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
{/* Initial Column */}
|
||||||
|
<div className="col-span-5">
|
||||||
|
<div className="flex flex-col space-y-2 relative ps-6 border-l-3 border-green-600 rounded-bl-2xl rounded-tl-2xl">
|
||||||
|
{/* Label */}
|
||||||
|
<div className="text-sm text-gray-600 font-medium mb-1">
|
||||||
|
DS
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Signature Image */}
|
||||||
|
<div className="bg-white rounded-lg p-1 flex items-center justify-center ">
|
||||||
|
<img
|
||||||
|
src={`data:image/png;base64,${signature.initialsBlob}`}
|
||||||
|
className="h-[50px] object-contain"
|
||||||
|
alt="DS Signature"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{/* Actions Column */}
|
||||||
|
<div className="col-span-2 flex justify-between items-start pt-9 ps-8 ">
|
||||||
|
<div className="relative ">
|
||||||
|
<button
|
||||||
|
onClick={() => toggleDropdown(signature.id)}
|
||||||
|
className="bg-gray-100 hover:bg-gray-200 text-gray-700 px-1.5 py-1.5 text-[13px] rounded-lg font-semibold flex items-center transition-all duration-200 border border-gray-300 "
|
||||||
|
>
|
||||||
|
ACTIONS
|
||||||
|
<FiMoreVertical />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Dropdown Menu */}
|
||||||
|
{activeDropdown === signature.id && (
|
||||||
|
<div className="absolute right-0 mt-0.5 w-28 bg-white rounded-lg shadow-xl border border-gray-200 z-10 py-1">
|
||||||
|
<button
|
||||||
|
onClick={() => handleEdit(signature.id)}
|
||||||
|
className="w-full px-4 py-1 text-left text-gray-500 hover:bg-blue-50 hover:text-blue-700 flex items-center gap-3 text-sm transition-colors duration-150"
|
||||||
|
>
|
||||||
|
<FiEdit3 />
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDelete(signature.id)}
|
||||||
|
className="w-full px-4 py-1 text-left text-gray-500 hover:bg-red-50 hover:text-red-700 flex items-center gap-3 text-sm transition-colors duration-150"
|
||||||
|
>
|
||||||
|
<FiTrash2 />
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
|
{/* Empty State (if no signatures) */}
|
||||||
|
{signatures.length === 0 && (
|
||||||
|
<div className="bg-white rounded-xl shadow-lg p-12 text-center mt-8">
|
||||||
|
<div className="text-gray-400 mb-4">
|
||||||
|
<FiEdit3 className="w-16 h-16 mx-auto" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-700 mb-2">No signatures yet</h3>
|
||||||
|
<p className="text-gray-500 mb-6">Create your first signature to get started.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Click outside to close dropdown */}
|
||||||
|
{activeDropdown && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-0"
|
||||||
|
onClick={() => setActiveDropdown(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showAddSignature && (
|
||||||
|
<AddSignatureModal setShowAddSignature={setShowAddSignature} getAllSignatures={getAllSignatures} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
}</>
|
||||||
{/* Empty State (if no signatures) */}
|
|
||||||
{signatures.length === 0 && (
|
|
||||||
<div className="bg-white rounded-xl shadow-lg p-12 text-center mt-8">
|
|
||||||
<div className="text-gray-400 mb-4">
|
|
||||||
<FiEdit3 className="w-16 h-16 mx-auto" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-xl font-semibold text-gray-700 mb-2">No signatures yet</h3>
|
|
||||||
<p className="text-gray-500 mb-6">Create your first signature to get started.</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Click outside to close dropdown */}
|
|
||||||
{activeDropdown && (
|
|
||||||
<div
|
|
||||||
className="fixed inset-0 z-0"
|
|
||||||
onClick={() => setActiveDropdown(null)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showAddSignature && (
|
|
||||||
<AddSignatureModal setShowAddSignature={setShowAddSignature} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue