Compare commits

..

10 Commits

18 changed files with 491 additions and 347 deletions

View File

@ -89,7 +89,7 @@
); );
--bs-root-font-size: 16px; --bs-root-font-size: 16px;
--bs-body-font-family: var(--bs-font-sans-serif); --bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 0.85rem; --bs-body-font-size: 0.875rem;
--bs-body-font-weight: 400; --bs-body-font-weight: 400;
--bs-body-line-height: 1.375; --bs-body-line-height: 1.375;
--bs-body-color: #646e78; --bs-body-color: #646e78;
@ -9060,7 +9060,7 @@ img[data-app-light-img][data-app-dark-img] {
} }
.table th { .table th {
color: var(--bs-heading-color); color: var(--bs-heading-color);
font-size: 0.8025rem; font-size: 0.8125rem;
letter-spacing: 0.2px; letter-spacing: 0.2px;
text-transform: uppercase; text-transform: uppercase;
} }
@ -20345,7 +20345,7 @@ li:not(:first-child) .dropdown-item,
} }
.fs-6 { .fs-6 {
font-size: 0.8375rem !important; font-size: 0.9375rem !important;
} }
.fs-tiny { .fs-tiny {

View File

@ -39,14 +39,12 @@ const TaskReportFilterPanel = ({ handleFilter }) => {
dateTo: localToUtc(formData.dateTo), dateTo: localToUtc(formData.dateTo),
}; };
handleFilter(filterPayload); handleFilter(filterPayload);
closePanel();
}; };
const onClear = () => { const onClear = () => {
setResetKey((prev) => prev + 1); setResetKey((prev) => prev + 1);
handleFilter(TaskReportDefaultValue); handleFilter(TaskReportDefaultValue);
reset(TaskReportDefaultValue); reset(TaskReportDefaultValue);
closePanel();
}; };
return ( return (
<FormProvider {...methods}> <FormProvider {...methods}>

View File

@ -204,24 +204,34 @@ const TaskReportList = () => {
<HoverPopup <HoverPopup
id="total_pending_task" id="total_pending_task"
title="Total Pending Task" title="Total Pending Task"
content={<p>This shows the total pending tasks for each activity on that date.</p>} content={
<div className="text-wrap" style={{ maxWidth: "200px" }}>
This shows the total pending tasks for each activity on that date.
</div>
}
> >
<i className="bx bx-xs ms-1 bx-info-circle cursor-pointer"></i> <i className="bx bx-xs ms-1 bx-info-circle cursor-pointer"></i>
</HoverPopup> </HoverPopup>
</span> </span>
</th> </th>
<th> <th>
<span> <span>
Reported/Planned{" "} Reported/Planned{" "}
<HoverPopup <HoverPopup
id="reportes_and_planned_task" id="reportes_and_planned_task"
title="Reported and Planned Task" title="Reported and Planned Task"
content={<p>This shows the reported versus planned tasks for each activity on that date.</p>} content={
<div className="text-wrap" style={{ maxWidth: "200px" }}>
This shows the reported versus planned tasks for each activity on that date.
</div>
}
> >
<i className="bx bx-xs ms-1 bx-info-circle cursor-pointer"></i> <i className="bx bx-xs ms-1 bx-info-circle cursor-pointer"></i>
</HoverPopup> </HoverPopup>
</span> </span>
</th> </th>
<th>Assign Date</th> <th>Assign Date</th>
<th>Team</th> <th>Team</th>
<th className="text-center">Actions</th> <th className="text-center">Actions</th>

View File

@ -100,7 +100,7 @@ const AttendanceOverview = () => {
}; };
return ( return (
<div className="bg-white px-4 rounded shadow d-flex flex-column h-100"> <div className="bg-white px-4 rounded shadow d-flex flex-column h-100" style={{ minHeight: "450px" }}>
{/* Header */} {/* Header */}
<div className="d-flex mt-2 justify-content-between align-items-center mb-3"> <div className="d-flex mt-2 justify-content-between align-items-center mb-3">
<div className="card-title mb-0 text-start"> <div className="card-title mb-0 text-start">

View File

@ -17,12 +17,19 @@ import ExpenseAnalysis from "./ExpenseAnalysis";
import ExpenseStatus from "./ExpenseStatus"; import ExpenseStatus from "./ExpenseStatus";
import ExpenseByProject from "./ExpenseByProject"; import ExpenseByProject from "./ExpenseByProject";
import ProjectStatistics from "../Project/ProjectStatistics"; import ProjectStatistics from "../Project/ProjectStatistics";
import ServiceJobs from "./ServiceJobs";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { REGULARIZE_ATTENDANCE, SELF_ATTENDANCE, TEAM_ATTENDANCE } from "../../utils/constants";
const Dashboard = () => { const Dashboard = () => {
// Get the selected project ID from Redux store // Get the selected project ID from Redux store
const projectId = useSelector((store) => store.localVariables.projectId); const projectId = useSelector((store) => store.localVariables.projectId);
const isAllProjectsSelected = projectId === null; const isAllProjectsSelected = projectId === null;
const canRegularize = useHasUserPermission(REGULARIZE_ATTENDANCE);
const canTeamAttendance = useHasUserPermission(TEAM_ATTENDANCE);
const canSelfAttendance = useHasUserPermission(SELF_ATTENDANCE);
return ( return (
<div className="container-fluid mt-5"> <div className="container-fluid mt-5">
@ -49,7 +56,7 @@ const Dashboard = () => {
<div className="col-xxl-6 col-lg-6"> <div className="col-xxl-6 col-lg-6">
<ProjectProgressChart /> <ProjectProgressChart />
</div> </div>
{!isAllProjectsSelected && ( {!isAllProjectsSelected && (canRegularize || canTeamAttendance || canSelfAttendance) && (
<div className="col-12 col-md-6 mb-sm-0 mb-4"> <div className="col-12 col-md-6 mb-sm-0 mb-4">
<AttendanceOverview /> <AttendanceOverview />
</div> </div>
@ -57,8 +64,10 @@ const Dashboard = () => {
{!isAllProjectsSelected && ( {!isAllProjectsSelected && (
<div className="col-xxl-4 col-lg-4"> <div className="col-xxl-4 col-lg-4">
<div className="card h-100">
<ProjectStatistics /> <ProjectStatistics />
</div> </div>
</div>
)} )}
<div className="col-12 col-xl-4 col-md-6"> <div className="col-12 col-xl-4 col-md-6">
<div className="card "> <div className="card ">
@ -74,6 +83,9 @@ const Dashboard = () => {
<div className="col-12 col-md-6"> <div className="col-12 col-md-6">
<ExpenseByProject /> <ExpenseByProject />
</div> </div>
{/* <div className="col-12 col-md-12">
<ServiceJobs />
</div> */}
</div> </div>
</div> </div>
); );

View File

@ -0,0 +1,237 @@
import React from "react";
const ServiceJobs = () => {
return (
<div className="col-xxl-4 col-lg-6">
<div className="card h-100">
<div className="card-header d-flex justify-content-between">
<div className="card-title mb-0 text-start">
<h5 className="mb-1">Service Jobs</h5>
<p className="card-subtitle">All Projects</p>
</div>
</div>
<div className="card-body p-0">
<div className="nav-align-top">
{/* Tabs */}
<ul className="nav nav-tabs nav-fill rounded-0 timeline-indicator-advanced" role="tablist">
<li className="nav-item">
<button className="nav-link active" data-bs-toggle="tab" data-bs-target="#tab-new">
My Jobs
</button>
</li>
<li className="nav-item">
<button className="nav-link" data-bs-toggle="tab" data-bs-target="#tab-preparing">
Assigned
</button>
</li>
<li className="nav-item">
<button className="nav-link" data-bs-toggle="tab" data-bs-target="#tab-shipping">
In Progress
</button>
</li>
</ul>
{/* Tab Content */}
<div className="tab-content border-0 mx-1 text-start">
{/* ---------------------- NEW TAB ---------------------- */}
<div className="tab-pane fade show active" id="tab-new">
{/* Entry 1 */}
<ul className="timeline mb-0">
<li className="timeline-item ps-6 border-left-dashed">
<span className="timeline-indicator-advanced timeline-indicator-success border-0 shadow-none">
<i className="bx bx-check-circle"></i>
</span>
<div className="timeline-event ps-1">
<div className="timeline-header">
<small className="text-success text-uppercase">Sender</small>
</div>
<h6 className="my-50">Myrtle Ullrich</h6>
<p className="text-body mb-0">101 Boulder, California(CA), 95959</p>
</div>
</li>
<li className="timeline-item ps-6 border-transparent">
<span className="timeline-indicator-advanced timeline-indicator-primary border-0 shadow-none">
<i className="bx bx-map"></i>
</span>
<div className="timeline-event ps-1">
<div className="timeline-header">
<small className="text-primary text-uppercase">Receiver</small>
</div>
<h6 className="my-50">Barry Schowalter</h6>
<p className="text-body mb-0">939 Orange, California(CA), 92118</p>
</div>
</li>
</ul>
<div className="border-1 border-light border-top border-dashed my-4"></div>
{/* Entry 2 */}
<ul className="timeline mb-0">
<li className="timeline-item ps-6 border-left-dashed">
<span className="timeline-indicator-advanced timeline-indicator-success border-0 shadow-none">
<i className="bx bx-check-circle"></i>
</span>
<div className="timeline-event ps-1">
<div className="timeline-header">
<small className="text-success text-uppercase">Sender</small>
</div>
<h6 className="my-50">Veronica Herman</h6>
<p className="text-body mb-0">162 Windsor, California(CA), 95492</p>
</div>
</li>
<li className="timeline-item ps-6 border-transparent">
<span className="timeline-indicator-advanced timeline-indicator-primary border-0 shadow-none">
<i className="bx bx-map"></i>
</span>
<div className="timeline-event ps-1">
<div className="timeline-header">
<small className="text-primary text-uppercase">Receiver</small>
</div>
<h6 className="my-50">Helen Jacobs</h6>
<p className="text-body mb-0">487 Sunset, California(CA), 94043</p>
</div>
</li>
</ul>
</div>
{/* ---------------------- PREPARING TAB ---------------------- */}
<div className="tab-pane fade" id="tab-preparing">
{/* Entry 1 */}
<ul className="timeline mb-0">
<li className="timeline-item ps-6 border-left-dashed">
<span className="timeline-indicator-advanced timeline-indicator-warning border-0 shadow-none">
<i className="bx bx-time-five"></i>
</span>
<div className="timeline-event ps-1">
<div className="timeline-header">
<small className="text-warning text-uppercase">Sender</small>
</div>
<h6 className="my-50">Oliver Grant</h6>
<p className="text-body mb-0">220 Pine St, California(CA), 95765</p>
</div>
</li>
<li className="timeline-item ps-6 border-transparent">
<span className="timeline-indicator-advanced timeline-indicator-primary border-0 shadow-none">
<i className="bx bx-map"></i>
</span>
<div className="timeline-event ps-1">
<div className="timeline-header">
<small className="text-primary text-uppercase">Receiver</small>
</div>
<h6 className="my-50">Samantha Lee</h6>
<p className="text-body mb-0">744 Bay Area, California(CA), 94016</p>
</div>
</li>
</ul>
<div className="border-1 border-light border-top border-dashed my-4"></div>
{/* Entry 2 */}
<ul className="timeline mb-0">
<li className="timeline-item ps-6 border-left-dashed">
<span className="timeline-indicator-advanced timeline-indicator-warning border-0 shadow-none">
<i className="bx bx-time-five"></i>
</span>
<div className="timeline-event ps-1">
<div className="timeline-header">
<small className="text-warning text-uppercase">Sender</small>
</div>
<h6 className="my-50">Marcus Howard</h6>
<p className="text-body mb-0">58 Avenue, California(CA), 95376</p>
</div>
</li>
<li className="timeline-item ps-6 border-transparent">
<span className="timeline-indicator-advanced timeline-indicator-primary border-0 shadow-none">
<i className="bx bx-map"></i>
</span>
<div className="timeline-event ps-1">
<div className="timeline-header">
<small className="text-primary text-uppercase">Receiver</small>
</div>
<h6 className="my-50">Daniel Foster</h6>
<p className="text-body mb-0">312 Marina, California(CA), 94109</p>
</div>
</li>
</ul>
</div>
{/* ---------------------- SHIPPING TAB ---------------------- */}
<div className="tab-pane fade" id="tab-shipping">
{/* Entry 1 */}
<ul className="timeline mb-0">
<li className="timeline-item ps-6 border-left-dashed">
<span className="timeline-indicator-advanced timeline-indicator-info border-0 shadow-none">
<i className="bx bx-package"></i>
</span>
<div className="timeline-event ps-1">
<div className="timeline-header">
<small className="text-info text-uppercase">Sender</small>
</div>
<h6 className="my-50">James Carter</h6>
<p className="text-body mb-0">441 Market St, California(CA), 94111</p>
</div>
</li>
<li className="timeline-item ps-6 border-transparent">
<span className="timeline-indicator-advanced timeline-indicator-primary border-0 shadow-none">
<i className="bx bx-map"></i>
</span>
<div className="timeline-event ps-1">
<div className="timeline-header">
<small className="text-primary text-uppercase">Receiver</small>
</div>
<h6 className="my-50">Linda Moore</h6>
<p className="text-body mb-0">990 Willow Road, California(CA), 94025</p>
</div>
</li>
</ul>
<div className="border-1 border-light border-top border-dashed my-4"></div>
{/* Entry 2 */}
<ul className="timeline mb-0">
<li className="timeline-item ps-6 border-left-dashed">
<span className="timeline-indicator-advanced timeline-indicator-info border-0 shadow-none">
<i className="bx bx-package"></i>
</span>
<div className="timeline-event ps-1">
<div className="timeline-header">
<small className="text-info text-uppercase">Sender</small>
</div>
<h6 className="my-50">Sarah Bennett</h6>
<p className="text-body mb-0">882 Canyon Rd, California(CA), 94704</p>
</div>
</li>
<li className="timeline-item ps-6 border-transparent">
<span className="timeline-indicator-advanced timeline-indicator-primary border-0 shadow-none">
<i className="bx bx-map"></i>
</span>
<div className="timeline-event ps-1">
<div className="timeline-header">
<small className="text-primary text-uppercase">Receiver</small>
</div>
<h6 className="my-50">George Simmons</h6>
<p className="text-body mb-0">19 Palm St, California(CA), 93001</p>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default ServiceJobs;

View File

@ -1,48 +1,24 @@
import { useState } from "react"; import { useState } from "react";
const PreviewDocument = ({ imageUrl }) => { const PreviewDocument = ({ imageUrl }) => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [rotation, setRotation] = useState(0); const [rotation, setRotation] = useState(0);
const [scale, setScale] = useState(1);
const zoomIn = () => setScale((prev) => Math.min(prev + 0.2, 3));
const zoomOut = () => setScale((prev) => Math.max(prev - 0.2, 0.4));
const resetAll = () => {
setRotation(0);
setScale(1);
};
return ( return (
<> <>
<div className="d-flex justify-content-start gap-3 mb-2"> <div className="d-flex justify-content-start">
<i <i
className="bx bx-rotate-right cursor-pointer fs-4" className="bx bx-rotate-right cursor-pointer"
title="Rotate"
onClick={() => setRotation((prev) => prev + 90)} onClick={() => setRotation((prev) => prev + 90)}
></i> ></i>
<i
className="bx bx-zoom-in cursor-pointer fs-4"
title="Zoom In"
onClick={zoomIn}
></i>
<i
className="bx bx-zoom-out cursor-pointer fs-4"
title="Zoom Out"
onClick={zoomOut}
></i>
</div> </div>
<div <div
className="position-relative d-flex flex-column justify-content-center align-items-center overflow-hidden" className="position-relative d-flex flex-column justify-content-center align-items-center"
style={{ minHeight: "80vh" }} style={{ minHeight: "80vh" }}
> >
{loading && ( {loading && (
<div className="text-secondary text-center mb-2"> <div className="text-secondary text-center mb-2">Loading...</div>
Loading...
</div>
)} )}
<div className="mb-3 d-flex justify-content-center align-items-center"> <div className="mb-3 d-flex justify-content-center align-items-center">
@ -54,18 +30,18 @@ const PreviewDocument = ({ imageUrl }) => {
maxHeight: "80vh", maxHeight: "80vh",
objectFit: "contain", objectFit: "contain",
display: loading ? "none" : "block", display: loading ? "none" : "block",
transform: `rotate(${rotation}deg) scale(${scale})`, transform: `rotate(${rotation}deg)`,
transition: "transform 0.3s ease", transition: "transform 0.3s ease",
cursor: "grab",
}} }}
onLoad={() => setLoading(false)} onLoad={() => setLoading(false)}
/> />
</div> </div>
<div className="position-absolute bottom-0 start-0 m-2"> <div className="position-absolute bottom-0 start-0 justify-content-center gap-2">
<button <button
className="btn btn-outline-secondary" className="btn btn-outline-secondary"
onClick={resetAll} onClick={() => setRotation(0)}
title="Reset Rotation"
> >
<i className="bx bx-reset"></i> Reset <i className="bx bx-reset"></i> Reset
</button> </button>
@ -75,6 +51,4 @@ const PreviewDocument = ({ imageUrl }) => {
); );
}; };
export default PreviewDocument; export default PreviewDocument;

View File

@ -25,7 +25,9 @@ const Sidebar = () => {
/> />
</span> */} </span> */}
<small className="app-brand-link fw-bold navbar-brand text-green fs-6"> <small
className="app-brand-link fw-bold navbar-brand text-green fs-6"
>
<span className="app-brand-logo demo"> <span className="app-brand-logo demo">
<img src="/img/brand/marco.png" width="50" /> <img src="/img/brand/marco.png" width="50" />
</span> </span>
@ -36,6 +38,7 @@ const Sidebar = () => {
</Link> </Link>
<small className="layout-menu-toggle menu-link text-large ms-auto"> <small className="layout-menu-toggle menu-link text-large ms-auto">
<i className="bx bx-chevron-left bx-sm d-flex align-items-center justify-content-center"></i> <i className="bx bx-chevron-left bx-sm d-flex align-items-center justify-content-center"></i>
</small> </small>
</div> </div>
@ -58,7 +61,7 @@ const Sidebar = () => {
</> </>
)} )}
{data && {data &&
data?.data?.map((section) => ( data?.data.map((section) => (
<React.Fragment <React.Fragment
key={section.id || section.header || section.items[0]?.id} key={section.id || section.header || section.items[0]?.id}
> >

View File

@ -165,7 +165,7 @@ const ProjectStatistics = ({ project }) => {
}, [selectedProject]); }, [selectedProject]);
return ( return (
<div className="card h-100"> <>
<div className="card-header text-start"> <div className="card-header text-start">
<h5 className="card-action-title mb-0"> <h5 className="card-action-title mb-0">
{" "} {" "}
@ -242,8 +242,8 @@ const ProjectStatistics = ({ project }) => {
</li> </li>
</ul> </ul>
</div> </div>
</>
</div>
); );
}; };

View File

@ -10,13 +10,11 @@ import {
import useMaster, { useServices } from "../../../hooks/masterHook/useMaster"; import useMaster, { useServices } from "../../../hooks/masterHook/useMaster";
import showToast from "../../../services/toastService"; import showToast from "../../../services/toastService";
import { useOrganizationEmployees } from "../../../hooks/useOrganization"; import { useOrganizationEmployees } from "../../../hooks/useOrganization";
import { useDispatch } from "react-redux";
import { changeMaster } from "../../../slices/localVariablesSlice";
const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => { const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => {
const selectedProject = useSelectedProject(); const selectedProject = useSelectedProject();
const debounceSearchTerm = useDebounce(searchTerm, 500); const debounceSearchTerm = useDebounce(searchTerm, 500);
const dispatch = useDispatch();
const { const {
data: employeesData = [], data: employeesData = [],
isLoading, isLoading,
@ -47,7 +45,6 @@ const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => {
}); });
useEffect(() => { useEffect(() => {
dispatch(changeMaster("Job Role"));
if (employeesData?.data?.length > 0) { if (employeesData?.data?.length > 0) {
const available = employeesData.data.filter((emp) => { const available = employeesData.data.filter((emp) => {
const projEmp = projectEmployees.find((pe) => pe.employeeId === emp.id); const projEmp = projectEmployees.find((pe) => pe.employeeId === emp.id);
@ -122,7 +119,7 @@ const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => {
status: true, status: true,
})); }));
handleAssignEmployee({ payload, actionType: "assign" }); handleAssignEmployee({ payload,actionType:"assign"} );
setEmployees((prev) => setEmployees((prev) =>
prev.map((emp) => ({ prev.map((emp) => ({
@ -135,11 +132,11 @@ const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => {
); );
}; };
if (isLoading) { if (isLoading) {
return (<div className="page-min-h d-flex justify-content-center align-items-center "><p className="text-muted">Loading employees...</p></div>); return ( <div className="page-min-h d-flex justify-content-center align-items-center "><p className="text-muted">Loading employees...</p></div>) ;
} }
if (isError) { if (isError) {
return ( return (
<div className="page-min-h d-flex justify-content-center align-items-center "> <div className="page-min-h d-flex justify-content-center align-items-center ">
{error?.status === 400 ? ( {error?.status === 400 ? (
@ -147,14 +144,14 @@ const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => {
) : ( ) : (
<p className="m-0 dange-text">Something went wrong. Please try again later.</p> <p className="m-0 dange-text">Something went wrong. Please try again later.</p>
)} )}
</div> </div>
); );
} }
if (employees.length === 0) { if (employees.length === 0) {
return (<div className="page-min-h d-flex justify-content-center align-items-center "><p className="text-muted">No available employees to assign.</p></div>); return(<div className="page-min-h d-flex justify-content-center align-items-center "><p className="text-muted">No available employees to assign.</p></div>) ;
} }
return ( return (
@ -186,7 +183,8 @@ const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => {
onChange={(e) => onChange={(e) =>
handleSelectChange(index, "serviceId", e.target.value) handleSelectChange(index, "serviceId", e.target.value)
} }
className={`form-select form-select-sm w-auto border-none rounded-0 py-1 px-auto ${emp.errors.serviceId ? "is-invalid" : "" className={`form-select form-select-sm w-auto border-none rounded-0 py-1 px-auto ${
emp.errors.serviceId ? "is-invalid" : ""
}`} }`}
> >
<option value="">Select Service</option> <option value="">Select Service</option>
@ -207,7 +205,8 @@ const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => {
onChange={(e) => onChange={(e) =>
handleSelectChange(index, "jobRole", e.target.value) handleSelectChange(index, "jobRole", e.target.value)
} }
className={`form-select form-select-sm w-auto border-none rounded-0 py-1 px-auto ${emp.errors.jobRole ? "is-invalid" : "" className={`form-select form-select-sm w-auto border-none rounded-0 py-1 px-auto ${
emp.errors.jobRole ? "is-invalid" : ""
}`} }`}
> >
<option value="">Select Job Role</option> <option value="">Select Job Role</option>

View File

@ -252,16 +252,15 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
<HoverPopup <HoverPopup
title="Payment Type" title="Payment Type"
id="payment_type" id="payment_type"
content={ content={
<div className=" w-50"> <div className="text-wrap" style={{ maxWidth: "200px" }}>
<p>
Choose whether the payment amount varies or remains fixed Choose whether the payment amount varies or remains fixed
each cycle. each cycle.
<br /> <br />
<strong>Is Variable:</strong> Amount changes per cycle. <strong>Is Variable:</strong> Amount changes per cycle.
<br /> <br />
<strong>Fixed:</strong> Amount stays constant. <strong>Fixed:</strong> Amount stays constant.
</p>
</div> </div>
} }
> >
@ -476,10 +475,10 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
title="Payment Buffer Days" title="Payment Buffer Days"
id="payment_buffer_days" id="payment_buffer_days"
content={ content={
<p> <div className="text-wrap" style={{ maxWidth: "200px" }}>
Number of extra days allowed after the due date before Number of extra days allowed after the due date before
payment is considered late. payment is considered late.
</p> </div>
} }
> >
<i className="bx bx-info-circle bx-xs text-muted cursor-pointer"></i> <i className="bx bx-info-circle bx-xs text-muted cursor-pointer"></i>
@ -511,10 +510,11 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
<HoverPopup <HoverPopup
title="End Date" title="End Date"
id="end_date" id="end_date"
content={ content={
<p> <div className="text-wrap" style={{ maxWidth: "200px" }}>
The date when the last payment in the recurrence occurs. The date when the last payment in the recurrence occurs.
</p> </div>
} }
> >
<i className="bx bx-info-circle bx-xs text-muted cursor-pointer"></i> <i className="bx bx-info-circle bx-xs text-muted cursor-pointer"></i>

View File

@ -66,10 +66,19 @@ const Jobs = () => {
<div className="col-12 col-md-6 text-start"> <div className="col-12 col-md-6 text-start">
<button <button
type="button" type="button"
className={`btn btn-sm ${showArchive ? "btn-secondary" : "btn-outline-secondary"}`} className={`btn btn-sm ${showArchive ? "btn-primary" : "btn-outline-secondary"}`}
onClick={() => setShowArchive(!showArchive)} onClick={() => setShowArchive(!showArchive)}
style={{ fontSize: "13px" }}
> >
<i className="bx bx-archive bx-sm me-1 mt-1"></i> Archived {showArchive ? (
<>
<i className="bx bx-list-ul me-1 mt-1"></i> Show Active
</>
) : (
<>
<i className="bx bx-archive me-1 mt-1"></i> Show Archived
</>
)}
</button> </button>
</div> </div>

View File

@ -89,7 +89,7 @@ const ServiceProjectCard = ({ project, isCore = true }) => {
> >
{project?.shortName ? project?.shortName : project?.name} {project?.shortName ? project?.shortName : project?.name}
</h5> </h5>
<div className="client-info text-body text-start"> <div className="client-info text-body">
<span>{project?.shortName ? project?.name : ""}</span> <span>{project?.shortName ? project?.name : ""}</span>
</div> </div>
</div> </div>

View File

@ -58,117 +58,43 @@ const HoverPopup = ({
return () => document.removeEventListener("click", handler); return () => document.removeEventListener("click", handler);
}, [Mode, visible, dispatch, id]); }, [Mode, visible, dispatch, id]);
// Positioning effect: respects align prop and stays inside boundary (drawer)
useEffect(() => { useEffect(() => {
if (!visible || !popupRef.current || !triggerRef.current) return; if (!visible || !popupRef.current || !triggerRef.current) return;
// run in next frame so DOM/layout settles
requestAnimationFrame(() => { requestAnimationFrame(() => {
const popup = popupRef.current; const popup = popupRef.current;
const boundaryEl = (boundaryRef && boundaryRef.current) || popup.parentElement;
// choose boundary: provided boundaryRef or nearest positioned parent (popup.parentElement)
const boundaryEl =
(boundaryRef && boundaryRef.current) || popup.parentElement;
if (!boundaryEl) return; if (!boundaryEl) return;
const boundaryRect = boundaryEl.getBoundingClientRect(); const boundaryRect = boundaryEl.getBoundingClientRect();
const triggerRect = triggerRef.current.getBoundingClientRect(); const triggerRect = triggerRef.current.getBoundingClientRect();
// reset styles first
popup.style.left = ""; popup.style.left = "";
popup.style.right = ""; popup.style.right = "";
popup.style.transform = ""; popup.style.transform = "";
popup.style.top = ""; popup.style.top = "";
const popupRect = popup.getBoundingClientRect(); const popupRect = popup.getBoundingClientRect();
const parentRect = boundaryRect; // alias const triggerCenterX = triggerRect.left + triggerRect.width / 2 - boundaryRect.left;
let left = triggerCenterX - popupRect.width / 2;
// Convert trigger center to parent coordinates // Clamp to boundaries
const triggerCenterX = left = Math.max(0, Math.min(left, boundaryRect.width - popupRect.width));
triggerRect.left + triggerRect.width / 2 - parentRect.left; popup.style.left = `${left}px`;
// preferred left so popup center aligns to trigger center:
const preferredLeft = triggerCenterX - popupRect.width / 2;
// Helpers to set styles in parent's coordinate system:
const setLeft = (leftPx) => {
popup.style.left = `${leftPx}px`;
popup.style.right = "auto";
popup.style.transform = "none";
};
const setRight = (rightPx) => {
popup.style.left = "auto";
popup.style.right = `${rightPx}px`;
popup.style.transform = "none";
};
// If user forced align:
if (align === "left") {
// align popup's left to parent's left (0)
setLeft(0);
return;
}
if (align === "right") {
// align popup's right to parent's right (0)
setRight(0);
return;
}
if (align === "center") {
popup.style.left = "50%";
popup.style.right = "auto";
popup.style.transform = "translateX(-50%)";
return;
}
// align === "auto": try preferred centered position, but flip fully if overflow
// clamp preferredLeft to boundaries so it doesn't render partially outside
const leftIfCentered = Math.max(
0,
Math.min(preferredLeft, parentRect.width - popupRect.width)
);
// if centered fits, use it
if (leftIfCentered === preferredLeft) {
setLeft(leftIfCentered);
return;
}
// if centering would overflow right -> stick popup fully to left (left=0)
if (preferredLeft > parentRect.width - popupRect.width) {
// place popup so its right aligns to parent's right
// i.e., left = parent width - popup width
setLeft(parentRect.width - popupRect.width);
return;
}
// if centering would overflow left -> stick popup fully to left=0
if (preferredLeft < 0) {
setLeft(0);
return;
}
// fallback center
setLeft(leftIfCentered);
}); });
}, [visible, align, boundaryRef]); }, [visible, align, boundaryRef]);
return ( return (
<div <div
className="d-inline-block position-relative" // <-- ADD THIS !! className="d-inline-block position-relative" // <-- ADD THIS !!
style={{ style={{ overflow: "visible" }}
maxWidth: "calc(700px - 100px)",
width: "100%",
wordWrap: "break-word",
overflow: "visible", // also make sure popup isn't clipped
}}
> >
<div <div
className="d-inline-block"
ref={triggerRef} ref={triggerRef}
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
onClick={handleClick} onClick={handleClick}
style={{ cursor: "pointer" }} style={{ cursor: "pointer", display: "inline-block" }}
> >
{children} {children}
</div> </div>
@ -180,8 +106,9 @@ const HoverPopup = ({
style={{ style={{
zIndex: 2000, zIndex: 2000,
top: "100%", top: "100%",
width: "max-content", minWidth: "200px",
minWidth: "120px", maxWidth: "300px",
wordWrap: "break-word",
}} }}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
@ -190,7 +117,7 @@ const HoverPopup = ({
</div> </div>
)} )}
</div> </div>
); );
}; };

View File

@ -149,6 +149,11 @@ export const useAllocationServiceProjectTeam = (onSuccessCallback) => {
//#endregion //#endregion
//#region Service Jobs //#region Service Jobs
export const useServiceProjectJobs = ( export const useServiceProjectJobs = (
@ -159,14 +164,7 @@ export const useServiceProjectJobs = (
isArchive isArchive
) => { ) => {
return useQuery({ return useQuery({
queryKey: [ queryKey: ["serviceProjectJobs", pageSize, pageNumber, isActive, project, isArchive],
"serviceProjectJobs",
pageSize,
pageNumber,
isActive,
project,
isArchive,
],
queryFn: async () => { queryFn: async () => {
const resp = await ServiceProjectRepository.GetJobList( const resp = await ServiceProjectRepository.GetJobList(
pageSize, pageSize,
@ -287,6 +285,7 @@ export const useUpdateServiceProjectJob = (onSuccessCallback) => {
} }
}, },
onError: (error) => { onError: (error) => {
showToast( showToast(
error?.response?.data?.message || error?.response?.data?.message ||
@ -298,8 +297,15 @@ export const useUpdateServiceProjectJob = (onSuccessCallback) => {
}); });
}; };
//#endregion //#endregion
//#region Branch //#region Branch
export const useBranches = ( export const useBranches = (
projectId, projectId,
@ -338,8 +344,8 @@ export const useBranchTypes = () => {
const resp = await ServiceProjectRepository.GetBranchTypeList(); const resp = await ServiceProjectRepository.GetBranchTypeList();
return resp.data; return resp.data;
}, },
}); })
}; }
export const useBranchDetails = (id) => { export const useBranchDetails = (id) => {
return useQuery({ return useQuery({
@ -348,9 +354,9 @@ export const useBranchDetails = (id) => {
const resp = await ServiceProjectRepository.GetBranchDetail(id); const resp = await ServiceProjectRepository.GetBranchDetail(id);
return resp.data; return resp.data;
}, },
enabled: !!id, enabled: !!id
}); })
}; }
export const useCreateBranch = (onSuccessCallBack) => { export const useCreateBranch = (onSuccessCallBack) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@ -396,6 +402,7 @@ export const useUpdateBranch = (onSuccessCallBack) => {
}); });
}; };
export const useDeleteBranch = () => { export const useDeleteBranch = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@ -405,10 +412,7 @@ export const useDeleteBranch = () => {
onSuccess: (_, variable) => { onSuccess: (_, variable) => {
queryClient.invalidateQueries({ queryKey: ["branches"] }); queryClient.invalidateQueries({ queryKey: ["branches"] });
showToast( showToast(`Branch ${variable.isActive ? "restored" : "deleted"} successfully`, "success");
`Branch ${variable.isActive ? "restored" : "deleted"} successfully`,
"success"
);
}, },
onError: (error) => { onError: (error) => {

View File

@ -24,6 +24,9 @@ import { useProjectAccess } from "../../hooks/useProjectAccess";
import "./ProjectDetails.css"; import "./ProjectDetails.css";
import ProjectOrganizations from "../../components/Project/ProjectOrganizations"; import ProjectOrganizations from "../../components/Project/ProjectOrganizations";
import ProjectStatistics from "../../components/Project/ProjectStatistics";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { REGULARIZE_ATTENDANCE, SELF_ATTENDANCE, TEAM_ATTENDANCE } from "../../utils/constants";
const ProjectDetails = () => { const ProjectDetails = () => {
const projectId = useSelectedProject(); const projectId = useSelectedProject();
@ -34,6 +37,10 @@ const ProjectDetails = () => {
useProjectDetails(projectId); useProjectDetails(projectId);
const { canView, loading: permsLoading } = useProjectAccess(projectId); const { canView, loading: permsLoading } = useProjectAccess(projectId);
const canRegularize = useHasUserPermission(REGULARIZE_ATTENDANCE);
const canTeamAttendance = useHasUserPermission(TEAM_ATTENDANCE);
const canSelfAttendance = useHasUserPermission(SELF_ATTENDANCE);
useEffect(() => { useEffect(() => {
if (!projectId && projectNames.length > 0) { if (!projectId && projectNames.length > 0) {
@ -82,13 +89,16 @@ const ProjectDetails = () => {
<div className="row"> <div className="row">
<div className="col-lg-4 col-md-5 mt-2"> <div className="col-lg-4 col-md-5 mt-2">
<AboutProject /> <AboutProject />
<ProjectOverview project={projectId} /> <div className="card"><ProjectStatistics project={projectId} /></div>
</div> </div>
<div className="col-lg-8 col-md-7 mt-2"> <div className="col-lg-8 col-md-7 mt-2">
<ProjectProgressChart ShowAllProject="false" DefaultRange="1M" /> <ProjectProgressChart ShowAllProject="false" DefaultRange="1M" />
{(canRegularize || canTeamAttendance || canSelfAttendance) && (
<div className="mt-5"> <div className="mt-5">
<AttendanceOverview /> <AttendanceOverview />
</div> </div>
)}
</div> </div>
</div> </div>
); );

View File

@ -1,4 +1,4 @@
import React, { createContext, useContext, useEffect, useRef, useState } from "react"; import React, { createContext, useContext, useEffect, useState } from "react";
import Breadcrumb from "../../components/common/Breadcrumb"; import Breadcrumb from "../../components/common/Breadcrumb";
import { import {
ITEMS_PER_PAGE, ITEMS_PER_PAGE,
@ -36,7 +36,7 @@ const ProjectPage = () => {
isOpen: false, isOpen: false,
project: null, project: null,
}); });
const dropdownRef = useRef(null);
const [projectList, setProjectList] = useState([]); const [projectList, setProjectList] = useState([]);
const [listView, setListView] = useState(false); const [listView, setListView] = useState(false);
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
@ -46,7 +46,6 @@ const ProjectPage = () => {
}); });
const HasManageProject = useHasUserPermission(MANAGE_PROJECT); const HasManageProject = useHasUserPermission(MANAGE_PROJECT);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [open, setOpen] = useState(false);
const [selectedStatuses, setSelectedStatuses] = useState( const [selectedStatuses, setSelectedStatuses] = useState(
PROJECT_STATUS.map((s) => s.id) PROJECT_STATUS.map((s) => s.id)
@ -71,16 +70,6 @@ const ProjectPage = () => {
sessionStorage.setItem("whichProjectDisplay", String(value)); sessionStorage.setItem("whichProjectDisplay", String(value));
}; };
useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
return ( return (
<ProjectContext.Provider value={contextDispatcher}> <ProjectContext.Provider value={contextDispatcher}>
<div className="container-fluid"> <div className="container-fluid">
@ -100,7 +89,8 @@ const ProjectPage = () => {
{/* Service Project Button */} {/* Service Project Button */}
<button <button
type="button" type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${!coreProjects ? "btn-primary text-white" : "" className={`btn px-2 py-1 rounded-0 text-tiny ${
!coreProjects ? "btn-primary text-white" : ""
}`} }`}
onClick={() => handleToggleProject(false)} onClick={() => handleToggleProject(false)}
> >
@ -109,7 +99,8 @@ const ProjectPage = () => {
{/* Organization Project Button */} {/* Organization Project Button */}
<button <button
type="button" type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${coreProjects ? "btn-primary text-white" : "" className={`btn px-2 py-1 rounded-0 text-tiny ${
coreProjects ? "btn-primary text-white" : ""
}`} }`}
onClick={() => handleToggleProject(true)} onClick={() => handleToggleProject(true)}
> >
@ -138,7 +129,8 @@ const ProjectPage = () => {
<div className="d-flex gap-2"> <div className="d-flex gap-2">
<button <button
type="button" type="button"
className={`btn btn-sm p-1 ${!listView ? "btn-primary" : "btn-outline-primary" className={`btn btn-sm p-1 ${
!listView ? "btn-primary" : "btn-outline-primary"
}`} }`}
onClick={() => setListView(false)} onClick={() => setListView(false)}
title="Card View" title="Card View"
@ -148,7 +140,8 @@ const ProjectPage = () => {
<button <button
type="button" type="button"
className={`btn btn-sm p-1 ${listView ? "btn-primary" : "btn-outline-primary" className={`btn btn-sm p-1 ${
listView ? "btn-primary" : "btn-outline-primary"
}`} }`}
onClick={() => setListView(true)} onClick={() => setListView(true)}
title="List View" title="List View"
@ -158,36 +151,17 @@ const ProjectPage = () => {
</div> </div>
{/* Dropdown Filter */} {/* Dropdown Filter */}
<div className="dropdown me-2 position-relative"> <div className="dropdown me-2">
<div <a
className="cursor-pointer p-1" className="dropdown-toggle hide-arrow cursor-pointer p-1"
onClick={() => setOpen(!open)} data-bs-toggle="dropdown"
aria-expanded="false"
title="Filter"
> >
<i <i className="bx bx-slider-alt fs-5"></i>
className={`bx bx-slider-alt fs-5 ${selectedStatuses.length !== PROJECT_STATUS.length ? "text-primary" : "" </a>
}`}
></i>
{selectedStatuses.length !== PROJECT_STATUS.length && ( <ul className="dropdown-menu p-2 text-capitalize">
<span className="badge bg-warning text-white rounded-pill position-absolute"
style={{
top: "-4px",
right: "-4px",
fontSize: "0.6rem",
padding: "2px 5px",
}}
>
{PROJECT_STATUS.length - selectedStatuses.length}
</span>
)}
</div>
{open && (
<ul
ref={dropdownRef}
className="dropdown-menu show p-2 text-capitalize"
onMouseDown={(e) => e.stopPropagation()} // IMPORTANT
>
{PROJECT_STATUS.map(({ id, label }) => ( {PROJECT_STATUS.map(({ id, label }) => (
<li key={id}> <li key={id}>
<div className="form-check"> <div className="form-check">
@ -195,25 +169,15 @@ const ProjectPage = () => {
className="form-check-input" className="form-check-input"
type="checkbox" type="checkbox"
checked={selectedStatuses.includes(id)} checked={selectedStatuses.includes(id)}
onClick={(e) => e.stopPropagation()} // IMPORTANT
onChange={() => handleStatusChange(id)} onChange={() => handleStatusChange(id)}
/> />
<label <label className="form-check-label">{label}</label>
className="form-check-label"
onClick={(e) => e.stopPropagation()} // OPTIONAL
>
{label}
</label>
</div> </div>
</li> </li>
))} ))}
</ul> </ul>
)}
</div> </div>
{HasManageProject && ( {HasManageProject && (
<button <button
type="button" type="button"

View File

@ -150,9 +150,6 @@ export function startSignalR(loggedUser) {
queryClient.invalidateQueries(["serviceProjects"]); queryClient.invalidateQueries(["serviceProjects"]);
queryClient.invalidateQueries(["serviceProject"]); queryClient.invalidateQueries(["serviceProject"]);
} }
if (keyword === "Project_Branch") {
queryClient.invalidateQueries(["branches"]);
}
if (keyword === "Service_Project_Allocation") { if (keyword === "Service_Project_Allocation") {
queryClient.invalidateQueries(["serviceProjectTeam"]); queryClient.invalidateQueries(["serviceProjectTeam"]);