Переглянути джерело

Add FT check for timesheet entry

tags/Baseline_30082024_FRONTEND_UAT
Wayne 1 рік тому
джерело
коміт
84a7df61d2
10 змінених файлів з 93 додано та 24 видалено
  1. +2
    -0
      src/app/api/timesheets/index.ts
  2. +18
    -2
      src/app/api/timesheets/utils.ts
  3. +13
    -2
      src/components/LeaveModal/LeaveCalendar.tsx
  4. +2
    -0
      src/components/LeaveModal/LeaveModal.tsx
  5. +2
    -4
      src/components/Logo/Logo.tsx
  6. +4
    -2
      src/components/TimeLeaveModal/TimeLeaveModal.tsx
  7. +15
    -3
      src/components/TimesheetAmendment/TimesheetAmendment.tsx
  8. +10
    -1
      src/components/UserWorkspacePage/UserWorkspacePage.tsx
  9. +23
    -7
      src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx
  10. +4
    -3
      src/config/authConfig.ts

+ 2
- 0
src/app/api/timesheets/index.ts Переглянути файл

@@ -13,6 +13,7 @@ export type TeamTimeSheets = {
timeEntries: RecordTimesheetInput;
staffId: string;
name: string;
employType: string | null;
};
};

@@ -21,6 +22,7 @@ export type TeamLeaves = {
leaveEntries: RecordLeaveInput;
staffId: string;
name: string;
employType: string | null;
};
};



+ 18
- 2
src/app/api/timesheets/utils.ts Переглянути файл

@@ -3,6 +3,7 @@ import { HolidaysResult } from "../holidays";
import { LeaveEntry, RecordTimeLeaveInput, TimeEntry } from "./actions";
import { convertDateArrayToString } from "@/app/utils/formatUtil";
import compact from "lodash/compact";
import dayjs from "dayjs";

