From e45491776351b3521db7e5c2d17fc7a5fed5500b Mon Sep 17 00:00:00 2001 From: pramod mahajan Date: Wed, 10 Sep 2025 02:41:59 +0530 Subject: [PATCH] refactored notes list --- src/components/Directory/CardViewContact.jsx | 5 +- src/components/Directory/DirectorySchema.js | 10 + src/components/Directory/ListViewContact.jsx | 219 ++++++++++-------- .../Directory/NoteCardDirectoryEditable.jsx | 103 ++++---- .../Directory/NotesCardViewDirectory.jsx | 49 +--- src/hooks/useDirectory.js | 110 ++++++++- src/pages/Directory/DirectoryPage.jsx | 51 ++-- src/pages/Directory/NoteFilterPanel.jsx | 76 ++++++ src/pages/Directory/NotesPage.jsx | 83 +++++-- src/repositories/DirectoryRepository.jsx | 44 +++- 10 files changed, 501 insertions(+), 249 deletions(-) create mode 100644 src/pages/Directory/NoteFilterPanel.jsx diff --git a/src/components/Directory/CardViewContact.jsx b/src/components/Directory/CardViewContact.jsx index 4fa2fc6c..f015956f 100644 --- a/src/components/Directory/CardViewContact.jsx +++ b/src/components/Directory/CardViewContact.jsx @@ -16,7 +16,7 @@ const CardViewContact = ({ IsDeleted, restore, }) => { - const { data , setManageContact} = useDirectoryContext(); + const { data , setManageContact,setContactOpen} = useDirectoryContext(); const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const {mutate:ActiveInActive,isPending} = useActiveInActiveContact() @@ -61,8 +61,7 @@ const {dirActions} = useDir() }`} onClick={() => { if (IsActive) { - setIsOpenModalNote(true); - setOpen_contact(contact); + setContactOpen({contact:contact,Open:true}); } }} > diff --git a/src/components/Directory/DirectorySchema.js b/src/components/Directory/DirectorySchema.js index 6c4ebaf6..d039c2c3 100644 --- a/src/components/Directory/DirectorySchema.js +++ b/src/components/Directory/DirectorySchema.js @@ -89,3 +89,13 @@ export const defaultContactFilter = { buckets: [], contactCategories: [], }; + +export const notesFilter = z.object({ + createdBy: z.array(z.string()).optional(), + organizations: z.array(z.string()).optional(), +}); + +export const defaultNotesFilter = { + createdBy: [], + organizations: [], +}; diff --git a/src/components/Directory/ListViewContact.jsx b/src/components/Directory/ListViewContact.jsx index d41eea5e..a4db59c4 100644 --- a/src/components/Directory/ListViewContact.jsx +++ b/src/components/Directory/ListViewContact.jsx @@ -6,16 +6,20 @@ import { useActiveInActiveContact } from "../../hooks/useDirectory"; import ConfirmModal from "../common/ConfirmModal"; const ListViewContact = ({ data, Pagination }) => { - const { showActive, setManageContact } = useDirectoryContext(); - const [deleteContact,setDeleteContact] = useState({contactId:null,Open:false}) - const [activeContact,setActiveContact] = useState(null) + const { showActive, setManageContact, setContactOpen } = + useDirectoryContext(); + const [deleteContact, setDeleteContact] = useState({ + contactId: null, + Open: false, + }); + const [activeContact, setActiveContact] = useState(null); const contactList = [ { key: "name", label: "Name", getValue: (e) => ( -
+
{
), - align: "text-start", + align: "text-center", }, { key: "email", @@ -73,7 +77,9 @@ const ListViewContact = ({ data, Pagination }) => { }, ]; - const { mutate: ActiveInActive, isPending } = useActiveInActiveContact(()=>setDeleteContact({contactId:null,Open:false})); + const { mutate: ActiveInActive, isPending } = useActiveInActiveContact(() => + setDeleteContact({ contactId: null, Open: false }) + ); const handleActiveInactive = (contactId) => { ActiveInActive({ contactId: contactId, contactStatus: !showActive }); @@ -81,103 +87,126 @@ const ListViewContact = ({ data, Pagination }) => { return ( <> - {deleteContact.Open && ( -
- setDeleteContact({contactId:null,Open:false})} - loading={isPending} - paramData={deleteContact.contactId} - /> -
- )} -
-
-
- - - - {contactList?.map((col) => ( -
- {col.label} + {deleteContact.Open && ( +
+ setDeleteContact({ contactId: null, Open: false })} + loading={isPending} + paramData={deleteContact.contactId} + /> +
+ )} +
+
+
+ + + + {contactList?.map((col) => ( + + ))} + - ))} - - - - - {Array.isArray(data) && data.length > 0 ? ( - data.map((row, i) => ( - - {contactList.map((col) => ( - - ))} - + + + {Array.isArray(data) && data.length > 0 ? ( + data.map((row, i) => ( + + {contactList.map((col) => ( + + ))} + + + )) + ) : ( + + - )) - ) : ( - - - - )} - -
+ {col.label} + + Action - Action -
- {col.getValue(row)} - - {showActive ? ( -
- +
+ {col.getValue(row)} + + {showActive ? ( +
+ + setContactOpen({ contact: row, Open: true }) + } + > + + setManageContact({ + isOpen: true, + contactId: row.id, + }) + } + > + + + setDeleteContact({ + contactId: row.id, + Open: true, + }) + } + > +
+ ) : ( - setManageContact({ - isOpen: true, - contactId: row.id, - }) - } + className={`bx ${ + isPending && activeContact === row.id + ? "bx-loader-alt bx-spin" + : "bx-recycle" + } me-1 text-primary cursor-pointer`} + title="Restore" + onClick={() => { + setActiveContact(row.id); + handleActiveInactive(row.id); + }} > - - setDeleteContact({contactId:row.id,Open:true})}> - - ) : ( - { - setActiveContact(row.id) - handleActiveInactive(row.id) - }} - > - )} + )} +
+ No contacts found
- No contacts found -
- {Pagination && ( -
{Pagination}
- )} + )} + +
+ {Pagination && ( +
+ {Pagination} +
+ )} +
-
); }; diff --git a/src/components/Directory/NoteCardDirectoryEditable.jsx b/src/components/Directory/NoteCardDirectoryEditable.jsx index 5ccfefdf..a73425c7 100644 --- a/src/components/Directory/NoteCardDirectoryEditable.jsx +++ b/src/components/Directory/NoteCardDirectoryEditable.jsx @@ -9,6 +9,7 @@ import ConfirmModal from "../common/ConfirmModal"; // Make sure path is correct import "../common/TextEditor/Editor.css"; import ProfileContactDirectory from "./ProfileContactDirectory"; import GlobalModel from "../common/GlobalModel"; +import { useActiveInActiveNote, useUpdateNote } from "../../hooks/useDirectory"; const NoteCardDirectoryEditable = ({ noteItem, @@ -25,59 +26,31 @@ const NoteCardDirectoryEditable = ({ const [open_contact, setOpen_contact] = useState(null); const [isOpenModalNote, setIsOpenModalNote] = useState(false); + const { mutate: UpdateNote, isPending: isUpatingNote } = useUpdateNote(() => + setEditing(false) + ); + const { mutate: ActiveInactive, isPending: isUpdatingStatus } = + useActiveInActiveNote(() => setIsDeleteModalOpen(false)); + const handleUpdateNote = async () => { - try { - setIsLoading(true); - const payload = { - id: noteItem.id, - note: editorValue, - contactId, - }; - const response = await DirectoryRepository.UpdateNote(noteItem.id, payload); + const payload = { + id: noteItem.id, + note: editorValue, + contactId, + }; - const cachedContactProfile = getCachedData("Contact Profile"); - if (cachedContactProfile?.contactId === contactId) { - const updatedCache = { - ...cachedContactProfile, - data: { - ...cachedContactProfile.data, - notes: cachedContactProfile.data.notes.map((note) => - note.id === noteItem.id ? response.data : note - ), - }, - }; - cacheData("Contact Profile", updatedCache); - } - - onNoteUpdate?.(response.data); - setEditing(false); - showToast("Note updated successfully", "success"); - } catch (error) { - showToast("Failed to update note", "error"); - } finally { - setIsLoading(false); - } + UpdateNote({ noteId: noteItem.id, notePayload: payload }); }; - const suspendEmployee = async () => { - try { - setIsDeleting(true); - await DirectoryRepository.DeleteNote(noteItem.id, false); - onNoteDelete?.(noteItem.id); - setIsDeleteModalOpen(false); - showToast("Note deleted successfully", "success"); - } catch (error) { - showToast("Failed to delete note", "error"); - } finally { - setIsDeleting(false); - } + const ActiveInActive = (noteItem) => { + ActiveInactive({ noteId: noteItem.id, noteStatus: !noteItem.isActive }); }; const contactProfile = (contactId) => { DirectoryRepository.GetContactProfile(contactId).then((res) => { - setOpen_contact(res?.data); - setIsOpenModalNote(true); - }); + setOpen_contact(res?.data); + setIsOpenModalNote(true); + }); }; const handleRestore = async () => { @@ -95,7 +68,6 @@ const NoteCardDirectoryEditable = ({ return ( <> - {isOpenModalNote && (
-
-
contactProfile(noteItem.contactId)}> +
contactProfile(noteItem.contactId)} + > - {noteItem?.contactName} + {noteItem?.contactName} {" "} + ({noteItem?.organizationName}) - -
-
- -
+
- by {noteItem?.createdBy?.firstName} {noteItem?.createdBy?.lastName} -   - on {moment + by{" "} + + {" "} + {noteItem?.createdBy?.firstName}{" "} + {noteItem?.createdBy?.lastName}{" "} + +  {" "} + + on{" "} + {moment .utc(noteItem?.createdAt) .add(5, "hours") .add(30, "minutes") .format("DD MMMM, YYYY [at] hh:mm A")} -
@@ -180,12 +157,12 @@ const NoteCardDirectoryEditable = ({
)} - ) : isRestoring ? ( + ) : isUpdatingStatus ? ( ) : ( ActiveInActive(noteItem)} title="Restore" > )} @@ -214,7 +191,7 @@ const NoteCardDirectoryEditable = ({ className="text-primary cursor-pointer" onClick={handleUpdateNote} > - {isLoading ? "Saving..." : "Submit"} + {isUpatingNote ? "Saving..." : "Submit"}
@@ -242,9 +219,9 @@ const NoteCardDirectoryEditable = ({ type={"delete"} header={"Delete Note"} message={"Are you sure you want to delete this note?"} - onSubmit={suspendEmployee} + onSubmit={ActiveInActive} onClose={() => setIsDeleteModalOpen(false)} - loading={isDeleting} + loading={isUpdatingStatus} paramData={noteItem} />
diff --git a/src/components/Directory/NotesCardViewDirectory.jsx b/src/components/Directory/NotesCardViewDirectory.jsx index 95619f61..1de790f0 100644 --- a/src/components/Directory/NotesCardViewDirectory.jsx +++ b/src/components/Directory/NotesCardViewDirectory.jsx @@ -116,11 +116,7 @@ const NotesCardViewDirectory = ({ return (
- {/* Filter Dropdown */} -
-
- {/* Notes List */}
{currentItems.map((noteItem) => ( (n.id === updatedNote.id ? updatedNote : n)) ); }} - onNoteDelete={() => fetchNotes(projectId)} // ✅ reload with projectId + onNoteDelete={() => fetchNotes(projectId)} /> ))}
- {/* Pagination */} - {totalPages > 1 && ( -
- {/* Previous Button */} - - - {/* Page Number Buttons */} - {[...Array(totalPages)].map((_, i) => { - const page = i + 1; - return ( - - ); - })} - - {/* Next Button */} - -
- )} +
); }; diff --git a/src/hooks/useDirectory.js b/src/hooks/useDirectory.js index ccf5f68c..2e6711e0 100644 --- a/src/hooks/useDirectory.js +++ b/src/hooks/useDirectory.js @@ -307,6 +307,56 @@ export const useContactDetails = (contactId)=>{ } +const cleanNoteFilter = (filter) => { + const cleaned = { ...filter }; + ["bucketIds", "contactCategories"].forEach((key) => { + if (Array.isArray(cleaned[key]) && cleaned[key].length === 0) { + delete cleaned[key]; + } + }); + return cleaned; +}; + +export const useNotes = ( + projectId, + pageSize, + pageNumber, + filter, + searchString="" +) => { + return useQuery({ + queryKey: [ + "Notes", + projectId, + pageSize, + pageNumber, + filter, + searchString, + ], + queryFn: async () => { + const cleanedFilter = cleanNoteFilter(filter); + const resp = await DirectoryRepository.GetNotes( + projectId, + pageSize, + pageNumber, + cleanedFilter, + searchString + ); + return resp.data; + }, + }); +}; + +export const useNoteFilter = ()=>{ + return useQuery({ + queryKey:["NoteFilter"], + queryFn:async()=> { + const resp = await DirectoryRepository.GetNoteFilter(); + return resp.data; + } + }) +} + // ---------------------------Mutation------------------------------------------------------------------ @@ -438,7 +488,65 @@ export const useActiveInActiveContact =(onSuccessCallBack)=>{ onSuccess: (_, variables) => { const {contactStatus} = variables; queryClient.invalidateQueries({queryKey: ["contacts"]}) - showToast(`Contact ${contactStatus ? "Active":"In-Active"} Successfully`, "success"); + showToast(`Contact ${contactStatus ? "Restored":"Deleted"} Successfully`, "success"); + if (onSuccessCallBack) onSuccessCallBack(); + }, + onError: (error) => { + showToast( + error.response.data.message || + "Something went wrong.Please try again later.", + "error" + ); + } + }) +} + +export const useCreateNote =()=>{ + const queryClient = useQueryClient(); + return useMutation({ + mutationFn:async(notPayload)=> await DirectoryRepository.CreateNote(notPayload), + onSuccess: (_, variables) => { + queryClient.invalidateQueries({queryKey: ["Notes"]}) + showToast(`Note Created Successfully`, "success"); + if (onSuccessCallBack) onSuccessCallBack(); + }, + onError: (error) => { + showToast( + error.response.data.message || + "Something went wrong.Please try again later.", + "error" + ); + } + }) +} + +export const useUpdateNote =(onSuccessCallBack)=>{ + const queryClient = useQueryClient(); + return useMutation({ + mutationFn:async({noteId,notePayload})=> await DirectoryRepository.UpdateNote(noteId,notePayload), + onSuccess: (_, variables) => { + queryClient.invalidateQueries({queryKey: ["Notes"]}) + showToast("Note updated Successfully", "success"); + if (onSuccessCallBack) onSuccessCallBack(); + }, + onError: (error) => { + showToast( + error.response.data.message || + "Something went wrong.Please try again later.", + "error" + ); + } + }) +} + +export const useActiveInActiveNote =(onSuccessCallBack)=>{ + const queryClient = useQueryClient(); + return useMutation({ + mutationFn:async({noteId,noteStatus})=> await DirectoryRepository.DeleteNote(noteId,noteStatus), + onSuccess: (_, variables) => { + const {noteStatus} = variables; + queryClient.invalidateQueries({queryKey: ["Notes"]}) + showToast(`Note ${noteStatus ? "Restored":"Deleted"} Successfully`, "success"); if (onSuccessCallBack) onSuccessCallBack(); }, onError: (error) => { diff --git a/src/pages/Directory/DirectoryPage.jsx b/src/pages/Directory/DirectoryPage.jsx index 01aa1808..3a9dd635 100644 --- a/src/pages/Directory/DirectoryPage.jsx +++ b/src/pages/Directory/DirectoryPage.jsx @@ -14,6 +14,7 @@ import ManageBucket from "../../components/Directory/ManageBucket"; import ManageBucket1 from "../../components/Directory/ManageBucket1"; import ManageContact from "../../components/Directory/ManageContact"; import BucketList from "../../components/Directory/BucketList"; +import ProfileContactDirectory from "../../components/Directory/ProfileContactDirectory"; const NotesPage = lazy(() => import("./NotesPage")); const ContactsPage = lazy(() => import("./ContactsPage")); @@ -39,9 +40,14 @@ export default function DirectoryPage({ IsPage = true }) { const [gridView, setGridView] = useState(false); const [isOpenBucket, setOpenBucket] = useState({}); const [isManageContact, setManageContact] = useState({ - isOpen:false,contactId:null + isOpen: false, + contactId: null, }); const [showActive, setShowActive] = useState(true); + const [contactOpen, setContactOpen] = useState({ + contact: null, + Open: false, + }); const { data, isLoading, isError, error } = useBucketList(); @@ -66,7 +72,7 @@ export default function DirectoryPage({ IsPage = true }) { label: "New Contact", icon: "bx bx-plus-circle", color: "warning", - onClick: () => setManageContact({isOpen:true,contactId:null}), + onClick: () => setManageContact({ isOpen: true, contactId: null }), }); } @@ -79,7 +85,8 @@ export default function DirectoryPage({ IsPage = true }) { showActive, gridView, data, - setManageContact + setManageContact, + setContactOpen, }; if (isLoading) return
Loading...
; @@ -123,20 +130,18 @@ export default function DirectoryPage({ IsPage = true }) {
-
@@ -213,7 +218,9 @@ export default function DirectoryPage({ IsPage = true }) { } > - {activeTab === "notes" && } + {activeTab === "notes" && ( + + )} {activeTab === "contacts" && ( )} @@ -230,13 +237,29 @@ export default function DirectoryPage({ IsPage = true }) {
)} + {contactOpen.Open && ( + setContactOpen({ contact: null, Open: false })} + > + + + )} {isManageContact.isOpen && ( setManageContact({isOpen:false,contactId:null})} + closeModal={() => + setManageContact({ isOpen: false, contactId: null }) + } > - setManageContact({isOpen:false,contactId:null})} /> + + setManageContact({ isOpen: false, contactId: null }) + } + /> )} diff --git a/src/pages/Directory/NoteFilterPanel.jsx b/src/pages/Directory/NoteFilterPanel.jsx new file mode 100644 index 00000000..8c6cba31 --- /dev/null +++ b/src/pages/Directory/NoteFilterPanel.jsx @@ -0,0 +1,76 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import React from "react"; +import { FormProvider, useForm } from "react-hook-form"; +import { + defaultNotesFilter, + notesFilter, +} from "../../components/Directory/DirectorySchema"; +import { useContactFilter, useNoteFilter } from "../../hooks/useDirectory"; +import { ExpenseFilterSkeleton } from "../../components/Expenses/ExpenseSkeleton"; +import SelectMultiple from "../../components/common/SelectMultiple"; + +const NoteFilterPanel = ({ onApply, clearFilter }) => { + const { data, isError, isLoading, error, isFetched, isFetching } = + useNoteFilter(); + + const methods = useForm({ + resolver: zodResolver(notesFilter), + defaultValues: defaultNotesFilter, + }); + + const closePanel = () => { + document.querySelector(".offcanvas.show .btn-close")?.click(); + }; + + const { register, handleSubmit, reset, watch } = methods; + + const onSubmit = (formData) => { + onApply(formData); + closePanel(); + }; + + const handleClose = () => { + reset(defaultNotesFilter); + closePanel(); + }; + + if (isLoading || isFetching) return ; + if (isError && isFetched) + return
Something went wrong Here- {error.message}
; + return ( + +
+
+ + item.name} + valueKey="id" + /> +
+
+ + +
+
+
+ ); +}; + +export default NoteFilterPanel; diff --git a/src/pages/Directory/NotesPage.jsx b/src/pages/Directory/NotesPage.jsx index c282bec6..de11c2f0 100644 --- a/src/pages/Directory/NotesPage.jsx +++ b/src/pages/Directory/NotesPage.jsx @@ -1,26 +1,81 @@ -import React, { useEffect } from 'react' -import { useFab } from '../../Context/FabContext'; +import React, { useEffect, useState } from "react"; +import { useFab } from "../../Context/FabContext"; +import { useNoteFilter, useNotes } from "../../hooks/useDirectory"; +import NoteFilterPanel from "./NoteFilterPanel"; +import { defaultNotesFilter } from "../../components/Directory/DirectorySchema"; +import { ITEMS_PER_PAGE } from "../../utils/constants"; +import { useDebounce } from "../../utils/appUtils"; +import NoteCardDirectoryEditable from "../../components/Directory/NoteCardDirectoryEditable"; +import Pagination from "../../components/common/Pagination"; -const NotesPage = () => { +const NotesPage = ({ projectId, searchText }) => { + const [filters, setFilter] = useState(defaultNotesFilter); + const [currentPage, setCurrentPage] = useState(1); + const debouncedSearch = useDebounce(searchText, 500); + const { data, isLoading, isError, error } = useNotes( + projectId, + ITEMS_PER_PAGE, + currentPage, + filters, + debouncedSearch + ); + const { setOffcanvasContent, setShowTrigger } = useFab(); - const {setOffcanvasContent,setShowTrigger} = useFab() - useEffect(() => { - setShowTrigger(true); - setOffcanvasContent( - "Notes Filters", -
hlleo
- ); + const clearFilter = () => { + setFilter(defaultContactFilter); + }; + useEffect(() => { + setShowTrigger(true); + setOffcanvasContent( + "Notes Filters", + + ); return () => { setShowTrigger(false); setOffcanvasContent("", null); }; }, []); + + const paginate = (page) => { + if (page >= 1 && page <= (data?.totalPages ?? 1)) { + setCurrentPage(page); + } + }; + + if (isError) return
{error.message}
; + if (isLoading) return
Loading...
; return ( -
+
+ {data?.data?.length > 0 ? ( + <> + {data.data.map((noteItem) => ( + + ))} +
+ +
+ + ) : ( +
+ {debouncedSearch + ? `No notes found matching "${searchText}"` + : Object.keys(filters).some((k) => filters[k] && filters[k].length) + ? "No notes found for the applied filters." + : "No notes available."} +
+ )}
- ) -} + ); +}; -export default NotesPage \ No newline at end of file +export default NotesPage; diff --git a/src/repositories/DirectoryRepository.jsx b/src/repositories/DirectoryRepository.jsx index 96e9b17f..e741c6c7 100644 --- a/src/repositories/DirectoryRepository.jsx +++ b/src/repositories/DirectoryRepository.jsx @@ -3,18 +3,27 @@ import { api } from "../utils/axiosClient"; export const DirectoryRepository = { GetOrganizations: () => api.get("/api/directory/organization"), GetDesignations: () => api.get("/api/directory/designations"), - GetContact:(id)=>api.get(`/api/Directory/profile/${id}`), + GetContact: (id) => api.get(`/api/Directory/profile/${id}`), - GetContacts: (isActive, projectId, pageSize, pageNumber, filter, searchString) => { - const payloadJsonString = JSON.stringify(filter); - return api.get( - `/api/Directory/list?active=${isActive}` + - (projectId ? `&projectId=${projectId}` : "") + - `&pageSize=${pageSize}&pageNumber=${pageNumber}&filter=${encodeURIComponent(payloadJsonString)}&searchString=${encodeURIComponent(searchString)}` - ); -}, + GetContacts: ( + isActive, + projectId, + pageSize, + pageNumber, + filter, + searchString + ) => { + const payloadJsonString = JSON.stringify(filter); + return api.get( + `/api/Directory/list?active=${isActive}` + + (projectId ? `&projectId=${projectId}` : "") + + `&pageSize=${pageSize}&pageNumber=${pageNumber}&filter=${encodeURIComponent( + payloadJsonString + )}&searchString=${encodeURIComponent(searchString)}` + ); + }, - GetContactFilter:()=>api.get("/api/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) => @@ -36,10 +45,21 @@ export const DirectoryRepository = { DeleteNote: (id, isActive) => api.delete(`/api/directory/note/${id}?active=${isActive}`), - GetNotes: (pageSize, pageNumber, filter, searchString) => { + GetNotes: ( + projectId, + pageSize, + pageNumber, + filter, + searchString + ) => { const payloadJsonString = JSON.stringify(filter); return api.get( - `/api/directory/notes?pageSize=${pageSize}&pageNumber=${pageNumber}&filter=${payloadJsonString}&searchString=${searchString}` + `/api/directory/notes?` + + (projectId ? `projectId=${projectId}` : "&") + + `pageSize=${pageSize}&pageNumber=${pageNumber}&filter=${encodeURIComponent( + payloadJsonString + )}&searchString=${encodeURIComponent(searchString)}` ); }, + GetNoteFilter:()=>api.get("/api/Directory/notes/filter") };