pramod_Task-#357 : Added Admin Update and Delete Functionality in Manage Buckets Modal #155

Merged
pramod.mahajan merged 32 commits from pramod_Task-#357 into Feature_Directory 2025-05-28 07:03:52 +00:00
21 changed files with 983 additions and 394 deletions

View File

@ -5069,6 +5069,9 @@ fieldset:disabled .btn {
.card-group > .card { .card-group > .card {
margin-bottom: var(--bs-card-group-margin); margin-bottom: var(--bs-card-group-margin);
} }
.card-minHeight{
min-height: 430px;
}
@media (min-width: 576px) { @media (min-width: 576px) {
.card-group { .card-group {
display: flex; display: flex;

View File

@ -0,0 +1,15 @@
import React, { createContext, useContext, useState } from "react";
const FabContext = createContext();
export const FabProvider = ({ children }) => {
const [actions, setActions] = useState([]);
return (
<FabContext.Provider value={{ actions, setActions }}>
{children}
</FabContext.Provider>
);
};
export const useFab = () => useContext(FabContext);

View File

@ -30,7 +30,7 @@ const CardViewDirectory = ({
(contact?.name || "").trim().split(" ")[1]?.charAt(0) || "" (contact?.name || "").trim().split(" ")[1]?.charAt(0) || ""
} }
/>{" "} />{" "}
<p className="m-0">{contact.name}</p> <span className="text-heading fs-6"> {contact.name}</span>
</div> </div>
<div> <div>
<div className={`dropdown z-2 ${!IsActive && "d-none"}`}> <div className={`dropdown z-2 ${!IsActive && "d-none"}`}>
@ -55,20 +55,19 @@ const CardViewDirectory = ({
setSelectedContact(contact); setSelectedContact(contact);
setIsOpenModal(true); setIsOpenModal(true);
}} }}
> >
<a className="dropdown-item px-2 py-0"> <a className="dropdown-item px-2 cursor-pointer py-1">
<i className="bx bx-pencil bx-xs me-2"></i> <i className="bx bx-edit bx-xs text-primary me-2"></i>
<span className="align-left small-text">Modify</span> <span className="align-left ">Modify</span>
</a> </a>
</li> </li>
<li> <li>
<a <a
className="dropdown-item px-2 py-0" className="dropdown-item px-2 cursor-pointer py-1"
onClick={() => IsDeleted(contact.id)} onClick={() => IsDeleted(contact.id)}
> >
<i className="bx bx-trash bx-xs me-2"></i> <i className="bx bx-trash text-danger bx-xs me-2"></i>
<span className="align-left small-text">Delete</span> <span className="align-left">Delete</span>
</a> </a>
</li> </li>
</ul> </ul>
@ -80,7 +79,7 @@ const CardViewDirectory = ({
<li className="list-inline-item me-1" style={{ fontSize: "10px" }}> <li className="list-inline-item me-1" style={{ fontSize: "10px" }}>
<i className="bx bx-building bx-xs"></i> <i className="bx bx-building bx-xs"></i>
</li> </li>
<li className="list-inline-item" style={{ fontSize: "10px" }}> <li className="list-inline-item text-small">
{contact.organization} {contact.organization}
</li> </li>
</ul> </ul>
@ -96,52 +95,54 @@ const CardViewDirectory = ({
> >
<hr className="my-0" /> <hr className="my-0" />
{contact.contactEmails[0] && ( {contact.contactEmails[0] && (
<ul className="list-inline my-1 "> <ul className="list-inline my-1 ">
<li className="list-inline-item me-2"> <li className="list-inline-item me-2">
<i className="bx bx-envelope bx-xs"></i> <i className="bx bx-envelope bx-xs"></i>
</li> </li>
<li className="list-inline-item small-text"> <li className="list-inline-item text-small">
{contact.contactEmails[0]?.emailAddress} {contact.contactEmails[0]?.emailAddress}
</li> </li>
</ul> </ul>
)} )}
{contact.contactPhones[0] && ( {contact.contactPhones[0] && (
<ul className="list-inline m-0"> <ul className="list-inline m-0 ">
<li className="list-inline-item me-2"> <li className="list-inline-item me-1">
<i <i
className={` ${getPhoneIcon( className={` ${getPhoneIcon(
contact.contactPhones[0].label contact.contactPhones[0].label
)} bx-xs`} )} bx-xs`}
></i> ></i>
</li> </li>
<li className="list-inline-item small-text"> <li className="list-inline-item text-small">
{contact.contactPhones[0]?.phoneNumber} {contact.contactPhones[0]?.phoneNumber}
</li> </li>
</ul> </ul>
)} )}
<ul className="list-inline m-0"> <ul className="list-inline m-0">
<li className="list-inline-item me-2"> <li className="list-inline-item me-2 my-1">
<i className="bx bx-merge bx-xs"></i> <i className="fa-solid fa-tag fs-6"></i>
</li> </li>
<li className="list-inline-item small-text"> <li className="list-inline-item text-small active">
{contact.contactCategory.name} {contact.contactCategory.name}
</li> </li>
</ul> </ul>
<ul className="list-inline m-0"> <ul className="list-inline m-0">
{contact.bucketIds.map((bucketId) => ( {contact.bucketIds?.map((bucketId) => (
<React.Fragment key={bucketId}> <li key={bucketId} className="list-inline-item me-1">
<li className="list-inline-item me-1"> <span className="badge bg-label-primary rounded-pill d-flex align-items-center gap-1" style={{padding:'0.1rem 0.3rem'}}>
<i className="bx bx-pin bx-xs"></i> <i className="bx bx-pin bx-xs"></i>
</li> <span className="small-text">
<li className="list-inline-item small-text"> {getBucketNameById(buckets, bucketId)}
{getBucketNameById(buckets, bucketId)} </span>
</li> </span>
</React.Fragment> </li>
))} ))}
</ul> </ul>
</div> </div>
</div> </div>
); );

View File

@ -6,7 +6,7 @@ export const ContactSchema = z
contactCategoryId: z.string().nullable().optional(), contactCategoryId: z.string().nullable().optional(),
address: z.string().optional(), address: z.string().optional(),
description: z.string().min(1, { message: "Description is required" }), description: z.string().min(1, { message: "Description is required" }),
projectIds: z.array(z.string()).min(1, "Project is required"), projectIds: z.array(z.string()), // min(1, "Project is required")
contactEmails: z contactEmails: z
.array( .array(
z.object({ z.object({
@ -39,7 +39,7 @@ export const ContactSchema = z
}) })
) )
.min(1, { message: "At least one tag is required" }), .min(1, { message: "At least one tag is required" }),
bucketIds: z.array(z.string()).optional(), bucketIds: z.array(z.string()).min(1,{message:"At least one Label required"}),
}) })
// .refine((data) => { // .refine((data) => {

View File

@ -0,0 +1,147 @@
import React, { useState } from "react";
import { useSortableData } from "../../hooks/useSortableData";
const EmployeeList = ({ employees }) => {
const [selectedIds, setSelectedIds] = useState([]);
const [searchTerm, setSearchTerm] = useState("");
const handleCheckboxChange = (id) => {
setSelectedIds((prev) =>
prev.includes(id) ? prev?.filter((empId) => empId !== id) : [...prev, id]
);
};
const getSelectedEmployees = () => {
console.log("Selected Employee IDs:", selectedIds);
};
const {
items: sortedEmployees,
requestSort,
sortConfig,
} = useSortableData(employees, {
key: (e) => `${e?.firstName} ${e?.lastName}`,
direction: "asc",
});
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 filteredEmployees = sortedEmployees?.filter((employee) => {
const fullName =
`${employee?.firstName} ${employee?.lastName}`?.toLowerCase();
// const email = employee.email.toLowerCase();
// const role = employee.jobRole.toLowerCase();
const term = searchTerm?.toLowerCase();
return fullName.includes(term);
// email.includes(term) ||
// role.includes(term)
});
return (
<>
<div className="d-flex justify-content-between mt-2">
<p className="m-0 fs-6 fw-normal">Add Employee</p>
<div className="px-1">
<input
type="search"
className="form-control form-control-sm"
placeholder="Search Employee..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
</div>
<div className="table-responsive px-1 my-1 px-sm-0">
<table className="table align-middle mb-0">
<thead className="table-light">
<tr>
<th
onClick={() =>
requestSort((e) => `${e.firstName} ${e.lastName}`)
}
className="text-start cursor-pointer"
>
<span className="ps-2">Name {getSortIcon()}</span>
</th>
<th className="text-start">Role</th>
<th scope="col">Status</th>
<th className="text-start">Bucket</th>
</tr>
</thead>
<tbody>
{employees.length === 0 ? (
<tr>
<td colSpan={4} >
<div className="d-flex justify-content-center align-items-center py-5">
No Employee Available
</div>
</td>
</tr>
) : filteredEmployees.length === 0 ? (
<tr className="my-4">
<td colSpan={4}>
<div className="d-flex justify-content-center align-items-center py-5">
No Matchinng Employee Found.
</div>
</td>
</tr>
) : (
filteredEmployees?.map((employee) => (
<tr key={employee.id}>
<td>
<div className="d-flex align-items-start text-start">
<input
className="form-check-input me-3 mt-1"
type="checkbox"
checked={selectedIds.includes(employee.id)}
onChange={() => handleCheckboxChange(employee.id)}
/>
<div>
<p className="fw-semibold mb-0">
{`${employee.firstName} ${employee.lastName}`}
</p>
<small className="text-muted">{employee.email}</small>
</div>
</div>
</td>
<td className="text-start">
<small className="text-muted">{employee.jobRole}</small>
</td>
<td>
<span
className={`badge rounded-pill px-3 py-1 ${
employee.isActive
? "bg-success-subtle text-success"
: "bg-danger-subtle text-danger"
}`}
>
{employee.isActive ? "Active" : "Inactive"}
</span>
</td>
<td className="text-start">
<small className="text-muted">
<i className="fa fa-hashtag" aria-hidden="true"></i>{" "}
{employee.jobRole}
</small>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</>
);
};
export default EmployeeList;

View File

@ -8,15 +8,36 @@ import Directory from "../../pages/Directory/Directory";
import { DirectoryRepository } from "../../repositories/DirectoryRepository"; import { DirectoryRepository } from "../../repositories/DirectoryRepository";
import { cacheData, getCachedData } from "../../slices/apiDataManager"; import { cacheData, getCachedData } from "../../slices/apiDataManager";
import { useBuckets } from "../../hooks/useDirectory"; import { useBuckets } from "../../hooks/useDirectory";
import EmployeeList from "./EmployeeList";
import { useAllEmployees, useEmployees } from "../../hooks/useEmployees";
import { useSortableData } from "../../hooks/useSortableData";
import ConfirmModal from "../common/ConfirmModal";
const ManageBucket = () => const ManageBucket = () => {
{
const [bucketList, setBucketList] = useState([]); const [bucketList, setBucketList] = useState([]);
const { employeesList } = useAllEmployees(false);
const { buckets } = useBuckets(); const { buckets, loading,refetch } = useBuckets();
const [action_bucket, setAction_bucket] = useState(false); const [action_bucket, setAction_bucket] = useState(false);
const [isSubmitting, setSubmitting] = useState(false); const [isSubmitting, setSubmitting] = useState(false);
const [selected_bucket, select_bucket] = useState(null); const [selected_bucket, select_bucket] = useState(null);
const [deleteBucket, setDeleteBucket] = useState(null);
const [searchTerm, setSearchTerm] = useState("");
const {
items: sortedBuckteList,
requestSort,
sortConfig,
} = useSortableData(bucketList, {
key: (e) => `${e.name}`,
direction: "asc",
});
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 { const {
register, register,
@ -29,55 +50,75 @@ const ManageBucket = () =>
name: "", name: "",
description: "", description: "",
}, },
} ); });
const onSubmit = async (data) => { const onSubmit = async (data) => {
setSubmitting(true); setSubmitting(true);
try { try {
let response; let response;
if ( selected_bucket ) if (selected_bucket) {
{ let payload = { ...data, id: selected_bucket.id };
let payload ={...data, id:selected_bucket.id} response = await DirectoryRepository.UpdateBuckets(
response = await DirectoryRepository.UpdateBuckets(selected_bucket.id, payload); selected_bucket.id,
const cache_buckets = getCachedData("buckets") || []; payload
const updatedBuckets = cache_buckets.map((bucket) => );
bucket.id === selected_bucket.id ? response?.data : bucket const cache_buckets = getCachedData("buckets") || [];
); const updatedBuckets = cache_buckets.map((bucket) =>
cacheData( "buckets", updatedBuckets ); bucket.id === selected_bucket.id ? response?.data : bucket
setBucketList(updatedBuckets); );
showToast("Bucket Updated Successfully", "success"); cacheData("buckets", updatedBuckets);
} else { setBucketList(updatedBuckets);
response = await DirectoryRepository.CreateBuckets(data); showToast("Bucket Updated Successfully", "success");
const cache_buckets = getCachedData("buckets") || []; } else {
const updatedBuckets = [...cache_buckets, response?.data]; response = await DirectoryRepository.CreateBuckets(data);
cacheData( "buckets", updatedBuckets ); const cache_buckets = getCachedData("buckets") || [];
setBucketList(updatedBuckets); const updatedBuckets = [...cache_buckets, response?.data];
showToast("Bucket Created Successfully", "success"); cacheData("buckets", updatedBuckets);
setBucketList(updatedBuckets);
showToast("Bucket Created Successfully", "success");
}
handleBack();
} catch (error) {
const message =
error?.response?.data?.message ||
error?.message ||
"Error occurred during API call";
showToast(message, "error");
} }
};
handleBack(); const handleDeleteContact = async () => {
} catch (error) { try {
const message = const resp = await DirectoryRepository.DeleteBucket( deleteBucket );
error?.response?.data?.message || const cache_buckets = getCachedData("buckets") || [];
error?.message || const updatedBuckets = cache_buckets.filter((bucket) =>
"Error occurred during API call"; bucket.id != deleteBucket
showToast(message, "error"); );
} 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(() => { useEffect(() => {
reset({ reset({
name: selected_bucket?.name || "", name: selected_bucket?.name || "",
description: selected_bucket?.description || "", description: selected_bucket?.description || "",
}); });
}, [ selected_bucket ] ); }, [selected_bucket]);
useEffect( () => useEffect(() => {
{ setBucketList(buckets);
setBucketList( buckets ) }, [buckets]);
}, [ buckets ] )
const handleBack = () => { const handleBack = () => {
select_bucket(null); select_bucket(null);
@ -85,141 +126,214 @@ const ManageBucket = () =>
setSubmitting(false); setSubmitting(false);
}; };
const sortedBucktesList = sortedBuckteList?.filter((bucket) => {
const term = searchTerm?.toLowerCase();
const name = bucket.name?.toLowerCase();
return name?.includes(term);
});
return ( return (
<div className="container m-0 p-0" style={{ minHeight: "200px" }}> <>
<div className="d-flex justify-content-center"> {deleteBucket && (
<p className="fs-h6 fw-semibold m-0">Manage Buckets</p> <div
</div> className={`modal fade ${deleteBucket ? "show" : ""}`}
<div className="d-flex justify-content-between px-2 px-sm-0 mt-5 mt-sm-1 align-items-center"> tabIndex="-1"
<i role="dialog"
className={`fa-solid fa-arrow-left fs-5 cursor-pointer ${ style={{
action_bucket ? "" : "d-none" display: deleteBucket ? "block" : "none",
}`} backgroundColor: deleteBucket ? "rgba(0,0,0,0.5)" : "transparent",
onClick={handleBack} }}
></i>
<button
type="button"
className={`btn btn-xs btn-primary ms-auto ${
action_bucket ? "d-none" : ""
}`}
onClick={() => setAction_bucket(true)}
> >
<i className="bx bx-plus-circle me-2"></i> <ConfirmModal
Add Bucket type={"delete"}
</button> header={"Delete Bucket"}
</div> message={"Are you sure you want delete?"}
<div> onSubmit={handleDeleteContact}
{!action_bucket ? ( onClose={() => setDeleteBucket(null)}
<div className="table-responsive text-nowrap pt-1 px-2 px-sm-0"> // loading={IsDeleting}
<table className="table px-2"> />
<thead className="p-0"> </div>
<tr className="p-0"> )}
<th colSpan={2}>
<div className="d-flex justify-content-start align-items-center gap-1">
<IconButton
size={12}
iconClass="fa-solid fa-bucket"
color="info"
/>
<span>Name</span>
</div>
</th>
<th className="text-start d-none d-sm-table-cell">
<div className="d-flex align-items-center justify-content-start gap-1">
<IconButton
size={12}
iconClass="fa-solid fa-file-lines"
color="primary"
/>
<span>Description</span>
</div>
</th>
<th>
<div className="d-flex align-items-center justify-content-center gap-1">
<IconButton
size={12}
iconClass="fa-solid fa-gear"
color="secondary"
/>
<span>Action</span>
</div>
</th>
</tr>
</thead>
<tbody className="table-border-bottom-0 overflow-auto"> <div className="container m-0 p-0" style={{ minHeight: "200px" }}>
{bucketList.map((bucket) => ( <div className="d-flex justify-content-center">
<tr key={bucket.id}> <p className="fs-6 fw-semibold m-0">Manage Buckets</p>
<td colSpan={2} className="text-start"> </div>
{bucket.name} <div className="d-flex justify-content-between px-2 px-sm-0 mt-5 mt-sm-1 align-items-center">
</td> {action_bucket ? (
<td className="text-start d-none d-sm-table-cell"> <i
{bucket.description} className={`fa-solid fa-arrow-left fs-5 cursor-pointer`}
</td> onClick={handleBack}
<td className="justify-content-center"> ></i>
<div className="d-flex justify-content-center align-items-center gap-2"> ) : (
<i <div className="d-flex align-items-center gap-2">
className="bx bx-edit bx-sm text-primary cursor-pointer"
onClick={() => {
select_bucket(bucket);
setAction_bucket(true);
}}
></i>
<i className="bx bx-trash bx-sm text-danger cursor-pointer"></i>
<i className="bx bx-user-plus cursor-pointer"></i>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<form onSubmit={handleSubmit(onSubmit)} className="px-2 px-sm-0">
<div className="">
<label className="form-label">Bucket Name</label>
<input <input
type="search"
className="form-control form-control-sm" className="form-control form-control-sm"
{...register("name")} placeholder="Search Bucket ..."
/> value={searchTerm}
{errors.name && ( onChange={(e) => setSearchTerm(e.target.value)}
<small className="danger-text">{errors.name.message}</small> />
)} <i
className={`bx bx-refresh cursor-pointer fs-4 ${loading ? "spin" : ""
}`}
title="Refresh"
onClick={() => rrefetch()}
/>
</div> </div>
<div className=""> )}
<label className="form-label">Bucket Discription</label>
<textarea <button
className="form-control form-control-sm" type="button"
{...register("description")} className={`btn btn-sm btn-primary ms-auto ${
/> action_bucket ? "d-none" : ""
{errors.description && ( }`}
<small className="danger-text"> onClick={() => setAction_bucket(true)}
{errors.description.message} >
</small> <i className="bx bx-plus-circle me-2"></i>
)} Add Bucket
</button>
</div>
<div>
{!action_bucket ? (
<div className="table-responsive text-nowrap pt-1 px-2 px-sm-0">
<table className="table px-2">
<thead className="p-0 table-light">
<tr className="p-0">
<th
colSpan={2}
className="cursor-pointer"
onClick={() => requestSort((e) => `${e.name} `)}
>
<div className="d-flex justify-content-start align-items-center gap-1 mx-2">
<span>Name {getSortIcon()}</span>
</div>
</th>
<th className="text-start d-none d-sm-table-cell">
<div className="d-flex align-items-center justify-content-center gap-1">
<span>Description</span>
</div>
</th>
<th>
<div className="d-flex align-items-center justify-content-center gap-1">
<span>Action</span>
</div>
</th>
</tr>
</thead>
<tbody className="table-border-bottom-0 overflow-auto">
{loading && (
<tr className="mt-10">
<td colSpan={4}>
{" "}
<div className="d-flex justify-content-center align-items-center py-5">
Loading...
</div>
</td>
</tr>
)}
{!loading && buckets.length == 0 && (
<tr>
<td colSpan={4}>
<div className="d-flex justify-content-center align-items-center py-5">
Bucket Not Available.
</div>
</td>
</tr>
)}
{!loading && sortedBucktesList.length == 0 && (
<tr>
<td className="text-center py-4 h-25" colSpan={4}>
<div className="d-flex justify-content-center align-items-center py-5">
No Matching Bucket Found.
</div>
</td>
</tr>
)}
{sortedBucktesList.map((bucket) => (
<tr key={bucket.id}>
<td colSpan={2} className="text-start text-wrap">
<i className="bx bx-right-arrow-alt me-1"></i>{" "}
{bucket.name}
</td>
<td
className="text-start d-none d-sm-table-cell text-wrap"
style={{ width: "60%" }}
>
{bucket.description}
</td>
<td className="justify-content-center">
<div className="d-flex justify-content-center align-items-center gap-2">
<i
className="bx bx-edit bx-sm text-primary cursor-pointer"
onClick={() => {
select_bucket(bucket);
setAction_bucket(true);
}}
></i>
<i
className="bx bx-trash bx-sm text-danger cursor-pointer"
onClick={() => setDeleteBucket(bucket?.id)}
></i>
<i className="bx bx-user-plus cursor-pointer"></i>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div> </div>
<div className="mt-2 d-flex justify-content-center gap-3"> ) : (
<button <>
type="reset" <form onSubmit={handleSubmit(onSubmit)} className="px-2 px-sm-0">
className="btn btn-sm btn-secondary" <div className="">
disabled={isSubmitting} <label className="form-label">Bucket Name</label>
> <input
Cancel className="form-control form-control-sm"
</button> {...register("name")}
<button />
type="submit" {errors.name && (
className="btn btn-sm btn-primary" <small className="danger-text">{errors.name.message}</small>
disabled={isSubmitting} )}
> </div>
{isSubmitting ? "Please wait..." : "Submit"} <div className="">
</button> <label className="form-label">Bucket Discription</label>
</div> <textarea
</form> className="form-control form-control-sm"
)} rows="3"
{...register("description")}
/>
{errors.description && (
<small className="danger-text">
{errors.description.message}
</small>
)}
</div>
<div className="mt-2 d-flex justify-content-center gap-3">
<button
type="reset"
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>
<EmployeeList employees={employeesList} />
</>
)}
</div>
</div> </div>
</div> </>
); );
}; };

View File

@ -14,10 +14,11 @@ import useMaster, {
} from "../../hooks/masterHook/useMaster"; } from "../../hooks/masterHook/useMaster";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { changeMaster } from "../../slices/localVariablesSlice"; import { changeMaster } from "../../slices/localVariablesSlice";
import { useBuckets } from "../../hooks/useDirectory"; import { useBuckets, useOrganization } from "../../hooks/useDirectory";
import { useProjects } from "../../hooks/useProjects"; import { useProjects } from "../../hooks/useProjects";
import SelectMultiple from "../common/SelectMultiple"; import SelectMultiple from "../common/SelectMultiple";
import {ContactSchema} from "./DirectorySchema"; import {ContactSchema} from "./DirectorySchema";
import InputSuggestions from "../common/InputSuggestion";
@ -25,13 +26,15 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
const selectedMaster = useSelector( const selectedMaster = useSelector(
(store) => store.localVariables.selectedMaster (store) => store.localVariables.selectedMaster
); );
const [categoryData, setCategoryData] = useState([]); const [ categoryData, setCategoryData ] = useState( [] );
const [TagsData, setTagsData] = useState([]); const [TagsData, setTagsData] = useState([]);
const { data, loading } = useMaster(); const { data, loading } = useMaster();
const { buckets, loading: bucketsLoaging } = useBuckets(); const { buckets, loading: bucketsLoaging } = useBuckets();
const { projects, loading: projectLoading } = useProjects(); const { projects, loading: projectLoading } = useProjects();
const { contactCategory, loading: contactCategoryLoading } = const { contactCategory, loading: contactCategoryLoading } =
useContactCategory(); useContactCategory();
const {organizationList,loading:orgLoading} = useOrganization()
const { contactTags, loading: Tagloading } = useContactTags(); const { contactTags, loading: Tagloading } = useContactTags();
const [IsSubmitting, setSubmitting] = useState(false); const [IsSubmitting, setSubmitting] = useState(false);
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -136,6 +139,7 @@ useEffect(() => {
setSubmitting(true); setSubmitting(true);
submitContact(cleaned, reset, setSubmitting); submitContact(cleaned, reset, setSubmitting);
}; };
const orgValue = watch("organization")
const handleClosed = () => { const handleClosed = () => {
onCLosed(); onCLosed();
@ -160,10 +164,12 @@ useEffect(() => {
<div className="col-md-6 text-start"> <div className="col-md-6 text-start">
<label className="form-label">Organization</label> <label className="form-label">Organization</label>
<input <InputSuggestions
className="form-control form-control-sm" organizationList={organizationList}
{...register("organization")} value={getValues("organization") || ""}
/> onChange={(val) => setValue("organization", val)}
error={errors.organization?.message}
/>
{errors.organization && ( {errors.organization && (
<small className="danger-text"> <small className="danger-text">
{errors.organization.message} {errors.organization.message}
@ -356,8 +362,7 @@ useEffect(() => {
<label className="form-label ">Select Label</label> <label className="form-label ">Select Label</label>
<ul <ul
className="d-flex flex-wrap px-1 list-unstyled overflow-auto mb-0" className="d-flex flex-wrap px-1 list-unstyled mb-0"
style={{ maxHeight: "80px" }}
> >
{bucketsLoaging && <p>Loading...</p>} {bucketsLoaging && <p>Loading...</p>}
{buckets?.map((item) => ( {buckets?.map((item) => (

View File

@ -6,7 +6,6 @@ import NotesDirectory from "./NotesDirectory";
const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => { const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
const { conatProfile, loading } = useContactProfile(contact?.id); const { conatProfile, loading } = useContactProfile(contact?.id);
const [activeTab, setActiveTab] = useState("profile");
const [profileContact, setProfileContact] = useState(); const [profileContact, setProfileContact] = useState();
useEffect(() => { useEffect(() => {
@ -34,9 +33,11 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
<i className="bx bx-building bx-xs"></i>{" "} <i className="bx bx-building bx-xs"></i>{" "}
{conatProfile?.organization} {conatProfile?.organization}
</span> </span>
<span className="small-text">Manager</span>
</div> </div>
</div> </div>
<div className="d-flex flex-column text-start"> <div className="row">
<div className="col-12 col-md-6 d-flex flex-column text-start">
{conatProfile?.contactEmails?.length > 0 && ( {conatProfile?.contactEmails?.length > 0 && (
<div className="d-flex mb-2"> <div className="d-flex mb-2">
<div style={{ width: "100px", minWidth: "100px" }}> <div style={{ width: "100px", minWidth: "100px" }}>
@ -61,7 +62,7 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
</div> </div>
<div> <div>
<ul className="list-inline mb-0"> <ul className="list-inline mb-0">
{conatProfile.contactPhones.map((phone, idx) => ( {conatProfile?.contactPhones.map((phone, idx) => (
<li className="list-inline-item me-3" key={idx}> <li className="list-inline-item me-3" key={idx}>
<i className="bx bx-phone bx-xs me-1"></i> <i className="bx bx-phone bx-xs me-1"></i>
{phone.phoneNumber} {phone.phoneNumber}
@ -87,7 +88,28 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
</div> </div>
</div> </div>
)} )}
</div> </div>
{ conatProfile?.buckets?.length > 0 &&
<div className="col-12 col-md-6 d-flex flex-column text-start">
{conatProfile?.contactEmails?.length > 0 && (
<div className="d-flex mb-2 align-items-center">
<div style={{ width: "100px", minWidth: "100px" }}>
<p className="m-0">Buckets</p>
</div>
<div>
<ul className="list-inline mb-0">
{conatProfile.buckets.map((bucket) => (
<li className="list-inline-item me-2" key={bucket.id}>
<span class="badge bg-label-primary my-1">{ bucket.name}</span>
</li>
))}
</ul>
</div>
</div>
)}
</div>
}
</div>
<hr className="my-1" /> <hr className="my-1" />
<NotesDirectory <NotesDirectory

View File

@ -386,8 +386,8 @@ await submitContact({ ...cleaned, id: existingContact.id });
<label className="form-label ">Select Label</label> <label className="form-label ">Select Label</label>
<ul <ul
className="d-flex flex-wrap px-1 list-unstyled overflow-auto mb-0" className="d-flex flex-wrap px-1 list-unstyled mb-0"
style={{ maxHeight: "80px" }}
> >
{bucketsLoaging && <p>Loading...</p>} {bucketsLoaging && <p>Loading...</p>}
{buckets?.map((item) => ( {buckets?.map((item) => (

View File

@ -0,0 +1,35 @@
.fab-container {
position: fixed;
bottom: 35px;
right: 30px;
z-index: 1050;
display: flex;
flex-direction: column;
align-items: end;
}
.fab-main {
/* width: 45px;
height: 45px;
border-radius: 100%;
background-color: #0d6efd;
color: white;
border: none; */
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
/* font-size: 24px; */
cursor: pointer;
pointer-events: auto;
}
.fab-option {
pointer-events: auto;
margin-bottom: 10px;
}
@media (max-width: 768px) {
.fab-container {
right: 20px;
left: 50%;
bottom: 20px;
}
}

View File

@ -0,0 +1,33 @@
import React, { useState } from "react";
import {useFab} from "../../Context/FabContext";
import './FloatingMenu.css'
const FloatingMenu = () => {
const { actions } = useFab();
const [open, setOpen] = useState(false);
if (actions.length === 0) return null;
return (
<div className="fab-container">
{open &&
actions.map((action, index) => (
<button
key={index}
className={`badge bg-label-${action.color} rounded-pill mb-2 d-inline-flex align-items-center gap-2 px-3 py-1 cursor-pointer fab-option`}
onClick={action.onClick}
title={action.label}
>
<i className={action.icon}></i>
<span>{action.label}</span>
</button>
))}
<button type="button" className="btn btn-lg btn-icon rounded-pill me-2 btn-primary fab-main " onClick={() => setOpen(!open)}>
<span className={`bx ${open ? "bx-x" : "bx-plus"}`}></span>
</button>
</div>
);
};
export default FloatingMenu;

View File

@ -54,7 +54,20 @@ useEffect(() => {
// Dynamically generate data-bs attributes // Dynamically generate data-bs attributes
const dataAttributesProps = Object.keys(dataAttributes).map(key => ({ const dataAttributesProps = Object.keys(dataAttributes).map(key => ({
[key]: dataAttributes[key], [key]: dataAttributes[key],
})); } ) );
// The gray background
const backdropStyle = {
position: 'fixed',
top: 0,
bottom: 0,
left: 0,
right: 0,
backgroundColor: 'rgba(0,0,0,0.3)',
};
return ( return (
<div <div
@ -65,8 +78,9 @@ useEffect(() => {
aria-hidden="true" aria-hidden="true"
ref={modalRef} // Assign the ref to the modal element ref={modalRef} // Assign the ref to the modal element
{...dataAttributesProps} {...dataAttributesProps}
style={backdropStyle}
> >
<div className={`modal-dialog ${dialogClass} ${modalSizeClass } mx-sm-auto mx-1`} role={role}> <div className={`modal-dialog ${dialogClass} ${modalSizeClass } mx-sm-auto mx-1`} role={role} >
<div className="modal-content"> <div className="modal-content">
<div className="modal-header p-0"> <div className="modal-header p-0">
{/* Close button inside the modal header */} {/* Close button inside the modal header */}

View File

@ -0,0 +1,79 @@
import React, { useState } from "react";
const InputSuggestions = ({
organizationList = [],
value,
onChange,
error,
}) => {
const [filteredList, setFilteredList] = useState([]);
const [showSuggestions, setShowSuggestions] = useState(false);
const handleInputChange = (e) => {
const val = e.target.value;
onChange(val);
const matches = organizationList.filter((org) =>
org.toLowerCase().includes(val.toLowerCase())
);
setFilteredList(matches);
setShowSuggestions(true);
};
const handleSelectSuggestion = (val) => {
onChange(val);
setShowSuggestions(false);
};
return (
<div className="position-relative">
<input
className="form-control form-control-sm"
value={value}
onChange={handleInputChange}
onBlur={() => setTimeout(() => setShowSuggestions(false), 150)}
onFocus={() => {
if (value) setShowSuggestions(true);
}}
/>
{showSuggestions && filteredList.length > 0 && (
<ul
className="list-group shadow-sm position-absolute w-100 bg-white border zindex-tooltip"
style={{
maxHeight: "180px",
overflowY: "auto",
marginTop: "2px",
zIndex: 1000,
borderRadius:"0px"
}}
>
{filteredList.map((org) => (
<li
key={org}
className="list-group-item list-group-item-action border-none "
style={{
cursor: "pointer",
padding: "5px 12px",
fontSize: "14px",
transition: "background-color 0.2s",
}}
onMouseDown={() => handleSelectSuggestion(org)}
onMouseEnter={(e) =>
(e.currentTarget.style.backgroundColor = "#f8f9fa")
}
onMouseLeave={(e) =>
(e.currentTarget.style.backgroundColor = "transparent")
}
>
{org}
</li>
))}
</ul>
)}
{error && <small className="danger-text">{error}</small>}
</div>
);
};
export default InputSuggestions;

View File

@ -1,21 +1,21 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from "react";
import { useFormContext } from 'react-hook-form'; import { useFormContext } from "react-hook-form";
import './MultiSelectDropdown.css'; import "./MultiSelectDropdown.css";
const SelectMultiple = ({ const SelectMultiple = ({
name, name,
options = [], options = [],
label = 'Select options', label = "Select options",
labelKey = 'name', labelKey = "name",
valueKey = 'id', valueKey = "id",
placeholder = 'Please select...', placeholder = "Please select...",
IsLoading = false IsLoading = false,
}) => { }) => {
const { setValue, watch } = useFormContext(); const { setValue, watch } = useFormContext();
const selectedValues = watch(name) || []; const selectedValues = watch(name) || [];
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState("");
const dropdownRef = useRef(null); const dropdownRef = useRef(null);
useEffect(() => { useEffect(() => {
@ -24,8 +24,8 @@ const SelectMultiple = ({
setIsOpen(false); setIsOpen(false);
} }
}; };
document.addEventListener('mousedown', handleClickOutside); document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside);
}, []); }, []);
const handleCheckboxChange = (value) => { const handleCheckboxChange = (value) => {
@ -44,35 +44,37 @@ const SelectMultiple = ({
<div ref={dropdownRef} className="multi-select-dropdown-container"> <div ref={dropdownRef} className="multi-select-dropdown-container">
<label className="form-label mb-1">{label}</label> <label className="form-label mb-1">{label}</label>
<div <div
className="multi-select-dropdown-header" className="multi-select-dropdown-header"
onClick={() => setIsOpen((prev) => !prev)} onClick={() => setIsOpen((prev) => !prev)}
> >
<span <span
className={ className={
selectedValues.length > 0 selectedValues.length > 0
? 'placeholder-style-selected' ? "placeholder-style-selected"
: 'placeholder-style' : "placeholder-style"
} }
> >
<div className="selected-badges-container"> <div className="selected-badges-container">
{selectedValues.length > 0 ? ( {selectedValues.length > 0 ? (
selectedValues.map((val) => { selectedValues.map((val) => {
const found = options.find((opt) => opt[valueKey] === val); const found = options.find((opt) => opt[valueKey] === val);
return ( return (
<span key={val} className="badge badge-selected-item mx-1"> <span
{found ? found[labelKey] : ''} key={val}
</span> className="badge badge-selected-item mx-1 mb-1"
); >
}) {found ? found[labelKey] : ""}
) : ( </span>
<span className="placeholder-text">{placeholder}</span> );
)} })
</div> ) : (
</span> <span className="placeholder-text">{placeholder}</span>
<i className="bx bx-chevron-down"></i> )}
</div> </div>
</span>
<i className="bx bx-chevron-down"></i>
</div>
{isOpen && ( {isOpen && (
<div className="multi-select-dropdown-options"> <div className="multi-select-dropdown-options">
@ -94,7 +96,9 @@ const SelectMultiple = ({
return ( return (
<div <div
key={valueVal} key={valueVal}
className={`multi-select-dropdown-option ${isChecked ? 'selected' : ''}`} className={`multi-select-dropdown-option ${
isChecked ? "selected" : ""
}`}
> >
<input <input
type="checkbox" type="checkbox"
@ -105,14 +109,16 @@ const SelectMultiple = ({
<label className="text-secondary">{labelVal}</label> <label className="text-secondary">{labelVal}</label>
</div> </div>
); );
} )} })}
{!IsLoading && filteredOptions.length === 0 && ( {!IsLoading && filteredOptions.length === 0 && (
<div className='multi-select-dropdown-Not-found'> <div className="multi-select-dropdown-Not-found">
<label className="text-muted">Not Found {`'${searchText}'`}</label> <label className="text-muted">
Not Found {`'${searchText}'`}
</label>
</div> </div>
)} )}
{IsLoading && filteredOptions.length === 0 && ( {IsLoading && filteredOptions.length === 0 && (
<div className='multi-select-dropdown-Not-found'> <div className="multi-select-dropdown-Not-found">
<label className="text-muted">Loading...</label> <label className="text-muted">Loading...</label>
</div> </div>
)} )}

View File

@ -37,47 +37,46 @@ export const useDirectory = (isActive) => {
}; };
}; };
export const useBuckets = () => { export const useBuckets = () => {
const [buckets, setBuckets] = useState([]); const [buckets, setBuckets] = useState([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(""); const [error, setError] = useState("");
const fetchBuckets = async () => { const fetchBuckets = async () => {
const cacheBuckets = getCachedData("buckets"); setLoading(true);
if (!cacheBuckets) { try {
setLoading( true ); const resp = await DirectoryRepository.GetBucktes();
try { setBuckets(resp.data);
const resp = await DirectoryRepository.GetBucktes(); cacheData("buckets", resp.data);
setBuckets(resp.data); setLoading(false);
cacheData( "buckets", resp.data ); } catch (error) {
setLoading(false); const msg =
} catch (error) { error?.response?.data?.message ||
const msg = error?.message ||
error?.response?.data?.message || error?.message || "Something went wrong"; "Something went wrong";
setError(msg); setError( msg );
} setLoading(false);
} else {
setBuckets(cacheBuckets);
} }
}; };
useEffect(() => { useEffect(() => {
fetchBuckets(); const cacheBuckets = getCachedData("buckets");
if (!cacheBuckets) {
fetchBuckets();
} else {
setBuckets(cacheBuckets);
}
}, []); }, []);
return { buckets, loading, error }; return { buckets, loading, error, refetch: fetchBuckets };
}; };
export const useContactProfile = (id) => export const useContactProfile = (id) => {
{ const [conatProfile, setContactProfile] = useState(null);
const [ conatProfile, setContactProfile ] = useState( null ); const [loading, setLoading] = useState(false);
const [ loading, setLoading ] = useState( false ); const [Error, setError] = useState("");
const [ Error, setError ] = useState( "" );
const fetchContactProfile = async () => {
const fetchContactProfile = async () => {
const cached = getCachedData("Contact Profile"); const cached = getCachedData("Contact Profile");
if (!cached || cached.contactId !== id) { if (!cached || cached.contactId !== id) {
@ -88,7 +87,9 @@ const fetchContactProfile = async () => {
cacheData("Contact Profile", { data: resp.data, contactId: id }); cacheData("Contact Profile", { data: resp.data, contactId: id });
} catch (err) { } catch (err) {
const msg = const msg =
err?.response?.data?.message || err?.message || "Something went wrong"; err?.response?.data?.message ||
err?.message ||
"Something went wrong";
setError(msg); setError(msg);
} finally { } finally {
setLoading(false); setLoading(false);
@ -99,35 +100,33 @@ const fetchContactProfile = async () => {
}; };
useEffect(() => { useEffect(() => {
if ( id ) if (id) {
{
fetchContactProfile(id); fetchContactProfile(id);
} }
}, [id]); }, [id]);
return { conatProfile, loading, Error }; return { conatProfile, loading, Error };
} };
export const useContactNotes = (id, IsActive) => {
const [contactNotes, setContactNotes] = useState([]);
const [loading, setLoading] = useState(false);
const [Error, setError] = useState("");
export const useContactNotes = (id,IsActive) => const fetchContactNotes = async () => {
{
const [ contactNotes, setContactNotes ] = useState( [] );
const [ loading, setLoading ] = useState( false );
const [ Error, setError ] = useState( "" );
const fetchContactNotes = async () => {
const cached = getCachedData("Contact Notes"); const cached = getCachedData("Contact Notes");
if (!cached || cached.contactId !== id) { if (!cached || cached.contactId !== id) {
setLoading(true); setLoading(true);
try { try {
const resp = await DirectoryRepository.GetNote(id,IsActive); const resp = await DirectoryRepository.GetNote(id, IsActive);
setContactNotes(resp.data); setContactNotes(resp.data);
cacheData("Contact Notes", { data: resp.data, contactId: id }); cacheData("Contact Notes", { data: resp.data, contactId: id });
} catch (err) { } catch (err) {
const msg = const msg =
err?.response?.data?.message || err?.message || "Something went wrong"; err?.response?.data?.message ||
err?.message ||
"Something went wrong";
setError(msg); setError(msg);
} finally { } finally {
setLoading(false); setLoading(false);
@ -138,11 +137,43 @@ const fetchContactNotes = async () => {
}; };
useEffect(() => { useEffect(() => {
if ( id ) if (id) {
{
fetchContactNotes(id); fetchContactNotes(id);
} }
}, [id]); }, [id]);
return { contactNotes, loading, Error }; return { contactNotes, loading, Error };
} };
export const useOrganization = () => {
const [organizationList, setOrganizationList] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const fetchOrg = async () => {
const cacheOrg = getCachedData("organizations");
if (cacheOrg?.length != 0) {
setLoading(true);
try {
const resp = await DirectoryRepository.GetOrganizations();
cacheData("organizations", resp.data);
setOrganizationList(resp.data);
setLoading(false);
} catch (error) {
const msg =
error?.response?.data?.message ||
error?.message ||
"Something went wrong";
setError(msg);
}
} else {
setOrganizationList(cacheOrg);
}
};
useEffect(() => {
fetchOrg();
}, []);
return { organizationList, loading, error };
};

View File

@ -0,0 +1,35 @@
import { useState, useMemo } from 'react';
export const useSortableData = (items, config = null) => {
const [sortConfig, setSortConfig] = useState(config);
const sortedItems = useMemo(() => {
let sortableItems = [...items];
if (sortConfig !== null) {
sortableItems.sort((a, b) => {
const aValue = sortConfig.key(a).toLowerCase();
const bValue = sortConfig.key(b).toLowerCase();
if (aValue < bValue) return sortConfig.direction === 'asc' ? -1 : 1;
if (aValue > bValue) return sortConfig.direction === 'asc' ? 1 : -1;
return 0;
});
}
return sortableItems;
}, [items, sortConfig]);
const requestSort = (keyFn) => {
let direction = 'asc';
if (
sortConfig &&
sortConfig.key.toString() === keyFn.toString() &&
sortConfig.direction === 'asc'
) {
direction = 'desc';
}
setSortConfig({ key: keyFn, direction });
};
return { items: sortedItems, requestSort, sortConfig };
};

View File

@ -4,25 +4,30 @@ import Header from "../components/Layout/Header";
import Sidebar from "../components/Layout/Sidebar"; import Sidebar from "../components/Layout/Sidebar";
import Footer from "../components/Layout/Footer"; import Footer from "../components/Layout/Footer";
import FloatingMenu from "../components/common/FloatingMenu";
import { FabProvider } from "../Context/FabContext";
const HomeLayout = () => { const HomeLayout = () => {
useEffect(() => { useEffect(() => {
Main(); Main();
}, []); }, []);
return ( return (
<div className="layout-wrapper layout-content-navbar" > <FabProvider>
<div className="layout-container" > <div className="layout-wrapper layout-content-navbar">
<Sidebar /> <div className="layout-container">
<div className="layout-page "> <Sidebar />
<Header /> <div className="layout-page ">
<div className="content-wrapper" > <Header />
<Outlet /> <div className="content-wrapper">
<Footer /> <Outlet />
<Footer />
</div>
</div> </div>
<FloatingMenu />
<div className="layout-overlay layout-menu-toggle"></div>
</div> </div>
<div className="layout-overlay layout-menu-toggle"></div>
</div> </div>
</div> </FabProvider>
); );
}; };

View File

@ -18,6 +18,7 @@ import ConfirmModal from "../../components/common/ConfirmModal";
import DirectoryListTableHeader from "./DirectoryListTableHeader"; import DirectoryListTableHeader from "./DirectoryListTableHeader";
import DirectoryPageHeader from "./DirectoryPageHeader"; import DirectoryPageHeader from "./DirectoryPageHeader";
import ManageBucket from "../../components/Directory/ManageBucket"; import ManageBucket from "../../components/Directory/ManageBucket";
import {useFab} from "../../Context/FabContext";
const Directory = () => const Directory = () =>
{ {
@ -36,11 +37,12 @@ const Directory = () =>
const [openBucketModal,setOpenBucketModal] = useState(false) const [openBucketModal,setOpenBucketModal] = useState(false)
const [tempSelectedBucketIds, setTempSelectedBucketIds] = useState([]); const [tempSelectedBucketIds, setTempSelectedBucketIds] = useState([]);
const [tempSelectedCategoryIds, setTempSelectedCategoryIds] = useState([]); const [ tempSelectedCategoryIds, setTempSelectedCategoryIds ] = useState( [] );
const {setActions} = useFab()
const { contacts, loading } = useDirectory(IsActive); const { contacts, loading , refetch} = useDirectory(IsActive);
const { contactCategory, loading: contactCategoryLoading } = const { contactCategory, loading: contactCategoryLoading } =
useContactCategory(); useContactCategory();
const {buckets} = useBuckets(); const {buckets} = useBuckets();
const submitContact = async (data) => { const submitContact = async (data) => {
@ -56,15 +58,17 @@ const Directory = () =>
); );
showToast("Contact updated successfully", "success"); showToast("Contact updated successfully", "success");
setIsOpenModal(false); setIsOpenModal(false);
setSelectedContact(null); setSelectedContact( null );
} else { } else {
response = await DirectoryRepository.CreateContact(data); response = await DirectoryRepository.CreateContact(data);
updatedContacts = [...contacts_cache, response.data]; updatedContacts = [...contacts_cache, response.data];
showToast("Contact created successfully", "success"); showToast("Contact created successfully", "success");
setIsOpenModal(false); setIsOpenModal(false);
} }
cacheData("Contacts", {data:updatedContacts,isActive:IsActive});
setContactList(updatedContacts); // cacheData("Contacts", {data:updatedContacts,isActive:IsActive});
// setContactList(updatedContacts);
refetch()
} catch (error) { } catch (error) {
const msg = const msg =
error.response?.data?.message || error.response?.data?.message ||
@ -80,7 +84,7 @@ const Directory = () =>
const contacts_cache = getCachedData("contacts")?.data || []; const contacts_cache = getCachedData("contacts")?.data || [];
const response = await DirectoryRepository.DeleteContact(deleteContact); const response = await DirectoryRepository.DeleteContact(deleteContact);
const updatedContacts = ContactList.filter((c) => c.id !== deleteContact); const updatedContacts = ContactList.filter( ( c ) => c.id !== deleteContact );
setContactList(updatedContacts); setContactList(updatedContacts);
cacheData("Contacts", {data:updatedContacts,isActive:IsActive}); cacheData("Contacts", {data:updatedContacts,isActive:IsActive});
showToast("Contact deleted successfully", "success"); showToast("Contact deleted successfully", "success");
@ -155,7 +159,7 @@ const Directory = () =>
return matchesSearch && matchesCategory && matchesBucket; return matchesSearch && matchesCategory && matchesBucket;
}).sort((a, b) => a.name.localeCompare(b.name)); }).sort((a, b) => a.name.localeCompare(b.name));
}, [ContactList, searchText, selectedCategoryIds, selectedBucketIds]); }, [ContactList, searchText, selectedCategoryIds, selectedBucketIds,selectedContact]);
const applyFilter = () => { const applyFilter = () => {
setSelectedBucketIds(tempSelectedBucketIds); setSelectedBucketIds(tempSelectedBucketIds);
@ -192,8 +196,30 @@ const Directory = () =>
} }
}; };
useEffect(() => {
setActions([
{
label: "New Contact",
icon: "bx bx-plus-circle",
color: "warning",
onClick: () => setIsOpenModal(true),
},
{
label: "Manage Bucket",
icon: "fa-solid fa-bucket fs-5 ",
color: "primary",
onClick: () => setOpenBucketModal(true),
},
]);
return () => setActions([]); // Clean up
}, []);
return ( return (
<div className="container-xxl flex-grow-1 container-p-y"> <div className="container-xxl flex-grow-1 container-p-y">
<Breadcrumb <Breadcrumb
data={[ data={[
{ label: "Home", link: "/dashboard" }, { label: "Home", link: "/dashboard" },
@ -257,13 +283,13 @@ const Directory = () =>
<GlobalModel <GlobalModel
isOpen={openBucketModal} isOpen={openBucketModal}
closeModal={() =>setOpenBucketModal(false)} closeModal={() =>setOpenBucketModal(false)}
size="md" size="lg"
> >
<ManageBucket buckets={buckets} /> <ManageBucket buckets={buckets} />
</GlobalModel> </GlobalModel>
)} )}
<div className="card p-2"> <div className="card p-2 card-minHeight">
<DirectoryPageHeader <DirectoryPageHeader
searchText={searchText} searchText={searchText}
setSearchText={setSearchText} setSearchText={setSearchText}
@ -333,6 +359,8 @@ const Directory = () =>
))} ))}
</div> </div>
)} )}
<div>
</div>
{!loading && currentItems < ITEMS_PER_PAGE && ( {!loading && currentItems < ITEMS_PER_PAGE && (
<nav aria-label="Page "> <nav aria-label="Page ">

View File

@ -11,43 +11,22 @@ const DirectoryListTableHeader = ( {children, IsActive} ) =>
<tr> <tr>
<th colSpan={2}> <th colSpan={2}>
<div className="d-flex align-items-center gap-1"> <div className="d-flex align-items-center gap-1">
<IconButton
size={12}
iconClass="bx bx-user"
color="secondary"
onClick={() => alert("User icon clicked")}
/>
<span>Name</span> <span>Name</span>
</div> </div>
</th> </th>
<th className="px-2 text-start"> <th className="px-2 text-start">
<div className="d-flex text-center align-items-center gap-1 justify-content-start"> <div className="d-flex text-center align-items-center gap-1 justify-content-start">
<IconButton
size={12}
iconClass="bx bx-envelope"
color="primary"
/>
<span>Email</span> <span>Email</span>
</div> </div>
</th> </th>
<th className="mx-2"> <th className="mx-2">
<div className="d-flex align-items-center m-0 p-0 gap-1"> <div className="d-flex align-items-center m-0 p-0 gap-1">
<IconButton
size={12}
iconClass="bx bx-phone"
color="warning"
onClick={() => alert("User icon clicked")}
/>
<span>Phone</span> <span>Phone</span>
</div> </div>
</th> </th>
<th className="mx-2"> <th className="mx-2">
<div className="d-flex align-items-center gap-1"> <div className="d-flex align-items-center gap-1">
<IconButton
size={12}
iconClass="bx bxs-grid-alt"
color="info"
/>
<span>Organization</span> <span>Organization</span>
</div> </div>
</th> </th>

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { useEffect, useState } from "react";
const DirectoryPageHeader = ({ const DirectoryPageHeader = ({
searchText, searchText,
@ -16,14 +16,19 @@ const DirectoryPageHeader = ({
applyFilter, applyFilter,
loading, loading,
IsActive, IsActive,
setIsOpenModal, setIsOpenModal,
setOpenBucketModal setOpenBucketModal,
}) => { }) => {
const [filtered, setFiltered] = useState();
useEffect(() => {
setFiltered(
tempSelectedBucketIds?.length + tempSelectedCategoryIds?.length
);
}, [tempSelectedBucketIds, tempSelectedCategoryIds]);
return ( return (
<> <>
<div className="row"> <div className="row"></div>
</div>
<div className="row mx-0 px-0 align-items-center"> <div className="row mx-0 px-0 align-items-center">
<div className="col-12 col-md-4 mb-2 px-1 d-flex align-items-center "> <div className="col-12 col-md-4 mb-2 px-1 d-flex align-items-center ">
<input <input
@ -66,22 +71,31 @@ const DirectoryPageHeader = ({
<div className="dropdown" style={{ width: "fit-content" }}> <div className="dropdown" style={{ width: "fit-content" }}>
<div className="dropdown" style={{ width: "fit-content" }}> <div className="dropdown" style={{ width: "fit-content" }}>
<a <a
className="dropdown-toggle hide-arrow cursor-pointer d-flex align-items-center" className="dropdown-toggle hide-arrow cursor-pointer d-flex align-items-center position-relative"
data-bs-toggle="dropdown" data-bs-toggle="dropdown"
aria-expanded="false" aria-expanded="false"
> >
<i className="fa-solid fa-filter ms-1 fs-5"></i> <i className={`fa-solid fa-filter ms-1 fs-5 ${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> </a>
<ul className="dropdown-menu p-3" style={{ width: "320px" }}> <ul className="dropdown-menu p-3" style={{ width: "320px" }}>
<div> <div>
<p className="small-text fw-semibold text-muted m-0"> <p className="text-small text-muted m-0">
Filter by Filter by
</p> </p>
{/* Bucket Filter */} {/* Bucket Filter */}
<div className="mt-1"> <div className="mt-1">
<p className="small-text mb-1 fw-semibold">Buckets</p> <p className="text-small mb-1 ">Buckets</p>
<div className="d-flex flex-wrap"> <div className="d-flex flex-wrap">
{filteredBuckets.map(({ id, name }) => ( {filteredBuckets.map(({ id, name }) => (
<div <div
@ -97,7 +111,7 @@ const DirectoryPageHeader = ({
onChange={() => handleTempBucketChange(id)} onChange={() => handleTempBucketChange(id)}
/> />
<label <label
className="form-check-label text-nowrap small-text " className="form-check-label text-nowrap text-small "
htmlFor={`bucket-${id}`} htmlFor={`bucket-${id}`}
> >
{name} {name}
@ -109,7 +123,7 @@ const DirectoryPageHeader = ({
<hr className="m-0" /> <hr className="m-0" />
{/* Category Filter */} {/* Category Filter */}
<div className="mt-1"> <div className="mt-1">
<p className="small-text mb-1 fw-semibold">Categories</p> <p className="text-small mb-1 ">Categories</p>
<div className="d-flex flex-wrap"> <div className="d-flex flex-wrap">
{filteredCategories.map(({ id, name }) => ( {filteredCategories.map(({ id, name }) => (
<div <div
@ -125,7 +139,7 @@ const DirectoryPageHeader = ({
onChange={() => handleTempCategoryChange(id)} onChange={() => handleTempCategoryChange(id)}
/> />
<label <label
className="form-check-label text-nowrap small-text" className="form-check-label text-nowrap text-small"
htmlFor={`cat-${id}`} htmlFor={`cat-${id}`}
> >
{name} {name}
@ -154,49 +168,69 @@ const DirectoryPageHeader = ({
</div> </div>
</div> </div>
</div> </div>
<div className="col-12 col-md-8 mb-2 px-1 text-md-end text-end"> <div className="col-12 col-md-8 mb-2 px-1 d-flex justify-content-end gap-2 align-items-center text-end">
<label className="switch switch-primary">
<input
type="checkbox"
className="switch-input"
onChange={() => setIsActive(!IsActive)}
value={IsActive}
disabled={loading}
/>
<span className="switch-toggle-slider">
<span className="switch-on">
{/* <i class="icon-base bx bx-check"></i> */}
</span>
<span className="switch-off">
{/* <i class="icon-base bx bx-x"></i> */}
</span>
</span>
<span className="switch-label small-text">
Show Inactive Contacts
</span>
</label>
<button <button
type="button" type="button"
className="btn btn-xs btn-primary" className="btn btn-sm btn-primary"
onClick={() => setIsOpenModal(true)} onClick={() => setIsOpenModal(true)}
> >
<i className="bx bx-plus-circle me-2"></i> <i className="bx bx-plus-circle me-2"></i>
New Contact New Contact
</button> </button>
</div>
<div className={`dropdown `}>
<button
type="button"
className="btn btn-xs btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0 m-0"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i
className="bx bx-dots-vertical-rounded bx-md text-muted "
data-bs-toggle="tooltip"
data-bs-offset="0,8"
data-bs-placement="top"
data-bs-custom-class="tooltip-dark"
title="More Action"
></i>
</button>
<ul className="dropdown-menu dropdown-menu-end w-auto">
<li>
<a className="dropdown-item px-2 ">
<label className="switch switch-primary align-self-start mb-2">
<input
type="checkbox"
className="switch-input"
onChange={() => setIsActive(!IsActive)}
value={IsActive}
disabled={loading}
/>
<span className="switch-toggle-slider">
<span className="switch-on"></span>
<span className="switch-off"></span>
</span>
<span className=" list-inline-item ">
Show Inactive Contacts
</span>
</label>
</a>
</li>
<li>
<a
className="dropdown-item cursor-pointer px-2 "
onClick={() => setOpenBucketModal(true)}
>
<i className="fa-solid fa-bucket fs-5 me-4"></i>
<span className="align-left">Manage Buckets</span>
</a>
</li>
</ul>
</div> </div>
<div className="col-12 d-flex justify-content-end">
<button
type="button"
className="btn btn-xs btn-primary"
onClick={()=>setOpenBucketModal(true)}
>
<i className="bx bx-plus-circle me-2"></i>
Manage Buckets
</button>
</div> </div>
</div>
</> </>
); );
}; };
export default DirectoryPageHeader; export default DirectoryPageHeader;

View File

@ -1,6 +1,8 @@
import {api} from "../utils/axiosClient"; import {api} from "../utils/axiosClient";
export const DirectoryRepository = { export const DirectoryRepository = {
GetOrganizations:()=>api.get('/api/directory/organization'),
GetContacts: (isActive) => api.get( `/api/directory?active=${isActive}` ), GetContacts: (isActive) => api.get( `/api/directory?active=${isActive}` ),
CreateContact: ( data ) => api.post( '/api/directory', data ), CreateContact: ( data ) => api.post( '/api/directory', data ),
UpdateContact: ( id, data ) => api.put( `/api/directory/${ id }`, data ), UpdateContact: ( id, data ) => api.put( `/api/directory/${ id }`, data ),
@ -8,7 +10,8 @@ export const DirectoryRepository = {
GetBucktes: () => api.get( `/api/directory/buckets` ), GetBucktes: () => api.get( `/api/directory/buckets` ),
CreateBuckets: ( data ) => api.post( `/api/Directory/bucket`, data ), CreateBuckets: ( data ) => api.post( `/api/Directory/bucket`, data ),
UpdateBuckets: (id,data) => api.put( `/api/Directory/bucket/${id}`,data ), UpdateBuckets: ( id, data ) => api.put( `/api/Directory/bucket/${ id }`, data ),
DeleteBucket:(id)=>api.delete(`/api/directory/bucket/${id}`),
GetContactProfile: ( id ) => api.get( `/api/directory/profile/${ id }` ), GetContactProfile: ( id ) => api.get( `/api/directory/profile/${ id }` ),