Compare commits
2 Commits
8ec313e6d8
...
79151e2cf3
| Author | SHA1 | Date |
|---|---|---|
|
|
79151e2cf3 | |
|
|
8bb43d6ee5 |
|
|
@ -47,6 +47,7 @@ const App = () => {
|
|||
<Route element={<AccountTransactionWrapper />}>
|
||||
<Route path="/accounts" element={<Accounts />} />
|
||||
<Route path="/accounts/:accountId/transactions" element={<Account />} />
|
||||
<Route path="/transactions" element={<Account />} />
|
||||
<Route path="/accounts/:accountId/transactions/:transactionId" element={<TransactionView />} />
|
||||
<Route path="/transactions/:filterParam" element={<Transactions />} />
|
||||
</Route>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,17 @@ import { FaCheck } from "react-icons/fa";
|
|||
import Cards from "../home/Cards";
|
||||
import AddInviteModal from "./AddInviteModal";
|
||||
import { Bounce, toast } from "react-toastify";
|
||||
import {
|
||||
FaCheckCircle,
|
||||
FaClock,
|
||||
FaHourglassHalf,
|
||||
FaTasks,
|
||||
FaChevronDown,
|
||||
FaPlus,
|
||||
FaUserPlus,
|
||||
FaFilter,
|
||||
FaCalendarAlt
|
||||
} from 'react-icons/fa';
|
||||
|
||||
interface Transaction {
|
||||
id: number;
|
||||
|
|
@ -21,31 +32,152 @@ const formatDate = (timestamp: number) => {
|
|||
return new Date(timestamp * 1000).toLocaleString();
|
||||
};
|
||||
|
||||
|
||||
|
||||
const Account = () => {
|
||||
const { accountId } = useParams<{ accountId: string }>();
|
||||
const mapApiDataToCards = (data: any): CardData[] => {
|
||||
return [
|
||||
{
|
||||
title: 'Action Needed',
|
||||
count: data.actionNeeded || 0,
|
||||
icon: <FaTasks size={18} className="text-indigo-600" />,
|
||||
gradient: 'bg-gradient-to-br from-indigo-50 to-indigo-100',
|
||||
shadowColor: 'shadow-indigo-200/50',
|
||||
iconBg: 'bg-indigo-100 border-indigo-200',
|
||||
textColor: 'text-indigo-700',
|
||||
trend: 'up'
|
||||
},
|
||||
{
|
||||
title: 'Expiring Soon',
|
||||
count: data.expiring || 0,
|
||||
icon: <FaHourglassHalf size={18} className="text-rose-600" />,
|
||||
gradient: 'bg-gradient-to-br from-rose-50 to-rose-100',
|
||||
shadowColor: 'shadow-rose-200/50',
|
||||
iconBg: 'bg-rose-100 border-rose-200',
|
||||
textColor: 'text-rose-700',
|
||||
trend: 'up'
|
||||
},
|
||||
{
|
||||
title: 'Completed',
|
||||
count: data.completed || 0,
|
||||
icon: <FaCheckCircle size={18} className="text-teal-600" />,
|
||||
gradient: 'bg-gradient-to-br from-teal-50 to-teal-100',
|
||||
shadowColor: 'shadow-teal-200/50',
|
||||
iconBg: 'bg-teal-100 border-teal-200',
|
||||
textColor: 'text-teal-700',
|
||||
trend: 'up'
|
||||
},
|
||||
{
|
||||
title: 'Waiting for Others',
|
||||
count: data.waitingForOthers || 0,
|
||||
icon: <FaClock size={18} className="text-stone-600" />,
|
||||
gradient: 'bg-gradient-to-br from-blue-50 to-blue-100',
|
||||
shadowColor: 'shadow-blue-200/50',
|
||||
iconBg: 'bg-blue-100 border-blue-200',
|
||||
textColor: 'text-blue-700',
|
||||
trend: 'down'
|
||||
},
|
||||
{
|
||||
title: 'declined',
|
||||
count: data.declined || 0,
|
||||
icon: <FaClock size={18} className="text-purple-600" />,
|
||||
gradient: 'bg-gradient-to-br from-purple-50 to-purple-100',
|
||||
shadowColor: 'shadow-purple-200/50',
|
||||
iconBg: 'bg-purple-100 border-purple-200',
|
||||
textColor: 'text-purple-700',
|
||||
trend: 'down'
|
||||
},
|
||||
{
|
||||
title: 'cancelled',
|
||||
count: data.cancelled || 0,
|
||||
icon: <FaClock size={18} className="text-green-600" />,
|
||||
gradient: 'bg-gradient-to-br from-green-50 to-green-100',
|
||||
shadowColor: 'shadow-green-200/50',
|
||||
iconBg: 'bg-green-100 border-green-200',
|
||||
textColor: 'text-green-700',
|
||||
trend: 'down'
|
||||
},
|
||||
{
|
||||
title: 'draft',
|
||||
count: data.draft || 0,
|
||||
icon: <FaClock size={18} className="text-stone-600" />,
|
||||
gradient: 'bg-gradient-to-br from-stone-50 to-stone-100',
|
||||
shadowColor: 'shadow-stone-200/50',
|
||||
iconBg: 'bg-stone-100 border-stone-200',
|
||||
textColor: 'text-stone-700',
|
||||
trend: 'down'
|
||||
}
|
||||
// Add more cards here if needed
|
||||
];
|
||||
};
|
||||
// const { accountId } = useParams<{ accountId: string }>();
|
||||
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
||||
const [selectedTransactions, setSelectedTransactions] = useState<number[]>([]);
|
||||
const [accountsData, setAccountsData] = useState([])
|
||||
const [inviteLoading, setInviteLoading] = useState(false);
|
||||
const [cards, setCards] = useState<CardData[]>([]);
|
||||
const [accountID, setAccountID] = useState<number>()
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const getUserDetails = async () => {
|
||||
try {
|
||||
let res = await api.get('/api/user/details')
|
||||
let accountIDData = res?.data?.account?.id
|
||||
setAccountID(accountIDData)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getUserDetails()
|
||||
}, [])
|
||||
|
||||
|
||||
const getDashboardData = async () => {
|
||||
setIsLoading(true)
|
||||
const payload = {
|
||||
account: accountID
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await api.get('/api/dashboard', { params: payload });
|
||||
console.log("Dashboard Data:", res.data);
|
||||
const cardDataArray = mapApiDataToCards(res.data);
|
||||
setCards(cardDataArray);
|
||||
} catch (error) {
|
||||
console.error("Error fetching dashboard data:", error);
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (accountID) {
|
||||
getDashboardData()
|
||||
fetchTransactions();
|
||||
}
|
||||
}, [accountID]);
|
||||
|
||||
const handleOpenModal = () => {
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const fetchTransactions = async () => {
|
||||
console.log("fetchTransactions")
|
||||
try {
|
||||
const response = await api(`/api/accounts/${accountId}/transactions`);
|
||||
const response = await api(`/api/accounts/${accountID}/transactions`);
|
||||
setTransactions(response.data || []);
|
||||
} catch (error) {
|
||||
console.error("Error fetching transactions:", error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (accountId) fetchTransactions();
|
||||
}, [accountId]);
|
||||
// useEffect(() => {
|
||||
// if (accountID) fetchTransactions();
|
||||
// }, [accountID]);
|
||||
|
||||
const getAccountDetails = async () => {
|
||||
try {
|
||||
|
|
@ -65,7 +197,7 @@ const Account = () => {
|
|||
const navigate = useNavigate();
|
||||
|
||||
const handleTransactionClick = (id: number) => {
|
||||
navigate(`/accounts/${accountId}/transactions/${id}`);
|
||||
navigate(`/accounts/${accountID}/transactions/${id}`);
|
||||
};
|
||||
|
||||
const handleInvite = async (data: { i_accountName: number; email: string; role: string }) => {
|
||||
|
|
@ -98,9 +230,12 @@ const Account = () => {
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Cards type="transactions" onOpenModal={handleOpenModal} />
|
||||
<Cards type="transactions" onOpenModal={handleOpenModal} cards={cards}
|
||||
isLoading={isLoading}/>
|
||||
{transactions.length > 0 && (
|
||||
<div className="overflow-x-auto py-2">
|
||||
<table className="min-w-full text-sm border border-gray-200 rounded bg-white shadow-sm">
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
// import { useSelector } from 'react-redux';
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { FaSignOutAlt, FaUser } from "react-icons/fa";
|
||||
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 AccountNavProps {
|
||||
accountName?: string | null;
|
||||
transactionName?: string | null;
|
||||
|
|
@ -28,9 +31,20 @@ interface UserData {
|
|||
account: Account;
|
||||
}
|
||||
|
||||
interface AccountDataArray {
|
||||
accountId: number, accountName: string, role: string, status: boolean
|
||||
}
|
||||
|
||||
const AccountNav: React.FC<AccountNavProps> = ({ 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<AccountDataArray[]>([]);
|
||||
const [hasModalBeenClosed, setHasModalBeenClosed] = useState(false); // Track if modal was closed
|
||||
const dropdownRef = useRef(null);
|
||||
|
||||
const [user, setUser] = useState<UserData>({
|
||||
id: "",
|
||||
firstName: "",
|
||||
|
|
@ -44,6 +58,62 @@ const AccountNav: React.FC<AccountNavProps> = ({ accountName = null, transaction
|
|||
},
|
||||
});
|
||||
|
||||
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 () => {
|
||||
|
|
@ -74,22 +144,26 @@ const AccountNav: React.FC<AccountNavProps> = ({ accountName = null, transaction
|
|||
// Add more routes here if needed
|
||||
};
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
// const [open, setOpen] = useState(false);
|
||||
// const dropdownRef = useRef<HTMLDivElement>(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() => {
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
let res = await api.post('/api/auth/logout')
|
||||
if (res.status === 200) {
|
||||
|
|
@ -101,12 +175,37 @@ const AccountNav: React.FC<AccountNavProps> = ({ accountName = null, transaction
|
|||
}
|
||||
};
|
||||
|
||||
// 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 = () => (
|
||||
<div className="animate-pulse px-4 py-3 border-b border-gray-100 last:border-b-0">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-8 h-8 bg-gray-200 rounded-full"></div>
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
|
||||
<div className="h-3 bg-gray-200 rounded w-1/2"></div>
|
||||
</div>
|
||||
<div className="w-16 h-6 bg-gray-200 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
return (
|
||||
<header className="w-full px-6 py-2 flex justify-between items-center bg-white border-b rounded-sm border-gray-200">
|
||||
<nav className="text-md font-medium text-gray-800 capitalize flex items-center flex-wrap">
|
||||
|
|
@ -137,10 +236,6 @@ const AccountNav: React.FC<AccountNavProps> = ({ accountName = null, transaction
|
|||
)}
|
||||
</nav>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
|
||||
{/* User Dropdown */}
|
||||
|
|
@ -148,7 +243,7 @@ const AccountNav: React.FC<AccountNavProps> = ({ accountName = null, transaction
|
|||
{/* Avatar Button */}
|
||||
<button
|
||||
className="flex items-center space-x-2 p-1 cursor-pointer rounded-lg transition-colors"
|
||||
onClick={() => setOpen(!open)}
|
||||
onClick={handleMainDropdownToggle}
|
||||
aria-expanded={open}
|
||||
aria-haspopup="true"
|
||||
>
|
||||
|
|
@ -189,6 +284,63 @@ const AccountNav: React.FC<AccountNavProps> = ({ accountName = null, transaction
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Account List Section */}
|
||||
<div className="border-b border-gray-100">
|
||||
<button
|
||||
onClick={handleAccountsClick}
|
||||
className="w-full flex items-center justify-between px-4 py-3 text-sm text-gray-700 hover:bg-gray-50 transition-colors cursor-pointer"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<FaBuilding className="mr-3 text-gray-500" />
|
||||
<span>Switch Account</span>
|
||||
</div>
|
||||
<FaChevronDown className={`text-gray-400 transition-transform ${accountsOpen ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
|
||||
{/* Accounts Dropdown */}
|
||||
{accountsOpen && (
|
||||
<div className="bg-gray-50 max-h-64 overflow-y-auto">
|
||||
{loadingAccounts ? (
|
||||
<div className="space-y-0">
|
||||
<ShimmerRow />
|
||||
<ShimmerRow />
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-gray-200">
|
||||
{accounts.length > 0 ? (accounts?.map((account) => (
|
||||
<button
|
||||
key={account.accountId}
|
||||
// onClick={() => handleAccountSwitch(account)}
|
||||
className="w-full px-4 py-3 hover:bg-white transition-colors cursor-pointer text-left"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-gray-200 to-gray-400 flex items-center justify-center text-white font-semibold text-xs">
|
||||
{account.accountName.slice(0, 2).toUpperCase()}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="font-medium text-gray-900 text-sm">{account.accountName}</span>
|
||||
{account.status && (
|
||||
<FaCheck className="text-green-500 text-xs" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-600 truncate">{account.company}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 bg-gray-200 px-2 py-1 rounded">
|
||||
{account.role}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))) : <span className='text-gray-400 font-medium text-[12px] flex justify-center p-3'>No Account...</span>}
|
||||
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Logout Section */}
|
||||
<div className="border-t border-gray-100 ">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ const getStatusLabel = (
|
|||
};
|
||||
|
||||
const Accounts = () => {
|
||||
|
||||
const [allAccounts, setAllAccounts] = useState<AccountData[]>([]);
|
||||
const [selectedAccounts, setSelectedAccounts] = useState<number[]>([]);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
|
@ -122,7 +123,7 @@ const Accounts = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Cards type="account" onOpenModal={handleOpenModal} />
|
||||
{/* <Cards type="account" onOpenModal={handleOpenModal} /> */}
|
||||
{allAccounts.length > 0 && (
|
||||
<div className="overflow-x-auto py-2">
|
||||
<table className="min-w-full text-sm border border-gray-200 rounded bg-white shadow-sm">
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { IoSettingsOutline } from "react-icons/io5";
|
|||
|
||||
import axios from 'axios';
|
||||
import { MdManageAccounts } from 'react-icons/md';
|
||||
import { GrTransaction } from 'react-icons/gr';
|
||||
|
||||
type SideBarProps = {
|
||||
showSideBar: boolean;
|
||||
|
|
@ -24,7 +25,8 @@ const menuPathMap: MenuPathMap = {
|
|||
'/': 'Home',
|
||||
'/templates': 'Templates',
|
||||
'/s/contact': 'Contact',
|
||||
'/accounts': 'Accounts',
|
||||
// '/accounts': 'Accounts',
|
||||
'transactions' : "Account"
|
||||
};
|
||||
|
||||
const SideBar: React.FC<SideBarProps> = ({ showSideBar, setShowSideBar }) => {
|
||||
|
|
@ -61,7 +63,8 @@ const SideBar: React.FC<SideBarProps> = ({ showSideBar, setShowSideBar }) => {
|
|||
<SendForSign showSideBar={showSideBar} />
|
||||
<NavLink to="/" icon={<FaHome />} label="Home" showSideBar={showSideBar} menuPathMap={menuPathMap} />
|
||||
<NavLink to="/templates" icon={<HiTemplate />} label="Templates" showSideBar={showSideBar} menuPathMap={menuPathMap} />
|
||||
<NavLink to="/accounts" icon={<MdManageAccounts />} label="Accounts" showSideBar={showSideBar} menuPathMap={menuPathMap} />
|
||||
{/* <NavLink to="/accounts" icon={<MdManageAccounts />} label="Accounts" showSideBar={showSideBar} menuPathMap={menuPathMap} /> */}
|
||||
<NavLink to="transactions" icon={<GrTransaction />} label="Transactions" showSideBar={showSideBar} menuPathMap={menuPathMap} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -24,54 +24,30 @@ interface CardData {
|
|||
trend?: 'up' | 'down' | 'stable';
|
||||
}
|
||||
|
||||
// interface CardData {
|
||||
// title: string;
|
||||
// count: number;
|
||||
// icon: React.ReactNode;
|
||||
// gradient: string;
|
||||
// shadowColor: string;
|
||||
// iconBg: string;
|
||||
// textColor: string;
|
||||
// trend: 'up' | 'down';
|
||||
// }
|
||||
|
||||
interface CardsProps {
|
||||
type?: 'dashboard' | 'account' | 'transactions';
|
||||
onOpenModal?: () => void;
|
||||
dashboardData: any;
|
||||
fromDate: string;
|
||||
toDate: string;
|
||||
onDateRangeChange: (range: { from: string; to: string }) => void;
|
||||
cards: CardData[];
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
const cards: CardData[] = [
|
||||
{
|
||||
title: 'Action Required',
|
||||
count: 12,
|
||||
icon: <FaTasks size={18} className="text-indigo-600" />,
|
||||
gradient: 'bg-gradient-to-br from-indigo-50 to-indigo-100',
|
||||
shadowColor: 'shadow-indigo-200/50',
|
||||
iconBg: 'bg-indigo-100 border-indigo-200',
|
||||
textColor: 'text-indigo-700',
|
||||
trend: 'up'
|
||||
},
|
||||
|
||||
{
|
||||
title: 'Expiring Soon',
|
||||
count: 5,
|
||||
icon: <FaHourglassHalf size={18} className="text-rose-600" />,
|
||||
gradient: 'bg-gradient-to-br from-rose-50 to-rose-100',
|
||||
shadowColor: 'shadow-rose-200/50',
|
||||
iconBg: 'bg-rose-100 border-rose-200',
|
||||
textColor: 'text-rose-700',
|
||||
trend: 'up'
|
||||
},
|
||||
{
|
||||
title: 'Completed',
|
||||
count: 27,
|
||||
icon: <FaCheckCircle size={18} className="text-teal-600" />,
|
||||
gradient: 'bg-gradient-to-br from-teal-50 to-teal-100',
|
||||
shadowColor: 'shadow-teal-200/50',
|
||||
iconBg: 'bg-teal-100 border-teal-200',
|
||||
textColor: 'text-teal-700',
|
||||
trend: 'up'
|
||||
},
|
||||
{
|
||||
title: 'Waiting for Others',
|
||||
count: 8,
|
||||
icon: <FaClock size={18} className="text-stone-600" />,
|
||||
gradient: 'bg-gradient-to-br from-stone-50 to-stone-100',
|
||||
shadowColor: 'shadow-stone-200/50',
|
||||
iconBg: 'bg-stone-100 border-stone-200',
|
||||
textColor: 'text-stone-700',
|
||||
trend: 'down'
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
const AccountData: CardData[] = [
|
||||
{
|
||||
|
|
@ -120,23 +96,53 @@ const AccountData: CardData[] = [
|
|||
const filters = ['Last 7 Days', 'Last 30 Days', 'Last 6 Months', 'This Year'];
|
||||
const accounts = ['All Accounts', 'Account 1', 'Account 2', 'Account 3'];
|
||||
|
||||
const Cards: React.FC<CardsProps> = ({ type = 'dashboard', onOpenModal }) => {
|
||||
const Cards: React.FC<CardsProps> = ({ type = 'dashboard', onOpenModal, onDateRangeChange, cards, isLoading }) => {
|
||||
const [selectedFilter, setSelectedFilter] = useState<string>('Last 7 Days');
|
||||
const [selectedAccount, setSelectedAccount] = useState<string>('All Accounts');
|
||||
const [showCards, setShowCards] = useState<CardData[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
// const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
|
||||
// useEffect(() => {
|
||||
// setIsLoading(true);
|
||||
// setTimeout(() => {
|
||||
// if (type === 'account') {
|
||||
// setShowCards(AccountData);
|
||||
// } else {
|
||||
// setShowCards(cards);
|
||||
// }
|
||||
// setIsLoading(false);
|
||||
// }, 500);
|
||||
// }, [type]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
setTimeout(() => {
|
||||
if (type === 'account') {
|
||||
setShowCards(AccountData);
|
||||
} else {
|
||||
setShowCards(cards);
|
||||
|
||||
if (type === 'dashboard') {
|
||||
const now = new Date();
|
||||
let fromDate = new Date();
|
||||
|
||||
switch (selectedFilter) {
|
||||
case 'Last 7 Days':
|
||||
fromDate.setDate(now.getDate() - 7);
|
||||
break;
|
||||
case 'Last 30 Days':
|
||||
fromDate.setDate(now.getDate() - 30);
|
||||
break;
|
||||
case 'Last 6 Months':
|
||||
fromDate.setMonth(now.getMonth() - 6);
|
||||
break;
|
||||
case 'This Year':
|
||||
fromDate = new Date(now.getFullYear(), 0, 1);
|
||||
break;
|
||||
default:
|
||||
fromDate.setDate(now.getDate() - 7);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}, 500);
|
||||
}, [type]);
|
||||
|
||||
const from = fromDate.toISOString().split('T')[0];
|
||||
const to = now.toISOString().split('T')[0];
|
||||
|
||||
onDateRangeChange({ from, to });
|
||||
}
|
||||
}, [selectedFilter]);
|
||||
|
||||
const getButtonConfig = () => {
|
||||
switch (type) {
|
||||
|
|
@ -182,122 +188,141 @@ const Cards: React.FC<CardsProps> = ({ type = 'dashboard', onOpenModal }) => {
|
|||
);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="w-full px-6 py-8">
|
||||
// if (isLoading) {
|
||||
// return (
|
||||
// <div className="w-full [h-50vh] px-6 py-8">
|
||||
// <div className="animate-pulse">
|
||||
// <div className="h-8 bg-gray-200 rounded w-32 mb-8"></div>
|
||||
// <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
// {[...Array(7)].map((_, i) => (
|
||||
// <div key={i} className="h-32 bg-gray-200 rounded-xl"></div>
|
||||
// ))}
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
return (
|
||||
<div className="w-full px-4 py-3 mt-2 rounded-md">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col lg:flex-row items-start lg:items-center justify-between mb-2 gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{/* <div className="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center shadow-lg">
|
||||
<FaFilter className="text-white text-lg" />
|
||||
</div> */}
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-800">Overview</h2>
|
||||
{/* <p className="text-gray-600 text-sm">Monitor performance metrics</p> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{/* Cards */}
|
||||
{isLoading ? <div className="w-full [h-50vh] px-6 py-8">
|
||||
<div className="animate-pulse">
|
||||
<div className="h-8 bg-gray-200 rounded w-32 mb-8"></div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{[...Array(4)].map((_, i) => (
|
||||
{[...Array(7)].map((_, i) => (
|
||||
<div key={i} className="h-32 bg-gray-200 rounded-xl"></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div> :
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4">
|
||||
{cards.map((card, index) => (
|
||||
<div
|
||||
key={card.title}
|
||||
className={`group relative overflow-hidden rounded-xl ${card.gradient} border border-white/40 p-4 transition-all duration-300 hover:scale-105 hover:shadow-lg ${card.shadowColor} cursor-pointer backdrop-blur-sm`}
|
||||
style={{
|
||||
animationDelay: `${index * 100}ms`
|
||||
}}
|
||||
>
|
||||
{/* Background Pattern */}
|
||||
<div className="absolute inset-0 opacity-5">
|
||||
<div className="absolute -top-6 -right-6 w-12 h-12 rounded-full bg-current"></div>
|
||||
<div className="absolute -bottom-6 -left-6 w-10 h-10 rounded-full bg-current"></div>
|
||||
</div>
|
||||
|
||||
return (
|
||||
<div className="w-full px-6 py-6 bg-gradient-to-br from-slate-50 to-slate-100 mt-2 rounded-t-md">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col lg:flex-row items-start lg:items-center justify-between mb-8 gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center shadow-lg">
|
||||
<FaFilter className="text-white text-lg" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-800">Overview</h2>
|
||||
<p className="text-gray-600 text-sm">Monitor performance metrics</p>
|
||||
</div>
|
||||
{/* Content */}
|
||||
<div className="relative z-10">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className={`p-2 rounded-lg border ${card.iconBg} group-hover:scale-110 transition-transform duration-200`}>
|
||||
{card.icon}
|
||||
</div>
|
||||
{renderTrendIndicator(card.trend, card.percentage)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-xs font-medium text-gray-600 group-hover:text-gray-700 transition-colors">
|
||||
{card.title}
|
||||
</h3>
|
||||
<div className="flex items-baseline gap-1">
|
||||
<p className={`text-xl font-bold ${card.textColor} group-hover:scale-105 transition-transform duration-200`}>
|
||||
{card.count}
|
||||
</p>
|
||||
<span className="text-[10px] text-gray-500">items</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hover Effect */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-3 w-full lg:w-auto">
|
||||
{/* Account Selector */}
|
||||
<div className="relative">
|
||||
<select
|
||||
value={selectedAccount}
|
||||
onChange={(e) => setSelectedAccount(e.target.value)}
|
||||
className="appearance-none text-sm text-gray-700 border border-gray-300 rounded-xl px-4 py-3 pr-10 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent bg-white shadow-sm hover:shadow-md transition-all cursor-pointer min-w-[140px]"
|
||||
>
|
||||
{accounts.map((account) => (
|
||||
<option key={account} value={account}>{account}</option>
|
||||
))}
|
||||
</select>
|
||||
<FaChevronDown className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 text-xs pointer-events-none" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
{/* Date Filter */}
|
||||
<div className="relative">
|
||||
<select
|
||||
value={selectedFilter}
|
||||
onChange={(e) => setSelectedFilter(e.target.value)}
|
||||
className="appearance-none text-sm text-gray-700 border border-gray-300 rounded-xl px-4 py-3 pr-10 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent bg-white shadow-sm hover:shadow-md transition-all cursor-pointer min-w-[140px]"
|
||||
>
|
||||
{filters.map((filter) => (
|
||||
<option key={filter} value={filter}>{filter}</option>
|
||||
))}
|
||||
</select>
|
||||
<FaCalendarAlt className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 text-xs pointer-events-none" />
|
||||
</div>
|
||||
export default Cards;
|
||||
|
||||
{/* Action Button */}
|
||||
|
||||
|
||||
|
||||
{/* <div className="flex flex-wrap gap-3 w-full lg:w-auto">
|
||||
{type === 'dashboard' && <>
|
||||
Account Selector
|
||||
<div className="relative">
|
||||
<select
|
||||
value={selectedAccount}
|
||||
onChange={(e) => setSelectedAccount(e.target.value)}
|
||||
className="appearance-none text-sm text-gray-700 border border-gray-300 rounded-xl px-4 py-3 pr-10 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent bg-white shadow-sm hover:shadow-md transition-all cursor-pointer min-w-[140px]"
|
||||
>
|
||||
{accounts.map((account) => (
|
||||
<option key={account} value={account}>{account}</option>
|
||||
))}
|
||||
</select>
|
||||
<FaChevronDown className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 text-xs pointer-events-none" />
|
||||
</div>
|
||||
|
||||
Date Filter
|
||||
<div className="relative">
|
||||
<select
|
||||
value={selectedFilter}
|
||||
onChange={(e) => setSelectedFilter(e.target.value)}
|
||||
className="appearance-none text-sm text-gray-700 border border-gray-300 rounded-xl px-4 py-3 pr-10 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent bg-white shadow-sm hover:shadow-md transition-all cursor-pointer min-w-[140px]"
|
||||
>
|
||||
{filters.map((filter) => (
|
||||
<option key={filter} value={filter}>{filter}</option>
|
||||
))}
|
||||
</select>
|
||||
<FaCalendarAlt className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 text-xs pointer-events-none" />
|
||||
</div>
|
||||
</>}
|
||||
|
||||
Action Button
|
||||
{buttonConfig && (
|
||||
<button
|
||||
className={`flex items-center gap-2 px-6 py-3 text-sm font-semibold text-white rounded-xl transition-all duration-200 shadow-lg hover:shadow-xl transform hover:scale-105 active:scale-95 cursor-pointer ${buttonConfig.gradient}`}
|
||||
className={`flex items-center gap-1 px-3 py-3 text-sm font-semibold text-white rounded-xl transition-all duration-200 shadow-lg hover:shadow-xl transform hover:scale-105 active:scale-95 cursor-pointer ${buttonConfig.gradient}`}
|
||||
onClick={onOpenModal}
|
||||
>
|
||||
{buttonConfig.icon}
|
||||
{buttonConfig.text}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-6">
|
||||
{showCards.map((card, index) => (
|
||||
<div
|
||||
key={card.title}
|
||||
className={`group relative overflow-hidden rounded-2xl ${card.gradient} border border-white/50 p-6 transition-all duration-300 hover:scale-105 hover:shadow-xl ${card.shadowColor} cursor-pointer backdrop-blur-sm`}
|
||||
style={{
|
||||
animationDelay: `${index * 100}ms`
|
||||
}}
|
||||
>
|
||||
{/* Background Pattern */}
|
||||
<div className="absolute inset-0 opacity-5">
|
||||
<div className="absolute -top-10 -right-10 w-20 h-20 rounded-full bg-current"></div>
|
||||
<div className="absolute -bottom-10 -left-10 w-16 h-16 rounded-full bg-current"></div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative z-10">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className={`p-3 rounded-xl border ${card.iconBg} group-hover:scale-110 transition-transform duration-200`}>
|
||||
{card.icon}
|
||||
</div>
|
||||
{renderTrendIndicator(card.trend, card.percentage)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-medium text-gray-600 group-hover:text-gray-700 transition-colors">
|
||||
{card.title}
|
||||
</h3>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<p className={`text-3xl font-bold ${card.textColor} group-hover:scale-105 transition-transform duration-200`}>
|
||||
{card.count}
|
||||
</p>
|
||||
<span className="text-xs text-gray-500">items</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hover Effect */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Cards;
|
||||
</div> */}
|
||||
|
|
@ -1,24 +1,193 @@
|
|||
import React from 'react'
|
||||
import HomeTopNav from './HomeTopNav'
|
||||
import UploadDoc from './UploadDoc'
|
||||
import FilterSort from './FilterSort'
|
||||
import ShowDocs from './ShowDocs'
|
||||
import Cards from './Cards'
|
||||
import StartSection from './StartSection'
|
||||
import AccountNav from '../accounts/AccountNav'
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import AccountNav from '../accounts/AccountNav';
|
||||
import Cards from './Cards';
|
||||
import StartSection from './StartSection';
|
||||
import api from '../../config/api';
|
||||
import {
|
||||
FaCheckCircle,
|
||||
FaClock,
|
||||
FaHourglassHalf,
|
||||
FaTasks,
|
||||
FaChevronDown,
|
||||
FaPlus,
|
||||
FaUserPlus,
|
||||
FaFilter,
|
||||
FaCalendarAlt
|
||||
} from 'react-icons/fa';
|
||||
|
||||
const Home = () => {
|
||||
return (
|
||||
<>
|
||||
{/* <HomeTopNav /> */}
|
||||
<AccountNav/>
|
||||
<Cards />
|
||||
<StartSection />
|
||||
{/* <UploadDoc /> */}
|
||||
{/* <FilterSort /> */}
|
||||
{/* <ShowDocs /> */}
|
||||
</>
|
||||
)
|
||||
interface CardData {
|
||||
title: string;
|
||||
count: number;
|
||||
icon: JSX.Element;
|
||||
gradient: string;
|
||||
shadowColor: string;
|
||||
iconBg: string;
|
||||
textColor: string;
|
||||
percentage?: number;
|
||||
trend?: 'up' | 'down' | 'stable';
|
||||
}
|
||||
|
||||
export default Home
|
||||
const Home = () => {
|
||||
const mapApiDataToCards = (data: any): CardData[] => {
|
||||
return [
|
||||
{
|
||||
title: 'Action Needed',
|
||||
count: data.actionNeeded || 0,
|
||||
icon: <FaTasks size={18} className="text-indigo-600" />,
|
||||
gradient: 'bg-gradient-to-br from-indigo-50 to-indigo-100',
|
||||
shadowColor: 'shadow-indigo-200/50',
|
||||
iconBg: 'bg-indigo-100 border-indigo-200',
|
||||
textColor: 'text-indigo-700',
|
||||
trend: 'up'
|
||||
},
|
||||
{
|
||||
title: 'Expiring Soon',
|
||||
count: data.expiring || 0,
|
||||
icon: <FaHourglassHalf size={18} className="text-rose-600" />,
|
||||
gradient: 'bg-gradient-to-br from-rose-50 to-rose-100',
|
||||
shadowColor: 'shadow-rose-200/50',
|
||||
iconBg: 'bg-rose-100 border-rose-200',
|
||||
textColor: 'text-rose-700',
|
||||
trend: 'up'
|
||||
},
|
||||
{
|
||||
title: 'Completed',
|
||||
count: data.completed || 0,
|
||||
icon: <FaCheckCircle size={18} className="text-teal-600" />,
|
||||
gradient: 'bg-gradient-to-br from-teal-50 to-teal-100',
|
||||
shadowColor: 'shadow-teal-200/50',
|
||||
iconBg: 'bg-teal-100 border-teal-200',
|
||||
textColor: 'text-teal-700',
|
||||
trend: 'up'
|
||||
},
|
||||
{
|
||||
title: 'Waiting for Others',
|
||||
count: data.waitingForOthers || 0,
|
||||
icon: <FaClock size={18} className="text-stone-600" />,
|
||||
gradient: 'bg-gradient-to-br from-blue-50 to-blue-100',
|
||||
shadowColor: 'shadow-blue-200/50',
|
||||
iconBg: 'bg-blue-100 border-blue-200',
|
||||
textColor: 'text-blue-700',
|
||||
trend: 'down'
|
||||
},
|
||||
// {
|
||||
// title: 'declined',
|
||||
// count: data.declined || 0,
|
||||
// icon: <FaClock size={18} className="text-purple-600" />,
|
||||
// gradient: 'bg-gradient-to-br from-purple-50 to-purple-100',
|
||||
// shadowColor: 'shadow-purple-200/50',
|
||||
// iconBg: 'bg-purple-100 border-purple-200',
|
||||
// textColor: 'text-purple-700',
|
||||
// trend: 'down'
|
||||
// },
|
||||
// {
|
||||
// title: 'cancelled',
|
||||
// count: data.cancelled || 0,
|
||||
// icon: <FaClock size={18} className="text-green-600" />,
|
||||
// gradient: 'bg-gradient-to-br from-green-50 to-green-100',
|
||||
// shadowColor: 'shadow-green-200/50',
|
||||
// iconBg: 'bg-green-100 border-green-200',
|
||||
// textColor: 'text-green-700',
|
||||
// trend: 'down'
|
||||
// },
|
||||
// {
|
||||
// title: 'draft',
|
||||
// count: data.draft || 0,
|
||||
// icon: <FaClock size={18} className="text-stone-600" />,
|
||||
// gradient: 'bg-gradient-to-br from-stone-50 to-stone-100',
|
||||
// shadowColor: 'shadow-stone-200/50',
|
||||
// iconBg: 'bg-stone-100 border-stone-200',
|
||||
// textColor: 'text-stone-700',
|
||||
// trend: 'down'
|
||||
// }
|
||||
// Add more cards here if needed
|
||||
];
|
||||
};
|
||||
|
||||
const [dashboardData, setDashboardData] = useState<any>(null);
|
||||
const [fromDate, setFromDate] = useState<string>(''); // ISO string
|
||||
const [toDate, setToDate] = useState<string>(''); // ISO string
|
||||
const [cards, setCards] = useState<CardData[]>([]);
|
||||
const [accountID, setAccountID] = useState<number>()
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
console.log("cards", cards)
|
||||
|
||||
const getUserDetails = async () => {
|
||||
try {
|
||||
let res = await api.get('/api/user/details')
|
||||
let accountIDData = res?.data?.account?.id
|
||||
setAccountID(accountIDData)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getUserDetails()
|
||||
}, [])
|
||||
|
||||
const getDashboardData = async (from: string, to: string) => {
|
||||
setIsLoading(true)
|
||||
const payload = {
|
||||
fromDate: from, // already ISO string
|
||||
toDate: to,
|
||||
account : accountID
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await api.get('/api/dashboard', { params: payload });
|
||||
setDashboardData(res.data);
|
||||
console.log("Dashboard Data:", res.data);
|
||||
const cardDataArray = mapApiDataToCards(res.data);
|
||||
setCards(cardDataArray);
|
||||
} catch (error) {
|
||||
console.error("Error fetching dashboard data:", error);
|
||||
}finally{
|
||||
setIsLoading(false)
|
||||
}
|
||||
};
|
||||
|
||||
const handleDateRangeChange = useCallback(async (range: { from: string; to: string }) => {
|
||||
const isoFrom = new Date(range.from).toISOString();
|
||||
const isoTo = new Date(range.to).toISOString();
|
||||
setFromDate(isoFrom);
|
||||
setToDate(isoTo);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (fromDate && toDate && accountID) {
|
||||
getDashboardData(fromDate, toDate);
|
||||
}
|
||||
}, [fromDate, toDate , accountID]);
|
||||
|
||||
|
||||
|
||||
|
||||
// Set default range on mount (last 7 days)
|
||||
useEffect(() => {
|
||||
const now = new Date();
|
||||
const from = new Date();
|
||||
from.setDate(now.getDate() - 7);
|
||||
|
||||
setFromDate(from.toISOString());
|
||||
setToDate(now.toISOString());
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AccountNav />
|
||||
<Cards
|
||||
onDateRangeChange={handleDateRangeChange}
|
||||
dashboardData={dashboardData}
|
||||
fromDate={fromDate}
|
||||
toDate={toDate}
|
||||
cards={cards}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<StartSection />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ const StartSection = () => {
|
|||
|
||||
return (
|
||||
<section className="px-4 py-3 w-full">
|
||||
<h2 className="text-lg font-semibold text-gray-800 mb-6">Get Started</h2>
|
||||
<h2 className="text-lg font-semibold text-gray-800 mb-2">Get Started</h2>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{cards.map((card, index) => (
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { fetchRecipients } from '../../../store/slices/selectedRecipientsSlice';
|
|||
import { fetchDocuments } from '../../../store/slices/documentSlice';
|
||||
import { FaQuestionCircle } from 'react-icons/fa';
|
||||
import typeToIconMap from '../../../helper/iconMap';
|
||||
import Loader from '../../dashboard/Loader';
|
||||
|
||||
export interface DroppedItemsObj {
|
||||
[key: string]: DroppedItem[]
|
||||
|
|
@ -210,23 +211,24 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
|
|||
"dimensions": dropTag.dimensions,
|
||||
"type": dropTag.type.toUpperCase(),
|
||||
// "subType": (dropTag.subType ? dropTag.subType.toUpperCase() : null),
|
||||
validation: {
|
||||
"minLength": 0,
|
||||
"maxLength": 0,
|
||||
"error": ("error" in dropTag && dropTag.error) ? `${dropTag.error}`.toUpperCase() : "error",
|
||||
"regex": "regex" in dropTag ? dropTag.regex : []
|
||||
},
|
||||
// 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",
|
||||
"regex": "regex" in dropTag ? dropTag.regex : []
|
||||
// }
|
||||
,
|
||||
"documentId": dropTag.documentId,
|
||||
"options": ("options" in dropTag) ? dropTag.options : [],
|
||||
"pageIndex": dropTag.pageIndex,
|
||||
"recipientId": dropTag.recipientId,
|
||||
"styles": "styles" in dropTag ? dropTag.styles : {
|
||||
"font": "Great Vibes",
|
||||
"font": "",
|
||||
"italic": false,
|
||||
"bold": false,
|
||||
"underline": false,
|
||||
"color": "black",
|
||||
"textSize": 14.0
|
||||
"textSize": 0
|
||||
},
|
||||
// "required": (dropTag.type.toUpperCase() !== "SIGNATURE" && dropTag.type.toUpperCase() !== "INITIALS") ? dropTag.required : true,
|
||||
"required": "required" in dropTag ? dropTag.required : true,
|
||||
|
|
@ -288,6 +290,7 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
|
|||
|
||||
// HANDLE DROP - START
|
||||
const handleDrop = (event: React.DragEvent<HTMLDivElement>, docId: string, documentId: number, pageIndex: number) => {
|
||||
console.log("// HANDLE DROP - START")
|
||||
event.preventDefault();
|
||||
|
||||
const data = window.dragElemData;
|
||||
|
|
@ -439,6 +442,7 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
|
|||
|
||||
// HANDLE EXPAND TAG - START
|
||||
const handleExpandTag = (event: React.MouseEvent<HTMLDivElement>, key: string, index: number) => {
|
||||
console.log("HANDLE EXPAND TAG - START")
|
||||
event.preventDefault(); // Prevent text selection issues
|
||||
|
||||
const expandingElemIcon = event.currentTarget;
|
||||
|
|
@ -620,13 +624,58 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
|
|||
// }
|
||||
|
||||
|
||||
// NEW DIRECTIONAL MOVEMENT HANDLER - START
|
||||
const handleDirectionalMove = (direction: 'up' | 'down' | 'left' | 'right', stepSize: number = 5) => {
|
||||
if (!selectedTag) return;
|
||||
|
||||
const { key, index } = selectedTag;
|
||||
const currentItem = droppedItems[key]?.[index];
|
||||
|
||||
if (!currentItem) return;
|
||||
|
||||
let newLeft = currentItem.dimensions.left;
|
||||
let newTop = currentItem.dimensions.top;
|
||||
|
||||
switch (direction) {
|
||||
case 'up':
|
||||
newTop = Math.max(0, newTop - stepSize);
|
||||
break;
|
||||
case 'down':
|
||||
newTop = newTop + stepSize;
|
||||
break;
|
||||
case 'left':
|
||||
newLeft = Math.max(0, newLeft - stepSize);
|
||||
break;
|
||||
case 'right':
|
||||
newLeft = newLeft + stepSize;
|
||||
break;
|
||||
}
|
||||
|
||||
setDroppedItems({
|
||||
...droppedItems,
|
||||
[key]: Array.isArray(droppedItems[key])
|
||||
? droppedItems[key].map((item, i) =>
|
||||
i === index
|
||||
? { ...item, dimensions: { ...item.dimensions, left: newLeft, top: newTop }, update: true }
|
||||
: item
|
||||
)
|
||||
: [],
|
||||
});
|
||||
};
|
||||
|
||||
// Create a wrapper that matches DirectionalControls' expected signature
|
||||
const directionalControlsHandler = (direction: 'up' | 'down' | 'left' | 'right') => {
|
||||
handleDirectionalMove(direction, 5); // Use default stepSize of 5
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Center Doc Container - START */}
|
||||
<div id="doc-con" className='zoom-con col-span-8 bg-slate-100 h-full w-full overflow-auto scroll-smooth'>
|
||||
|
||||
{
|
||||
selectedDocuments.length > 0 && selectedDocuments.map((doc, indxDoc) => {
|
||||
selectedDocuments.length > 0 ? selectedDocuments.map((doc, indxDoc) => {
|
||||
return (
|
||||
<div key={indxDoc} className='flex flex-col'>
|
||||
<div className='py-2 text-sm'>
|
||||
|
|
@ -750,13 +799,18 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
|
|||
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}) : <Loader />
|
||||
}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{selectedTag && <RightSideBar selectedTag={droppedItems[selectedTag.key][selectedTag.index]} setDroppedItems={setDroppedItems} selTagKey={selectedTag.key} selTagIndx={selectedTag.index} />}
|
||||
{selectedTag && <RightSideBar selectedTag={droppedItems[selectedTag.key][selectedTag.index]} setDroppedItems={setDroppedItems} selTagKey={selectedTag.key} selTagIndx={selectedTag.index} DirectionalControlsProps={{
|
||||
onMove: directionalControlsHandler,
|
||||
disabled: false,
|
||||
stepSize: 5,
|
||||
className: ''
|
||||
}} />}
|
||||
{/* Center Doc Container - END */}
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,120 @@
|
|||
import React, { useEffect, useRef } from 'react';
|
||||
import { FaArrowUp, FaArrowDown, FaArrowLeft, FaArrowRight, FaRegCircle } from 'react-icons/fa';
|
||||
|
||||
interface DirectionalControlsProps {
|
||||
onMove: (direction: 'up' | 'down' | 'left' | 'right', stepSize?: number) => void;
|
||||
disabled?: boolean;
|
||||
stepSize?: number;
|
||||
className?: string;
|
||||
selectedType: {
|
||||
left?: number;
|
||||
top?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
}
|
||||
|
||||
const DirectionalControls: React.FC<DirectionalControlsProps> = ({
|
||||
onMove,
|
||||
disabled = false,
|
||||
stepSize = 1,
|
||||
className = '',
|
||||
selectedType,
|
||||
}) => {
|
||||
const onMoveRef = useRef(onMove);
|
||||
|
||||
useEffect(() => {
|
||||
onMoveRef.current = onMove;
|
||||
}, [onMove]);
|
||||
|
||||
const handleMove = (direction: 'up' | 'down' | 'left' | 'right') => {
|
||||
if (!disabled) {
|
||||
onMoveRef.current(direction, stepSize);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (disabled) return;
|
||||
|
||||
switch (event.key) {
|
||||
case 'ArrowUp':
|
||||
event.preventDefault();
|
||||
handleMove('up');
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
handleMove('down');
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
event.preventDefault();
|
||||
handleMove('left');
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
event.preventDefault();
|
||||
handleMove('right');
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [disabled, stepSize]);
|
||||
|
||||
const buttonClass = `
|
||||
p-1 rounded-sm transition-all duration-200 border shadow-sm text-[10px]
|
||||
${disabled
|
||||
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||
: 'bg-white text-gray-600 hover:bg-blue-100 active:bg-blue-200 cursor-pointer'}
|
||||
flex items-center justify-center w-6 h-6
|
||||
`;
|
||||
|
||||
|
||||
const { top = 0, left = 0, width = 0, height = 0 } = selectedType || {};
|
||||
|
||||
const right = left + width;
|
||||
const bottom = top + height;
|
||||
|
||||
return (
|
||||
<div className={`grid grid-cols-3 grid-rows-1 text-[10px] text-gray-700 ${className}`}>
|
||||
{/* Top Row */}
|
||||
<div></div>
|
||||
<div className="flex flex-col items-center">
|
||||
<button onClick={() => handleMove('up')} disabled={disabled} className={buttonClass} title="Move up" type="button">
|
||||
<FaArrowUp size={12} />
|
||||
</button>
|
||||
<span className="mt-1">Top: {Math.round(top)}px</span>
|
||||
</div>
|
||||
<div></div>
|
||||
|
||||
{/* Middle Row */}
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<button onClick={() => handleMove('left')} disabled={disabled} className={buttonClass} title="Move left" type="button">
|
||||
<FaArrowLeft size={12} />
|
||||
</button>
|
||||
<span className="mt-1">Left: {Math.round(left)}px</span>
|
||||
</div>
|
||||
|
||||
<div className="w-8 h-8 flex items-center justify-center text-xs text-gray-400 ps-5"><FaRegCircle/></div>
|
||||
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<button onClick={() => handleMove('right')} disabled={disabled} className={buttonClass} title="Move right" type="button">
|
||||
<FaArrowRight size={12} />
|
||||
</button>
|
||||
<span className="mt-1">Right: {Math.round(right)}px</span>
|
||||
</div>
|
||||
|
||||
{/* Bottom Row */}
|
||||
<div></div>
|
||||
<div className="flex flex-col items-center">
|
||||
<button onClick={() => handleMove('down')} disabled={disabled} className={buttonClass} title="Move down" type="button">
|
||||
<FaArrowDown size={12} />
|
||||
</button>
|
||||
<span className="mt-1">Bottom: {Math.round(bottom)}px</span>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DirectionalControls;
|
||||
|
|
@ -1,29 +1,68 @@
|
|||
import React from 'react'
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
interface MaxMinLenAttrIntr {
|
||||
maxLength: number;
|
||||
minLength: number;
|
||||
handlePropertyChange: (changeTo: string, changeWith: string | boolean | number) => void;
|
||||
}
|
||||
|
||||
const MaxMinLenAttr : React.FC<MaxMinLenAttrIntr> = ({ maxLength, minLength }) => {
|
||||
const MaxMinLenAttr: React.FC<MaxMinLenAttrIntr> = ({ maxLength, minLength, handlePropertyChange }) => {
|
||||
const firstChangedField = useRef<null | 'minLength' | 'maxLength'>(null);
|
||||
|
||||
const handleMaxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newMax = Number(e.target.value);
|
||||
if (!firstChangedField.current) {
|
||||
firstChangedField.current = 'maxLength';
|
||||
}
|
||||
|
||||
if (firstChangedField.current === 'minLength' && newMax < minLength) {
|
||||
alert(`Max Length must be greater than or equal to Min Length (${minLength})`);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePropertyChange('maxLength', newMax);
|
||||
};
|
||||
|
||||
const handleMinChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newMin = Number(e.target.value);
|
||||
if (!firstChangedField.current) {
|
||||
firstChangedField.current = 'minLength';
|
||||
}
|
||||
|
||||
if (firstChangedField.current === 'maxLength' && newMin > maxLength) {
|
||||
alert(`Min Length must be less than or equal to Max Length (${maxLength})`);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePropertyChange('minLength', newMin);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<label htmlFor="">Max Length</label>
|
||||
<label htmlFor="maxLength">Max Length</label>
|
||||
<input
|
||||
value={minLength ? minLength : 0}
|
||||
id="maxLength"
|
||||
value={maxLength}
|
||||
type="number"
|
||||
className="outline-none border-1 rounded px-2 py-1 border-slate-200" />
|
||||
min={0}
|
||||
className="outline-none border-1 rounded px-2 py-1 border-slate-200"
|
||||
onChange={handleMaxChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<label htmlFor="">Min Length</label>
|
||||
<label htmlFor="minLength">Min Length</label>
|
||||
<input
|
||||
value={maxLength ? maxLength : 255}
|
||||
id="minLength"
|
||||
value={minLength}
|
||||
type="number"
|
||||
className="outline-none border-1 rounded px-2 py-1 border-slate-200" />
|
||||
min={0}
|
||||
className="outline-none border-1 rounded px-2 py-1 border-slate-200"
|
||||
onChange={handleMinChange}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default MaxMinLenAttr
|
||||
export default MaxMinLenAttr;
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ const RecipientsAttrCon: React.FC<RecipientsAttrConIntr> = ({ selectedRecipientI
|
|||
key={indx}
|
||||
className='flex items-center justify-between gap-2 w-full p-2 text-xs rounded-sm cursor-pointer hover:bg-slate-100'
|
||||
onClick={() => {
|
||||
// handlePropertyChange()
|
||||
handlePropertyChange('recipientId', recipient.id);
|
||||
setTimeout(() => {
|
||||
setDropDownToggle(false);
|
||||
}, 0)
|
||||
|
|
|
|||
|
|
@ -16,12 +16,27 @@ import TemporalDateAttr from "./TemporalDateAttr";
|
|||
import SelectTagAttr from "./SelectTagAttr";
|
||||
import MaxMinLenAttr from "./MaxMinLenAttr";
|
||||
import RecipientsAttrCon from "./RecipientsAttrCon";
|
||||
import MovableFieldContainer from "./MovableField";
|
||||
import DirectionalControls from "./DirectionalControls";
|
||||
|
||||
// interface DirectionalControlsProps {
|
||||
// onMove: (direction: 'up' | 'down' | 'left' | 'right') => void;
|
||||
// disabled?: boolean;
|
||||
// stepSize?: number;
|
||||
// className?: string;
|
||||
// }
|
||||
|
||||
export interface DroppedItemProps {
|
||||
selectedTag: DroppedItem
|
||||
setDroppedItems: React.Dispatch<React.SetStateAction<DroppedItemsObj>>
|
||||
selTagKey: string,
|
||||
selTagIndx: number
|
||||
selTagIndx: number,
|
||||
DirectionalControlsProps: {
|
||||
onMove: (direction: 'up' | 'down' | 'left' | 'right', stepSize?: number) => void;
|
||||
disabled?: boolean;
|
||||
stepSize?: number;
|
||||
className?: string;
|
||||
}
|
||||
}
|
||||
|
||||
export enum TEXT {
|
||||
|
|
@ -77,15 +92,32 @@ export enum DROPDOWNTOGGLE {
|
|||
DDMMYY_FORMAT
|
||||
}
|
||||
|
||||
const RightSideBar: React.FC<DroppedItemProps> = ({ selectedTag, setDroppedItems, selTagKey, selTagIndx }) => {
|
||||
const defaultStyles = {
|
||||
font: "",
|
||||
italic: false,
|
||||
bold: false,
|
||||
underline: false,
|
||||
color: "#000000",
|
||||
textSize: 0
|
||||
};
|
||||
|
||||
|
||||
|
||||
const RightSideBar: React.FC<DroppedItemProps> = ({ selectedTag, setDroppedItems, selTagKey, selTagIndx, DirectionalControlsProps }) => {
|
||||
|
||||
const selectedType = selectedTag.type in enumMap && enumMap[selectedTag.type as keyof typeof enumMap];
|
||||
|
||||
console.log("selectedTag", selectedTag, selectedType)
|
||||
|
||||
|
||||
|
||||
const [styles, setStyles] = useState(defaultStyles);
|
||||
|
||||
const [dropDownToggle, setDropDownToggle] = useState<DROPDOWNTOGGLE | null>(null);
|
||||
const [decimalPlace, setDecimalPlace] = useState<number>(2)
|
||||
const [styleOpen, setStyleOpen] = useState(false);
|
||||
|
||||
const handlePropertyChange = (changeTo: string, changeWith: string | boolean | number, is24Hrs=null) => {
|
||||
const handlePropertyChange = (changeTo: string, changeWith: string | boolean | number, is24Hrs = null) => {
|
||||
console.log("Handle Property Change Called", "changeTo", changeTo, "changeWith", changeWith)
|
||||
if (changeTo === '' || changeWith === '') return
|
||||
|
||||
|
|
@ -150,16 +182,23 @@ const RightSideBar: React.FC<DroppedItemProps> = ({ selectedTag, setDroppedItems
|
|||
));
|
||||
|
||||
} else {
|
||||
setDroppedItems((prev) => (
|
||||
{
|
||||
setDroppedItems((prev) => {
|
||||
console.log('Previous state:', prev);
|
||||
|
||||
const updatedItems = Array.isArray(prev[selTagKey])
|
||||
? prev[selTagKey].map((item, i) =>
|
||||
i === selTagIndx ? { ...item, [changeTo]: changeWith, update: true } : item
|
||||
)
|
||||
: [];
|
||||
|
||||
console.log('Updated items:', updatedItems);
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[selTagKey]: Array.isArray(prev[selTagKey]) // Ensure it's an array
|
||||
? prev[selTagKey].map((item, i) =>
|
||||
i === selTagIndx ? { ...item, [changeTo]: changeWith, update: true } : item
|
||||
)
|
||||
: [],
|
||||
}// Provide a fallback in case it's undefined or not an array
|
||||
));
|
||||
[selTagKey]: updatedItems,
|
||||
};
|
||||
});
|
||||
|
||||
}
|
||||
setDropDownToggle(null)
|
||||
}
|
||||
|
|
@ -193,24 +232,67 @@ const RightSideBar: React.FC<DroppedItemProps> = ({ selectedTag, setDroppedItems
|
|||
handlePropertyChange("decimalPlace", val)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if ("styles" in selectedTag) {
|
||||
const fontSize = selectedTag?.styles?.textSize;
|
||||
const tagHeight = selectedTag?.dimensions?.height - 18;
|
||||
// useEffect(() => {
|
||||
// if ("styles" in selectedTag) {
|
||||
// console.log("styles" in selectedTag)
|
||||
// const fontSize = selectedTag?.styles?.textSize;
|
||||
// const tagHeight = selectedTag?.dimensions?.height - 18;
|
||||
|
||||
if (selectedTag.subType !== TEXT.PASSWORD && selectedTag.subType !== TEXT.PARAGRAPH && tagHeight > (fontSize + fontSize)) {
|
||||
handlePropertyChange("subType", TEXT.PARAGRAPH)
|
||||
} else if (selectedTag.subType !== TEXT.PASSWORD && selectedTag.subType !== TEXT.SINGLE_LINE && tagHeight <= (fontSize + fontSize)) {
|
||||
console.log("Change subType Called from elseif")
|
||||
handlePropertyChange("subType", TEXT.SINGLE_LINE)
|
||||
}
|
||||
// console.log(fontSize)
|
||||
|
||||
// if (selectedTag.subType !== TEXT.PASSWORD && selectedTag.subType !== TEXT.PARAGRAPH && tagHeight > (fontSize + fontSize)) {
|
||||
// handlePropertyChange("subType", TEXT.PARAGRAPH)
|
||||
// } else if (selectedTag.subType !== TEXT.PASSWORD && selectedTag.subType !== TEXT.SINGLE_LINE && tagHeight <= (fontSize + fontSize)) {
|
||||
// console.log("Change subType Called from elseif")
|
||||
// handlePropertyChange("subType", TEXT.SINGLE_LINE)
|
||||
// }
|
||||
// }
|
||||
// }, [selectedTag])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedTag) return;
|
||||
|
||||
if (!selectedTag.styles) {
|
||||
handlePropertyChange("styles", defaultStyles);
|
||||
setStyles(defaultStyles);
|
||||
} else {
|
||||
setStyles({ ...defaultStyles, ...selectedTag.styles });
|
||||
}
|
||||
}, [selectedTag])
|
||||
|
||||
// Optional subType logic
|
||||
const fontSize = selectedTag?.styles?.textSize || defaultStyles.textSize;
|
||||
const tagHeight = selectedTag?.dimensions?.height - 18;
|
||||
|
||||
if (
|
||||
selectedTag.subType !== TEXT.PASSWORD &&
|
||||
selectedTag.subType !== TEXT.PARAGRAPH &&
|
||||
tagHeight > fontSize * 2
|
||||
) {
|
||||
handlePropertyChange("subType", TEXT.PARAGRAPH);
|
||||
} else if (
|
||||
selectedTag.subType !== TEXT.PASSWORD &&
|
||||
selectedTag.subType !== TEXT.SINGLE_LINE &&
|
||||
tagHeight <= fontSize * 2
|
||||
) {
|
||||
handlePropertyChange("subType", TEXT.SINGLE_LINE);
|
||||
}
|
||||
}, [selectedTag]);
|
||||
|
||||
const handleStyleChange = (key, value) => {
|
||||
const updatedStyles = {
|
||||
...(selectedTag.styles ?? defaultStyles),
|
||||
[key]: value
|
||||
};
|
||||
setStyles(updatedStyles);
|
||||
handlePropertyChange("styles", updatedStyles);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Right Side Bar Start */}
|
||||
<div className='col-span-2 w-full h-full bg-white text-sm'>
|
||||
<div className='col-span-2 w-full h-full bg-white text-sm overflow-y-auto'>
|
||||
<p className="p-2 font-semibold text-md">Properties</p>
|
||||
<hr className="text-slate-200" />
|
||||
|
||||
|
|
@ -220,11 +302,20 @@ const RightSideBar: React.FC<DroppedItemProps> = ({ selectedTag, setDroppedItems
|
|||
<RecipientsAttrCon selectedRecipientId={selectedTag.recipientId} handlePropertyChange={handlePropertyChange} />
|
||||
}
|
||||
|
||||
<DirectionalControls {...DirectionalControlsProps} selectedType={selectedTag.dimensions}/>
|
||||
|
||||
|
||||
{selectedTag.label &&
|
||||
<LabelAttr label={selectedTag.label} handlePropertyChange={handlePropertyChange} />}
|
||||
|
||||
{(selectedTag.type !== TagsTypes.SIGNATURE && selectedTag.type !== TagsTypes.INITIALS) &&
|
||||
<RequiredAttr required={selectedTag.required ? selectedTag.required : false} handlePropertyChange={handlePropertyChange} />}
|
||||
|
||||
<>
|
||||
<RequiredAttr required={selectedTag.required ? selectedTag.required : false} handlePropertyChange={handlePropertyChange} />
|
||||
</>
|
||||
|
||||
|
||||
}
|
||||
|
||||
{selectedTag.type.toLowerCase() === TagsTypes.TEMPORAL &&
|
||||
<CurrTimeStampAttr type={selectedTag.type} handlePropertyChange={handlePropertyChange} />}
|
||||
|
|
@ -290,11 +381,13 @@ const RightSideBar: React.FC<DroppedItemProps> = ({ selectedTag, setDroppedItems
|
|||
}
|
||||
|
||||
{
|
||||
selectedTag.type === TagsTypes.TEXT &&
|
||||
<MaxMinLenAttr maxLength={selectedTag.maxLength} minLength={selectedTag.minLength} />
|
||||
(selectedTag.type.toLowerCase() === TagsTypes.TEXT || selectedTag.type.toLowerCase() === TagsTypes.COMPANY) &&
|
||||
<MaxMinLenAttr maxLength={selectedTag.maxLength} minLength={selectedTag.minLength} handlePropertyChange={handlePropertyChange} />
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
{
|
||||
<div className="">
|
||||
<div className="flex items-center justify-between mr-2 cursor-pointer"
|
||||
|
|
@ -313,6 +406,8 @@ const RightSideBar: React.FC<DroppedItemProps> = ({ selectedTag, setDroppedItems
|
|||
type="text"
|
||||
placeholder="e.g. Arial"
|
||||
className="outline-none border-1 rounded px-2 py-1 border-slate-200"
|
||||
value={styles.font}
|
||||
onChange={(e) => handleStyleChange("font", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -323,6 +418,10 @@ const RightSideBar: React.FC<DroppedItemProps> = ({ selectedTag, setDroppedItems
|
|||
// value={}
|
||||
type="number"
|
||||
className="outline-none border-1 rounded px-2 py-1 border-slate-200"
|
||||
value={styles.textSize}
|
||||
onChange={(e) =>
|
||||
handleStyleChange("textSize", parseInt(e.target.value, 10) || 0)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -332,21 +431,27 @@ const RightSideBar: React.FC<DroppedItemProps> = ({ selectedTag, setDroppedItems
|
|||
<input
|
||||
type="color"
|
||||
className="outline-none border-1 rounded px-2 py-1 border-slate-200 w-10 h-8"
|
||||
value={styles.color}
|
||||
onChange={(e) => handleStyleChange("color", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Toggles for Bold, Italic, Underline */}
|
||||
<div className="flex gap-4 items-center mt-2">
|
||||
<label className="flex gap-1 items-center">
|
||||
<input type="checkbox" />
|
||||
<input type="checkbox" checked={styles.bold}
|
||||
onChange={(e) => handleStyleChange("bold", e.target.checked)} />
|
||||
Bold
|
||||
</label>
|
||||
<label className="flex gap-1 items-center">
|
||||
<input type="checkbox" />
|
||||
<input type="checkbox" checked={styles.italic} onChange={(e) => handleStyleChange("italic", e.target.checked)} />
|
||||
Italic
|
||||
</label>
|
||||
<label className="flex gap-1 items-center">
|
||||
<input type="checkbox" />
|
||||
<input type="checkbox" checked={styles.underline}
|
||||
onChange={(e) =>
|
||||
handleStyleChange("underline", e.target.checked)
|
||||
} />
|
||||
Underline
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,22 +15,32 @@ const SelectTagAttr: React.FC<SelectTagAttrInt> = ({ options, selTagKey, selTagI
|
|||
const [selectedOption, setSelectedOption] = useState<string>()
|
||||
|
||||
const handleAddSelectOption = () => {
|
||||
if (selectedOption && selectedOption.length) {
|
||||
setDroppedItems((prev) => (
|
||||
{
|
||||
...prev,
|
||||
[selTagKey]: Array.isArray(prev[selTagKey])
|
||||
? prev[selTagKey].map((item, i) => {
|
||||
if (i === selTagIndx) {
|
||||
return { ...item, options: item.options ? [...item.options, selectedOption] : [selectedOption], update: true };
|
||||
}
|
||||
return item;
|
||||
})
|
||||
: [],
|
||||
}
|
||||
));
|
||||
}
|
||||
if (selectedOption && selectedOption.length) {
|
||||
setDroppedItems((prev) => {
|
||||
console.log("Previous droppedItems:", prev);
|
||||
|
||||
const newState = {
|
||||
...prev,
|
||||
[selTagKey]: Array.isArray(prev[selTagKey])
|
||||
? prev[selTagKey].map((item, i) => {
|
||||
if (i === selTagIndx) {
|
||||
return {
|
||||
...item,
|
||||
options: item.options ? [...item.options, selectedOption] : [selectedOption],
|
||||
update: true,
|
||||
};
|
||||
}
|
||||
return item;
|
||||
})
|
||||
: [],
|
||||
};
|
||||
|
||||
console.log("New droppedItems state:", newState);
|
||||
return newState;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleSelectOptionDel = (indx: number) => {
|
||||
setDroppedItems((prev) => (
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ interface Signature {
|
|||
signatureHash: string;
|
||||
signatureBlob: string;
|
||||
signatureStampedBlob: string;
|
||||
default: boolean
|
||||
default: boolean;
|
||||
signatureMimeType:string;
|
||||
initialsMimeType:string
|
||||
}
|
||||
|
||||
const SignatureManager: React.FC = () => {
|
||||
|
|
@ -155,7 +157,7 @@ const SignatureManager: React.FC = () => {
|
|||
{/* Signature Image */}
|
||||
<div className="bg-white rounded-lg p-1 flex items-center justify-center ">
|
||||
<img
|
||||
src={`data:image/png;base64,${signature.signatureBlob}`}
|
||||
src={`data:${signature.signatureMimeType};base64,${signature.signatureBlob}`}
|
||||
className="h-[50px] w-[250px] object-contain"
|
||||
alt="Signature"
|
||||
/>
|
||||
|
|
@ -179,7 +181,7 @@ const SignatureManager: React.FC = () => {
|
|||
{/* Signature Image */}
|
||||
<div className="bg-white rounded-lg p-1 flex items-center justify-center ">
|
||||
<img
|
||||
src={`data:image/png;base64,${signature.initialsBlob}`}
|
||||
src={`data:${signature.initialsMimeType};base64,${signature.initialsBlob}`}
|
||||
className="h-[50px] object-contain"
|
||||
alt="DS Signature"
|
||||
/>
|
||||
|
|
|
|||
Loading…
Reference in New Issue