added comment, attchment
This commit is contained in:
parent
297e0712bc
commit
86819dde03
@ -5,8 +5,13 @@
|
|||||||
}
|
}
|
||||||
.offcanvas.offcanvas-wide {
|
.offcanvas.offcanvas-wide {
|
||||||
width: 700px !important; /* adjust as needed */
|
width: 700px !important; /* adjust as needed */
|
||||||
max-width: 90vw; /* responsive fallback */
|
|
||||||
}
|
}
|
||||||
|
.sticky-section {
|
||||||
|
position: sticky;
|
||||||
|
top: var(--sticky-top, 0px) !important;
|
||||||
|
z-index: 1025;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ===========================% Background_Colors %========================================================== */
|
/* ===========================% Background_Colors %========================================================== */
|
||||||
.bg-light-primary {
|
.bg-light-primary {
|
||||||
|
|||||||
@ -20,7 +20,7 @@ const Filelist = ({ files, removeFile, expenseToEdit,sm=6,md=4 }) => {
|
|||||||
<i
|
<i
|
||||||
className={`bx ${getIconByFileType(
|
className={`bx ${getIconByFileType(
|
||||||
file?.contentType
|
file?.contentType
|
||||||
)} fs-3 text-primary`}
|
)} fs-3 `}
|
||||||
style={{ minWidth: "30px" }}
|
style={{ minWidth: "30px" }}
|
||||||
></i>
|
></i>
|
||||||
|
|
||||||
|
|||||||
214
src/components/ServiceProject/JobComments.jsx
Normal file
214
src/components/ServiceProject/JobComments.jsx
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import Avatar from "../common/Avatar";
|
||||||
|
import { useAppForm } from "../../hooks/appHooks/useAppForm";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { JobCommentSchema } from "./ServiceProjectSchema";
|
||||||
|
import {
|
||||||
|
useAddCommentJob,
|
||||||
|
useJobComments,
|
||||||
|
} from "../../hooks/useServiceProject";
|
||||||
|
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
||||||
|
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||||
|
import Filelist from "../Expenses/Filelist";
|
||||||
|
import { formatFileSize, getIconByFileType } from "../../utils/appUtils";
|
||||||
|
|
||||||
|
const JobComments = ({ data }) => {
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
watch,
|
||||||
|
reset,
|
||||||
|
setValue,
|
||||||
|
formState: { errors },
|
||||||
|
} = useAppForm({
|
||||||
|
resolver: zodResolver(JobCommentSchema),
|
||||||
|
default: { comment: "", attachments: [] },
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: comments,
|
||||||
|
fetchNextPage,
|
||||||
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
} = useJobComments(data?.id, ITEMS_PER_PAGE, 1);
|
||||||
|
const jobComments = comments?.pages?.flatMap((p) => p?.data ?? []) ?? [];
|
||||||
|
|
||||||
|
const { mutate: AddComment, isPending } = useAddCommentJob(() => reset());
|
||||||
|
const onSubmit = (formData) => {
|
||||||
|
formData.jobTicketId = data?.id;
|
||||||
|
AddComment(formData);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.documentElement.style.setProperty("--sticky-top", `-25px`);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const files = watch("attachments");
|
||||||
|
const onFileChange = async (e) => {
|
||||||
|
const newFiles = Array.from(e.target.files);
|
||||||
|
if (newFiles.length === 0) return;
|
||||||
|
|
||||||
|
const existingFiles = Array.isArray(watch("attachments"))
|
||||||
|
? watch("attachments")
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const parsedFiles = await Promise.all(
|
||||||
|
newFiles.map(async (file) => {
|
||||||
|
const base64Data = await toBase64(file);
|
||||||
|
return {
|
||||||
|
fileName: file.name,
|
||||||
|
base64Data,
|
||||||
|
contentType: file.type,
|
||||||
|
fileSize: file.size,
|
||||||
|
description: "",
|
||||||
|
isActive: true,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const combinedFiles = [
|
||||||
|
...existingFiles,
|
||||||
|
...parsedFiles.filter(
|
||||||
|
(newFile) =>
|
||||||
|
!existingFiles?.some(
|
||||||
|
(f) =>
|
||||||
|
f.fileName === newFile.fileName && f.fileSize === newFile.fileSize
|
||||||
|
)
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
setValue("attachments", combinedFiles, {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldValidate: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const toBase64 = (file) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
reader.onload = () => resolve(reader.result.split(",")[1]);
|
||||||
|
reader.onerror = (error) => reject(error);
|
||||||
|
});
|
||||||
|
const removeFile = (index) => {
|
||||||
|
const newFiles = files.filter((_, i) => i !== index);
|
||||||
|
setValue("attachments", newFiles, { shouldValidate: true });
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="row">
|
||||||
|
<div className="sticky-section bg-white p-3">
|
||||||
|
<h6 className="m-0 fw-semibold mb-3">Add Comment</h6>
|
||||||
|
|
||||||
|
<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="d-flex justify-content-end gap-3 align-items-center text-end mt-3">
|
||||||
|
<div
|
||||||
|
onClick={() => document.getElementById("attachments").click()}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<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-paperclip"></i>
|
||||||
|
Add Attachment
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary btn-sm px-3"
|
||||||
|
type="submit"
|
||||||
|
disabled={!watch("comment")?.trim() || isPending}
|
||||||
|
>
|
||||||
|
<i className="bx bx-send me-1"></i>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{files?.length > 0 && (
|
||||||
|
<Filelist files={files} removeFile={removeFile} />
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div className="card-body p-0">
|
||||||
|
<div className="list-group p-0 m-0">
|
||||||
|
{jobComments?.map((item) => {
|
||||||
|
const user = item?.createdBy;
|
||||||
|
|
||||||
|
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">
|
||||||
|
<Avatar
|
||||||
|
firstName={user?.firstName}
|
||||||
|
lastName={user?.lastName}
|
||||||
|
/>
|
||||||
|
<div className="">
|
||||||
|
<div className="d-flex flex-row gap-3">
|
||||||
|
<span className="fw-semibold">
|
||||||
|
{user?.firstName} {user?.lastName}
|
||||||
|
</span>
|
||||||
|
<span className="text-secondary">
|
||||||
|
<em>{formatUTCToLocalTime(item?.createdAt, true)}</em>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-muted text-secondary">
|
||||||
|
{user?.jobRoleName}
|
||||||
|
</div>
|
||||||
|
<div className="text-wrap">
|
||||||
|
<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 ">
|
||||||
|
{item.attachments?.map((file) => (
|
||||||
|
<div className="d-flex align-items-center">
|
||||||
|
<i
|
||||||
|
className={`bx bx-xxl ${getIconByFileType(
|
||||||
|
file?.contentType
|
||||||
|
)} fs-3`}
|
||||||
|
></i>
|
||||||
|
<div className="d-flex flex-column">
|
||||||
|
<p className="m-0">{file.fileName}</p>
|
||||||
|
<small className="text-secondary">
|
||||||
|
{formatFileSize(file.fileSize)}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default JobComments;
|
||||||
52
src/components/ServiceProject/JobStatusLog.jsx
Normal file
52
src/components/ServiceProject/JobStatusLog.jsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const JobStatusLog = ({ data }) => {
|
||||||
|
return (
|
||||||
|
<div className="card shadow-none p-0">
|
||||||
|
<div className="card-body p-0">
|
||||||
|
<div className="list-group">
|
||||||
|
|
||||||
|
{data?.map((item) => (
|
||||||
|
<div key={item.id} className="list-group-item border-0 border-bottom pb-3">
|
||||||
|
|
||||||
|
<div className="d-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<span className="fw-semibold">
|
||||||
|
{item.nextStatus?.displayName ?? item.status?.displayName ?? "Status"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className="badge bg-primary">
|
||||||
|
Level {item.nextStatus?.level ?? item.status?.level}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="mb-1 mt-2 text-muted" style={{ fontSize: "0.9rem" }}>
|
||||||
|
{item.comment}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="d-flex align-items-center mt-2">
|
||||||
|
<div className="rounded-circle bg-light d-flex justify-content-center align-items-center"
|
||||||
|
style={{ width: 32, height: 32 }}>
|
||||||
|
<i className="bx bx-user"></i>
|
||||||
|
</div>
|
||||||
|
<div className="ms-2">
|
||||||
|
<div className="fw-semibold" style={{ fontSize: "0.85rem" }}>
|
||||||
|
{item.updatedBy?.firstName} {item.updatedBy?.lastName}
|
||||||
|
</div>
|
||||||
|
<div className="text-muted" style={{ fontSize: "0.75rem" }}>
|
||||||
|
{item.updatedBy?.jobRoleName}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default JobStatusLog;
|
||||||
@ -7,6 +7,8 @@ import OffcanvasComponent from "../common/OffcanvasComponent";
|
|||||||
import showToast from "../../services/toastService";
|
import showToast from "../../services/toastService";
|
||||||
import ManageJob from "./ManageJob";
|
import ManageJob from "./ManageJob";
|
||||||
import ManageJobTicket from "./ManageJobTicket";
|
import ManageJobTicket from "./ManageJobTicket";
|
||||||
|
import GlobalModel from "../common/GlobalModel";
|
||||||
|
import PreviewDocument from "../Expenses/PreviewDocument";
|
||||||
|
|
||||||
export const JonContext = createContext();
|
export const JonContext = createContext();
|
||||||
export const useServiceProjectJobContext = () => {
|
export const useServiceProjectJobContext = () => {
|
||||||
@ -72,6 +74,10 @@ const Jobs = () => {
|
|||||||
<JobList filterByProject={selectedProject} />
|
<JobList filterByProject={selectedProject} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<GlobalModel>
|
||||||
|
<PreviewDocument />
|
||||||
|
</GlobalModel>
|
||||||
</JonContext.Provider>
|
</JonContext.Provider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,15 +1,34 @@
|
|||||||
import React from "react";
|
import React, { useEffect } 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 JobComments from "./JobComments";
|
||||||
|
|
||||||
const ManageJobTicket = ({ Job }) => {
|
const ManageJobTicket = ({ Job }) => {
|
||||||
const { data, isLoading, isError, error } = useServiceProjectJobDetails(
|
const { data, isLoading, isError, error } = useServiceProjectJobDetails(
|
||||||
Job?.job
|
Job?.job
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const tabsData = [
|
||||||
|
{
|
||||||
|
id: "comment",
|
||||||
|
title: "Coments",
|
||||||
|
active: true,
|
||||||
|
content: <JobComments data={data} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "history",
|
||||||
|
title: "History",
|
||||||
|
active: false,
|
||||||
|
content: <JobStatusLog data={data?.updateLogs} />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
if (isLoading) return <SpinnerLoader />;
|
if (isLoading) return <SpinnerLoader />;
|
||||||
if (isError)
|
if (isError)
|
||||||
return (
|
return (
|
||||||
@ -41,27 +60,54 @@ const ManageJobTicket = ({ Job }) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="d-flex justify-content-between">
|
<div className="d-flex justify-content-between">
|
||||||
<div className="d-flex align-items-center">
|
<div className="d-flex align-items-center">
|
||||||
<small className="fs-6 text-secondary fs-italic me-3 mt-2">
|
<small className="fs-6 text-secondary fs-italic me-3 mt-2">
|
||||||
<em>Created By</em>
|
<em>Created By</em>
|
||||||
</small>{" "}
|
</small>{" "}
|
||||||
<Avatar size="xs"
|
<Avatar
|
||||||
firstName={data?.createdBy?.firstName}
|
size="xs"
|
||||||
lastName={data?.createdBy?.lastName}
|
firstName={data?.createdBy?.firstName}
|
||||||
/>{" "}
|
lastName={data?.createdBy?.lastName}
|
||||||
<span className="fw-medium">{`${data?.createdBy?.firstName} ${data?.createdBy?.lastName}`}</span>
|
/>{" "}
|
||||||
</div>
|
<span className="fw-medium">{`${data?.createdBy?.firstName} ${data?.createdBy?.lastName}`}</span>
|
||||||
<div className="d-flex align-items-center">
|
</div>
|
||||||
<small className="fs-6 text-secondary fs-italic me-3 mt-2">
|
<div className="d-flex align-items-center">
|
||||||
<em>Created By</em>
|
<small className="fs-6 text-secondary fs-italic me-3 mt-2">
|
||||||
</small><EmployeeAvatarGroup employees={data?.assignees}/>
|
<em>Assigned</em>
|
||||||
</div>
|
</small>
|
||||||
|
<EmployeeAvatarGroup employees={data?.assignees} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr className="divider"/>
|
|
||||||
<div className="col-12">
|
|
||||||
<div className="">
|
|
||||||
|
|
||||||
|
<div className="nav-align-top nav-tabs-shadow p-0 shadow-none">
|
||||||
|
<ul className="nav nav-tabs" role="tablist">
|
||||||
|
{tabsData.map((tab) => (
|
||||||
|
<li className="nav-item" key={tab.id}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`nav-link ${tab.active ? "active" : ""}`}
|
||||||
|
data-bs-toggle="tab"
|
||||||
|
data-bs-target={`#tab-${tab.id}`}
|
||||||
|
role="tab"
|
||||||
|
>
|
||||||
|
{tab.title}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div className="tab-content p-1 px-sm-3">
|
||||||
|
{tabsData.map((tab) => (
|
||||||
|
<div
|
||||||
|
key={tab.id}
|
||||||
|
className={`tab-pane fade ${tab.active ? "show active" : ""}`}
|
||||||
|
id={`tab-${tab.id}`}
|
||||||
|
role="tabpanel"
|
||||||
|
>
|
||||||
|
{tab.content}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -4,7 +4,9 @@ export const projectSchema = z.object({
|
|||||||
name: z.string().min(1, "Name is required"),
|
name: z.string().min(1, "Name is required"),
|
||||||
shortName: z.string().min(1, "Short name is required"),
|
shortName: z.string().min(1, "Short name is required"),
|
||||||
clientId: z.string().min(1, { message: "Client is required" }),
|
clientId: z.string().min(1, { message: "Client is required" }),
|
||||||
services:z.array(z.string()).min(1,{message:"At least one service required"}),
|
services: z
|
||||||
|
.array(z.string())
|
||||||
|
.min(1, { message: "At least one service required" }),
|
||||||
address: z.string().min(1, "Address is required"),
|
address: z.string().min(1, "Address is required"),
|
||||||
assignedDate: z.string().min(1, { message: "Date is required" }),
|
assignedDate: z.string().min(1, { message: "Date is required" }),
|
||||||
statusId: z.string().min(1, "Status is required"),
|
statusId: z.string().min(1, "Status is required"),
|
||||||
@ -31,7 +33,6 @@ export const defaultProjectValues = {
|
|||||||
|
|
||||||
//#region JobSchema
|
//#region JobSchema
|
||||||
|
|
||||||
|
|
||||||
export const TagSchema = z.object({
|
export const TagSchema = z.object({
|
||||||
name: z.string().min(1, "Tag name is required"),
|
name: z.string().min(1, "Tag name is required"),
|
||||||
isActive: z.boolean().default(true),
|
isActive: z.boolean().default(true),
|
||||||
@ -39,27 +40,49 @@ export const TagSchema = z.object({
|
|||||||
export const jobSchema = z.object({
|
export const jobSchema = z.object({
|
||||||
title: z.string().min(1, "Title is required"),
|
title: z.string().min(1, "Title is required"),
|
||||||
description: z.string().min(1, "Description is required"),
|
description: z.string().min(1, "Description is required"),
|
||||||
projectId: z.string().min(1,"Project is required"),
|
projectId: z.string().min(1, "Project is required"),
|
||||||
|
|
||||||
assignees: z
|
assignees: z.array(z.string()).nonempty("At least one assignee is required"),
|
||||||
.array(z.string() )
|
|
||||||
.nonempty("At least one assignee is required"),
|
|
||||||
|
|
||||||
startDate: z.string(),
|
startDate: z.string(),
|
||||||
dueDate: z.string(),
|
dueDate: z.string(),
|
||||||
|
|
||||||
tags: z.array(TagSchema).optional().default([]),
|
tags: z.array(TagSchema).optional().default([]),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
||||||
|
const ALLOWED_TYPES = [
|
||||||
|
"application/pdf",
|
||||||
|
"image/png",
|
||||||
|
"image/jpg",
|
||||||
|
"image/jpeg",
|
||||||
|
];
|
||||||
|
export const JobCommentSchema = z.object({
|
||||||
|
comment: z.string().min(1, { message: "Please leave comment" }),
|
||||||
|
attachments: z.array(
|
||||||
|
z.object({
|
||||||
|
fileName: z.string().min(1, { message: "Filename is required" }),
|
||||||
|
base64Data: z.string().nullable(),
|
||||||
|
contentType: z.string().refine((val) => ALLOWED_TYPES.includes(val), {
|
||||||
|
message: "Only PDF, PNG, JPG, or JPEG files are allowed",
|
||||||
|
}),
|
||||||
|
documentId: z.string().optional(),
|
||||||
|
fileSize: z.number().max(MAX_FILE_SIZE, {
|
||||||
|
message: "File size must be less than or equal to 5MB",
|
||||||
|
}),
|
||||||
|
description: z.string().optional(),
|
||||||
|
isActive: z.boolean().default(true),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
export const defaultJobValue = {
|
export const defaultJobValue = {
|
||||||
title:"",
|
title: "",
|
||||||
description:"",
|
description: "",
|
||||||
projectId:"",
|
projectId: "",
|
||||||
assignees:[],
|
assignees: [],
|
||||||
startDate:null,
|
startDate: null,
|
||||||
dueDate:null,
|
dueDate: null,
|
||||||
tags:[]
|
tags: [],
|
||||||
|
};
|
||||||
|
|
||||||
}
|
//#endregion
|
||||||
|
|
||||||
//#endregion
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { ServiceProjectRepository } from "../repositories/ServiceProjectRepository";
|
import { ServiceProjectRepository } from "../repositories/ServiceProjectRepository";
|
||||||
import showToast from "../services/toastService";
|
import showToast from "../services/toastService";
|
||||||
|
|
||||||
@ -102,26 +102,75 @@ export const useActiveInActiveServiceProject = (onSuccessCallback) => {
|
|||||||
|
|
||||||
//#region Service Jobs
|
//#region Service Jobs
|
||||||
|
|
||||||
|
export const useServiceProjectJobs = (
|
||||||
export const useServiceProjectJobs=(pageSize,pageNumber,isActive=true,project)=>{
|
pageSize,
|
||||||
|
pageNumber,
|
||||||
|
isActive = true,
|
||||||
|
project
|
||||||
|
) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey:["serviceProjectJobs",pageSize,pageNumber,isActive,project],
|
queryKey: ["serviceProjectJobs", pageSize, pageNumber, isActive, project],
|
||||||
queryFn:async() =>{
|
queryFn: async () => {
|
||||||
const resp = await ServiceProjectRepository.GetJobList(pageSize,pageNumber,isActive,project);
|
const resp = await ServiceProjectRepository.GetJobList(
|
||||||
return resp.data;
|
pageSize,
|
||||||
}
|
pageNumber,
|
||||||
})
|
isActive,
|
||||||
}
|
project
|
||||||
export const useServiceProjectJobDetails = (job)=>{
|
);
|
||||||
return useQuery({
|
|
||||||
queryKey:['service-job',job],
|
|
||||||
queryFn:async() =>{
|
|
||||||
const resp = await ServiceProjectRepository.GetJobDetails(job);
|
|
||||||
return resp.data;
|
return resp.data;
|
||||||
},
|
},
|
||||||
enabled:!!job
|
});
|
||||||
})
|
};
|
||||||
}
|
export const useJobComments = (jobId, pageSize, pageNumber ) => {
|
||||||
|
return useInfiniteQuery({
|
||||||
|
queryKey: ["jobComments", jobId, pageSize],
|
||||||
|
|
||||||
|
queryFn: async ({ pageParam = pageNumber }) => {
|
||||||
|
const resp = await ServiceProjectRepository.GetJobComment(jobId, pageSize, pageParam);
|
||||||
|
return resp.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
initialPageParam: pageNumber,
|
||||||
|
|
||||||
|
getNextPageParam: (lastPage, allPages) => {
|
||||||
|
if (!lastPage?.data?.length) return null;
|
||||||
|
|
||||||
|
return allPages.length + pageNumber;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useServiceProjectJobDetails = (job) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["service-job", job],
|
||||||
|
queryFn: async () => {
|
||||||
|
const resp = await ServiceProjectRepository.GetJobDetails(job);
|
||||||
|
return resp.data;
|
||||||
|
},
|
||||||
|
enabled: !!job,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
export const useAddCommentJob = (onSuccessCallback) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (data) => await ServiceProjectRepository.AddComment(data),
|
||||||
|
onSuccess: (data, variables) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["jobComments", variables?.jobTicketId],
|
||||||
|
});
|
||||||
|
if (onSuccessCallback) onSuccessCallback();
|
||||||
|
showToast("Job Created successfully", "success");
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
showToast(
|
||||||
|
error?.response?.data?.message ||
|
||||||
|
error.message ||
|
||||||
|
"Failed to update project",
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useCreateServiceProjectJob = (onSuccessCallback) => {
|
export const useCreateServiceProjectJob = (onSuccessCallback) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|||||||
@ -15,9 +15,14 @@ export const ServiceProjectRepository = {
|
|||||||
//#region Job
|
//#region Job
|
||||||
|
|
||||||
CreateJob: (data) => api.post(`/api/ServiceProject/job/create`, data),
|
CreateJob: (data) => api.post(`/api/ServiceProject/job/create`, data),
|
||||||
GetJobList: (pageSize, pageNumber, isActive,projectId,) =>
|
GetJobList: (pageSize, pageNumber, isActive, projectId) =>
|
||||||
api.get(
|
api.get(
|
||||||
`/api/ServiceProject/job/list?pageSize=${pageSize}&pageNumber=${pageNumber}&isActive=${isActive}&projectId=${projectId}`
|
`/api/ServiceProject/job/list?pageSize=${pageSize}&pageNumber=${pageNumber}&isActive=${isActive}&projectId=${projectId}`
|
||||||
),
|
),
|
||||||
GetJobDetails:(id)=>api.get(`/api/ServiceProject/job/details/${id}`)
|
GetJobDetails: (id) => api.get(`/api/ServiceProject/job/details/${id}`),
|
||||||
|
AddComment: (data) => api.post("/api/ServiceProject/job/add/comment", data),
|
||||||
|
GetJobComment: (jobTicketId,pageSize,pageNumber) =>
|
||||||
|
api.get(
|
||||||
|
`/api/ServiceProject/job/comment/list?jobTicketId=${jobTicketId}&pageSize=${pageSize}&pageNumber=${pageNumber}`
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -51,13 +51,15 @@ export const useDebounce = (value, delay = 500) => {
|
|||||||
export const getIconByFileType = (type = "") => {
|
export const getIconByFileType = (type = "") => {
|
||||||
const lower = type.toLowerCase();
|
const lower = type.toLowerCase();
|
||||||
|
|
||||||
if (lower === "application/pdf") return "bxs-file-pdf";
|
if (lower === "application/pdf") return "bxs-file-pdf text-danger";
|
||||||
if (lower.includes("word")) return "bxs-file-doc";
|
if (lower.includes("word")) return "bxs-file-doc text-primary text-primry";
|
||||||
if (lower.includes("excel") || lower.includes("spreadsheet"))
|
if (lower.includes("excel") || lower.includes("spreadsheet"))
|
||||||
return "bxs-file-xls";
|
return "bxs-file-xls text-primry";
|
||||||
if (lower === "image/png") return "bxs-file-png";
|
if (lower === "image/png") return "bxs-file-png";
|
||||||
if (lower === "image/jpeg" || lower === "image/jpg") return "bxs-file-jpg";
|
if (lower === "image/jpeg" || lower === "image/jpg")
|
||||||
if (lower.includes("zip") || lower.includes("rar")) return "bxs-file-archive";
|
return "bxs-file-jpg text-primry";
|
||||||
|
if (lower.includes("zip") || lower.includes("rar"))
|
||||||
|
return "bxs-file-archive text-secondary";
|
||||||
|
|
||||||
return "bx bx-file";
|
return "bx bx-file";
|
||||||
};
|
};
|
||||||
@ -192,19 +194,12 @@ export const frequencyLabel = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const badgeColors = [
|
const badgeColors = ["primary", "secondary", "success", "warning", "info"];
|
||||||
"primary",
|
|
||||||
"secondary",
|
|
||||||
"success",
|
|
||||||
"warning",
|
|
||||||
"info",
|
|
||||||
];
|
|
||||||
|
|
||||||
let colorIndex = 0;
|
let colorIndex = 0;
|
||||||
|
|
||||||
export function getNextBadgeColor(type="label") {
|
export function getNextBadgeColor(type = "label") {
|
||||||
const color = badgeColors[colorIndex];
|
const color = badgeColors[colorIndex];
|
||||||
colorIndex = (colorIndex + 1) % badgeColors.length;
|
colorIndex = (colorIndex + 1) % badgeColors.length;
|
||||||
return `rounded-pill text-bg-${color}`;
|
return `rounded-pill text-bg-${color}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user