diff --git a/src/components/ServiceProject/JobList.jsx b/src/components/ServiceProject/JobList.jsx index a58fdd85..e6d073d2 100644 --- a/src/components/ServiceProject/JobList.jsx +++ b/src/components/ServiceProject/JobList.jsx @@ -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) =>
{e?.project?.name}
, - isAlwaysVisible: true, - className: "text-start d-none d-sm-table-cell", - }, - - { - key: "employee", - label: "Team", - getValue: (e) => , - 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 ( + + {statusName} + + ); + }, + 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 ( + + {days !== null ? `${days} days` : "N/A"} + + ); + }, + isAlwaysVisible: true, + className: "text-start d-none d-sm-table-cell" + } ]; return ( diff --git a/src/components/ServiceProject/Jobs.jsx b/src/components/ServiceProject/Jobs.jsx index 6870c7d0..e0906f79 100644 --- a/src/components/ServiceProject/Jobs.jsx +++ b/src/components/ServiceProject/Jobs.jsx @@ -58,24 +58,7 @@ const Jobs = () => {
-
-
- {" "} - -
+
- + { 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 }) => {
-
{serviceProjectId ? "Update Project":"Create Project"}
+
{serviceProjectId ? "Update Project" : "Create Project"}
-
+
@@ -150,6 +150,7 @@ const ManageServiceProject = ({ serviceProjectId, onClose }) => { type="text" className="form-control form-control-sm" {...register("name")} + placeholder="Enter Project Name.." /> {errors?.name && ( {errors.name.message} @@ -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 && ( {errors.shortName.message} @@ -194,19 +196,23 @@ const ManageServiceProject = ({ serviceProjectId, onClose }) => { valueKey="id" label="Select Service" /> - {errors?.services && ( + {errors?.services && ( {errors.services.message} )}
- +
{ + e.target.value = e.target.value.replace(/[^A-Za-z ]/g, ""); + }} /> {errors?.contactName && ( {errors.contactName.message} @@ -220,6 +226,7 @@ const ManageServiceProject = ({ serviceProjectId, onClose }) => { type="text" className="form-control form-control-sm" {...register("contactEmail")} + placeholder="Enter Employee Email.." /> {errors?.contactEmail && ( {errors.contactEmail.message} @@ -231,8 +238,13 @@ const ManageServiceProject = ({ serviceProjectId, onClose }) => { { + e.target.value = e.target.value.replace(/[^0-9]/g, ""); + }} /> {errors?.contactPhone && ( {errors.contactPhone.message} @@ -244,7 +256,7 @@ const ManageServiceProject = ({ serviceProjectId, onClose }) => {
@@ -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); + // }} />
@@ -290,8 +303,8 @@ const ManageServiceProject = ({ serviceProjectId, onClose }) => { {isPending || isUpdating ? "Please wait..." : serviceProjectId - ? "Update" - : "Submit"} + ? "Update" + : "Submit"}
diff --git a/src/components/ServiceProject/ServiceProjectSchema.jsx b/src/components/ServiceProject/ServiceProjectSchema.jsx index f53b9957..7f3228d4 100644 --- a/src/components/ServiceProject/ServiceProjectSchema.jsx +++ b/src/components/ServiceProject/ServiceProjectSchema.jsx @@ -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: "", diff --git a/src/components/common/DatePicker.jsx b/src/components/common/DatePicker.jsx index d48b0258..74d1cc8f 100644 --- a/src/components/common/DatePicker.jsx +++ b/src/components/common/DatePicker.jsx @@ -51,7 +51,7 @@ const DatePicker = ({
{ diff --git a/src/hooks/useServiceProject.jsx b/src/hooks/useServiceProject.jsx index a828d9dd..c5527163 100644 --- a/src/hooks/useServiceProject.jsx +++ b/src/hooks/useServiceProject.jsx @@ -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"); diff --git a/src/pages/project/ProjectPage.jsx b/src/pages/project/ProjectPage.jsx index 9e69d2a6..c2935128 100644 --- a/src/pages/project/ProjectPage.jsx +++ b/src/pages/project/ProjectPage.jsx @@ -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 ( @@ -75,10 +75,39 @@ const handleToggleProject = (e) => { />
-
+
-
-
+ {/* LEFT SIDE — DATE TOGGLE BUTTONS */} +
+
+ {/* Service Project Button */} + + {/* Organization Project Button */} + + +
+
+ + + {/* RIGHT SIDE — SEARCH + CARD/LIST + DROPDOWN */} +
+ + {/* Search */} +
{ />
-
+ {/* Card/List Buttons */} +
+
-
+ {/* Dropdown Filter */} +
+
    {PROJECT_STATUS.map(({ id, label }) => (
  • handleStatusChange(id)} @@ -144,56 +168,27 @@ const handleToggleProject = (e) => { ))}
-
- - -
-
- {HasManageProject && ( -
- + {HasManageProject && ( - -
- )} + )} + + +
+ +
diff --git a/src/utils/appUtils.js b/src/utils/appUtils.js index 1c681809..250d3488 100644 --- a/src/utils/appUtils.js +++ b/src/utils/appUtils.js @@ -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 }; +}