reafctor contact list with filter

This commit is contained in:
pramod mahajan 2025-09-09 20:04:48 +05:30
parent b91487712d
commit 025b13ea64
11 changed files with 452 additions and 113 deletions

View File

@ -4,7 +4,8 @@ import { getBucketNameById } from "./DirectoryUtils";
import { useBuckets } from "../../hooks/useDirectory";
import { getPhoneIcon } from "./DirectoryUtils";
import { useDir } from "../../Context/DireContext";
const CardViewDirectory = ({
import { useDirectoryContext } from "../../pages/Directory/DirectoryPage";
const CardViewContact = ({
IsActive,
contact,
setSelectedContact,
@ -14,7 +15,7 @@ const CardViewDirectory = ({
IsDeleted,
restore,
}) => {
const { buckets } = useBuckets();
const { data } = useDirectoryContext();
const { dirActions, setDirActions } = useDir();
return (
@ -25,8 +26,9 @@ const CardViewDirectory = ({
<div className="card-body px-1 py-2 pb-0">
<div className="d-flex justify-content-between">
<div
className={`d-flex align-items-center ${IsActive && "cursor-pointer"
}`}
className={`d-flex align-items-center ${
IsActive && "cursor-pointer"
}`}
onClick={() => {
if (IsActive) {
setIsOpenModalNote(true);
@ -89,10 +91,11 @@ const CardViewDirectory = ({
)}
{!IsActive && (
<i
className={`bx ${dirActions.action && dirActions.id === contact.id
className={`bx ${
dirActions.action && dirActions.id === contact.id
? "bx-loader-alt bx-spin"
: "bx-recycle"
} me-1 text-primary cursor-pointer`}
} me-1 text-primary cursor-pointer`}
title="Restore"
onClick={() => {
setDirActions({ action: false, id: contact.id });
@ -104,17 +107,15 @@ const CardViewDirectory = ({
</div>
<ul className="list-inline m-0 ps-4 d-flex align-items-start">
{/* <li className="list-inline-item me-1 small">
<i className="fa-solid fa-briefcase me-2"></i>
</li> */}
<li className="list-inline-item text-break small px-1 ms-5">
{contact.organization}
{contact?.organization}
</li>
</ul>
</div>
<div
className={`card-footer text-start px-9 py-1 ${IsActive && "cursor-pointer"
}`}
className={`card-footer text-start px-9 py-1 ${
IsActive && "cursor-pointer"
}`}
onClick={() => {
if (IsActive) {
setIsOpenModalNote(true);
@ -123,10 +124,10 @@ const CardViewDirectory = ({
}}
>
<hr className="my-0" />
{contact.designation && (
{contact?.designation && (
<ul className="list-unstyled my-1 d-flex align-items-start ms-2">
<li className="me-2">
<i class="fa-solid fa-id-badge ms-1"></i>
<i className="fa-solid fa-id-badge ms-1"></i>
</li>
<li className="flex-grow-1 text-break small">
{contact.designation}
@ -188,7 +189,7 @@ const CardViewDirectory = ({
>
<i className="bx bx-pin bx-xs"></i>
<span className="small-text">
{getBucketNameById(buckets, bucketId)}
{getBucketNameById(data, bucketId)}
</span>
</span>
</li>
@ -199,4 +200,4 @@ const CardViewDirectory = ({
);
};
export default CardViewDirectory;
export default CardViewContact;

View File

@ -1,47 +1,49 @@
import { z } from "zod";
export const ContactSchema = z
.object({
name: z.string().min(1, "Name is required"),
organization: z.string().min(1, "Organization name is required"),
contactCategoryId: z.string().nullable().optional(),
address: z.string().optional(),
description: z.string().min(1, { message: "Description is required" }),
designation: z.string().min(1, {message:"Designation is requried"}),
projectIds: z.array(z.string()).nullable().optional(), // min(1, "Project is required")
contactEmails: z
.array(
z.object({
label: z.string(),
emailAddress: z.string().email("Invalid email").or(z.literal("")),
})
)
.optional()
.default([]),
import { array, z } from "zod";
export const ContactSchema = z.object({
name: z.string().min(1, "Name is required"),
organization: z.string().min(1, "Organization name is required"),
contactCategoryId: z.string().nullable().optional(),
address: z.string().optional(),
description: z.string().min(1, { message: "Description is required" }),
designation: z.string().min(1, { message: "Designation is requried" }),
projectIds: z.array(z.string()).nullable().optional(), // min(1, "Project is required")
contactEmails: z
.array(
z.object({
label: z.string(),
emailAddress: z.string().email("Invalid email").or(z.literal("")),
})
)
.optional()
.default([]),
contactPhones: z
.array(
z.object({
label: z.string(),
phoneNumber: z
.string()
.min(6, "Invalid Number")
.max(13, "Invalid Number")
.regex(/^[\d\s+()-]+$/, "Invalid phone number format").or(z.literal("")),
})
)
.optional()
.default([]),
contactPhones: z
.array(
z.object({
label: z.string(),
phoneNumber: z
.string()
.min(6, "Invalid Number")
.max(13, "Invalid Number")
.regex(/^[\d\s+()-]+$/, "Invalid phone number format")
.or(z.literal("")),
})
)
.optional()
.default([]),
tags: z
.array(
z.object({
id: z.string().nullable(),
name: z.string(),
})
)
.min(1, { message: "At least one tag is required" }),
bucketIds: z.array(z.string()).nonempty({ message: "At least one bucket is required" })
})
tags: z
.array(
z.object({
id: z.string().nullable(),
name: z.string(),
})
)
.min(1, { message: "At least one tag is required" }),
bucketIds: z
.array(z.string())
.nonempty({ message: "At least one bucket is required" }),
});
// .refine((data) => {
// const hasValidEmail = (data.contactEmails || []).some(
@ -57,24 +59,33 @@ bucketIds: z.array(z.string()).nonempty({ message: "At least one bucket is requi
// path: ["contactPhone"],
// });
// Buckets
export const bucketScheam = z.object( {
name: z.string().min( 1, {message: "Name is required"} ),
description:z.string().min(1,{message:"Description is required"})
})
export const bucketScheam = z.object({
name: z.string().min(1, { message: "Name is required" }),
description: z.string().min(1, { message: "Description is required" }),
});
export const defaultContactValue = {
name: "",
organization: "",
contactCategoryId: null,
address: "",
description: "",
designation: "",
projectIds: [],
contactEmails: [],
contactPhones: [],
tags: [],
bucketIds: [],
}
name: "",
organization: "",
contactCategoryId: null,
address: "",
description: "",
designation: "",
projectIds: [],
contactEmails: [],
contactPhones: [],
tags: [],
bucketIds: [],
};
export const contactsFilter = z.object({
buckets: z.array(z.string()).optional(),
contactCategories: z.array(z.string()).optional(),
});
export const defaultContactFilter = {
buckets: [],
contactCategories: [],
};

View File

@ -21,6 +21,6 @@ export const getPhoneIcon = (type) => {
export const getBucketNameById = (buckets, id) => {
const bucket = buckets.find(b => b.id === id);
const bucket = buckets?.find(b => b.id === id);
return bucket ? bucket.name : 'Unknown';
};

View File

@ -0,0 +1,124 @@
import React from "react";
import Avatar from "../common/Avatar";
import Pagination from "../common/Pagination";
const ListViewContact = ({ data, Pagination }) => {
const contactList = [
{
key: "name",
label: "Name",
getValue: (e) => (
<div className="d-flex align-items-center">
<Avatar
size="xs"
classAvatar="m-0"
firstName={(e?.name || "").trim().split(" ")[0] || ""}
lastName={(e?.name || "").trim().split(" ")[1] || ""}
/>
<span
className="text-truncate d-inline-block "
style={{ maxWidth: "150px" }}
>
{e?.name || "N/A"}
</span>
</div>
),
align: "text-start",
},
{
key: "email",
label: "Email",
getValue: (e) => (
<span
className="text-truncate d-inline-block"
style={{ maxWidth: "200px" }}
>
{e?.contactEmails?.[0]?.emailAddress || "N/A"}
</span>
),
align: "text-start",
},
{
key: "organization",
label: "Organization",
getValue: (e) => (
<span
className="text-truncate d-inline-block"
style={{ maxWidth: "200px" }}
>
{e?.organization || "N/A"}
</span>
),
align: "text-start",
},
{
key: "category",
label: "Category",
getValue: (e) => (
<span
className="text-truncate d-inline-block"
style={{ maxWidth: "150px" }}
>
{e?.contactCategory?.name || "N/A"}
</span>
),
align: "text-start",
},
];
return (
<div className="card ">
<div className="card-datatable table-responsive" id="horizontal-example">
<div className="dataTables_wrapper no-footer ">
<table className="table border-top dataTable text-nowrap">
<thead>
<tr className="shadow-sm">
{contactList?.map((col) => (
<th key={col.key} className={col.align}>
{col.label}
</th>
))}
<th className="sticky-action-column bg-white text-center">
Action
</th>
</tr>
</thead>
<tbody>
{Array.isArray(data) && data.length > 0 ? (
data.map((row, i) => (
<tr key={i}>
{contactList.map((col) => (
<td key={col.key} className={col.align}>
{col.getValue(row)}
</td>
))}
<td className="text-center">
<div className="d-flex justify-content-center gap-2">
<i className="bx bx-show text-primary cursor-pointer"></i>
<i className="bx bx-edit text-secondary cursor-pointer"></i>
<i className="bx bx-trash text-danger cursor-pointer"></i>
</div>
</td>
</tr>
))
) : (
<tr>
<td colSpan={contactList.length + 1} className="text-center">
No contacts found
</td>
</tr>
)}
</tbody>
</table>
{Pagination && (
<div className="d-flex justify-content-start p-3">{Pagination}</div>
)}
</div>
</div>
</div>
);
};
export default ListViewContact;

View File

@ -239,9 +239,10 @@ export const useDirectoryNotes = (
),
});
};
const cleanFilter = (filter) => {
const cleaned = { ...filter };
["bucketIds", "categories"].forEach((key) => {
["bucketIds", "contactCategories"].forEach((key) => {
if (Array.isArray(cleaned[key]) && cleaned[key].length === 0) {
delete cleaned[key];
}
@ -255,7 +256,7 @@ export const useContactList = (
pageSize,
pageNumber,
filter,
searchString
searchString=""
) => {
return useQuery({
queryKey: [
@ -264,12 +265,12 @@ export const useContactList = (
projectId,
pageSize,
pageNumber,
JSON.stringify(filter),
filter,
searchString,
],
queryFn: async () => {
const cleanedFilter = cleanFilter(filter);
const resp = await DirectoryRepository.GetContacts(
const resp = await DirectoryRepository.GetContact(
isActive,
projectId,
pageSize,
@ -277,11 +278,21 @@ export const useContactList = (
cleanedFilter,
searchString
);
return resp.data; // returning only the data
return resp.data;
},
});
};
export const useContactFilter = ()=>{
return useQuery({
queryKey:["contactFilter"],
queryFn:async()=> {
const resp = await DirectoryRepository.GetContactFilter();
return resp.data;
}
})
}
// ---------------------------Mutation------------------------------------------------------------------

View File

@ -0,0 +1,76 @@
import { zodResolver } from "@hookform/resolvers/zod";
import React from "react";
import { FormProvider, useForm } from "react-hook-form";
import {
contactsFilter,
defaultContactFilter,
} from "../../components/Directory/DirectorySchema";
import { useContactFilter } from "../../hooks/useDirectory";
import { ExpenseFilterSkeleton } from "../../components/Expenses/ExpenseSkeleton";
import SelectMultiple from "../../components/common/SelectMultiple";
const ContactFilterPanel = ({ onApply, clearFilter }) => {
const { data, isError, isLoading, error, isFetched, isFetching } =
useContactFilter();
const methods = useForm({
resolver: zodResolver(contactsFilter),
defaultValues: defaultContactFilter,
});
const closePanel = () => {
document.querySelector(".offcanvas.show .btn-close")?.click();
};
const { register, handleSubmit, reset, watch } = methods;
const onSubmit = (formData) => {
onApply(formData);
closePanel();
};
const handleClose = () => {
reset(defaultContactFilter);
closePanel();
};
if (isLoading || isFetching) return <ExpenseFilterSkeleton />;
if (isError && isFetched)
return <div>Something went wrong Here- {error.message} </div>;
return (
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start">
<div className="row g-2">
<SelectMultiple
name="buckets"
label="Buckets :"
options={data.buckets}
labelKey="name"
valueKey="id"
/>
<SelectMultiple
name="contactCategories"
label="Contact Category :"
options={data.contactCategories}
labelKey={(item) => item.name}
valueKey="id"
/>
</div>
<div className="d-flex justify-content-end py-3 gap-2">
<button
type="button"
className="btn btn-secondary btn-xs"
onClick={handleClose}
>
Clear
</button>
<button type="submit" className="btn btn-primary btn-xs">
Apply
</button>
</div>
</form>
</FormProvider>
);
};
export default ContactFilterPanel;

View File

@ -1,14 +1,38 @@
import React, { useEffect } from "react";
import React, { useEffect, useState } from "react";
import { useFab } from "../../Context/FabContext";
import { useContactList } from "../../hooks/useDirectory";
import { useDirectoryContext } from "./DirectoryPage";
import CardViewContact from "../../components/Directory/CardViewContact";
import { ITEMS_PER_PAGE } from "../../utils/constants";
import ContactFilterPanel from "./ContactFilterPanel";
import { defaultContactFilter } from "../../components/Directory/DirectorySchema";
import { useDebounce } from "../../utils/appUtils";
import Pagination from "../../components/common/Pagination";
import ListViewContact from "../../components/Directory/ListViewContact";
const ContactsPage = () => {
const {data,isError,isLoading,error} = useContactList()
const ContactsPage = ({ searchText }) => {
const [currentPage, setCurrentPage] = useState(1);
const [filters, setFilter] = useState(defaultContactFilter);
const debouncedSearch = useDebounce(searchText, 500);
const { showActive, gridView } = useDirectoryContext();
const { data, isError, isLoading, error } = useContactList(
showActive,
null,
ITEMS_PER_PAGE,
currentPage,
filters,
debouncedSearch
);
const { setOffcanvasContent, setShowTrigger } = useFab();
const clearFilter = () => {
setFilter(defaultContactFilter);
};
useEffect(() => {
setShowTrigger(true);
setOffcanvasContent("Contacts Filters", <div>hlleo</div>);
setOffcanvasContent(
"Contacts Filters",
<ContactFilterPanel onApply={setFilter} clearFilter={clearFilter} />
);
return () => {
setShowTrigger(false);
@ -16,9 +40,56 @@ const ContactsPage = () => {
};
}, []);
if(isError) return <div>{error.message}</div>
if(isLoading) return <div>Loading...</div>
return <div className="container"></div>;
const paginate = (page) => {
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
setCurrentPage(page);
}
};
if (isError) return <div>{error.message}</div>;
if (isLoading) return <div>Loading...</div>;
return (
<div className="row mt-5">
{gridView ? (
<>
{data?.data?.map((contact) => (
<div
key={contact.id}
className="col-12 col-sm-6 col-md-4 col-lg-4 mb-4"
>
<CardViewContact IsActive={showActive} contact={contact} />
</div>
))}
{data?.data?.length > 0 && (
<div className="col-12 d-flex justify-content-start mt-3">
<Pagination
currentPage={currentPage}
totalPages={data.totalPages}
onPageChange={paginate}
/>
</div>
)}
</>
) : (
<div className="col-12">
<ListViewContact
data={data.data}
Pagination={
<Pagination
currentPage={currentPage}
totalPages={data.totalPages}
onPageChange={paginate}
/>
}
/>
</div>
)}
</div>
)
};
export default ContactsPage;

View File

@ -9,7 +9,7 @@ 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/CardViewDirectory";
import CardViewDirectory from "../../components/Directory/CardViewContact";
import { useContactCategory } from "../../hooks/masterHook/useMaster";
import usePagination from "../../hooks/usePagination";
import { ITEMS_PER_PAGE } from "../../utils/constants";

View File

@ -13,6 +13,7 @@ import GlobalModel from "../../components/common/GlobalModel";
import ManageBucket from "../../components/Directory/ManageBucket";
import ManageBucket1 from "../../components/Directory/ManageBucket1";
import ManageContact from "../../components/Directory/ManageContact";
import BucketList from "../../components/Directory/BucketList";
const NotesPage = lazy(() => import("./NotesPage"));
const ContactsPage = lazy(() => import("./ContactsPage"));
@ -31,10 +32,14 @@ export const useDirectoryContext = () => {
return context;
};
export default function DirectoryPage({ IsPage = true }) {
const [searchContact, setsearchContact] = useState("");
const [searchNote, setSearchNote] = useState("");
const [activeTab, setActiveTab] = useState("notes");
const { setActions } = useFab();
const [gridView, setGridView] = useState(false);
const [isOpenBucket, setOpenBucket] = useState(false);
const [isManageContact,setManageContact] = useState(false)
const [isManageContact, setManageContact] = useState(false);
const [showActive, setShowActive] = useState(true);
const { data, isLoading, isError, error } = useBucketList();
@ -59,7 +64,7 @@ export default function DirectoryPage({ IsPage = true }) {
label: "New Contact",
icon: "bx bx-plus-circle",
color: "warning",
onClick: ()=>setManageContact(true),
onClick: () => setManageContact(true),
});
}
@ -68,11 +73,17 @@ export default function DirectoryPage({ IsPage = true }) {
return () => setActions([]);
}, [IsPage, data]);
const contextValues = {
showActive,
gridView,
data,
};
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>{error.message}</div>;
return (
<>
<DirectoryContext.Provider>
<DirectoryContext.Provider value={contextValues}>
<div className="container-fluid">
<Breadcrumb
data={[
@ -136,22 +147,52 @@ export default function DirectoryPage({ IsPage = true }) {
type="search"
className="form-control form-control-sm"
placeholder="Search notes..."
value={searchNote}
onChange={(e) => setSearchNote(e.target.value)}
/>
)}
{activeTab === "contacts" && (
<div className="d-flex gap-2 align-items-center">
<input
type="text"
className="form-control form-control-sm"
placeholder="Search contacts..."
/>
<button className="btn btn-xs btn-outline-secondary">
<i className="bx bx-list-ul"></i>
</button>
<button className="btn btn-xs btn-outline-secondary">
<i className="bx bx-grid-alt"></i>
</button>
<div className="d-flex align-items-center">
<div className="d-flex gap-2 align-items-center">
<input
type="text"
className="form-control form-control-sm"
placeholder="Search contacts..."
value={searchContact}
onChange={(e) => setsearchContact(e.target.value)}
/>
<button
className={`btn btn-xs ${
!gridView ? "btn-primary" : "btn-outline-secondary"
}`}
onClick={() => setGridView(false)}
>
<i className="bx bx-list-ul"></i>
</button>
<button
className={`btn btn-xs ${
gridView ? "btn-primary" : "btn-outline-secondary"
}`}
onClick={() => setGridView(true)}
>
<i className="bx bx-grid-alt"></i>
</button>
</div>
<div className="form-check form-switch text-start m-0 ms-5">
<input
type="checkbox"
className="form-check-input"
role="switch"
id="inactiveEmployeesCheckbox"
checked={showActive}
onChange={(e) => setShowActive(e.target.checked)}
/>
<label className="form-check-label ms-0">
{showActive ? "In Active" : "Active"}
</label>
</div>
</div>
)}
</div>
@ -170,7 +211,9 @@ export default function DirectoryPage({ IsPage = true }) {
}
>
{activeTab === "notes" && <NotesPage />}
{activeTab === "contacts" && <ContactsPage />}
{activeTab === "contacts" && (
<ContactsPage searchText={searchContact} />
)}
</Suspense>
</div>
@ -185,9 +228,13 @@ export default function DirectoryPage({ IsPage = true }) {
)}
{isManageContact && (
<GlobalModel size="lg" isOpen={isManageContact} closeModal={()=>setManageContact(false)}>
<ManageContact closeModal={()=>setManageContact(false)}/>
</GlobalModel>
<GlobalModel
size="lg"
isOpen={isManageContact}
closeModal={() => setManageContact(false)}
>
<ManageContact closeModal={() => setManageContact(false)} />
</GlobalModel>
)}
</div>
</DirectoryContext.Provider>

View File

@ -3,7 +3,6 @@ import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useSelector } from "react-redux";
// Components
import ExpenseList from "../../components/Expenses/ExpenseList";
import ViewExpense from "../../components/Expenses/ViewExpense";
import Breadcrumb from "../../components/common/Breadcrumb";

View File

@ -17,14 +17,13 @@ export const DirectoryRepository = {
GetContact: (isActive, projectId, pageSize, pageNumber, filter, searchString) => {
const payloadJsonString = JSON.stringify(filter);
return api.get(
`/api/directory/notes?active=${isActive}` +
`/api/Directory/list?active=${isActive}` +
(projectId ? `&projectId=${projectId}` : "") +
`&pageSize=${pageSize}&pageNumber=${pageNumber}&filter=${encodeURIComponent(payloadJsonString)}&searchString=${encodeURIComponent(searchString)}`
);
},
GetContactFilter:()=>api.get("directory/contact/filter"),
GetContactFilter:()=>api.get("/api/directory/contact/filter"),
CreateContact: (data) => api.post("/api/directory", data),
UpdateContact: (id, data) => api.put(`/api/directory/${id}`, data),
DeleteContact: (id, isActive) =>