Merge pull request 'Directory - Contacts Exported to Excel does not format properly' (#397) from Kartik_Bug#1064 into Refactor_Directory

Reviewed-on: #397
Merged
This commit is contained in:
pramod.mahajan 2025-09-15 11:47:56 +00:00
commit 45bd5a7f66
4 changed files with 145 additions and 57 deletions

View File

@ -11,8 +11,21 @@ import Pagination from "../../components/common/Pagination";
import ListViewContact from "../../components/Directory/ListViewContact"; import ListViewContact from "../../components/Directory/ListViewContact";
import { CardViewContactSkeleton, ListViewContactSkeleton } from "../../components/Directory/DirectoryPageSkeleton"; import { CardViewContactSkeleton, ListViewContactSkeleton } from "../../components/Directory/DirectoryPageSkeleton";
// Utility function to format contacts for CSV export
const formatExportData = (contacts) => {
return contacts.map(contact => ({
Email: contact.contactEmails?.map(e => e.emailAddress).join(", ") || "",
Phone: contact.contactPhones?.map(p => p.phoneNumber).join(", ") || "",
Created: contact.createdAt ? new Date(contact.createdAt).toLocaleString() : "",
Location: contact.address || "",
Organization: contact.organization || "",
Category: contact.contactCategory?.name || "",
Tags: contact.tags?.map(t => t.name).join(", ") || "",
Buckets: contact.bucketIds?.join(", ") || "",
}));
};
const ContactsPage = ({projectId ,searchText }) => { const ContactsPage = ({ projectId, searchText, onExport }) => {
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [filters, setFilter] = useState(defaultContactFilter); const [filters, setFilter] = useState(defaultContactFilter);
const debouncedSearch = useDebounce(searchText, 500); const debouncedSearch = useDebounce(searchText, 500);
@ -26,9 +39,9 @@ const ContactsPage = ({projectId ,searchText }) => {
debouncedSearch debouncedSearch
); );
const { setOffcanvasContent, setShowTrigger } = useFab(); const { setOffcanvasContent, setShowTrigger } = useFab();
const clearFilter = () => {
setFilter(defaultContactFilter); const clearFilter = () => setFilter(defaultContactFilter);
};
useEffect(() => { useEffect(() => {
setShowTrigger(true); setShowTrigger(true);
setOffcanvasContent( setOffcanvasContent(
@ -42,6 +55,13 @@ const ContactsPage = ({projectId ,searchText }) => {
}; };
}, []); }, []);
// 🔹 Format contacts for export
useEffect(() => {
if (data?.data && onExport) {
onExport(formatExportData(data.data));
}
}, [data?.data]);
const paginate = (page) => { const paginate = (page) => {
if (page >= 1 && page <= (data?.totalPages ?? 1)) { if (page >= 1 && page <= (data?.totalPages ?? 1)) {
setCurrentPage(page); setCurrentPage(page);
@ -49,16 +69,14 @@ const ContactsPage = ({projectId ,searchText }) => {
}; };
if (isError) return <div>{error.message}</div>; if (isError) return <div>{error.message}</div>;
if (isLoading) return <ListViewContactSkeleton/>; if (isLoading) return gridView ? <CardViewContactSkeleton /> : <ListViewContactSkeleton />;
return ( return (
<div className="row mt-5"> <div className="row mt-5">
{gridView ? ( {gridView ? (
<> <>
{data?.data?.map((contact) => ( {data?.data?.map((contact) => (
<div <div key={contact.id} className="col-12 col-sm-6 col-md-4 col-lg-4 mb-4">
key={contact.id}
className="col-12 col-sm-6 col-md-4 col-lg-4 mb-4"
>
<CardViewContact IsActive={showActive} contact={contact} /> <CardViewContact IsActive={showActive} contact={contact} />
</div> </div>
))} ))}
@ -88,10 +106,7 @@ const ContactsPage = ({projectId ,searchText }) => {
</div> </div>
)} )}
</div> </div>
);
)
}; };
export default ContactsPage; export default ContactsPage;

View File

@ -15,6 +15,7 @@ import BucketList from "../../components/Directory/BucketList";
import { MainDirectoryPageSkeleton } from "../../components/Directory/DirectoryPageSkeleton"; import { MainDirectoryPageSkeleton } from "../../components/Directory/DirectoryPageSkeleton";
import ContactProfile from "../../components/Directory/ContactProfile"; import ContactProfile from "../../components/Directory/ContactProfile";
import GlobalModel from "../../components/common/GlobalModel"; import GlobalModel from "../../components/common/GlobalModel";
import { exportToCSV } from "../../utils/exportUtils";
const NotesPage = lazy(() => import("./NotesPage")); const NotesPage = lazy(() => import("./NotesPage"));
const ContactsPage = lazy(() => import("./ContactsPage")); const ContactsPage = lazy(() => import("./ContactsPage"));
@ -49,6 +50,18 @@ export default function DirectoryPage({ IsPage = true, projectId=null }) {
Open: false, Open: false,
}); });
const [notesData, setNotesData] = useState([]);
const [ContactData, setContactData] = useState([]);
const handleExport = (type) => {
if (activeTab === "notes" && type === "csv") {
exportToCSV(notesData, "notes.csv");
}
if (activeTab === "contacts" && type === "csv") {
exportToCSV(ContactData, "contact.csv");
}
};
const { data, isLoading, isError, error } = useBucketList(); const { data, isLoading, isError, error } = useBucketList();
const handleTabClick = (tab, e) => { const handleTabClick = (tab, e) => {
@ -139,7 +152,11 @@ export default function DirectoryPage({ IsPage = true, projectId=null }) {
</button> </button>
<ul className="dropdown-menu"> <ul className="dropdown-menu">
<li> <li>
<a className="dropdown-item" href="#"> {/* <a className="dropdown-item" href="#"> */}
<a
className="dropdown-item cursor-pointer"
onClick={() => handleExport("csv")}
>
<i className="bx bx-file me-1"></i> CSV <i className="bx bx-file me-1"></i> CSV
</a> </a>
</li> </li>
@ -214,10 +231,10 @@ export default function DirectoryPage({ IsPage = true, projectId=null }) {
<div> <div>
<Suspense fallback={<MainDirectoryPageSkeleton />}> <Suspense fallback={<MainDirectoryPageSkeleton />}>
{activeTab === "notes" && ( {activeTab === "notes" && (
<NotesPage projectId={projectId} searchText={searchNote} /> <NotesPage projectId={projectId} searchText={searchNote} onExport={setNotesData} />
)} )}
{activeTab === "contacts" && ( {activeTab === "contacts" && (
<ContactsPage projectId={projectId} searchText={searchContact} /> <ContactsPage projectId={projectId} searchText={searchContact} onExport={setContactData} />
)} )}
</Suspense> </Suspense>
</div> </div>

View File

@ -1,6 +1,7 @@
// NotesPage.jsx
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useFab } from "../../Context/FabContext"; import { useFab } from "../../Context/FabContext";
import { useNoteFilter, useNotes } from "../../hooks/useDirectory"; import { useNotes } from "../../hooks/useDirectory";
import NoteFilterPanel from "./NoteFilterPanel"; import NoteFilterPanel from "./NoteFilterPanel";
import { defaultNotesFilter } from "../../components/Directory/DirectorySchema"; import { defaultNotesFilter } from "../../components/Directory/DirectorySchema";
import { ITEMS_PER_PAGE } from "../../utils/constants"; import { ITEMS_PER_PAGE } from "../../utils/constants";
@ -9,10 +10,11 @@ import NoteCardDirectoryEditable from "../../components/Directory/NoteCardDirect
import Pagination from "../../components/common/Pagination"; import Pagination from "../../components/common/Pagination";
import { NoteCardSkeleton } from "../../components/Directory/DirectoryPageSkeleton"; import { NoteCardSkeleton } from "../../components/Directory/DirectoryPageSkeleton";
const NotesPage = ({ projectId, searchText }) => { const NotesPage = ({ projectId, searchText, onExport }) => {
const [filters, setFilter] = useState(defaultNotesFilter); const [filters, setFilter] = useState(defaultNotesFilter);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const debouncedSearch = useDebounce(searchText, 500); const debouncedSearch = useDebounce(searchText, 500);
const { data, isLoading, isError, error } = useNotes( const { data, isLoading, isError, error } = useNotes(
projectId, projectId,
ITEMS_PER_PAGE, ITEMS_PER_PAGE,
@ -20,11 +22,13 @@ const NotesPage = ({ projectId, searchText }) => {
filters, filters,
debouncedSearch debouncedSearch
); );
const { setOffcanvasContent, setShowTrigger } = useFab(); const { setOffcanvasContent, setShowTrigger } = useFab();
const clearFilter = () => { const clearFilter = () => {
setFilter(defaultContactFilter); setFilter(defaultNotesFilter);
}; };
useEffect(() => { useEffect(() => {
setShowTrigger(true); setShowTrigger(true);
setOffcanvasContent( setOffcanvasContent(
@ -38,6 +42,30 @@ const NotesPage = ({ projectId, searchText }) => {
}; };
}, []); }, []);
// 🔹 Format data for export
const formatExportData = (notes) => {
return notes.map((n) => ({
ContactName: n.contactName || "",
Note: n.note ? n.note.replace(/<[^>]+>/g, "") : "", // strip HTML tags
Organization: n.organizationName || "",
CreatedBy: n.createdBy
? `${n.createdBy.firstName || ""} ${n.createdBy.lastName || ""}`.trim()
: "",
CreatedAt: n.createdAt ? new Date(n.createdAt).toLocaleString() : "",
UpdatedBy: n.updatedBy
? `${n.updatedBy.firstName || ""} ${n.updatedBy.lastName || ""}`.trim()
: "",
UpdatedAt: n.updatedAt ? new Date(n.updatedAt).toLocaleString() : "",
}));
};
// 🔹 Pass formatted notes to parent for export
useEffect(() => {
if (data?.data && onExport) {
onExport(formatExportData(data.data));
}
}, [data?.data]);
const paginate = (page) => { const paginate = (page) => {
if (page >= 1 && page <= (data?.totalPages ?? 1)) { if (page >= 1 && page <= (data?.totalPages ?? 1)) {
setCurrentPage(page); setCurrentPage(page);
@ -46,6 +74,7 @@ const NotesPage = ({ projectId, searchText }) => {
if (isError) return <div>{error.message}</div>; if (isError) return <div>{error.message}</div>;
if (isLoading) return <NoteCardSkeleton />; if (isLoading) return <NoteCardSkeleton />;
return ( return (
<div className="d-flex flex-column text-start mt-5"> <div className="d-flex flex-column text-start mt-5">
{data?.data?.length > 0 ? ( {data?.data?.length > 0 ? (

27
src/utils/exportUtils.js Normal file
View File

@ -0,0 +1,27 @@
// utils/exportUtils.js
export const exportToCSV = (data, filename = "export.csv") => {
if (!data || data.length === 0) return;
const headers = Object.keys(data[0]);
const csvRows = [];
// Add headers
csvRows.push(headers.join(","));
// Add values
data.forEach(row => {
const values = headers.map(header => `"${row[header] ?? ""}"`);
csvRows.push(values.join(","));
});
// Create CSV Blob
const csvContent = csvRows.join("\n");
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
// Create download link
const link = document.createElement("a");
const url = URL.createObjectURL(blob);
link.setAttribute("href", url);
link.setAttribute("download", filename);
link.click();
};