Merge pull request 'Service_Project_Management_Bugs :- Changes for Jobs' (#511) from Service_Project_Management_Bugs into Service_Project_Managment

Reviewed-on: #511
Merged
This commit is contained in:
pramod.mahajan 2025-11-17 04:16:04 +00:00
commit 3d1d8264ad
9 changed files with 197 additions and 141 deletions

View File

@ -1,5 +1,5 @@
import React, { useState } from "react";
import { getNextBadgeColor } from "../../utils/appUtils";
import { daysLeft, getNextBadgeColor } from "../../utils/appUtils";
import { useServiceProjectJobs } from "../../hooks/useServiceProject";
import { ITEMS_PER_PAGE } from "../../utils/constants";
import EmployeeAvatarGroup from "../common/EmployeeAvatarGroup";
@ -37,36 +37,51 @@ const JobList = ({ filterByProject }) => {
isAlwaysVisible: true,
className: "text-start",
},
{
key: "project",
label: "Project",
getValue: (e) => <div className="text-start ">{e?.project?.name}</div>,
isAlwaysVisible: true,
className: "text-start d-none d-sm-table-cell",
},
{
key: "employee",
label: "Team",
getValue: (e) => <EmployeeAvatarGroup employees={e.assignees} />,
isAlwaysVisible: true,
className: "text-start d-none d-sm-table-cell",
},
{
key: "startDate",
label: "Start Date",
getValue: (e) => formatUTCToLocalTime(e.startDate),
isAlwaysVisible: true,
className: "text-center d-none d-sm-table-cell ",
},
{
key: "dueDate",
label: "Due To",
label: "Due On",
getValue: (e) => formatUTCToLocalTime(e.startDate),
isAlwaysVisible: true,
className: "text-center d-none d-sm-table-cell",
className: "text-start d-none d-sm-table-cell",
},
{
key: "status",
label: "Status",
getValue: (e) => {
const statusName = e?.status?.displayName || "N/A";
const statusColorMap = {
Assigned: "label-primary",
Pending: "label-warning",
Completed: "label-success",
Cancelled: "label-danger",
};
const badgeColor = statusColorMap[statusName] || "label-secondary";
return (
<span className={`badge bg-${badgeColor}`} style={{ fontSize: "12px" }}>
{statusName}
</span>
);
},
isAlwaysVisible: true,
className: "text-start d-none d-sm-table-cell",
},
{
key: "daysLeft",
label: "Days Left",
getValue: (e) => {
const { days, color } = daysLeft(e.startDate, e.dueDate);
return (
<span className={`badge bg-${color}`} style={{ fontSize: "12px" }}>
{days !== null ? `${days} days` : "N/A"}
</span>
);
},
isAlwaysVisible: true,
className: "text-start d-none d-sm-table-cell"
}
];
return (

View File

@ -58,24 +58,7 @@ const Jobs = () => {
</OffcanvasComponent>
<div className="card page-min-h my-2 px-4">
<div className="row">
<div className="col-12 py-2 d-flex justify-content-between ">
<div>
{" "}
<select
className="form-select form-select-sm"
value={selectedProject}
onChange={(e) => setSelectedProject(e.target.value)}
>
<option disabled selected>
Select Poject
</option>
{data?.data?.map((project) => (
<option key={project.id} value={project.id}>
{project.name}
</option>
))}
</select>
</div>
<div className="col-12 py-2 d-flex justify-content-end ">
<div className="px-2">
<button
className="btn btn-sm btn-primary"

View File

@ -134,7 +134,7 @@ const ManageJob = ({ Job }) => {
/>
</div>
<div className="col-12 col-md-6 mb-2 mb-md-4">
<Label required>End Date</Label>
<Label required>Select Employee</Label>
<PmsEmployeeInputTag
control={control}
name="assignees"

View File

@ -59,7 +59,7 @@ const ManageServiceProject = ({ serviceProjectId, onClose }) => {
const onSubmit = (formData) => {
if (serviceProjectId) {
let existingServiceIds = projectdata?.services?.map((s) => s.id) || [];
let existingServiceIds = projectdata?.services?.map((s) => s.id) || [];
const oldServices = projectdata.services.map((service) => ({
serviceId: service.id,
@ -110,10 +110,10 @@ const ManageServiceProject = ({ serviceProjectId, onClose }) => {
<FormProvider {...methods}>
<form className="px-3 py-2" onSubmit={handleSubmit(onSubmit)}>
<div className=" text-center">
<h5>{serviceProjectId ? "Update Project":"Create Project"}</h5>
<h5>{serviceProjectId ? "Update Project" : "Create Project"}</h5>
</div>
<div className="row text-start">
<div className="col-12 mb-2">
<div className="col-12 mb-2">
<Label htmlFor="name" required>
Client
</Label>
@ -150,6 +150,7 @@ const ManageServiceProject = ({ serviceProjectId, onClose }) => {
type="text"
className="form-control form-control-sm"
{...register("name")}
placeholder="Enter Project Name.."
/>
{errors?.name && (
<span className="danger-text">{errors.name.message}</span>
@ -163,6 +164,7 @@ const ManageServiceProject = ({ serviceProjectId, onClose }) => {
type="text"
className="form-control form-control-sm"
{...register("shortName")}
placeholder="Enter Project Short Name.."
/>
{errors?.shortName && (
<span className="danger-text">{errors.shortName.message}</span>
@ -194,19 +196,23 @@ const ManageServiceProject = ({ serviceProjectId, onClose }) => {
valueKey="id"
label="Select Service"
/>
{errors?.services && (
{errors?.services && (
<span className="danger-text">{errors.services.message}</span>
)}
</div>
<div className="col-12 col-md-6 mb-2">
<Label htmlFor="name" required>
Contact Name
Contact Person
</Label>
<input
type="text"
className="form-control form-control-sm"
{...register("contactName")}
placeholder="Enter Employee name.."
onInput={(e) => {
e.target.value = e.target.value.replace(/[^A-Za-z ]/g, "");
}}
/>
{errors?.contactName && (
<span className="danger-text">{errors.contactName.message}</span>
@ -220,6 +226,7 @@ const ManageServiceProject = ({ serviceProjectId, onClose }) => {
type="text"
className="form-control form-control-sm"
{...register("contactEmail")}
placeholder="Enter Employee Email.."
/>
{errors?.contactEmail && (
<span className="danger-text">{errors.contactEmail.message}</span>
@ -231,8 +238,13 @@ const ManageServiceProject = ({ serviceProjectId, onClose }) => {
</Label>
<input
type="text"
maxLength={10}
className="form-control form-control-sm"
{...register("contactPhone")}
placeholder="Enter Employee Contact.."
onInput={(e) => {
e.target.value = e.target.value.replace(/[^0-9]/g, "");
}}
/>
{errors?.contactPhone && (
<span className="danger-text">{errors.contactPhone.message}</span>
@ -244,7 +256,7 @@ const ManageServiceProject = ({ serviceProjectId, onClose }) => {
</Label>
<DatePicker
name="assignedDate"
className="w-full"
className="w-100"
control={control}
/>
</div>
@ -258,9 +270,10 @@ const ManageServiceProject = ({ serviceProjectId, onClose }) => {
className="form-control"
// maxLength={maxAddressLength}
{...register("address")}
// onChange={(e) => {
// setAddressLength(e.target.value.length);
// }}
placeholder="Enter Project Address.."
// onChange={(e) => {
// setAddressLength(e.target.value.length);
// }}
/>
</div>
@ -290,8 +303,8 @@ const ManageServiceProject = ({ serviceProjectId, onClose }) => {
{isPending || isUpdating
? "Please wait..."
: serviceProjectId
? "Update"
: "Submit"}
? "Update"
: "Submit"}
</button>
</div>
</form>

View File

@ -1,22 +1,47 @@
import { z } from "zod";
import { PROJECT_STATUS } from "../../utils/constants";
//#region Service Project
export const projectSchema = z.object({
name: z.string().min(1, "Name is required"),
shortName: z.string().min(1, "Short name is required"),
clientId: z.string().min(1, { message: "Client is required" }),
name: z
.string()
.trim()
.min(1, "Name is required"),
shortName: z
.string()
.trim()
.min(1, "Short name is required"),
clientId: z.string().trim().min(1, { message: "Client is required" }),
services: z
.array(z.string())
.min(1, { message: "At least one service required" }),
address: z.string().min(1, "Address is required"),
assignedDate: z.string().min(1, { message: "Date is required" }),
statusId: z.string().min(1, "Status is required"),
contactName: z.string().min(1, "Contact name is required"),
address: z.string().trim().min(1, "Address is required"),
assignedDate: z.string().trim().min(1, { message: "Date is required" }),
statusId: z.string().trim().min(1, "Status is required"),
contactName: z
.string()
.trim()
.min(1, "Contact name is required")
.regex(/^[A-Za-z\s]+$/, "Contact name can contain only letters"),
contactPhone: z
.string()
.trim()
.regex(/^\d+$/, "Phone number must contain only digits")
.min(7, "Invalid phone number")
.max(15, "Phone number too long"),
contactEmail: z.string().email("Invalid email address"),
contactEmail: z
.string()
.trim()
.email("Invalid email address"),
});
export const defaultProjectValues = {
@ -26,7 +51,7 @@ export const defaultProjectValues = {
services: [],
address: "",
assignedDate: "",
statusId: "",
statusId: PROJECT_STATUS.find((s) => s.label === "Active")?.id,
contactName: "",
contactPhone: "",
contactEmail: "",

View File

@ -51,7 +51,7 @@ const DatePicker = ({
<div className={`position-relative ${className} w-max `}>
<input
type="text"
className="form-control form-control"
className="form-control form-control form-control-sm"
placeholder={placeholder}
value={displayValue}
onChange={(e) => {

View File

@ -190,7 +190,7 @@ export const useJobComments = (jobId, pageSize, pageNumber) => {
return allPages.length + pageNumber;
},
});
};
};
export const useJobTags = () => {
return useQuery({
queryKey: ["Job_Tags"],
@ -238,7 +238,7 @@ export const useCreateServiceProjectJob = (onSuccessCallback) => {
return await ServiceProjectRepository.CreateJob(payload);
},
onSuccess: (data, variables) => {
queryClient.invalidateQueries({ queryKey: [""] });
queryClient.invalidateQueries({ queryKey: ["serviceProjectJobs"] });
if (onSuccessCallback) onSuccessCallback();
showToast("Job Created successfully", "success");

View File

@ -40,10 +40,10 @@ const ProjectPage = () => {
const [projectList, setProjectList] = useState([]);
const [listView, setListView] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [coreProjects, setCoreProjects] = useState(() => {
const storedValue = sessionStorage.getItem('whichProjectDisplay');
return storedValue === 'true';
});
const [coreProjects, setCoreProjects] = useState(() => {
const storedValue = sessionStorage.getItem('whichProjectDisplay');
return storedValue === 'true';
});
const HasManageProject = useHasUserPermission(MANAGE_PROJECT);
const [selectedStatuses, setSelectedStatuses] = useState(
@ -58,11 +58,11 @@ const ProjectPage = () => {
};
const handleToggleProject = (e) => {
const checked = e.target.checked;
setCoreProjects(checked);
sessionStorage.setItem('whichProjectDisplay', String(checked));
};
const handleToggleProject = (value) => {
setCoreProjects(value);
sessionStorage.setItem("whichProjectDisplay", String(value));
};
return (
<ProjectContext.Provider value={contextDispatcher}>
@ -75,10 +75,39 @@ const handleToggleProject = (e) => {
/>
<div className="card cursor-pointer mb-5">
<div className="card-body p-2 pb-1">
<div className="card-body p-2 pb-1">
<div className="d-flex flex-wrap justify-content-between align-items-start">
<div className="d-flex flex-wrap align-items-start">
<div className="flex-grow-1 me-2 mb-2">
{/* LEFT SIDE — DATE TOGGLE BUTTONS */}
<div className="mb-2">
<div className="d-inline-flex border rounded-pill overflow-hidden shadow-none">
{/* Service Project Button */}
<button
type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${!coreProjects ? "btn-primary text-white" : ""
}`}
onClick={() => handleToggleProject(false)}
>
Service Project
</button>
{/* Organization Project Button */}
<button
type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${coreProjects ? "btn-primary text-white" : ""
}`}
onClick={() => handleToggleProject(true)}
>
Infra Project
</button>
</div>
</div>
{/* RIGHT SIDE — SEARCH + CARD/LIST + DROPDOWN */}
<div className="d-flex flex-wrap align-items-center justify-content-end">
{/* Search */}
<div className="me-2" style={{ minWidth: "200px" }}>
<input
type="search"
className="form-control form-control-sm"
@ -91,49 +120,44 @@ const handleToggleProject = (e) => {
/>
</div>
<div className="d-flex gap-2 mb-2">
{/* Card/List Buttons */}
<div className="d-flex gap-2">
<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)}
data-bs-toggle="tooltip"
data-bs-custom-class="tooltip"
title="Card View"
>
<i className="bx bx-grid-alt fs-5"></i>
</button>
<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)}
data-bs-toggle="tooltip"
data-bs-custom-class="tooltip"
title="List View"
>
<i className="bx bx-list-ul fs-5"></i>
</button>
</div>
<div className="dropdown mt-1">
{/* Dropdown Filter */}
<div className="dropdown me-2">
<a
className="dropdown-toggle hide-arrow cursor-pointer p-1 mt-3 "
className="dropdown-toggle hide-arrow cursor-pointer p-1"
data-bs-toggle="dropdown"
aria-expanded="false"
data-bs-custom-class="tooltip"
title="Filter"
>
<i className="bx bx-slider-alt ms-1"></i>
<i className="bx bx-slider-alt fs-5"></i>
</a>
<ul className="dropdown-menu p-2 text-capitalize">
{PROJECT_STATUS.map(({ id, label }) => (
<li key={id}>
<div className="form-check">
<input
className="form-check-input "
className="form-check-input"
type="checkbox"
checked={selectedStatuses.includes(id)}
onChange={() => handleStatusChange(id)}
@ -144,56 +168,27 @@ const handleToggleProject = (e) => {
))}
</ul>
</div>
<div className="form-check form-switch d-flex align-items-center text-nowrap ms-3">
<input
type="checkbox"
className="form-check-input"
id="activeEmployeeSwitch"
checked={coreProjects}
onChange={handleToggleProject}
/>
<label
className="form-check-label ms-2"
htmlFor="activeEmployeeSwitch"
>
{coreProjects ? "Organization Project" : "Service Project"}
</label>
</div>
</div>
{HasManageProject && (
<div class="btn-group">
<button type="button" className="btn btn-primary">
<i className="bx bx-plus-circle me-2"></i> New Project
</button>
{HasManageProject && (
<button
type="button"
class="btn btn-primary dropdown-toggle dropdown-toggle-split"
data-bs-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
className="btn btn-primary btn-sm d-flex align-items-center"
onClick={() =>
coreProjects
? setMangeProject({ isOpen: true, Project: null }) // Organization Project Infra
: setManageServiceProject({ isOpen: true, Project: null }) // Service Project
}
>
<span class="visually-hidden">Toggle Dropdown</span>
<i className="bx bx-plus-circle me-2"></i>
New Project
</button>
<ul class="dropdown-menu">
<li
onClick={() =>
setManageServiceProject({ isOpen: true, Project: null })
}
>
<a class="dropdown-item">Service Project</a>
</li>
<li
onClick={() =>
setMangeProject({ isOpen: true, Project: null })
}
>
<a class="dropdown-item">Infra Project</a>
</li>
</ul>
</div>
)}
)}
</div>
</div>
</div>
</div>

View File

@ -203,3 +203,28 @@ export function getNextBadgeColor(type = "label") {
colorIndex = (colorIndex + 1) % badgeColors.length;
return `rounded-pill text-bg-${color}`;
}
export function daysLeft(startDate, dueDate) {
if (!startDate || !dueDate) {
return { days: null, color: "label-secondary" };
}
const start = new Date(startDate);
const due = new Date(dueDate);
const today = new Date();
const diffTime = due.getTime() - today.getTime();
const days = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
let color = "label-primary"; // default
if (days < 0) {
color = "label-danger"; // overdue → red
} else if (days <= 15) {
color = "label-warning"; // near due → yellow
} else {
color = "label-primary"; // safe range
}
return { days, color };
}