Merge branch 'Project_Branch_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Project_Branch_Management
This commit is contained in:
commit
8f86b05d35
@ -25,8 +25,7 @@ const Sidebar = () => {
|
|||||||
/>
|
/>
|
||||||
</span> */}
|
</span> */}
|
||||||
|
|
||||||
<a
|
<small
|
||||||
href="/"
|
|
||||||
className="app-brand-link fw-bold navbar-brand text-green fs-6"
|
className="app-brand-link fw-bold navbar-brand text-green fs-6"
|
||||||
>
|
>
|
||||||
<span className="app-brand-logo demo">
|
<span className="app-brand-logo demo">
|
||||||
@ -35,12 +34,13 @@ const Sidebar = () => {
|
|||||||
<span className="text-blue">OnField</span>
|
<span className="text-blue">OnField</span>
|
||||||
<span>Work</span>
|
<span>Work</span>
|
||||||
<span className="text-dark">.com</span>
|
<span className="text-dark">.com</span>
|
||||||
</a>
|
</small>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<a 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>
|
||||||
</a>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="menu-inner-shadow"></div>
|
<div className="menu-inner-shadow"></div>
|
||||||
|
|||||||
@ -5,8 +5,8 @@ import { useServiceProjects } from "../../hooks/useServiceProject";
|
|||||||
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
||||||
import OffcanvasComponent from "../common/OffcanvasComponent";
|
import OffcanvasComponent from "../common/OffcanvasComponent";
|
||||||
import showToast from "../../services/toastService";
|
import showToast from "../../services/toastService";
|
||||||
import ManageJob from "./ManageJob";
|
import ManageJob from "./ServiceProjectJob/ManageJob";
|
||||||
import ManageJobTicket from "./ManageJobTicket";
|
import ManageJobTicket from "./ServiceProjectJob/ManageJobTicket";
|
||||||
import GlobalModel from "../common/GlobalModel";
|
import GlobalModel from "../common/GlobalModel";
|
||||||
import PreviewDocument from "../Expenses/PreviewDocument";
|
import PreviewDocument from "../Expenses/PreviewDocument";
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,86 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { useBranch } from "../../../hooks/useServiceProject";
|
||||||
|
import { SpinnerLoader } from "../../common/Loader";
|
||||||
|
import Error from "../../common/Error";
|
||||||
|
import { BranchDetailsSkeleton } from "../ServiceProjectSeketon";
|
||||||
|
|
||||||
|
const BranchDetails = ({ branch }) => {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
const { data, isLoading, isError, error } = useBranch(branch);
|
||||||
|
|
||||||
|
const googleMapUrl = data?.googleMapUrl || data?.locationLink;
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
if (!googleMapUrl) return;
|
||||||
|
|
||||||
|
await navigator.clipboard.writeText(googleMapUrl);
|
||||||
|
setCopied(true);
|
||||||
|
|
||||||
|
setTimeout(() => setCopied(false), 3000);
|
||||||
|
};
|
||||||
|
if (isLoading) return <BranchDetailsSkeleton />;
|
||||||
|
if (isError)
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Error error={error} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div className="">
|
||||||
|
<div className="d-flex mb-2">
|
||||||
|
<span className="fs-6 fw-medium">
|
||||||
|
<i className="bx bx-sm me-2 bx-buildings"></i>Branch Details
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="row mb-1">
|
||||||
|
<div className="col-4 col-md-4 text-secondary">Name:</div>
|
||||||
|
<div className="col-8 col-md-8">{data?.branchName}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row mb-1">
|
||||||
|
<div className="col-4 col-md-4 text-secondary">Type:</div>
|
||||||
|
<div className="col-8 col-md-8">{data?.branchType}</div>
|
||||||
|
</div>
|
||||||
|
<div className="row mb-1">
|
||||||
|
<div className="col-4 col-md-4 text-secondary">Contact No:</div>
|
||||||
|
<div className="col-8 col-md-8">{data?.contactInformation}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row mb-1">
|
||||||
|
<div className="col-4 col-md-4 text-secondary">Address:</div>
|
||||||
|
<div className="col-8 col-md-8">{data?.address}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{googleMapUrl && (
|
||||||
|
<div className="row mb-1">
|
||||||
|
<div className="col-4 col-md-4 text-secondary">Map:</div>
|
||||||
|
|
||||||
|
<div className="col-8 col-md-8 d-flex align-items-center gap-2">
|
||||||
|
<a
|
||||||
|
href={googleMapUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary text-decoration-underline text-break"
|
||||||
|
style={{ wordBreak: "break-all" }}
|
||||||
|
>
|
||||||
|
Open in Google Maps
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<i
|
||||||
|
className={`bx ${
|
||||||
|
copied ? "bx-check-double text-secondry " : "bx-copy"
|
||||||
|
}`}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
onClick={handleCopy}
|
||||||
|
></i>
|
||||||
|
|
||||||
|
{copied && <span className="text-secondry small">Copied!</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BranchDetails;
|
||||||
@ -1,17 +1,16 @@
|
|||||||
import SelectField from "../common/Forms/SelectField";
|
import SelectField from "../../common/Forms/SelectField";
|
||||||
import { useJobStatus } from "../../hooks/masterHook/useMaster";
|
import Error from "../../common/Error";
|
||||||
import { SpinnerLoader } from "../common/Loader";
|
|
||||||
import Error from "../common/Error";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import {
|
||||||
AppFormController,
|
AppFormController,
|
||||||
AppFormProvider,
|
AppFormProvider,
|
||||||
useAppForm,
|
useAppForm,
|
||||||
} from "../../hooks/appHooks/useAppForm";
|
} from "../../../hooks/appHooks/useAppForm";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { closePopup } from "../../slices/localVariablesSlice";
|
import { closePopup } from "../../../slices/localVariablesSlice";
|
||||||
import { useUpdateServiceProjectJob } from "../../hooks/useServiceProject";
|
import { useUpdateServiceProjectJob } from "../../../hooks/useServiceProject";
|
||||||
|
import { useJobStatus } from "../../../hooks/masterHook/useMaster";
|
||||||
|
|
||||||
export const ChangeStatusSchema = z.object({
|
export const ChangeStatusSchema = z.object({
|
||||||
statusId: z.string().min(1, { message: "Please select status" }),
|
statusId: z.string().min(1, { message: "Please select status" }),
|
||||||
@ -53,6 +52,11 @@ const ChangeStatus = ({ statusId, projectId, jobId, popUpId }) => {
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<AppFormProvider {...methods}>
|
<AppFormProvider {...methods}>
|
||||||
|
<div className="d-flex mb-2">
|
||||||
|
<span className="fs-6 fw-medium">
|
||||||
|
Change Status
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<form className="row text-start" onSubmit={handleSubmit(onSubmit)}>
|
<form className="row text-start" onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<AppFormController
|
<AppFormController
|
||||||
@ -1,16 +1,16 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import Avatar from "../common/Avatar";
|
import Avatar from "../../common/Avatar";
|
||||||
import { useAppForm } from "../../hooks/appHooks/useAppForm";
|
import { useAppForm } from "../../../hooks/appHooks/useAppForm";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { JobCommentSchema } from "./ServiceProjectSchema";
|
import { JobCommentSchema } from "../ServiceProjectSchema";
|
||||||
import {
|
import {
|
||||||
useAddCommentJob,
|
useAddCommentJob,
|
||||||
useJobComments,
|
useJobComments,
|
||||||
} from "../../hooks/useServiceProject";
|
} from "../../../hooks/useServiceProject";
|
||||||
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
import { ITEMS_PER_PAGE } from "../../../utils/constants";
|
||||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
import { formatUTCToLocalTime } from "../../../utils/dateUtils";
|
||||||
import Filelist from "../Expenses/Filelist";
|
import Filelist from "../../Expenses/Filelist";
|
||||||
import { formatFileSize, getIconByFileType } from "../../utils/appUtils";
|
import { formatFileSize, getIconByFileType } from "../../../utils/appUtils";
|
||||||
|
|
||||||
const JobComments = ({ data }) => {
|
const JobComments = ({ data }) => {
|
||||||
const {
|
const {
|
||||||
@ -161,46 +161,48 @@ const JobComments = ({ data }) => {
|
|||||||
const user = item?.createdBy;
|
const user = item?.createdBy;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="d-flex align-items-start mt-2 mx-0 px-0">
|
||||||
key={item.id}
|
<Avatar
|
||||||
className="list-group-item border-0 border-bottom p-0"
|
size="xs"
|
||||||
>
|
firstName={user?.firstName}
|
||||||
<div className="d-flex align-items-start mt-2 mx-0 px-0">
|
lastName={user?.lastName}
|
||||||
<Avatar
|
/>
|
||||||
firstName={user?.firstName}
|
|
||||||
lastName={user?.lastName}
|
<div className="w-100">
|
||||||
/>
|
<div className="d-flex flex-row align-items-center gap-3 w-100">
|
||||||
<div className="">
|
<span className="fw-semibold">
|
||||||
<div className="d-flex flex-row gap-3">
|
{user?.firstName} {user?.lastName}
|
||||||
<span className="fw-semibold">
|
</span>
|
||||||
{user?.firstName} {user?.lastName}
|
|
||||||
</span>
|
<span className="text-secondary">
|
||||||
<span className="text-secondary">
|
<em>{formatUTCToLocalTime(item?.createdAt, true)}</em>
|
||||||
<em>{formatUTCToLocalTime(item?.createdAt, true)}</em>
|
</span>
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted text-secondary">
|
|
||||||
{user?.jobRoleName}
|
<div className="text-muted text-secondary">
|
||||||
</div>
|
{user?.jobRoleName}
|
||||||
<div className="text-wrap">
|
</div>
|
||||||
<p className="mb-1 mt-2 text-muted">{item.comment}</p>
|
|
||||||
<div className="d-flex flex-wrap jusify-content-end gap-2 gap-sm-6 ">
|
<div className="text-wrap">
|
||||||
{item.attachments?.map((file) => (
|
<p className="mb-1 mt-2 text-muted">{item.comment}</p>
|
||||||
<div className="d-flex align-items-center">
|
|
||||||
<i
|
<div className="d-flex flex-wrap jusify-content-end gap-2 gap-sm-6">
|
||||||
className={`bx bx-xxl ${getIconByFileType(
|
{item.attachments?.map((file) => (
|
||||||
file?.contentType
|
<div className="d-flex align-items-center">
|
||||||
)} fs-3`}
|
<i
|
||||||
></i>
|
className={`bx bx-xxl ${getIconByFileType(
|
||||||
<div className="d-flex flex-column">
|
file?.contentType
|
||||||
<p className="m-0">{file.fileName}</p>
|
)} fs-3`}
|
||||||
<small className="text-secondary">
|
></i>
|
||||||
{formatFileSize(file.fileSize)}
|
<div className="d-flex flex-column">
|
||||||
</small>
|
<p className="m-0">{file.fileName}</p>
|
||||||
</div>
|
<small className="text-secondary">
|
||||||
|
{formatFileSize(file.fileSize)}
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import Avatar from "../common/Avatar";
|
import Avatar from "../../common/Avatar";
|
||||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
import { formatUTCToLocalTime } from "../../../utils/dateUtils";
|
||||||
|
|
||||||
|
|
||||||
const JobStatusLog = ({ data }) => {
|
const JobStatusLog = ({ data }) => {
|
||||||
return (
|
return (
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import Breadcrumb from "../common/Breadcrumb";
|
import Breadcrumb from "../../common/Breadcrumb";
|
||||||
import Label from "../common/Label";
|
import Label from "../../common/Label";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { defaultJobValue, jobSchema } from "./ServiceProjectSchema";
|
import { defaultJobValue, jobSchema } from "../ServiceProjectSchema";
|
||||||
import {
|
import {
|
||||||
useBranches,
|
useBranches,
|
||||||
useCreateServiceProjectJob,
|
useCreateServiceProjectJob,
|
||||||
@ -10,23 +10,23 @@ import {
|
|||||||
useServiceProjectJobDetails,
|
useServiceProjectJobDetails,
|
||||||
useServiceProjects,
|
useServiceProjects,
|
||||||
useUpdateServiceProjectJob,
|
useUpdateServiceProjectJob,
|
||||||
} from "../../hooks/useServiceProject";
|
} from "../../../hooks/useServiceProject";
|
||||||
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
import { ITEMS_PER_PAGE } from "../../../utils/constants";
|
||||||
import DatePicker from "../common/DatePicker";
|
import DatePicker from "../../common/DatePicker";
|
||||||
import PmsEmployeeInputTag from "../common/PmsEmployeeInputTag";
|
import PmsEmployeeInputTag from "../../common/PmsEmployeeInputTag";
|
||||||
import TagInput from "../common/TagInput";
|
import TagInput from "../../common/TagInput";
|
||||||
import { localToUtc } from "../../utils/appUtils";
|
import { localToUtc } from "../../../utils/appUtils";
|
||||||
import SelectField from "../common/Forms/SelectField";
|
import SelectField from "../../common/Forms/SelectField";
|
||||||
import {
|
import {
|
||||||
AppFormController,
|
AppFormController,
|
||||||
AppFormProvider,
|
AppFormProvider,
|
||||||
useAppForm,
|
useAppForm,
|
||||||
} from "../../hooks/appHooks/useAppForm";
|
} from "../../../hooks/appHooks/useAppForm";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { useDispatch } from "react-redux";
|
import { useDispatch } from "react-redux";
|
||||||
import { useJobStatus } from "../../hooks/masterHook/useMaster";
|
import { useJobStatus } from "../../../hooks/masterHook/useMaster";
|
||||||
import { useServiceProjectJobContext } from "./Jobs";
|
import { useServiceProjectJobContext } from "../Jobs";
|
||||||
import { SelectFieldSearch } from "../common/Forms/SelectFieldServerSide";
|
import { SelectFieldSearch } from "../../common/Forms/SelectFieldServerSide";
|
||||||
|
|
||||||
const ManageJob = ({ Job }) => {
|
const ManageJob = ({ Job }) => {
|
||||||
const { setManageJob, setSelectedJob } = useServiceProjectJobContext();
|
const { setManageJob, setSelectedJob } = useServiceProjectJobContext();
|
||||||
@ -165,7 +165,7 @@ const ManageJob = ({ Job }) => {
|
|||||||
dueDate: JobData.dueDate ?? null,
|
dueDate: JobData.dueDate ?? null,
|
||||||
tags: JobData.tags ?? [],
|
tags: JobData.tags ?? [],
|
||||||
statusId: JobData.status.id,
|
statusId: JobData.status.id,
|
||||||
branchId : JobData?.projectBranch?.id
|
projectBranchId : JobData?.projectBranch?.id
|
||||||
});
|
});
|
||||||
}, [JobData, Job, projectId]);
|
}, [JobData, Job, projectId]);
|
||||||
return (
|
return (
|
||||||
@ -248,8 +248,8 @@ const ManageJob = ({ Job }) => {
|
|||||||
<SelectFieldSearch
|
<SelectFieldSearch
|
||||||
label="Select Branch"
|
label="Select Branch"
|
||||||
placeholder="Select Branch"
|
placeholder="Select Branch"
|
||||||
value={watch("branchId")}
|
value={watch("projectBranchId")}
|
||||||
onChange={(val) => setValue("branchId", val)}
|
onChange={(val) => setValue("projectBranchId", val)}
|
||||||
valueKey="id"
|
valueKey="id"
|
||||||
labelKey="branchName"
|
labelKey="branchName"
|
||||||
hookParams={[projectId, true, 10, 1]}
|
hookParams={[projectId, true, 10, 1]}
|
||||||
@ -1,25 +1,27 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import { useServiceProjectJobDetails } from "../../hooks/useServiceProject";
|
import { useServiceProjectJobDetails } from "../../../hooks/useServiceProject";
|
||||||
import { SpinnerLoader } from "../common/Loader";
|
import { SpinnerLoader } from "../../common/Loader";
|
||||||
import Error from "../common/Error";
|
import Error from "../../common/Error";
|
||||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
import { formatUTCToLocalTime } from "../../../utils/dateUtils";
|
||||||
import Avatar from "../common/Avatar";
|
import Avatar from "../../common/Avatar";
|
||||||
import EmployeeAvatarGroup from "../common/EmployeeAvatarGroup";
|
import EmployeeAvatarGroup from "../../common/EmployeeAvatarGroup";
|
||||||
import JobStatusLog from "./JobStatusLog";
|
import { daysLeft, getJobStatusBadge } from "../../../utils/appUtils";
|
||||||
import JobComments from "./JobComments";
|
import HoverPopup from "../../common/HoverPopup";
|
||||||
import { daysLeft, getJobStatusBadge } from "../../utils/appUtils";
|
|
||||||
import HoverPopup from "../common/HoverPopup";
|
|
||||||
import ChangeStatus from "./ChangeStatus";
|
import ChangeStatus from "./ChangeStatus";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { STATUS_JOB_CLOSED } from "../../utils/constants";
|
import { STATUS_JOB_CLOSED } from "../../../utils/constants";
|
||||||
import Tooltip from "../common/Tooltip";
|
import Tooltip from "../../common/Tooltip";
|
||||||
|
import BranchDetails from "../ServiceProjectBranch/BranchDetails";
|
||||||
|
import { JobDetailsSkeleton } from "../ServiceProjectSeketon";
|
||||||
|
import JobComments from "./JobComments";
|
||||||
|
import JobStatusLog from "./JobStatusLog";
|
||||||
|
|
||||||
const ManageJobTicket = ({ Job }) => {
|
const ManageJobTicket = ({ Job }) => {
|
||||||
const { projectId } = useParams();
|
const { projectId } = useParams();
|
||||||
const { data, isLoading, isError, error } = useServiceProjectJobDetails(
|
const { data, isLoading, isError, error } = useServiceProjectJobDetails(
|
||||||
Job?.job
|
Job?.job
|
||||||
);
|
);
|
||||||
|
const drawerRef = useRef();
|
||||||
const tabsData = [
|
const tabsData = [
|
||||||
{
|
{
|
||||||
id: "comment",
|
id: "comment",
|
||||||
@ -37,7 +39,7 @@ const ManageJobTicket = ({ Job }) => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (isLoading) return <SpinnerLoader />;
|
if (isLoading) return <JobDetailsSkeleton />;
|
||||||
if (isError)
|
if (isError)
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -45,7 +47,7 @@ const ManageJobTicket = ({ Job }) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div className="row text-start">
|
<div className="row text-start" ref={drawerRef}>
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<h6 className="fs-5 fw-semibold">{data?.title}</h6>
|
<h6 className="fs-5 fw-semibold">{data?.title}</h6>
|
||||||
<div className="d-flex justify-content-between align-items-start flex-wrap mb-2">
|
<div className="d-flex justify-content-between align-items-start flex-wrap mb-2">
|
||||||
@ -61,7 +63,6 @@ const ManageJobTicket = ({ Job }) => {
|
|||||||
{STATUS_JOB_CLOSED !== data?.status?.id && (
|
{STATUS_JOB_CLOSED !== data?.status?.id && (
|
||||||
<HoverPopup
|
<HoverPopup
|
||||||
id="STATUS_CHANEG"
|
id="STATUS_CHANEG"
|
||||||
title="Change Status"
|
|
||||||
Mode="click"
|
Mode="click"
|
||||||
className=""
|
className=""
|
||||||
content={
|
content={
|
||||||
@ -124,68 +125,29 @@ const ManageJobTicket = ({ Job }) => {
|
|||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
<div className="d-flex my-2">
|
{data?.projectBranch && (
|
||||||
<span className="fw-semibold">
|
<div className="d-flex flex-row gap-3 my-2">
|
||||||
{" "}
|
<span className="fw-semibold">
|
||||||
<i className="bx bx-buildings me-1"></i>Branch Name :
|
<i className="bx bx-buildings me-1"></i> Branch Name :
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
<div className="d-block mt-4 mb-3">
|
<HoverPopup
|
||||||
<div className="row align-items-start align-items-md-start gap-2 mb-1">
|
id="BRANCH_DETAILS"
|
||||||
<div className="col-12 col-md-auto">
|
Mode="click"
|
||||||
<small className="fs-6 ">Created By</small>
|
align="auto"
|
||||||
</div>
|
boundaryRef={drawerRef}
|
||||||
<div className="col d-flex flex-row align-items-center ">
|
content={<BranchDetails branch={data?.projectBranch?.id} />}
|
||||||
<Avatar
|
>
|
||||||
size="xs"
|
<span className="text text-decoration-underline ">
|
||||||
firstName={data?.createdBy?.firstName}
|
{data?.projectBranch?.branchName}
|
||||||
lastName={data?.createdBy?.lastName}
|
</span>
|
||||||
/>{" "}
|
</HoverPopup>
|
||||||
<div className="d-flex flex-row align-items-center">
|
|
||||||
<p className="m-0">{`${data?.createdBy?.firstName} ${data?.createdBy?.lastName}`}</p>
|
|
||||||
<small className="text-secondary ms-1">
|
|
||||||
({data?.createdBy?.jobRoleName})
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{data?.assignees?.length > 0 && (
|
<div className="card shadow-none border ">
|
||||||
<div className="row align-items-start align-items-md-start gap-2">
|
<span className="text-secondry">People</span>
|
||||||
<div className="col-12 col-md-auto">
|
</div>
|
||||||
<small className="fs-6">Assigned To</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col">
|
|
||||||
<div className="row gap-4">
|
|
||||||
{data?.assignees?.map((emp) => (
|
|
||||||
<div
|
|
||||||
key={emp.id}
|
|
||||||
className="col-6 col-sm-6 col-md-4 col-lg-4"
|
|
||||||
>
|
|
||||||
<div className="d-flex align-items-center gap-2">
|
|
||||||
<Avatar
|
|
||||||
size="xs"
|
|
||||||
firstName={emp.firstName}
|
|
||||||
lastName={emp.lastName}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="d-flex flex-column">
|
|
||||||
<span className=" text-truncate">
|
|
||||||
{emp.firstName} {emp.lastName}
|
|
||||||
</span>
|
|
||||||
<small className="text-secondary text-truncate">
|
|
||||||
{emp.jobRoleName}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="nav-align-top nav-tabs-shadow p-0 shadow-none">
|
<div className="nav-align-top nav-tabs-shadow p-0 shadow-none">
|
||||||
@ -0,0 +1,80 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useAppForm } from "../../../hooks/appHooks/useAppForm";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { JobCommentSchema } from "../ServiceProjectSchema";
|
||||||
|
import Avatar from "../../common/Avatar";
|
||||||
|
import Filelist from "../../Expenses/Filelist";
|
||||||
|
|
||||||
|
const UpdateJobComment = () => {
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
watch,
|
||||||
|
reset,
|
||||||
|
setValue,
|
||||||
|
formState: { errors },
|
||||||
|
} = useAppForm({
|
||||||
|
resolver: zodResolver(JobCommentSchema),
|
||||||
|
defaultValues: { comment: "", attachments: [] },
|
||||||
|
});
|
||||||
|
const onSubmit = () => {};
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div className="d-flex">
|
||||||
|
<Avatar firstName={"A"} lastName={"D"} />
|
||||||
|
|
||||||
|
<div className="flex-grow-1">
|
||||||
|
<textarea
|
||||||
|
className="form-control"
|
||||||
|
rows={3}
|
||||||
|
placeholder="Write your comment..."
|
||||||
|
{...register("comment")}
|
||||||
|
></textarea>
|
||||||
|
|
||||||
|
{errors?.comment && (
|
||||||
|
<small className="danger-text">{errors?.comment?.message}</small>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* <div className="flex-grow-1 ms-10 mt-2">
|
||||||
|
{files?.length > 0 && (
|
||||||
|
<Filelist files={} removeFile={removeFile} />
|
||||||
|
)}
|
||||||
|
</div> */}
|
||||||
|
<div className="d-flex justify-content-end gap-2 align-items-center text-end mt-3 ms-10 ms-md-0">
|
||||||
|
<div
|
||||||
|
onClick={() => document.getElementById("attachments").click()}
|
||||||
|
className="cursor-pointer"
|
||||||
|
style={{ whiteSpace: "nowrap" }}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".pdf,.jpg,.jpeg,.png"
|
||||||
|
id="attachments"
|
||||||
|
multiple
|
||||||
|
className="d-none"
|
||||||
|
{...register("attachments")}
|
||||||
|
onChange={(e) => {
|
||||||
|
onFileChange(e);
|
||||||
|
e.target.value = "";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<i className="bx bx-sm bx-paperclip mb-1 me-1"></i>
|
||||||
|
Add Attachment
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="btn btn-primary btn-sm px-1 py-1"
|
||||||
|
type="submit"
|
||||||
|
disabled={!watch("comment")?.trim() || isPending}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UpdateJobComment;
|
||||||
@ -75,7 +75,7 @@ export const jobSchema = z.object({
|
|||||||
|
|
||||||
statusId: z.string().optional().nullable(),
|
statusId: z.string().optional().nullable(),
|
||||||
|
|
||||||
branchId: z.string().optional().nullable(),
|
projectBranchId: z.string().optional().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
||||||
|
|||||||
138
src/components/ServiceProject/ServiceProjectSeketon.jsx
Normal file
138
src/components/ServiceProject/ServiceProjectSeketon.jsx
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const SkeletonLine = ({ height = 18, width = "100%", className = "" }) => (
|
||||||
|
<div
|
||||||
|
className={`skeleton ${className}`}
|
||||||
|
style={{
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
borderRadius: "4px",
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const BranchDetailsSkeleton = () => {
|
||||||
|
return (
|
||||||
|
<div className="w-100">
|
||||||
|
<div className="d-flex mb-3">
|
||||||
|
<SkeletonLine height={22} width="280px" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row mb-2">
|
||||||
|
<div className="col-4">
|
||||||
|
<SkeletonLine height={16} width="70%" />
|
||||||
|
</div>
|
||||||
|
<div className="col-8">
|
||||||
|
<SkeletonLine height={16} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row mb-2">
|
||||||
|
<div className="col-4">
|
||||||
|
<SkeletonLine height={16} width="70%" />
|
||||||
|
</div>
|
||||||
|
<div className="col-8">
|
||||||
|
<SkeletonLine height={16} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row mb-2">
|
||||||
|
<div className="col-4">
|
||||||
|
<SkeletonLine height={16} width="70%" />
|
||||||
|
</div>
|
||||||
|
<div className="col-8">
|
||||||
|
<SkeletonLine height={16} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row mb-2">
|
||||||
|
<div className="col-4">
|
||||||
|
<SkeletonLine height={16} width="70%" />
|
||||||
|
</div>
|
||||||
|
<div className="col-8">
|
||||||
|
<SkeletonLine height={16} width="90%" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row mb-2">
|
||||||
|
<div className="col-4">
|
||||||
|
<SkeletonLine height={16} width="70%" />
|
||||||
|
</div>
|
||||||
|
<div className="col-8 d-flex gap-2 align-items-center">
|
||||||
|
<SkeletonLine height={16} width="60%" />
|
||||||
|
<SkeletonLine height={16} width="20px" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const JobDetailsSkeleton = () => {
|
||||||
|
return (
|
||||||
|
<div className="row text-start">
|
||||||
|
<div className="col-12">
|
||||||
|
{/* Title */}
|
||||||
|
<SkeletonLine height={24} width="50%" />
|
||||||
|
|
||||||
|
{/* Job ID + Status */}
|
||||||
|
<div className="d-flex justify-content-between align-items-start flex-wrap mb-3 mt-2">
|
||||||
|
<SkeletonLine height={18} width="30%" />
|
||||||
|
<div className="d-flex flex-row gap-2">
|
||||||
|
<SkeletonLine height={22} width="70px" />
|
||||||
|
<SkeletonLine height={22} width="22px" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<SkeletonLine height={40} width="100%" />
|
||||||
|
|
||||||
|
{/* Created Date */}
|
||||||
|
<div className="d-flex my-3">
|
||||||
|
<SkeletonLine height={16} width="40%" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Start / Due Date */}
|
||||||
|
<div className="d-flex justify-content-between mb-4">
|
||||||
|
<SkeletonLine height={16} width="50%" />
|
||||||
|
<SkeletonLine height={22} width="70px" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Branch Name */}
|
||||||
|
<div className="d-flex flex-row gap-3 my-2">
|
||||||
|
<SkeletonLine height={16} width="30%" />
|
||||||
|
<SkeletonLine height={16} width="40%" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Created By */}
|
||||||
|
<div className="row align-items-center my-3">
|
||||||
|
<div className="col-12 col-md-auto mb-2">
|
||||||
|
<SkeletonLine height={16} width="80px" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col d-flex align-items-center gap-2">
|
||||||
|
<SkeletonLine height={30} width="30px" /> {/* Avatar */}
|
||||||
|
<SkeletonLine height={16} width="40%" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Assigned To */}
|
||||||
|
<div className="row mt-2">
|
||||||
|
<div className="col-12 col-md-auto mb-2">
|
||||||
|
<SkeletonLine height={16} width="90px" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className="mt-4">
|
||||||
|
<div className="d-flex gap-3 mb-3">
|
||||||
|
<SkeletonLine height={35} width="80px" />
|
||||||
|
<SkeletonLine height={35} width="80px" />
|
||||||
|
<SkeletonLine height={35} width="80px" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SkeletonLine height={150} width="100%" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,11 +1,11 @@
|
|||||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import {
|
import { closePopup, openPopup, togglePopup } from "../../slices/localVariablesSlice";
|
||||||
closePopup,
|
|
||||||
openPopup,
|
|
||||||
togglePopup,
|
|
||||||
} from "../../slices/localVariablesSlice";
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* align: "auto" | "left" | "right"
|
||||||
|
* boundaryRef: optional ref to the drawer/container element to use as boundary
|
||||||
|
*/
|
||||||
const HoverPopup = ({
|
const HoverPopup = ({
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
@ -13,6 +13,8 @@ const HoverPopup = ({
|
|||||||
children,
|
children,
|
||||||
className = "",
|
className = "",
|
||||||
Mode = "hover",
|
Mode = "hover",
|
||||||
|
align = "auto",
|
||||||
|
boundaryRef = null,
|
||||||
}) => {
|
}) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const visible = useSelector((s) => s.localVariables.popups[id] || false);
|
const visible = useSelector((s) => s.localVariables.popups[id] || false);
|
||||||
@ -23,11 +25,9 @@ const HoverPopup = ({
|
|||||||
const handleMouseEnter = () => {
|
const handleMouseEnter = () => {
|
||||||
if (Mode === "hover") dispatch(openPopup(id));
|
if (Mode === "hover") dispatch(openPopup(id));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
const handleMouseLeave = () => {
|
||||||
if (Mode === "hover") dispatch(closePopup(id));
|
if (Mode === "hover") dispatch(closePopup(id));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClick = (e) => {
|
const handleClick = (e) => {
|
||||||
if (Mode === "click") {
|
if (Mode === "click") {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -35,43 +35,111 @@ const HoverPopup = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Close on outside click when using click mode
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (Mode !== "click" || !visible) return;
|
if (Mode !== "click" || !visible) return;
|
||||||
|
|
||||||
const handleOutside = (e) => {
|
const handler = (e) => {
|
||||||
if (
|
if (
|
||||||
!popupRef.current?.contains(e.target) &&
|
popupRef.current &&
|
||||||
!triggerRef.current?.contains(e.target)
|
!popupRef.current.contains(e.target) &&
|
||||||
|
triggerRef.current &&
|
||||||
|
!triggerRef.current.contains(e.target)
|
||||||
) {
|
) {
|
||||||
dispatch(closePopup(id));
|
dispatch(closePopup(id));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
document.addEventListener("click", handleOutside);
|
|
||||||
return () => document.removeEventListener("click", handleOutside);
|
|
||||||
}, [visible, Mode, id]);
|
|
||||||
|
|
||||||
|
document.addEventListener("click", handler);
|
||||||
|
return () => document.removeEventListener("click", handler);
|
||||||
|
}, [Mode, visible, dispatch, id]);
|
||||||
|
|
||||||
|
// Positioning effect: respects align prop and stays inside boundary (drawer)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!visible || !popupRef.current) return;
|
if (!visible || !popupRef.current || !triggerRef.current) return;
|
||||||
|
|
||||||
const popup = popupRef.current;
|
// run in next frame so DOM/layout settles
|
||||||
const rect = popup.getBoundingClientRect();
|
requestAnimationFrame(() => {
|
||||||
|
const popup = popupRef.current;
|
||||||
|
|
||||||
popup.style.left = "50%";
|
// choose boundary: provided boundaryRef or nearest positioned parent (popup.parentElement)
|
||||||
popup.style.right = "auto";
|
const boundaryEl = (boundaryRef && boundaryRef.current) || popup.parentElement;
|
||||||
popup.style.transform = "translateX(-50%)";
|
if (!boundaryEl) return;
|
||||||
|
|
||||||
if (rect.right > window.innerWidth) {
|
const boundaryRect = boundaryEl.getBoundingClientRect();
|
||||||
popup.style.left = "auto";
|
const triggerRect = triggerRef.current.getBoundingClientRect();
|
||||||
popup.style.right = "0";
|
|
||||||
popup.style.transform = "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rect.left < 0) {
|
// reset styles first
|
||||||
popup.style.left = "0";
|
popup.style.left = "";
|
||||||
popup.style.right = "auto";
|
popup.style.right = "";
|
||||||
popup.style.transform = "none";
|
popup.style.transform = "";
|
||||||
}
|
popup.style.top = "";
|
||||||
}, [visible]);
|
|
||||||
|
// default: place below trigger and center horizontally relative to parent
|
||||||
|
// We'll use absolute positioning with respect to the positioned parent container.
|
||||||
|
// Ensure popup is positioned using left/right in parent's coordinate system.
|
||||||
|
// Compute desired left (centered under trigger)
|
||||||
|
const popupRect = popup.getBoundingClientRect();
|
||||||
|
const parentRect = boundaryRect; // alias
|
||||||
|
|
||||||
|
// Convert trigger center to parent coordinates
|
||||||
|
const triggerCenterX = triggerRect.left + triggerRect.width / 2 - parentRect.left;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="d-inline-block position-relative">
|
<div className="d-inline-block position-relative">
|
||||||
@ -88,16 +156,18 @@ const HoverPopup = ({
|
|||||||
{visible && (
|
{visible && (
|
||||||
<div
|
<div
|
||||||
ref={popupRef}
|
ref={popupRef}
|
||||||
className={`bg-white border rounded shadow-sm p-3 w-max position-absolute top-100 mt-2 ${className}`}
|
// position absolute; it should be inside a positioned parent (the drawer)
|
||||||
|
className={`hover-popup bg-white border rounded shadow-sm p-3 position-absolute mt-2 ${className}`}
|
||||||
style={{
|
style={{
|
||||||
zIndex: 2000,
|
zIndex: 2000,
|
||||||
left: "50%",
|
top: "100%", // open below trigger
|
||||||
transform: "translateX(-50%)",
|
// left/right will be set by effect in parent coordinates
|
||||||
|
width: "max-content",
|
||||||
|
minWidth: "120px",
|
||||||
}}
|
}}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{title && <h6 className="fw-semibold mb-2">{title}</h6>}
|
{title && <h6 className="fw-semibold mb-2">{title}</h6>}
|
||||||
|
|
||||||
<div>{content}</div>
|
<div>{content}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -60,7 +60,7 @@ import PaymentRequestPage from "../pages/PaymentRequest/PaymentRequestPage";
|
|||||||
import RecurringExpensePage from "../pages/RecurringExpense/RecurringExpensePage";
|
import RecurringExpensePage from "../pages/RecurringExpense/RecurringExpensePage";
|
||||||
import AdvancePaymentPage from "../pages/AdvancePayment/AdvancePaymentPage";
|
import AdvancePaymentPage from "../pages/AdvancePayment/AdvancePaymentPage";
|
||||||
import ServiceProjectDetail from "../pages/ServiceProject/ServiceProjectDetail";
|
import ServiceProjectDetail from "../pages/ServiceProject/ServiceProjectDetail";
|
||||||
import ManageJob from "../components/ServiceProject/ManageJob";
|
import ManageJob from "../components/ServiceProject/ServiceProjectJob/ManageJob";
|
||||||
const router = createBrowserRouter(
|
const router = createBrowserRouter(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user