added view comments

This commit is contained in:
pramod.mahajan 2025-11-20 09:26:02 +05:30
parent 0210e17170
commit e99d49b83e
10 changed files with 410 additions and 116 deletions

View File

@ -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";

View File

@ -1,25 +1,86 @@
import React from 'react' import React, { useState } from "react";
import { useBranch } from '../../../hooks/useServiceProject' import { useBranch } from "../../../hooks/useServiceProject";
import { SpinnerLoader } from '../../common/Loader' import { SpinnerLoader } from "../../common/Loader";
import Error from '../../common/Error' import Error from "../../common/Error";
import { BranchDetailsSkeleton } from "../ServiceProjectSeketon";
const BranchDetails = ({ branch }) => { const BranchDetails = ({ branch }) => {
const {data,isLoading,isError,error} = useBranch(branch) const [copied, setCopied] = useState(false);
if(isLoading) return <div><SpinnerLoader/></div>
if(isError) return <div><Error error={error}/></div>
return (
<div className='row w-auto'>
<div className='col-12 d-flex flex-row gap-3'>
<span className='text-secondry'>Name:</span> <span>{data.branchName}</span>
</div>
<div className='col-12 d-flex flex-row gap-3'>
<span className='text-secondry'>Type:</span> <span>{data.branchType}</span>
</div>
<div className='col-12 d-flex flex-row gap-3'>
<span className='text-secondry'>Address:</span> <span>{data.address}</span>
</div>
</div>
)
}
export default BranchDetails 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;

View File

@ -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

View File

@ -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,29 +161,32 @@ const JobComments = ({ data }) => {
const user = item?.createdBy; const user = item?.createdBy;
return ( return (
<div
key={item.id}
className="list-group-item border-0 border-bottom p-0"
>
<div className="d-flex align-items-start mt-2 mx-0 px-0"> <div className="d-flex align-items-start mt-2 mx-0 px-0">
<Avatar <Avatar
size="xs"
firstName={user?.firstName} firstName={user?.firstName}
lastName={user?.lastName} lastName={user?.lastName}
/> />
<div className="">
<div className="d-flex flex-row gap-3"> <div className="w-100">
<div className="d-flex flex-row align-items-center gap-3 w-100">
<span className="fw-semibold"> <span className="fw-semibold">
{user?.firstName} {user?.lastName} {user?.firstName} {user?.lastName}
</span> </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"> <div className="text-muted text-secondary">
{user?.jobRoleName} {user?.jobRoleName}
</div> </div>
<div className="text-wrap"> <div className="text-wrap">
<p className="mb-1 mt-2 text-muted">{item.comment}</p> <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="d-flex flex-wrap jusify-content-end gap-2 gap-sm-6">
{item.attachments?.map((file) => ( {item.attachments?.map((file) => (
<div className="d-flex align-items-center"> <div className="d-flex align-items-center">
@ -204,7 +207,6 @@ const JobComments = ({ data }) => {
</div> </div>
</div> </div>
</div> </div>
</div>
); );
})} })}
</div> </div>

View File

@ -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 (

View File

@ -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();

View File

@ -1,19 +1,20 @@
import React, { useEffect, useRef } 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 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();
@ -38,7 +39,7 @@ const drawerRef = useRef();
}, },
]; ];
if (isLoading) return <SpinnerLoader />; if (isLoading) return <JobDetailsSkeleton />;
if (isError) if (isError)
return ( return (
<div> <div>
@ -46,7 +47,7 @@ const drawerRef = useRef();
</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">
@ -62,7 +63,6 @@ const drawerRef = useRef();
{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={
@ -128,11 +128,19 @@ const drawerRef = useRef();
{data?.projectBranch && ( {data?.projectBranch && (
<div className="d-flex flex-row gap-3 my-2"> <div className="d-flex flex-row gap-3 my-2">
<span className="fw-semibold"> <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>
<HoverPopup align="left" boundaryRef={drawerRef} id="BRANCH_DETAILS" Mode="click" content={ <BranchDetails branch={data?.projectBranch?.id}/>} >
<span className="text">{data?.projectBranch?.branchName}</span> <HoverPopup
id="BRANCH_DETAILS"
Mode="click"
align="auto"
boundaryRef={drawerRef}
content={<BranchDetails branch={data?.projectBranch?.id} />}
>
<span className="text text-decoration-underline ">
{data?.projectBranch?.branchName}
</span>
</HoverPopup> </HoverPopup>
</div> </div>
)} )}

View File

@ -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;

View 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>
);
};

View File

@ -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(
[ [
{ {