diff --git a/src/components/Directory/NoteCardDirectoryEditable.jsx b/src/components/Directory/NoteCardDirectoryEditable.jsx new file mode 100644 index 00000000..4f5bf410 --- /dev/null +++ b/src/components/Directory/NoteCardDirectoryEditable.jsx @@ -0,0 +1,165 @@ +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 "../common/TextEditor/Editor.css"; + +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 handleUpdateNote = async () => { + try { + setIsLoading(true); + const payload = { + id: noteItem.id, + note: editorValue, + contactId, + }; + const response = await DirectoryRepository.UpdateNote(noteItem.id, payload); + + // Optional cache update + 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); + } + + // Notify parent + onNoteUpdate?.(response.data); + setEditing(false); + showToast("Note updated successfully", "success"); + } catch (error) { + showToast("Failed to update note", "error"); + } finally { + setIsLoading(false); + } + }; + + const handleDeleteOrRestore = async (shouldRestore) => { + try { + shouldRestore ? setIsRestoring(true) : setIsDeleting(true); + await DirectoryRepository.DeleteNote(noteItem.id, shouldRestore); + onNoteDelete?.(noteItem.id); + showToast(`Note ${shouldRestore ? "restored" : "deleted"} successfully`, "success"); + } catch (error) { + showToast("Failed to process note", "error"); + } finally { + setIsDeleting(false); + setIsRestoring(false); + } + }; + + return ( +
+ {/* Header */} +
+
+ +
+ + {noteItem?.createdBy?.firstName} {noteItem?.createdBy?.lastName} + + + {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 ? ( + handleDeleteOrRestore(false)} + title="Delete" + > + ) : ( +
+ )} + + ) : isRestoring ? ( + + ) : ( + handleDeleteOrRestore(true)} + title="Restore" + > + )} +
+
+
+ {/* Editor or Content */} + {editing ? ( + <> + +
+ setEditing(false)} + > + Cancel + + + {isLoading ? "Saving..." : "Submit"} + +
+ + ) : ( +
+ )} +
+ ); +}; + +export default NoteCardDirectoryEditable; \ No newline at end of file diff --git a/src/components/Directory/NotesCardViewDirectory.jsx b/src/components/Directory/NotesCardViewDirectory.jsx new file mode 100644 index 00000000..7f5e53f3 --- /dev/null +++ b/src/components/Directory/NotesCardViewDirectory.jsx @@ -0,0 +1,140 @@ +import React, { useEffect, useState, useMemo } from "react"; +import { DirectoryRepository } from "../../repositories/DirectoryRepository"; +import NoteCardDirectoryEditable from "./NoteCardDirectoryEditable"; + +const NotesCardViewDirectory = ({ notes, setNotes, searchText }) => { + const [allNotes, setAllNotes] = useState([]); + const [filteredNotes, setFilteredNotes] = useState([]); + const [loading, setLoading] = useState(true); + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const pageSize = 20; + + const fetchNotes = async () => { + setLoading(true); + try { + const response = await DirectoryRepository.GetNotes(1000, 1); // fetch all for search + const fetchedNotes = response.data?.data || []; + setAllNotes(fetchedNotes); + } catch (error) { + console.error("Failed to fetch notes:", error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchNotes(); + }, []); + + // Search + update pagination + exportable data + useEffect(() => { + const lowerSearch = searchText?.toLowerCase() || ""; + const filtered = allNotes.filter((noteItem) => { + const plainNote = noteItem?.note?.replace(/<[^>]+>/g, "").toLowerCase(); + const fullName = `${noteItem?.contact?.firstName || ""} ${noteItem?.contact?.lastName || ""}`.toLowerCase(); + const createdDate = new Date(noteItem?.createdAt).toLocaleDateString("en-IN").toLowerCase(); + + return ( + plainNote.includes(lowerSearch) || + fullName.includes(lowerSearch) || + createdDate.includes(lowerSearch) + ); + }); + + setFilteredNotes(filtered); + setNotes(filtered); // for export + setCurrentPage(1); + setTotalPages(Math.ceil(filtered.length / pageSize)); + }, [searchText, allNotes]); + + 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 ( +
+
+ {currentItems.map((noteItem) => ( + { + setAllNotes((prevNotes) => + prevNotes.map((n) => (n.id === updatedNote.id ? updatedNote : n)) + ); + }} + onNoteDelete={() => { + fetchNotes(); // refresh after delete + }} + /> + ))} +
+ + {/* 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 fe013661..e0bd2598 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,12 @@ 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 [tempSelectedBucketIds, setTempSelectedBucketIds] = useState([]); const [tempSelectedCategoryIds, setTempSelectedCategoryIds] = useState([]); @@ -332,8 +334,8 @@ const Directory = ({ IsPage = true, prefernceContacts }) => { searchText={searchText} setSearchText={setSearchText} setIsActive={setIsActive} - listView={listView} - setListView={setListView} + viewType={viewType} + setViewType={setViewType} filteredBuckets={filteredBuckets} tempSelectedBucketIds={tempSelectedBucketIds} handleTempBucketChange={handleTempBucketChange} @@ -346,56 +348,39 @@ const Directory = ({ IsPage = true, prefernceContacts }) => { IsActive={IsActive} setOpenBucketModal={setOpenBucketModal} contactsToExport={contacts} + notesToExport={notes} />
- {/* Messages when listView is false */} - {!listView && ( -
- {loading &&

Loading...

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

Loading...

} */} + + {/* Notes View */} + {/* {!loading && viewType === "notes" && notes?.length > 0 && ( +

No matching note found

+ )} */} + + {/* Contact (card/list) View */} + {!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 ? ( + {/* List View */} + {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) => ( {
- ) : ( + )} + + {/* Card View */} + {viewType === "card" && (
{!loading && currentItems.map((contact) => ( @@ -436,15 +424,25 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
)} + {/* Notes View */} + {viewType === "notes" && ( +
+ +
+ )} + {/* Pagination */} {!loading && + viewType !== "notes" && contacts?.length > 0 && currentItems.length > ITEMS_PER_PAGE && ( )}
+
); }; -export default Directory; +export default Directory; \ No newline at end of file diff --git a/src/pages/Directory/DirectoryPageHeader.jsx b/src/pages/Directory/DirectoryPageHeader.jsx index cec3147a..ae577e98 100644 --- a/src/pages/Directory/DirectoryPageHeader.jsx +++ b/src/pages/Directory/DirectoryPageHeader.jsx @@ -1,13 +1,12 @@ -import React, { useEffect, useState, useRef } from "react"; -import { ITEMS_PER_PAGE } from "../../utils/constants"; +import React, { useEffect, useState } from "react"; import { exportToCSV, exportToExcel, printTable, exportToPDF } from "../../utils/tableExportUtils"; const DirectoryPageHeader = ({ searchText, setSearchText, setIsActive, - listView, - setListView, + viewType, + setViewType, filteredBuckets, tempSelectedBucketIds, handleTempBucketChange, @@ -18,54 +17,88 @@ const DirectoryPageHeader = ({ applyFilter, loading, IsActive, - setOpenBucketModal, - contactsToExport, // This prop receives the paginated data (currentItems) + contactsToExport, + notesToExport, // ✅ Add this prop }) => { const [filtered, setFiltered] = useState(0); const handleExport = (type) => { - // Check if there's data to export - if (!contactsToExport || contactsToExport.length === 0) { - console.warn("No data to export. The current view is empty."); - // Optionally, you might want to show a user-friendly toast message here - // showToast("No data to export on the current page.", "info"); - return; + let dataToExport = []; + + if (viewType === "notes") { + if (!notesToExport || notesToExport.length === 0) { + console.warn("No notes to export."); + return; + } + + const decodeHtmlEntities = (html) => { + const textarea = document.createElement("textarea"); + textarea.innerHTML = html; + return textarea.value; + }; + + const cleanNoteText = (html) => { + if (!html) return ""; + const stripped = html.replace(/<[^>]+>/g, ""); // remove HTML tags + const decoded = decodeHtmlEntities(stripped); + return decoded.replace(/\u00A0/g, " ").replace(/\s+/g, " ").trim(); // fix non-breaking space + }; + + const cleanName = (name) => { + if (!name) return ""; + return name.replace(/\u00A0/g, " ").replace(/\s+/g, " ").trim(); // sanitize name + }; + + dataToExport = notesToExport.map(note => ({ + "Name": cleanName(`${note.createdBy?.firstName || ""} ${note.createdBy?.lastName || ""}`), + "Notes": cleanNoteText(note.note), + "Created At": note.createdAt + ? new Date(note.createdAt).toLocaleString("en-IN") + : "", + "Updated At": note.updatedAt + ? new Date(note.updatedAt).toLocaleString("en-IN") + : "", + "Updated By": cleanName( + `${note.updatedBy?.firstName || ""} ${note.updatedBy?.lastName || ""}` + ), + })); + + } else { + if (!contactsToExport || contactsToExport.length === 0) { + console.warn("No contacts to export."); + return; + } + + dataToExport = contactsToExport.map(contact => ({ + Name: contact.name || '', + Organization: contact.organization || '', + Email: contact.contactEmails?.map(email => email.emailAddress).join(', ') || '', + Phone: contact.contactPhones?.map(phone => phone.phoneNumber).join(', ') || '', + Category: contact.contactCategory?.name || '', + Tags: contact.tags?.map(tag => tag.name).join(', ') || '', + })); } - // --- Core Change: Map contactsToExport to a simplified format --- - // const simplifiedContacts = contactsToExport.map(contact => ({ - // Name: contact.name || '', - // Organization: contact.organization || '', // Added Organization - // Email: contact.contactEmails && contact.contactEmails.length > 0 ? contact.contactEmails[0].emailAddress : '', - // Phone: contact.contactPhones && contact.contactPhones.length > 0 ? contact.contactPhones[0].phoneNumber : '', // Changed 'Contact' to 'Phone' for clarity - // Category: contact.contactCategory ? contact.contactCategory.name : '', // Changed 'Role' to 'Category' - // })); + const today = new Date(); + const formattedDate = `${today.getFullYear()}${String(today.getMonth() + 1).padStart(2, '0')}${String(today.getDate()).padStart(2, '0')}`; - const simplifiedContacts = contactsToExport.map(contact => ({ - Name: contact.name || '', - Organization: contact.organization || '', - Email: contact.contactEmails && contact.contactEmails.length > 0 - ? contact.contactEmails.map(email => email.emailAddress).join(', ') - : '', - Phone: contact.contactPhones && contact.contactPhones.length > 0 - ? contact.contactPhones.map(phone => phone.phoneNumber).join(', ') - : '', - Category: contact.contactCategory ? contact.contactCategory.name : '', - })); + const filename = + viewType === "notes" + ? `Directory_Notes_${formattedDate}` + : `Directory_Contacts_${formattedDate}`; - console.log("Kaerik", simplifiedContacts) switch (type) { case "csv": - exportToCSV(simplifiedContacts, "directory_contacts"); + exportToCSV(dataToExport, filename); break; case "excel": - exportToExcel(simplifiedContacts, "directory_contacts"); + exportToExcel(dataToExport, filename); break; case "pdf": - exportToPDF(simplifiedContacts, "directory_contacts"); + exportToPDF(dataToExport, filename); break; case "print": - printTable(simplifiedContacts, "directory_contacts"); + printTable(dataToExport, filename); break; default: break; @@ -73,15 +106,43 @@ const DirectoryPageHeader = ({ }; useEffect(() => { - setFiltered( - tempSelectedBucketIds?.length + tempSelectedCategoryIds?.length - ); + setFiltered(tempSelectedBucketIds?.length + tempSelectedCategoryIds?.length); }, [tempSelectedBucketIds, tempSelectedCategoryIds]); return ( <> -
-
+ {/* Top Tabs */} +
+
+
    +
  • + +
  • +
  • + +
  • +
+
+
+
+ + {/* Controls: Search, Filter, View, Toggle, Export */} +
+
+ + {/* Search */} setSearchText(e.target.value)} style={{ width: "200px" }} /> -
- - -
-
-
- - -
-
- +
-
-
-
- + )} - {/* Export Dropdown */} -
- + +
+ +
+
+ +
+ {/* Show Inactive Toggle - only for list/card */} + {(viewType === "list" || viewType === "card") && ( + + )} + + {/* Export */} +
+
@@ -278,4 +296,7 @@ const DirectoryPageHeader = ({ ); }; -export default DirectoryPageHeader; \ No newline at end of file +export default DirectoryPageHeader; + + + 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}`), };