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 */}
+
+
+
+ -
+ setViewType("notes")}
+ type="button"
+ >
+ Notes
+
+
+ -
+ setViewType("card")}
+ type="button"
+ >
+ Contacts
+
+
+
+
+
+
+
+ {/* Controls: Search, Filter, View, Toggle, Export */}
+
+
+
+ {/* Search */}
setSearchText(e.target.value)}
style={{ width: "200px" }}
/>
-
- setListView(false)}
- data-bs-toggle="tooltip"
- data-bs-offset="0,8"
- data-bs-placement="top"
- data-bs-custom-class="tooltip"
- title="Card View"
- >
-
-
- setListView(true)}
- data-bs-toggle="tooltip"
- data-bs-offset="0,8"
- data-bs-placement="top"
- data-bs-custom-class="tooltip"
- title="List View"
- >
-
-
-
-
-
-
-
+ )}
- {/* Export Dropdown */}
-
+
+
+
@@ -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}`),
};