浏览代码

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; timeEntries: RecordTimesheetInput;
staffId: string; staffId: string;
name: string; name: string;
employType: string | null;
}; };
}; };


@@ -21,6 +22,7 @@ export type TeamLeaves = {
leaveEntries: RecordLeaveInput; leaveEntries: RecordLeaveInput;
staffId: string; staffId: string;
name: 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 { LeaveEntry, RecordTimeLeaveInput, TimeEntry } from "./actions";
import { convertDateArrayToString } from "@/app/utils/formatUtil"; import { convertDateArrayToString } from "@/app/utils/formatUtil";
import compact from "lodash/compact"; import compact from "lodash/compact";
import dayjs from "dayjs";


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


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


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

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


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


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


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


if (totalInputHours + leaveHours > DAILY_NORMAL_MAX_HOURS) { 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."; 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 ( } else if (
totalInputHours + totalOtHours + leaveHours > totalInputHours + totalOtHours + leaveHours >
TIMESHEET_DAILY_MAX_HOURS TIMESHEET_DAILY_MAX_HOURS


+ 13
- 2
src/components/LeaveModal/LeaveCalendar.tsx 查看文件

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


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


@@ -215,6 +217,10 @@ const LeaveCalendar: React.FC<Props> = ({
if (!date) { if (!date) {
throw Error("Invalid 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 leaves = localLeaveRecords[date] || [];
const timesheets = timesheetRecords[date] || []; const timesheets = timesheetRecords[date] || [];


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


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


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


const handleSaveLeave = useCallback( const handleSaveLeave = useCallback(


+ 2
- 0
src/components/LeaveModal/LeaveModal.tsx 查看文件

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


+ 2
- 4
src/components/Logo/Logo.tsx 查看文件

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

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


<path id="logo" <path id="logo"
fill="#89ba17" stroke="#89ba17" stroke-width="1"
fill="#89ba17" stroke="#89ba17" strokeWidth="1"
d="M 98.00,125.00 d="M 98.00,125.00
C 92.11,126.67 84.23,126.00 78.00,126.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 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" /> 41.00,156.00 39.00,156.00 39.00,156.00 Z" />


<path id="word" <path id="word"
fill="#111927" stroke="#111927" stroke-width="1"
fill="#111927" stroke="#111927" strokeWidth="1"
d="M 273.00,64.00 d="M 273.00,64.00
C 273.00,64.00 279.96,66.35 279.96,66.35 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 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[]; companyHolidays: HolidaysResult[];
fastEntryEnabled?: boolean; fastEntryEnabled?: boolean;
leaveTypes: LeaveType[]; leaveTypes: LeaveType[];
isFullTime: boolean;
} }


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


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


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


const onCancel = useCallback(() => { const onCancel = useCallback(() => {


+ 15
- 3
src/components/TimesheetAmendment/TimesheetAmendment.tsx 查看文件

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


const handleSave = useCallback( 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 { useTranslation } from "react-i18next";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack"; 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 { Menu, MenuItem, SxProps, Typography } from "@mui/material";
import AssignedProjects from "./AssignedProjects"; import AssignedProjects from "./AssignedProjects";
import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; import { AssignedProject, ProjectWithTasks } from "@/app/api/projects";
@@ -33,6 +38,7 @@ export interface Props {
fastEntryEnabled: boolean; fastEntryEnabled: boolean;
maintainNormalStaffWorkspaceAbility: boolean; maintainNormalStaffWorkspaceAbility: boolean;
maintainManagementStaffWorkspaceAbility: boolean; maintainManagementStaffWorkspaceAbility: boolean;
isFullTime: boolean;
} }


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


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


+ 23
- 7
src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx 查看文件

@@ -11,8 +11,12 @@ import {
fetchTimesheets, fetchTimesheets,
} from "@/app/api/timesheets"; } from "@/app/api/timesheets";
import { fetchHolidays } from "@/app/api/holidays"; 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 UserWorkspaceWrapper: React.FC = async () => {
const [ const [
@@ -25,6 +29,7 @@ const UserWorkspaceWrapper: React.FC = async () => {
leaveTypes, leaveTypes,
holidays, holidays,
abilities, abilities,
userStaff,
] = await Promise.all([ ] = await Promise.all([
fetchTeamMemberLeaves(), fetchTeamMemberLeaves(),
fetchTeamMemberTimesheets(), fetchTeamMemberTimesheets(),
@@ -34,15 +39,24 @@ const UserWorkspaceWrapper: React.FC = async () => {
fetchLeaves(), fetchLeaves(),
fetchLeaveTypes(), fetchLeaveTypes(),
fetchHolidays(), 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 ( return (
<UserWorkspacePage <UserWorkspacePage
isFullTime={isFullTime}
teamLeaves={teamLeaves} teamLeaves={teamLeaves}
teamTimesheets={teamTimesheets} teamTimesheets={teamTimesheets}
allProjects={allProjects} allProjects={allProjects}
@@ -54,7 +68,9 @@ const UserWorkspaceWrapper: React.FC = async () => {
// Change to access check // Change to access check
fastEntryEnabled={fastEntryEnabled} fastEntryEnabled={fastEntryEnabled}
maintainNormalStaffWorkspaceAbility={maintainNormalStaffWorkspaceAbility} maintainNormalStaffWorkspaceAbility={maintainNormalStaffWorkspaceAbility}
maintainManagementStaffWorkspaceAbility={maintainManagementStaffWorkspaceAbility}
maintainManagementStaffWorkspaceAbility={
maintainManagementStaffWorkspaceAbility
}
/> />
); );
}; };


+ 4
- 3
src/config/authConfig.ts 查看文件

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


正在加载...
取消
保存