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:
commit
3d1d8264ad
@ -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 (
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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: "",
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user