completely refactored directory module
This commit is contained in:
parent
b66106c301
commit
d6de8cbfc1
@ -16,27 +16,15 @@ const CardViewContact = ({
|
|||||||
IsDeleted,
|
IsDeleted,
|
||||||
restore,
|
restore,
|
||||||
}) => {
|
}) => {
|
||||||
const { data , setManageContact,setContactOpen} = useDirectoryContext();
|
const { data, setManageContact, setContactOpen } = useDirectoryContext();
|
||||||
const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
|
|
||||||
const {mutate:ActiveInActive,isPending} = useActiveInActiveContact()
|
const { mutate: ActiveInActive, isPending } = useActiveInActiveContact();
|
||||||
const handleActiveInactive = (contactId) => {
|
const handleActiveInactive = (contactId) => {
|
||||||
ActiveInActive({ contactId, contactStatus: !IsActive });
|
ActiveInActive({ contactId, contactStatus: !IsActive });
|
||||||
};
|
};
|
||||||
const {dirActions} = useDir()
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{IsDeleteModalOpen && (
|
|
||||||
<div
|
|
||||||
className={`modal fade show`}
|
|
||||||
tabIndex="-1"
|
|
||||||
role="dialog"
|
|
||||||
style={{
|
|
||||||
display: "block",
|
|
||||||
backgroundColor: "rgba(0,0,0,0.5)",
|
|
||||||
}}
|
|
||||||
aria-hidden="false"
|
|
||||||
>
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
type="delete"
|
type="delete"
|
||||||
header="Delete Contact"
|
header="Delete Contact"
|
||||||
@ -45,9 +33,8 @@ const {dirActions} = useDir()
|
|||||||
onClose={() => setIsDeleteModalOpen(false)}
|
onClose={() => setIsDeleteModalOpen(false)}
|
||||||
loading={isPending}
|
loading={isPending}
|
||||||
paramData={contact.id}
|
paramData={contact.id}
|
||||||
|
isOpen={IsDeleteModalOpen}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="card text-start border-1"
|
className="card text-start border-1"
|
||||||
@ -61,7 +48,7 @@ const {dirActions} = useDir()
|
|||||||
}`}
|
}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (IsActive) {
|
if (IsActive) {
|
||||||
setContactOpen({contact:contact,Open:true});
|
setContactOpen({ contact: contact, Open: true });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -96,7 +83,12 @@ const {dirActions} = useDir()
|
|||||||
</button>
|
</button>
|
||||||
<ul className="dropdown-menu dropdown-menu-end w-auto">
|
<ul className="dropdown-menu dropdown-menu-end w-auto">
|
||||||
<li
|
<li
|
||||||
onClick={()=>setManageContact({isOpen:true,contactId:contact?.id})}
|
onClick={() =>
|
||||||
|
setManageContact({
|
||||||
|
isOpen: true,
|
||||||
|
contactId: contact?.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<a className="dropdown-item px-2 cursor-pointer py-1">
|
<a className="dropdown-item px-2 cursor-pointer py-1">
|
||||||
<i className="bx bx-edit bx-xs text-primary me-2"></i>
|
<i className="bx bx-edit bx-xs text-primary me-2"></i>
|
||||||
@ -118,12 +110,10 @@ const {dirActions} = useDir()
|
|||||||
{!IsActive && (
|
{!IsActive && (
|
||||||
<i
|
<i
|
||||||
className={`bx ${
|
className={`bx ${
|
||||||
isPending
|
isPending ? "bx-loader-alt bx-spin" : "bx-recycle"
|
||||||
? "bx-loader-alt bx-spin"
|
|
||||||
: "bx-recycle"
|
|
||||||
} me-1 text-primary cursor-pointer`}
|
} me-1 text-primary cursor-pointer`}
|
||||||
title="Restore"
|
title="Restore"
|
||||||
onClick={()=>handleActiveInactive(contact.id)}
|
onClick={() => handleActiveInactive(contact.id)}
|
||||||
></i>
|
></i>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
12
src/components/Directory/ContactNotes.jsx
Normal file
12
src/components/Directory/ContactNotes.jsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { useContactNotes1 } from '../../hooks/useDirectory'
|
||||||
|
|
||||||
|
const ContactNotes = ({contactId}) => {
|
||||||
|
|
||||||
|
const {} = useContactNotes1(contactId)
|
||||||
|
return (
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ContactNotes
|
||||||
@ -1,18 +1,16 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useContactProfile } from "../../hooks/useDirectory";
|
import { useContactNotes, useContactProfile1 } from "../../hooks/useDirectory";
|
||||||
|
import { ContactProfileSkeleton } from "./DirectoryPageSkeleton";
|
||||||
import Avatar from "../common/Avatar";
|
import Avatar from "../common/Avatar";
|
||||||
import moment from "moment";
|
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||||
import NotesDirectory from "./NotesDirectory";
|
import NotesDirectory from "./NotesDirectory";
|
||||||
|
|
||||||
const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
|
const ContactProfile = ({ contactId }) => {
|
||||||
const { contactProfile, loading, refetch } = useContactProfile(contact?.id);
|
const { data, isError, isLoading, error } = useContactProfile1(contactId.id);
|
||||||
const [copiedIndex, setCopiedIndex] = useState(null);
|
const [copiedIndex, setCopiedIndex] = useState(null);
|
||||||
|
|
||||||
const [profileContactState, setProfileContactState] = useState(null);
|
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
|
||||||
// Safely access description, defaulting to an empty string if not present
|
const description = data?.description || "";
|
||||||
const description = profileContactState?.description || "";
|
|
||||||
const limit = 500;
|
const limit = 500;
|
||||||
|
|
||||||
const toggleReadMore = () => setExpanded(!expanded);
|
const toggleReadMore = () => setExpanded(!expanded);
|
||||||
@ -21,50 +19,8 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
|
|||||||
const displayText = expanded
|
const displayText = expanded
|
||||||
? description
|
? description
|
||||||
: description.slice(0, limit) + (isLong ? "..." : "");
|
: description.slice(0, limit) + (isLong ? "..." : "");
|
||||||
|
if (isError) return <div>{error.message}</div>;
|
||||||
useEffect(() => {
|
if (isLoading) return <ContactProfileSkeleton />;
|
||||||
if (contactProfile) {
|
|
||||||
const names = (contact?.name || "").trim().split(" ");
|
|
||||||
let firstName = "";
|
|
||||||
let middleName = "";
|
|
||||||
let lastName = "";
|
|
||||||
let fullName = contact?.name || "";
|
|
||||||
|
|
||||||
// Logic to determine first, middle, and last names
|
|
||||||
if (names.length === 1) {
|
|
||||||
firstName = names[0];
|
|
||||||
} else if (names.length === 2) {
|
|
||||||
firstName = names[0];
|
|
||||||
lastName = names[1];
|
|
||||||
} else if (names.length >= 3) {
|
|
||||||
firstName = names[0];
|
|
||||||
middleName = names[1]; // This was an error in the original prompt, corrected to names[1]
|
|
||||||
lastName = names[names.length - 1];
|
|
||||||
// Reconstruct full name to be precise with spacing
|
|
||||||
fullName = `${firstName} ${middleName ? middleName + " " : ""}${lastName}`;
|
|
||||||
} else {
|
|
||||||
// Fallback if no names or empty string
|
|
||||||
firstName = "Contact";
|
|
||||||
fullName = "Contact";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
setProfileContactState({
|
|
||||||
...contactProfile,
|
|
||||||
firstName: contactProfile.firstName || firstName,
|
|
||||||
// Adding middleName and lastName to the state for potential future use or more granular access
|
|
||||||
middleName: contactProfile.middleName || middleName,
|
|
||||||
lastName: contactProfile.lastName || lastName,
|
|
||||||
fullName: contactProfile.fullName || fullName, // Prioritize fetched fullName, fallback to derived
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [contactProfile, contact?.name]);
|
|
||||||
|
|
||||||
const handleCopy = (email, index) => {
|
|
||||||
navigator.clipboard.writeText(email);
|
|
||||||
setCopiedIndex(index);
|
|
||||||
setTimeout(() => setCopiedIndex(null), 2000);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-1">
|
<div className="p-1">
|
||||||
@ -76,23 +32,19 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
|
|||||||
<Avatar
|
<Avatar
|
||||||
size="sm"
|
size="sm"
|
||||||
classAvatar="m-0"
|
classAvatar="m-0"
|
||||||
firstName={
|
firstName={(data?.name || "").trim().split(" ")[0]?.charAt(0) || ""}
|
||||||
(contact?.name || "").trim().split(" ")[0]?.charAt(0) || ""
|
lastName={(data?.name || "").trim().split(" ")[1]?.charAt(0) || ""}
|
||||||
}
|
|
||||||
lastName={
|
|
||||||
(contact?.name || "").trim().split(" ")[1]?.charAt(0) || ""
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<div className="d-flex flex-column text-start ms-1">
|
<div className="d-flex flex-column text-start ms-1">
|
||||||
<span className="m-0 fw-semibold">{contact?.name}</span>
|
<span className="m-0 fw-semibold">{data?.name}</span>
|
||||||
<small className="text-secondary small-text">
|
<small className="text-secondary small-text">
|
||||||
{profileContactState?.designation}
|
{data?.designation}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="row ms-9">
|
<div className="row ms-9">
|
||||||
<div className="col-12 col-md-6 d-flex flex-column text-start">
|
<div className="col-12 col-md-6 d-flex flex-column text-start">
|
||||||
{profileContactState?.contactEmails?.length > 0 && (
|
{data?.contactEmails?.length > 0 && (
|
||||||
<div className="d-flex mb-2 align-items-start">
|
<div className="d-flex mb-2 align-items-start">
|
||||||
<div
|
<div
|
||||||
className="d-flex align-items-start"
|
className="d-flex align-items-start"
|
||||||
@ -107,13 +59,16 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
|
|||||||
|
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<ul className="list-unstyled mb-0">
|
<ul className="list-unstyled mb-0">
|
||||||
{profileContactState.contactEmails.map((email, idx) => (
|
{data.contactEmails.map((email, idx) => (
|
||||||
<li className="d-flex align-items-center mb-1" key={idx}>
|
<li className="d-flex align-items-center mb-1" key={idx}>
|
||||||
<span className="me-1 text-break overflow-wrap">
|
<span className="me-1 text-break overflow-wrap">
|
||||||
{email.emailAddress}
|
{email.emailAddress}
|
||||||
</span>
|
</span>
|
||||||
<i
|
<i
|
||||||
className={`bx bx-copy-alt cursor-pointer bx-xs text-start ${copiedIndex === idx ? "text-secondary" : "text-primary"
|
className={`bx bx-copy-alt cursor-pointer bx-xs text-start ${
|
||||||
|
copiedIndex === idx
|
||||||
|
? "text-secondary"
|
||||||
|
: "text-primary"
|
||||||
}`}
|
}`}
|
||||||
title={copiedIndex === idx ? "Copied!" : "Copy Email"}
|
title={copiedIndex === idx ? "Copied!" : "Copy Email"}
|
||||||
style={{ flexShrink: 0 }}
|
style={{ flexShrink: 0 }}
|
||||||
@ -126,7 +81,7 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{profileContactState?.contactPhones?.length > 0 && (
|
{data?.contactPhones?.length > 0 && (
|
||||||
<div className="d-flex mb-2 align-items-start">
|
<div className="d-flex mb-2 align-items-start">
|
||||||
<div className="d-flex" style={{ minWidth: "130px" }}>
|
<div className="d-flex" style={{ minWidth: "130px" }}>
|
||||||
<span className="d-flex align-items-center">
|
<span className="d-flex align-items-center">
|
||||||
@ -138,10 +93,10 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<ul className="list-inline mb-0">
|
<ul className="list-inline mb-0">
|
||||||
{profileContactState.contactPhones.map((phone, idx) => (
|
{data.contactPhones.map((phone, idx) => (
|
||||||
<li className="list-inline-item me-1" key={idx}>
|
<li className="list-inline-item me-1" key={idx}>
|
||||||
{phone.phoneNumber}
|
{phone.phoneNumber}
|
||||||
{idx < profileContactState.contactPhones.length - 1 && ","}
|
{idx < data.contactPhones.length - 1 && ","}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@ -149,7 +104,7 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{profileContactState?.createdAt && (
|
{data?.createdAt && (
|
||||||
<div className="d-flex mb-2 align-items-start">
|
<div className="d-flex mb-2 align-items-start">
|
||||||
<div className="d-flex" style={{ minWidth: "130px" }}>
|
<div className="d-flex" style={{ minWidth: "130px" }}>
|
||||||
<span className="d-flex align-items-center">
|
<span className="d-flex align-items-center">
|
||||||
@ -160,14 +115,12 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="d-flex align-items-center">
|
<div className="d-flex align-items-center">
|
||||||
<span>
|
<span>{formatUTCToLocalTime(data.createdAt)}</span>
|
||||||
{moment(profileContactState.createdAt).format("DD MMMM, YYYY")}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{profileContactState?.address && (
|
{data?.address && (
|
||||||
<div className="d-flex mb-2 align-items-start">
|
<div className="d-flex mb-2 align-items-start">
|
||||||
<div className="d-flex" style={{ minWidth: "130px" }}>
|
<div className="d-flex" style={{ minWidth: "130px" }}>
|
||||||
<span className="d-flex align-items-start">
|
<span className="d-flex align-items-start">
|
||||||
@ -177,14 +130,14 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
|
|||||||
<span style={{ marginLeft: "26px" }}>:</span>
|
<span style={{ marginLeft: "26px" }}>:</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-break small">{profileContactState.address}</span>
|
<span className="text-break small">{data.address}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-12 col-md-6 d-flex flex-column text-start">
|
<div className="col-12 col-md-6 d-flex flex-column text-start">
|
||||||
{profileContactState?.organization && (
|
{data?.organization && (
|
||||||
<div className="d-flex mb-2 align-items-start">
|
<div className="d-flex mb-2 align-items-start">
|
||||||
<div className="d-flex" style={{ minWidth: "130px" }}>
|
<div className="d-flex" style={{ minWidth: "130px" }}>
|
||||||
<span className="d-flex align-items-center">
|
<span className="d-flex align-items-center">
|
||||||
@ -196,13 +149,13 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
|
|||||||
|
|
||||||
<div className="d-flex align-items-center">
|
<div className="d-flex align-items-center">
|
||||||
<span style={{ wordBreak: "break-word" }}>
|
<span style={{ wordBreak: "break-word" }}>
|
||||||
{profileContactState.organization}
|
{data.organization}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{profileContactState?.contactCategory && (
|
{data?.contactCategory && (
|
||||||
<div className="d-flex mb-2 align-items-start">
|
<div className="d-flex mb-2 align-items-start">
|
||||||
<div className="d-flex" style={{ minWidth: "130px" }}>
|
<div className="d-flex" style={{ minWidth: "130px" }}>
|
||||||
<span className="d-flex align-items-center">
|
<span className="d-flex align-items-center">
|
||||||
@ -215,14 +168,14 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
|
|||||||
<div>
|
<div>
|
||||||
<ul className="list-inline mb-0">
|
<ul className="list-inline mb-0">
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
{profileContactState.contactCategory.name}
|
{data.contactCategory.name}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{profileContactState?.tags?.length > 0 && (
|
{data?.tags?.length > 0 && (
|
||||||
<div className="d-flex mb-2 align-items-start">
|
<div className="d-flex mb-2 align-items-start">
|
||||||
<div className="d-flex" style={{ minWidth: "130px" }}>
|
<div className="d-flex" style={{ minWidth: "130px" }}>
|
||||||
<span className="d-flex align-items-center">
|
<span className="d-flex align-items-center">
|
||||||
@ -234,7 +187,7 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<ul className="list-inline mb-0">
|
<ul className="list-inline mb-0">
|
||||||
{profileContactState.tags.map((tag, index) => (
|
{data.tags.map((tag, index) => (
|
||||||
<li key={index} className="list-inline-item">
|
<li key={index} className="list-inline-item">
|
||||||
{tag.name}
|
{tag.name}
|
||||||
</li>
|
</li>
|
||||||
@ -244,7 +197,7 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{profileContactState?.buckets?.length > 0 && (
|
{data?.buckets?.length > 0 && (
|
||||||
<div className="d-flex mb-2 align-items-start">
|
<div className="d-flex mb-2 align-items-start">
|
||||||
<div className="d-flex" style={{ minWidth: "130px" }}>
|
<div className="d-flex" style={{ minWidth: "130px" }}>
|
||||||
<span className="d-flex align-items-center">
|
<span className="d-flex align-items-center">
|
||||||
@ -256,7 +209,7 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<ul className="list-inline mb-0">
|
<ul className="list-inline mb-0">
|
||||||
{profileContactState.buckets.map((bucket) => (
|
{data.buckets.map((bucket) => (
|
||||||
<li className="list-inline-item me-2" key={bucket.id}>
|
<li className="list-inline-item me-2" key={bucket.id}>
|
||||||
<span className="badge bg-label-primary my-1">
|
<span className="badge bg-label-primary my-1">
|
||||||
{bucket.name}
|
{bucket.name}
|
||||||
@ -269,7 +222,7 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{profileContactState?.projects?.length > 0 && (
|
{data?.projects?.length > 0 && (
|
||||||
<div className="d-flex mb-2 align-items-start">
|
<div className="d-flex mb-2 align-items-start">
|
||||||
<div className="d-flex" style={{ minWidth: "130px" }}>
|
<div className="d-flex" style={{ minWidth: "130px" }}>
|
||||||
<span className="d-flex align-items-center">
|
<span className="d-flex align-items-center">
|
||||||
@ -280,11 +233,11 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-start">
|
<div className="text-start">
|
||||||
<ul className="list-inline mb-0">
|
<ul className="list-inline text-wrap mb-0">
|
||||||
{profileContactState.projects.map((project, index) => (
|
{data.projects.map((project, index) => (
|
||||||
<li className="list-inline-item me-2" key={project.id}>
|
<li className="list-inline-item me-2" key={project.id}>
|
||||||
{project.name}
|
{project.name}
|
||||||
{index < profileContactState.projects.length - 1 && ","}
|
{index < data.projects.length - 1 && ","}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@ -324,15 +277,10 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<hr className="my-1" />
|
<hr className="my-1" />
|
||||||
<NotesDirectory
|
<NotesDirectory contactId={data?.id} />
|
||||||
refetchProfile={refetch}
|
|
||||||
isLoading={loading}
|
|
||||||
contactProfile={profileContactState}
|
|
||||||
setProfileContact={setProfileContactState}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProfileContactDirectory;
|
export default ContactProfile;
|
||||||
275
src/components/Directory/DirectoryPageSkeleton.jsx
Normal file
275
src/components/Directory/DirectoryPageSkeleton.jsx
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const SkeletonLine = ({ height = 20, width = "100%", className = "" }) => (
|
||||||
|
<div
|
||||||
|
className={`skeleton mb-2 ${className}`}
|
||||||
|
style={{
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
borderRadius: "4px",
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const NoteCardSkeleton = () => {
|
||||||
|
return (
|
||||||
|
<div className="mt-5">
|
||||||
|
{Array.from({ length: 3 }).map((_, idx) => (
|
||||||
|
<div key={idx} className="card shadow-sm border-1 mb-3 p-3 rounded">
|
||||||
|
<div className="d-flex justify-content-between align-items-center mb-1">
|
||||||
|
<div className="d-flex align-items-center">
|
||||||
|
<div
|
||||||
|
className="skeleton rounded-circle me-2"
|
||||||
|
style={{ width: 32, height: 32 }}
|
||||||
|
></div>
|
||||||
|
<div>
|
||||||
|
<SkeletonLine height={10} width="150px" />
|
||||||
|
<SkeletonLine height={10} width="100px" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Icons */}
|
||||||
|
<div className="d-flex gap-2">
|
||||||
|
<div
|
||||||
|
className="skeleton rounded"
|
||||||
|
style={{ width: 20, height: 20 }}
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
className="skeleton rounded"
|
||||||
|
style={{ width: 20, height: 20 }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr className="mt-0 mb-2" />
|
||||||
|
|
||||||
|
{/* Note Content */}
|
||||||
|
<div className="mx-4 px-2 text-start">
|
||||||
|
<SkeletonLine height={16} width="90%" />
|
||||||
|
<SkeletonLine height={16} width="80%" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MainDirectoryPageSkeleton = () => {
|
||||||
|
return (
|
||||||
|
<div className="container-fluid">
|
||||||
|
<div className="mt-5 card shadow-sm border-1 mb-3 p-3 rounded">
|
||||||
|
{/* Tabs & Export */}
|
||||||
|
<div className="d-flex justify-content-between align-items-center mb-3 px-2">
|
||||||
|
<div className="d-flex gap-3">
|
||||||
|
<SkeletonLine height={30} width="80px" />
|
||||||
|
<SkeletonLine height={30} width="90px" />
|
||||||
|
</div>
|
||||||
|
<SkeletonLine height={30} width="100px" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search / Controls */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<NoteCardSkeleton />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
// 32702.75
|
||||||
|
|
||||||
|
// Skeleton for ListViewContact
|
||||||
|
export const ListViewContactSkeleton = ({ rows = 5 }) => {
|
||||||
|
const columns = ["Name", "Email", "Organization", "Category", "Action"];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-5">
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-datatable table-responsive">
|
||||||
|
<table className="table border-top dataTable text-nowrap">
|
||||||
|
<thead>
|
||||||
|
<tr className="shadow-sm">
|
||||||
|
{columns.map((col) => (
|
||||||
|
<th key={col} className="text-center">
|
||||||
|
<SkeletonLine height={20} width="80px" />
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="px-2">
|
||||||
|
{Array.from({ length: rows }).map((_, idx) => (
|
||||||
|
<tr key={idx}>
|
||||||
|
{/* Name / Avatar */}
|
||||||
|
<td className="px-2 py-3">
|
||||||
|
<div className="d-flex align-items-center gap-2">
|
||||||
|
<div
|
||||||
|
className="skeleton rounded-circle"
|
||||||
|
style={{ width: 32, height: 32 }}
|
||||||
|
></div>
|
||||||
|
<SkeletonLine height={12} width="100px" />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{/* Email */}
|
||||||
|
<td className="px-2 py-3">
|
||||||
|
<SkeletonLine height={12} width="120px" />
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{/* Organization */}
|
||||||
|
<td className="px-2 py-3">
|
||||||
|
<SkeletonLine height={12} width="120px" />
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{/* Category */}
|
||||||
|
<td className="px-2 py-3">
|
||||||
|
<SkeletonLine height={12} width="100px" />
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<td className="px-2 py-3 text-center">
|
||||||
|
<div className="d-flex justify-content-center gap-2">
|
||||||
|
<div
|
||||||
|
className="skeleton rounded"
|
||||||
|
style={{ width: 20, height: 20 }}
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
className="skeleton rounded"
|
||||||
|
style={{ width: 20, height: 20 }}
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
className="skeleton rounded"
|
||||||
|
style={{ width: 20, height: 20 }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CardViewContactSkeleton = ({ rows = 6 }) => {
|
||||||
|
return (
|
||||||
|
<div className="row mt-3">
|
||||||
|
{Array.from({ length: rows }).map((_, idx) => (
|
||||||
|
<div key={idx} className="col-12 col-sm-6 col-md-4 col-lg-4 mb-4">
|
||||||
|
<div className="card text-start border-1 h-100">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="card-body px-2 py-2">
|
||||||
|
<div className="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<div className="d-flex align-items-center gap-2">
|
||||||
|
<div
|
||||||
|
className="skeleton rounded-circle"
|
||||||
|
style={{ width: 32, height: 32 }}
|
||||||
|
/>
|
||||||
|
<SkeletonLine height={14} width="120px" />
|
||||||
|
</div>
|
||||||
|
<div className="d-flex gap-2">
|
||||||
|
<div
|
||||||
|
className="skeleton rounded"
|
||||||
|
style={{ width: 20, height: 20 }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="skeleton rounded"
|
||||||
|
style={{ width: 20, height: 20 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<SkeletonLine height={12} width="150px" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="card-footer px-3 py-2">
|
||||||
|
<SkeletonLine height={12} width="80%" className="mb-1" />
|
||||||
|
<SkeletonLine height={12} width="60%" className="mb-1" />
|
||||||
|
<SkeletonLine height={12} width="70%" className="mb-1" />
|
||||||
|
<div className="d-flex gap-1 mt-1">
|
||||||
|
{Array.from({ length: 3 }).map((_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="skeleton rounded"
|
||||||
|
style={{ width: 50, height: 20 }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const ContactProfileSkeleton = () => {
|
||||||
|
return (
|
||||||
|
<div className="p-1">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="text-center m-0 p-0 mb-3">
|
||||||
|
<SkeletonLine width="120px" height={20} className="mx-auto" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Avatar and Name */}
|
||||||
|
<div className="d-flex align-items-center mb-3">
|
||||||
|
<div className="skeleton rounded-circle" style={{ width: 40, height: 40 }} />
|
||||||
|
<div className="d-flex flex-column text-start ms-2">
|
||||||
|
<SkeletonLine width="120px" height={14} />
|
||||||
|
<SkeletonLine width="80px" height={12} className="mt-1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Two-column details */}
|
||||||
|
<div className="row ms-9">
|
||||||
|
<div className="col-12 col-md-6 d-flex flex-column text-start">
|
||||||
|
{Array.from({ length: 5 }).map((_, idx) => (
|
||||||
|
<div key={idx} className="d-flex mb-2 align-items-start">
|
||||||
|
<SkeletonLine width="100px" height={12} className="me-2" />
|
||||||
|
<SkeletonLine width="150px" height={12} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-12 col-md-6 d-flex flex-column text-start">
|
||||||
|
{Array.from({ length: 5 }).map((_, idx) => (
|
||||||
|
<div key={idx} className="d-flex mb-2 align-items-start">
|
||||||
|
<SkeletonLine width="100px" height={12} className="me-2" />
|
||||||
|
<SkeletonLine width="150px" height={12} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Projects */}
|
||||||
|
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<div className="d-flex mb-2 align-items-start" style={{ marginLeft: "3rem" }}>
|
||||||
|
<SkeletonLine width="100px" height={12} className="me-2" />
|
||||||
|
<SkeletonLine width="100%" height={50} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr className="my-1" />
|
||||||
|
|
||||||
|
{/* Notes Section */}
|
||||||
|
{Array.from({ length: 3 }).map((_, idx) => (
|
||||||
|
<div key={idx} className="mb-2">
|
||||||
|
<SkeletonLine width="100%" height={60} className="mb-1" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NoetCard =({cards = 2})=>{
|
||||||
|
return(
|
||||||
|
<div className="row">
|
||||||
|
{Array.from({ length: cards }).map((_, idx) => (
|
||||||
|
<div key={idx} className="mb-2">
|
||||||
|
<SkeletonLine width="100%" height={60} className="mb-1" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -87,17 +87,6 @@ const ListViewContact = ({ data, Pagination }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{deleteContact.Open && (
|
|
||||||
<div
|
|
||||||
className={`modal fade show`}
|
|
||||||
tabIndex="-1"
|
|
||||||
role="dialog"
|
|
||||||
style={{
|
|
||||||
display: "block",
|
|
||||||
backgroundColor: "rgba(0,0,0,0.5)",
|
|
||||||
}}
|
|
||||||
aria-hidden="false"
|
|
||||||
>
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
type="delete"
|
type="delete"
|
||||||
header="Delete Contact"
|
header="Delete Contact"
|
||||||
@ -106,9 +95,8 @@ const ListViewContact = ({ data, Pagination }) => {
|
|||||||
onClose={() => setDeleteContact({ contactId: null, Open: false })}
|
onClose={() => setDeleteContact({ contactId: null, Open: false })}
|
||||||
loading={isPending}
|
loading={isPending}
|
||||||
paramData={deleteContact.contactId}
|
paramData={deleteContact.contactId}
|
||||||
|
isOpen={deleteContact.Open}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="card ">
|
<div className="card ">
|
||||||
<div
|
<div
|
||||||
className="card-datatable table-responsive"
|
className="card-datatable table-responsive"
|
||||||
|
|||||||
@ -1,138 +0,0 @@
|
|||||||
import React, { useEffect } from "react";
|
|
||||||
import Avatar from "../common/Avatar";
|
|
||||||
import { getEmailIcon, getPhoneIcon } from "./DirectoryUtils";
|
|
||||||
import { useDir } from "../../Context/DireContext";
|
|
||||||
|
|
||||||
const ListViewDirectory = ({
|
|
||||||
IsActive,
|
|
||||||
contact,
|
|
||||||
setSelectedContact,
|
|
||||||
setIsOpenModal,
|
|
||||||
setOpen_contact,
|
|
||||||
setIsOpenModalNote,
|
|
||||||
IsDeleted,
|
|
||||||
restore,
|
|
||||||
}) => {
|
|
||||||
const { dirActions, setDirActions } = useDir();
|
|
||||||
|
|
||||||
// Get the first email and phone number if they exist
|
|
||||||
const firstEmail = contact.contactEmails?.[0];
|
|
||||||
const firstPhone = contact.contactPhones?.[0];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr className={!IsActive ? "bg-light" : ""}>
|
|
||||||
<td
|
|
||||||
className="text-start cursor-pointer"
|
|
||||||
style={{ width: "18%" }}
|
|
||||||
colSpan={2}
|
|
||||||
onClick={() => {
|
|
||||||
if (IsActive) {
|
|
||||||
setIsOpenModalNote(true);
|
|
||||||
setOpen_contact(contact);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="d-flex align-items-center">
|
|
||||||
<Avatar
|
|
||||||
size="xs"
|
|
||||||
classAvatar="m-0"
|
|
||||||
firstName={
|
|
||||||
(contact?.name || "").trim().split(" ")[0]?.charAt(0) || ""
|
|
||||||
}
|
|
||||||
lastName={
|
|
||||||
(contact?.name || "").trim().split(" ")[1]?.charAt(0) || ""
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<span className="text-truncate mx-0" style={{ maxWidth: "150px" }}>
|
|
||||||
{contact?.name || ""}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td className="px-2" style={{ width: "20%" }}>
|
|
||||||
<div className="d-flex flex-column align-items-start text-truncate">
|
|
||||||
{firstEmail ? (
|
|
||||||
<span key={firstEmail.id} className="text-truncate">
|
|
||||||
<i
|
|
||||||
className={getEmailIcon(firstEmail.label)}
|
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
></i>
|
|
||||||
<a
|
|
||||||
href={`mailto:${firstEmail.emailAddress}`}
|
|
||||||
className="text-decoration-none ms-1"
|
|
||||||
>
|
|
||||||
{firstEmail.emailAddress}
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="small-text m-0 px-2">NA</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td className="px-2" style={{ width: "20%" }}>
|
|
||||||
<div className="d-flex flex-column align-items-start text-truncate">
|
|
||||||
{firstPhone ? (
|
|
||||||
<span key={firstPhone.id}>
|
|
||||||
<i
|
|
||||||
className={getPhoneIcon(firstPhone.label)}
|
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
></i>
|
|
||||||
<span className="ms-1">{firstPhone.phoneNumber}</span>
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-small m-0 px-2">NA</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td
|
|
||||||
colSpan={2}
|
|
||||||
className="text-start text-truncate px-2"
|
|
||||||
style={{ width: "20%", maxWidth: "200px" }}
|
|
||||||
>
|
|
||||||
{contact.organization}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td className="px-2" style={{ width: "10%" }}>
|
|
||||||
<span className="text-truncate">
|
|
||||||
{contact?.contactCategory?.name || "Other"}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td className="align-middle text-center" style={{ width: "12%" }}>
|
|
||||||
{IsActive && (
|
|
||||||
<>
|
|
||||||
<i
|
|
||||||
className="bx bx-edit bx-sm text-primary cursor-pointer me-2"
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedContact(contact);
|
|
||||||
setIsOpenModal(true);
|
|
||||||
}}
|
|
||||||
></i>
|
|
||||||
<i
|
|
||||||
className="bx bx-trash bx-sm text-danger cursor-pointer"
|
|
||||||
onClick={() => IsDeleted(contact.id)}
|
|
||||||
></i>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!IsActive && (
|
|
||||||
<i
|
|
||||||
className={`bx ${
|
|
||||||
dirActions.action && dirActions.id === contact.id
|
|
||||||
? "bx-loader-alt bx-spin"
|
|
||||||
: "bx-recycle"
|
|
||||||
} me-1 text-primary cursor-pointer`}
|
|
||||||
title="Restore"
|
|
||||||
onClick={() => {
|
|
||||||
setDirActions({ action: false, id: contact.id });
|
|
||||||
restore(contact.id);
|
|
||||||
}}
|
|
||||||
></i>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ListViewDirectory;
|
|
||||||
@ -1,421 +0,0 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import IconButton from "../common/IconButton";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { bucketScheam } from "./DirectorySchema";
|
|
||||||
import showToast from "../../services/toastService";
|
|
||||||
import Directory from "../../pages/Directory/Directory";
|
|
||||||
import { DirectoryRepository } from "../../repositories/DirectoryRepository";
|
|
||||||
import { cacheData, getCachedData } from "../../slices/apiDataManager";
|
|
||||||
import { useBuckets, useCreateBucket, useUpdateBucket } from "../../hooks/useDirectory";
|
|
||||||
import EmployeeList from "./EmployeeList";
|
|
||||||
import { useAllEmployees, useEmployees } from "../../hooks/useEmployees";
|
|
||||||
import { useSortableData } from "../../hooks/useSortableData";
|
|
||||||
import ConfirmModal from "../common/ConfirmModal";
|
|
||||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
|
||||||
import { DIRECTORY_ADMIN, DIRECTORY_MANAGER } from "../../utils/constants";
|
|
||||||
import { useProfile } from "../../hooks/useProfile";
|
|
||||||
|
|
||||||
const ManageBucket = () => {
|
|
||||||
const { profile } = useProfile();
|
|
||||||
const [bucketList, setBucketList] = useState([]);
|
|
||||||
const { employeesList } = useAllEmployees(false);
|
|
||||||
const [selectedEmployee, setSelectEmployee] = useState([]);
|
|
||||||
const { buckets, loading, refetch } = useBuckets();
|
|
||||||
const [action_bucket, setAction_bucket] = useState(false);
|
|
||||||
const [isSubmitting, setSubmitting] = useState(false);
|
|
||||||
const [selected_bucket, select_bucket] = useState(null);
|
|
||||||
const [deleteBucket, setDeleteBucket] = useState(null);
|
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
|
||||||
const DirManager = useHasUserPermission(DIRECTORY_MANAGER);
|
|
||||||
const DirAdmin = useHasUserPermission(DIRECTORY_ADMIN);
|
|
||||||
const {
|
|
||||||
items: sortedBuckteList,
|
|
||||||
requestSort,
|
|
||||||
sortConfig,
|
|
||||||
} = useSortableData(bucketList, {
|
|
||||||
key: (e) => `${e.name}`,
|
|
||||||
direction: "asc",
|
|
||||||
});
|
|
||||||
|
|
||||||
const { mutate: handleCreatedBucket, isPending: creatingBucket } =
|
|
||||||
useCreateBucket();
|
|
||||||
|
|
||||||
const {mutate:UpdateBucket,isPending:updatingBucket} = useUpdateBucket()
|
|
||||||
const getSortIcon = () => {
|
|
||||||
if (!sortConfig) return null;
|
|
||||||
return sortConfig.direction === "asc" ? (
|
|
||||||
<i className="bx bx-caret-up text-secondary"></i>
|
|
||||||
) : (
|
|
||||||
<i className="bx bx-caret-down text-secondary"></i>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
reset,
|
|
||||||
formState: { errors },
|
|
||||||
} = useForm({
|
|
||||||
resolver: zodResolver(bucketScheam),
|
|
||||||
defaultValues: {
|
|
||||||
name: "",
|
|
||||||
description: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = async (data) => {
|
|
||||||
setSubmitting(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const cache_buckets = getCachedData("buckets") || [];
|
|
||||||
let response;
|
|
||||||
|
|
||||||
const arraysAreEqual = (a, b) => {
|
|
||||||
if (a.length !== b.length) return false;
|
|
||||||
const setA = new Set(a);
|
|
||||||
const setB = new Set(b);
|
|
||||||
return [...setA].every((id) => setB.has(id));
|
|
||||||
};
|
|
||||||
|
|
||||||
if (selected_bucket) {
|
|
||||||
const BucketPayload = { ...data, id: selected_bucket.id };
|
|
||||||
|
|
||||||
response = await DirectoryRepository.UpdateBuckets(
|
|
||||||
selected_bucket.id,
|
|
||||||
payload
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatedBuckets = cache_buckets.map((bucket) =>
|
|
||||||
bucket.id === selected_bucket.id ? response?.data : bucket
|
|
||||||
);
|
|
||||||
|
|
||||||
cacheData("buckets", updatedBuckets);
|
|
||||||
setBucketList(updatedBuckets);
|
|
||||||
|
|
||||||
const existingEmployeeIds = selected_bucket?.employeeIds || [];
|
|
||||||
const employeesToUpdate = selectedEmployee.filter((emp) => {
|
|
||||||
const isExisting = existingEmployeeIds.includes(emp.employeeId);
|
|
||||||
return (!isExisting && emp.isActive) || (isExisting && !emp.isActive);
|
|
||||||
});
|
|
||||||
|
|
||||||
const newActiveEmployeeIds = selectedEmployee
|
|
||||||
.filter((emp) => {
|
|
||||||
const isExisting = existingEmployeeIds.includes(emp.employeeId);
|
|
||||||
return (
|
|
||||||
(!isExisting && emp.isActive) || (isExisting && !emp.isActive)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.map((emp) => emp.employeeId);
|
|
||||||
|
|
||||||
if (
|
|
||||||
!arraysAreEqual(newActiveEmployeeIds, existingEmployeeIds) &&
|
|
||||||
employeesToUpdate.length !== 0
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
response = await DirectoryRepository.AssignedBuckets(
|
|
||||||
selected_bucket.id,
|
|
||||||
employeesToUpdate
|
|
||||||
);
|
|
||||||
} catch (assignError) {
|
|
||||||
const assignMessage =
|
|
||||||
assignError?.response?.data?.message ||
|
|
||||||
assignError?.message ||
|
|
||||||
"Error assigning employees.";
|
|
||||||
showToast(assignMessage, "error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const updatedData = cache_buckets?.map((bucket) =>
|
|
||||||
bucket.id === response?.data?.id ? response.data : bucket
|
|
||||||
);
|
|
||||||
|
|
||||||
cacheData("buckets", updatedData);
|
|
||||||
|
|
||||||
setBucketList(updatedData);
|
|
||||||
showToast("Bucket Updated Successfully", "success");
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
UpdateBucket({bucketId:selected_bucket.id,BucketPayload:BucketPayload})
|
|
||||||
} else {
|
|
||||||
// response = await DirectoryRepository.CreateBuckets(data);
|
|
||||||
|
|
||||||
// const updatedBuckets = [...cache_buckets, response?.data];
|
|
||||||
// cacheData("buckets", updatedBuckets);
|
|
||||||
// setBucketList(updatedBuckets);
|
|
||||||
// showToast("Bucket Created Successfully", "success");
|
|
||||||
const BucketPayload = data;
|
|
||||||
handleCreatedBucket(BucketPayload);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleBack();
|
|
||||||
} catch (error) {
|
|
||||||
const message =
|
|
||||||
error?.response?.data?.message ||
|
|
||||||
error?.message ||
|
|
||||||
"Error occurred during API call";
|
|
||||||
showToast(message, "error");
|
|
||||||
} finally {
|
|
||||||
setSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteContact = async () => {
|
|
||||||
try {
|
|
||||||
const resp = await DirectoryRepository.DeleteBucket(deleteBucket);
|
|
||||||
const cache_buckets = getCachedData("buckets") || [];
|
|
||||||
const updatedBuckets = cache_buckets.filter(
|
|
||||||
(bucket) => bucket.id !== deleteBucket
|
|
||||||
);
|
|
||||||
cacheData("buckets", updatedBuckets);
|
|
||||||
setBucketList(updatedBuckets);
|
|
||||||
showToast("Bucket deleted successfully", "success");
|
|
||||||
setDeleteBucket(null);
|
|
||||||
} catch (error) {
|
|
||||||
const message =
|
|
||||||
error?.response?.data?.message ||
|
|
||||||
error?.message ||
|
|
||||||
"Error occurred during API call.";
|
|
||||||
showToast(message, "error");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
reset({
|
|
||||||
name: selected_bucket?.name || "",
|
|
||||||
description: selected_bucket?.description || "",
|
|
||||||
});
|
|
||||||
}, [selected_bucket]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setBucketList(buckets);
|
|
||||||
}, [buckets]);
|
|
||||||
|
|
||||||
const handleBack = () => {
|
|
||||||
select_bucket(null);
|
|
||||||
setAction_bucket(false);
|
|
||||||
setSubmitting(false);
|
|
||||||
reset({ name: "", description: "" });
|
|
||||||
setSelectEmployee([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const sortedBucktesList = sortedBuckteList?.filter((bucket) => {
|
|
||||||
const term = searchTerm?.toLowerCase();
|
|
||||||
const name = bucket.name?.toLowerCase();
|
|
||||||
return name?.includes(term);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{deleteBucket && (
|
|
||||||
<ConfirmModal
|
|
||||||
isOpen={!!deleteBucket}
|
|
||||||
type="delete"
|
|
||||||
header="Delete Bucket"
|
|
||||||
message="Are you sure you want to delete this bucket?"
|
|
||||||
onSubmit={handleDeleteContact}
|
|
||||||
onClose={() => setDeleteBucket(null)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="container m-0 p-0" style={{ minHeight: "00px" }}>
|
|
||||||
<div className="d-flex justify-content-center">
|
|
||||||
<p className="fs-6 fw-semibold m-0">Manage Buckets</p>
|
|
||||||
</div>
|
|
||||||
<div className="d-flex justify-content-between px-2 px-sm-0 mt-5 mt-3 align-items-center ">
|
|
||||||
{action_bucket ? (
|
|
||||||
<i
|
|
||||||
className={`fa-solid fa-arrow-left fs-5 cursor-pointer`}
|
|
||||||
onClick={handleBack}
|
|
||||||
></i>
|
|
||||||
) : (
|
|
||||||
<div className="d-flex align-items-center gap-2">
|
|
||||||
<input
|
|
||||||
type="search"
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
placeholder="Search Bucket ..."
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
/>
|
|
||||||
<i
|
|
||||||
className={`bx bx-refresh cursor-pointer fs-4 ${
|
|
||||||
loading ? "spin" : ""
|
|
||||||
}`}
|
|
||||||
title="Refresh"
|
|
||||||
onClick={() => refetch()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`btn btn-sm btn-primary ms-auto ${
|
|
||||||
action_bucket ? "d-none" : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => {
|
|
||||||
setAction_bucket(true);
|
|
||||||
select_bucket(null);
|
|
||||||
reset({ name: "", description: "" });
|
|
||||||
setSelectEmployee([]);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<i className="bx bx-plus-circle me-2"></i>
|
|
||||||
Add Bucket
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{!action_bucket ? (
|
|
||||||
<div className="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3 pt-3 px-2 px-sm-0">
|
|
||||||
{loading && (
|
|
||||||
<div className="col-12">
|
|
||||||
<div
|
|
||||||
className="d-flex justify-content-center align-items-center py-5 w-100"
|
|
||||||
style={{ marginLeft: "250px" }}
|
|
||||||
>
|
|
||||||
Loading...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!loading && buckets.length === 0 && searchTerm.trim() === "" && (
|
|
||||||
<div className="col-12">
|
|
||||||
<div
|
|
||||||
className="d-flex justify-content-center align-items-center py-5 w-100"
|
|
||||||
style={{ marginLeft: "250px" }}
|
|
||||||
>
|
|
||||||
No buckets available.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!loading &&
|
|
||||||
buckets.length > 0 &&
|
|
||||||
sortedBucktesList.length === 0 && (
|
|
||||||
<div className="col-12">
|
|
||||||
<div
|
|
||||||
className="d-flex justify-content-center align-items-center py-5 w-100"
|
|
||||||
style={{ marginLeft: "250px" }}
|
|
||||||
>
|
|
||||||
No matching buckets found.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!loading &&
|
|
||||||
sortedBucktesList.map((bucket) => (
|
|
||||||
<div className="col" key={bucket.id}>
|
|
||||||
<div className="card h-100">
|
|
||||||
<div className="card-body p-4">
|
|
||||||
<h6 className="card-title d-flex justify-content-between align-items-center">
|
|
||||||
<span>{bucket.name}</span>
|
|
||||||
{(DirManager ||
|
|
||||||
DirAdmin ||
|
|
||||||
bucket?.createdBy?.id ===
|
|
||||||
profile?.employeeInfo?.id) && (
|
|
||||||
<div className="d-flex gap-2">
|
|
||||||
<i
|
|
||||||
className="bx bx-edit bx-sm text-primary cursor-pointer"
|
|
||||||
onClick={() => {
|
|
||||||
select_bucket(bucket);
|
|
||||||
setAction_bucket(true);
|
|
||||||
const initialSelectedEmployees = employeesList
|
|
||||||
.filter((emp) =>
|
|
||||||
bucket.employeeIds?.includes(
|
|
||||||
emp.employeeId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.map((emp) => ({ ...emp, isActive: true }));
|
|
||||||
setSelectEmployee(initialSelectedEmployees);
|
|
||||||
}}
|
|
||||||
></i>
|
|
||||||
<i
|
|
||||||
className="bx bx-trash bx-sm text-danger cursor-pointer ms-0"
|
|
||||||
onClick={() => setDeleteBucket(bucket?.id)}
|
|
||||||
></i>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</h6>
|
|
||||||
<h6 className="card-subtitle mb-2 text-muted text-start">
|
|
||||||
Contacts:{" "}
|
|
||||||
{bucket.numberOfContacts
|
|
||||||
? bucket.numberOfContacts
|
|
||||||
: 0}
|
|
||||||
</h6>
|
|
||||||
<p
|
|
||||||
className="card-text text-start"
|
|
||||||
title={bucket.description}
|
|
||||||
>
|
|
||||||
{bucket.description || "No description available."}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="px-2 px-sm-0">
|
|
||||||
<div className="mb-3">
|
|
||||||
<label htmlFor="bucketName" className="form-label">
|
|
||||||
Bucket Name
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="bucketName"
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
{...register("name")}
|
|
||||||
/>
|
|
||||||
{errors.name && (
|
|
||||||
<small className="danger-text">{errors.name.message}</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="mb-3">
|
|
||||||
<label htmlFor="bucketDescription" className="form-label">
|
|
||||||
Bucket Description
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
id="bucketDescription"
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
rows="3"
|
|
||||||
{...register("description")}
|
|
||||||
/>
|
|
||||||
{errors.description && (
|
|
||||||
<small className="danger-text">
|
|
||||||
{errors.description.message}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{selected_bucket && (
|
|
||||||
<EmployeeList
|
|
||||||
employees={employeesList}
|
|
||||||
onChange={(data) => setSelectEmployee(data)}
|
|
||||||
bucket={selected_bucket}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mt-4 d-flex justify-content-center gap-3">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleBack}
|
|
||||||
className="btn btn-sm btn-secondary"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="btn btn-sm btn-primary"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
{isSubmitting ? "Please wait..." : "Submit"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ManageBucket;
|
|
||||||
@ -1,486 +0,0 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import {
|
|
||||||
useForm,
|
|
||||||
useFieldArray,
|
|
||||||
FormProvider,
|
|
||||||
useFormContext,
|
|
||||||
} from "react-hook-form";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import TagInput from "../common/TagInput";
|
|
||||||
import IconButton from "../common/IconButton";
|
|
||||||
import useMaster, {
|
|
||||||
useContactCategory,
|
|
||||||
useContactTags,
|
|
||||||
} from "../../hooks/masterHook/useMaster";
|
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
import { changeMaster } from "../../slices/localVariablesSlice";
|
|
||||||
import {
|
|
||||||
useBuckets,
|
|
||||||
useDesignation,
|
|
||||||
useOrganization,
|
|
||||||
} from "../../hooks/useDirectory";
|
|
||||||
import { useProjects } from "../../hooks/useProjects";
|
|
||||||
import SelectMultiple from "../common/SelectMultiple";
|
|
||||||
import { ContactSchema } from "./DirectorySchema";
|
|
||||||
import InputSuggestions from "../common/InputSuggestion";
|
|
||||||
import Label from "../common/Label";
|
|
||||||
|
|
||||||
const ManageDirectory = ({ submitContact, onCLosed }) => {
|
|
||||||
const selectedMaster = useSelector(
|
|
||||||
(store) => store.localVariables.selectedMaster
|
|
||||||
);
|
|
||||||
const [categoryData, setCategoryData] = useState([]);
|
|
||||||
|
|
||||||
const [TagsData, setTagsData] = useState([]);
|
|
||||||
const { data, loading } = useMaster();
|
|
||||||
const { buckets, loading: bucketsLoaging } = useBuckets();
|
|
||||||
const { projects, loading: projectLoading } = useProjects();
|
|
||||||
const { contactCategory, loading: contactCategoryLoading } =
|
|
||||||
useContactCategory();
|
|
||||||
const { organizationList, loading: orgLoading } = useOrganization();
|
|
||||||
const { designationList, loading: designloading } = useDesignation();
|
|
||||||
const { contactTags, loading: Tagloading } = useContactTags();
|
|
||||||
const [IsSubmitting, setSubmitting] = useState(false);
|
|
||||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
|
||||||
const [filteredDesignationList, setFilteredDesignationList] = useState([]);
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const methods = useForm({
|
|
||||||
resolver: zodResolver(ContactSchema),
|
|
||||||
defaultValues: {
|
|
||||||
name: "",
|
|
||||||
organization: "",
|
|
||||||
contactCategoryId: null,
|
|
||||||
address: "",
|
|
||||||
description: "",
|
|
||||||
designation: "",
|
|
||||||
projectIds: [],
|
|
||||||
contactEmails: [],
|
|
||||||
contactPhones: [],
|
|
||||||
tags: [],
|
|
||||||
bucketIds: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
control,
|
|
||||||
getValues,
|
|
||||||
trigger,
|
|
||||||
setValue,
|
|
||||||
watch,
|
|
||||||
reset,
|
|
||||||
formState: { errors },
|
|
||||||
} = methods;
|
|
||||||
|
|
||||||
const {
|
|
||||||
fields: emailFields,
|
|
||||||
append: appendEmail,
|
|
||||||
remove: removeEmail,
|
|
||||||
} = useFieldArray({ control, name: "contactEmails" });
|
|
||||||
|
|
||||||
const {
|
|
||||||
fields: phoneFields,
|
|
||||||
append: appendPhone,
|
|
||||||
remove: removePhone,
|
|
||||||
} = useFieldArray({ control, name: "contactPhones" });
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (emailFields.length === 0) {
|
|
||||||
appendEmail({ label: "Work", emailAddress: "" });
|
|
||||||
}
|
|
||||||
if (phoneFields.length === 0) {
|
|
||||||
appendPhone({ label: "Office", phoneNumber: "" });
|
|
||||||
}
|
|
||||||
}, [emailFields.length, phoneFields.length]);
|
|
||||||
|
|
||||||
const handleAddEmail = async () => {
|
|
||||||
const emails = getValues("contactEmails");
|
|
||||||
const lastIndex = emails.length - 1;
|
|
||||||
const valid = await trigger(`contactEmails.${lastIndex}.emailAddress`);
|
|
||||||
if (valid) {
|
|
||||||
appendEmail({ label: "Work", emailAddress: "" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddPhone = async () => {
|
|
||||||
const phones = getValues("contactPhones");
|
|
||||||
const lastIndex = phones.length - 1;
|
|
||||||
const valid = await trigger(`contactPhones.${lastIndex}.phoneNumber`);
|
|
||||||
if (valid) {
|
|
||||||
appendPhone({ label: "Office", phoneNumber: "" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const watchBucketIds = watch("bucketIds");
|
|
||||||
|
|
||||||
// handle logic when input of desgination is changed
|
|
||||||
const handleDesignationChange = (e) => {
|
|
||||||
const val = e.target.value;
|
|
||||||
|
|
||||||
const matches = designationList.filter((org) =>
|
|
||||||
org.toLowerCase().includes(val.toLowerCase())
|
|
||||||
);
|
|
||||||
setFilteredDesignationList(matches);
|
|
||||||
setShowSuggestions(true);
|
|
||||||
setTimeout(() => setShowSuggestions(false), 5000);
|
|
||||||
};
|
|
||||||
|
|
||||||
// handle logic when designation is selected
|
|
||||||
const handleSelectDesignation = (val) => {
|
|
||||||
setShowSuggestions(false);
|
|
||||||
setValue("designation", val);
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleBucketId = (id) => {
|
|
||||||
const updated = watchBucketIds?.includes(id)
|
|
||||||
? watchBucketIds.filter((val) => val !== id)
|
|
||||||
: [...watchBucketIds, id];
|
|
||||||
|
|
||||||
setValue("bucketIds", updated, { shouldValidate: true });
|
|
||||||
};
|
|
||||||
const handleCheckboxChange = (id) => {
|
|
||||||
const updated = watchBucketIds.includes(id)
|
|
||||||
? watchBucketIds.filter((i) => i !== id)
|
|
||||||
: [...watchBucketIds, id];
|
|
||||||
|
|
||||||
setValue("bucketIds", updated, { shouldValidate: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = (data) => {
|
|
||||||
const cleaned = {
|
|
||||||
...data,
|
|
||||||
contactEmails: (data.contactEmails || []).filter(
|
|
||||||
(e) => e.emailAddress?.trim() !== ""
|
|
||||||
),
|
|
||||||
contactPhones: (data.contactPhones || []).filter(
|
|
||||||
(p) => p.phoneNumber?.trim() !== ""
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
setSubmitting(true);
|
|
||||||
submitContact(cleaned, reset, setSubmitting);
|
|
||||||
};
|
|
||||||
const orgValue = watch("organization");
|
|
||||||
|
|
||||||
const handleClosed = () => {
|
|
||||||
onCLosed();
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<FormProvider {...methods}>
|
|
||||||
<form className="p-2 p-sm-0" onSubmit={handleSubmit(onSubmit)}>
|
|
||||||
<div className="d-flex justify-content-center align-items-center">
|
|
||||||
<h5 className="m-0 fw-18"> Create New Contact</h5>
|
|
||||||
</div>
|
|
||||||
<div className="row">
|
|
||||||
<div className="col-md-6 text-start">
|
|
||||||
<Label className="form-label" required>Name</Label>
|
|
||||||
<input
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
{...register("name")}
|
|
||||||
/>
|
|
||||||
{errors.name && (
|
|
||||||
<small className="danger-text">{errors.name.message}</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-md-6 text-start">
|
|
||||||
<Label className="form-label" required>Organization</Label>
|
|
||||||
<InputSuggestions
|
|
||||||
organizationList={organizationList}
|
|
||||||
value={getValues("organization") || ""}
|
|
||||||
onChange={(val) => setValue("organization", val)}
|
|
||||||
error={errors.organization?.message}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="row mt-1">
|
|
||||||
<div className="col-md-6 text-start">
|
|
||||||
<Label className="form-label" required>Designation</Label>
|
|
||||||
<input
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
{...register("designation")}
|
|
||||||
onChange={handleDesignationChange}
|
|
||||||
/>
|
|
||||||
{showSuggestions && filteredDesignationList.length > 0 && (
|
|
||||||
<ul
|
|
||||||
className="list-group shadow-sm position-absolute bg-white border w-50 zindex-tooltip"
|
|
||||||
style={{
|
|
||||||
maxHeight: "180px",
|
|
||||||
overflowY: "auto",
|
|
||||||
marginTop: "2px",
|
|
||||||
zIndex: 1000,
|
|
||||||
borderRadius: "0px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{filteredDesignationList.map((designation) => (
|
|
||||||
<li
|
|
||||||
key={designation}
|
|
||||||
className="list-group-item list-group-item-action border-none "
|
|
||||||
style={{
|
|
||||||
cursor: "pointer",
|
|
||||||
padding: "5px 12px",
|
|
||||||
fontSize: "14px",
|
|
||||||
transition: "background-color 0.2s",
|
|
||||||
}}
|
|
||||||
onMouseDown={() => handleSelectDesignation(designation)}
|
|
||||||
onMouseEnter={(e) =>
|
|
||||||
(e.currentTarget.style.backgroundColor = "#f8f9fa")
|
|
||||||
}
|
|
||||||
onMouseLeave={(e) =>
|
|
||||||
(e.currentTarget.style.backgroundColor = "transparent")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{designation}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
{errors.designation && (
|
|
||||||
<small className="danger-text">
|
|
||||||
{errors.designation.message}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="row mt-1">
|
|
||||||
<div className="col-md-6">
|
|
||||||
{emailFields.map((field, index) => (
|
|
||||||
<div
|
|
||||||
key={field.id}
|
|
||||||
className="row d-flex align-items-center mb-1"
|
|
||||||
>
|
|
||||||
<div className="col-5 text-start">
|
|
||||||
<label className="form-label">Label</label>
|
|
||||||
<select
|
|
||||||
className="form-select form-select-sm"
|
|
||||||
{...register(`contactEmails.${index}.label`)}
|
|
||||||
>
|
|
||||||
<option value="Work">Work</option>
|
|
||||||
<option value="Personal">Personal</option>
|
|
||||||
<option value="Other">Other</option>
|
|
||||||
</select>
|
|
||||||
{errors.contactEmails?.[index]?.label && (
|
|
||||||
<small className="danger-text">
|
|
||||||
{errors.contactEmails[index].label.message}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="col-7 text-start">
|
|
||||||
<label className="form-label">Email</label>
|
|
||||||
<div className="d-flex align-items-center">
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
{...register(`contactEmails.${index}.emailAddress`)}
|
|
||||||
placeholder="email@example.com"
|
|
||||||
/>
|
|
||||||
{index === emailFields.length - 1 ? (
|
|
||||||
<i
|
|
||||||
className="bx bx-plus-circle bx-xs ms-1 cursor-pointer text-primary"
|
|
||||||
onClick={handleAddEmail}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<i
|
|
||||||
className="bx bx-minus-circle bx-xs ms-1 cursor-pointer text-primary"
|
|
||||||
onClick={() => removeEmail(index)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{errors.contactEmails?.[index]?.emailAddress && (
|
|
||||||
<small className="danger-text">
|
|
||||||
{errors.contactEmails[index].emailAddress.message}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="col-md-6">
|
|
||||||
{phoneFields.map((field, index) => (
|
|
||||||
<div
|
|
||||||
key={field.id}
|
|
||||||
className="row d-flex align-items-center mb-2"
|
|
||||||
>
|
|
||||||
<div className="col-5 text-start">
|
|
||||||
<label className="form-label">Label</label>
|
|
||||||
<select
|
|
||||||
className="form-select form-select-sm"
|
|
||||||
{...register(`contactPhones.${index}.label`)}
|
|
||||||
>
|
|
||||||
<option value="Office">Office</option>
|
|
||||||
<option value="Personal">Personal</option>
|
|
||||||
<option value="Business">Business</option>
|
|
||||||
</select>
|
|
||||||
{errors.phone?.[index]?.label && (
|
|
||||||
<small className="danger-text">
|
|
||||||
{errors.ContactPhones[index].label.message}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="col-7 text-start">
|
|
||||||
<label className="form-label">Phone</label>
|
|
||||||
<div className="d-flex align-items-center">
|
|
||||||
<input
|
|
||||||
type="tel"
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
{...register(`contactPhones.${index}.phoneNumber`)}
|
|
||||||
placeholder="9876543210"
|
|
||||||
onInput={handlePhoneInput}
|
|
||||||
maxLength={10}
|
|
||||||
/>
|
|
||||||
{index === phoneFields.length - 1 ? (
|
|
||||||
<i
|
|
||||||
className="bx bx-plus-circle bx-xs ms-1 cursor-pointer text-primary"
|
|
||||||
onClick={handleAddPhone}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<i
|
|
||||||
className="bx bx-minus-circle bx-xs ms-1 cursor-pointer text-danager"
|
|
||||||
onClick={() => removePhone(index)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{errors.contactPhones?.[index]?.phoneNumber && (
|
|
||||||
<small className="danger-text">
|
|
||||||
{errors.contactPhones[index].phoneNumber.message}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{errors.contactPhone?.message && (
|
|
||||||
<div className="danger-text">{errors.contactPhone.message}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="row my-1">
|
|
||||||
<div className="col-md-6 text-start">
|
|
||||||
<label className="form-label">Category</label>
|
|
||||||
<select
|
|
||||||
className="form-select form-select-sm"
|
|
||||||
{...register("contactCategoryId")}
|
|
||||||
>
|
|
||||||
{contactCategoryLoading && !contactCategory ? (
|
|
||||||
<option disabled value="">
|
|
||||||
Loading...
|
|
||||||
</option>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<option disabled value="">
|
|
||||||
Select Category
|
|
||||||
</option>
|
|
||||||
{contactCategory?.map((cate) => (
|
|
||||||
<option key={cate.id} value={cate.id}>
|
|
||||||
{cate.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
{errors.contactCategoryId && (
|
|
||||||
<small className="danger-text">
|
|
||||||
{errors.contactCategoryId.message}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="col-12 col-md-6 text-start">
|
|
||||||
<SelectMultiple
|
|
||||||
name="projectIds"
|
|
||||||
label="Select Projects"
|
|
||||||
options={projects}
|
|
||||||
labelKey="name"
|
|
||||||
valueKey="id"
|
|
||||||
IsLoading={projectLoading}
|
|
||||||
/>
|
|
||||||
{errors.projectIds && (
|
|
||||||
<small className="danger-text">{errors.projectIds.message}</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-12 text-start">
|
|
||||||
<TagInput name="tags" label="Tags" options={contactTags} />
|
|
||||||
{errors.tags && (
|
|
||||||
<small className="danger-text">{errors.tags.message}</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="row">
|
|
||||||
<div className="col-md-12 mt-1 text-start">
|
|
||||||
<Label className="form-label" required>Select Bucket</Label>
|
|
||||||
|
|
||||||
<ul className="d-flex flex-wrap px-1 list-unstyled mb-0">
|
|
||||||
{bucketsLoaging && <p>Loading...</p>}
|
|
||||||
{buckets?.map((item) => (
|
|
||||||
<li
|
|
||||||
key={item.id}
|
|
||||||
className="list-inline-item flex-shrink-0 me-6 mb-1"
|
|
||||||
>
|
|
||||||
<div className="form-check ">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="form-check-input"
|
|
||||||
id={`item-${item.id}`}
|
|
||||||
checked={watchBucketIds.includes(item.id)}
|
|
||||||
onChange={() => handleCheckboxChange(item.id)}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="form-check-label"
|
|
||||||
htmlFor={`item-${item.id}`}
|
|
||||||
>
|
|
||||||
{item.name}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
{errors.bucketIds && (
|
|
||||||
<small className="danger-text mt-0">
|
|
||||||
{errors.bucketIds.message}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-12 text-start">
|
|
||||||
<label className="form-label">Address</label>
|
|
||||||
<textarea
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
rows="2"
|
|
||||||
{...register("address")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-12 text-start mt-1">
|
|
||||||
<Label className="form-label" required>Description</Label>
|
|
||||||
<textarea
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
rows="2"
|
|
||||||
{...register("description")}
|
|
||||||
/>
|
|
||||||
{errors.description && (
|
|
||||||
<small className="danger-text">{errors.description.message}</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="d-flex justify-content-end gap-2 py-2 mt-3">
|
|
||||||
<button
|
|
||||||
className="btn btn-sm btn-secondary"
|
|
||||||
type="button"
|
|
||||||
onClick={handleClosed}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button className="btn btn-sm btn-primary" type="submit">
|
|
||||||
{IsSubmitting ? "Please Wait..." : "Submit"}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</FormProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ManageDirectory;
|
|
||||||
@ -6,21 +6,15 @@ import { DirectoryRepository } from "../../repositories/DirectoryRepository";
|
|||||||
import showToast from "../../services/toastService";
|
import showToast from "../../services/toastService";
|
||||||
import { cacheData, getCachedData } from "../../slices/apiDataManager";
|
import { cacheData, getCachedData } from "../../slices/apiDataManager";
|
||||||
import "../common/TextEditor/Editor.css";
|
import "../common/TextEditor/Editor.css";
|
||||||
|
import { useActiveInActiveNote } from "../../hooks/useDirectory";
|
||||||
|
|
||||||
const NoteCardDirectory = ({
|
const NoteCardDirectory = ({ noteItem, contactId }) => {
|
||||||
refetchProfile,
|
|
||||||
refetchNotes,
|
|
||||||
noteItem,
|
|
||||||
contactId,
|
|
||||||
setProfileContact,
|
|
||||||
}) => {
|
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const [editorValue, setEditorValue] = useState(noteItem.note);
|
const [editorValue, setEditorValue] = useState(noteItem.note);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isDeleting, setIsDeleting] = useState(false);
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
const [isActivProcess, setActiveProcessing] = useState(false);
|
const [isActivProcess, setActiveProcessing] = useState(false);
|
||||||
|
|
||||||
// State to manage hover status
|
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
const handleUpdateNote = async () => {
|
const handleUpdateNote = async () => {
|
||||||
@ -36,12 +30,6 @@ const NoteCardDirectory = ({
|
|||||||
noteItem.id,
|
noteItem.id,
|
||||||
payload
|
payload
|
||||||
);
|
);
|
||||||
setProfileContact((prev) => ({
|
|
||||||
...prev,
|
|
||||||
notes: prev.notes.map((note) =>
|
|
||||||
note.id === noteItem.id ? response?.data : note
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const cached_contactProfile = getCachedData("Contact Profile");
|
const cached_contactProfile = getCachedData("Contact Profile");
|
||||||
|
|
||||||
@ -81,10 +69,6 @@ const NoteCardDirectory = ({
|
|||||||
noteItem.id,
|
noteItem.id,
|
||||||
activeStatue
|
activeStatue
|
||||||
);
|
);
|
||||||
setProfileContact((prev) => ({
|
|
||||||
...prev,
|
|
||||||
notes: prev.notes.filter((note) => note.id !== noteItem.id),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const cachedContactProfile = getCachedData("Contact Profile");
|
const cachedContactProfile = getCachedData("Contact Profile");
|
||||||
|
|
||||||
@ -106,8 +90,6 @@ const NoteCardDirectory = ({
|
|||||||
}
|
}
|
||||||
setIsDeleting(false);
|
setIsDeleting(false);
|
||||||
setActiveProcessing(false);
|
setActiveProcessing(false);
|
||||||
refetchNotes(contactId, false);
|
|
||||||
refetchProfile(contactId);
|
|
||||||
showToast(
|
showToast(
|
||||||
`Note ${activeStatue ? "Restored" : "Deleted"} Successfully`,
|
`Note ${activeStatue ? "Restored" : "Deleted"} Successfully`,
|
||||||
"success"
|
"success"
|
||||||
@ -121,6 +103,10 @@ const NoteCardDirectory = ({
|
|||||||
showToast(msg, "error");
|
showToast(msg, "error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { mutate: handleActiveInActive, isPending } = useActiveInActiveNote(
|
||||||
|
() => {}
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="card p-1 shadow-sm border-1 mb-5 conntactNote rounded"
|
className="card p-1 shadow-sm border-1 mb-5 conntactNote rounded"
|
||||||
@ -167,10 +153,15 @@ const NoteCardDirectory = ({
|
|||||||
onClick={() => setEditing(true)}
|
onClick={() => setEditing(true)}
|
||||||
></i>
|
></i>
|
||||||
|
|
||||||
{!isDeleting ? (
|
{!isPending ? (
|
||||||
<i
|
<i
|
||||||
className="bx bx-trash bx-sm me-1 text-secondary cursor-pointer"
|
className="bx bx-trash bx-sm me-1 text-secondary cursor-pointer"
|
||||||
onClick={() => handleDeleteNote(!noteItem?.isActive)}
|
onClick={() =>
|
||||||
|
handleActiveInActive({
|
||||||
|
noteId: noteItem?.id,
|
||||||
|
noteStatus: !noteItem?.isActive,
|
||||||
|
})
|
||||||
|
}
|
||||||
></i>
|
></i>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
@ -181,12 +172,17 @@ const NoteCardDirectory = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : isActivProcess ? (
|
) : isPending ? (
|
||||||
<i className="bx bx-loader-alt bx-spin text-primary"></i>
|
<i className="bx bx-loader-alt bx-spin text-primary"></i>
|
||||||
) : (
|
) : (
|
||||||
<i
|
<i
|
||||||
className="bx bx-recycle me-1 text-primary cursor-pointer"
|
className="bx bx-recycle me-1 text-primary cursor-pointer"
|
||||||
onClick={() => handleDeleteNote(!noteItem?.isActive)}
|
onClick={() =>
|
||||||
|
handleActiveInActive({
|
||||||
|
noteId: noteItem?.id,
|
||||||
|
noteStatus: !noteItem?.isActive,
|
||||||
|
})
|
||||||
|
}
|
||||||
title="Restore"
|
title="Restore"
|
||||||
></i>
|
></i>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import showToast from "../../services/toastService";
|
|||||||
import { cacheData, getCachedData } from "../../slices/apiDataManager";
|
import { cacheData, getCachedData } from "../../slices/apiDataManager";
|
||||||
import ConfirmModal from "../common/ConfirmModal"; // Make sure path is correct
|
import ConfirmModal from "../common/ConfirmModal"; // Make sure path is correct
|
||||||
import "../common/TextEditor/Editor.css";
|
import "../common/TextEditor/Editor.css";
|
||||||
import ProfileContactDirectory from "./ProfileContactDirectory";
|
|
||||||
import GlobalModel from "../common/GlobalModel";
|
import GlobalModel from "../common/GlobalModel";
|
||||||
import { useActiveInActiveNote, useUpdateNote } from "../../hooks/useDirectory";
|
import { useActiveInActiveNote, useUpdateNote } from "../../hooks/useDirectory";
|
||||||
|
|
||||||
@ -68,24 +67,7 @@ const NoteCardDirectoryEditable = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isOpenModalNote && (
|
|
||||||
<GlobalModel
|
|
||||||
isOpen={isOpenModalNote}
|
|
||||||
closeModal={() => {
|
|
||||||
setOpen_contact(null);
|
|
||||||
setIsOpenModalNote(false);
|
|
||||||
}}
|
|
||||||
size="xl"
|
|
||||||
>
|
|
||||||
{open_contact && (
|
|
||||||
<ProfileContactDirectory
|
|
||||||
contact={open_contact}
|
|
||||||
setOpen_contact={setOpen_contact}
|
|
||||||
closeModal={() => setIsOpenModalNote(false)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</GlobalModel>
|
|
||||||
)}
|
|
||||||
<div
|
<div
|
||||||
className="card shadow-sm border-1 mb-3 p-4 rounded"
|
className="card shadow-sm border-1 mb-3 p-4 rounded"
|
||||||
style={{
|
style={{
|
||||||
@ -203,29 +185,16 @@ const NoteCardDirectoryEditable = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Delete Confirm Modal */}
|
|
||||||
{isDeleteModalOpen && (
|
|
||||||
<div
|
|
||||||
className={`modal fade ${isDeleteModalOpen ? "show" : ""}`}
|
|
||||||
tabIndex="-1"
|
|
||||||
role="dialog"
|
|
||||||
style={{
|
|
||||||
display: isDeleteModalOpen ? "block" : "none",
|
|
||||||
backgroundColor: "rgba(0,0,0,0.5)",
|
|
||||||
}}
|
|
||||||
aria-hidden="false"
|
|
||||||
>
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
type={"delete"}
|
type="delete"
|
||||||
header={"Delete Note"}
|
header="Delete Note"
|
||||||
message={"Are you sure you want to delete this note?"}
|
message="Are you sure you want to delete this note?"
|
||||||
onSubmit={ActiveInActive}
|
onSubmit={ActiveInActive}
|
||||||
onClose={() => setIsDeleteModalOpen(false)}
|
onClose={() => setIsDeleteModalOpen(false)}
|
||||||
loading={isUpdatingStatus}
|
loading={isUpdatingStatus}
|
||||||
paramData={noteItem}
|
paramData={noteItem}
|
||||||
|
isOpen={isDeleteModalOpen}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,45 +1,44 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useState } from "react";
|
||||||
import Editor from "../common/TextEditor/Editor";
|
import Editor from "../common/TextEditor/Editor";
|
||||||
import Avatar from "../common/Avatar";
|
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { DirectoryRepository } from "../../repositories/DirectoryRepository";
|
|
||||||
import moment from "moment";
|
|
||||||
import { cacheData, getCachedData } from "../../slices/apiDataManager";
|
|
||||||
import NoteCardDirectory from "./NoteCardDirectory";
|
import NoteCardDirectory from "./NoteCardDirectory";
|
||||||
import showToast from "../../services/toastService";
|
import showToast from "../../services/toastService";
|
||||||
import { useContactNotes } from "../../hooks/useDirectory";
|
import {
|
||||||
|
useActiveInActiveNote,
|
||||||
|
useContactNotes1,
|
||||||
|
useCreateNote,
|
||||||
|
useUpdateNote,
|
||||||
|
} from "../../hooks/useDirectory";
|
||||||
|
import { NoetCard } from "./DirectoryPageSkeleton";
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
note: z.string().min(1, { message: "Note is required" }),
|
note: z.string().min(1, { message: "Note is required" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const NotesDirectory = ({
|
const NotesDirectory = ({ contactId }) => {
|
||||||
refetchProfile,
|
const [isActive, setIsActive] = useState(true);
|
||||||
isLoading,
|
|
||||||
contactProfile, // This contactProfile now reliably includes firstName, middleName, lastName, and fullName
|
|
||||||
setProfileContact,
|
|
||||||
}) => {
|
|
||||||
const [IsActive, setIsActive] = useState(true);
|
|
||||||
const { contactNotes, refetch } = useContactNotes(
|
|
||||||
contactProfile?.id,
|
|
||||||
IsActive
|
|
||||||
);
|
|
||||||
|
|
||||||
const [IsSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
const [showEditor, setShowEditor] = useState(false);
|
const [showEditor, setShowEditor] = useState(false);
|
||||||
|
|
||||||
|
// Queries & mutations
|
||||||
|
const { data, isError, isLoading } = useContactNotes1(contactId, isActive);
|
||||||
|
const { mutate: createNote, isPending } = useCreateNote(() =>
|
||||||
|
setShowEditor(false)
|
||||||
|
);
|
||||||
|
const { mutate: updateNote } = useUpdateNote();
|
||||||
|
const { mutate: toggleNoteStatus } = useActiveInActiveNote();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
setValue,
|
setValue,
|
||||||
watch,
|
watch,
|
||||||
|
reset,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm({
|
} = useForm({
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
defaultValues: {
|
defaultValues: { note: "" },
|
||||||
note: "",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const noteValue = watch("note");
|
const noteValue = watch("note");
|
||||||
@ -48,116 +47,53 @@ const NotesDirectory = ({
|
|||||||
setValue("note", value, { shouldValidate: true });
|
setValue("note", value, { shouldValidate: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = async (data) => {
|
const onSubmit = (formData) => {
|
||||||
const newNote = { ...data, contactId: contactProfile?.id };
|
const notPayload = { ...formData, contactId };
|
||||||
try {
|
createNote(notPayload);
|
||||||
setIsSubmitting(true);
|
|
||||||
const response = await DirectoryRepository.CreateNote(newNote);
|
|
||||||
|
|
||||||
const createdNote = response.data;
|
|
||||||
|
|
||||||
setProfileContact((prev) => ({
|
|
||||||
...prev,
|
|
||||||
notes: [...(prev.notes || []), createdNote],
|
|
||||||
}));
|
|
||||||
|
|
||||||
const cached_contactProfile = getCachedData("Contact Profile");
|
|
||||||
if (
|
|
||||||
cached_contactProfile &&
|
|
||||||
cached_contactProfile.contactId === contactProfile?.id
|
|
||||||
) {
|
|
||||||
const updatedProfile = {
|
|
||||||
...cached_contactProfile.data,
|
|
||||||
notes: [...(cached_contactProfile.data.notes || []), createdNote],
|
|
||||||
};
|
|
||||||
cacheData("Contact Profile", {
|
|
||||||
contactId: contactProfile?.id,
|
|
||||||
data: updatedProfile,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setValue("note", "");
|
|
||||||
setIsSubmitting(false);
|
|
||||||
showToast("Note added successfully!", "success");
|
|
||||||
setShowEditor(false);
|
|
||||||
setIsActive(true);
|
|
||||||
refetch(contactProfile?.id, true);
|
|
||||||
} catch (error) {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
const msg =
|
|
||||||
error.response?.data?.message ||
|
|
||||||
error.message ||
|
|
||||||
"Error occurred during API calling";
|
|
||||||
showToast(msg, "error");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCancel = () => {
|
|
||||||
setValue("note", "");
|
|
||||||
setShowEditor(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSwitch = () => {
|
const handleSwitch = () => {
|
||||||
setIsActive((prevIsActive) => {
|
setIsActive((prev) => !prev);
|
||||||
const newState = !prevIsActive;
|
|
||||||
refetch(contactProfile?.id, newState);
|
|
||||||
return newState;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use the fullName from contactProfile, which now includes middle and last names if available
|
const handleCancel =()=>{
|
||||||
const contactName =
|
reset()
|
||||||
contactProfile?.fullName || contactProfile?.firstName || "Contact";
|
setShowEditor(false)
|
||||||
const noNotesMessage = `Be the first to share your insights! ${contactName} currently has no notes.`;
|
}
|
||||||
|
|
||||||
const notesToDisplay = IsActive
|
|
||||||
? contactProfile?.notes || []
|
|
||||||
: contactNotes || [];
|
|
||||||
|
|
||||||
const hasNotes = notesToDisplay.length > 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="text-start mt-10">
|
<div className="text-start mt-10">
|
||||||
<div className="d-flex align-items-center justify-content-between">
|
<div className="d-flex align-items-center justify-content-between">
|
||||||
<div className="row w-100 align-items-center">
|
<div className="row w-100 align-items-center">
|
||||||
{hasNotes && (
|
{data?.length > 0 && (
|
||||||
<div className="col col-2">
|
<div className="col col-2">
|
||||||
<p className="fw-semibold m-0 ms-3">Notes :</p>
|
<p className="fw-semibold m-0 ms-3">Notes :</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="col d-flex justify-content-end gap-2 pe-0">
|
<div className="col d-flex justify-content-end gap-2 pe-0">
|
||||||
{" "}
|
|
||||||
<div className="d-flex align-items-center justify-content-between">
|
<div className="d-flex align-items-center justify-content-between">
|
||||||
|
{/* Switch */}
|
||||||
<label
|
<label
|
||||||
className="switch switch-primary"
|
className="switch switch-primary"
|
||||||
style={{
|
style={{ fontSize: "15px" }}
|
||||||
fontSize: "15px",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="switch-input"
|
className="switch-input"
|
||||||
onChange={handleSwitch}
|
onChange={handleSwitch}
|
||||||
checked={!IsActive} // invert binding
|
checked={!isActive} // invert binding
|
||||||
style={{ transform: "scale(0.8)" }}
|
style={{ transform: "scale(0.8)" }}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className="switch-toggle-slider"
|
className="switch-toggle-slider"
|
||||||
style={{
|
style={{ width: "30px", height: "15px" }}
|
||||||
width: "30px", // narrower slider
|
|
||||||
height: "15px", // shorter slider
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<span className="switch-on"></span>
|
<span className="switch-on"></span>
|
||||||
<span className="switch-off"></span>
|
<span className="switch-off"></span>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className="switch-label"
|
className="switch-label"
|
||||||
style={{
|
style={{ fontSize: "14px", marginLeft: "-14px" }}
|
||||||
fontSize: "14px", // smaller label text
|
|
||||||
marginLeft: "-14px"
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Include Deleted Notes
|
Include Deleted Notes
|
||||||
</span>
|
</span>
|
||||||
@ -189,6 +125,7 @@ const NotesDirectory = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Editor */}
|
||||||
{showEditor && (
|
{showEditor && (
|
||||||
<div className="card m-2 mb-5 position-relative">
|
<div className="card m-2 mb-5 position-relative">
|
||||||
<span
|
<span
|
||||||
@ -202,9 +139,9 @@ const NotesDirectory = ({
|
|||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<Editor
|
<Editor
|
||||||
value={noteValue}
|
value={noteValue}
|
||||||
loading={IsSubmitting}
|
loading={isPending}
|
||||||
onChange={handleEditorChange}
|
onChange={handleEditorChange}
|
||||||
onCancel={onCancel}
|
onCancel={handleCancel}
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
/>
|
/>
|
||||||
{errors.note && (
|
{errors.note && (
|
||||||
@ -214,31 +151,27 @@ const NotesDirectory = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className=" justify-content-start px-1 mt-1">
|
<div className="justify-content-start px-1 mt-1">
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="text-center">
|
<NoetCard/>
|
||||||
{" "}
|
|
||||||
<p>Loading...</p>{" "}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
{!isLoading && notesToDisplay.length > 0
|
|
||||||
? notesToDisplay
|
{!isLoading && data?.length > 0
|
||||||
.slice()
|
? data.map((noteItem) => (
|
||||||
.reverse()
|
|
||||||
.map((noteItem) => (
|
|
||||||
<NoteCardDirectory
|
<NoteCardDirectory
|
||||||
refetchProfile={refetchProfile}
|
|
||||||
refetchNotes={refetch}
|
|
||||||
refetchContact={refetch}
|
|
||||||
noteItem={noteItem}
|
|
||||||
contactId={contactProfile?.id}
|
|
||||||
setProfileContact={setProfileContact}
|
|
||||||
key={noteItem.id}
|
key={noteItem.id}
|
||||||
|
noteItem={noteItem}
|
||||||
|
contactId={contactId}
|
||||||
|
// updateNote={updateNote}
|
||||||
|
// toggleNoteStatus={toggleNoteStatus}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
: !isLoading &&
|
: !isLoading &&
|
||||||
!showEditor && (
|
!showEditor && (
|
||||||
<div className="text-center mt-5">{noNotesMessage}</div>
|
<div className="text-center mt-5">
|
||||||
|
Be the first to share your insights! Aadnya Construction
|
||||||
|
currently has no notes.
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,562 +0,0 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import {
|
|
||||||
useForm,
|
|
||||||
useFieldArray,
|
|
||||||
FormProvider,
|
|
||||||
useFormContext,
|
|
||||||
} from "react-hook-form";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import TagInput from "../common/TagInput";
|
|
||||||
import IconButton from "../common/IconButton";
|
|
||||||
import useMaster, {
|
|
||||||
useContactCategory,
|
|
||||||
useContactTags,
|
|
||||||
} from "../../hooks/masterHook/useMaster";
|
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
import { changeMaster } from "../../slices/localVariablesSlice";
|
|
||||||
import {
|
|
||||||
useBuckets,
|
|
||||||
useDesignation,
|
|
||||||
useOrganization,
|
|
||||||
} from "../../hooks/useDirectory";
|
|
||||||
import { useProjects } from "../../hooks/useProjects";
|
|
||||||
import SelectMultiple from "../common/SelectMultiple";
|
|
||||||
import { ContactSchema } from "./DirectorySchema";
|
|
||||||
import InputSuggestions from "../common/InputSuggestion";
|
|
||||||
import Label from "../common/Label";
|
|
||||||
|
|
||||||
const UpdateContact = ({ submitContact, existingContact, onCLosed }) => {
|
|
||||||
const selectedMaster = useSelector(
|
|
||||||
(store) => store.localVariables.selectedMaster
|
|
||||||
);
|
|
||||||
const [categoryData, setCategoryData] = useState([]);
|
|
||||||
const [TagsData, setTagsData] = useState([]);
|
|
||||||
const { data, loading } = useMaster();
|
|
||||||
const { buckets, loading: bucketsLoaging } = useBuckets();
|
|
||||||
const { projects, loading: projectLoading } = useProjects();
|
|
||||||
const { contactCategory, loading: contactCategoryLoading } =
|
|
||||||
useContactCategory();
|
|
||||||
const { contactTags, loading: Tagloading } = useContactTags();
|
|
||||||
const [IsSubmitting, setSubmitting] = useState(false);
|
|
||||||
const [isInitialized, setIsInitialized] = useState(false);
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const { organizationList } = useOrganization();
|
|
||||||
const { designationList } = useDesignation();
|
|
||||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
|
||||||
const [filteredDesignationList, setFilteredDesignationList] = useState([]);
|
|
||||||
|
|
||||||
const methods = useForm({
|
|
||||||
resolver: zodResolver(ContactSchema),
|
|
||||||
defaultValues: {
|
|
||||||
name: "",
|
|
||||||
organization: "",
|
|
||||||
contactCategoryId: null,
|
|
||||||
address: "",
|
|
||||||
description: "",
|
|
||||||
designation: "",
|
|
||||||
projectIds: [],
|
|
||||||
contactEmails: [],
|
|
||||||
contactPhones: [],
|
|
||||||
tags: [],
|
|
||||||
bucketIds: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
control,
|
|
||||||
getValues,
|
|
||||||
trigger,
|
|
||||||
setValue,
|
|
||||||
watch,
|
|
||||||
reset,
|
|
||||||
formState: { errors },
|
|
||||||
} = methods;
|
|
||||||
|
|
||||||
const {
|
|
||||||
fields: emailFields,
|
|
||||||
append: appendEmail,
|
|
||||||
remove: removeEmail,
|
|
||||||
} = useFieldArray({ control, name: "contactEmails" });
|
|
||||||
|
|
||||||
const {
|
|
||||||
fields: phoneFields,
|
|
||||||
append: appendPhone,
|
|
||||||
remove: removePhone,
|
|
||||||
} = useFieldArray({ control, name: "contactPhones" });
|
|
||||||
|
|
||||||
const handleAddEmail = async () => {
|
|
||||||
const emails = getValues("contactEmails");
|
|
||||||
const lastIndex = emails.length - 1;
|
|
||||||
const valid = await trigger(`contactEmails.${lastIndex}.emailAddress`);
|
|
||||||
if (valid) {
|
|
||||||
appendEmail({ label: "Work", emailAddress: "" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddPhone = async () => {
|
|
||||||
const phones = getValues("contactPhones");
|
|
||||||
const lastIndex = phones.length - 1;
|
|
||||||
const valid = await trigger(`contactPhones.${lastIndex}.phoneNumber`);
|
|
||||||
if (valid) {
|
|
||||||
appendPhone({ label: "Office", phoneNumber: "" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// handle logic when input of desgination is changed
|
|
||||||
const handleDesignationChange = (e) => {
|
|
||||||
const val = e.target.value;
|
|
||||||
|
|
||||||
const matches = designationList.filter((org) =>
|
|
||||||
org.toLowerCase().includes(val.toLowerCase())
|
|
||||||
);
|
|
||||||
setFilteredDesignationList(matches);
|
|
||||||
setShowSuggestions(true);
|
|
||||||
setTimeout(() => setShowSuggestions(false), 5000);
|
|
||||||
};
|
|
||||||
|
|
||||||
// handle logic when designation is selected
|
|
||||||
const handleSelectDesignation = (val) => {
|
|
||||||
setShowSuggestions(false);
|
|
||||||
setValue("designation", val);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePhoneInput = (e) => {
|
|
||||||
const value = e.target.value.replace(/[^0-9]/g, "");
|
|
||||||
e.target.value = value.slice(0, 10);
|
|
||||||
};
|
|
||||||
|
|
||||||
const watchBucketIds = watch("bucketIds");
|
|
||||||
|
|
||||||
const toggleBucketId = (id) => {
|
|
||||||
const updated = watchBucketIds?.includes(id)
|
|
||||||
? watchBucketIds.filter((val) => val !== id)
|
|
||||||
: [...watchBucketIds, id];
|
|
||||||
|
|
||||||
setValue("bucketIds", updated, { shouldValidate: true });
|
|
||||||
};
|
|
||||||
const handleCheckboxChange = (id) => {
|
|
||||||
const updated = watchBucketIds.includes(id)
|
|
||||||
? watchBucketIds.filter((i) => i !== id)
|
|
||||||
: [...watchBucketIds, id];
|
|
||||||
|
|
||||||
setValue("bucketIds", updated, { shouldValidate: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = async (data) => {
|
|
||||||
const cleaned = {
|
|
||||||
...data,
|
|
||||||
contactEmails: (data.contactEmails || [])
|
|
||||||
.filter((e) => e.emailAddress?.trim() !== "")
|
|
||||||
.map((email, index) => {
|
|
||||||
const existingEmail = existingContact.contactEmails?.[index];
|
|
||||||
return existingEmail ? { ...email, id: existingEmail.id } : email;
|
|
||||||
}),
|
|
||||||
contactPhones: (data.contactPhones || [])
|
|
||||||
.filter((p) => p.phoneNumber?.trim() !== "")
|
|
||||||
.map((phone, index) => {
|
|
||||||
const existingPhone = existingContact.contactPhones?.[index];
|
|
||||||
return existingPhone ? { ...phone, id: existingPhone.id } : phone;
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
setSubmitting(true);
|
|
||||||
await submitContact({ ...cleaned, id: existingContact.id });
|
|
||||||
|
|
||||||
setSubmitting(false);
|
|
||||||
};
|
|
||||||
const orgValue = watch("organization");
|
|
||||||
const handleClosed = () => {
|
|
||||||
onCLosed();
|
|
||||||
};
|
|
||||||
useEffect(() => {
|
|
||||||
const isValidContact =
|
|
||||||
existingContact &&
|
|
||||||
typeof existingContact === "object" &&
|
|
||||||
!Array.isArray(existingContact);
|
|
||||||
|
|
||||||
if (!isInitialized && isValidContact && TagsData) {
|
|
||||||
reset({
|
|
||||||
name: existingContact.name || "",
|
|
||||||
organization: existingContact.organization || "",
|
|
||||||
contactEmails: existingContact.contactEmails || [],
|
|
||||||
contactPhones: existingContact.contactPhones || [],
|
|
||||||
contactCategoryId: existingContact.contactCategory?.id || null,
|
|
||||||
address: existingContact.address || "",
|
|
||||||
description: existingContact.description || "",
|
|
||||||
designation: existingContact.designation || "",
|
|
||||||
projectIds: existingContact.projectIds || null,
|
|
||||||
tags: existingContact.tags || [],
|
|
||||||
bucketIds: existingContact.bucketIds || [],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
|
||||||
!existingContact.contactPhones ||
|
|
||||||
existingContact.contactPhones.length === 0
|
|
||||||
) {
|
|
||||||
appendPhone({ label: "Office", phoneNumber: "" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!existingContact.contactEmails ||
|
|
||||||
existingContact.contactEmails.length === 0
|
|
||||||
) {
|
|
||||||
appendEmail({ label: "Work", emailAddress: "" });
|
|
||||||
}
|
|
||||||
setIsInitialized(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// return()=> reset()
|
|
||||||
}, [existingContact, buckets, projects]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormProvider {...methods}>
|
|
||||||
<form className="p-2 p-sm-0" onSubmit={handleSubmit(onSubmit)}>
|
|
||||||
<div className="d-flex justify-content-center align-items-center">
|
|
||||||
<h5 className="m-0 fw-18"> Update Contact</h5>
|
|
||||||
</div>
|
|
||||||
<div className="row">
|
|
||||||
<div className="col-md-6 text-start">
|
|
||||||
<label className="form-label">Name</label>
|
|
||||||
<input
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
{...register("name")}
|
|
||||||
/>
|
|
||||||
{errors.name && (
|
|
||||||
<small className="danger-text">{errors.name.message}</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-md-6 text-start">
|
|
||||||
<label className="form-label">Organization</label>
|
|
||||||
<InputSuggestions
|
|
||||||
organizationList={organizationList}
|
|
||||||
value={getValues("organization") || ""}
|
|
||||||
onChange={(val) => setValue("organization", val)}
|
|
||||||
error={errors.organization?.message}
|
|
||||||
/>
|
|
||||||
{errors.organization && (
|
|
||||||
<small className="danger-text">
|
|
||||||
{errors.organization.message}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="row mt-1">
|
|
||||||
<div className="col-md-6 text-start">
|
|
||||||
<Label className="form-label" required>Designation</Label>
|
|
||||||
<input
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
{...register("designation")}
|
|
||||||
onChange={handleDesignationChange}
|
|
||||||
/>
|
|
||||||
{showSuggestions && filteredDesignationList.length > 0 && (
|
|
||||||
<ul
|
|
||||||
className="list-group shadow-sm position-absolute w-50 bg-white border zindex-tooltip"
|
|
||||||
style={{
|
|
||||||
maxHeight: "180px",
|
|
||||||
overflowY: "auto",
|
|
||||||
marginTop: "2px",
|
|
||||||
zIndex: 1000,
|
|
||||||
borderRadius: "0px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{filteredDesignationList.map((designation) => (
|
|
||||||
<li
|
|
||||||
key={designation}
|
|
||||||
className="list-group-item list-group-item-action border-none "
|
|
||||||
style={{
|
|
||||||
cursor: "pointer",
|
|
||||||
padding: "5px 12px",
|
|
||||||
fontSize: "14px",
|
|
||||||
transition: "background-color 0.2s",
|
|
||||||
}}
|
|
||||||
onMouseDown={() => handleSelectDesignation(designation)}
|
|
||||||
onMouseEnter={(e) =>
|
|
||||||
(e.currentTarget.style.backgroundColor = "#f8f9fa")
|
|
||||||
}
|
|
||||||
onMouseLeave={(e) =>
|
|
||||||
(e.currentTarget.style.backgroundColor = "transparent")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{designation}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
{errors.designation && (
|
|
||||||
<small className="danger-text">
|
|
||||||
{errors.designation.message}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="row mt-1">
|
|
||||||
<div className="col-md-6">
|
|
||||||
{emailFields.map((field, index) => (
|
|
||||||
<div
|
|
||||||
key={field.id}
|
|
||||||
className="row d-flex align-items-center mb-1"
|
|
||||||
>
|
|
||||||
<div className="col-5 text-start">
|
|
||||||
<label className="form-label">Label</label>
|
|
||||||
<select
|
|
||||||
className="form-select form-select-sm"
|
|
||||||
{...register(`contactEmails.${index}.label`)}
|
|
||||||
>
|
|
||||||
<option value="Work">Work</option>
|
|
||||||
<option value="Personal">Personal</option>
|
|
||||||
<option value="Other">Other</option>
|
|
||||||
</select>
|
|
||||||
{errors.contactEmails?.[index]?.label && (
|
|
||||||
<small className="danger-text">
|
|
||||||
{errors.contactEmails[index].label.message}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="col-7 text-start">
|
|
||||||
<label className="form-label">Email</label>
|
|
||||||
<div className="d-flex align-items-center">
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
{...register(`contactEmails.${index}.emailAddress`)}
|
|
||||||
placeholder="email@example.com"
|
|
||||||
/>
|
|
||||||
{index === emailFields.length - 1 ? (
|
|
||||||
// <button
|
|
||||||
// type="button"
|
|
||||||
// className="btn btn-xs btn-primary ms-1"
|
|
||||||
|
|
||||||
// style={{ width: "24px", height: "24px" }}
|
|
||||||
// >
|
|
||||||
<i
|
|
||||||
className="bx bx-plus-circle bx-xs ms-1 cursor-pointer text-primary"
|
|
||||||
onClick={handleAddEmail}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
// <button
|
|
||||||
// type="button"
|
|
||||||
// className="btn btn-xs btn-danger ms-1 p-0"
|
|
||||||
// onClick={() => removeEmail(index)}
|
|
||||||
// style={{ width: "24px", height: "24px" }}
|
|
||||||
// >
|
|
||||||
<i
|
|
||||||
className="bx bx-minus-circle bx-xs ms-1 cursor-pointer text-danger"
|
|
||||||
onClick={() => removeEmail(index)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{errors.contactEmails?.[index]?.emailAddress && (
|
|
||||||
<small className="danger-text">
|
|
||||||
{errors.contactEmails[index].emailAddress.message}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="col-md-6">
|
|
||||||
{phoneFields.map((field, index) => (
|
|
||||||
<div
|
|
||||||
key={field.id}
|
|
||||||
className="row d-flex align-items-center mb-2"
|
|
||||||
>
|
|
||||||
<div className="col-5 text-start">
|
|
||||||
<label className="form-label">Label</label>
|
|
||||||
<select
|
|
||||||
className="form-select form-select-sm"
|
|
||||||
{...register(`contactPhones.${index}.label`)}
|
|
||||||
>
|
|
||||||
<option value="Office">Office</option>
|
|
||||||
<option value="Personal">Personal</option>
|
|
||||||
<option value="Business">Business</option>
|
|
||||||
</select>
|
|
||||||
{errors.phone?.[index]?.label && (
|
|
||||||
<small className="danger-text">
|
|
||||||
{errors.ContactPhones[index].label.message}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="col-7 text-start">
|
|
||||||
<label className="form-label">Phone</label>
|
|
||||||
<div className="d-flex align-items-center">
|
|
||||||
<input
|
|
||||||
type="tel"
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
{...register(`contactPhones.${index}.phoneNumber`)}
|
|
||||||
placeholder="9876543210"
|
|
||||||
onInput={handlePhoneInput}
|
|
||||||
maxLength={10}
|
|
||||||
/>
|
|
||||||
{index === phoneFields.length - 1 ? (
|
|
||||||
// <button
|
|
||||||
// type="button"
|
|
||||||
// className="btn btn-xs btn-primary ms-1"
|
|
||||||
// onClick={handleAddPhone}
|
|
||||||
// style={{ width: "24px", height: "24px" }}
|
|
||||||
// >
|
|
||||||
<i
|
|
||||||
className="bx bx-plus-circle bx-xs ms-1 cursor-pointer text-primary"
|
|
||||||
onClick={handleAddPhone}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
// <button
|
|
||||||
// type="button"
|
|
||||||
// className="btn btn-xs btn-danger ms-1"
|
|
||||||
// onClick={() => removePhone(index)}
|
|
||||||
// style={{ width: "24px", height: "24px" }}
|
|
||||||
// >
|
|
||||||
<i
|
|
||||||
className="bx bx-minus-circle bx-xs ms-1 cursor-pointer text-danger"
|
|
||||||
onClick={() => removePhone(index)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{errors.contactPhones?.[index]?.phoneNumber && (
|
|
||||||
<small className="danger-text">
|
|
||||||
{errors.contactPhones[index].phoneNumber.message}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{errors.contactPhone?.message && (
|
|
||||||
<div className="danger-text">{errors.contactPhone.message}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="row my-1">
|
|
||||||
<div className="col-md-6 text-start">
|
|
||||||
<label className="form-label">Category</label>
|
|
||||||
<select
|
|
||||||
className="form-select form-select-sm"
|
|
||||||
{...register("contactCategoryId")}
|
|
||||||
>
|
|
||||||
{contactCategoryLoading && !contactCategory ? (
|
|
||||||
<option disabled value="">
|
|
||||||
Loading...
|
|
||||||
</option>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<option disabled value="">
|
|
||||||
Select Category
|
|
||||||
</option>
|
|
||||||
{contactCategory?.map((cate) => (
|
|
||||||
<option key={cate.id} value={cate.id}>
|
|
||||||
{cate.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
{errors.contactCategoryId && (
|
|
||||||
<small className="danger-text">
|
|
||||||
{errors.contactCategoryId.message}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="col-12 col-md-6 text-start">
|
|
||||||
<SelectMultiple
|
|
||||||
name="projectIds"
|
|
||||||
label="Select Projects"
|
|
||||||
options={projects}
|
|
||||||
labelKey="name"
|
|
||||||
valueKey="id"
|
|
||||||
IsLoading={projectLoading}
|
|
||||||
/>
|
|
||||||
{errors.projectIds && (
|
|
||||||
<small className="danger-text">{errors.projectIds.message}</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-12 text-start">
|
|
||||||
<TagInput name="tags" label="Tags" options={contactTags} />
|
|
||||||
{errors.tags && (
|
|
||||||
<small className="danger-text">{errors.tags.message}</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="row">
|
|
||||||
<div className="col-md-12 mt-1 text-start">
|
|
||||||
<label className="form-label ">Select Label</label>
|
|
||||||
|
|
||||||
<ul className="d-flex flex-wrap px-1 list-unstyled mb-0">
|
|
||||||
{bucketsLoaging && <p>Loading...</p>}
|
|
||||||
{buckets?.map((item) => (
|
|
||||||
<li
|
|
||||||
key={item.id}
|
|
||||||
className="list-inline-item flex-shrink-0 me-6 mb-2"
|
|
||||||
>
|
|
||||||
<div className="form-check ">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="form-check-input"
|
|
||||||
id={`item-${item.id}`}
|
|
||||||
checked={watchBucketIds.includes(item.id)}
|
|
||||||
onChange={() => handleCheckboxChange(item.id)}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="form-check-label"
|
|
||||||
htmlFor={`item-${item.id}`}
|
|
||||||
>
|
|
||||||
{item.name}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
{errors.bucketIds && (
|
|
||||||
<small className="danger-text mt-0">
|
|
||||||
{errors.bucketIds.message}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-12 text-start">
|
|
||||||
<label className="form-label">Address</label>
|
|
||||||
<textarea
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
rows="2"
|
|
||||||
{...register("address")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-12 text-start">
|
|
||||||
<label className="form-label">Description</label>
|
|
||||||
<textarea
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
rows="2"
|
|
||||||
{...register("description")}
|
|
||||||
/>
|
|
||||||
{errors.description && (
|
|
||||||
<small className="danger-text">{errors.description.message}</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="d-flex justify-content-end gap-2 py-0 mt-4">
|
|
||||||
<button
|
|
||||||
className="btn btn-sm btn-label-secondary"
|
|
||||||
type="button"
|
|
||||||
onClick={handleClosed}
|
|
||||||
disabled={IsSubmitting}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-sm btn-primary"
|
|
||||||
type="submit"
|
|
||||||
disabled={IsSubmitting}
|
|
||||||
>
|
|
||||||
{IsSubmitting ? "Please Wait..." : "Update"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</FormProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default UpdateContact;
|
|
||||||
@ -257,7 +257,7 @@ export const useContactList = (
|
|||||||
pageSize,
|
pageSize,
|
||||||
pageNumber,
|
pageNumber,
|
||||||
filter,
|
filter,
|
||||||
searchString=""
|
searchString = ""
|
||||||
) => {
|
) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: [
|
queryKey: [
|
||||||
@ -284,28 +284,26 @@ export const useContactList = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useContactFilter = ()=>{
|
export const useContactFilter = () => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey:["contactFilter"],
|
queryKey: ["contactFilter"],
|
||||||
queryFn:async()=> {
|
queryFn: async () => {
|
||||||
const resp = await DirectoryRepository.GetContactFilter();
|
const resp = await DirectoryRepository.GetContactFilter();
|
||||||
return resp.data;
|
return resp.data;
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const useContactDetails = (contactId)=>{
|
|
||||||
return useQuery({
|
|
||||||
queryKey:["Contact",contactId],
|
|
||||||
queryFn:async()=>{
|
|
||||||
const resp = await await DirectoryRepository.GetContact(contactId);
|
|
||||||
return resp.data
|
|
||||||
},
|
},
|
||||||
enabled:!!contactId
|
});
|
||||||
})
|
};
|
||||||
}
|
|
||||||
|
|
||||||
|
export const useContactDetails = (contactId) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["Contact", contactId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const resp = await await DirectoryRepository.GetContact(contactId);
|
||||||
|
return resp.data;
|
||||||
|
},
|
||||||
|
enabled: !!contactId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const cleanNoteFilter = (filter) => {
|
const cleanNoteFilter = (filter) => {
|
||||||
const cleaned = { ...filter };
|
const cleaned = { ...filter };
|
||||||
@ -322,17 +320,10 @@ export const useNotes = (
|
|||||||
pageSize,
|
pageSize,
|
||||||
pageNumber,
|
pageNumber,
|
||||||
filter,
|
filter,
|
||||||
searchString=""
|
searchString = ""
|
||||||
) => {
|
) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: [
|
queryKey: ["Notes", projectId, pageSize, pageNumber, filter, searchString],
|
||||||
"Notes",
|
|
||||||
projectId,
|
|
||||||
pageSize,
|
|
||||||
pageNumber,
|
|
||||||
filter,
|
|
||||||
searchString,
|
|
||||||
],
|
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const cleanedFilter = cleanNoteFilter(filter);
|
const cleanedFilter = cleanNoteFilter(filter);
|
||||||
const resp = await DirectoryRepository.GetNotes(
|
const resp = await DirectoryRepository.GetNotes(
|
||||||
@ -347,26 +338,47 @@ export const useNotes = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useNoteFilter = ()=>{
|
export const useNoteFilter = () => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey:["NoteFilter"],
|
queryKey: ["NoteFilter"],
|
||||||
queryFn:async()=> {
|
queryFn: async () => {
|
||||||
const resp = await DirectoryRepository.GetNoteFilter();
|
const resp = await DirectoryRepository.GetNoteFilter();
|
||||||
return resp.data;
|
return resp.data;
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export const useContactProfile1 = (contactId) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["ContactProfile", contactId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const resp = await DirectoryRepository.GetContactProfile(contactId);
|
||||||
|
return resp.data;
|
||||||
|
},
|
||||||
|
enabled: !!contactId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useContactNotes1 = (contactId, active) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["ContactNotes", contactId, active],
|
||||||
|
queryFn: async () => {
|
||||||
|
const resp = await DirectoryRepository.GetContactNotes(contactId, active);
|
||||||
|
return resp.data;
|
||||||
|
},
|
||||||
|
enabled: !!contactId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// ---------------------------Mutation------------------------------------------------------------------
|
// ---------------------------Mutation------------------------------------------------------------------
|
||||||
|
|
||||||
export const useCreateBucket =(onSuccessCallBack)=>{
|
export const useCreateBucket = (onSuccessCallBack) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn:async(BucketPayload)=> await DirectoryRepository.CreateBuckets(BucketPayload),
|
mutationFn: async (BucketPayload) =>
|
||||||
|
await DirectoryRepository.CreateBuckets(BucketPayload),
|
||||||
onSuccess: (_, variables) => {
|
onSuccess: (_, variables) => {
|
||||||
queryClient.invalidateQueries({queryKey: ["bucketList"]})
|
queryClient.invalidateQueries({ queryKey: ["bucketList"] });
|
||||||
showToast("Bucket created Successfully", "success");
|
showToast("Bucket created Successfully", "success");
|
||||||
if (onSuccessCallBack) onSuccessCallBack();
|
if (onSuccessCallBack) onSuccessCallBack();
|
||||||
},
|
},
|
||||||
@ -377,15 +389,15 @@ export const useCreateBucket =(onSuccessCallBack)=>{
|
|||||||
"error"
|
"error"
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
export const useUpdateBucket = (onSuccessCallBack) => {
|
export const useUpdateBucket = (onSuccessCallBack) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({ bucketId, BucketPayload}) => await DirectoryRepository.UpdateBuckets(bucketId, BucketPayload),
|
mutationFn: async ({ bucketId, BucketPayload }) =>
|
||||||
|
await DirectoryRepository.UpdateBuckets(bucketId, BucketPayload),
|
||||||
onSuccess: (_, variables) => {
|
onSuccess: (_, variables) => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["bucketList"] });
|
queryClient.invalidateQueries({ queryKey: ["bucketList"] });
|
||||||
showToast("Bucket updated successfully", "success");
|
showToast("Bucket updated successfully", "success");
|
||||||
@ -401,15 +413,17 @@ export const useUpdateBucket = (onSuccessCallBack) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useAssignEmpToBucket = () => {
|
||||||
export const useAssignEmpToBucket =()=>{
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
|
mutationFn: async ({ bucketId, EmployeePayload }) =>
|
||||||
mutationFn:async({bucketId,EmployeePayload})=> await DirectoryRepository.AssignedBuckets(bucketId,EmployeePayload),
|
await DirectoryRepository.AssignedBuckets(bucketId, EmployeePayload),
|
||||||
onSuccess: (_, variables) => {
|
onSuccess: (_, variables) => {
|
||||||
const {EmployeePayload} = variables
|
const { EmployeePayload } = variables;
|
||||||
queryClient.invalidateQueries({queryKey: ["bucketList"]})
|
queryClient.invalidateQueries({ queryKey: ["bucketList"] });
|
||||||
showToast(`Bucket shared ${EmployeePayload?.length} Employee Successfully`, "success");
|
showToast(
|
||||||
|
`Bucket shared ${EmployeePayload?.length} Employee Successfully`,
|
||||||
|
"success"
|
||||||
|
);
|
||||||
if (onSuccessCallBack) onSuccessCallBack();
|
if (onSuccessCallBack) onSuccessCallBack();
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@ -419,15 +433,15 @@ export const useAssignEmpToBucket =()=>{
|
|||||||
"error"
|
"error"
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
export const useDeleteBucket = (onSuccessCallBack) => {
|
export const useDeleteBucket = (onSuccessCallBack) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (bucketId) =>
|
mutationFn: async (bucketId) =>
|
||||||
await DirectoryRepository.DeleteBucket(bucketId),
|
await DirectoryRepository.DeleteBucket(bucketId),
|
||||||
onSuccess: (_, variables) => {
|
onSuccess: (_, variables) => {
|
||||||
queryClient.invalidateQueries({queryKey: ["bucketList"]})
|
queryClient.invalidateQueries({ queryKey: ["bucketList"] });
|
||||||
showToast("Bucket deleted Successfully", "success");
|
showToast("Bucket deleted Successfully", "success");
|
||||||
if (onSuccessCallBack) onSuccessCallBack();
|
if (onSuccessCallBack) onSuccessCallBack();
|
||||||
},
|
},
|
||||||
@ -441,12 +455,13 @@ export const useDeleteBucket = (onSuccessCallBack) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCreateContact = (onSuccessCallBack)=>{
|
export const useCreateContact = (onSuccessCallBack) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn:async(contactPayload) => await DirectoryRepository.CreateContact(contactPayload),
|
mutationFn: async (contactPayload) =>
|
||||||
|
await DirectoryRepository.CreateContact(contactPayload),
|
||||||
onSuccess: (_, variables) => {
|
onSuccess: (_, variables) => {
|
||||||
queryClient.invalidateQueries({queryKey: ["contacts"]})
|
queryClient.invalidateQueries({ queryKey: ["contacts"] });
|
||||||
showToast("Contact created Successfully", "success");
|
showToast("Contact created Successfully", "success");
|
||||||
if (onSuccessCallBack) onSuccessCallBack();
|
if (onSuccessCallBack) onSuccessCallBack();
|
||||||
},
|
},
|
||||||
@ -456,18 +471,17 @@ export const useCreateContact = (onSuccessCallBack)=>{
|
|||||||
"Something went wrong.Please try again later.",
|
"Something went wrong.Please try again later.",
|
||||||
"error"
|
"error"
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
})
|
export const useUpdateContact = (onSuccessCallBack) => {
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const useUpdateContact =(onSuccessCallBack)=>{
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn:async({contactId,contactPayload})=> await DirectoryRepository.UpdateContact(contactId,contactPayload),
|
mutationFn: async ({ contactId, contactPayload }) =>
|
||||||
|
await DirectoryRepository.UpdateContact(contactId, contactPayload),
|
||||||
onSuccess: (_, variables) => {
|
onSuccess: (_, variables) => {
|
||||||
queryClient.invalidateQueries({queryKey: ["contacts"]})
|
queryClient.invalidateQueries({ queryKey: ["contacts"] });
|
||||||
showToast("Contact updated Successfully", "success");
|
showToast("Contact updated Successfully", "success");
|
||||||
if (onSuccessCallBack) onSuccessCallBack();
|
if (onSuccessCallBack) onSuccessCallBack();
|
||||||
},
|
},
|
||||||
@ -477,18 +491,22 @@ export const useUpdateContact =(onSuccessCallBack)=>{
|
|||||||
"Something went wrong.Please try again later.",
|
"Something went wrong.Please try again later.",
|
||||||
"error"
|
"error"
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useActiveInActiveContact =(onSuccessCallBack)=>{
|
export const useActiveInActiveContact = (onSuccessCallBack) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn:async({contactId,contactStatus})=> await DirectoryRepository.DeleteContact(contactId,contactStatus),
|
mutationFn: async ({ contactId, contactStatus }) =>
|
||||||
|
await DirectoryRepository.DeleteContact(contactId, contactStatus),
|
||||||
onSuccess: (_, variables) => {
|
onSuccess: (_, variables) => {
|
||||||
const {contactStatus} = variables;
|
const { contactStatus } = variables;
|
||||||
queryClient.invalidateQueries({queryKey: ["contacts"]})
|
queryClient.invalidateQueries({ queryKey: ["contacts"] });
|
||||||
showToast(`Contact ${contactStatus ? "Restored":"Deleted"} Successfully`, "success");
|
showToast(
|
||||||
|
`Contact ${contactStatus ? "Restored" : "Deleted"} Successfully`,
|
||||||
|
"success"
|
||||||
|
);
|
||||||
if (onSuccessCallBack) onSuccessCallBack();
|
if (onSuccessCallBack) onSuccessCallBack();
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@ -497,16 +515,18 @@ export const useActiveInActiveContact =(onSuccessCallBack)=>{
|
|||||||
"Something went wrong.Please try again later.",
|
"Something went wrong.Please try again later.",
|
||||||
"error"
|
"error"
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useCreateNote =()=>{
|
export const useCreateNote = (onSuccessCallBack) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn:async(notPayload)=> await DirectoryRepository.CreateNote(notPayload),
|
mutationFn: async (notPayload) =>
|
||||||
|
await DirectoryRepository.CreateNote(notPayload),
|
||||||
onSuccess: (_, variables) => {
|
onSuccess: (_, variables) => {
|
||||||
queryClient.invalidateQueries({queryKey: ["Notes"]})
|
queryClient.invalidateQueries({ queryKey: ["Notes"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["ContactNotes"] });
|
||||||
showToast(`Note Created Successfully`, "success");
|
showToast(`Note Created Successfully`, "success");
|
||||||
if (onSuccessCallBack) onSuccessCallBack();
|
if (onSuccessCallBack) onSuccessCallBack();
|
||||||
},
|
},
|
||||||
@ -516,16 +536,18 @@ export const useCreateNote =()=>{
|
|||||||
"Something went wrong.Please try again later.",
|
"Something went wrong.Please try again later.",
|
||||||
"error"
|
"error"
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useUpdateNote =(onSuccessCallBack)=>{
|
export const useUpdateNote = (onSuccessCallBack) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn:async({noteId,notePayload})=> await DirectoryRepository.UpdateNote(noteId,notePayload),
|
mutationFn: async ({ noteId, notePayload }) =>
|
||||||
|
await DirectoryRepository.UpdateNote(noteId, notePayload),
|
||||||
onSuccess: (_, variables) => {
|
onSuccess: (_, variables) => {
|
||||||
queryClient.invalidateQueries({queryKey: ["Notes"]})
|
queryClient.invalidateQueries({ queryKey: ["Notes"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["ContactNotes"] });
|
||||||
showToast("Note updated Successfully", "success");
|
showToast("Note updated Successfully", "success");
|
||||||
if (onSuccessCallBack) onSuccessCallBack();
|
if (onSuccessCallBack) onSuccessCallBack();
|
||||||
},
|
},
|
||||||
@ -535,18 +557,23 @@ export const useUpdateNote =(onSuccessCallBack)=>{
|
|||||||
"Something went wrong.Please try again later.",
|
"Something went wrong.Please try again later.",
|
||||||
"error"
|
"error"
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useActiveInActiveNote =(onSuccessCallBack)=>{
|
export const useActiveInActiveNote = (onSuccessCallBack) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn:async({noteId,noteStatus})=> await DirectoryRepository.DeleteNote(noteId,noteStatus),
|
mutationFn: async ({ noteId, noteStatus }) =>
|
||||||
|
await DirectoryRepository.DeleteNote(noteId, noteStatus),
|
||||||
onSuccess: (_, variables) => {
|
onSuccess: (_, variables) => {
|
||||||
const {noteStatus} = variables;
|
const { noteStatus } = variables;
|
||||||
queryClient.invalidateQueries({queryKey: ["Notes"]})
|
queryClient.invalidateQueries({ queryKey: ["Notes"] });
|
||||||
showToast(`Note ${noteStatus ? "Restored":"Deleted"} Successfully`, "success");
|
queryClient.invalidateQueries({ queryKey: ["ContactNotes"] });
|
||||||
|
showToast(
|
||||||
|
`Note ${noteStatus ? "Restored" : "Deleted"} Successfully`,
|
||||||
|
"success"
|
||||||
|
);
|
||||||
if (onSuccessCallBack) onSuccessCallBack();
|
if (onSuccessCallBack) onSuccessCallBack();
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@ -555,6 +582,6 @@ export const useActiveInActiveNote =(onSuccessCallBack)=>{
|
|||||||
"Something went wrong.Please try again later.",
|
"Something went wrong.Please try again later.",
|
||||||
"error"
|
"error"
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|||||||
@ -9,6 +9,8 @@ import { defaultContactFilter } from "../../components/Directory/DirectorySchema
|
|||||||
import { useDebounce } from "../../utils/appUtils";
|
import { useDebounce } from "../../utils/appUtils";
|
||||||
import Pagination from "../../components/common/Pagination";
|
import Pagination from "../../components/common/Pagination";
|
||||||
import ListViewContact from "../../components/Directory/ListViewContact";
|
import ListViewContact from "../../components/Directory/ListViewContact";
|
||||||
|
import { CardViewContactSkeleton, ListViewContactSkeleton } from "../../components/Directory/DirectoryPageSkeleton";
|
||||||
|
|
||||||
|
|
||||||
const ContactsPage = ({ searchText }) => {
|
const ContactsPage = ({ searchText }) => {
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
@ -47,7 +49,7 @@ const ContactsPage = ({ searchText }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (isError) return <div>{error.message}</div>;
|
if (isError) return <div>{error.message}</div>;
|
||||||
if (isLoading) return <div>Loading...</div>;
|
if (isLoading) return <ListViewContactSkeleton/>;
|
||||||
return (
|
return (
|
||||||
<div className="row mt-5">
|
<div className="row mt-5">
|
||||||
{gridView ? (
|
{gridView ? (
|
||||||
|
|||||||
@ -1,485 +0,0 @@
|
|||||||
import React, { useEffect, useMemo, useState } from "react";
|
|
||||||
import Breadcrumb from "../../components/common/Breadcrumb";
|
|
||||||
import IconButton from "../../components/common/IconButton";
|
|
||||||
import GlobalModel from "../../components/common/GlobalModel";
|
|
||||||
import ManageDirectory from "../../components/Directory/ManageDirectory";
|
|
||||||
import ListViewDirectory from "../../components/Directory/ListViewDirectory";
|
|
||||||
import { useBuckets, useDirectory } from "../../hooks/useDirectory";
|
|
||||||
import { DirectoryRepository } from "../../repositories/DirectoryRepository";
|
|
||||||
import { cacheData, getCachedData } from "../../slices/apiDataManager";
|
|
||||||
import showToast from "../../services/toastService";
|
|
||||||
import UpdateContact from "../../components/Directory/UpdateContact";
|
|
||||||
import CardViewDirectory from "../../components/Directory/CardViewContact";
|
|
||||||
import { useContactCategory } from "../../hooks/masterHook/useMaster";
|
|
||||||
import usePagination from "../../hooks/usePagination";
|
|
||||||
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
|
||||||
import ProfileContactDirectory from "../../components/Directory/ProfileContactDirectory";
|
|
||||||
import ConfirmModal from "../../components/common/ConfirmModal";
|
|
||||||
import DirectoryListTableHeader from "./DirectoryListTableHeader";
|
|
||||||
import DirectoryPageHeader from "./DirectoryPageHeader";
|
|
||||||
import ManageBucket from "../../components/Directory/ManageBucket";
|
|
||||||
import { useFab } from "../../Context/FabContext";
|
|
||||||
import { DireProvider, useDir } from "../../Context/DireContext";
|
|
||||||
import NotesCardViewDirectory from "../../components/Directory/NotesCardViewDirectory";
|
|
||||||
|
|
||||||
const Directory = ({ IsPage = true, prefernceContacts }) => {
|
|
||||||
const [projectPrefernce, setPerfence] = useState(null);
|
|
||||||
const [IsActive, setIsActive] = useState(true);
|
|
||||||
const [isOpenModal, setIsOpenModal] = useState(false);
|
|
||||||
const [isOpenModalNote, setIsOpenModalNote] = useState(false);
|
|
||||||
const [selectedContact, setSelectedContact] = useState(null);
|
|
||||||
const [open_contact, setOpen_contact] = useState(null);
|
|
||||||
const [ContactList, setContactList] = useState([]);
|
|
||||||
const [contactCategories, setContactCategories] = useState([]);
|
|
||||||
const [searchText, setSearchText] = useState("");
|
|
||||||
const [viewType, setViewType] = useState("notes");
|
|
||||||
const [selectedBucketIds, setSelectedBucketIds] = useState([]);
|
|
||||||
const [deleteContact, setDeleteContact] = useState(null);
|
|
||||||
const [IsDeleting, setDeleting] = useState(false);
|
|
||||||
const [openBucketModal, setOpenBucketModal] = useState(false);
|
|
||||||
const [notes, setNotes] = useState([]);
|
|
||||||
const [filterAppliedNotes, setFilterAppliedNotes] = useState([]);
|
|
||||||
// const [selectedOrgs, setSelectedOrgs] = useState([]);
|
|
||||||
|
|
||||||
const [selectedNoteNames, setSelectedNoteNames] = useState([]);
|
|
||||||
|
|
||||||
const [tempSelectedBucketIds, setTempSelectedBucketIds] = useState([]);
|
|
||||||
const [tempSelectedCategoryIds, setTempSelectedCategoryIds] = useState([]);
|
|
||||||
const { setActions } = useFab();
|
|
||||||
const { dirActions, setDirActions } = useDir();
|
|
||||||
|
|
||||||
const { contacts, loading, refetch } = useDirectory(
|
|
||||||
IsActive,
|
|
||||||
projectPrefernce
|
|
||||||
);
|
|
||||||
const { contactCategory, loading: contactCategoryLoading } =
|
|
||||||
useContactCategory();
|
|
||||||
const { buckets, refetch: refetchBucket } = useBuckets();
|
|
||||||
|
|
||||||
const submitContact = async (data) => {
|
|
||||||
try {
|
|
||||||
let response;
|
|
||||||
let updatedContacts;
|
|
||||||
const contacts_cache = getCachedData("contacts")?.data || [];
|
|
||||||
|
|
||||||
if (selectedContact) {
|
|
||||||
response = await DirectoryRepository.UpdateContact(data.id, data);
|
|
||||||
updatedContacts = contacts_cache.map((contact) =>
|
|
||||||
contact.id === data.id ? response.data : contact
|
|
||||||
);
|
|
||||||
showToast("Contact updated successfully", "success");
|
|
||||||
setIsOpenModal(false);
|
|
||||||
setSelectedContact(null);
|
|
||||||
} else {
|
|
||||||
response = await DirectoryRepository.CreateContact(data);
|
|
||||||
updatedContacts = [...contacts_cache, response.data];
|
|
||||||
showToast("Contact created successfully", "success");
|
|
||||||
setIsOpenModal(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
refetch(IsActive, prefernceContacts);
|
|
||||||
refetchBucket();
|
|
||||||
} catch (error) {
|
|
||||||
const msg =
|
|
||||||
error.response?.data?.message ||
|
|
||||||
error.message ||
|
|
||||||
"Error occurred during API call!";
|
|
||||||
showToast(msg, "error");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteContact = async (overrideId = null) => {
|
|
||||||
try {
|
|
||||||
if (!IsActive) {
|
|
||||||
setDirActions((prev) => ({ ...prev, action: true }));
|
|
||||||
} else {
|
|
||||||
setDeleting(true);
|
|
||||||
}
|
|
||||||
const id = overrideId || (!IsActive ? dirActions.id : deleteContact);
|
|
||||||
if (!id) {
|
|
||||||
showToast("No contact selected for deletion", "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await DirectoryRepository.DeleteContact(id, !IsActive);
|
|
||||||
|
|
||||||
const updatedContacts = ContactList.filter((c) => c.id !== id);
|
|
||||||
setContactList(updatedContacts);
|
|
||||||
cacheData("Contacts", { data: updatedContacts, isActive: IsActive });
|
|
||||||
|
|
||||||
showToast(
|
|
||||||
`Contact ${IsActive ? "Deleted" : "Restored"} successfully`,
|
|
||||||
"success"
|
|
||||||
);
|
|
||||||
|
|
||||||
setDeleteContact(null);
|
|
||||||
refetchBucket();
|
|
||||||
setDirActions({ action: false, id: null });
|
|
||||||
setDeleting(false);
|
|
||||||
} catch (error) {
|
|
||||||
const msg =
|
|
||||||
error?.response?.data?.message ||
|
|
||||||
error.message ||
|
|
||||||
"Error occurred during API call";
|
|
||||||
showToast(msg, "error");
|
|
||||||
|
|
||||||
setDeleting(false);
|
|
||||||
setDirActions({ action: false, id: null });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const closedModel = () => {
|
|
||||||
setIsOpenModal(false);
|
|
||||||
setSelectedContact(null);
|
|
||||||
setOpen_contact(null);
|
|
||||||
};
|
|
||||||
const [selectedCategoryIds, setSelectedCategoryIds] = useState(
|
|
||||||
contactCategory.map((category) => category.id)
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setContactList(contacts);
|
|
||||||
|
|
||||||
setTempSelectedCategoryIds([]);
|
|
||||||
setTempSelectedBucketIds([]);
|
|
||||||
}, [contacts]);
|
|
||||||
|
|
||||||
const usedCategoryIds = [
|
|
||||||
...new Set(contacts.map((c) => c.contactCategory?.id)),
|
|
||||||
];
|
|
||||||
const filteredCategories = contactCategory.filter((category) =>
|
|
||||||
usedCategoryIds.includes(category.id)
|
|
||||||
);
|
|
||||||
const handleTempBucketChange = (id) => {
|
|
||||||
setTempSelectedBucketIds((prev) =>
|
|
||||||
prev.includes(id) ? prev.filter((bid) => bid !== id) : [...prev, id]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTempCategoryChange = (id) => {
|
|
||||||
setTempSelectedCategoryIds((prev) =>
|
|
||||||
prev.includes(id) ? prev.filter((cid) => cid !== id) : [...prev, id]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const usedBucketIds = [
|
|
||||||
...new Set(contacts.flatMap((c) => c.bucketIds || [])),
|
|
||||||
];
|
|
||||||
|
|
||||||
const filteredBuckets = buckets.filter((bucket) =>
|
|
||||||
usedBucketIds.includes(bucket.id)
|
|
||||||
);
|
|
||||||
|
|
||||||
const filteredContacts = useMemo(() => {
|
|
||||||
return ContactList.filter((c) => {
|
|
||||||
const matchesSearch =
|
|
||||||
c.name.toLowerCase().includes(searchText.toLowerCase()) ||
|
|
||||||
c.organization.toLowerCase().includes(searchText.toLowerCase());
|
|
||||||
|
|
||||||
const matchesCategory =
|
|
||||||
selectedCategoryIds.length === 0 ||
|
|
||||||
selectedCategoryIds.includes(c.contactCategory?.id);
|
|
||||||
|
|
||||||
const matchesBucket =
|
|
||||||
selectedBucketIds.length === 0 ||
|
|
||||||
(c.bucketIds || []).some((id) => selectedBucketIds.includes(id));
|
|
||||||
|
|
||||||
return matchesSearch && matchesCategory && matchesBucket;
|
|
||||||
}).sort((a, b) => a?.name?.localeCompare(b.name));
|
|
||||||
}, [
|
|
||||||
ContactList,
|
|
||||||
searchText,
|
|
||||||
selectedCategoryIds,
|
|
||||||
selectedBucketIds,
|
|
||||||
selectedContact,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const applyFilter = () => {
|
|
||||||
setSelectedBucketIds(tempSelectedBucketIds);
|
|
||||||
setSelectedCategoryIds(tempSelectedCategoryIds);
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearFilter = () => {
|
|
||||||
setTempSelectedBucketIds([]);
|
|
||||||
setTempSelectedCategoryIds([]);
|
|
||||||
setSelectedBucketIds([]);
|
|
||||||
setSelectedCategoryIds([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { currentPage, totalPages, currentItems, paginate } = usePagination(
|
|
||||||
filteredContacts,
|
|
||||||
ITEMS_PER_PAGE
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderModalContent = () => {
|
|
||||||
if (selectedContact) {
|
|
||||||
return (
|
|
||||||
<UpdateContact
|
|
||||||
existingContact={selectedContact}
|
|
||||||
submitContact={submitContact}
|
|
||||||
onCLosed={closedModel}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!open_contact) {
|
|
||||||
return (
|
|
||||||
<ManageDirectory submitContact={submitContact} onCLosed={closedModel} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const actions = [];
|
|
||||||
|
|
||||||
if (IsPage) {
|
|
||||||
actions.push({
|
|
||||||
label: "Manage Bucket",
|
|
||||||
icon: "fa-solid fa-bucket fs-5",
|
|
||||||
color: "primary",
|
|
||||||
onClick: () => setOpenBucketModal(true),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (buckets?.length > 0) {
|
|
||||||
actions.push({
|
|
||||||
label: "New Contact",
|
|
||||||
icon: "bx bx-plus-circle",
|
|
||||||
color: "warning",
|
|
||||||
onClick: () => setIsOpenModal(true),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setActions(actions);
|
|
||||||
|
|
||||||
return () => setActions([]);
|
|
||||||
}, [IsPage, buckets]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setPerfence(prefernceContacts);
|
|
||||||
}, [prefernceContacts]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={IsPage ? "container-fluid" : ""}>
|
|
||||||
{IsPage && (
|
|
||||||
<Breadcrumb
|
|
||||||
data={[
|
|
||||||
{ label: "Home", link: "/dashboard" },
|
|
||||||
{ label: "Directory", link: null },
|
|
||||||
]}
|
|
||||||
></Breadcrumb>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isOpenModal && (
|
|
||||||
<GlobalModel
|
|
||||||
isOpen={isOpenModal}
|
|
||||||
closeModal={() => {
|
|
||||||
setSelectedContact(null);
|
|
||||||
setIsOpenModal(false);
|
|
||||||
}}
|
|
||||||
size="xl"
|
|
||||||
>
|
|
||||||
{renderModalContent()}
|
|
||||||
</GlobalModel>
|
|
||||||
)}
|
|
||||||
{isOpenModalNote && (
|
|
||||||
<GlobalModel
|
|
||||||
isOpen={isOpenModalNote}
|
|
||||||
closeModal={() => {
|
|
||||||
setOpen_contact(null);
|
|
||||||
setIsOpenModalNote(false);
|
|
||||||
}}
|
|
||||||
size="xl"
|
|
||||||
>
|
|
||||||
{open_contact && (
|
|
||||||
<ProfileContactDirectory
|
|
||||||
contact={open_contact}
|
|
||||||
setOpen_contact={setOpen_contact}
|
|
||||||
closeModal={() => setIsOpenModalNote(false)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</GlobalModel>
|
|
||||||
)}
|
|
||||||
{deleteContact && (
|
|
||||||
<ConfirmModal
|
|
||||||
isOpen={!!deleteContact}
|
|
||||||
type="delete"
|
|
||||||
header="Delete Contact"
|
|
||||||
message="Are you sure you want delete?"
|
|
||||||
onSubmit={handleDeleteContact}
|
|
||||||
onClose={() => setDeleteContact(null)}
|
|
||||||
loading={IsDeleting}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{openBucketModal && (
|
|
||||||
<GlobalModel
|
|
||||||
isOpen={openBucketModal}
|
|
||||||
closeModal={() => setOpenBucketModal(false)}
|
|
||||||
size="lg"
|
|
||||||
>
|
|
||||||
<ManageBucket buckets={buckets} />
|
|
||||||
</GlobalModel>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="card p-0 mb-0">
|
|
||||||
<div className="card-body p-1 pb-0">
|
|
||||||
<DirectoryPageHeader
|
|
||||||
searchText={searchText}
|
|
||||||
setSearchText={setSearchText}
|
|
||||||
setIsActive={setIsActive}
|
|
||||||
viewType={viewType}
|
|
||||||
setViewType={setViewType}
|
|
||||||
filteredBuckets={filteredBuckets}
|
|
||||||
tempSelectedBucketIds={tempSelectedBucketIds}
|
|
||||||
handleTempBucketChange={handleTempBucketChange}
|
|
||||||
filteredCategories={filteredCategories}
|
|
||||||
tempSelectedCategoryIds={tempSelectedCategoryIds}
|
|
||||||
handleTempCategoryChange={handleTempCategoryChange}
|
|
||||||
clearFilter={clearFilter}
|
|
||||||
applyFilter={applyFilter}
|
|
||||||
loading={loading}
|
|
||||||
IsActive={IsActive}
|
|
||||||
setOpenBucketModal={setOpenBucketModal}
|
|
||||||
contactsToExport={contacts}
|
|
||||||
notesToExport={notes}
|
|
||||||
selectedNoteNames={selectedNoteNames}
|
|
||||||
setSelectedNoteNames={setSelectedNoteNames}
|
|
||||||
notesForFilter={notes}
|
|
||||||
setFilterAppliedNotes={setFilterAppliedNotes}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="card-minHeight mt-0">
|
|
||||||
{viewType === "list" && (
|
|
||||||
<div className="card cursor-pointer mt-3">
|
|
||||||
<div className="card-body p-2 pb-1" style={{ minHeight: "200px" }}>
|
|
||||||
<DirectoryListTableHeader>
|
|
||||||
{!loading &&
|
|
||||||
currentItems.map((contact) => (
|
|
||||||
<ListViewDirectory
|
|
||||||
key={contact.id}
|
|
||||||
IsActive={IsActive}
|
|
||||||
contact={contact}
|
|
||||||
setSelectedContact={setSelectedContact}
|
|
||||||
setIsOpenModal={setIsOpenModal}
|
|
||||||
setOpen_contact={setOpen_contact}
|
|
||||||
setIsOpenModalNote={setIsOpenModalNote}
|
|
||||||
IsDeleted={setDeleteContact}
|
|
||||||
restore={handleDeleteContact}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</DirectoryListTableHeader>
|
|
||||||
|
|
||||||
{/* Empty state AFTER list */}
|
|
||||||
{!loading && contacts?.length === 0 && (
|
|
||||||
<p className="mt-3 ms-3 text-muted">No contact found</p>
|
|
||||||
)}
|
|
||||||
{!loading &&
|
|
||||||
contacts?.length > 0 &&
|
|
||||||
currentItems.length === 0 && (
|
|
||||||
<p className="mt-3 ms-3 text-muted">
|
|
||||||
No matching contact found
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{viewType === "card" && (
|
|
||||||
<div className="row mt-10">
|
|
||||||
{!loading &&
|
|
||||||
currentItems.map((contact) => (
|
|
||||||
<div
|
|
||||||
key={contact.id}
|
|
||||||
className="col-12 col-sm-6 col-md-4 col-lg-4 mb-4"
|
|
||||||
>
|
|
||||||
<CardViewDirectory
|
|
||||||
IsActive={IsActive}
|
|
||||||
contact={contact}
|
|
||||||
setSelectedContact={setSelectedContact}
|
|
||||||
setIsOpenModal={setIsOpenModal}
|
|
||||||
setOpen_contact={setOpen_contact}
|
|
||||||
setIsOpenModalNote={setIsOpenModalNote}
|
|
||||||
IsDeleted={setDeleteContact}
|
|
||||||
restore={handleDeleteContact}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Empty state for card view */}
|
|
||||||
{!loading && contacts?.length === 0 && (
|
|
||||||
<p className="mt-3 ms-3 text-muted">No contact found</p>
|
|
||||||
)}
|
|
||||||
{!loading && contacts?.length > 0 && currentItems.length === 0 && (
|
|
||||||
<p className="mt-3 ms-3 text-muted">No matching contact found</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{viewType === "notes" && (
|
|
||||||
<div className="mt-0">
|
|
||||||
<NotesCardViewDirectory
|
|
||||||
notes={notes}
|
|
||||||
setNotesForFilter={setNotes}
|
|
||||||
searchText={searchText}
|
|
||||||
setIsOpenModalNote={setIsOpenModalNote}
|
|
||||||
filterAppliedNotes={filterAppliedNotes}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Pagination */}
|
|
||||||
{!loading &&
|
|
||||||
viewType !== "notes" &&
|
|
||||||
contacts?.length > 0 &&
|
|
||||||
currentItems.length > ITEMS_PER_PAGE && (
|
|
||||||
<nav aria-label="Page navigation">
|
|
||||||
<ul className="pagination pagination-sm justify-content-end py-1">
|
|
||||||
<li
|
|
||||||
className={`page-item ${currentPage === 1 ? "disabled" : ""}`}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="page-link btn-xs"
|
|
||||||
onClick={() => paginate(currentPage - 1)}
|
|
||||||
>
|
|
||||||
«
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
{[...Array(totalPages)].map((_, index) => (
|
|
||||||
<li
|
|
||||||
key={index}
|
|
||||||
className={`page-item ${
|
|
||||||
currentPage === index + 1 ? "active" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="page-link"
|
|
||||||
onClick={() => paginate(index + 1)}
|
|
||||||
>
|
|
||||||
{index + 1}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<li
|
|
||||||
className={`page-item ${
|
|
||||||
currentPage === totalPages ? "disabled" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="page-link"
|
|
||||||
onClick={() => paginate(currentPage + 1)}
|
|
||||||
>
|
|
||||||
»
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Directory;
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import IconButton from "../../components/common/IconButton";
|
|
||||||
|
|
||||||
const DirectoryListTableHeader = ({ children }) => {
|
|
||||||
return (
|
|
||||||
<div className="table-responsive text-nowrap py-2" style={{ minHeight: "80px"}}>
|
|
||||||
<table className="table px-2">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th colSpan={2} className="text-start">
|
|
||||||
<div className="d-flex align-items-center gap-1">
|
|
||||||
<span>Name</span>
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
<th className="px-2 text-start">
|
|
||||||
<div className="d-flex align-items-center gap-1">
|
|
||||||
<span>Email</span>
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
<th className="mx-2 text-start">
|
|
||||||
<div className="d-flex align-items-center gap-1">
|
|
||||||
<span>Phone</span>
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
<th colSpan={2} className="mx-2 ps-20 text-start">
|
|
||||||
<span>Organization</span>
|
|
||||||
</th>
|
|
||||||
<th className="mx-2 text-start">
|
|
||||||
<span>Category</span>
|
|
||||||
</th>
|
|
||||||
<th className="text-start">
|
|
||||||
<span>Action</span>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="table-border-bottom-0 overflow-auto text-start">
|
|
||||||
{children}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DirectoryListTableHeader;
|
|
||||||
@ -9,12 +9,12 @@ import {
|
|||||||
import Breadcrumb from "../../components/common/Breadcrumb";
|
import Breadcrumb from "../../components/common/Breadcrumb";
|
||||||
import { useFab } from "../../Context/FabContext";
|
import { useFab } from "../../Context/FabContext";
|
||||||
import { useBucketList, useBuckets } from "../../hooks/useDirectory";
|
import { useBucketList, useBuckets } from "../../hooks/useDirectory";
|
||||||
import GlobalModel from "../../components/common/GlobalModel";
|
|
||||||
import ManageBucket from "../../components/Directory/ManageBucket";
|
|
||||||
import ManageBucket1 from "../../components/Directory/ManageBucket1";
|
import ManageBucket1 from "../../components/Directory/ManageBucket1";
|
||||||
import ManageContact from "../../components/Directory/ManageContact";
|
import ManageContact from "../../components/Directory/ManageContact";
|
||||||
import BucketList from "../../components/Directory/BucketList";
|
import BucketList from "../../components/Directory/BucketList";
|
||||||
import ProfileContactDirectory from "../../components/Directory/ProfileContactDirectory";
|
import { MainDirectoryPageSkeleton } from "../../components/Directory/DirectoryPageSkeleton";
|
||||||
|
import ContactProfile from "../../components/Directory/ContactProfile";
|
||||||
|
import GlobalModel from "../../components/common/GlobalModel";
|
||||||
|
|
||||||
const NotesPage = lazy(() => import("./NotesPage"));
|
const NotesPage = lazy(() => import("./NotesPage"));
|
||||||
const ContactsPage = lazy(() => import("./ContactsPage"));
|
const ContactsPage = lazy(() => import("./ContactsPage"));
|
||||||
@ -32,13 +32,13 @@ export const useDirectoryContext = () => {
|
|||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
export default function DirectoryPage({ IsPage = true }) {
|
export default function DirectoryPage({ IsPage = true, projectId }) {
|
||||||
const [searchContact, setsearchContact] = useState("");
|
const [searchContact, setsearchContact] = useState("");
|
||||||
const [searchNote, setSearchNote] = useState("");
|
const [searchNote, setSearchNote] = useState("");
|
||||||
const [activeTab, setActiveTab] = useState("notes");
|
const [activeTab, setActiveTab] = useState("notes");
|
||||||
const { setActions } = useFab();
|
const { setActions } = useFab();
|
||||||
const [gridView, setGridView] = useState(false);
|
const [gridView, setGridView] = useState(false);
|
||||||
const [isOpenBucket, setOpenBucket] = useState({});
|
const [isOpenBucket, setOpenBucket] = useState(false);
|
||||||
const [isManageContact, setManageContact] = useState({
|
const [isManageContact, setManageContact] = useState({
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
contactId: null,
|
contactId: null,
|
||||||
@ -89,7 +89,7 @@ export default function DirectoryPage({ IsPage = true }) {
|
|||||||
setContactOpen,
|
setContactOpen,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) return <div>Loading...</div>;
|
if (isLoading) return <MainDirectoryPageSkeleton />;
|
||||||
if (isError) return <div>{error.message}</div>;
|
if (isError) return <div>{error.message}</div>;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -198,7 +198,7 @@ export default function DirectoryPage({ IsPage = true }) {
|
|||||||
onChange={(e) => setShowActive(e.target.checked)}
|
onChange={(e) => setShowActive(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
<label className="form-check-label ms-0">
|
<label className="form-check-label ms-0">
|
||||||
{showActive ? "In Active" : "Active"}
|
{showActive ? "In-ACtive":"Active" }
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -209,15 +209,7 @@ export default function DirectoryPage({ IsPage = true }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Suspense
|
<Suspense fallback={<MainDirectoryPageSkeleton />}>
|
||||||
fallback={
|
|
||||||
<div className="text-center py-5">
|
|
||||||
<div className="spinner-border text-primary" role="status">
|
|
||||||
<span className="visually-hidden">Loading...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{activeTab === "notes" && (
|
{activeTab === "notes" && (
|
||||||
<NotesPage projectId={null} searchText={searchNote} />
|
<NotesPage projectId={null} searchText={searchNote} />
|
||||||
)}
|
)}
|
||||||
@ -243,7 +235,7 @@ export default function DirectoryPage({ IsPage = true }) {
|
|||||||
isOpen={contactOpen.Open}
|
isOpen={contactOpen.Open}
|
||||||
closeModal={() => setContactOpen({ contact: null, Open: false })}
|
closeModal={() => setContactOpen({ contact: null, Open: false })}
|
||||||
>
|
>
|
||||||
<ProfileContactDirectory contact={contactOpen.contact} />
|
<ContactProfile contactId={contactOpen.contact} />
|
||||||
</GlobalModel>
|
</GlobalModel>
|
||||||
)}
|
)}
|
||||||
{isManageContact.isOpen && (
|
{isManageContact.isOpen && (
|
||||||
|
|||||||
@ -1,591 +0,0 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { exportToCSV, exportToExcel, printTable, exportToPDF } from "../../utils/tableExportUtils";
|
|
||||||
|
|
||||||
const DirectoryPageHeader = ({
|
|
||||||
searchText,
|
|
||||||
setSearchText,
|
|
||||||
setIsActive,
|
|
||||||
viewType,
|
|
||||||
setViewType,
|
|
||||||
filteredBuckets,
|
|
||||||
tempSelectedBucketIds,
|
|
||||||
handleTempBucketChange,
|
|
||||||
filteredCategories,
|
|
||||||
tempSelectedCategoryIds,
|
|
||||||
handleTempCategoryChange,
|
|
||||||
clearFilter,
|
|
||||||
applyFilter,
|
|
||||||
loading,
|
|
||||||
IsActive,
|
|
||||||
contactsToExport,
|
|
||||||
notesToExport,
|
|
||||||
selectedNoteNames,
|
|
||||||
setSelectedNoteNames,
|
|
||||||
notesForFilter,
|
|
||||||
setFilterAppliedNotes
|
|
||||||
}) => {
|
|
||||||
const [filtered, setFiltered] = useState(0);
|
|
||||||
const [filteredNotes, setFilteredNotes] = useState([]);
|
|
||||||
const [noteCreators, setNoteCreators] = useState([]);
|
|
||||||
const [allCreators, setAllCreators] = useState([]);
|
|
||||||
const [allOrganizations, setAllOrganizations] = useState([]);
|
|
||||||
const [filteredOrganizations, setFilteredOrganizations] = useState([]);
|
|
||||||
const [selectedCreators, setSelectedCreators] = useState([]);
|
|
||||||
const [selectedOrgs, setSelectedOrgs] = useState([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setFiltered(tempSelectedBucketIds?.length + tempSelectedCategoryIds?.length);
|
|
||||||
}, [tempSelectedBucketIds, tempSelectedCategoryIds]);
|
|
||||||
|
|
||||||
// New state to track active filters for notes
|
|
||||||
const [notesFilterCount, setNotesFilterCount] = useState(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Calculate the number of active filters for notes
|
|
||||||
setNotesFilterCount(selectedCreators.length + selectedOrgs.length);
|
|
||||||
}, [selectedCreators, selectedOrgs]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (viewType === "notes") {
|
|
||||||
if (notesToExport && notesToExport.length > 0) {
|
|
||||||
const uniqueNames = [...new Set(notesToExport.map(note => {
|
|
||||||
const firstName = note.createdBy?.firstName || "";
|
|
||||||
const lastName = note.createdBy?.lastName || "";
|
|
||||||
return `${firstName} ${lastName}`.trim();
|
|
||||||
}).filter(name => name !== ""))];
|
|
||||||
setNoteCreators(uniqueNames.sort());
|
|
||||||
} else {
|
|
||||||
setNoteCreators([]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setNoteCreators([]);
|
|
||||||
}
|
|
||||||
}, [notesToExport, viewType]);
|
|
||||||
|
|
||||||
// Separate effect to clear selection only when switching away from notes
|
|
||||||
useEffect(() => {
|
|
||||||
if (viewType !== "notes" && selectedNoteNames.length > 0) {
|
|
||||||
setSelectedNoteNames([]);
|
|
||||||
}
|
|
||||||
}, [viewType]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const creatorsSet = new Set();
|
|
||||||
const orgsSet = new Set();
|
|
||||||
|
|
||||||
notesForFilter.forEach((note) => {
|
|
||||||
const creator = `${note.createdBy?.firstName || ""} ${note.createdBy?.lastName || ""}`.trim();
|
|
||||||
if (creator) creatorsSet.add(creator);
|
|
||||||
|
|
||||||
const org = note.organizationName;
|
|
||||||
if (org) orgsSet.add(org);
|
|
||||||
});
|
|
||||||
|
|
||||||
setAllCreators([...creatorsSet].sort());
|
|
||||||
setAllOrganizations([...orgsSet].sort());
|
|
||||||
setFilteredOrganizations([...orgsSet].sort());
|
|
||||||
}, [notesForFilter])
|
|
||||||
|
|
||||||
|
|
||||||
const handleToggleNoteName = (name) => {
|
|
||||||
setSelectedNoteNames(prevSelectedNames => {
|
|
||||||
if (prevSelectedNames.includes(name)) {
|
|
||||||
return prevSelectedNames.filter(n => n !== name);
|
|
||||||
} else {
|
|
||||||
return [...prevSelectedNames, name];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateFilteredOrganizations = () => {
|
|
||||||
if (selectedCreators.length === 0) {
|
|
||||||
setFilteredOrganizations(allOrganizations);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredOrgsSet = new Set();
|
|
||||||
notesForFilter.forEach((note) => {
|
|
||||||
const creator = `${note.createdBy?.firstName || ""} ${note.createdBy?.lastName || ""}`.trim();
|
|
||||||
if (selectedCreators.includes(creator)) {
|
|
||||||
if (note.organizationName) {
|
|
||||||
filteredOrgsSet.add(note.organizationName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
setFilteredOrganizations([...filteredOrgsSet].sort());
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleToggleCreator = (name) => {
|
|
||||||
const updated = selectedCreators.includes(name)
|
|
||||||
? selectedCreators.filter((n) => n !== name)
|
|
||||||
: [...selectedCreators, name];
|
|
||||||
|
|
||||||
setSelectedCreators(updated);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleToggleOrg = (name) => {
|
|
||||||
const updated = selectedOrgs.includes(name)
|
|
||||||
? selectedOrgs.filter((n) => n !== name)
|
|
||||||
: [...selectedOrgs, name];
|
|
||||||
|
|
||||||
setSelectedOrgs(updated);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleExport = (type) => {
|
|
||||||
let dataToExport = [];
|
|
||||||
|
|
||||||
if (viewType === "notes") {
|
|
||||||
if (!notesToExport || notesToExport.length === 0) {
|
|
||||||
console.warn("No notes to export.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const decodeHtmlEntities = (html) => {
|
|
||||||
const textarea = document.createElement("textarea");
|
|
||||||
textarea.innerHTML = html;
|
|
||||||
return textarea.value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const cleanNoteText = (html) => {
|
|
||||||
if (!html) return "";
|
|
||||||
const stripped = html.replace(/<[^>]+>/g, "");
|
|
||||||
const decoded = decodeHtmlEntities(stripped);
|
|
||||||
return decoded.replace(/\u00A0/g, " ").replace(/\s+/g, " ").trim();
|
|
||||||
};
|
|
||||||
|
|
||||||
const cleanName = (name) => {
|
|
||||||
if (!name) return "";
|
|
||||||
return name.replace(/\u00A0/g, " ").replace(/\s+/g, " ").trim();
|
|
||||||
};
|
|
||||||
|
|
||||||
dataToExport = notesToExport.map(note => ({
|
|
||||||
"Name": cleanName(`${note.createdBy?.firstName || ""} ${note.createdBy?.lastName || ""}`),
|
|
||||||
"Notes": cleanNoteText(note.note),
|
|
||||||
"Created At": note.createdAt
|
|
||||||
? new Date(note.createdAt).toLocaleString("en-IN")
|
|
||||||
: "",
|
|
||||||
"Updated At": note.updatedAt
|
|
||||||
? new Date(note.updatedAt).toLocaleString("en-IN")
|
|
||||||
: "",
|
|
||||||
"Updated By": cleanName(
|
|
||||||
`${note.updatedBy?.firstName || ""} ${note.updatedBy?.lastName || ""}`
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
|
|
||||||
} else {
|
|
||||||
if (!contactsToExport || contactsToExport.length === 0) {
|
|
||||||
console.warn("No contacts to export.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dataToExport = contactsToExport.map(contact => ({
|
|
||||||
Name: contact.name || '',
|
|
||||||
Organization: contact.organization || '',
|
|
||||||
Email: contact.contactEmails?.map(email => email.emailAddress).join(', ') || '',
|
|
||||||
Phone: contact.contactPhones?.map(phone => phone.phoneNumber).join(', ') || '',
|
|
||||||
Category: contact.contactCategory?.name || '',
|
|
||||||
Tags: contact.tags?.map(tag => tag.name).join(', ') || '',
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
const today = new Date();
|
|
||||||
const formattedDate = `${today.getFullYear()}${String(today.getMonth() + 1).padStart(2, '0')}${String(today.getDate()).padStart(2, '0')}`;
|
|
||||||
|
|
||||||
const filename =
|
|
||||||
viewType === "notes"
|
|
||||||
? `Directory_Notes_${formattedDate}`
|
|
||||||
: `Directory_Contacts_${formattedDate}`;
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case "csv":
|
|
||||||
exportToCSV(dataToExport, filename);
|
|
||||||
break;
|
|
||||||
case "excel":
|
|
||||||
exportToExcel(dataToExport, filename);
|
|
||||||
break;
|
|
||||||
case "pdf":
|
|
||||||
exportToPDF(dataToExport, filename);
|
|
||||||
break;
|
|
||||||
case "print":
|
|
||||||
printTable(dataToExport, filename);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const applyCombinedFilter = () => {
|
|
||||||
const lowerSearch = searchText?.toLowerCase() || "";
|
|
||||||
|
|
||||||
const filtered = notesForFilter.filter((noteItem) => {
|
|
||||||
const creator = `${noteItem.createdBy?.firstName || ""} ${noteItem.createdBy?.lastName || ""}`.trim();
|
|
||||||
const org = noteItem.organizationName;
|
|
||||||
|
|
||||||
const matchesCreator = selectedCreators.length === 0 || selectedCreators.includes(creator);
|
|
||||||
const matchesOrg = selectedOrgs.length === 0 || selectedOrgs.includes(org);
|
|
||||||
|
|
||||||
const plainNote = noteItem?.note?.replace(/<[^>]+>/g, "").toLowerCase();
|
|
||||||
|
|
||||||
const stringValues = [];
|
|
||||||
const extractStrings = (obj) => {
|
|
||||||
for (const key in obj) {
|
|
||||||
const value = obj[key];
|
|
||||||
if (typeof value === "string") {
|
|
||||||
stringValues.push(value.toLowerCase());
|
|
||||||
} else if (typeof value === "object" && value !== null) {
|
|
||||||
extractStrings(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
extractStrings(noteItem);
|
|
||||||
stringValues.push(plainNote, creator.toLowerCase());
|
|
||||||
|
|
||||||
const matchesSearch = stringValues.some((val) => val.includes(lowerSearch));
|
|
||||||
|
|
||||||
return matchesCreator && matchesOrg && matchesSearch;
|
|
||||||
});
|
|
||||||
|
|
||||||
setFilteredNotes(filtered);
|
|
||||||
setFilterAppliedNotes(filtered);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="row mx-0 px-0 align-items-center mt-0">
|
|
||||||
<div className="col-12 col-md-6 mb-0 px-1 d-flex align-items-center gap-4">
|
|
||||||
<ul className="nav nav-tabs mb-0" role="tablist">
|
|
||||||
<li className="nav-item" role="presentation">
|
|
||||||
<button
|
|
||||||
className={`nav-link ${viewType === "notes" ? "active" : ""} fs-6`}
|
|
||||||
onClick={() => setViewType("notes")}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<i className="bx bx-note me-1"></i> Notes
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li className="nav-item" role="presentation">
|
|
||||||
<button
|
|
||||||
// Corrected: Apply 'active' if viewType is either 'card' or 'list'
|
|
||||||
className={`nav-link ${viewType === "card" || viewType === "list" ? "active" : ""} fs-6`}
|
|
||||||
onClick={() => setViewType("card")} // You might want to default to 'card' when switching to contacts
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<i className="bx bx-user me-1"></i> Contacts
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr className="my-0 mb-2" style={{ borderTop: "1px solid #dee2e6" }} />
|
|
||||||
|
|
||||||
<div className="row mx-0 px-0 align-items-center mt-2">
|
|
||||||
<div className="col-12 col-md-6 mb-2 px-1 d-flex align-items-center gap-2">
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="search"
|
|
||||||
className="form-control me-0"
|
|
||||||
placeholder={viewType === "notes" ? "Search Notes..." : "Search Contact..."}
|
|
||||||
value={searchText}
|
|
||||||
onChange={(e) => setSearchText(e.target.value)}
|
|
||||||
style={{ width: "200px", height: "30px" }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Filter by funnel icon for Notes view */}
|
|
||||||
{viewType === "notes" && (
|
|
||||||
<div className="dropdown" style={{ width: "fit-content" }}>
|
|
||||||
<a
|
|
||||||
className="dropdown-toggle hide-arrow cursor-pointer d-flex align-items-center position-relative"
|
|
||||||
data-bs-toggle="dropdown"
|
|
||||||
aria-expanded="false"
|
|
||||||
>
|
|
||||||
<i className={`bx bx-slider-alt ${notesFilterCount > 0 ? "text-primary" : "text-muted"}`}></i>
|
|
||||||
{notesFilterCount > 0 && (
|
|
||||||
<span className="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-warning" style={{ fontSize: "0.4rem" }}>
|
|
||||||
{notesFilterCount}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div className="dropdown-menu p-0" style={{ minWidth: "550px" }}>
|
|
||||||
{/* Scrollable Filter Content */}
|
|
||||||
<div
|
|
||||||
className="p-3"
|
|
||||||
style={{
|
|
||||||
maxHeight: "300px",
|
|
||||||
overflowY: "auto",
|
|
||||||
overflowX: "hidden",
|
|
||||||
whiteSpace: "normal"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{allCreators.length === 0 && filteredOrganizations.length === 0 ? (
|
|
||||||
<div className="text-center text-muted py-5">
|
|
||||||
No filter found
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="d-flex gap-3">
|
|
||||||
{/* Created By */}
|
|
||||||
<div style={{ flexBasis: "30%", maxHeight: "260px", overflowY: "auto" }}>
|
|
||||||
<div style={{ position: "sticky", top: 0, background: "#fff", zIndex: 1 }}>
|
|
||||||
<p className="text-muted mb-2 pt-2">Created By</p>
|
|
||||||
</div>
|
|
||||||
{allCreators.map((name, idx) => (
|
|
||||||
<div className="form-check mb-1" key={`creator-${idx}`}>
|
|
||||||
<input
|
|
||||||
className="form-check-input form-check-input-sm"
|
|
||||||
type="checkbox"
|
|
||||||
id={`creator-${idx}`}
|
|
||||||
checked={selectedCreators.includes(name)}
|
|
||||||
onChange={() => handleToggleCreator(name)}
|
|
||||||
style={{ width: "1rem", height: "1rem" }}
|
|
||||||
/>
|
|
||||||
<label className="form-check-label text-nowrap" htmlFor={`creator-${idx}`}>
|
|
||||||
{name}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Organization */}
|
|
||||||
<div style={{ maxHeight: "260px", overflowY: "auto", overflowX: "hidden" }}>
|
|
||||||
<div style={{ position: "sticky", top: 0, background: "#fff", zIndex: 1 }}>
|
|
||||||
<p className="text-muted mb-2 pt-2">Organization</p>
|
|
||||||
</div>
|
|
||||||
{filteredOrganizations.map((org, idx) => (
|
|
||||||
<div className="form-check mb-1" key={`org-${idx}`}>
|
|
||||||
<input
|
|
||||||
className="form-check-input form-check-input-sm"
|
|
||||||
type="checkbox"
|
|
||||||
id={`org-${idx}`}
|
|
||||||
checked={selectedOrgs.includes(org)}
|
|
||||||
onChange={() => handleToggleOrg(org)}
|
|
||||||
style={{ width: "1rem", height: "1rem" }}
|
|
||||||
/>
|
|
||||||
<label className="form-check-label text-nowrap" htmlFor={`org-${idx}`}>
|
|
||||||
{org}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
{/* Sticky Footer Buttons */}
|
|
||||||
<div
|
|
||||||
className="d-flex justify-content-end gap-2 p-2 "
|
|
||||||
style={{
|
|
||||||
background: "#fff",
|
|
||||||
position: "sticky",
|
|
||||||
bottom: 0
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="btn btn-xs btn-secondary"
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedCreators([]);
|
|
||||||
setSelectedOrgs([]);
|
|
||||||
setFilteredOrganizations(allOrganizations);
|
|
||||||
setFilterAppliedNotes(notesForFilter);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Clear
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-xs btn-primary"
|
|
||||||
onClick={() => {
|
|
||||||
applyCombinedFilter();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Apply Filter
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
|
||||||
{(viewType === "card" || viewType === "list") && (
|
|
||||||
<div className="d-flex gap-2">
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`btn btn-sm p-1 ${viewType === "card" ? "btn-primary" : "btn-outline-primary"}`}
|
|
||||||
onClick={() => setViewType("card")}
|
|
||||||
>
|
|
||||||
<i className="bx bx-grid-alt"></i>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`btn btn-sm p-1 ${viewType === "list" ? "btn-primary" : "btn-outline-primary"}`}
|
|
||||||
onClick={() => setViewType("list")}
|
|
||||||
>
|
|
||||||
<i className="bx bx-list-ul"></i>
|
|
||||||
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Filter by funnel icon for Contacts view (retains numerical badge) */}
|
|
||||||
{viewType !== "notes" && (
|
|
||||||
<div className="dropdown" style={{ width: "fit-content" }}>
|
|
||||||
<a
|
|
||||||
className="dropdown-toggle hide-arrow cursor-pointer d-flex align-items-center position-relative"
|
|
||||||
data-bs-toggle="dropdown"
|
|
||||||
aria-expanded="false"
|
|
||||||
>
|
|
||||||
<i className={`bx bx-slider-alt ${filtered > 0 ? "text-primary" : "text-muted"}`}></i>
|
|
||||||
{filtered > 0 && (
|
|
||||||
<span className="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-warning" style={{ fontSize: "0.4rem" }}>
|
|
||||||
{filtered}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<ul className="dropdown-menu p-3" style={{ width: "700px" }}>
|
|
||||||
<p className="text-muted m-0 h6">Filter by</p>
|
|
||||||
|
|
||||||
{filteredBuckets.length === 0 && filteredCategories.length === 0 ? (
|
|
||||||
<div className="text-center text-muted py-5">
|
|
||||||
No filter found
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="d-flex flex-nowrap">
|
|
||||||
<div className="mt-1 me-4" style={{ flexBasis: "50%" }}>
|
|
||||||
<p className="text-small mb-1">Buckets</p>
|
|
||||||
<div className="d-flex flex-wrap">
|
|
||||||
{filteredBuckets.map(({ id, name }) => (
|
|
||||||
<div
|
|
||||||
className="form-check me-3 mb-1"
|
|
||||||
style={{ minWidth: "calc(50% - 15px)" }}
|
|
||||||
key={id}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
className="form-check-input form-check-input-sm"
|
|
||||||
type="checkbox"
|
|
||||||
id={`bucket-${id}`}
|
|
||||||
checked={tempSelectedBucketIds.includes(id)}
|
|
||||||
onChange={() => handleTempBucketChange(id)}
|
|
||||||
style={{ width: "1rem", height: "1rem" }}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="form-check-label text-nowrap text-small"
|
|
||||||
htmlFor={`bucket-${id}`}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-1" style={{ flexBasis: "50%" }}>
|
|
||||||
<p className="text-small mb-1">Categories</p>
|
|
||||||
<div className="d-flex flex-wrap">
|
|
||||||
{filteredCategories.map(({ id, name }) => (
|
|
||||||
<div
|
|
||||||
className="form-check me-3 mb-1"
|
|
||||||
style={{ minWidth: "calc(50% - 15px)" }}
|
|
||||||
key={id}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
className="form-check-input form-check-input-sm"
|
|
||||||
type="checkbox"
|
|
||||||
id={`cat-${id}`}
|
|
||||||
checked={tempSelectedCategoryIds.includes(id)}
|
|
||||||
onChange={() => handleTempCategoryChange(id)}
|
|
||||||
style={{ width: "1rem", height: "1rem" }}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="form-check-label text-nowrap text-small"
|
|
||||||
htmlFor={`cat-${id}`}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="d-flex justify-content-end gap-2 mt-1">
|
|
||||||
<button
|
|
||||||
className="btn btn-xs btn-secondary"
|
|
||||||
onClick={(e) => {
|
|
||||||
clearFilter();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Clear
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-xs btn-primary"
|
|
||||||
onClick={(e) => {
|
|
||||||
applyFilter();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Apply Filter
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-12 col-md-6 mb-2 px-5 d-flex justify-content-end align-items-center gap-2">
|
|
||||||
{(viewType === "list" || viewType === "card") && (
|
|
||||||
<label className="switch switch-primary mb-0">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="switch-input me-3"
|
|
||||||
onChange={() => setIsActive(!IsActive)}
|
|
||||||
checked={!IsActive}
|
|
||||||
disabled={loading}
|
|
||||||
/>
|
|
||||||
<span className="switch-toggle-slider">
|
|
||||||
<span className="switch-on"></span>
|
|
||||||
<span className="switch-off"></span>
|
|
||||||
</span>
|
|
||||||
<span className="ms-12">Show Inactive Contacts</span>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="btn-group">
|
|
||||||
<button
|
|
||||||
className="btn btn-sm btn-label-secondary dropdown-toggle"
|
|
||||||
type="button"
|
|
||||||
data-bs-toggle="dropdown"
|
|
||||||
aria-expanded="false"
|
|
||||||
>
|
|
||||||
<i className="bx bx-export me-2 bx-sm"></i>Export
|
|
||||||
</button>
|
|
||||||
<ul className="dropdown-menu">
|
|
||||||
<li>
|
|
||||||
<a className="dropdown-item" href="#" onClick={(e) => { e.preventDefault(); handleExport("csv"); }}>
|
|
||||||
<i className="bx bx-file me-1"></i> CSV
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a className="dropdown-item" href="#" onClick={(e) => { e.preventDefault(); handleExport("excel"); }}>
|
|
||||||
<i className="bx bxs-file-export me-1"></i> Excel
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{viewType !== "notes" && (
|
|
||||||
<li>
|
|
||||||
<a className="dropdown-item" href="#" onClick={(e) => { e.preventDefault(); handleExport("pdf"); }}>
|
|
||||||
<i className="bx bxs-file-pdf me-1"></i> PDF
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DirectoryPageHeader;
|
|
||||||
@ -7,6 +7,7 @@ import { ITEMS_PER_PAGE } from "../../utils/constants";
|
|||||||
import { useDebounce } from "../../utils/appUtils";
|
import { useDebounce } from "../../utils/appUtils";
|
||||||
import NoteCardDirectoryEditable from "../../components/Directory/NoteCardDirectoryEditable";
|
import NoteCardDirectoryEditable from "../../components/Directory/NoteCardDirectoryEditable";
|
||||||
import Pagination from "../../components/common/Pagination";
|
import Pagination from "../../components/common/Pagination";
|
||||||
|
import { NoteCardSkeleton } from "../../components/Directory/DirectoryPageSkeleton";
|
||||||
|
|
||||||
const NotesPage = ({ projectId, searchText }) => {
|
const NotesPage = ({ projectId, searchText }) => {
|
||||||
const [filters, setFilter] = useState(defaultNotesFilter);
|
const [filters, setFilter] = useState(defaultNotesFilter);
|
||||||
@ -44,7 +45,7 @@ const NotesPage = ({ projectId, searchText }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (isError) return <div>{error.message}</div>;
|
if (isError) return <div>{error.message}</div>;
|
||||||
if (isLoading) return <div>Loading...</div>;
|
if (isLoading) return <NoteCardSkeleton/>;
|
||||||
return (
|
return (
|
||||||
<div className="d-flex flex-column text-start mt-5">
|
<div className="d-flex flex-column text-start mt-5">
|
||||||
{data?.data?.length > 0 ? (
|
{data?.data?.length > 0 ? (
|
||||||
|
|||||||
@ -18,7 +18,6 @@ import {
|
|||||||
import "./ProjectDetails.css";
|
import "./ProjectDetails.css";
|
||||||
import { useProjectDetails } from "../../hooks/useProjects";
|
import { useProjectDetails } from "../../hooks/useProjects";
|
||||||
import { ComingSoonPage } from "../Misc/ComingSoonPage";
|
import { ComingSoonPage } from "../Misc/ComingSoonPage";
|
||||||
import Directory from "../Directory/Directory";
|
|
||||||
import eventBus from "../../services/eventBus";
|
import eventBus from "../../services/eventBus";
|
||||||
import ProjectProgressChart from "../../components/Dashboard/ProjectProgressChart";
|
import ProjectProgressChart from "../../components/Dashboard/ProjectProgressChart";
|
||||||
import { useProjectName } from "../../hooks/useProjects";
|
import { useProjectName } from "../../hooks/useProjects";
|
||||||
@ -27,6 +26,7 @@ import { setProjectId } from "../../slices/localVariablesSlice";
|
|||||||
import ProjectDocument from "../../components/Project/ProjectDocuments";
|
import ProjectDocument from "../../components/Project/ProjectDocuments";
|
||||||
import ProjectDocuments from "../../components/Project/ProjectDocuments";
|
import ProjectDocuments from "../../components/Project/ProjectDocuments";
|
||||||
import ProjectSetting from "../../components/Project/ProjectSetting";
|
import ProjectSetting from "../../components/Project/ProjectSetting";
|
||||||
|
import DirectoryPage from "../Directory/DirectoryPage";
|
||||||
|
|
||||||
const ProjectDetails = () => {
|
const ProjectDetails = () => {
|
||||||
|
|
||||||
@ -119,7 +119,7 @@ const ProjectDetails = () => {
|
|||||||
case "directory":
|
case "directory":
|
||||||
return (
|
return (
|
||||||
<div className="row mt-2">
|
<div className="row mt-2">
|
||||||
<Directory IsPage={false} prefernceContacts={projects_Details.id} />
|
<DirectoryPage IsPage={false} projectId={projects_Details.id} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case "documents":
|
case "documents":
|
||||||
|
|||||||
@ -39,8 +39,9 @@ export const DirectoryRepository = {
|
|||||||
GetContactProfile: (id) => api.get(`/api/directory/profile/${id}`),
|
GetContactProfile: (id) => api.get(`/api/directory/profile/${id}`),
|
||||||
|
|
||||||
CreateNote: (data) => api.post("/api/directory/note", data),
|
CreateNote: (data) => api.post("/api/directory/note", data),
|
||||||
GetNote: (id, isActive) =>
|
GetContactNotes: (id, isActive) =>
|
||||||
api.get(`/api/directory/notes/${id}?active=${isActive}`),
|
api.get(`/api/directory/notes/${id}?active=${isActive}`),
|
||||||
|
|
||||||
UpdateNote: (id, data) => api.put(`/api/directory/note/${id}`, data),
|
UpdateNote: (id, data) => api.put(`/api/directory/note/${id}`, data),
|
||||||
DeleteNote: (id, isActive) =>
|
DeleteNote: (id, isActive) =>
|
||||||
api.delete(`/api/directory/note/${id}?active=${isActive}`),
|
api.delete(`/api/directory/note/${id}?active=${isActive}`),
|
||||||
|
|||||||
@ -34,13 +34,19 @@ import LegalInfoCard from "../pages/TermsAndConditions/LegalInfoCard";
|
|||||||
|
|
||||||
// Protected Route Wrapper
|
// Protected Route Wrapper
|
||||||
import ProtectedRoute from "./ProtectedRoute";
|
import ProtectedRoute from "./ProtectedRoute";
|
||||||
import Directory from "../pages/Directory/Directory";
|
|
||||||
import LoginWithOtp from "../pages/authentication/LoginWithOtp";
|
import LoginWithOtp from "../pages/authentication/LoginWithOtp";
|
||||||
import ExpensePage from "../pages/Expense/ExpensePage";
|
import ExpensePage from "../pages/Expense/ExpensePage";
|
||||||
import TenantDetails from "../pages/Tenant/TenantDetails";
|
import TenantDetails from "../pages/Tenant/TenantDetails";
|
||||||
import SelfTenantDetails from "../pages/Tenant/SelfTenantDetails";
|
import SelfTenantDetails from "../pages/Tenant/SelfTenantDetails";
|
||||||
import SuperTenantDetails from "../pages/Tenant/SuperTenantDetails";
|
import SuperTenantDetails from "../pages/Tenant/SuperTenantDetails";
|
||||||
import DirectoryPage from "../pages/Directory/DirectoryPage";
|
import DirectoryPage from "../pages/Directory/DirectoryPage";
|
||||||
|
import RootRedirect from "./RootRedirect";
|
||||||
|
import MainLogin from "../pages/authentication/MainLogin";
|
||||||
|
import MainLoginWithOTPPage from "../pages/authentication/MainLoginWithOTPPage";
|
||||||
|
import MainRegisterPage from "../pages/authentication/MainRegisterPage";
|
||||||
|
import MainForgetPage from "../pages/authentication/MainForgetPage";
|
||||||
|
import MainResetPasswordPage from "../pages/authentication/MainResetPasswordPage";
|
||||||
|
import TenantPage from "../pages/Tenant/TenantPage";
|
||||||
|
|
||||||
const router = createBrowserRouter(
|
const router = createBrowserRouter(
|
||||||
[
|
[
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user