Merge branch 'OnFieldWork_V1' of https://git.marcoaiot.com/admin/marco.pms.web into Kartik_Bug#1462
This commit is contained in:
commit
8357fa9b22
@ -133,8 +133,11 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.length) {
|
||||
filtering(data);
|
||||
}, [data, showPending]);
|
||||
}
|
||||
}, [data, showPending]);
|
||||
|
||||
|
||||
// New useEffect to handle search filtering
|
||||
const filteredSearchData = useMemo(() => {
|
||||
@ -148,31 +151,6 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
|
||||
});
|
||||
}, [processedData, searchTerm]);
|
||||
|
||||
// const filteredSearchData = useMemo(() => {
|
||||
// let tempData = processedData;
|
||||
|
||||
// if (searchTerm) {
|
||||
// const lowercasedSearchTerm = searchTerm.toLowerCase();
|
||||
// tempData = tempData.filter((item) => {
|
||||
// const fullName = `${item.firstName} ${item.lastName}`.toLowerCase();
|
||||
// return fullName.includes(lowercasedSearchTerm);
|
||||
// });
|
||||
// }
|
||||
|
||||
// if (filters?.selectedOrganization) {
|
||||
// tempData = tempData.filter(
|
||||
// (item) => item.organization?.name === filters.selectedOrganization
|
||||
// );
|
||||
// }
|
||||
|
||||
// if (filters?.selectedServices?.length > 0) {
|
||||
// tempData = tempData.filter((item) =>
|
||||
// filters.selectedServices.includes(item.service?.name)
|
||||
// );
|
||||
// }
|
||||
|
||||
// return tempData;
|
||||
// }, [processedData, searchTerm, filters]);
|
||||
|
||||
|
||||
const {
|
||||
@ -250,12 +228,12 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
|
||||
className="dataTables_length text-start py-2 d-flex justify-content-between"
|
||||
id="DataTables_Table_0_length"
|
||||
>
|
||||
<div className="d-flex align-items-center my-0 ">
|
||||
<div className="d-flex align-items-center my-0 ms-sm-8 ">
|
||||
<DateRangePicker
|
||||
onRangeChange={setDateRange}
|
||||
defaultStartDate={yesterday}
|
||||
/>
|
||||
<div className="form-check form-switch text-start m-0 ms-5">
|
||||
<div className="form-check form-switch text-start ms-1 ms-md-5 align-items-center mb-0">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
@ -270,9 +248,9 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="table-responsive text-nowrap" style={{ minHeight: "200px" }}>
|
||||
<div className="table-responsive text-nowrap " >
|
||||
{isLoading ? (
|
||||
<div className="d-flex justify-content-center align-items-center" style={{ height: "200px" }}>
|
||||
<div className="d-flex justify-content-center align-items-center">
|
||||
<p className="text-secondary">Loading...</p>
|
||||
</div>
|
||||
) : filteredSearchData?.length > 0 ? (
|
||||
@ -370,7 +348,7 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<div className="my-12"><span className="text-secondary">No data available for the selected date range. Please Select another date.</span></div>
|
||||
<div className="my-12"><span className="text-secondary">No data for this date range. Please choose another.</span></div>
|
||||
)}
|
||||
</div>
|
||||
{paginatedAttendances?.length == 0 && filteredSearchData?.length > 0 && (
|
||||
|
@ -110,8 +110,8 @@ const CheckInCheckOut = ({ modeldata, closeModal, handleSubmitForm }) => {
|
||||
}, [projectNames, projectId, loading]);
|
||||
|
||||
return (
|
||||
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="col-12 d-flex justify-content-center">
|
||||
<form className="row p-2" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="col-12 d-flex justify-content-center mt-2">
|
||||
<label className="fs-5 text-dark text-center">
|
||||
{modeldata?.checkInTime && !modeldata?.checkOutTime
|
||||
? `Check out for ${currentProject?.name}`
|
||||
@ -220,7 +220,7 @@ export const Regularization = ({ modeldata, closeModal, handleSubmitForm }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
|
||||
<form className="row " onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="col-12 col-md-12">
|
||||
<p>Regularize Attendance</p>
|
||||
<label className="form-label" htmlFor="description">
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
import Chart from "react-apexcharts";
|
||||
import { useExpenseAnalysis } from "../../hooks/useDashboard_Data";
|
||||
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||
@ -10,16 +10,12 @@ const ExpenseAnalysis = () => {
|
||||
const projectId = useSelectedProject();
|
||||
|
||||
const methods = useForm({
|
||||
defaultValues: {
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
},
|
||||
defaultValues: { startDate: "", endDate: "" },
|
||||
});
|
||||
|
||||
const { watch } = methods;
|
||||
|
||||
|
||||
const [startDate, endDate] = watch(["startDate", "endDate"]);
|
||||
|
||||
const { data, isLoading, isError, error, isFetching } = useExpenseAnalysis(
|
||||
projectId,
|
||||
startDate ? localToUtc(startDate) : null,
|
||||
@ -28,18 +24,13 @@ const ExpenseAnalysis = () => {
|
||||
|
||||
if (isError) return <div>{error.message}</div>;
|
||||
|
||||
|
||||
|
||||
|
||||
const report = data?.report ?? [];
|
||||
|
||||
const { labels, series, total } = useMemo(() => {
|
||||
const { labels, series, total } = useMemo(() => {
|
||||
const labels = report.map((item) => item.projectName);
|
||||
const series = report.map((item) => item.totalApprovedAmount || 0);
|
||||
const total = formatCurrency(data?.totalAmount || 0);
|
||||
|
||||
return { labels, series, total };
|
||||
}, [report, data?.totalAmount]);
|
||||
}, [report, data?.totalAmount]);
|
||||
|
||||
const donutOptions = {
|
||||
chart: { type: "donut" },
|
||||
@ -63,26 +54,34 @@ const { labels, series, total } = useMemo(() => {
|
||||
},
|
||||
},
|
||||
},
|
||||
responsive: [
|
||||
{
|
||||
breakpoint: 576, // mobile breakpoint
|
||||
options: {
|
||||
chart: { width: "100%" },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="card-header d-flex justify-content-between align-items-center ">
|
||||
<div>
|
||||
<h5 className="mb-1 card-title text-start">Expense Breakdown</h5>
|
||||
<p className="card-subtitle">Category Wise Expense Breakdown</p>
|
||||
{/* Header */}
|
||||
<div className="card-header d-flex flex-column flex-sm-row justify-content-between align-items-start align-items-sm-center gap-2">
|
||||
<div className="text-start w-100">
|
||||
<h5 className="mb-1 card-title">Expense Breakdown</h5>
|
||||
<p className="card-subtitle mb-0">Category Wise Expense Breakdown</p>
|
||||
</div>
|
||||
|
||||
<div className="text-end">
|
||||
<div className="text-start text-sm-end w-75">
|
||||
<FormProvider {...methods}>
|
||||
<DateRangePicker1 />
|
||||
</FormProvider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card body */}
|
||||
<div className="card-body position-relative">
|
||||
{/* Initial loading: show full loader */}
|
||||
{isLoading && (
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center"
|
||||
@ -92,14 +91,12 @@ const { labels, series, total } = useMemo(() => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Data display */}
|
||||
{!isLoading && report.length === 0 && (
|
||||
<div className="text-center py-5 text-muted ">No data found</div>
|
||||
<div className="text-center py-5 text-muted">No data found</div>
|
||||
)}
|
||||
|
||||
{!isLoading && report.length > 0 && (
|
||||
<>
|
||||
{/* Overlay spinner for refetch */}
|
||||
{isFetching && (
|
||||
<div className="position-absolute top-0 start-0 w-100 h-100 d-flex justify-content-center align-items-center bg-white bg-opacity-75">
|
||||
<span>Loading...</span>
|
||||
@ -109,17 +106,18 @@ const { labels, series, total } = useMemo(() => {
|
||||
<div className="d-flex justify-content-center mb-3">
|
||||
<Chart
|
||||
options={donutOptions}
|
||||
series={report.map((item) => item.totalApprovedAmount || 0)}
|
||||
series={series}
|
||||
type="donut"
|
||||
width="320"
|
||||
width="100%"
|
||||
height={320}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-2 w-100">
|
||||
<div className="row">
|
||||
<div className="row g-2">
|
||||
{report.map((item, idx) => (
|
||||
<div
|
||||
className="col-6 d-flex justify-content-start align-items-start mb-2"
|
||||
className="col-12 col-sm-6 d-flex align-items-start"
|
||||
key={idx}
|
||||
>
|
||||
<div className="avatar me-2">
|
||||
@ -127,16 +125,14 @@ const { labels, series, total } = useMemo(() => {
|
||||
className="avatar-initial rounded-2"
|
||||
style={{
|
||||
backgroundColor:
|
||||
donutOptions.colors[
|
||||
idx % donutOptions.colors.length
|
||||
],
|
||||
donutOptions.colors[idx % donutOptions.colors.length],
|
||||
}}
|
||||
>
|
||||
<i className="bx bx-receipt fs-4"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div className="d-flex flex-column gap-1 text-start">
|
||||
<small className="fw-semibold ">{item.projectName}</small>
|
||||
<small className="fw-semibold">{item.projectName}</small>
|
||||
<span className="fw-semibold text-muted ms-1">
|
||||
{formatCurrency(item.totalApprovedAmount)}
|
||||
</span>
|
||||
|
@ -113,7 +113,7 @@ const ExpenseByProject = () => {
|
||||
</div>
|
||||
|
||||
{/* Range Buttons + Expense Dropdown */}
|
||||
<div className="d-flex align-items-center flex-wrap">
|
||||
<div className="d-flex align-items-center flex-wrap ">
|
||||
{["1M", "3M", "6M", "12M", "All"].map((item) => (
|
||||
<button
|
||||
key={item}
|
||||
@ -129,7 +129,7 @@ const ExpenseByProject = () => {
|
||||
))}
|
||||
{viewMode === "Category" && (
|
||||
<select
|
||||
className="form-select form-select-sm ms-auto mb-3"
|
||||
className="form-select form-select-sm ms-auto mb-3 mt-1 mt-sm-0"
|
||||
value={selectedType}
|
||||
onChange={(e) => setSelectedType(e.target.value)}
|
||||
disabled={typeLoading}
|
||||
|
@ -131,11 +131,12 @@ const ProjectListView = ({
|
||||
|
||||
const handleMoveDetails = (project) => {
|
||||
dispatch(setProjectId(project));
|
||||
localStorage.setItem("lastActiveProjectTab","profile")
|
||||
localStorage.setItem("lastActiveProjectTab", "profile")
|
||||
navigate("/projects/details");
|
||||
};
|
||||
return (
|
||||
<div className="card page-min-h py-4 px-6 shadow-sm">
|
||||
<div className="table-responsive text-nowrap">
|
||||
<table className="table table-hover align-middle m-0">
|
||||
<thead className="border-bottom">
|
||||
<tr>
|
||||
@ -257,8 +258,7 @@ const ProjectListView = ({
|
||||
</li>
|
||||
))}
|
||||
<li
|
||||
className={`page-item ${
|
||||
currentPage === totalPages && "disabled"
|
||||
className={`page-item ${currentPage === totalPages && "disabled"
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
@ -274,6 +274,7 @@ const ProjectListView = ({
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -63,19 +63,18 @@ const DateRangePicker = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`col-${sm} col-sm-${md} px-1`}>
|
||||
<div className={`position-relative w-auto justify-content-center`}>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm ps-2 pe-5 me-4"
|
||||
className="form-control form-control-sm w-100 pe-8 "
|
||||
placeholder="From to End"
|
||||
id="flatpickr-range"
|
||||
ref={inputRef}
|
||||
/>
|
||||
|
||||
<i
|
||||
className="bx bx-calendar calendar-icon cursor-pointer position-relative top-50 translate-middle-y"
|
||||
className="bx bx-calendar calendar-icon cursor-pointer position-absolute top-50 end-0 translate-middle-y me-2 "
|
||||
onClick={handleIconClick}
|
||||
style={{ right: "22px", bottom: "-8px" }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@ -130,7 +129,7 @@ export const DateRangePicker1 = ({
|
||||
mode: "range",
|
||||
dateFormat: "d-m-Y",
|
||||
allowInput: allowText,
|
||||
maxDate ,
|
||||
maxDate,
|
||||
onChange: (selectedDates) => {
|
||||
if (selectedDates.length === 2) {
|
||||
const [start, end] = selectedDates;
|
||||
@ -172,7 +171,7 @@ export const DateRangePicker1 = ({
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [resetSignal, defaultRange, setValue, startField, endField]);
|
||||
}, [resetSignal, defaultRange, setValue, startField, endField]);
|
||||
|
||||
|
||||
const start = getValues(startField);
|
||||
@ -183,7 +182,7 @@ export const DateRangePicker1 = ({
|
||||
<div className={`position-relative ${className}`}>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
className="form-control form-control-sm ps-2 pe-5 me-4 cursor-pointer"
|
||||
placeholder={placeholder}
|
||||
defaultValue={formattedValue}
|
||||
ref={(el) => {
|
||||
|
@ -124,7 +124,7 @@ const AttendancePage = () => {
|
||||
<div className="nav-align-top nav-tabs-shadow ">
|
||||
{/* Tabs */}
|
||||
<div className="nav-align-top nav-tabs-shadow bg-white border-bottom pt-5">
|
||||
<div className="row align-items-center g-0 mb-3 mb-md-0 mx-5">
|
||||
<div className="row align-items-center g-0 mb-3 mb-md-0 mx-2 mx-sm-5">
|
||||
{/* Tabs */}
|
||||
<div className="col-12 col-md">
|
||||
<ul className="nav nav-tabs" role="tablist">
|
||||
|
@ -19,9 +19,15 @@ 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,11 +70,49 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
|
||||
const [ContactData, setContactData] = useState([]);
|
||||
|
||||
const handleExport = (type) => {
|
||||
if (activeTab === "notes" && type === "csv") {
|
||||
exportToCSV(notesData, "notes.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,
|
||||
});
|
||||
}
|
||||
if (activeTab === "contacts" && type === "csv") {
|
||||
exportToCSV(ContactData, "contact.csv");
|
||||
break;
|
||||
default:
|
||||
console.warn("Unsupported export type");
|
||||
}
|
||||
};
|
||||
|
||||
@ -139,7 +183,8 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
|
||||
<ul className="nav nav-tabs">
|
||||
<li className="nav-item cursor-pointer">
|
||||
<a
|
||||
className={`nav-link ${activeTab === "notes" ? "active" : ""
|
||||
className={`nav-link ${
|
||||
activeTab === "notes" ? "active" : ""
|
||||
} fs-6`}
|
||||
onClick={(e) => handleTabClick("notes", e)}
|
||||
>
|
||||
@ -149,7 +194,8 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
|
||||
</li>
|
||||
<li className="nav-item cursor-pointer">
|
||||
<a
|
||||
className={`nav-link ${activeTab === "contacts" ? "active" : ""
|
||||
className={`nav-link ${
|
||||
activeTab === "contacts" ? "active" : ""
|
||||
} fs-6`}
|
||||
onClick={(e) => handleTabClick("contacts", e)}
|
||||
>
|
||||
@ -182,16 +228,23 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
|
||||
value={searchContact}
|
||||
onChange={(e) => setsearchContact(e.target.value)}
|
||||
/>
|
||||
<div className="d-none d-md-flex gap-2">
|
||||
{" "}
|
||||
<button
|
||||
className={`btn btn-sm p-1 ${gridView ? " btn-primary" : " btn-outline-primary"
|
||||
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"
|
||||
className={`btn btn-sm p-1 ${
|
||||
!gridView
|
||||
? "btn-primary"
|
||||
: "btn-outline-primary"
|
||||
}`}
|
||||
onClick={() => setGridView(false)}
|
||||
>
|
||||
@ -199,6 +252,7 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="dropdown z-2">
|
||||
@ -212,20 +266,7 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
|
||||
<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" }}>
|
||||
<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>}
|
||||
|
||||
<ul className="dropdown-menu dropdown-menu-end shadow-sm ">
|
||||
{activeTab === "contacts" && (
|
||||
<li className="dropdown-item d-flex align-items-center">
|
||||
<div className="form-check form-switch mb-0">
|
||||
@ -235,23 +276,80 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
|
||||
role="switch"
|
||||
id="inactiveContactsSwitch"
|
||||
checked={showActive}
|
||||
onChange={(e) => setShowActive(e.target.checked)}
|
||||
onChange={(e) =>
|
||||
setShowActive(e.target.checked)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<span>{showActive ? "Active Contacts" : "Inactive Contacts"}</span>
|
||||
<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>
|
||||
<li className={`d-block d-md-none ${activeTab === "contacts" ? "d-block":"d-none"}`}>
|
||||
<span
|
||||
className={`dropdown-item ${
|
||||
gridView ? " text-primary" : ""
|
||||
}`}
|
||||
onClick={() => setGridView(true)}
|
||||
>
|
||||
<i className="bx bx-grid-alt"></i> Card View
|
||||
</span>
|
||||
</li>
|
||||
|
||||
<li className={` d-block d-md-none ${activeTab === "contacts" ? "d-block":"d-none"}`}>
|
||||
<span
|
||||
className={`dropdown-item ${
|
||||
!gridView ? "text-primary" : ""
|
||||
}`}
|
||||
onClick={() => setGridView(false)}
|
||||
>
|
||||
<i className="bx bx-list-ul"></i> List View
|
||||
</span>
|
||||
</li>
|
||||
|
||||
{/* Divider */}
|
||||
{activeTab === "contacts" && (
|
||||
<li>
|
||||
<hr className="dropdown-divider" />
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<Suspense fallback={<MainDirectoryPageSkeleton />}>
|
||||
{activeTab === "notes" && (
|
||||
|
@ -42,7 +42,7 @@ const NotesPage = ({ projectId, searchText, onExport }) => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 🔹 Format data for export
|
||||
// Format data for export
|
||||
const formatExportData = (notes) => {
|
||||
return notes.map((n) => ({
|
||||
ContactName: n.contactName || "",
|
||||
@ -59,7 +59,7 @@ const NotesPage = ({ projectId, searchText, onExport }) => {
|
||||
}));
|
||||
};
|
||||
|
||||
// 🔹 Pass formatted notes to parent for export
|
||||
// Pass formatted notes to parent for export
|
||||
useEffect(() => {
|
||||
if (data?.data && onExport) {
|
||||
onExport(formatExportData(data.data));
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
} from "../../utils/constants";
|
||||
|
||||
import { defaultFilter, SearchSchema } from "../../components/Expenses/ExpenseSchema";
|
||||
import PreviewDocument from "../../components/Expenses/PreviewDocument";
|
||||
|
||||
// Context
|
||||
export const ExpenseContext = createContext();
|
||||
|
@ -1,27 +0,0 @@
|
||||
// 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();
|
||||
};
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user