diff --git a/src/components/Directory/NoteCardDirectoryEditable.jsx b/src/components/Directory/NoteCardDirectoryEditable.jsx new file mode 100644 index 00000000..e6991c2e --- /dev/null +++ b/src/components/Directory/NoteCardDirectoryEditable.jsx @@ -0,0 +1,256 @@ +import React, { useState } from "react"; +import ReactQuill from "react-quill"; +import moment from "moment"; +import Avatar from "../common/Avatar"; +import { DirectoryRepository } from "../../repositories/DirectoryRepository"; +import showToast from "../../services/toastService"; +import { cacheData, getCachedData } from "../../slices/apiDataManager"; +import ConfirmModal from "../common/ConfirmModal"; // Make sure path is correct +import "../common/TextEditor/Editor.css"; +import ProfileContactDirectory from "./ProfileContactDirectory"; +import GlobalModel from "../common/GlobalModel"; + +const NoteCardDirectoryEditable = ({ + noteItem, + contactId, + onNoteUpdate, + onNoteDelete, +}) => { + const [editing, setEditing] = useState(false); + const [editorValue, setEditorValue] = useState(noteItem.note); + const [isLoading, setIsLoading] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + const [isRestoring, setIsRestoring] = useState(false); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [open_contact, setOpen_contact] = useState(null); + const [isOpenModalNote, setIsOpenModalNote] = useState(false); + + const handleUpdateNote = async () => { + try { + setIsLoading(true); + const payload = { + id: noteItem.id, + note: editorValue, + contactId, + }; + const response = await DirectoryRepository.UpdateNote(noteItem.id, payload); + + 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); + } + }; + + 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 contactProfile = (contactId) => { + DirectoryRepository.GetContactProfile(contactId).then((res) => { + setOpen_contact(res?.data); + setIsOpenModalNote(true); + }); + }; + + const handleRestore = async () => { + try { + setIsRestoring(true); + await DirectoryRepository.DeleteNote(noteItem.id, true); + onNoteDelete?.(noteItem.id); + showToast("Note restored successfully", "success"); + } catch (error) { + showToast("Failed to restore note", "error"); + } finally { + setIsRestoring(false); + } + }; + + return ( + <> + + {isOpenModalNote && ( + { + setOpen_contact(null); + setIsOpenModalNote(false); + }} + size="xl" + > + {open_contact && ( + setIsOpenModalNote(false)} + /> + )} + + )} +
+ {/* Header */} +
+
+ + +
+
contactProfile(noteItem.contactId)}> + + {noteItem?.contactName} + ( {noteItem?.organizationName}) + + + +
+
+ + +
+
+ + by {noteItem?.createdBy?.firstName} {noteItem?.createdBy?.lastName} +   + on {moment + .utc(noteItem?.createdAt) + .add(5, "hours") + .add(30, "minutes") + .format("MMMM DD, YYYY [at] hh:mm A")} + + + +
+
+
+ + {/* Action Icons */} +
+ {noteItem.isActive ? ( + <> + setEditing(true)} + title="Edit" + > + {!isDeleting ? ( + setIsDeleteModalOpen(true)} + title="Delete" + > + ) : ( +
+ )} + + ) : isRestoring ? ( + + ) : ( + + )} +
+
+ +
+ + {/* Editor or Content */} + {editing ? ( + <> + +
+ setEditing(false)} + > + Cancel + + + {isLoading ? "Saving..." : "Submit"} + +
+ + ) : ( +
+ )} +
+ + {/* Delete Confirm Modal */} + {isDeleteModalOpen && ( +
+ setIsDeleteModalOpen(false)} + loading={isDeleting} + paramData={noteItem} + /> +
+ )} + + ); +}; + +export default NoteCardDirectoryEditable; diff --git a/src/components/Directory/NotesCardViewDirectory.jsx b/src/components/Directory/NotesCardViewDirectory.jsx new file mode 100644 index 00000000..be207ffd --- /dev/null +++ b/src/components/Directory/NotesCardViewDirectory.jsx @@ -0,0 +1,173 @@ +import React, { useEffect, useState, useMemo } from "react"; +import { DirectoryRepository } from "../../repositories/DirectoryRepository"; +import NoteCardDirectoryEditable from "./NoteCardDirectoryEditable"; + +const NotesCardViewDirectory = ({ notes, setNotesForFilter, searchText, filterAppliedNotes }) => { + const [allNotes, setAllNotes] = useState([]); + const [filteredNotes, setFilteredNotes] = useState([]); + const [loading, setLoading] = useState(true); + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [selectedCreators, setSelectedCreators] = useState([]); + const [selectedOrgs, setSelectedOrgs] = useState([]); + const pageSize = 20; + + useEffect(() => { + fetchNotes(); + }, []); + + const fetchNotes = async () => { + setLoading(true); + try { + const response = await DirectoryRepository.GetNotes(1000, 1); + const fetchedNotes = response.data?.data || []; + setAllNotes(fetchedNotes); + setNotesForFilter(fetchedNotes) + + const creatorsSet = new Set(); + const orgsSet = new Set(); + + fetchedNotes.forEach((note) => { + const creator = `${note.createdBy?.firstName || ""} ${note.createdBy?.lastName || ""}`.trim(); + if (creator) creatorsSet.add(creator); + + const org = note.organizationName; + if (org) orgsSet.add(org); + }); + + } catch (error) { + console.error("Failed to fetch notes:", error); + } finally { + setLoading(false); + } + }; + + + + const applyCombinedFilter = () => { + const lowerSearch = searchText?.toLowerCase() || ""; + + const filtered = allNotes.filter((noteItem) => { + const creator = `${noteItem.createdBy?.firstName || ""} ${noteItem.createdBy?.lastName || ""}`.trim(); + const org = noteItem.organizationName; + + const matchesCreator = selectedCreators.length === 0 || selectedCreators.includes(creator); + const matchesOrg = selectedOrgs.length === 0 || selectedOrgs.includes(org); + + const plainNote = noteItem?.note?.replace(/<[^>]+>/g, "").toLowerCase(); + + const stringValues = []; + const extractStrings = (obj) => { + for (const key in obj) { + const value = obj[key]; + if (typeof value === "string") { + stringValues.push(value.toLowerCase()); + } else if (typeof value === "object" && value !== null) { + extractStrings(value); + } + } + }; + extractStrings(noteItem); + stringValues.push(plainNote, creator.toLowerCase()); + + const matchesSearch = stringValues.some((val) => val.includes(lowerSearch)); + + return matchesCreator && matchesOrg && matchesSearch; + }); + + setFilteredNotes(filtered); + setCurrentPage(1); + setTotalPages(Math.ceil(filtered.length / pageSize)); + }; + + useEffect(() => { + applyCombinedFilter(); + }, [searchText, allNotes]); + + useEffect(() => { + setFilteredNotes(filterAppliedNotes); + }, [filterAppliedNotes]) + + const currentItems = useMemo(() => { + const startIndex = (currentPage - 1) * pageSize; + return filteredNotes.slice(startIndex, startIndex + pageSize); + }, [filteredNotes, currentPage]); + + const handlePageClick = (page) => { + if (page !== currentPage) { + setCurrentPage(page); + } + }; + + if (loading) return

Loading notes...

; + + if (!filteredNotes.length) return

No matching notes found

; + + + return ( +
+ {/* Filter Dropdown */} +
+
+ + {/* Notes List */} +
+ {currentItems.map((noteItem) => ( + { + setAllNotes((prevNotes) => + prevNotes.map((n) => (n.id === updatedNote.id ? updatedNote : n)) + ); + }} + onNoteDelete={() => fetchNotes()} + /> + ))} +
+ + {/* Pagination */} + {totalPages > 1 && ( +
+
+ + + {[...Array(totalPages)].map((_, i) => { + const page = i + 1; + return ( + + ); + })} + + +
+
+ )} +
+ ); +}; + +export default NotesCardViewDirectory; diff --git a/src/pages/Directory/Directory.jsx b/src/pages/Directory/Directory.jsx index af6535b8..ca95181f 100644 --- a/src/pages/Directory/Directory.jsx +++ b/src/pages/Directory/Directory.jsx @@ -20,6 +20,7 @@ import DirectoryPageHeader from "./DirectoryPageHeader"; import ManageBucket from "../../components/Directory/ManageBucket"; import { useFab } from "../../Context/FabContext"; import { DireProvider, useDir } from "../../Context/DireContext"; +import NotesCardViewDirectory from "../../components/Directory/NotesCardViewDirectory"; const Directory = ({ IsPage = true, prefernceContacts }) => { const [projectPrefernce, setPerfence] = useState(null); @@ -31,11 +32,17 @@ const Directory = ({ IsPage = true, prefernceContacts }) => { const [ContactList, setContactList] = useState([]); const [contactCategories, setContactCategories] = useState([]); const [searchText, setSearchText] = useState(""); - const [listView, setListView] = useState(false); + const [viewType, setViewType] = useState("notes"); const [selectedBucketIds, setSelectedBucketIds] = useState([]); const [deleteContact, setDeleteContact] = useState(null); const [IsDeleting, setDeleting] = useState(false); const [openBucketModal, setOpenBucketModal] = useState(false); + const [notes, setNotes] = useState([]); + const [filterAppliedNotes, setFilterAppliedNotes] = useState([]); + // const [selectedOrgs, setSelectedOrgs] = useState([]); + + // ✅ Changed to an array for multiple selections + const [selectedNoteNames, setSelectedNoteNames] = useState([]); const [tempSelectedBucketIds, setTempSelectedBucketIds] = useState([]); const [tempSelectedCategoryIds, setTempSelectedCategoryIds] = useState([]); @@ -71,8 +78,6 @@ const Directory = ({ IsPage = true, prefernceContacts }) => { setIsOpenModal(false); } - // cacheData("Contacts", {data:updatedContacts,isActive:IsActive}); - // setContactList(updatedContacts); refetch(IsActive, prefernceContacts); refetchBucket(); } catch (error) { @@ -249,6 +254,7 @@ const Directory = ({ IsPage = true, prefernceContacts }) => { return () => setActions([]); }, [IsPage, buckets]); + useEffect(() => { setPerfence(prefernceContacts); }, [prefernceContacts]); @@ -326,14 +332,14 @@ const Directory = ({ IsPage = true, prefernceContacts }) => { )} -
+
{ IsActive={IsActive} setOpenBucketModal={setOpenBucketModal} contactsToExport={contacts} + notesToExport={notes} + selectedNoteNames={selectedNoteNames} + setSelectedNoteNames={setSelectedNoteNames} + notesForFilter={notes} + setFilterAppliedNotes={setFilterAppliedNotes} />
-
- {/* Messages when listView is false */} - {!listView && ( -
- {loading &&

Loading...

} - {!loading && contacts?.length === 0 && ( +
+ {(viewType === "card" || viewType === "list" || viewType === "notes") && ( +
+ {!loading && (viewType === "card" || viewType === "list") && contacts?.length === 0 && (

No contact found

)} - {!loading && contacts?.length > 0 && currentItems.length === 0 && ( -

No matching contact found

- )} + {!loading && + (viewType === "card" || viewType === "list") && + contacts?.length > 0 && + currentItems.length === 0 && ( +

No matching contact found

+ )}
)} - {/* Table view (listView === true) */} - - {listView ? ( + {viewType === "list" && (
- {loading && ( - - - {" "} -

Loading...

{" "} - - - )} - - {!loading && contacts?.length === 0 && ( - - -

No contact found

- - - )} - - {!loading && - currentItems.length === 0 && - contacts?.length > 0 && ( - - -

No matching contact found

- - - )} - {!loading && currentItems.map((contact) => ( {
- ) : ( -
+ )} + + {viewType === "card" && ( +
{!loading && currentItems.map((contact) => (
{
)} + {viewType === "notes" && ( +
+ +
+ )} + {/* Pagination */} {!loading && + viewType !== "notes" && contacts?.length > 0 && currentItems.length > ITEMS_PER_PAGE && (
+
+
+ +
+
+ setSearchText(e.target.value)} style={{ width: "200px" }} /> -
- - -
-
-
+ + {/* Filter by funnel icon for Notes view */} + {viewType === "notes" && ( +
{/* Added minWidth here */} - {filtered > 0 && ( - +
+ {/* Created By */} +
+

Created By

+ {allCreators.map((name, idx) => ( +
+ handleToggleCreator(name)} + /> + +
+ ))} +
+ + {/* Divider */} + {/*
*/} + + {/* Organization */} +
+

Organization

+ {filteredOrganizations.map((org, idx) => ( +
+ handleToggleOrg(org)} + /> + +
+ ))} +
+
+ + {/* Buttons */} +
+ + +
+
+ +
+ )} + + + {(viewType === "card" || viewType === "list") && ( +
+ + + +
+ )} + + {/* Filter by funnel icon for Contacts view (retains numerical badge) */} + {viewType !== "notes" && ( +
+ -
    -
    -

    Filter by

    +
      +

      Filter by

      - {/* Bucket Filter */} -
      -

      Buckets

      +
      +
      +

      Buckets

      {filteredBuckets.map(({ id, name }) => ( -
      +
      handleTempBucketChange(id)} /> -
      ))}
      -
      - {/* Category Filter */} -
      -

      Categories

      +
      +

      Categories

      {filteredCategories.map(({ id, name }) => ( -
      +
      handleTempCategoryChange(id)} /> -
      ))}
      +
      -
      - - -
      +
      + +
    -
-
-
- + )} + +
+ +
+ {(viewType === "list" || viewType === "card") && ( + + )} - {/* Export Dropdown */} +
diff --git a/src/repositories/DirectoryRepository.jsx b/src/repositories/DirectoryRepository.jsx index 502ea80b..c99601ed 100644 --- a/src/repositories/DirectoryRepository.jsx +++ b/src/repositories/DirectoryRepository.jsx @@ -32,4 +32,7 @@ export const DirectoryRepository = { UpdateNote: (id, data) => api.put(`/api/directory/note/${id}`, data), DeleteNote: (id, isActive) => api.delete(`/api/directory/note/${id}?active=${isActive}`), + + GetNotes: (pageSize, pageNumber) => + api.get(`/api/directory/notes?pageSize=${pageSize}&pageNumber=${pageNumber}`), };