export type TimeEntryError = {
[field in keyof TimeEntry]?: string;
@@ -80,6 +81,7 @@ export const validateLeaveEntry = (
export const validateTimeLeaveRecord = (
records: RecordTimeLeaveInput,
companyHolidays: HolidaysResult[],
isFullTime?: boolean,
): { [date: string]: string } | undefined => {
const errors: { [date: string]: string } = {};

@@ -91,14 +93,18 @@ export const validateTimeLeaveRecord = (
);

Object.keys(records).forEach((date) => {
const dayJsObj = dayjs(date);
const isHoliday =
holidays.has(date) || dayJsObj.day() === 0 || dayJsObj.day() === 6;

const entries = records[date];
// Check each entry
for (const entry of entries) {
let entryError;
if (entry.type === "leaveEntry") {
entryError = validateLeaveEntry(entry, holidays.has(date));
entryError = validateLeaveEntry(entry, isHoliday);
} else {
entryError = validateTimeEntry(entry, holidays.has(date));
entryError = validateTimeEntry(entry, isHoliday);
}

if (entryError) {
@@ -111,6 +117,8 @@ export const validateTimeLeaveRecord = (
const totalHourError = checkTotalHours(
entries.filter((e) => e.type === "timeEntry") as TimeEntry[],
entries.filter((e) => e.type === "leaveEntry") as LeaveEntry[],
isHoliday,
isFullTime,
);

if (totalHourError) {
@@ -124,6 +132,8 @@ export const validateTimeLeaveRecord = (
export const checkTotalHours = (
timeEntries: TimeEntry[],
leaves: LeaveEntry[],
isHoliday?: boolean,
isFullTime?: boolean,
): string | undefined => {
const leaveHours = leaves.reduce((acc, entry) => acc + entry.inputHours, 0);

@@ -137,6 +147,12 @@ export const checkTotalHours = (

if (totalInputHours + leaveHours > DAILY_NORMAL_MAX_HOURS) {
return "The daily normal hours (timesheet hours + leave hours) cannot be more than {{DAILY_NORMAL_MAX_HOURS}}. Please use other hours for exceeding hours or decrease the leave hours.";
} else if (
isFullTime &&
!isHoliday &&
totalInputHours + leaveHours !== DAILY_NORMAL_MAX_HOURS
) {
return "The daily normal hours (timesheet hours + leave hours) for full-time staffs should be {{DAILY_NORMAL_MAX_HOURS}}.";
} else if (
totalInputHours + totalOtHours + leaveHours >
TIMESHEET_DAILY_MAX_HOURS


+ 13
- 2
src/components/LeaveModal/LeaveCalendar.tsx Переглянути файл

@@ -34,6 +34,7 @@ export interface Props {
allProjects: ProjectWithTasks[];
leaveRecords: RecordLeaveInput;
timesheetRecords: RecordTimesheetInput;
isFullTime: boolean;
}

interface EventClickArg {
@@ -53,6 +54,7 @@ const LeaveCalendar: React.FC<Props> = ({
leaveTypes,
timesheetRecords,
leaveRecords,
isFullTime,
}) => {
const { t } = useTranslation(["home", "common"]);

@@ -215,6 +217,10 @@ const LeaveCalendar: React.FC<Props> = ({
if (!date) {
throw Error("Invalid date");
}
const dayJsObj = dayjs(date);
const holiday = getHolidayForDate(date, companyHolidays);
const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6;

const leaves = localLeaveRecords[date] || [];
const timesheets = timesheetRecords[date] || [];

@@ -224,11 +230,16 @@ const LeaveCalendar: React.FC<Props> = ({
"id",
);

const totalHourError = checkTotalHours(timesheets, leavesWithNewEntry);
const totalHourError = checkTotalHours(
timesheets,
leavesWithNewEntry,
Boolean(isHoliday),
isFullTime,
);

if (totalHourError) throw Error(totalHourError);
},
[localLeaveRecords, timesheetRecords],
[companyHolidays, isFullTime, localLeaveRecords, timesheetRecords],
);

const handleSaveLeave = useCallback(


+ 2
- 0
src/components/LeaveModal/LeaveModal.tsx Переглянути файл

@@ -35,6 +35,7 @@ const LeaveModal: React.FC<Props> = ({
allProjects,
leaveRecords,
timesheetRecords,
isFullTime,
}) => {
const { t } = useTranslation("home");
const isMobile = useIsMobile();
@@ -42,6 +43,7 @@ const LeaveModal: React.FC<Props> = ({
const title = t("Record leave");
const content = (
<LeaveCalendar
isFullTime={isFullTime}
leaveTypes={leaveTypes}
companyHolidays={companyHolidays}
allProjects={allProjects}


+ 2
- 4
src/components/Logo/Logo.tsx Переглянути файл

@@ -1,5 +1,3 @@
import { Grid } from "@mui/material";

interface Props {
width?: number;
height?: number;
@@ -16,7 +14,7 @@ const Logo: React.FC<Props> = ({ width, height }) => {
<g clipPath="url(#a)">

<path id="logo"
fill="#89ba17" stroke="#89ba17" stroke-width="1"
fill="#89ba17" stroke="#89ba17" strokeWidth="1"
d="M 98.00,125.00
C 92.11,126.67 84.23,126.00 78.00,126.00
68.19,126.00 48.68,126.75 40.00,125.00
@@ -66,7 +64,7 @@ const Logo: React.FC<Props> = ({ width, height }) => {
41.00,156.00 39.00,156.00 39.00,156.00 Z" />

<path id="word"
fill="#111927" stroke="#111927" stroke-width="1"
fill="#111927" stroke="#111927" strokeWidth="1"
d="M 273.00,64.00
C 273.00,64.00 279.96,66.35 279.96,66.35
283.26,67.45 289.15,67.63 290.83,63.79


+ 4
- 2
src/components/TimeLeaveModal/TimeLeaveModal.tsx Переглянути файл

@@ -49,6 +49,7 @@ interface Props {
companyHolidays: HolidaysResult[];
fastEntryEnabled?: boolean;
leaveTypes: LeaveType[];
isFullTime: boolean;
}

const modalSx: SxProps = {
@@ -71,6 +72,7 @@ const TimeLeaveModal: React.FC<Props> = ({
companyHolidays,
fastEntryEnabled,
leaveTypes,
isFullTime
}) => {
const { t } = useTranslation("home");

@@ -106,7 +108,7 @@ const TimeLeaveModal: React.FC<Props> = ({

const onSubmit = useCallback<SubmitHandler<RecordTimeLeaveInput>>(
async (data) => {
const errors = validateTimeLeaveRecord(data, companyHolidays);
const errors = validateTimeLeaveRecord(data, companyHolidays, isFullTime);
if (errors) {
Object.keys(errors).forEach((date) =>
formProps.setError(date, {
@@ -131,7 +133,7 @@ const TimeLeaveModal: React.FC<Props> = ({
formProps.reset(newFormValues);
onClose();
},
[companyHolidays, formProps, onClose],
[companyHolidays, formProps, onClose, isFullTime],
);

const onCancel = useCallback(() => {


+ 15
- 3
src/components/TimesheetAmendment/TimesheetAmendment.tsx Переглянути файл

@@ -347,6 +347,10 @@ const TimesheetAmendment: React.FC<Props> = ({
if (!date) {
throw Error("Invalid date");
}
const dayJsObj = dayjs(date);
const holiday = getHolidayForDate(date, companyHolidays);
const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6;

const intStaffId = parseInt(selectedStaff.id);
const leaves = localTeamLeaves[intStaffId].leaveEntries[date] || [];
const timesheets =
@@ -360,7 +364,11 @@ const TimesheetAmendment: React.FC<Props> = ({
leaves,
"id",
);
totalHourError = checkTotalHours(timesheets, leavesWithNewEntry);
totalHourError = checkTotalHours(
timesheets,
leavesWithNewEntry,
Boolean(isHoliday),
);
} else {
// newEntry is a timesheet entry
const timesheetsWithNewEntry = unionBy(
@@ -368,11 +376,15 @@ const TimesheetAmendment: React.FC<Props> = ({
timesheets,
"id",
);
totalHourError = checkTotalHours(timesheetsWithNewEntry, leaves);
totalHourError = checkTotalHours(
timesheetsWithNewEntry,
leaves,
Boolean(isHoliday),
);
}
if (totalHourError) throw Error(totalHourError);
},
[localTeamLeaves, localTeamTimesheets, selectedStaff.id],
[localTeamLeaves, localTeamTimesheets, selectedStaff, companyHolidays],
);

const handleSave = useCallback(


+ 10
- 1
src/components/UserWorkspacePage/UserWorkspacePage.tsx Переглянути файл

@@ -4,7 +4,12 @@ import React, { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
import { CalendarMonth, EditCalendar, Luggage, MoreTime } from "@mui/icons-material";
import {
CalendarMonth,
EditCalendar,
Luggage,
MoreTime,
} from "@mui/icons-material";
import { Menu, MenuItem, SxProps, Typography } from "@mui/material";
import AssignedProjects from "./AssignedProjects";
import { AssignedProject, ProjectWithTasks } from "@/app/api/projects";
@@ -33,6 +38,7 @@ export interface Props {
fastEntryEnabled: boolean;
maintainNormalStaffWorkspaceAbility: boolean;
maintainManagementStaffWorkspaceAbility: boolean;
isFullTime: boolean;
}

const menuItemSx: SxProps = {
@@ -52,6 +58,7 @@ const UserWorkspacePage: React.FC<Props> = ({
fastEntryEnabled,
maintainNormalStaffWorkspaceAbility,
maintainManagementStaffWorkspaceAbility,
isFullTime,
}) => {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);

@@ -181,6 +188,7 @@ const UserWorkspacePage: React.FC<Props> = ({
assignedProjects={assignedProjects}
timesheetRecords={defaultTimesheets}
leaveRecords={defaultLeaveRecords}
isFullTime={isFullTime}
/>
<LeaveModal
open={isLeaveCalendarVisible}
@@ -190,6 +198,7 @@ const UserWorkspacePage: React.FC<Props> = ({
allProjects={allProjects}
leaveRecords={defaultLeaveRecords}
timesheetRecords={defaultTimesheets}
isFullTime={isFullTime}
/>
{assignedProjects.length > 0 ? (
<AssignedProjects


+ 23
- 7
src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx Переглянути файл

@@ -11,8 +11,12 @@ import {
fetchTimesheets,
} from "@/app/api/timesheets";
import { fetchHolidays } from "@/app/api/holidays";
import { getUserAbilities } from "@/app/utils/commonUtil";
import { MAINTAIN_TIMESHEET_FAST_TIME_ENTRY, MAINTAIN_NORMAL_STAFF_WORKSPACE, MAINTAIN_MANAGEMENT_STAFF_WORKSPACE } from "@/middleware";
import { getUserAbilities, getUserStaff } from "@/app/utils/commonUtil";
import {
MAINTAIN_TIMESHEET_FAST_TIME_ENTRY,
MAINTAIN_NORMAL_STAFF_WORKSPACE,
MAINTAIN_MANAGEMENT_STAFF_WORKSPACE,
} from "@/middleware";

const UserWorkspaceWrapper: React.FC = async () => {
const [
@@ -25,6 +29,7 @@ const UserWorkspaceWrapper: React.FC = async () => {
leaveTypes,
holidays,
abilities,
userStaff,
] = await Promise.all([
fetchTeamMemberLeaves(),
fetchTeamMemberTimesheets(),
@@ -34,15 +39,24 @@ const UserWorkspaceWrapper: React.FC = async () => {
fetchLeaves(),
fetchLeaveTypes(),
fetchHolidays(),
getUserAbilities()
getUserAbilities(),
getUserStaff(),
]);

const fastEntryEnabled = abilities.includes(MAINTAIN_TIMESHEET_FAST_TIME_ENTRY)
const maintainNormalStaffWorkspaceAbility = abilities.includes(MAINTAIN_NORMAL_STAFF_WORKSPACE)
const maintainManagementStaffWorkspaceAbility = abilities.includes(MAINTAIN_MANAGEMENT_STAFF_WORKSPACE)
const fastEntryEnabled = abilities.includes(
MAINTAIN_TIMESHEET_FAST_TIME_ENTRY,
);
const maintainNormalStaffWorkspaceAbility = abilities.includes(
MAINTAIN_NORMAL_STAFF_WORKSPACE,
);
const maintainManagementStaffWorkspaceAbility = abilities.includes(
MAINTAIN_MANAGEMENT_STAFF_WORKSPACE,
);
const isFullTime = userStaff?.employType === "FT";

return (
<UserWorkspacePage
isFullTime={isFullTime}
teamLeaves={teamLeaves}
teamTimesheets={teamTimesheets}
allProjects={allProjects}
@@ -54,7 +68,9 @@ const UserWorkspaceWrapper: React.FC = async () => {
// Change to access check
fastEntryEnabled={fastEntryEnabled}
maintainNormalStaffWorkspaceAbility={maintainNormalStaffWorkspaceAbility}
maintainManagementStaffWorkspaceAbility={maintainManagementStaffWorkspaceAbility}
maintainManagementStaffWorkspaceAbility={
maintainManagementStaffWorkspaceAbility
}
/>
);
};


+ 4
- 3
src/config/authConfig.ts Переглянути файл

@@ -6,10 +6,11 @@ export interface SessionStaff {
id: number;
teamId: number;
isTeamLead: boolean;
employType: string | null;
}
export interface SessionWithTokens extends Session {
staff?: SessionStaff;
role?: String;
role?: string;
abilities?: string[];
accessToken?: string;
refreshToken?: string;
@@ -60,14 +61,14 @@ export const authOptions: AuthOptions = {
session({ session, token }) {
const sessionWithToken: SessionWithTokens = {
...session,
role: token.role as String,
role: token.role as string,
// Add the data from the token to the session
abilities: (token.abilities as ability[]).map(
(item: ability) => item.actionSubjectCombo,
) as string[],
accessToken: token.accessToken as string | undefined,
refreshToken: token.refreshToken as string | undefined,
staff: token.staff as SessionStaff
staff: token.staff as SessionStaff,
};
// console.log(sessionWithToken)
return sessionWithToken;


Завантаження…
Відмінити
Зберегти