diff --git a/src/App.tsx b/src/App.tsx index b8c8d23..b204b47 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,18 +14,11 @@ import SignDocument from "./components/client/SignDocument"; import Transactions from "./components/transactions/Transactions"; import ProtectedRoute from "./components/routes_helper/ProtectedRoute"; -import { HashRouter } from 'react-router-dom'; -import { useDispatch, useSelector } from "react-redux"; -import { fetchUserDetails } from "./store/slices/userSlice"; -import { AppDispatch, RootState } from "./store/store"; -import { useEffect, useLayoutEffect } from "react"; - import Setting from "./components/setting/Setting"; import Accounts from "./components/accounts/Accounts"; import Account from "./components/accounts/Account"; import TransactionView from "./components/transactions/TransactionView"; -import AccountTransactionWrapper from "./components/AccountTransactionWrapper"; -import SigningOrderDiagram from "./SigningOrderDiagram "; +import Templates from "./components/templates/Templates"; const App = () => { return ( @@ -41,16 +34,15 @@ const App = () => { }> } /> - } /> {/* } /> */} - - }> + {/* }> */} } /> } /> } /> } /> } /> - + } /> + {/* */} } /> diff --git a/src/components/AccountTransactionWrapper.tsx b/src/components/AccountTransactionWrapper.tsx index 7d558d5..43f2303 100644 --- a/src/components/AccountTransactionWrapper.tsx +++ b/src/components/AccountTransactionWrapper.tsx @@ -1,13 +1,14 @@ import { Outlet } from 'react-router-dom' -import AccountNav from './accounts/AccountNav' +// import AccountNav from './accounts/AccountNav' +import CommonNav from './CommonNav' const AccountTransactionWrapper = () => { return ( <> - + ) diff --git a/src/components/CommonNav.tsx b/src/components/CommonNav.tsx new file mode 100644 index 0000000..49005a1 --- /dev/null +++ b/src/components/CommonNav.tsx @@ -0,0 +1,363 @@ +import React, { useEffect, useRef, useState } from 'react'; +// import { useSelector } from 'react-redux'; +import { Link, useLocation, useNavigate } from 'react-router-dom'; +import { FaSignOutAlt, FaUser, FaBuilding, FaChevronDown, FaCheck } from "react-icons/fa"; +import api from '../config/api'; + +// import { FaUser, FaSignOutAlt, FaBuilding, FaChevronDown, FaCheck } from 'react-icons/fa'; + +// import { RootState } from '../../store/store'; +// import { UserState } from '../../store/slices/userSlice'; + + + +interface CommonNavProps { + accountName?: string | null; + transactionName?: string | null; +} + +// Define the shape of state object +interface Account { + id: number; + isDefault: boolean; + name: string; +} + +interface UserData { + id: string; + firstName: string; + lastName: string; + email: string; + language: string; + account: Account; +} + +interface AccountDataArray { + accountId: number, accountName: string, role: string, status: boolean +} + +const CommonNav: React.FC = ({ accountName = null, transactionName = null }) => { + // const user: UserState = useSelector((state: RootState) => state.user); + + const [open, setOpen] = useState(false); + const [accountsOpen, setAccountsOpen] = useState(false); + const [loadingAccounts, setLoadingAccounts] = useState(false); + const [accounts, setAccounts] = useState([]); + const [hasModalBeenClosed, setHasModalBeenClosed] = useState(false); // Track if modal was closed + const dropdownRef = useRef(null); + + const [user, setUser] = useState({ + id: "", + firstName: "", + lastName: "", + email: "", + language: "", + account: { + id: 0, + isDefault: false, + name: "", + }, + }); + + const getAccountDetails = async () => { + try { + let res = await api.get('/api/user/accounts') + let items = res?.data + console.log('items', items) + // Extract only the fields you want + const cleanedData: AccountDataArray[] = items.map((item: any) => ({ + accountId: item.accountId, + accountName: item.accountName, + role: item.role, + status: item.status, + })); + + setAccounts(cleanedData); + } catch (error) { + console.log(error) + } + } + + + const handleAccountsClick = async () => { + setAccountsOpen(!accountsOpen); + + if (!accountsOpen) { + setLoadingAccounts(true); + + // Fetch API if: + // 1. No accounts data exists (first time), OR + // 2. Modal was previously closed (need fresh data) + if (accounts.length === 0 || hasModalBeenClosed) { + await getAccountDetails(); + setHasModalBeenClosed(false); // Reset the flag after fetching + } + + setLoadingAccounts(false); + } + }; + + const handleAccountSwitch = (account) => { + setUser(prev => ({ + ...prev, + account: { name: account.name, id: account.id } + })); + + // Update accounts active status + setAccounts(prevAccounts => + prevAccounts.map(acc => ({ + ...acc, + isActive: acc.id === account.id + })) + ); + + setAccountsOpen(false); + setOpen(false); + }; + + const navigate = useNavigate() + + const getUserDetails = async () => { + try { + let res = await api.get(`/api/user/details`) + console.log("getuser", res) + setUser(res?.data) + } catch (error) { + console.error("Error fetching user details:", error); + } + } + + useEffect(() => { + getUserDetails() + }, []) + + + const location = useLocation(); + + const pathnames = location.pathname + .split("/") + .filter((x) => x && isNaN(Number(x))); // remove numeric IDs + + const labelMap: Record = { + accounts: "Accounts", + transactions: "Transactions", + templates: "Templates" + // Add more routes here if needed + }; + + // const [open, setOpen] = useState(false); + // const dropdownRef = useRef(null); + + // Close dropdown when clicked outside + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + if (open) { + setHasModalBeenClosed(true); // Mark modal as closed when clicking outside + } + setOpen(false); + setAccountsOpen(false); // Also close accounts dropdown + } + } + + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, [open]); + + const handleLogout = async () => { + try { + let res = await api.post('/api/auth/logout') + if (res.status === 200) { + setOpen(false); + navigate("/login") + } + } catch (error) { + console.log(error) + } + }; + + // Handle main dropdown toggle + const handleMainDropdownToggle = () => { + if (open) { + setHasModalBeenClosed(true); // Mark as closed when manually closing + } + setOpen(!open); + if (!open) { + setAccountsOpen(false); // Close accounts dropdown when opening main dropdown + } + }; + + const formatSegment = (segment: string) => + segment + .replace(/[-_]/g, " ") + .replace(/\b\w/g, (char) => char.toUpperCase()); + + + const ShimmerRow = () => ( +
+
+
+
+
+
+
+
+
+
+ ); + + + return ( +
+ + +
+ + {/* User Dropdown */} +
+ {/* Avatar Button */} + + + {/* Enhanced Dropdown */} + {open && ( +
+ {/* User Info Section */} +
+
+
+ {(user) ? ( + + {(user?.firstName + " " + user?.lastName).split(' ').map((item) => item.slice(0, 1)).join("").toUpperCase()} + ) : ( + + )} +
+
+
{user.firstName + ' ' + user.lastName || '-'}
+
{user.email || '-'}
+
+ Account #{user?.account?.name || '-'} +
+
+
+
+ + {/* Account List Section */} +
+ + + {/* Accounts Dropdown */} + {accountsOpen && ( +
+ {loadingAccounts ? ( +
+ + +
+ ) : ( +
+ {accounts.length > 0 ? (accounts?.map((account) => ( + + ))) : No Account...} + +
+ )} +
+ )} +
+ + {/* Logout Section */} +
+ +
+
+ )} +
+
+
+ ); +}; + +export default CommonNav; \ No newline at end of file diff --git a/src/components/dashboard/Dashboard.tsx b/src/components/dashboard/Dashboard.tsx index d9c2391..5865246 100644 --- a/src/components/dashboard/Dashboard.tsx +++ b/src/components/dashboard/Dashboard.tsx @@ -17,6 +17,7 @@ import confetti from 'canvas-confetti' import { useWindowSize } from 'react-use' import { FaCheckCircle } from 'react-icons/fa' import { IoClose } from 'react-icons/io5' +import CommonNav from '../CommonNav' function Dashboard() { const [showSideBar, setShowSideBar] = React.useState(true) @@ -103,31 +104,31 @@ function Dashboard() { useEffect(() => { - if (showConfetti) { - const duration = 3000; - const end = Date.now() + duration; + if (showConfetti) { + const duration = 3000; + const end = Date.now() + duration; - const interval = setInterval(() => { - if (Date.now() > end) { - clearInterval(interval); - return; - } + const interval = setInterval(() => { + if (Date.now() > end) { + clearInterval(interval); + return; + } - confetti({ - particleCount: 80, // more particles - angle: Math.random() * 360, // fire in random direction - spread: 220, // wider spread - startVelocity: 15, // more power - scalar: 1.2, // bigger pieces (default is 1) - origin: { - x: Math.random(), - y: Math.random() * 0.6, - }, - zIndex: 9999, - }); - }, 350); // fire every 150ms - } -}, [showConfetti]); + confetti({ + particleCount: 80, // more particles + angle: Math.random() * 360, // fire in random direction + spread: 220, // wider spread + startVelocity: 15, // more power + scalar: 1.2, // bigger pieces (default is 1) + origin: { + x: Math.random(), + y: Math.random() * 0.6, + }, + zIndex: 9999, + }); + }, 350); // fire every 150ms + } + }, [showConfetti]); @@ -173,6 +174,7 @@ function Dashboard() {
{/* Main Content */}
+
diff --git a/src/components/home/Home.tsx b/src/components/home/Home.tsx index db94bf1..c9769b7 100644 --- a/src/components/home/Home.tsx +++ b/src/components/home/Home.tsx @@ -159,7 +159,6 @@ const Home = () => { return ( <> - { }, []) const handleNextClick = async (isSendNow: boolean) => { - if (!selectedDocuments.length || !selectedRecipients.length) { + let formError = { + documents: false, + recipients: false, + subject: false + } + + if (!selectedDocuments.length) { + formError.documents = true + } + if (!selectedRecipients.length) { + formError.recipients = true + } + if (!emailDtls.subject.length) { + formError.subject = true + } + + if (formError.documents || formError.recipients || formError.subject) { console.log(selectedDocuments, 'line 193') toast.dismiss(); - toast.error("Add Documents | Recipients First", { + toast.error(`${formError.documents ? "Add Document" : ""} + ${(formError.documents && formError.recipients) ? " | " : ""} + ${formError.recipients ? "Add Recipient" : ""} + ${(formError.documents || formError.recipients) ? " | " : ""} + ${formError.subject ? "Add Subject" : ""}`, { position: "top-center", autoClose: 5000, hideProgressBar: false, @@ -218,7 +238,8 @@ const ProcessStepOne = () => { setEmailDtls({ ...emailDtls, subject: e.target.value })} placeholder="Email Subject" className="w-full px-3 py-2 rounded border border-gray-300 focus:outline-none focus:ring-2 focus:ring-[var(--primary)]" diff --git a/src/components/process/processStepOne/RecipientAddCard.tsx b/src/components/process/processStepOne/RecipientAddCard.tsx index a31a88b..3b734a7 100644 --- a/src/components/process/processStepOne/RecipientAddCard.tsx +++ b/src/components/process/processStepOne/RecipientAddCard.tsx @@ -32,7 +32,7 @@ const RecipientAddCard = () => { const handleRecipientAdd = (e: React.FormEvent) => { e.preventDefault(); - if (recipient.firstName === "" || recipient.lastName === "" || recipient.email === "" || !recipient.type) return; + if (recipient.lastName === "" || recipient.email === "" || !recipient.type) return; // Validate Email Format const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; @@ -106,7 +106,7 @@ const RecipientAddCard = () => { {/* Name Inputs */}
setOpenContactModal(true)} onBlur={handleContactModalBlur} diff --git a/src/components/process/processStepTwo/DocumentContainer.tsx b/src/components/process/processStepTwo/DocumentContainer.tsx index d1a42e0..2264c29 100644 --- a/src/components/process/processStepTwo/DocumentContainer.tsx +++ b/src/components/process/processStepTwo/DocumentContainer.tsx @@ -14,6 +14,8 @@ import { fetchRecipients } from '../../../store/slices/selectedRecipientsSlice'; import { fetchDocuments } from '../../../store/slices/documentSlice'; import { FaQuestionCircle } from 'react-icons/fa'; import typeToIconMap from '../../../helper/iconMap'; + +import { IoAdd } from 'react-icons/io5'; import Loader from '../../dashboard/Loader'; export interface DroppedItemsObj { @@ -211,7 +213,6 @@ const DocumentContainer: React.FC = ({ isSend }) => { "dimensions": dropTag.dimensions, "type": dropTag.type.toUpperCase(), // "subType": (dropTag.subType ? dropTag.subType.toUpperCase() : null), - // validation: { "minLength": "minLength" in dropTag ? dropTag.minLength : 0, "maxLength": "maxLength" in dropTag ? dropTag.maxLength : 0, "error": ("error" in dropTag && dropTag.error) ? `${dropTag.error}`.toUpperCase() : "error", @@ -523,6 +524,11 @@ const DocumentContainer: React.FC = ({ isSend }) => { })() } + const handleAddRadioClick = (key:string) => { + console.log("Handle Radio Clicled", key) + console.log(droppedItems, "line 525") + } + // const handleAddRadio = (e: React.MouseEvent, key: string, index: number) => { // // const dataToSet: DroppedItemsObj = { // // ...droppedItems, @@ -747,21 +753,53 @@ const DocumentContainer: React.FC = ({ isSend }) => { {/* Expand Circle - ENDS */} + { + item.type !== "radio" ? +
handleTagMouseDown(e, key, index)} + // style={{ width: `${item.width}px !important` }} + className='p-2 rounded-sm h-full shadow opacity-40 cursor-grab w-full' + style={{ background: selectedRecipient[0]?.color }} + > +

+ {IconComponent} +

+
+ : + <> +
+ {/* Radio - Container - START */} +
-
handleTagMouseDown(e, key, index)} - // style={{ width: `${item.width}px !important` }} - className='p-2 rounded-sm h-full shadow opacity-40 cursor-grab w-full' - style={{ background: selectedRecipient[0]?.color }} - > -

- {IconComponent} -

-
+ {/* Radio- Item - START */} +
handleTagMouseDown(e, key, index)} + // style={{ width: `${item.width}px !important` }} + className='p-2 rounded-sm h-full shadow opacity-40 cursor-grab w-full' + style={{ background: selectedRecipient[0]?.color }} + > +

+ {IconComponent} +

+
+ {/* Radio- Item - END */} +
+ {/* Radio - Container - END */} + + + handleAddRadioClick(key)} className='ml-[50%] translate-x-[-50%] cursor-pointer' /> +
+ + + + }
) } diff --git a/src/components/templates/Templates.tsx b/src/components/templates/Templates.tsx new file mode 100644 index 0000000..b32dc6a --- /dev/null +++ b/src/components/templates/Templates.tsx @@ -0,0 +1,128 @@ +import { PiDotsThreeOutlineVerticalFill } from "react-icons/pi"; +import { FaCheck, FaDatabase } from "react-icons/fa"; + +import AddAccountModal from "../accounts/AddAccountModal"; +import { Bounce, toast } from "react-toastify"; +import api from "../../config/api"; +import { useState } from "react"; + +const Templates = () => { + const [openMenuId, setOpenMenuId] = useState(null); + + + const handleAction = async (accountId: string, action: 'accept' | 'reject') => { + try { + let response = await api.post(`/api/accounts/${accountId}/accept/invite`) + console.log(response) + if (response.data.status === 202) { + toast.success("Login Successful", { + position: "top-center", + autoClose: 5000, + hideProgressBar: false, + closeOnClick: false, + pauseOnHover: true, + draggable: true, + progress: undefined, + theme: "light", + transition: Bounce, + }); + } + } catch (error) { + console.error('Error posting data:', error); + } finally { + setOpenMenuId(null); // close menu + } + }; + + return ( + <> + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameOwnerCreated DateLast Change
+
+ {/* {selectedTransactions.includes(tx.id) && ( + + )} */} +
+
+
+ {"test"} +
+
{"1"} + + {"role"} + + + + {"text"} + + + + + + + {/* Dropdown menu */} + {/* {openMenuId === "1" && ( +
e.stopPropagation()} // prevent row click closing + > + + +
+ )} */} +
+
+ + + ); +}; + +export default Templates; diff --git a/src/config/api.tsx b/src/config/api.tsx index bdfb4f3..0d90aa8 100644 --- a/src/config/api.tsx +++ b/src/config/api.tsx @@ -29,7 +29,7 @@ api.interceptors.response.use( const originalRequest = error.config; // Prevent multiple refresh attempts // if (error.response?.status === 401 || error.response?.status === 500) { - if (error.response?.status === 401) { + if (error.response?.status === 401 || error.response?.status === 500) { originalRequest._retry = true; try { if (!isRefreshing) { diff --git a/vite.config.ts b/vite.config.ts index 4a95a51..70397db 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -11,7 +11,8 @@ export default defineConfig({ 'quality-friendly-goldfish.ngrok-free.app', 'officially-powerful-goat.ngrok-free.app', '89f0-49-36-189-120.ngrok-free.app', - 'c8ee-49-36-191-130.ngrok-free.app' + 'c8ee-49-36-191-130.ngrok-free.app', + 'c76f1251c173.ngrok-free.app' ], }, build: {