jai #1

Merged
jaipandey merged 6 commits from jai into master 2025-07-07 06:15:27 +00:00
7 changed files with 440 additions and 139 deletions
Showing only changes of commit 2bb54b2043 - Show all commits

View File

@ -4,6 +4,8 @@ import AccountNav from "./AccountNav";
import api from "../../config/api";
import { useNavigate, useParams } from "react-router-dom";
import { FaCheck } from "react-icons/fa";
import Cards from "../home/Cards";
import AddInviteModal from "./AddInviteModal";
interface Transaction {
id: number;
@ -23,6 +25,12 @@ const Account = () => {
const [transactions, setTransactions] = useState<Transaction[]>([]);
const [selectedTransactions, setSelectedTransactions] = useState<number[]>([]);
const [isModalOpen, setIsModalOpen] = useState(false);
const handleOpenModal = () => {
setIsModalOpen(true);
};
const fetchTransactions = async () => {
try {
const response = await api(`/api/accounts/${accountId}/transactions`);
@ -42,8 +50,14 @@ const Account = () => {
navigate(`/accounts/${accountId}/transactions/${id}`);
};
const handleAddAccount = (accountName: string) => {
console.log(accountId)
setIsModalOpen(false);
};
return (
<>
<Cards type="account" onOpenModal={handleOpenModal} />
{transactions.length > 0 && (
<div className="overflow-x-auto py-4">
<table className="min-w-full text-sm border border-gray-200 rounded bg-white shadow-sm">
@ -102,6 +116,8 @@ const Account = () => {
</table>
</div>
)}
{isModalOpen && <AddInviteModal onClose={() => setIsModalOpen(false)}
onAddAccount={handleAddAccount} />}
</>
);
};

View File

@ -1,64 +1,63 @@
import React, { use, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { Link, useLocation, useParams } from 'react-router-dom';
import { FaCog } from 'react-icons/fa';
import { IoIosNotifications } from 'react-icons/io';
import { RootState } from '../../store/store';
import { UserState } from '../../store/slices/userSlice';
import React, { useEffect, useRef, useState } from 'react';
// import { useSelector } from 'react-redux';
import { Link, useLocation } from 'react-router-dom';
import { FaSignOutAlt,FaUser } from "react-icons/fa";
import api from '../../config/api';
// const fetchAccountData = async (accountId: string) => {
// try {
// if(!accountId) return null;
// const response = await api.get(`/api/accounts/${accountId}`);
// return response.data;
// } catch (error) {
// console.error("Error fetching account data:", error);
// throw error;
// }
// }
// const fetchTransactionData = async (accountId: string, transactionId: string) => {
// try {
// if(!accountId || !transactionId) return null;
// const response = await api.get(`/api/accounts/${accountId}/transactions/${transactionId}`);
// return response.data;
// } catch (error) {
// console.error("Error fetching account data:", error);
// throw error;
// }
// }
// import { RootState } from '../../store/store';
// import { UserState } from '../../store/slices/userSlice';
interface AccountNavProps {
accountName: string | null;
transactionName: string | null;
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;
}
const AccountNav: React.FC<AccountNavProps> = ({ accountName = null, transactionName = null }) => {
const user: UserState = useSelector((state: RootState) => state.user);
// const user: UserState = useSelector((state: RootState) => state.user);
const initials = user.username
? user.username
.split(' ')
.map((n) => n[0])
.join('')
.slice(0, 2)
.toUpperCase()
: 'U';
const [user, setUser] = useState<UserData>({
id: "",
firstName: "",
lastName: "",
email: "",
language: "",
account: {
id: 0,
isDefault: false,
name: "",
},
});
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()
}, [])
// useEffect(() => {
// if (accountId) {
// api.get(`/api/accounts/${accountId}`)
// .then(response => {
// // Handle the account data if needed
// console.log(response.data, "Line 29");
// })
// .catch(error => {
// console.error("Error fetching account data:", error);
// });
// }
// }, [accountId, transactionId]);
const location = useLocation();
@ -66,74 +65,121 @@ const AccountNav: React.FC<AccountNavProps> = ({ accountName = null, transaction
.split("/")
.filter((x) => x && isNaN(Number(x))); // remove numeric IDs
const labelMap = {
const labelMap: Record<string, string> = {
accounts: "Accounts",
transactions: "Transactions",
// Add more routes here if needed
};
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)) {
setOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const handleLogout = () => {
// Add logout logic here
console.log('Logging out...');
setOpen(false);
};
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">
{pathnames.map((segment, index) => {
const label = labelMap[segment] || segment;
const to = "/" + pathnames.slice(0, index + 1).join("/");
const isLast = index === pathnames.length - 1;
<nav className="text-md font-medium text-gray-800 capitalize">
{pathnames.map((segment, index) => {
const label = labelMap[segment] || segment;
const to = "/" + pathnames.slice(0, index + 1).join("/");
return (
<span key={to}>
{!isLast ? (
<>
<Link
to={to}
className="text-blue-600 hover:text-blue-700 hover:underline transition-colors"
>
{label}
</Link>
<span className="mx-2 text-gray-400">{">"}</span>
</>
) : (
<span className="text-gray-900 font-semibold">{label}</span>
)}
</span>
);
})}
</nav>
const isLast = index === pathnames.length - 1;
<div className="flex items-center space-x-2">
return (
<span key={to}>
{!isLast ? (
<>
<Link
to={to}
className="text-blue-600 hover:underline"
>
{label}
</Link>
<span className="mx-1">{">"}</span>
</>
) : (
<span>{label}</span>
)}
</span>
);
})}
</nav>
{/* <h1 className="text-md font-medium text-gray-800 capitalize">Account</h1> */}
<div className="flex items-center">
{/* Notification + Settings with smaller gap */}
<div className="flex items-center">
{/* User Dropdown */}
<div className="relative" ref={dropdownRef}>
{/* Avatar Button */}
<button
aria-label="Notifications"
className="p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-600"
className="flex items-center space-x-2 p-1 cursor-pointer rounded-lg transition-colors"
onClick={() => setOpen(!open)}
aria-expanded={open}
aria-haspopup="true"
>
<IoIosNotifications className="text-xl" />
<div className="w-8 h-8 rounded-full overflow-hidden bg-gradient-to-br from-green-500 to-blue-600 flex items-center justify-center text-white font-semibold text-sm shadow-md">
{user ? (
<span
className="w-full h-full object-cover p-1.5 font-bold"
>{(user.firstName.slice(0,1) + user.lastName.slice(0,1)).toUpperCase()}</span>
) : (
<FaUser className="text-lg" />
)}
</div>
</button>
<Link
to="/s/settings"
aria-label="Settings"
className="p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-600"
>
<FaCog className="text-sm" />
</Link>
</div>
{/* Enhanced Dropdown */}
{open && (
<div className="absolute right-0 mt-2 w-72 top-8 rounded-xl shadow-xl bg-white border border-gray-200 z-50 overflow-hidden">
{/* User Info Section */}
<div className="px-4 py-4 bg-gradient-to-r from-blue-50 to-purple-50 border-b border-gray-100">
<div className="flex items-center space-x-3">
<div className="w-12 h-12 rounded-full overflow-hidden bg-gradient-to-br from-green-500 to-blue-600 flex items-center justify-center text-white font-semibold shadow-md">
{(user) ? (
<span
className="w-full h-full object-cover px-3 text-xl py-2.5 font-bold"
>{(user.firstName.slice(0,1) + user.lastName.slice(0,1)).toUpperCase()}</span>
) : (
<FaUser className="text-lg" />
)}
</div>
<div className="flex-1 min-w-0">
<div className="font-semibold text-gray-900 truncate">{user.firstName + ' ' +user.lastName || '-'}</div>
<div className="text-sm text-gray-600 truncate">{user.email || '-'}</div>
<div className="text-xs text-gray-500 mt-1">
Account #{user?.account?.name || '-'}
</div>
</div>
</div>
</div>
{/* Avatar with separate spacing */}
<div className="w-9 h-9 ms-3 rounded-full overflow-hidden bg-[var(--primary)] flex items-center justify-center text-white font-semibold text-sm">
{user.picture ? (
<img
src={user.picture}
alt="User avatar"
className="w-full h-full object-cover"
/>
) : (
initials
{/* Logout Section */}
<div className="border-t border-gray-100 ">
<button
onClick={handleLogout}
className="w-full flex items-center px-4 py-3 text-sm text-red-600 hover:bg-red-50 transition-colors cursor-pointer"
>
<FaSignOutAlt className="mr-3" />
<span>Log Out</span>
</button>
</div>
</div>
)}
</div>
</div>

View File

@ -6,6 +6,9 @@ import AccountNav from "./AccountNav";
import api from "../../config/api";
import { AccountData, AccountStatus, UserRole } from "../../utlity/types";
import { useNavigate } from "react-router-dom";
import Cards from "../home/Cards";
import AddAccountModal from "./AddAccountModal";
import { Bounce, toast } from "react-toastify";
// import { UserRole, AccountStatus, AccountData } from "../../types/account";
@ -32,6 +35,13 @@ const getStatusLabel = (
const Accounts = () => {
const [allAccounts, setAllAccounts] = useState<AccountData[]>([]);
const [selectedAccounts, setSelectedAccounts] = useState<number[]>([]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [accounts, setAccounts] = useState<string[]>([]);
const [openMenuId, setOpenMenuId] = useState<string | null>(null);
const handleOpenModal = () => {
setIsModalOpen(true);
};
const navigate = useNavigate();
@ -53,8 +63,43 @@ const Accounts = () => {
navigate(`/accounts/${id}/transactions`);
};
const handleAddAccount = (accountName: string) => {
setAccounts((prev) => [...prev, accountName]);
setIsModalOpen(false);
};
const handleMenuToggle = (accountId: string) => {
setOpenMenuId(openMenuId === accountId ? null : accountId);
};
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 (
<>
<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">
@ -101,8 +146,38 @@ const Accounts = () => {
{status.text}
</span>
</td>
<td className="px-4 py-3 text-center">
<PiDotsThreeOutlineVerticalFill className="text-lg text-slate-300 hover:text-black mx-auto" />
<td className="px-4 py-3 text-center relative">
<button
onClick={(e) => {
e.stopPropagation();
handleMenuToggle(account.accountId);
}}
className="mx-auto"
aria-label="Actions menu"
>
<PiDotsThreeOutlineVerticalFill className="text-lg text-slate-300 hover:text-black" />
</button>
{/* Dropdown menu */}
{openMenuId === account.accountId && (
<div
className="absolute right-13 top-6 mt-1 w-18 bg-white border text-[13px] font-bold border-gray-300 rounded-md shadow-lg z-10"
onClick={(e) => e.stopPropagation()} // prevent row click closing
>
<button
className="w-full px-3 py-1 text-left text-green-700 hover:bg-green-100 rounded-t-md cursor-pointer"
onClick={() => handleAction(account.accountId, 'accept')}
>
Accept
</button>
<button
className="w-full px-3 py-1 text-left text-red-700 hover:bg-red-100 rounded-b-md cursor-pointer"
onClick={() => handleAction(account.accountId, 'reject')}
>
Reject
</button>
</div>
)}
</td>
</tr>
);
@ -111,6 +186,8 @@ const Accounts = () => {
</table>
</div>
)}
{isModalOpen && <AddAccountModal onClose={() => setIsModalOpen(false)}
onAddAccount={handleAddAccount} />}
</>
);
};

View File

@ -0,0 +1,52 @@
import React, { useState } from 'react';
interface AddAccountModalProps {
onClose: () => void;
onAddAccount: (accountName: string) => void;
}
const AddAccountModal: React.FC<AddAccountModalProps> = ({ onClose, onAddAccount }) => {
const [inputValue, setInputValue] = useState('');
const handleAddClick = () => {
if (inputValue.trim()) {
onAddAccount(inputValue.trim());
setInputValue(''); // clear input after submit
}
};
return (
<div className="fixed inset-0 backdrop-blur-sm flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl p-6 w-80">
<h3 className="text-lg font-medium text-gray-800 mb-4">Add New Account</h3>
<input
type="text"
placeholder="Enter account name"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
className="w-full mb-4 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
/>
<div className="flex justify-end gap-3">
<button
onClick={() => {
setInputValue('');
onClose();
}}
className="px-4 py-1.5 text-[13px] cursor-pointer font-semibold text-gray-600 hover:bg-gray-100 rounded-md transition-colors"
>
Cancel
</button>
<button
onClick={handleAddClick}
className="px-2 py-1.5 cursor-pointer font-semibold bg-[var(--modalSubmitBG)] text-[13px] text-white hover:bg-[var(--modalSubmitBGHover)] rounded-md transition-colors"
disabled={!inputValue.trim()}
>
Add Account
</button>
</div>
</div>
</div>
);
};
export default AddAccountModal;

View File

@ -0,0 +1,51 @@
import { useState } from "react";
interface AddAccountModalProps {
onClose: () => void;
onAddAccount: (accountName: string) => void;
}
const AddInviteModal : React.FC<AddAccountModalProps> = ({ onClose, onAddAccount }) => {
const [inputValue, setInputValue] = useState('');
const handleAddClick = () => {
if (inputValue.trim()) {
onAddAccount(inputValue.trim());
setInputValue(''); // clear input after submit
}
};
return (
<div className="fixed inset-0 backdrop-blur-sm flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl p-6 w-80">
<h3 className="text-lg font-medium text-gray-800 mb-4">Send Invite</h3>
<input
type="text"
placeholder="Enter account name"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
className="w-full mb-4 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
/>
<div className="flex justify-end gap-3">
<button
onClick={() => {
setInputValue('');
onClose();
}}
className="px-4 py-1.5 text-[13px] cursor-pointer font-semibold text-gray-600 hover:bg-gray-100 rounded-md transition-colors"
>
Cancel
</button>
<button
onClick={handleAddClick}
className="px-2 py-1.5 cursor-pointer font-semibold bg-[var(--modalSubmitBG)] text-[13px] text-white hover:bg-[var(--modalSubmitBGHover)] rounded-md transition-colors"
disabled={!inputValue.trim()}
>
Add Account
</button>
</div>
</div>
</div>
)
}
export default AddInviteModal

View File

@ -1,7 +1,22 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { FaCheckCircle, FaClock, FaHourglassHalf, FaTasks } from 'react-icons/fa';
const cards = [
// Define a type for card data
interface CardData {
title: string;
count: number;
icon: JSX.Element;
ring: string;
bg: string;
}
interface CardsProps {
type?: 'dashboard';
// onCardClick?: (card: CardData) => void;
onOpenModal?: () => void;
}
const cards: CardData[] = [
{
title: 'Action Required',
count: 12,
@ -32,48 +47,89 @@ const cards = [
},
];
const AccountData: CardData[] = [
{
title: 'Transaction',
count: 12,
icon: <FaTasks size={20} className="text-orange-500" />,
ring: 'ring-orange-400/20',
bg: 'bg-orange-50',
},
{
title: 'In-Progress',
count: 8,
icon: <FaClock size={20} className="text-yellow-500" />,
ring: 'ring-yellow-400/20',
bg: 'bg-yellow-50',
},
{
title: 'Deactive',
count: 5,
icon: <FaHourglassHalf size={20} className="text-red-500" />,
ring: 'ring-red-400/20',
bg: 'bg-red-50',
},
{
title: 'Completed',
count: 27,
icon: <FaCheckCircle size={20} className="text-green-500" />,
ring: 'ring-green-400/20',
bg: 'bg-green-50',
},
];
const filters = ['Last 7 Days', 'Last 30 Days', 'Last 6 Months', 'This Year'];
const Cards = () => {
const [selectedFilter, setSelectedFilter] = useState('Last 7 Days');
const Cards: React.FC<CardsProps> = ({ type, onOpenModal }) => {
const [selectedFilter, setSelectedFilter] = useState<string>('Last 7 Days');
const [showCards, setShowCards] = useState<CardData[]>([]);
useEffect(() => {
if (type === 'account') {
setShowCards(AccountData);
} else {
setShowCards(cards);
}
}, [type]);
return (
<div className="w-full px-4 py-3">
{/* Header */}
<div className="flex flex-col md:flex-row items-start md:items-center justify-between mb-5 gap-3">
<h2 className="text-lg font-medium text-gray-700">Overview</h2>
<div className="flex flex-col md:flex-row items-start md:items-center justify-between mb-5 gap-3">
<h2 className="text-lg font-medium text-gray-700">Overview</h2>
<div className="flex gap-2 w-full md:w-auto">
{/* Account Selector */}
<select
value={'All'}
onChange={() => {}}
className="text-sm text-gray-700 border border-gray-300 rounded-sm px-3 py-1.5 focus:outline-none bg-white shadow-sm"
>
<option>All</option>
<option>Account 1</option>
<option>Account 2</option>
<option>Account 3</option>
</select>
{/* Date Filter */}
<select
value={selectedFilter}
onChange={(e) => setSelectedFilter(e.target.value)}
className="text-sm text-gray-700 border border-gray-300 rounded-sm px-3 py-1.5 focus:outline-none bg-white shadow-sm"
>
{filters.map((f) => (
<option key={f}>{f}</option>
))}
</select>
</div>
</div>
<div className="flex gap-2 w-full md:w-auto">
{/* Account Selector */}
<select
value={'All'}
onChange={() => { }}
className="text-sm text-gray-700 border border-gray-300 rounded-sm px-3 py-1.5 focus:outline-none bg-white shadow-sm"
>
<option>All</option>
<option>Account 1</option>
<option>Account 2</option>
<option>Account 3</option>
</select>
{/* Date Filter */}
<select
value={selectedFilter}
onChange={(e) => setSelectedFilter(e.target.value)}
className="text-sm text-gray-700 border border-gray-300 rounded-sm px-3 py-1.5 focus:outline-none bg-white shadow-sm"
>
{filters.map((f) => (
<option key={f}>{f}</option>
))}
</select>
{type === 'account' &&
<button className="px-2 py-1.5 cursor-pointer font-semibold bg-[var(--primary)] text-[13px] text-white hover:bg-[var(--primaryLight2)] rounded-md transition-colors" onClick={onOpenModal}>Add Account</button>}
</div>
</div>
{/* Cards */}
<div className="cursor-pointer grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{cards.map((card) => (
{showCards.map((card) => (
<div
key={card.title}
className={`rounded-md bg-white/60 backdrop-blur-md border border-gray-200 p-5 transition-transform shadow-md hover:scale-[1.02] hover:shadow-lg`}

View File

@ -43,6 +43,9 @@
--hideModalBg: #2d2d2d;
--processStepOneTextHead: #000000;
--modalSubmitBG:#1919d7;
--modalSubmitBGHover:#395ee1;
}