Adding Excel and PDF in Directory.
This commit is contained in:
parent
c1a8b87a2c
commit
8f2149e0ed
@ -19,9 +19,9 @@ 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";
|
|
||||||
import ConfirmModal from "../../components/common/ConfirmModal";
|
import ConfirmModal from "../../components/common/ConfirmModal";
|
||||||
import { useSelectedProject } from "../../slices/apiDataManager";
|
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||||
|
import { exportToCSV, exportToExcel, exportToPDF, exportToPDF1, printTable } from "../../utils/tableExportUtils";
|
||||||
|
|
||||||
const NotesPage = lazy(() => import("./NotesPage"));
|
const NotesPage = lazy(() => import("./NotesPage"));
|
||||||
const ContactsPage = lazy(() => import("./ContactsPage"));
|
const ContactsPage = lazy(() => import("./ContactsPage"));
|
||||||
@ -64,13 +64,46 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
|
|||||||
const [ContactData, setContactData] = useState([]);
|
const [ContactData, setContactData] = useState([]);
|
||||||
|
|
||||||
const handleExport = (type) => {
|
const handleExport = (type) => {
|
||||||
if (activeTab === "notes" && type === "csv") {
|
let exportData = activeTab === "notes" ? notesData : ContactData;
|
||||||
exportToCSV(notesData, "notes.csv");
|
if (!exportData?.length) return;
|
||||||
}
|
|
||||||
if (activeTab === "contacts" && type === "csv") {
|
switch (type) {
|
||||||
exportToCSV(ContactData, "contact.csv");
|
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();
|
const { data, isLoading, isError, error } = useBucketList();
|
||||||
|
|
||||||
@ -213,19 +246,6 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<ul className="dropdown-menu dropdown-menu-end shadow-sm p-2" style={{ minWidth: "220px" }}>
|
<ul className="dropdown-menu dropdown-menu-end shadow-sm p-2" style={{ minWidth: "220px" }}>
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
className="dropdown-item d-flex align-items-center gap-2"
|
|
||||||
onClick={() => handleExport("csv")}
|
|
||||||
>
|
|
||||||
<i className="bx bx-file"></i>
|
|
||||||
<span>Export to CSV</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
{/* Divider */}
|
|
||||||
{activeTab === "contacts" && <li><hr className="dropdown-divider" /></li>}
|
|
||||||
|
|
||||||
{activeTab === "contacts" && (
|
{activeTab === "contacts" && (
|
||||||
<li className="dropdown-item d-flex align-items-center">
|
<li className="dropdown-item d-flex align-items-center">
|
||||||
<div className="form-check form-switch mb-0">
|
<div className="form-check form-switch mb-0">
|
||||||
@ -241,17 +261,31 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
|
|||||||
<span>{showActive ? "Active Contacts" : "Inactive Contacts"}</span>
|
<span>{showActive ? "Active Contacts" : "Inactive Contacts"}</span>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
|
<li>
|
||||||
|
<button className="dropdown-item d-flex align-items-center gap-2" onClick={() => handleExport("csv")}>
|
||||||
|
<i className="bx bx-file"></i><span>Export to CSV</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button className="dropdown-item d-flex align-items-center gap-2" onClick={() => handleExport("excel")}>
|
||||||
|
<i className="bx bx-spreadsheet"></i><span>Export to Excel</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button className="dropdown-item d-flex align-items-center gap-2" onClick={() => handleExport("pdf")}>
|
||||||
|
<i className="bx bxs-file-pdf"></i><span>Export to PDF</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
{activeTab === "contacts" && <li><hr className="dropdown-divider" /></li>}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Suspense fallback={<MainDirectoryPageSkeleton />}>
|
<Suspense fallback={<MainDirectoryPageSkeleton />}>
|
||||||
{activeTab === "notes" && (
|
{activeTab === "notes" && (
|
||||||
|
|||||||
@ -1,117 +1,360 @@
|
|||||||
// NotesPage.jsx
|
import {
|
||||||
import React, { useEffect, useState } from "react";
|
useState,
|
||||||
|
Suspense,
|
||||||
|
lazy,
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
} from "react";
|
||||||
|
import Breadcrumb from "../../components/common/Breadcrumb";
|
||||||
import { useFab } from "../../Context/FabContext";
|
import { useFab } from "../../Context/FabContext";
|
||||||
import { useNotes } from "../../hooks/useDirectory";
|
import {
|
||||||
import NoteFilterPanel from "./NoteFilterPanel";
|
useBucketList,
|
||||||
import { defaultNotesFilter } from "../../components/Directory/DirectorySchema";
|
useBuckets,
|
||||||
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
useDeleteBucket,
|
||||||
import { useDebounce } from "../../utils/appUtils";
|
} from "../../hooks/useDirectory";
|
||||||
import NoteCardDirectoryEditable from "../../components/Directory/NoteCardDirectoryEditable";
|
import ManageBucket1 from "../../components/Directory/ManageBucket1";
|
||||||
import Pagination from "../../components/common/Pagination";
|
import ManageContact from "../../components/Directory/ManageContact";
|
||||||
import { NoteCardSkeleton } from "../../components/Directory/DirectoryPageSkeleton";
|
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 NotesPage = lazy(() => import("./NotesPage"));
|
||||||
const [filters, setFilter] = useState(defaultNotesFilter);
|
const ContactsPage = lazy(() => import("./ContactsPage"));
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
|
||||||
const debouncedSearch = useDebounce(searchText, 500);
|
|
||||||
|
|
||||||
const { data, isLoading, isError, error } = useNotes(
|
export const DirectoryContext = createContext();
|
||||||
projectId,
|
export const useDirectoryContext = () => {
|
||||||
ITEMS_PER_PAGE,
|
const context = useContext(DirectoryContext);
|
||||||
currentPage,
|
|
||||||
filters,
|
|
||||||
debouncedSearch
|
|
||||||
);
|
|
||||||
|
|
||||||
const { setOffcanvasContent, setShowTrigger } = useFab();
|
if (!context) {
|
||||||
|
return (
|
||||||
const clearFilter = () => {
|
<div className="container-fluid">
|
||||||
setFilter(defaultNotesFilter);
|
<p>Your Action is out of context</p>
|
||||||
};
|
</div>
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setShowTrigger(true);
|
|
||||||
setOffcanvasContent(
|
|
||||||
"Notes Filters",
|
|
||||||
<NoteFilterPanel onApply={setFilter} clearFilter={clearFilter} />
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
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 () => {
|
const [notesData, setNotesData] = useState([]);
|
||||||
setShowTrigger(false);
|
const [ContactData, setContactData] = useState([]);
|
||||||
setOffcanvasContent("", null);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 🔹 Format data for export
|
const handleExport = (type) => {
|
||||||
const formatExportData = (notes) => {
|
let exportData = activeTab === "notes" ? notesData : ContactData;
|
||||||
return notes.map((n) => ({
|
if (!exportData?.length) return;
|
||||||
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
|
switch (type) {
|
||||||
useEffect(() => {
|
case "csv":
|
||||||
if (data?.data && onExport) {
|
exportToCSV(exportData, activeTab === "notes" ? "Notes" : "Contacts");
|
||||||
onExport(formatExportData(data.data));
|
break;
|
||||||
}
|
case "excel":
|
||||||
}, [data?.data]);
|
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) => {
|
// Sanitize and trim long text to avoid PDF overflow
|
||||||
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
|
const sanitizedData = exportData.map(item => ({
|
||||||
setCurrentPage(page);
|
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 <div>{error.message}</div>;
|
// Export with proper spacing
|
||||||
if (isLoading) return <NoteCardSkeleton />;
|
exportToPDF(sanitizedData, "Contacts", columns, {
|
||||||
|
columnWidths: [200, 120, 180, 120, 200], // Adjust widths per column
|
||||||
return (
|
fontSizeHeader: 12,
|
||||||
<div className="d-flex flex-column text-start mt-3">
|
fontSizeRow: 10,
|
||||||
{data?.data?.length > 0 ? (
|
rowHeight: 25,
|
||||||
<>
|
});
|
||||||
{data.data.map((noteItem) => (
|
}
|
||||||
<NoteCardDirectoryEditable
|
break;
|
||||||
key={noteItem.id}
|
default:
|
||||||
noteItem={noteItem}
|
console.warn("Unsupported export type");
|
||||||
contactId={noteItem.contactId}
|
}
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<div className="col-12 d-flex justify-content-start mt-3">
|
|
||||||
<Pagination
|
|
||||||
currentPage={currentPage}
|
|
||||||
totalPages={data.totalPages}
|
|
||||||
onPageChange={paginate}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
// Card for "No notes available"
|
|
||||||
<div
|
|
||||||
className="card text-center d-flex align-items-center justify-content-center"
|
|
||||||
style={{ height: "200px" }}
|
|
||||||
>
|
|
||||||
<p className="text-muted mb-0">
|
|
||||||
{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."}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NotesPage;
|
|
||||||
|
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 <MainDirectoryPageSkeleton />;
|
||||||
|
if (isError) return <div>{error.message}</div>;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DirectoryContext.Provider value={contextValues}>
|
||||||
|
<div className={`${IsPage ? "container-fluid" : ""}`}>
|
||||||
|
{IsPage && (
|
||||||
|
<Breadcrumb
|
||||||
|
data={[
|
||||||
|
{ label: "Home", link: "/dashboard" },
|
||||||
|
{ label: "Directory", link: null },
|
||||||
|
]}
|
||||||
|
></Breadcrumb>
|
||||||
|
)}
|
||||||
|
<div className="card ">
|
||||||
|
<div className="d-flex-row px-2">
|
||||||
|
<div className="d-flex justify-content-between align-items-center mb-1">
|
||||||
|
<ul className="nav nav-tabs">
|
||||||
|
<li className="nav-item cursor-pointer">
|
||||||
|
<a
|
||||||
|
className={`nav-link ${activeTab === "notes" ? "active" : ""
|
||||||
|
} fs-6`}
|
||||||
|
onClick={(e) => handleTabClick("notes", e)}
|
||||||
|
>
|
||||||
|
<i className="bx bx-notepad bx-sm me-1_5"></i>
|
||||||
|
<span className="d-none d-md-inline">Notes</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li className="nav-item cursor-pointer">
|
||||||
|
<a
|
||||||
|
className={`nav-link ${activeTab === "contacts" ? "active" : ""
|
||||||
|
} fs-6`}
|
||||||
|
onClick={(e) => handleTabClick("contacts", e)}
|
||||||
|
>
|
||||||
|
<i className="bx bxs-contact bx-sm me-1_5"></i>
|
||||||
|
<span className="d-none d-md-inline">Contacts</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div className="mb-1 px-2 py-3">
|
||||||
|
<div className="d-flex align-items-center justify-content-between">
|
||||||
|
<div className="d-flex align-items-center gap-3">
|
||||||
|
{activeTab === "notes" && (
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
placeholder="Search notes..."
|
||||||
|
value={searchNote}
|
||||||
|
onChange={(e) => setSearchNote(e.target.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === "contacts" && (
|
||||||
|
<div className="d-flex align-items-center gap-3">
|
||||||
|
<div className="d-flex gap-2 align-items-center">
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
placeholder="Search contacts..."
|
||||||
|
value={searchContact}
|
||||||
|
onChange={(e) => setsearchContact(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className={`btn btn-sm p-1 ${gridView ? " btn-primary" : " btn-outline-primary"
|
||||||
|
}`}
|
||||||
|
onClick={() => setGridView(true)}
|
||||||
|
>
|
||||||
|
<i className="bx bx-grid-alt"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className={`btn btn-sm p-1 ${!gridView ? "btn-primary" : "btn-outline-primary"
|
||||||
|
}`}
|
||||||
|
onClick={() => setGridView(false)}
|
||||||
|
>
|
||||||
|
<i className="bx bx-list-ul"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="dropdown z-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-icon p-0 m-0"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
title="More Actions"
|
||||||
|
>
|
||||||
|
<i className="bx bx-dots-vertical-rounded text-muted bx-md"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ul className="dropdown-menu dropdown-menu-end shadow-sm p-2" style={{ minWidth: "220px" }}>
|
||||||
|
{activeTab === "contacts" && (
|
||||||
|
<li className="dropdown-item d-flex align-items-center">
|
||||||
|
<div className="form-check form-switch mb-0">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="form-check-input"
|
||||||
|
role="switch"
|
||||||
|
id="inactiveContactsSwitch"
|
||||||
|
checked={showActive}
|
||||||
|
onChange={(e) => setShowActive(e.target.checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span>{showActive ? "Active Contacts" : "Inactive Contacts"}</span>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
<li>
|
||||||
|
<button className="dropdown-item d-flex align-items-center gap-2" onClick={() => handleExport("csv")}>
|
||||||
|
<i className="bx bx-file"></i><span>Export to CSV</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button className="dropdown-item d-flex align-items-center gap-2" onClick={() => handleExport("excel")}>
|
||||||
|
<i className="bx bx-spreadsheet"></i><span>Export to Excel</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button className="dropdown-item d-flex align-items-center gap-2" onClick={() => handleExport("pdf")}>
|
||||||
|
<i className="bx bxs-file-pdf"></i><span>Export to PDF</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
{activeTab === "contacts" && <li><hr className="dropdown-divider" /></li>}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Suspense fallback={<MainDirectoryPageSkeleton />}>
|
||||||
|
{activeTab === "notes" && (
|
||||||
|
<NotesPage
|
||||||
|
projectId={projectId}
|
||||||
|
searchText={searchNote}
|
||||||
|
onExport={setNotesData}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{activeTab === "contacts" && (
|
||||||
|
<ContactsPage
|
||||||
|
projectId={projectId}
|
||||||
|
searchText={searchContact}
|
||||||
|
onExport={setContactData}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isOpenBucket && (
|
||||||
|
<GlobalModel
|
||||||
|
size="lg"
|
||||||
|
isOpen={isOpenBucket}
|
||||||
|
closeModal={() => setOpenBucket(false)}
|
||||||
|
>
|
||||||
|
<ManageBucket1 closeModal={() => setOpenBucket(false)} />
|
||||||
|
</GlobalModel>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{contactOpen.Open && (
|
||||||
|
<GlobalModel
|
||||||
|
size="xl"
|
||||||
|
isOpen={contactOpen.Open}
|
||||||
|
closeModal={() => setContactOpen({ contact: null, Open: false })}
|
||||||
|
>
|
||||||
|
<ContactProfile contactId={contactOpen.contact} />
|
||||||
|
</GlobalModel>
|
||||||
|
)}
|
||||||
|
{isManageContact.isOpen && (
|
||||||
|
<GlobalModel
|
||||||
|
size="lg"
|
||||||
|
isOpen={isManageContact}
|
||||||
|
closeModal={() =>
|
||||||
|
setManageContact({ isOpen: false, contactId: null })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ManageContact
|
||||||
|
contactId={isManageContact.contactId}
|
||||||
|
closeModal={() =>
|
||||||
|
setManageContact({ isOpen: false, contactId: null })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</GlobalModel>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{deleteBucket.isOpen && (
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={deleteBucket.isOpen}
|
||||||
|
type="delete"
|
||||||
|
header="Delete Bucket"
|
||||||
|
message="Are you sure you want delete?"
|
||||||
|
onSubmit={handleDelete}
|
||||||
|
onClose={() => setDeleteBucket({ isOpen: false, bucketId: null })}
|
||||||
|
loading={Deleting}
|
||||||
|
paramData={deleteBucket.bucketId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DirectoryContext.Provider>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -40,39 +40,52 @@ export const exportToExcel = (data, fileName = "data") => {
|
|||||||
* @param {Array} data - Array of objects to export
|
* @param {Array} data - Array of objects to export
|
||||||
* @param {string} fileName - File name for the PDF (optional)
|
* @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;
|
if (!data || data.length === 0) return;
|
||||||
|
|
||||||
const pdfDoc = await PDFDocument.create();
|
const pdfDoc = await PDFDocument.create();
|
||||||
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
|
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||||
|
|
||||||
// Landscape dimensions
|
// Default options
|
||||||
const pageWidth = 1000; // wider for more space
|
const {
|
||||||
|
columnWidths = [], // array of widths per column
|
||||||
|
fontSizeHeader = 12,
|
||||||
|
fontSizeRow = 10,
|
||||||
|
rowHeight = 25,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const pageWidth = 1000;
|
||||||
const pageHeight = 600;
|
const pageHeight = 600;
|
||||||
let page = pdfDoc.addPage([pageWidth, pageHeight]);
|
let page = pdfDoc.addPage([pageWidth, pageHeight]);
|
||||||
const margin = 30;
|
const margin = 30;
|
||||||
let y = pageHeight - margin;
|
let y = pageHeight - margin;
|
||||||
|
|
||||||
const headers = columns || Object.keys(data[0]);
|
const headers = columns || Object.keys(data[0]);
|
||||||
const rowHeight = 25; // slightly taller rows for readability
|
|
||||||
const columnSpacing = 150; // increase space between columns
|
|
||||||
|
|
||||||
// Draw headers
|
// Draw headers
|
||||||
headers.forEach((header, i) => {
|
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;
|
y -= rowHeight;
|
||||||
|
|
||||||
// Draw rows
|
// Draw rows
|
||||||
data.forEach(row => {
|
data.forEach(row => {
|
||||||
headers.forEach((header, i) => {
|
headers.forEach((header, i) => {
|
||||||
const text = row[header] ? row[header].toString() : '';
|
const x = margin + (columnWidths[i] ? columnWidths.slice(0, i).reduce((a, b) => a + b, 0) : i * 150);
|
||||||
page.drawText(text, { x: margin + i * columnSpacing, y, font, size: 10 });
|
const text = row[header] || '';
|
||||||
|
page.drawText(text, { x, y, font, size: fontSizeRow });
|
||||||
});
|
});
|
||||||
y -= rowHeight;
|
y -= rowHeight;
|
||||||
|
|
||||||
if (y < margin) {
|
if (y < margin) {
|
||||||
page = pdfDoc.addPage([pageWidth, pageHeight]); // landscape for new page
|
page = pdfDoc.addPage([pageWidth, pageHeight]);
|
||||||
y = pageHeight - margin;
|
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.
|
* Print the HTML table by accepting the table element or a reference.
|
||||||
* @param {HTMLElement} table - The table element (or ref) to print
|
* @param {HTMLElement} table - The table element (or ref) to print
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user