From 8f2149e0ed3ed18a1853395a3fffb98736edd1c4 Mon Sep 17 00:00:00 2001 From: Kartik Sharma Date: Fri, 10 Oct 2025 14:18:07 +0530 Subject: [PATCH] Adding Excel and PDF in Directory. --- src/pages/Directory/DirectoryPage.jsx | 84 +++-- src/pages/Directory/NotesPage.jsx | 453 ++++++++++++++++++++------ src/utils/tableExportUtils.jsx | 120 ++++++- 3 files changed, 518 insertions(+), 139 deletions(-) diff --git a/src/pages/Directory/DirectoryPage.jsx b/src/pages/Directory/DirectoryPage.jsx index 5f819e69..19374023 100644 --- a/src/pages/Directory/DirectoryPage.jsx +++ b/src/pages/Directory/DirectoryPage.jsx @@ -19,9 +19,9 @@ import BucketList from "../../components/Directory/BucketList"; import { MainDirectoryPageSkeleton } from "../../components/Directory/DirectoryPageSkeleton"; import ContactProfile from "../../components/Directory/ContactProfile"; import GlobalModel from "../../components/common/GlobalModel"; -import { exportToCSV } from "../../utils/exportUtils"; import ConfirmModal from "../../components/common/ConfirmModal"; import { useSelectedProject } from "../../slices/apiDataManager"; +import { exportToCSV, exportToExcel, exportToPDF, exportToPDF1, printTable } from "../../utils/tableExportUtils"; const NotesPage = lazy(() => import("./NotesPage")); const ContactsPage = lazy(() => import("./ContactsPage")); @@ -64,13 +64,46 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) { 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"); - } - }; + let exportData = activeTab === "notes" ? notesData : ContactData; + if (!exportData?.length) return; + + switch (type) { + case "csv": + exportToCSV(exportData, activeTab === "notes" ? "Notes" : "Contacts"); + break; + case "excel": + exportToExcel(exportData, activeTab === "notes" ? "Notes" : "Contacts"); + break; + case "pdf": + if (activeTab === "notes") { + exportToPDF1(exportData, "Notes"); + } else { + // Columns for Contacts PDF + const columns = ["Email", "Phone", "Organization", "Category", "Tags"]; + + // Sanitize and trim long text to avoid PDF overflow + const sanitizedData = exportData.map(item => ({ + Email: (item.Email || "").slice(0, 40), + Phone: (item.Phone || "").slice(0, 20), + Organization: (item.Organization || "").slice(0, 30), + Category: (item.Category || "").slice(0, 20), + Tags: (item.Tags || "").slice(0, 40), + })); + + // Export with proper spacing + exportToPDF(sanitizedData, "Contacts", columns, { + columnWidths: [200, 120, 180, 120, 200], // Adjust widths per column + fontSizeHeader: 12, + fontSizeRow: 10, + rowHeight: 25, + }); + } + break; + default: + console.warn("Unsupported export type"); + } +}; + const { data, isLoading, isError, error } = useBucketList(); @@ -213,19 +246,6 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) { - - - -
}> {activeTab === "notes" && ( diff --git a/src/pages/Directory/NotesPage.jsx b/src/pages/Directory/NotesPage.jsx index 58790562..19374023 100644 --- a/src/pages/Directory/NotesPage.jsx +++ b/src/pages/Directory/NotesPage.jsx @@ -1,117 +1,360 @@ -// NotesPage.jsx -import React, { useEffect, useState } from "react"; +import { + useState, + Suspense, + lazy, + createContext, + useContext, + useEffect, +} from "react"; +import Breadcrumb from "../../components/common/Breadcrumb"; import { useFab } from "../../Context/FabContext"; -import { 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"; -import { NoteCardSkeleton } from "../../components/Directory/DirectoryPageSkeleton"; +import { + useBucketList, + useBuckets, + useDeleteBucket, +} from "../../hooks/useDirectory"; +import ManageBucket1 from "../../components/Directory/ManageBucket1"; +import ManageContact from "../../components/Directory/ManageContact"; +import BucketList from "../../components/Directory/BucketList"; +import { MainDirectoryPageSkeleton } from "../../components/Directory/DirectoryPageSkeleton"; +import ContactProfile from "../../components/Directory/ContactProfile"; +import GlobalModel from "../../components/common/GlobalModel"; +import ConfirmModal from "../../components/common/ConfirmModal"; +import { useSelectedProject } from "../../slices/apiDataManager"; +import { exportToCSV, exportToExcel, exportToPDF, exportToPDF1, printTable } from "../../utils/tableExportUtils"; -const NotesPage = ({ projectId, searchText, onExport }) => { - const [filters, setFilter] = useState(defaultNotesFilter); - const [currentPage, setCurrentPage] = useState(1); - const debouncedSearch = useDebounce(searchText, 500); +const NotesPage = lazy(() => import("./NotesPage")); +const ContactsPage = lazy(() => import("./ContactsPage")); - const { data, isLoading, isError, error } = useNotes( - projectId, - ITEMS_PER_PAGE, - currentPage, - filters, - debouncedSearch - ); +export const DirectoryContext = createContext(); +export const useDirectoryContext = () => { + const context = useContext(DirectoryContext); - const { setOffcanvasContent, setShowTrigger } = useFab(); - - const clearFilter = () => { - setFilter(defaultNotesFilter); - }; - - useEffect(() => { - setShowTrigger(true); - setOffcanvasContent( - "Notes Filters", - + if (!context) { + return ( +
+

Your Action is out of context

+
); + } + return context; +}; +export default function DirectoryPage({ IsPage = true, projectId = null }) { + const [searchContact, setsearchContact] = useState(""); + const [searchNote, setSearchNote] = useState(""); + const [activeTab, setActiveTab] = useState("notes"); + const { setActions } = useFab(); + const [gridView, setGridView] = useState(true); + const [isOpenBucket, setOpenBucket] = useState(false); + const [isManageContact, setManageContact] = useState({ + isOpen: false, + contactId: null, + }); + const [deleteBucket, setDeleteBucket] = useState({ + isOpen: false, + bucketId: null, + }); + const [showActive, setShowActive] = useState(true); + const [contactOpen, setContactOpen] = useState({ + contact: null, + Open: false, + }); - return () => { - setShowTrigger(false); - setOffcanvasContent("", null); - }; - }, []); + const [notesData, setNotesData] = useState([]); + const [ContactData, setContactData] = useState([]); - // 🔹 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() : "", - })); - }; + const handleExport = (type) => { + let exportData = activeTab === "notes" ? notesData : ContactData; + if (!exportData?.length) return; - // 🔹 Pass formatted notes to parent for export - useEffect(() => { - if (data?.data && onExport) { - onExport(formatExportData(data.data)); - } - }, [data?.data]); + switch (type) { + case "csv": + exportToCSV(exportData, activeTab === "notes" ? "Notes" : "Contacts"); + break; + case "excel": + exportToExcel(exportData, activeTab === "notes" ? "Notes" : "Contacts"); + break; + case "pdf": + if (activeTab === "notes") { + exportToPDF1(exportData, "Notes"); + } else { + // Columns for Contacts PDF + const columns = ["Email", "Phone", "Organization", "Category", "Tags"]; - const paginate = (page) => { - if (page >= 1 && page <= (data?.totalPages ?? 1)) { - setCurrentPage(page); - } - }; + // Sanitize and trim long text to avoid PDF overflow + const sanitizedData = exportData.map(item => ({ + Email: (item.Email || "").slice(0, 40), + Phone: (item.Phone || "").slice(0, 20), + Organization: (item.Organization || "").slice(0, 30), + Category: (item.Category || "").slice(0, 20), + Tags: (item.Tags || "").slice(0, 40), + })); - if (isError) return
{error.message}
; - if (isLoading) return ; - - return ( -
- {data?.data?.length > 0 ? ( - <> - {data.data.map((noteItem) => ( - - ))} - -
- -
- - ) : ( - // Card for "No notes available" -
-

- {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 with proper spacing + exportToPDF(sanitizedData, "Contacts", columns, { + columnWidths: [200, 120, 180, 120, 200], // Adjust widths per column + fontSizeHeader: 12, + fontSizeRow: 10, + rowHeight: 25, + }); + } + break; + default: + console.warn("Unsupported export type"); + } }; -export default NotesPage; \ No newline at end of file + + const { data, isLoading, isError, error } = useBucketList(); + + const handleTabClick = (tab, e) => { + e.preventDefault(); + setActiveTab(tab); + }; + + useEffect(() => { + const actions = []; + + if (IsPage) { + actions.push({ + label: "Manage Bucket", + icon: "fa-solid fa-bucket fs-5", + color: "primary", + onClick: () => setOpenBucket(true), + }); + } + if (data?.length > 0) { + actions.push({ + label: "New Contact", + icon: "bx bx-plus-circle", + color: "warning", + onClick: () => setManageContact({ isOpen: true, contactId: null }), + }); + } + + setActions(actions); + + return () => setActions([]); + }, [IsPage, data]); + + const contextValues = { + showActive, + gridView, + data, + setManageContact, + setContactOpen, + setDeleteBucket, + }; + + const { mutate: DeleteBucket, isPending: Deleting } = useDeleteBucket(() => { + setDeleteBucket({ isOpen: false, bucketId: null }); + }); + const handleDelete = (bucketId) => { + DeleteBucket(bucketId); + }; + if (isLoading) return ; + if (isError) return
{error.message}
; + return ( + <> + +
+ {IsPage && ( + + )} +
+
+
+ + +
+
+
+ {activeTab === "notes" && ( + setSearchNote(e.target.value)} + /> + )} + + {activeTab === "contacts" && ( +
+
+ setsearchContact(e.target.value)} + /> + + + +
+
+ )} +
+
+ + +
    + {activeTab === "contacts" && ( +
  • +
    + setShowActive(e.target.checked)} + /> +
    + {showActive ? "Active Contacts" : "Inactive Contacts"} +
  • + )} +
  • + +
  • +
  • + +
  • +
  • + +
  • + + {/* Divider */} + {activeTab === "contacts" &&

  • } +
+
+
+
+
+
+
+
+ }> + {activeTab === "notes" && ( + + )} + {activeTab === "contacts" && ( + + )} + +
+ + {isOpenBucket && ( + setOpenBucket(false)} + > + setOpenBucket(false)} /> + + )} + + {contactOpen.Open && ( + setContactOpen({ contact: null, Open: false })} + > + + + )} + {isManageContact.isOpen && ( + + setManageContact({ isOpen: false, contactId: null }) + } + > + + setManageContact({ isOpen: false, contactId: null }) + } + /> + + )} + + {deleteBucket.isOpen && ( + setDeleteBucket({ isOpen: false, bucketId: null })} + loading={Deleting} + paramData={deleteBucket.bucketId} + /> + )} +
+
+ + ); +} diff --git a/src/utils/tableExportUtils.jsx b/src/utils/tableExportUtils.jsx index 57af704f..768ff5ac 100644 --- a/src/utils/tableExportUtils.jsx +++ b/src/utils/tableExportUtils.jsx @@ -40,39 +40,52 @@ export const exportToExcel = (data, fileName = "data") => { * @param {Array} data - Array of objects to export * @param {string} fileName - File name for the PDF (optional) */ -export const exportToPDF = async (data, fileName = "data", columns = null) => { +const sanitizeText = (text) => { + if (!text) return ""; + // Replace all non-ASCII characters with "?" or remove them + return text.replace(/[^\x00-\x7F]/g, "?"); +}; + +export const exportToPDF = async (data, fileName = "data", columns = null, options = {}) => { if (!data || data.length === 0) return; const pdfDoc = await PDFDocument.create(); const font = await pdfDoc.embedFont(StandardFonts.Helvetica); - // Landscape dimensions - const pageWidth = 1000; // wider for more space + // Default options + const { + columnWidths = [], // array of widths per column + fontSizeHeader = 12, + fontSizeRow = 10, + rowHeight = 25, + } = options; + + const pageWidth = 1000; const pageHeight = 600; let page = pdfDoc.addPage([pageWidth, pageHeight]); const margin = 30; let y = pageHeight - margin; const headers = columns || Object.keys(data[0]); - const rowHeight = 25; // slightly taller rows for readability - const columnSpacing = 150; // increase space between columns // Draw headers headers.forEach((header, i) => { - page.drawText(header, { x: margin + i * columnSpacing, y, font, size: 12 }); + const x = margin + (columnWidths[i] ? columnWidths.slice(0, i).reduce((a, b) => a + b, 0) : i * 150); + page.drawText(header, { x, y, font, size: fontSizeHeader }); }); y -= rowHeight; // Draw rows data.forEach(row => { headers.forEach((header, i) => { - const text = row[header] ? row[header].toString() : ''; - page.drawText(text, { x: margin + i * columnSpacing, y, font, size: 10 }); + const x = margin + (columnWidths[i] ? columnWidths.slice(0, i).reduce((a, b) => a + b, 0) : i * 150); + const text = row[header] || ''; + page.drawText(text, { x, y, font, size: fontSizeRow }); }); y -= rowHeight; if (y < margin) { - page = pdfDoc.addPage([pageWidth, pageHeight]); // landscape for new page + page = pdfDoc.addPage([pageWidth, pageHeight]); y = pageHeight - margin; } }); @@ -87,6 +100,95 @@ export const exportToPDF = async (data, fileName = "data", columns = null) => { + + +/** + * Export JSON data to PDF in a card-style format + * @param {Array} data - Array of objects to export + * @param {string} fileName - File name for the PDF (optional) + */ +export const exportToPDF1 = async (data, fileName = "data") => { + if (!data || data.length === 0) return; + + const pdfDoc = await PDFDocument.create(); + const font = await pdfDoc.embedFont(StandardFonts.Helvetica); + const boldFont = await pdfDoc.embedFont(StandardFonts.HelveticaBold); + + const pageWidth = 600; + const pageHeight = 800; + const margin = 30; + const cardSpacing = 20; + const cardPadding = 10; + let page = pdfDoc.addPage([pageWidth, pageHeight]); + let y = pageHeight - margin; + + for (const item of data) { + const title = item.ContactName || ""; + const subtitle = `by ${item.CreatedBy || ""} on ${item.CreatedAt || ""}`; + const body = item.Note || ""; + + const cardHeight = 80 + (body.length / 60) * 14; // approximate height for body text + + if (y - cardHeight < margin) { + page = pdfDoc.addPage([pageWidth, pageHeight]); + y = pageHeight - margin; + } + + // Draw card border + page.drawRectangle({ + x: margin, + y: y - cardHeight, + width: pageWidth - 2 * margin, + height: cardHeight, + borderColor: rgb(0.7, 0.7, 0.7), + borderWidth: 1, + color: rgb(1, 1, 1), + }); + + // Draw title + page.drawText(title, { + x: margin + cardPadding, + y: y - 20, + font: boldFont, + size: 12, + color: rgb(0.1, 0.1, 0.1), + }); + + // Draw subtitle + page.drawText(subtitle, { + x: margin + cardPadding, + y: y - 35, + font, + size: 10, + color: rgb(0.4, 0.4, 0.4), + }); + + // Draw body text (wrap manually) + const lines = body.match(/(.|[\r\n]){1,80}/g) || []; + lines.forEach((line, i) => { + page.drawText(line, { + x: margin + cardPadding, + y: y - 50 - i * 12, + font, + size: 10, + color: rgb(0.2, 0.2, 0.2), + }); + }); + + y -= cardHeight + cardSpacing; + } + + const pdfBytes = await pdfDoc.save(); + const blob = new Blob([pdfBytes], { type: 'application/pdf' }); + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.download = `${fileName}.pdf`; + link.click(); +}; + + + + /** * Print the HTML table by accepting the table element or a reference. * @param {HTMLElement} table - The table element (or ref) to print