|
|
|
@@ -1,31 +1,44 @@ |
|
|
|
import React, { useCallback, useEffect, useMemo, useState } from "react"; |
|
|
|
|
|
|
|
import { HolidaysResult } from "@/app/api/holidays"; |
|
|
|
import { TeamTimeSheets } from "@/app/api/timesheets"; |
|
|
|
import { LeaveType, TeamLeaves, TeamTimeSheets } from "@/app/api/timesheets"; |
|
|
|
import dayGridPlugin from "@fullcalendar/daygrid"; |
|
|
|
import interactionPlugin from "@fullcalendar/interaction"; |
|
|
|
import { Autocomplete, Stack, TextField, useTheme } from "@mui/material"; |
|
|
|
import { useTranslation } from "react-i18next"; |
|
|
|
import transform from "lodash/transform"; |
|
|
|
import { getPublicHolidaysForNYears } from "@/app/utils/holidayUtils"; |
|
|
|
import { |
|
|
|
getHolidayForDate, |
|
|
|
getPublicHolidaysForNYears, |
|
|
|
} from "@/app/utils/holidayUtils"; |
|
|
|
import { |
|
|
|
INPUT_DATE_FORMAT, |
|
|
|
convertDateArrayToString, |
|
|
|
} from "@/app/utils/formatUtil"; |
|
|
|
import StyledFullCalendar from "../StyledFullCalendar"; |
|
|
|
import { ProjectWithTasks } from "@/app/api/projects"; |
|
|
|
import { TimeEntry, saveMemberEntry } from "@/app/api/timesheets/actions"; |
|
|
|
import { |
|
|
|
LeaveEntry, |
|
|
|
TimeEntry, |
|
|
|
saveMemberEntry, |
|
|
|
saveMemberLeave, |
|
|
|
} from "@/app/api/timesheets/actions"; |
|
|
|
import TimesheetEditModal, { |
|
|
|
Props as TimesheetEditModalProps, |
|
|
|
} from "../TimesheetTable/TimesheetEditModal"; |
|
|
|
import { Props as LeaveEditModalProps } from "../LeaveTable/LeaveEditModal"; |
|
|
|
import LeaveEditModal from "../LeaveTable/LeaveEditModal"; |
|
|
|
import dayjs from "dayjs"; |
|
|
|
|
|
|
|
export interface Props { |
|
|
|
leaveTypes: LeaveType[]; |
|
|
|
teamLeaves: TeamLeaves; |
|
|
|
teamTimesheets: TeamTimeSheets; |
|
|
|
companyHolidays: HolidaysResult[]; |
|
|
|
allProjects: ProjectWithTasks[]; |
|
|
|
} |
|
|
|
|
|
|
|
type MemberOption = TeamTimeSheets[0] & { id: string }; |
|
|
|
type MemberOption = TeamTimeSheets[0] & TeamLeaves[0] & { id: string }; |
|
|
|
|
|
|
|
interface EventClickArg { |
|
|
|
event: { |
|
|
|
@@ -33,7 +46,7 @@ interface EventClickArg { |
|
|
|
startStr: string; |
|
|
|
extendedProps: { |
|
|
|
calendar?: string; |
|
|
|
entry?: TimeEntry; |
|
|
|
entry?: TimeEntry | LeaveEntry; |
|
|
|
memberId?: string; |
|
|
|
}; |
|
|
|
}; |
|
|
|
@@ -41,8 +54,10 @@ interface EventClickArg { |
|
|
|
|
|
|
|
const TimesheetAmendment: React.FC<Props> = ({ |
|
|
|
teamTimesheets, |
|
|
|
teamLeaves, |
|
|
|
companyHolidays, |
|
|
|
allProjects, |
|
|
|
leaveTypes, |
|
|
|
}) => { |
|
|
|
const { t } = useTranslation(["home", "common"]); |
|
|
|
|
|
|
|
@@ -56,23 +71,34 @@ const TimesheetAmendment: React.FC<Props> = ({ |
|
|
|
}, {}); |
|
|
|
}, [allProjects]); |
|
|
|
|
|
|
|
const leaveMap = useMemo(() => { |
|
|
|
return leaveTypes.reduce<{ [id: LeaveType["id"]]: string }>( |
|
|
|
(acc, leaveType) => ({ ...acc, [leaveType.id]: leaveType.name }), |
|
|
|
{}, |
|
|
|
); |
|
|
|
}, [leaveTypes]); |
|
|
|
|
|
|
|
// Use a local state to manage updates after a mutation |
|
|
|
const [localTeamTimesheets, setLocalTeamTimesheets] = |
|
|
|
useState(teamTimesheets); |
|
|
|
const [localTeamLeaves, setLocalTeamLeaves] = useState(teamLeaves); |
|
|
|
|
|
|
|
// member select |
|
|
|
const allMembers = useMemo(() => { |
|
|
|
return transform<TeamTimeSheets[0], MemberOption[]>( |
|
|
|
localTeamTimesheets, |
|
|
|
(acc, memberTimesheet, id) => { |
|
|
|
const leaves = localTeamLeaves[parseInt(id)]; |
|
|
|
return acc.push({ |
|
|
|
...leaves, |
|
|
|
...memberTimesheet, |
|
|
|
id, |
|
|
|
}); |
|
|
|
}, |
|
|
|
[], |
|
|
|
); |
|
|
|
}, [localTeamTimesheets]); |
|
|
|
}, [localTeamLeaves, localTeamTimesheets]); |
|
|
|
|
|
|
|
const [selectedStaff, setSelectedStaff] = useState<MemberOption>( |
|
|
|
allMembers[0], |
|
|
|
); |
|
|
|
@@ -91,10 +117,11 @@ const TimesheetAmendment: React.FC<Props> = ({ |
|
|
|
const [editModalOpen, setEditModalOpen] = useState(false); |
|
|
|
|
|
|
|
const openEditModal = useCallback( |
|
|
|
(defaultValues?: TimeEntry, recordDate?: string) => { |
|
|
|
(defaultValues?: TimeEntry, recordDate?: string, isHoliday?: boolean) => { |
|
|
|
setEditModalProps({ |
|
|
|
defaultValues, |
|
|
|
recordDate, |
|
|
|
isHoliday, |
|
|
|
}); |
|
|
|
setEditModalOpen(true); |
|
|
|
}, |
|
|
|
@@ -105,6 +132,28 @@ const TimesheetAmendment: React.FC<Props> = ({ |
|
|
|
setEditModalOpen(false); |
|
|
|
}, []); |
|
|
|
|
|
|
|
// leave edit modal related |
|
|
|
const [leaveEditModalProps, setLeaveEditModalProps] = useState< |
|
|
|
Partial<LeaveEditModalProps> |
|
|
|
>({}); |
|
|
|
const [leaveEditModalOpen, setLeaveEditModalOpen] = useState(false); |
|
|
|
|
|
|
|
const openLeaveEditModal = useCallback( |
|
|
|
(defaultValues?: LeaveEntry, recordDate?: string, isHoliday?: boolean) => { |
|
|
|
setLeaveEditModalProps({ |
|
|
|
defaultValues, |
|
|
|
recordDate, |
|
|
|
isHoliday, |
|
|
|
}); |
|
|
|
setLeaveEditModalOpen(true); |
|
|
|
}, |
|
|
|
[], |
|
|
|
); |
|
|
|
|
|
|
|
const closeLeaveEditModal = useCallback(() => { |
|
|
|
setLeaveEditModalOpen(false); |
|
|
|
}, []); |
|
|
|
|
|
|
|
// calendar related |
|
|
|
const holidays = useMemo(() => { |
|
|
|
return [ |
|
|
|
@@ -123,11 +172,34 @@ const TimesheetAmendment: React.FC<Props> = ({ |
|
|
|
})); |
|
|
|
}, [companyHolidays, theme.palette.error.main]); |
|
|
|
|
|
|
|
const leaveEntries = useMemo( |
|
|
|
() => |
|
|
|
Object.keys(selectedStaff.leaveEntries).flatMap((date, index) => { |
|
|
|
return selectedStaff.leaveEntries[date].map((entry) => ({ |
|
|
|
id: `${date}-${index}-leave-${entry.id}`, |
|
|
|
date, |
|
|
|
title: `${t("{{count}} hour", { |
|
|
|
ns: "common", |
|
|
|
count: entry.inputHours || 0, |
|
|
|
})} (${leaveMap[entry.leaveTypeId]})`, |
|
|
|
backgroundColor: theme.palette.warning.light, |
|
|
|
borderColor: theme.palette.warning.light, |
|
|
|
textColor: theme.palette.text.primary, |
|
|
|
extendedProps: { |
|
|
|
calendar: "leaveEntry", |
|
|
|
entry, |
|
|
|
memberId: selectedStaff.id, |
|
|
|
}, |
|
|
|
})); |
|
|
|
}), |
|
|
|
[leaveMap, selectedStaff, t, theme], |
|
|
|
); |
|
|
|
|
|
|
|
const timeEntries = useMemo( |
|
|
|
() => |
|
|
|
Object.keys(selectedStaff.timeEntries).flatMap((date, index) => { |
|
|
|
return selectedStaff.timeEntries[date].map((entry) => ({ |
|
|
|
id: `${date}-${index}-entry-${entry.id}`, |
|
|
|
id: `${date}-${index}-time-${entry.id}`, |
|
|
|
date, |
|
|
|
title: `${t("{{count}} hour", { |
|
|
|
ns: "common", |
|
|
|
@@ -151,21 +223,41 @@ const TimesheetAmendment: React.FC<Props> = ({ |
|
|
|
|
|
|
|
const handleEventClick = useCallback( |
|
|
|
({ event }: EventClickArg) => { |
|
|
|
const dayJsObj = dayjs(event.startStr); |
|
|
|
const holiday = getHolidayForDate(event.startStr, companyHolidays); |
|
|
|
const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; |
|
|
|
|
|
|
|
if ( |
|
|
|
event.extendedProps.calendar === "timeEntry" && |
|
|
|
event.extendedProps.entry |
|
|
|
) { |
|
|
|
openEditModal(event.extendedProps.entry, event.startStr); |
|
|
|
openEditModal( |
|
|
|
event.extendedProps.entry as TimeEntry, |
|
|
|
event.startStr, |
|
|
|
Boolean(isHoliday), |
|
|
|
); |
|
|
|
} else if ( |
|
|
|
event.extendedProps.calendar === "leaveEntry" && |
|
|
|
event.extendedProps.entry |
|
|
|
) { |
|
|
|
openLeaveEditModal( |
|
|
|
event.extendedProps.entry as LeaveEntry, |
|
|
|
event.startStr, |
|
|
|
Boolean(isHoliday), |
|
|
|
); |
|
|
|
} |
|
|
|
}, |
|
|
|
[openEditModal], |
|
|
|
[companyHolidays, openEditModal, openLeaveEditModal], |
|
|
|
); |
|
|
|
|
|
|
|
const handleDateClick = useCallback( |
|
|
|
(e: { dateStr: string }) => { |
|
|
|
openEditModal(undefined, e.dateStr); |
|
|
|
const dayJsObj = dayjs(e.dateStr); |
|
|
|
const holiday = getHolidayForDate(e.dateStr, companyHolidays); |
|
|
|
const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; |
|
|
|
openEditModal(undefined, e.dateStr, Boolean(isHoliday)); |
|
|
|
}, |
|
|
|
[openEditModal], |
|
|
|
[companyHolidays, openEditModal], |
|
|
|
); |
|
|
|
|
|
|
|
const handleSave = useCallback( |
|
|
|
@@ -189,6 +281,26 @@ const TimesheetAmendment: React.FC<Props> = ({ |
|
|
|
[selectedStaff.id], |
|
|
|
); |
|
|
|
|
|
|
|
const handleSaveLeave = useCallback( |
|
|
|
async (leaveEntry: LeaveEntry, recordDate?: string) => { |
|
|
|
const intStaffId = parseInt(selectedStaff.id); |
|
|
|
const newMemberLeaves = await saveMemberLeave({ |
|
|
|
staffId: intStaffId, |
|
|
|
recordDate, |
|
|
|
entry: leaveEntry, |
|
|
|
}); |
|
|
|
setLocalTeamLeaves((leaves) => ({ |
|
|
|
...leaves, |
|
|
|
[intStaffId]: { |
|
|
|
...leaves[intStaffId], |
|
|
|
leaveEntries: newMemberLeaves, |
|
|
|
}, |
|
|
|
})); |
|
|
|
setLeaveEditModalOpen(false); |
|
|
|
}, |
|
|
|
[selectedStaff.id], |
|
|
|
); |
|
|
|
|
|
|
|
return ( |
|
|
|
<Stack spacing={2}> |
|
|
|
<Autocomplete |
|
|
|
@@ -207,7 +319,7 @@ const TimesheetAmendment: React.FC<Props> = ({ |
|
|
|
plugins={[dayGridPlugin, interactionPlugin]} |
|
|
|
initialView="dayGridMonth" |
|
|
|
buttonText={{ today: t("Today") }} |
|
|
|
events={[...holidays, ...timeEntries]} |
|
|
|
events={[...holidays, ...timeEntries, ...leaveEntries]} |
|
|
|
eventClick={handleEventClick} |
|
|
|
dateClick={handleDateClick} |
|
|
|
/> |
|
|
|
@@ -220,6 +332,14 @@ const TimesheetAmendment: React.FC<Props> = ({ |
|
|
|
onSave={handleSave} |
|
|
|
{...editModalProps} |
|
|
|
/> |
|
|
|
<LeaveEditModal |
|
|
|
modalSx={{ maxWidth: 400 }} |
|
|
|
leaveTypes={leaveTypes} |
|
|
|
open={leaveEditModalOpen} |
|
|
|
onClose={closeLeaveEditModal} |
|
|
|
onSave={handleSaveLeave} |
|
|
|
{...leaveEditModalProps} |
|
|
|
/> |
|
|
|
</Stack> |
|
|
|
); |
|
|
|
}; |
|
|
|
|