Compare commits

...

14 Commits

Author SHA1 Message Date
33033dea9d Create a seprate file for export functionality in employee. 2025-10-15 18:02:36 +05:30
79ac3ee481 Changes in excel import. 2025-10-15 17:48:58 +05:30
10227a2722 Export to PDF button not functioning 2025-10-13 16:40:58 +05:30
9592108472 Removing extra margin-top on Project-details. 2025-10-13 16:35:28 +05:30
3b032b7b07 Merge pull request 'Incorrect Toggle Switch Text for Active/Inactive Employee in Project Teams' (#475) from Kartik_Bug#1455 into Issues_Oct_main_2W
Reviewed-on: #475
merged
2025-10-13 09:33:09 +00:00
b8891d403f Incorrect Toggle Switch Text for Active/Inactive Employee in Project Teams 2025-10-13 09:33:09 +00:00
01568db61c Merge pull request '“NA” Should Be Displayed When Employee Has No Email Instead of “–”' (#476) from Kartik_Bug#1451 into Issues_Oct_main_2W
Reviewed-on: #476
Merged
2025-10-13 09:32:51 +00:00
80a974e3be “NA” Should Be Displayed When Employee Has No Email Instead of “–” 2025-10-13 09:32:51 +00:00
f3e05a11d6 Merge pull request 'Adding Project and Service field in View Organization popup.' (#477) from Kartik_Task#1477 into Issues_Oct_main_2W
Reviewed-on: #477
Merged
2025-10-13 09:32:32 +00:00
222e6495a8 Adding Project and Service field in View Organization popup. 2025-10-13 09:32:32 +00:00
18a3b8a85b Merge pull request 'Filter Sidebar Should Auto-Close When Navigating to Another Page' (#478) from Kartik_Bug#1450 into Issues_Oct_main_2W
Reviewed-on: #478
merged
2025-10-13 09:32:17 +00:00
d75296ffe8 Filter Sidebar Should Auto-Close When Navigating to Another Page 2025-10-13 14:51:47 +05:30
6649cab6a2 Added cursor-not-allowed when user can delete the organization. 2025-10-13 14:16:11 +05:30
eab23389ed Correction in Projects Completion Status in this weidget data cannot be shown. 2025-10-13 12:51:42 +05:30
18 changed files with 474 additions and 265 deletions

View File

@ -280,3 +280,7 @@
.w-8-xl{ width: 2rem; }
.w-10-xl{ width: 2.5rem; }
}
.cursor-not-allowed{
cursor: not-allowed;
}

View File

@ -126,7 +126,7 @@ const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizat
checked={ShowPending}
onChange={(e) => setShowPending(e.target.checked)}
/>
<label className="form-check-label ms-0">Show Pending</label>
<label className="form-check-label ms-0">Pending Attendance</label>
</div>
</div>
{attLoading ? (

View File

@ -190,7 +190,7 @@ useEffect(() => {
checked={showPending}
onChange={(e) => setShowPending(e.target.checked)}
/>
<label className="form-check-label ms-0">Show Pending</label>
<label className="form-check-label ms-0">Pending Attendance</label>
</div>
</div>
</div>

View File

@ -23,7 +23,7 @@ const HorizontalBarChart = ({
if (loading) {
return (
<div className="w-full h-[380px] flex items-center justify-center bg-gray-100 rounded-xl">
<span className="text-gray-500 text-sm">Loading chart...</span>
<span className="text-gray-500">Loading chart...</span>
{/* Replace this with a skeleton or spinner if you prefer */}
</div>
);

View File

@ -3,7 +3,8 @@ import HorizontalBarChart from "../Charts/HorizontalBarChart";
import { useProjects } from "../../hooks/useProjects";
const ProjectCompletionChart = () => {
const { projects, loading } = useProjects();
const { data: projects = [], isLoading: loading, isError, error } = useProjects();
// Bar chart logic
const projectNames = projects?.map((p) => p.name) || [];
@ -11,7 +12,7 @@ const ProjectCompletionChart = () => {
projects?.map((p) => {
const completed = p.completedWork || 0;
const planned = p.plannedWork || 1;
const percent = (completed / planned) * 100;
const percent = planned ? (completed / planned) * 100 : 0;
return Math.min(Math.round(percent), 100);
}) || [];

View File

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { useDocumentFilterEntities } from "../../hooks/useDocument";
import { FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
@ -9,9 +9,11 @@ import {
import { DateRangePicker1 } from "../common/DateRangePicker";
import SelectMultiple from "../common/SelectMultiple";
import moment from "moment";
import { useLocation } from "react-router-dom";
const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
const [resetKey, setResetKey] = useState(0);
const location = useLocation();
const { data, isError, isLoading, error } =
useDocumentFilterEntities(entityTypeId);
@ -52,6 +54,13 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
closePanel();
};
// Close popup when navigating to another component
useEffect(() => {
return () => {
closePanel();
};
}, []);
if (isLoading) return <div>Loading...</div>;
if (isError)
return <div>Error: {error?.message || "Something went wrong!"}</div>;
@ -63,6 +72,8 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
documentTag = [],
} = data?.data || {};
return (
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)}>
@ -73,18 +84,16 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
<div className="d-inline-flex border rounded-pill overflow-hidden shadow-none">
<button
type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${
isUploadedAt ? "active btn-secondary text-white" : ""
}`}
className={`btn px-2 py-1 rounded-0 text-tiny ${isUploadedAt ? "active btn-secondary text-white" : ""
}`}
onClick={() => setValue("isUploadedAt", true)}
>
Uploaded On
</button>
<button
type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${
!isUploadedAt ? "active btn-secondary text-white" : ""
}`}
className={`btn px-2 py-1 rounded-0 text-tiny ${!isUploadedAt ? "active btn-secondary text-white" : ""
}`}
onClick={() => setValue("isUploadedAt", false)}
>
Updated On

View File

@ -144,12 +144,12 @@ const Documents = ({ Document_Entity, Entity }) => {
<span className="switch-off"></span>
</span>
<span className="switch-label">
{isActive ? "Active" : "In-Active"}
{isActive ? "Active Document" : "In-Active Document"}
</span>
</label>
</div>
<div className="col-6 col-md-6 col-lg-8 text-end">
<div className="col-12 col-md-6 col-lg-8 text-end">
{(isSelf || canUploadDocument) && (
<button
className="btn btn-sm btn-primary me-3"

View File

@ -51,7 +51,6 @@ const EmpAttendance = () => {
new Date(b?.checkInTime).getTime() - new Date(a?.checkInTime).getTime()
);
console.log(sorted);
const { currentPage, totalPages, currentItems, paginate } = usePagination(
sorted,

View File

@ -0,0 +1,84 @@
import moment from "moment";
import { exportToExcel, exportToCSV, exportToPDF, printTable } from "../../utils/tableExportUtils";
/**
* Handles export operations for employee data.
* @param {string} type - Export type: 'csv', 'excel', 'pdf', or 'print'
* @param {Array} employeeList - Full employee data array
* @param {Array} filteredData - Filtered employee data (if search applied)
* @param {string} searchText - Current search text (used to decide dataset)
* @param {RefObject} tableRef - Table reference (used for print)
*/
const handleEmployeeExport = (type, employeeList, filteredData, searchText, tableRef) => {
// Export full list (filtered if search applied)
const dataToExport = searchText ? filteredData : employeeList;
if (!dataToExport || dataToExport.length === 0) return;
// Map and format employee data for export
const exportData = dataToExport.map((item) => ({
"First Name": item.firstName || "",
"Middle Name": item.middleName || "",
"Last Name": item.lastName || "",
"Email": item.email || "",
"Gender": item.gender || "",
"Birth Date": item.birthdate
? moment(item.birthdate).format("DD-MMM-YYYY")
: "",
"Joining Date": item.joiningDate
? moment(item.joiningDate).format("DD-MMM-YYYY")
: "",
"Permanent Address": item.permanentAddress || "",
"Current Address": item.currentAddress || "",
"Phone Number": item.phoneNumber || "",
"Emergency Phone Number": item.emergencyPhoneNumber || "",
"Emergency Contact Person": item.emergencyContactPerson || "",
"Is Active": item.isActive ? "Active" : "Inactive",
"Job Role": item.jobRole || "",
}));
switch (type) {
case "csv":
exportToCSV(exportData, "employees");
break;
case "excel":
exportToExcel(exportData, "employees");
break;
case "pdf":
exportToPDF(
dataToExport.map((item) => ({
Name: `${item.firstName || ""} ${item.lastName || ""}`.trim(),
Email: item.email || "",
"Phone Number": item.phoneNumber || "",
"Job Role": item.jobRole || "",
"Joining Date": item.joiningDate
? moment(item.joiningDate).format("DD-MMM-YYYY")
: "",
Gender: item.gender || "",
Status: item.isActive ? "Active" : "Inactive",
})),
"employees",
[
"Name",
"Email",
"Phone Number",
"Job Role",
"Joining Date",
"Gender",
"Status",
]
);
break;
case "print":
printTable(tableRef.current);
break;
default:
break;
}
};
export default handleEmployeeExport;

View File

@ -131,7 +131,7 @@ const OrganizationsList = ({searchText}) => {
<div className="d-flex justify-content-center gap-2">
<i className="bx bx-show text-primary cursor-pointer" onClick={()=>onOpen({startStep:5,orgData:org.id,flowType:"view"})}></i>
<i className="bx bx-edit text-secondary cursor-pointer" onClick={()=>onOpen({startStep:4,orgData:org,flowType:"edit"})}></i>
<i className="bx bx-trash text-danger cursor-pointer"></i>
<i className="bx bx-trash text-danger cursor-not-allowed"></i>
</div>
</td>
</tr>

View File

@ -22,9 +22,8 @@ const VieworgDataanization = ({ orgId }) => {
</div>
<div className="text-end">
<span
className={`badge bg-label-${
data?.isActive ? "primary" : "secondary"
} `}
className={`badge bg-label-${data?.isActive ? "primary" : "secondary"
} `}
>
{data?.isActive ? "Active" : "In-Active"}{" "}
</span>
@ -105,9 +104,101 @@ const VieworgDataanization = ({ orgId }) => {
<div className="text-muted text-start">{data?.address}</div>
</div>
</div>
<div className="d-flex text-secondary mb-2">
{" "}
<i className="bx bx-sm bx-briefcase me-1" /> Projects And Services
<div className="col-12 mb-3">
<div
className="d-flex justify-content-between align-items-center text-secondary mb-2 cursor-pointer"
data-bs-toggle="collapse"
data-bs-target="#collapse-projects-services"
aria-expanded="false"
>
<div>
<i className="bx bx-sm bx-briefcase me-1" /> Projects
</div>
<i className="bx bx-chevron-down me-2"></i>
</div>
{/* remove "show" from className */}
<div id="collapse-projects-services" className="collapse">
{data?.projects && data.projects.length > 0 ? (
data.projects
.reduce((acc, curr) => {
const projectId = curr.project.id;
if (!acc.find((p) => p.id === projectId)) {
acc.push(curr.project);
}
return acc;
}, [])
.map((project) => (
<div key={project.id} className="mb-2 rounded p-2">
<div
className="d-flex justify-content-between align-items-center cursor-pointer"
data-bs-toggle="collapse"
data-bs-target={`#collapse-${project.id}`}
aria-expanded="false"
>
<label className="form-label fw-semibold">
<i className="bx bx-buildings me-2"></i>
{project.name}
</label>
<i className="bx bx-chevron-down"></i>
</div>
<div id={`collapse-${project.id}`} className="collapse mt-2 ps-5">
{data.projects
.filter((p) => p.project.id === project.id)
.map((p) => (
<div key={p.service.id} className="mb-1 text-muted">
<i className="bx bx-wrench me-2"></i>
{p.service.name}
</div>
))}
</div>
</div>
))
) : (
<div className="text-muted fst-italic ps-2">No projects available</div>
)}
</div>
</div>
{/* Services Section */}
<div className="col-12 mb-3">
<div
className="d-flex justify-content-between align-items-center text-secondary mb-2 cursor-pointer"
data-bs-toggle="collapse"
data-bs-target="#collapse-services"
aria-expanded="false"
>
<div>
<i className="bx bx-sm bx-cog me-1" /> Services
</div>
<i className="bx bx-chevron-down me-2"></i>
</div>
{/* collapse is closed initially */}
<div id="collapse-services" className="collapse">
{data?.services && data.services.length > 0 ? (
<div className="row">
{data.services.map((service) => (
<div key={service.id} className="col-md-12 mb-3">
<div className="card h-100 shadow-sm border-0">
<div className="card-body">
<h6 className="fw-semibold mb-1">
<i className="bx bx-wrench me-1"></i>
{service.name}
</h6>
<p className="text-muted small mb-0">
{service.description || "No description available."}
</p>
</div>
</div>
</div>
))}
</div>
) : (
<div className="text-muted fst-italic ps-2">No services available</div>
)}
</div>
</div>
</div>
);

View File

@ -201,7 +201,7 @@ const Teams = () => {
className="form-check-label ms-2"
htmlFor="activeEmployeeSwitch"
>
{activeEmployee ? "Active Employees" : "Include Inactive Employees"}
{activeEmployee ? "Active Employees" : "In-active Employees"}
</label>
</div>
</div>

View File

@ -1,5 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import React from "react";
import React, { useEffect } from "react";
import { FormProvider, useForm } from "react-hook-form";
import {
contactsFilter,
@ -8,11 +8,14 @@ import {
import { useContactFilter } from "../../hooks/useDirectory";
import { ExpenseFilterSkeleton } from "../../components/Expenses/ExpenseSkeleton";
import SelectMultiple from "../../components/common/SelectMultiple";
import { useLocation } from "react-router-dom";
const ContactFilterPanel = ({ onApply, clearFilter }) => {
const { data, isError, isLoading, error, isFetched, isFetching } =
useContactFilter();
const location = useLocation();
const methods = useForm({
resolver: zodResolver(contactsFilter),
defaultValues: defaultContactFilter,
@ -30,14 +33,24 @@ const ContactFilterPanel = ({ onApply, clearFilter }) => {
};
const handleClose = () => {
reset(defaultContactFilter);
onApply(defaultContactFilter);
reset(defaultContactFilter);
onApply(defaultContactFilter);
closePanel();
};
useEffect(() => {
return () => {
closePanel();
};
}, []);
if (isLoading || isFetching) return <ExpenseFilterSkeleton />;
if (isError && isFetched)
return <div>Something went wrong Here- {error.message} </div>;
return (
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start">

View File

@ -139,9 +139,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" : ""
} fs-6`}
className={`nav-link ${activeTab === "notes" ? "active" : ""
} fs-6`}
onClick={(e) => handleTabClick("notes", e)}
>
<i className="bx bx-notepad bx-sm me-1_5"></i>
@ -150,9 +149,8 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
</li>
<li className="nav-item cursor-pointer">
<a
className={`nav-link ${
activeTab === "contacts" ? "active" : ""
} fs-6`}
className={`nav-link ${activeTab === "contacts" ? "active" : ""
} fs-6`}
onClick={(e) => handleTabClick("contacts", e)}
>
<i className="bx bxs-contact bx-sm me-1_5"></i>
@ -168,105 +166,84 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
{activeTab === "notes" && (
<div className="col-8 col-md-3">
<input
type="search"
className="form-control form-control-sm"
placeholder="Search notes..."
value={searchNote}
onChange={(e) => setSearchNote(e.target.value)}
/>
type="search"
className="form-control form-control-sm"
placeholder="Search notes..."
value={searchNote}
onChange={(e) => setSearchNote(e.target.value)}
/>
</div>
)}
{activeTab === "contacts" && (
<div className="d-flex align-items-center gap-3">
<div className="col-12 col-md-8 d-flex flex-row gap-2">
<div className="col-7 col-md-4">
<input
type="search"
className="form-control form-control-sm"
placeholder="Search contacts..."
value={searchContact}
onChange={(e) => setsearchContact(e.target.value)}
/>
</div>
<button
className={`btn btn-sm p-1 ${
!gridView ? "btn-primary" : "btn-outline-primary"
}`}
onClick={() => setGridView(false)}
>
<i className="bx bx-list-ul"></i>
</button>
<button
className={`btn btn-sm p-1 ${
gridView ? " btn-primary" : " btn-outline-primary"
}`}
<div className="col-7 col-md-4">
<input
type="search"
className="form-control form-control-sm"
placeholder="Search contacts..."
value={searchContact}
onChange={(e) => setsearchContact(e.target.value)}
/>
</div>
<button
className={`btn btn-sm p-1 ${gridView ? " btn-primary" : " btn-outline-primary"
}`}
onClick={() => setGridView(true)}
>
<i className="bx bx-grid-alt"></i>
</button>
<div className="form-check form-switch d-flex align-items-center d-none d-md-flex">
<input
type="checkbox"
className="form-check-input"
role="switch"
id="inactiveEmployeesCheckbox"
checked={showActive}
onChange={(e) => setShowActive(e.target.checked)}
/>
<label
className="form-check-label ms-2"
htmlFor="inactiveEmployeesCheckbox"
<button
className={`btn btn-sm p-1 ${!gridView ? "btn-primary" : "btn-outline-primary"
}`}
onClick={() => setGridView(false)}
>
{showActive ? "Active" : "Inactive"} Contacts
</label>
<i className="bx bx-list-ul"></i>
</button>
<div className="form-check form-switch d-flex align-items-end d-none d-md-flex">
<input
type="checkbox"
className="form-check-input"
role="switch"
id="inactiveEmployeesCheckbox"
checked={showActive}
onChange={(e) => setShowActive(e.target.checked)}
/>
<label
className="form-check-label ms-2"
htmlFor="inactiveEmployeesCheckbox"
>
{showActive ? "Active" : "In-active"} Contacts
</label>
</div>
</div>
</div>
</div>
)}
</div>
<div className="col-12 col-md-2 d-flex justify-content-end align-items-center gap-2">
<div className={`form-check form-switch d-flex align-items-center ${activeTab === "contacts" ? " d-flex d-md-none m-0":"d-none" }`}>
<input
type="checkbox"
className="form-check-input"
role="switch"
id="inactiveEmployeesCheckbox"
checked={showActive}
onChange={(e) => setShowActive(e.target.checked)}
/>
<label
className="form-check-label ms-2"
htmlFor="inactiveEmployeesCheckbox"
<div className="col-12 col-md-2 d-flex justify-content-end align-items-center gap-2">
<div className=" btn-group">
<button
className="btn btn-sm btn-label-secondary dropdown-toggle"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i className="bx bx-export me-2 bx-sm"></i>Export
</button>
<ul className="dropdown-menu">
<li>
<a
className="dropdown-item cursor-pointer"
onClick={() => handleExport("csv")}
>
{showActive ? "Active" : "Inactive"} Contacts
</label>
</div>
<div className=" btn-group">
<button
className="btn btn-sm btn-label-secondary dropdown-toggle"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i className="bx bx-export me-2 bx-sm"></i>Export
</button>
<ul className="dropdown-menu">
<li>
<a
className="dropdown-item cursor-pointer"
onClick={() => handleExport("csv")}
>
<i className="bx bx-file me-1"></i> CSV
</a>
</li>
</ul>
<i className="bx bx-file me-1"></i> CSV
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,5 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import React from "react";
import React, { useEffect } from "react";
import { FormProvider, useForm } from "react-hook-form";
import {
defaultNotesFilter,
@ -8,11 +8,18 @@ import {
import { useContactFilter, useNoteFilter } from "../../hooks/useDirectory";
import { ExpenseFilterSkeleton } from "../../components/Expenses/ExpenseSkeleton";
import SelectMultiple from "../../components/common/SelectMultiple";
import { useLocation } from "react-router-dom";
const NoteFilterPanel = ({ onApply, clearFilter }) => {
const { data, isError, isLoading, error, isFetched, isFetching } =
useNoteFilter();
useEffect(() => {
return () => {
closePanel();
};
}, []);
const methods = useForm({
resolver: zodResolver(notesFilter),
defaultValues: defaultNotesFilter,
@ -31,7 +38,7 @@ const NoteFilterPanel = ({ onApply, clearFilter }) => {
const handleClose = () => {
reset(defaultNotesFilter);
onApply(defaultNotesFilter);
onApply(defaultNotesFilter);
closePanel();
};

View File

@ -38,6 +38,7 @@ import usePagination from "../../hooks/usePagination";
import { setProjectId } from "../../slices/localVariablesSlice";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import Pagination from "../../components/common/Pagination";
import handleEmployeeExport from "../../components/Employee/handleEmployeeExport";
const EmployeeList = () => {
const selectedProjectId = useSelector(
@ -134,26 +135,11 @@ const EmployeeList = () => {
const tableRef = useRef(null);
const handleExport = (type) => {
if (!currentItems || currentItems.length === 0) return;
switch (type) {
case "csv":
exportToCSV(currentItems, "employees");
break;
case "excel":
exportToExcel(currentItems, "employees");
break;
case "pdf":
exportToPDF(currentItems, "employees");
break;
case "print":
printTable(tableRef.current);
break;
default:
break;
}
handleEmployeeExport(type, employeeList, filteredData, searchText, tableRef);
};
const handleAllEmployeesToggle = (e) => {
const isChecked = e.target.checked;
setShowInactive(false);
@ -176,12 +162,10 @@ const EmployeeList = () => {
useEffect(() => {
if (!loading && Array.isArray(employees)) {
const sorted = [...employees].sort((a, b) => {
const nameA = `${a.firstName || ""}${a.middleName || ""}${
a.lastName || ""
}`.toLowerCase();
const nameB = `${b.firstName || ""}${b.middleName || ""}${
b.lastName || ""
}`.toLowerCase();
const nameA = `${a.firstName || ""}${a.middleName || ""}${a.lastName || ""
}`.toLowerCase();
const nameB = `${b.firstName || ""}${b.middleName || ""}${b.lastName || ""
}`.toLowerCase();
return nameA?.localeCompare(nameB);
});
@ -258,9 +242,8 @@ const EmployeeList = () => {
? "Suspend Employee"
: "Reactivate Employee"
}
message={`Are you sure you want to ${
selectedEmpFordelete?.isActive ? "suspend" : "reactivate"
} this employee?`}
message={`Are you sure you want to ${selectedEmpFordelete?.isActive ? "suspend" : "reactivate"
} this employee?`}
onSubmit={(id) =>
suspendEmployee({
employeeId: id,
@ -309,7 +292,7 @@ const EmployeeList = () => {
className="form-check-label ms-0"
htmlFor="inactiveEmployeesCheckbox"
>
Show Inactive Employees
In-active Employees
</label>
</div>
</div>
@ -471,9 +454,8 @@ const EmployeeList = () => {
Status
</th>
<th
className={`sorting_disabled ${
!Manage_Employee && "d-none"
}`}
className={`sorting_disabled ${!Manage_Employee && "d-none"
}`}
rowSpan="1"
colSpan="1"
style={{ width: "50px" }}
@ -493,20 +475,20 @@ const EmployeeList = () => {
)}
{!loading &&
displayData?.length === 0 &&
(!searchText ) ? (
displayData?.length === 0 &&
(!searchText) ? (
<tr>
<td colSpan={8} className="border-0 py-3">
<div className="py-4">
<div className="py-4">
No Data Found
</div>
</div>
</td>
</tr>
) : null}
{!loading &&
displayData?.length === 0 &&
(searchText ) ? (
{!loading &&
displayData?.length === 0 &&
(searchText) ? (
<tr>
<td colSpan={8} className="border-0 py-3">
<div className="py-4">
@ -542,18 +524,17 @@ const EmployeeList = () => {
</div>
</div>
</td>
<td className="text-start d-none d-sm-table-cell">
<td className="text-start d-none d-sm-table-cell">
{item.email ? (
<span className="text-truncate">
<i className="bx bxs-envelope text-primary me-2"></i>
{item.email}
</span>
) : (
<span className="text-truncate text-italic">
-
</span>
<span className="d-block text-start text-muted fst-italic">NA</span>
)}
</td>
<td className="text-start d-none d-sm-table-cell">
<span className="text-truncate">
<i className="bx bxs-phone-call text-primary me-2"></i>
@ -567,9 +548,14 @@ const EmployeeList = () => {
</span>
</td>
<td className=" d-none d-md-table-cell">
{moment(item.joiningDate)?.format("DD-MMM-YYYY")}
<td className="d-none d-md-table-cell">
{item.joiningDate ? (
moment(item.joiningDate).format("DD-MMM-YYYY")
) : (
<span className="d-block text-center text-muted fst-italic">NA</span>
)}
</td>
<td>
{showInactive ? (
<span

View File

@ -77,7 +77,7 @@ const ProjectDetails = () => {
<AboutProject />
<ProjectOverview project={projectId} />
</div>
<div className="col-lg-8 col-md-7 mt-5">
<div className="col-lg-8 col-md-7 mt-2">
<ProjectProgressChart ShowAllProject="false" DefaultRange="1M" />
<div className="mt-5">
<AttendanceOverview />

View File

@ -40,112 +40,57 @@ 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") => {
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;
// Create a new PDF document
const pdfDoc = await PDFDocument.create();
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
// Set up the font
const font = await pdfDoc.embedFont(StandardFonts.Helvetica); // Use Helvetica font
// Calculate column widths dynamically based on data content
const headers = Object.keys(data[0]);
const rows = data.map(item => headers.map(header => item[header] || ''));
// Default options
const {
columnWidths = [], // array of widths per column
fontSizeHeader = 12,
fontSizeRow = 10,
rowHeight = 25,
} = options;
const getMaxColumnWidth = (columnIndex) => {
let maxWidth = font.widthOfTextAtSize(headers[columnIndex], 12);
rows.forEach(row => {
const cellText = row[columnIndex].toString();
maxWidth = Math.max(maxWidth, font.widthOfTextAtSize(cellText, 10));
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]);
// Draw headers
headers.forEach((header, i) => {
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 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 });
});
return maxWidth + 10; // Padding for better spacing
};
y -= rowHeight;
const columnWidths = headers.map((_, index) => getMaxColumnWidth(index));
const tableX = 30; // X-coordinate for the table start
const rowHeight = 20; // Height of each row (can be adjusted)
const maxPageHeight = 750; // Max available height for content (before a new page is added)
const pageMargin = 30; // Margin from the top of the page
let tableY = maxPageHeight; // Start Y position for the table
const maxPageWidth = 600; // Max available width for content (before a new page is added)
// Add the headers and rows to the page
const addHeadersToPage = (page, scaleFactor) => {
let xPosition = tableX;
headers.forEach((header, index) => {
page.drawText(header, {
x: xPosition,
y: tableY,
font,
size: 12 * scaleFactor, // Scale the header font size
color: rgb(0, 0, 0),
});
xPosition += columnWidths[index] * scaleFactor; // Adjust X position based on scaling
});
tableY -= rowHeight; // Move down after adding headers
};
// Add a new page and reset the table position
const addNewPage = (scaleFactor) => {
const page = pdfDoc.addPage([600, 800]);
tableY = maxPageHeight; // Reset Y position for the new page
addHeadersToPage(page, scaleFactor); // Re-add headers to the new page
return page;
};
// Create the first page and add headers
let page = pdfDoc.addPage([600, 800]);
// Check if the content fits within the page width, scale if necessary
const checkPageWidth = (row) => {
let totalWidth = columnWidths.reduce((acc, width) => acc + width, 0);
let scaleFactor = 1;
if (totalWidth > maxPageWidth) {
scaleFactor = maxPageWidth / totalWidth; // Scale down if necessary
if (y < margin) {
page = pdfDoc.addPage([pageWidth, pageHeight]);
y = pageHeight - margin;
}
return scaleFactor;
};
// Function to check for page breaks when adding a new row
const checkPageBreak = () => {
if (tableY - rowHeight < pageMargin) {
page = addNewPage(scaleFactor); // Add a new page if there is no space for the next row
}
};
// Add rows to the PDF with pagination and horizontal scaling
rows.forEach(row => {
checkPageBreak(); // Check for page break before adding each row
const scaleFactor = checkPageWidth(row); // Get the scaling factor for the row
// Add headers to the first page and each new page with the same scale factor
if (tableY === maxPageHeight) {
addHeadersToPage(page, scaleFactor); // Add headers only on the first page
}
let xPosition = tableX;
row.forEach((value, index) => {
page.drawText(value.toString(), {
x: xPosition,
y: tableY,
font,
size: 10 * scaleFactor, // Scale the font size
color: rgb(0, 0, 0),
});
xPosition += columnWidths[index] * scaleFactor; // Adjust X position based on scaling
});
tableY -= rowHeight; // Move down to the next row position
});
// Serialize the document to bytes
const pdfBytes = await pdfDoc.save();
// Trigger a download of the PDF
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
@ -153,15 +98,110 @@ export const exportToPDF = async (data, fileName = "data") => {
link.click();
};
/**
* 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
*/
export const printTable = (table) => {
if (table) {
const newWindow = window.open("", "", "width=600,height=600"); // Open a new window
const clone = table.cloneNode(true);
// Remove last column (Actions) from all rows
clone.querySelectorAll("tr").forEach((row) => {
row.removeChild(row.lastElementChild);
});
// Inject styles for the table and body
const newWindow = window.open("", "", "width=600,height=600");
newWindow.document.write("<html><head><title>Print Table</title>");
const style = document.createElement('style');
style.innerHTML = `
@ -171,16 +211,14 @@ export const printTable = (table) => {
th { background-color: #f2f2f2; }
`;
newWindow.document.head.appendChild(style);
newWindow.document.write("</head><body>");
newWindow.document.write(table.outerHTML); // Write the table HTML to the new window
newWindow.document.write(clone.outerHTML);
newWindow.document.write("</body></html>");
newWindow.document.close(); // Close the document stream
// Wait for the document to load before triggering print
newWindow.document.close();
newWindow.onload = () => {
newWindow.print(); // Trigger the print dialog after the content is loaded
newWindow.print();
};
}
};