Merge remote-tracking branch 'origin/main'

This commit is contained in:
=bhagwanSingh 2025-07-31 12:32:21 +05:30
commit 09e7f85e29
7 changed files with 186 additions and 121 deletions

View File

@ -245,6 +245,7 @@ const Account = () => {
return ( return (
<> <>
<Cards type="transactions" onOpenModal={handleOpenModal} cards={cards} <Cards type="transactions" onOpenModal={handleOpenModal} cards={cards}
isLoading={isLoading} /> isLoading={isLoading} />

View File

@ -3,23 +3,24 @@ import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
import api from '../../config/api'; import api from '../../config/api';
import { Transaction } from '../transactions/Transactions'; import { Transaction } from '../transactions/Transactions';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { FaCheck } from 'react-icons/fa'; import { FaCheck } from 'react-icons/fa';
const formatDate = (timestamp: number) => { const formatDate = (timestamp: number) => {
return new Date(timestamp * 1000).toLocaleString(); return new Date(timestamp * 1000).toLocaleString();
}; };
const TransactionDataTable = () => { const TransactionDataTable = () => {
const navigate = useNavigate()
const [currentEmailId, setCurrentEmailId] = useState<string | null>('null'); const [currentEmailId, setCurrentEmailId] = useState<string | null>('null');
const [selectedTransactions, setSelectedTransactions] = useState<number[]>([]); const [selectedTransactions, setSelectedTransactions] = useState<number[]>([]);
const [activeAccountId, setActiveAccountId] = useState<string | null>( const [activeAccountId, setActiveAccountId] = useState<string | null>(
JSON.parse(sessionStorage.getItem('activeAccountId') || 'null') JSON.parse(sessionStorage.getItem('activeAccountId') || 'null')
); );
const navigate = useNavigate();
const fetchTransactions = async () => { const fetchTransactions = async () => {
console.log("fetchTransactions") console.log("fetchTransactions")
try { try {
@ -47,6 +48,9 @@ const TransactionDataTable = () => {
setCurrentEmailId(userDetails?.email) setCurrentEmailId(userDetails?.email)
}, []); }, []);
return ( return (
<> <>
{transactions.length > 0 && ( {transactions.length > 0 && (

View File

@ -1,20 +1,32 @@
import { useEffect, useState, useRef } from 'react'; import { useEffect, useState, useRef } from 'react';
import { FaEllipsisV } from 'react-icons/fa'; import { FaEllipsisV } from 'react-icons/fa';
import api from '../../config/api'; import api from '../../config/api';
import ContactFormModal from './ContactsFormModal'; import ContactFormModal, { ContactData } from './ContactsFormModal';
import { Bounce, toast } from 'react-toastify'; import { Bounce, toast } from 'react-toastify';
import { IoMdAdd } from 'react-icons/io'; import { IoMdAdd } from 'react-icons/io';
import LoaderUi from '../../helper/LoaderUi';
type ContactSchema = [{
id: number,
firstName: string
lastName: string
email: string
}]
const Contacts = () => { const Contacts = () => {
const activeAccountId = JSON.parse(sessionStorage.getItem('activeAccountId') || 'null'); const activeAccountId = JSON.parse(sessionStorage.getItem('activeAccountId') || 'null');
const [contacts, setContacts] = useState([]); const [contacts, setContacts] = useState<ContactSchema>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [openMenuId, setOpenMenuId] = useState(null); const [openMenuId, setOpenMenuId] = useState(null);
const blurTimeoutRef = useRef(null); const blurTimeoutRef = useRef(null);
const [createAndInviteLoader, setCreateAndInviteLoader] = useState(false); const [createAndInviteLoader, setCreateAndInviteLoader] = useState(false);
const [showModal, setShowModal] = useState(false) const [showModal, setShowModal] = useState(false)
const [editJsonData, setEditJsonData] = useState({})
const [actionStatus, setActionStatus] = useState("")
console.log("editJsonData", editJsonData)
const fetchContacts = async () => { const fetchContacts = async () => {
setLoading(true)
try { try {
const res = await api.get(`/api/accounts/${activeAccountId}/contacts`); const res = await api.get(`/api/accounts/${activeAccountId}/contacts`);
setContacts(res.data.content || []); setContacts(res.data.content || []);
@ -33,17 +45,27 @@ const Contacts = () => {
const handleEdit = (contactId) => { const handleEdit = (contactId) => {
console.log('Edit contact:', contactId); console.log('Edit contact:', contactId);
let jsonEditData = contacts.find((prev) => prev.id === contactId)
setEditJsonData(jsonEditData)
setActionStatus("Edit")
setShowModal((prev) => !prev)
setOpenMenuId(null); setOpenMenuId(null);
}; };
const handleDelete = async(contactId) => { const handleDelete = async (contactId) => {
console.log('Delete contact:', contactId); console.log('Delete contact:', contactId);
setOpenMenuId(null);
try {
let res = await api.delete(`/api/accounts/{accountId}/contacts/{contactId}`)
if (res?.data) {
} try {
let res = await api.delete(`/api/accounts/${activeAccountId}/contacts/${contactId}`)
if (res?.data) {
toast.error("Contact delete successfully!", {
position: "top-center",
autoClose: 1500,
transition: Bounce,
});
fetchContacts()
setOpenMenuId(null);
}
} catch (error) { } catch (error) {
console.log(error) console.log(error)
} }
@ -61,10 +83,15 @@ const Contacts = () => {
}, 100); }, 100);
}; };
const handleContactSubmit = async (data: ContactData) => { const handleContactSubmit = async (data: ContactData, type: "") => {
try { try {
let res;
setCreateAndInviteLoader(true); setCreateAndInviteLoader(true);
const res = await api.post(`/api/accounts/${activeAccountId}/contacts`, [data]); if (type === 'Edit') {
res = await api.put(`/api/accounts/${activeAccountId}/contacts`, [data])
} else {
res = await api.post(`/api/accounts/${activeAccountId}/contacts`, [data]);
}
if (res.status === 200) { if (res.status === 200) {
toast.success('Contact created successfully!', { toast.success('Contact created successfully!', {
position: "top-center", position: "top-center",
@ -72,16 +99,16 @@ const Contacts = () => {
transition: Bounce, transition: Bounce,
}); });
setShowModal(false) setShowModal(false)
fetchContacts(); fetchContacts();
} }
// return res; // return res;
} catch (err) { } catch (err) {
console.error("Error creating contact", err); console.error("Error creating contact", err);
toast.error("Failed to create contact", { toast.error("Failed to create contact", {
position: "top-center", position: "top-center",
autoClose: 1500, autoClose: 1500,
transition: Bounce, transition: Bounce,
}); });
throw err; throw err;
} finally { } finally {
setCreateAndInviteLoader(false); setCreateAndInviteLoader(false);
@ -90,82 +117,84 @@ const Contacts = () => {
return ( return (
<> <>
<div className='flex justify-end my-2 cursor-pointer'> <div className='px-1.5'>
<div <div className='flex justify-end my-2 cursor-pointer'>
onClick={() => setShowModal((prev) => !prev)} <div
className="flex items-center gap-2 bg-[var(--primary)] px-3 py-1.5 rounded-sm hover:opacity-90 "> onClick={() => setShowModal((prev) => !prev)}
<IoMdAdd className="text-white" /> className="flex items-center gap-2 bg-[var(--primary)] px-3 py-1.5 rounded-sm hover:opacity-90 ">
<button <IoMdAdd className="text-white" />
className="text-white rounded text-sm hover:opacity-90 cursor-pointer"> <button
Add Contact className="text-white rounded text-sm hover:opacity-90 cursor-pointer">
</button> Add Contact
</button>
</div>
</div> </div>
</div> <div className="overflow-y-auto max-h-[79vh] border border-gray-200 rounded shadow-sm no-scrollbar">
<div className="overflow-y-auto max-h-[79vh] border border-gray-200 rounded shadow-sm no-scrollbar"> <table className="min-w-full text-sm bg-white">
<table className="min-w-full text-sm bg-white"> <thead className="bg-gray-100 text-gray-700 uppercase text-xs tracking-wider sticky top-0 z-10">
<thead className="bg-gray-100 text-gray-700 uppercase text-xs tracking-wider sticky top-0 z-10">
<tr>
<th className="px-4 py-3 text-left">First Name</th>
<th className="px-4 py-3 text-left">Last Name</th>
<th className="px-4 py-3 text-left">Email</th>
<th className="px-4 py-3 text-center">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{loading ? (
<tr> <tr>
<td colSpan="4" className="text-center py-4">Loading...</td> <th className="px-4 py-3 text-left">First Name</th>
<th className="px-4 py-3 text-left">Last Name</th>
<th className="px-4 py-3 text-left">Email</th>
<th className="px-4 py-3 text-center">Actions</th>
</tr> </tr>
) : contacts.length === 0 ? ( </thead>
<tr> <tbody className="divide-y divide-gray-200">
<td colSpan="4" className="text-center py-4">No contacts found.</td> {loading ? (
</tr> <tr>
) : ( <td colSpan="4" className="relative h-100"><LoaderUi/></td>
contacts.map((contact) => (
<tr key={contact.id} className="hover:bg-gray-50">
<td className="px-4 py-3">{contact.firstName}</td>
<td className="px-4 py-3">{contact.lastName}</td>
<td className="px-4 py-3">{contact.email}</td>
<td className="px-4 py-3 text-center">
<div
className="relative inline-block"
tabIndex={0}
onBlur={handleBlur}
>
<button
className="text-gray-600 hover:text-black p-1 focus:outline-none"
onClick={() => toggleMenu(contact.id)}
>
<FaEllipsisV />
</button>
{openMenuId === contact.id && (
<div className="absolute top-full right-0 mt-1 bg-white border border-gray-200 rounded shadow-lg z-20 min-w-[100px]">
<button
onClick={() => handleEdit(contact.id)}
className="block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 text-blue-600"
>
Edit
</button>
<button
onClick={() => handleDelete(contact.id)}
className="block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 text-red-600"
>
Delete
</button>
</div>
)}
</div>
</td>
</tr> </tr>
)) ) : contacts.length === 0 ? (
)} <tr>
</tbody> <td colSpan="4" className="text-center py-4">No contacts found.</td>
</table> </tr>
{showModal && ) : (
<ContactFormModal onSubmit={handleContactSubmit} contacts.map((contact) => (
loading={createAndInviteLoader} setShowModal={setShowModal}/> <tr key={contact.id} className="hover:bg-gray-50">
} <td className="px-4 py-3">{contact.firstName}</td>
<td className="px-4 py-3">{contact.lastName}</td>
<td className="px-4 py-3">{contact.email}</td>
<td className="px-4 py-3 text-center">
<div
className="relative inline-block"
tabIndex={0}
onBlur={handleBlur}
>
<button
className="text-gray-600 hover:text-black p-1 focus:outline-none"
onClick={() => toggleMenu(contact.id)}
>
<FaEllipsisV />
</button>
{openMenuId === contact.id && (
<div className="absolute top-full right-0 mt-1 bg-white border border-gray-200 rounded shadow-lg z-20 min-w-[100px]">
<button
onMouseDown={() => handleEdit(contact.id)}
className="block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 text-blue-600"
>
Edit
</button>
<button
onMouseDown={() => handleDelete(contact.id)}
className="block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 text-red-600"
>
Delete
</button>
</div>
)}
</div>
</td>
</tr>
))
)}
</tbody>
</table>
{showModal &&
<ContactFormModal onSubmit={handleContactSubmit}
loading={createAndInviteLoader} setShowModal={setShowModal} actionStatus={actionStatus} editJsonData={editJsonData} />
}
</div>
</div> </div>
</> </>
); );

View File

@ -1,5 +1,5 @@
// ContactForm.tsx // ContactForm.tsx
import React, { useState } from 'react'; import React, { useEffect, useState } from 'react';
import { FaSpinner } from 'react-icons/fa'; import { FaSpinner } from 'react-icons/fa';
export interface ContactData { export interface ContactData {
@ -17,9 +17,11 @@ interface Props {
onSubmit: (data: ContactData) => void; onSubmit: (data: ContactData) => void;
loading: boolean; loading: boolean;
setShowModal: () => React.Dispatch<React.SetStateAction<boolean>> setShowModal: () => React.Dispatch<React.SetStateAction<boolean>>
actionStatus: string,
editJsonData: any
} }
const ContactFormModal = ({ onSubmit, loading, setShowModal }: Props) => { const ContactFormModal = ({ onSubmit, loading, setShowModal, actionStatus, editJsonData }: Props) => {
const [formData, setFormData] = useState<ContactData>({ const [formData, setFormData] = useState<ContactData>({
email: '', email: '',
firstName: '', firstName: '',
@ -32,6 +34,21 @@ const ContactFormModal = ({ onSubmit, loading, setShowModal }: Props) => {
const [errors, setErrors] = useState<{ [key: string]: string }>({}); const [errors, setErrors] = useState<{ [key: string]: string }>({});
useEffect(() => {
if (actionStatus === 'Edit' && editJsonData) {
setFormData({
email: editJsonData.email,
firstName: editJsonData.firstName,
lastName: editJsonData.lastName,
title: editJsonData.title,
company: editJsonData.company,
website: editJsonData.website,
phones: editJsonData.phones,
})
}
}, [actionStatus, editJsonData])
const handleChange = (field: keyof ContactData, value: string) => { const handleChange = (field: keyof ContactData, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value })); setFormData((prev) => ({ ...prev, [field]: value }));
if (errors[field]) { if (errors[field]) {
@ -115,13 +132,22 @@ const ContactFormModal = ({ onSubmit, loading, setShowModal }: Props) => {
const handleSubmit = async () => { const handleSubmit = async () => {
const validationErrors = validate(); const validationErrors = validate();
let type = 'Add'
let payload = formData
if (Object.keys(validationErrors).length > 0) { if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors); setErrors(validationErrors);
return; return;
} }
try { try {
await onSubmit(formData); if (actionStatus === 'Edit') {
type = 'Edit'
payload = {
...formData,
contactId: editJsonData.id
}
}
await onSubmit(payload, type);
setFormData({ setFormData({
email: '', email: '',
@ -158,7 +184,7 @@ const ContactFormModal = ({ onSubmit, loading, setShowModal }: Props) => {
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">{label}</label> <label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">{label}</label>
<input <input
type={field === 'email' ? 'email' : 'text'} type={field === 'email' ? 'email' : 'text'}
value={formData[field as keyof ContactData] as string} value={(formData[field as keyof ContactData] as string) || ''}
onChange={(e) => handleChange(field as keyof ContactData, e.target.value)} onChange={(e) => handleChange(field as keyof ContactData, e.target.value)}
required={true} required={true}
className="px-3 py-1.5 border border-gray-400 rounded-md focus:outline-none focus:ring-1 focus:ring-[var(--primary)] text-sm" className="px-3 py-1.5 border border-gray-400 rounded-md focus:outline-none focus:ring-1 focus:ring-[var(--primary)] text-sm"
@ -172,7 +198,7 @@ const ContactFormModal = ({ onSubmit, loading, setShowModal }: Props) => {
<div> <div>
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">Phone Numbers</label> <label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">Phone Numbers</label>
<div className="space-y-2"> <div className="space-y-2">
{formData.phones.map((phone, index) => ( {formData?.phones?.map((phone, index) => (
<div key={index} className="flex flex-col gap-1"> <div key={index} className="flex flex-col gap-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input <input
@ -229,8 +255,9 @@ const ContactFormModal = ({ onSubmit, loading, setShowModal }: Props) => {
SAVING... SAVING...
</> </>
) : ( ) : (
'SAVE CONTACT' `${actionStatus?.toUpperCase() || 'SAVE'} CONTACT`
)} )}
</button> </button>

View File

@ -1,4 +1,3 @@
import React from 'react'
import { LuSignature } from "react-icons/lu"; import { LuSignature } from "react-icons/lu";
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { RootState } from '../../store/store'; import { RootState } from '../../store/store';

View File

@ -139,7 +139,15 @@ const Templates = () => {
</tr> </tr>
</thead> </thead>
{templates.length > 0 ? ( {loading ? (
<tbody>
<tr>
<td colSpan="6" className="relative h-100">
<LoaderUi />
</td>
</tr>
</tbody>
) : templates.length > 0 ? (
<tbody className="divide-y divide-gray-200"> <tbody className="divide-y divide-gray-200">
{templates.map((item, index) => { {templates.map((item, index) => {
return ( return (
@ -200,18 +208,12 @@ const Templates = () => {
) : ( ) : (
<tbody> <tbody>
<tr> <tr>
<td colSpan="5"> <td colSpan="6" className="text-center py-10 text-gray-500">
<div className="relative flex justify-center items-center h-100 w-full"> No templates found.
{/* Pulsing background div */}
<div className="absolute inset-0 bg-gradient-to-r from-gray-200 to-white animate-pulse"></div>
{/* Loader stays on top, no animation */}
<LoaderUi classStyle="relative h-16 w-16" />
</div>
</td> </td>
</tr> </tr>
</tbody> </tbody>
)} )}
</table> </table>
</div> </div>

View File

@ -1,19 +1,22 @@
import { LuSignature } from "react-icons/lu";
interface StyleProps { interface StyleProps {
classStyle : string classStyle?: string;
} }
const LoaderUi = ({ classStyle }:StyleProps) => { const LoaderUi = ({ classStyle = '' }: StyleProps) => {
return ( return (
<div className={classStyle}> <div className={`absolute inset-0 flex items-center justify-center z-50 ${classStyle}`}>
<svg viewBox="0 0 240 240" height="240" width="240" className="pl"> {/* Overlay */}
<circle strokeLinecap="round" strokeDashoffset="-330" strokeDasharray="0 660" strokeWidth="20" stroke="#000" fill="none" r="105" cy="120" cx="120" className="pl__ring pl__ring--a"></circle> <div className="absolute inset-0 bg-black/30 opacity-60"></div>
<circle strokeLinecap="round" strokeDashoffset="-110" strokeDasharray="0 220" strokeWidth="20" stroke="#000" fill="none" r="35" cy="120" cx="120" className="pl__ring pl__ring--b"></circle>
<circle strokeLinecap="round" strokeDasharray="0 440" strokeWidth="20" stroke="#000" fill="none" r="70" cy="120" cx="85" className="pl__ring pl__ring--c"></circle>
<circle strokeLinecap="round" strokeDasharray="0 440" strokeWidth="20" stroke="#000" fill="none" r="70" cy="120" cx="155" className="pl__ring pl__ring--d"></circle>
</svg>
</div>
)
}
export default LoaderUi {/* Loader box */}
<div className="relative z-10 bg-[var(--loaderBg)] flex flex-col justify-center items-center gap-2 px-16 py-8 rounded-md shadow-lg">
<LuSignature className="text-4xl font-bold animate-text" />
<p>Loading...!</p>
</div>
</div>
);
};
export default LoaderUi;