fixed expense date, added project name at check in checkout modal

This commit is contained in:
pramod.mahajan 2025-10-01 15:34:19 +05:30
parent 2aae7194b7
commit 49b597c833
10 changed files with 193 additions and 168 deletions

View File

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
@ -9,6 +9,7 @@ import showToast from "../../services/toastService";
import { checkIfCurrentDate } from "../../utils/dateUtils";
import { useMarkAttendance } from "../../hooks/useAttendance";
import { useSelectedProject } from "../../slices/apiDataManager";
import { useProjectName } from "../../hooks/useProjects";
const createSchema = (modeldata) => {
return z
@ -19,31 +20,36 @@ const createSchema = (modeldata) => {
.max(200, "Description should be less than 200 characters")
.optional(),
})
.refine((data) => {
if (modeldata?.checkInTime && !modeldata?.checkOutTime) {
const checkIn = new Date(modeldata.checkInTime);
const [time, modifier] = data.markTime.split(" ");
const [hourStr, minuteStr] = time.split(":");
let hour = parseInt(hourStr, 10);
const minute = parseInt(minuteStr, 10);
.refine(
(data) => {
if (modeldata?.checkInTime && !modeldata?.checkOutTime) {
const checkIn = new Date(modeldata.checkInTime);
const [time, modifier] = data.markTime.split(" ");
const [hourStr, minuteStr] = time.split(":");
let hour = parseInt(hourStr, 10);
const minute = parseInt(minuteStr, 10);
if (modifier === "PM" && hour !== 12) hour += 12;
if (modifier === "AM" && hour === 12) hour = 0;
if (modifier === "PM" && hour !== 12) hour += 12;
if (modifier === "AM" && hour === 12) hour = 0;
const checkOut = new Date(checkIn);
checkOut.setHours(hour, minute, 0, 0);
const checkOut = new Date(checkIn);
checkOut.setHours(hour, minute, 0, 0);
return checkOut >= checkIn;
return checkOut >= checkIn;
}
return true;
},
{
message: "Checkout time must be later than check-in time",
path: ["markTime"],
}
return true;
}, {
message: "Checkout time must be later than check-in time",
path: ["markTime"],
});
);
};
const CheckInCheckOut = ({ modeldata, closeModal, handleSubmitForm }) => {
const [currentProject, setCurrentProject] = useState(null);
const projectId = useSelectedProject();
const { projectNames, loading } = useProjectName();
const { mutate: MarkAttendance } = useMarkAttendance();
const [isLoading, setIsLoading] = useState(false);
const coords = usePositionTracker();
@ -95,17 +101,24 @@ const CheckInCheckOut = ({ modeldata, closeModal, handleSubmitForm }) => {
closeModal();
};
useEffect(() => {
if (projectId && projectNames) {
setCurrentProject(
projectNames?.find((project) => project.id === projectId)
);
}
}, [projectNames, projectId, loading]);
return (
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
<div className="col-12 d-flex justify-content-center">
<label className="fs-5 text-dark text-center">
{modeldata?.checkInTime && !modeldata?.checkOutTime
? "Check-out :"
: "Check-in :"}
? `Check out for ${currentProject?.name}`
: `Check In for ${currentProject?.name}`}
</label>
</div>
<div className="col-6 col-md-6 text-start">
<label className="form-label" htmlFor="checkInDate">
{modeldata?.checkInTime && !modeldata?.checkOutTime

View File

@ -13,11 +13,10 @@ const formatDate = (dateStr) => {
});
};
const AttendanceOverview = () => {
const AttendanceOverview = ({projectId}) => {
const [dayRange, setDayRange] = useState(7);
const [view, setView] = useState("chart");
const projectId = useSelector((store) => store.localVariables.projectId);
const { attendanceOverviewData, loading, error } = useAttendanceOverviewData(
projectId,
dayRange

View File

@ -1,5 +1,5 @@
import React from "react";
import { useSelector } from "react-redux";
import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
// import {
// useDashboardProjectsCardData,
// useDashboardTeamsCardData,
@ -14,28 +14,38 @@ import { useSelector } from "react-redux";
// import ProjectProgressChart from "./ProjectProgressChart";
// import ProjectOverview from "../Project/ProjectOverview";
import AttendanceOverview from "./AttendanceChart";
import { useSelectedProject } from "../../slices/apiDataManager";
import { useProjectName } from "../../hooks/useProjects";
const Dashboard = () => {
// const { projectsCardData } = useDashboardProjectsCardData();
// const { teamsCardData } = useDashboardTeamsCardData();
// const { tasksCardData } = useDashboardTasksCardData();
const { projectNames, loading: projectLoading } = useProjectName();
const selectedProject = useSelectedProject();
const dispatch = useDispatch();
// const { projectsCardData } = useDashboardProjectsCardData();
// const { teamsCardData } = useDashboardTeamsCardData();
// const { tasksCardData } = useDashboardTasksCardData();
// Get the selected project ID from Redux store
const projectId = useSelector((store) => store.localVariables.projectId);
const isAllProjectsSelected = projectId === null;
// Get the selected project ID from Redux store
return (
<div className="container-fluid mt-5">
<div className="row gy-4">
{!isAllProjectsSelected && (
<div className="col-xxl-6 col-lg-6">
<AttendanceOverview /> {/* ✅ Removed unnecessary projectId prop */}
</div>
)}
</div>
</div>
);
useEffect(() => {
if (!projectLoading && projectNames.length === 1 && !selectedProject) {
dispatch(setProjectId(projectNames[0].id));
}
}, [projectNames, projectLoading, selectedProject, dispatch]);
console.log(projectNames)
// Show attendance if project selected or single project exists
const shouldShowAttendance =
!projectLoading && (selectedProject || projectNames.length === 1);
return (
<div className="container-fluid mt-5">
<div className="row gy-4">
{/* {shouldShowAttendance && ( */}
<div className="col-xxl-6 col-lg-6">
<AttendanceOverview projectId={selectedProject} />
</div>
</div>
</div>
);
};
export default Dashboard;
export default Dashboard;

View File

@ -117,6 +117,7 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
endField="endDate"
resetSignal={resetKey}
defaultRange={false}
maxDate={new Date()}
/>
</div>

View File

@ -1,4 +1,5 @@
import { z } from "zod";
import { localToUtc } from "../../utils/appUtils";
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
const ALLOWED_TYPES = [
@ -17,15 +18,12 @@ export const ExpenseSchema = (expenseTypes) => {
.min(1, { message: "Expense type is required" }),
paymentModeId: z.string().min(1, { message: "Payment mode is required" }),
paidById: z.string().min(1, { message: "Employee name is required" }),
transactionDate: z
.string()
.min(1, { message: "Date is required" })
,
transactionDate: z.string().min(1, { message: "Date is required" }),
transactionId: z.string().optional(),
description: z.string().min(1, { message: "Description is required" }),
location: z.string().min(1, { message: "Location is required" }),
supplerName: z.string().min(1, { message: "Supplier name is required" }),
gstNumber :z.string().optional(),
gstNumber: z.string().optional(),
amount: z.coerce
.number({
invalid_type_error: "Amount is required and must be a number",
@ -54,8 +52,6 @@ export const ExpenseSchema = (expenseTypes) => {
})
)
.nonempty({ message: "At least one file attachment is required" }),
})
.refine(
(data) => {
@ -68,9 +64,14 @@ export const ExpenseSchema = (expenseTypes) => {
path: ["paidById"],
}
)
.superRefine((data, ctx) => {
const expenseType = expenseTypes.find((et) => et.id === data.expensesTypeId);
if (expenseType?.noOfPersonsRequired && (!data.noOfPersons || data.noOfPersons < 1)) {
.superRefine((data, ctx) => {
const expenseType = expenseTypes.find(
(et) => et.id === data.expensesTypeId
);
if (
expenseType?.noOfPersonsRequired &&
(!data.noOfPersons || data.noOfPersons < 1)
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "No. of Persons is required and must be at least 1",
@ -92,12 +93,14 @@ export const defaultExpense = {
supplerName: "",
amount: "",
noOfPersons: "",
gstNumber:"",
gstNumber: "",
billAttachments: [],
};
export const ExpenseActionScheam = (isReimbursement = false) => {
export const ExpenseActionScheam = (
isReimbursement = false,
transactionDate
) => {
return z
.object({
comment: z.string().min(1, { message: "Please leave comment" }),
@ -122,6 +125,15 @@ export const ExpenseActionScheam = (isReimbursement = false) => {
message: "Reimburse Date is required",
});
}
// let reimburse_Date = localToUtc(data.reimburseDate);
// if (transactionDate > reimburse_Date) {
// ctx.addIssue({
// code: z.ZodIssueCode.custom,
// path: ["reimburseDate"],
// message:
// "Reimburse Date must be greater than or equal to Expense created Date",
// });
// }
if (!data.reimburseById) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
@ -133,7 +145,7 @@ export const ExpenseActionScheam = (isReimbursement = false) => {
});
};
export const defaultActionValues = {
export const defaultActionValues = {
comment: "",
statusId: "",
@ -142,8 +154,6 @@ export const ExpenseActionScheam = (isReimbursement = false) => {
reimburseById: null,
};
export const SearchSchema = z.object({
projectIds: z.array(z.string()).optional(),
statusIds: z.array(z.string()).optional(),
@ -163,4 +173,3 @@ export const defaultFilter = {
startDate: null,
endDate: null,
};

View File

@ -9,7 +9,7 @@ import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { defaultActionValues, ExpenseActionScheam } from "./ExpenseSchema";
import { useExpenseContext } from "../../pages/Expense/ExpensePage";
import { getColorNameFromHex, getIconByFileType } from "../../utils/appUtils";
import { getColorNameFromHex, getIconByFileType, localToUtc } from "../../utils/appUtils";
import { ExpenseDetailsSkeleton } from "./ExpenseSkeleton";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import {
@ -38,7 +38,7 @@ const ViewExpense = ({ ExpenseId }) => {
const IsReview = useHasUserPermission(REVIEW_EXPENSE);
const [imageLoaded, setImageLoaded] = useState({});
const { setDocumentView } = useExpenseContext();
const ActionSchema = ExpenseActionScheam(IsPaymentProcess) ?? z.object({});
const ActionSchema = ExpenseActionScheam(IsPaymentProcess,data?.createdAt) ?? z.object({});
const navigate = useNavigate();
const {
register,
@ -91,9 +91,7 @@ const ViewExpense = ({ ExpenseId }) => {
const onSubmit = (formData) => {
const Payload = {
...formData,
reimburseDate: moment
.utc(formData.reimburseDate, "DD-MM-YYYY")
.toISOString(),
reimburseDate:localToUtc(formData.reimburseDate),
expenseId: ExpenseId,
comment: formData.comment,
};
@ -397,7 +395,8 @@ const ViewExpense = ({ ExpenseId }) => {
<DatePicker
name="reimburseDate"
control={control}
minDate={data?.transactionDate}
minDate={data?.createdAt}
maxDate={new Date()}
/>
{errors.reimburseDate && (
<small className="danger-text">
@ -410,7 +409,7 @@ const ViewExpense = ({ ExpenseId }) => {
<EmployeeSearchInput
control={control}
name="reimburseById"
projectId={null}
projectId={null}
/>
</div>
</div>

View File

@ -1,3 +1,4 @@
import { useCallback, useEffect, useState,useMemo } from "react";
import getGreetingMessage from "../../utils/greetingHandler";
import {
cacheData,
@ -14,119 +15,103 @@ import { useLocation, useNavigate, useParams } from "react-router-dom";
import Avatar from "../../components/common/Avatar";
import { useChangePassword } from "../Context/ChangePasswordContext";
import { useProjectModal, useProjects } from "../../hooks/useProjects";
import { useCallback, useEffect, useState } from "react";
import { useProjectName } from "../../hooks/useProjects";
import eventBus from "../../services/eventBus";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { MANAGE_PROJECT } from "../../utils/constants";
import { ALLOW_PROJECTSTATUS_ID, MANAGE_PROJECT, UUID_REGEX } from "../../utils/constants";
import { useAuthModal, useLogout } from "../../hooks/useAuth";
const Header = () => {
const { profile } = useProfile();
const { profile } = useProfile();
const { data: masterData } = useMaster();
const location = useLocation();
const dispatch = useDispatch();
const { data, loading } = useMaster();
const navigate = useNavigate();
const { onOpen } = useAuthModal();
const HasManageProjectPermission = useHasUserPermission(MANAGE_PROJECT);
const { mutate: logout, isPending: logouting } = useLogout();
const { openModal } = useProjectModal();
const { mutate: logout, isPending: logouting } = useLogout();
const { onOpen } = useAuthModal();
const { openChangePassword } = useChangePassword();
const HasManageProjectPermission = useHasUserPermission(MANAGE_PROJECT);
const isDashboardPath =
/^\/dashboard$/.test(location.pathname) || /^\/$/.test(location.pathname);
const isProjectPath = /^\/projects$/.test(location.pathname);
const pathname = location.pathname;
const showProjectDropdown = (pathname) => {
const isDirectoryPath = /^\/directory$/.test(pathname);
// ======= MEMO CHECKS =======
const isDashboardPath = pathname === "/" || pathname === "/dashboard";
const isProjectPath = pathname === "/projects";
const isDirectory = pathname === "/directory";
const isEmployeeList = pathname === "/employees";
const isExpense = pathname === "/expenses";
const isEmployeeProfile = UUID_REGEX.test(pathname);
// const isProfilePage = /^\/employee$/.test(location.pathname);
const isProfilePage =
/^\/employee\/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(
pathname
);
const isExpensePage = /^\/expenses$/.test(pathname);
const isEmployeePage = /^\/employees$/.test(pathname)
const hideDropPaths =
isDirectory || isEmployeeList || isExpense || isEmployeeProfile;
return !(isDirectoryPath || isProfilePage || isExpensePage || isEmployeePage);
};
const allowedProjectStatusIds = [
"603e994b-a27f-4e5d-a251-f3d69b0498ba",
"cdad86aa-8a56-4ff4-b633-9c629057dfef",
"b74da4c2-d07e-46f2-9919-e75e49b12731",
];
const getRole = (roles, joRoleId) => {
if (!Array.isArray(roles)) return "User";
let role = roles.find((role) => role.id === joRoleId);
return role ? role.name : "User";
};
const handleProfilePage = () => {
navigate(`/employee/${profile?.employeeInfo?.id}`);
};
const showProjectDropdown = !hideDropPaths;
// ===== Project Names & Selected Project =====
const { projectNames, loading: projectLoading, fetchData } = useProjectName();
const selectedProject = useSelectedProject();
const projectsForDropdown = isDashboardPath
? projectNames
: projectNames?.filter((project) =>
allowedProjectStatusIds.includes(project.projectStatusId)
);
const projectsForDropdown = useMemo(
() =>
isDashboardPath
? projectNames
: projectNames?.filter((project) =>
ALLOW_PROJECTSTATUS_ID.includes(project.projectStatusId)
),
[projectNames, isDashboardPath]
);
let currentProjectDisplayName;
if (projectLoading) {
currentProjectDisplayName = "Loading...";
} else if (!projectNames || projectNames.length === 0) {
currentProjectDisplayName = "No Projects Assigned";
} else if (projectNames.length === 1) {
currentProjectDisplayName = projectNames[0].name;
} else {
if (selectedProject === null) {
currentProjectDisplayName = projectNames[0].name;
} else {
const selectedProjectObj = projectNames.find(
(p) => p?.id === selectedProject
);
currentProjectDisplayName = selectedProjectObj
? selectedProjectObj.name
: "All Projects";
}
}
const currentProjectDisplayName = useMemo(() => {
if (projectLoading) return "Loading...";
if (!projectNames?.length) return "No Projects Assigned";
if (projectNames.length === 1) return projectNames[0].name;
const { openChangePassword } = useChangePassword();
const selectedObj = projectNames.find((p) => p.id === selectedProject);
return selectedObj
? selectedObj.name
: projectNames[0]?.name || "No Projects Assigned";
}, [projectLoading, projectNames, selectedProject]);
// ===== Role Helper =====
const getRole = (roles, joRoleId) => {
if (!Array.isArray(roles)) return "User";
return roles.find((r) => r.id === joRoleId)?.name || "User";
};
// ===== Navigate to Profile =====
const handleProfilePage = () =>
navigate(`/employee/${profile?.employeeInfo?.id}`);
// ===== Set default project on load =====
useEffect(() => {
if (
projectNames &&
projectNames.length > 0 &&
projectNames?.length &&
selectedProject === undefined &&
!getCachedData("hasReceived")
) {
if (projectNames.length === 1) {
dispatch(setProjectId(projectNames[0]?.id || null));
dispatch(setProjectId(projectNames[0].id || null));
} else {
if (isDashboardPath) {
dispatch(setProjectId(null));
} else {
const firstAllowedProject = projectNames.find((project) =>
allowedProjectStatusIds.includes(project.projectStatusId)
const firstAllowed = projectNames.find((project) =>
ALLOW_PROJECTSTATUS_ID.includes(project.projectStatusId)
);
dispatch(setProjectId(firstAllowedProject?.id || null));
dispatch(setProjectId(firstAllowed?.id || null));
}
}
}
}, [projectNames, selectedProject, dispatch, isDashboardPath]);
// ===== Event Handlers =====
const handler = useCallback(
async (data) => {
if (!HasManageProjectPermission) {
await fetchData();
const projectExist = data.projectIds.some(
(item) => item === selectedProject
);
if (projectExist) {
if (data.projectIds?.includes(selectedProject)) {
cacheData("hasReceived", false);
}
}
@ -136,14 +121,15 @@ const Header = () => {
const newProjectHandler = useCallback(
async (msg) => {
if ( msg.keyword === "Create_Project") {
await fetchData();
} else if (projectNames?.some((item) => item.id === msg.response.id)) {
if (
msg.keyword === "Create_Project" ||
projectNames?.some((p) => p.id === msg.response?.id)
) {
await fetchData();
cacheData("hasReceived", false);
}
cacheData("hasReceived", false);
},
[ projectNames, fetchData]
[projectNames, fetchData]
);
useEffect(() => {
@ -160,10 +146,10 @@ const Header = () => {
};
}, [handler, newProjectHandler]);
const handleProjectChange = (project) => {
dispatch(setProjectId(project));
if (isProjectPath && project !== null) {
// ===== Project Change =====
const handleProjectChange = (projectId) => {
dispatch(setProjectId(projectId));
if (isProjectPath && projectId !== null) {
navigate("/projects/details");
}
};
@ -187,7 +173,7 @@ const Header = () => {
className="navbar-nav-right d-flex align-items-center justify-content-between"
id="navbar-collapse"
>
{showProjectDropdown(location.pathname) && (
{showProjectDropdown && (
<div className="align-items-center">
<i className="rounded-circle bx bx-building-house bx-sm-lg bx-md me-2"></i>
<div className="btn-group">
@ -213,16 +199,6 @@ const Header = () => {
className="dropdown-menu"
style={{ overflow: "auto", maxHeight: "300px" }}
>
{isProjectPath && (
<li>
<button
className="dropdown-item"
onClick={() => handleProjectChange(null)}
>
All Projects
</button>
</li>
)}
{[...projectsForDropdown]
.sort((a, b) => a?.name?.localeCompare(b.name))
.map((project) => (
@ -290,7 +266,7 @@ const Header = () => {
{profile?.employeeInfo?.firstName}
</span>
<small className="text-muted">
{getRole(data, profile?.employeeInfo?.joRoleId)}
{getRole(masterData, profile?.employeeInfo?.joRoleId)}
</small>
</div>
</div>

View File

@ -86,6 +86,11 @@ const ContactsPage = ({ projectId, searchText, onExport }) => {
<Loader />
</div>
)}
{data?.data?.length === 0 && (<div className="py-12 text-secondary">
{searchText ? `No contact found for "${searchText}"`:"No contacts found" }
</div>)}
{data?.data?.map((contact) => (
<div
key={contact.id}

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { format, parseISO } from "date-fns";
import { parseISO, formatISO } from "date-fns";
export const formatFileSize = (bytes) => {
if (bytes < 1024) return bytes + " B";
else if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + " KB";
@ -72,12 +72,11 @@ export const normalizeAllowedContentTypes = (allowedContentType) => {
export function localToUtc(localDateString) {
if (!localDateString || typeof localDateString !== "string") return null;
const [year, month, day] = localDateString.trim().split("-");
if (!year || !month || !day) return null;
const date = new Date(Number(year), Number(month) - 1, Number(day), 0, 0, 0);
const date = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day), 0, 0, 0));
return isNaN(date.getTime()) ? null : date.toISOString();
}
}

View File

@ -1,3 +1,8 @@
export const BASE_URL = process.env.VITE_BASE_URL;
// export const BASE_URL = "https://api.marcoaiot.com";
export const THRESH_HOLD = 48; // hours
export const DURATION_TIME = 10; // minutes
export const ITEMS_PER_PAGE = 20;
@ -140,8 +145,17 @@ export const PROJECT_STATUS = [
label: "Completed",
},
];
export const UUID_REGEX =
/^\/employee\/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
export const ALLOW_PROJECTSTATUS_ID = [
"603e994b-a27f-4e5d-a251-f3d69b0498ba",
"cdad86aa-8a56-4ff4-b633-9c629057dfef",
"b74da4c2-d07e-46f2-9919-e75e49b12731",
];
export const DEFAULT_EMPTY_STATUS_ID = "00000000-0000-0000-0000-000000000000";
export const BASE_URL = process.env.VITE_BASE_URL;
// export const BASE_URL = "https://api.marcoaiot.com";