| @@ -4,6 +4,7 @@ import UserWorkspacePage from "@/components/UserWorkspacePage"; | |||||
| import { | import { | ||||
| fetchLeaveTypes, | fetchLeaveTypes, | ||||
| fetchLeaves, | fetchLeaves, | ||||
| fetchTeamMemberLeaves, | |||||
| fetchTeamMemberTimesheets, | fetchTeamMemberTimesheets, | ||||
| fetchTimesheets, | fetchTimesheets, | ||||
| } from "@/app/api/timesheets"; | } from "@/app/api/timesheets"; | ||||
| @@ -31,6 +32,7 @@ const Home: React.FC = async () => { | |||||
| fetchProjectWithTasks(); | fetchProjectWithTasks(); | ||||
| fetchHolidays(); | fetchHolidays(); | ||||
| fetchTeamMemberTimesheets(username); | fetchTeamMemberTimesheets(username); | ||||
| fetchTeamMemberLeaves(username); | |||||
| return ( | return ( | ||||
| <I18nProvider namespaces={["home", "common"]}> | <I18nProvider namespaces={["home", "common"]}> | ||||
| @@ -79,6 +79,21 @@ export const saveMemberEntry = async (data: { | |||||
| ); | ); | ||||
| }; | }; | ||||
| export const saveMemberLeave = async (data: { | |||||
| staffId: number; | |||||
| entry: LeaveEntry; | |||||
| recordDate?: string; | |||||
| }) => { | |||||
| return serverFetchJson<RecordLeaveInput>( | |||||
| `${BASE_API_URL}/timesheets/saveMemberLeave`, | |||||
| { | |||||
| method: "POST", | |||||
| body: JSON.stringify(data), | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| }, | |||||
| ); | |||||
| }; | |||||
| export const revalidateCacheAfterAmendment = () => { | export const revalidateCacheAfterAmendment = () => { | ||||
| revalidatePath("/(main)/home"); | revalidatePath("/(main)/home"); | ||||
| }; | }; | ||||
| @@ -16,6 +16,14 @@ export type TeamTimeSheets = { | |||||
| }; | }; | ||||
| }; | }; | ||||
| export type TeamLeaves = { | |||||
| [memberId: number]: { | |||||
| leaveEntries: RecordLeaveInput; | |||||
| staffId: string; | |||||
| name: string; | |||||
| }; | |||||
| }; | |||||
| export const fetchTimesheets = cache(async (username: string) => { | export const fetchTimesheets = cache(async (username: string) => { | ||||
| return serverFetchJson<RecordTimesheetInput>(`${BASE_API_URL}/timesheets`, { | return serverFetchJson<RecordTimesheetInput>(`${BASE_API_URL}/timesheets`, { | ||||
| next: { tags: [`timesheets_${username}`] }, | next: { tags: [`timesheets_${username}`] }, | ||||
| @@ -45,3 +53,9 @@ export const fetchTeamMemberTimesheets = cache(async (username: string) => { | |||||
| }, | }, | ||||
| ); | ); | ||||
| }); | }); | ||||
| export const fetchTeamMemberLeaves = cache(async (username: string) => { | |||||
| return serverFetchJson<TeamLeaves>(`${BASE_API_URL}/timesheets/teamLeaves`, { | |||||
| next: { tags: [`team_leaves_${username}`] }, | |||||
| }); | |||||
| }); | |||||
| @@ -1,6 +1,7 @@ | |||||
| import { LeaveType } from "@/app/api/timesheets"; | import { LeaveType } from "@/app/api/timesheets"; | ||||
| import { LeaveEntry } from "@/app/api/timesheets/actions"; | import { LeaveEntry } from "@/app/api/timesheets/actions"; | ||||
| import { LEAVE_DAILY_MAX_HOURS } from "@/app/api/timesheets/utils"; | import { LEAVE_DAILY_MAX_HOURS } from "@/app/api/timesheets/utils"; | ||||
| import { shortDateFormatter } from "@/app/utils/formatUtil"; | |||||
| import { roundToNearestQuarter } from "@/app/utils/manhourUtils"; | import { roundToNearestQuarter } from "@/app/utils/manhourUtils"; | ||||
| import { Check, Delete } from "@mui/icons-material"; | import { Check, Delete } from "@mui/icons-material"; | ||||
| import { | import { | ||||
| @@ -15,16 +16,20 @@ import { | |||||
| Select, | Select, | ||||
| SxProps, | SxProps, | ||||
| TextField, | TextField, | ||||
| Typography, | |||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import React, { useCallback, useEffect } from "react"; | import React, { useCallback, useEffect } from "react"; | ||||
| import { Controller, useForm } from "react-hook-form"; | import { Controller, useForm } from "react-hook-form"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| export interface Props extends Omit<ModalProps, "children"> { | export interface Props extends Omit<ModalProps, "children"> { | ||||
| onSave: (leaveEntry: LeaveEntry) => void; | |||||
| onSave: (leaveEntry: LeaveEntry, recordDate?: string) => Promise<void>; | |||||
| onDelete?: () => void; | onDelete?: () => void; | ||||
| leaveTypes: LeaveType[]; | leaveTypes: LeaveType[]; | ||||
| defaultValues?: Partial<LeaveEntry>; | defaultValues?: Partial<LeaveEntry>; | ||||
| modalSx?: SxProps; | |||||
| recordDate?: string; | |||||
| isHoliday?: boolean; | |||||
| } | } | ||||
| const modalSx: SxProps = { | const modalSx: SxProps = { | ||||
| @@ -46,8 +51,14 @@ const LeaveEditModal: React.FC<Props> = ({ | |||||
| onClose, | onClose, | ||||
| leaveTypes, | leaveTypes, | ||||
| defaultValues, | defaultValues, | ||||
| recordDate, | |||||
| modalSx: mSx, | |||||
| isHoliday, | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation("home"); | |||||
| const { | |||||
| t, | |||||
| i18n: { language }, | |||||
| } = useTranslation("home"); | |||||
| const { register, control, reset, getValues, trigger, formState } = | const { register, control, reset, getValues, trigger, formState } = | ||||
| useForm<LeaveEntry>({ | useForm<LeaveEntry>({ | ||||
| defaultValues: { | defaultValues: { | ||||
| @@ -62,10 +73,10 @@ const LeaveEditModal: React.FC<Props> = ({ | |||||
| const saveHandler = useCallback(async () => { | const saveHandler = useCallback(async () => { | ||||
| const valid = await trigger(); | const valid = await trigger(); | ||||
| if (valid) { | if (valid) { | ||||
| onSave(getValues()); | |||||
| await onSave(getValues(), recordDate); | |||||
| reset({ id: Date.now() }); | reset({ id: Date.now() }); | ||||
| } | } | ||||
| }, [getValues, onSave, reset, trigger]); | |||||
| }, [getValues, onSave, recordDate, reset, trigger]); | |||||
| const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( | const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( | ||||
| (...args) => { | (...args) => { | ||||
| @@ -77,7 +88,16 @@ const LeaveEditModal: React.FC<Props> = ({ | |||||
| return ( | return ( | ||||
| <Modal open={open} onClose={closeHandler}> | <Modal open={open} onClose={closeHandler}> | ||||
| <Paper sx={modalSx}> | |||||
| <Paper sx={{ ...modalSx, ...mSx }}> | |||||
| {recordDate && ( | |||||
| <Typography | |||||
| variant="h6" | |||||
| marginBlockEnd={2} | |||||
| color={isHoliday ? "error.main" : undefined} | |||||
| > | |||||
| {shortDateFormatter(language).format(new Date(recordDate))} | |||||
| </Typography> | |||||
| )} | |||||
| <FormControl fullWidth> | <FormControl fullWidth> | ||||
| <InputLabel>{t("Leave Type")}</InputLabel> | <InputLabel>{t("Leave Type")}</InputLabel> | ||||
| <Controller | <Controller | ||||
| @@ -72,7 +72,7 @@ const MobileLeaveEntry: React.FC<Props> = ({ | |||||
| }, []); | }, []); | ||||
| const onSaveEntry = useCallback( | const onSaveEntry = useCallback( | ||||
| (entry: LeaveEntry) => { | |||||
| async (entry: LeaveEntry) => { | |||||
| const existingEntry = currentEntries.find((e) => e.id === entry.id); | const existingEntry = currentEntries.find((e) => e.id === entry.id); | ||||
| if (existingEntry) { | if (existingEntry) { | ||||
| setValue( | setValue( | ||||
| @@ -1,31 +1,44 @@ | |||||
| import React, { useCallback, useEffect, useMemo, useState } from "react"; | import React, { useCallback, useEffect, useMemo, useState } from "react"; | ||||
| import { HolidaysResult } from "@/app/api/holidays"; | 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 dayGridPlugin from "@fullcalendar/daygrid"; | ||||
| import interactionPlugin from "@fullcalendar/interaction"; | import interactionPlugin from "@fullcalendar/interaction"; | ||||
| import { Autocomplete, Stack, TextField, useTheme } from "@mui/material"; | import { Autocomplete, Stack, TextField, useTheme } from "@mui/material"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import transform from "lodash/transform"; | import transform from "lodash/transform"; | ||||
| import { getPublicHolidaysForNYears } from "@/app/utils/holidayUtils"; | |||||
| import { | |||||
| getHolidayForDate, | |||||
| getPublicHolidaysForNYears, | |||||
| } from "@/app/utils/holidayUtils"; | |||||
| import { | import { | ||||
| INPUT_DATE_FORMAT, | INPUT_DATE_FORMAT, | ||||
| convertDateArrayToString, | convertDateArrayToString, | ||||
| } from "@/app/utils/formatUtil"; | } from "@/app/utils/formatUtil"; | ||||
| import StyledFullCalendar from "../StyledFullCalendar"; | import StyledFullCalendar from "../StyledFullCalendar"; | ||||
| import { ProjectWithTasks } from "@/app/api/projects"; | 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, { | import TimesheetEditModal, { | ||||
| Props as TimesheetEditModalProps, | Props as TimesheetEditModalProps, | ||||
| } from "../TimesheetTable/TimesheetEditModal"; | } from "../TimesheetTable/TimesheetEditModal"; | ||||
| import { Props as LeaveEditModalProps } from "../LeaveTable/LeaveEditModal"; | |||||
| import LeaveEditModal from "../LeaveTable/LeaveEditModal"; | |||||
| import dayjs from "dayjs"; | |||||
| export interface Props { | export interface Props { | ||||
| leaveTypes: LeaveType[]; | |||||
| teamLeaves: TeamLeaves; | |||||
| teamTimesheets: TeamTimeSheets; | teamTimesheets: TeamTimeSheets; | ||||
| companyHolidays: HolidaysResult[]; | companyHolidays: HolidaysResult[]; | ||||
| allProjects: ProjectWithTasks[]; | allProjects: ProjectWithTasks[]; | ||||
| } | } | ||||
| type MemberOption = TeamTimeSheets[0] & { id: string }; | |||||
| type MemberOption = TeamTimeSheets[0] & TeamLeaves[0] & { id: string }; | |||||
| interface EventClickArg { | interface EventClickArg { | ||||
| event: { | event: { | ||||
| @@ -33,7 +46,7 @@ interface EventClickArg { | |||||
| startStr: string; | startStr: string; | ||||
| extendedProps: { | extendedProps: { | ||||
| calendar?: string; | calendar?: string; | ||||
| entry?: TimeEntry; | |||||
| entry?: TimeEntry | LeaveEntry; | |||||
| memberId?: string; | memberId?: string; | ||||
| }; | }; | ||||
| }; | }; | ||||
| @@ -41,8 +54,10 @@ interface EventClickArg { | |||||
| const TimesheetAmendment: React.FC<Props> = ({ | const TimesheetAmendment: React.FC<Props> = ({ | ||||
| teamTimesheets, | teamTimesheets, | ||||
| teamLeaves, | |||||
| companyHolidays, | companyHolidays, | ||||
| allProjects, | allProjects, | ||||
| leaveTypes, | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation(["home", "common"]); | const { t } = useTranslation(["home", "common"]); | ||||
| @@ -56,23 +71,34 @@ const TimesheetAmendment: React.FC<Props> = ({ | |||||
| }, {}); | }, {}); | ||||
| }, [allProjects]); | }, [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 | // Use a local state to manage updates after a mutation | ||||
| const [localTeamTimesheets, setLocalTeamTimesheets] = | const [localTeamTimesheets, setLocalTeamTimesheets] = | ||||
| useState(teamTimesheets); | useState(teamTimesheets); | ||||
| const [localTeamLeaves, setLocalTeamLeaves] = useState(teamLeaves); | |||||
| // member select | // member select | ||||
| const allMembers = useMemo(() => { | const allMembers = useMemo(() => { | ||||
| return transform<TeamTimeSheets[0], MemberOption[]>( | return transform<TeamTimeSheets[0], MemberOption[]>( | ||||
| localTeamTimesheets, | localTeamTimesheets, | ||||
| (acc, memberTimesheet, id) => { | (acc, memberTimesheet, id) => { | ||||
| const leaves = localTeamLeaves[parseInt(id)]; | |||||
| return acc.push({ | return acc.push({ | ||||
| ...leaves, | |||||
| ...memberTimesheet, | ...memberTimesheet, | ||||
| id, | id, | ||||
| }); | }); | ||||
| }, | }, | ||||
| [], | [], | ||||
| ); | ); | ||||
| }, [localTeamTimesheets]); | |||||
| }, [localTeamLeaves, localTeamTimesheets]); | |||||
| const [selectedStaff, setSelectedStaff] = useState<MemberOption>( | const [selectedStaff, setSelectedStaff] = useState<MemberOption>( | ||||
| allMembers[0], | allMembers[0], | ||||
| ); | ); | ||||
| @@ -91,10 +117,11 @@ const TimesheetAmendment: React.FC<Props> = ({ | |||||
| const [editModalOpen, setEditModalOpen] = useState(false); | const [editModalOpen, setEditModalOpen] = useState(false); | ||||
| const openEditModal = useCallback( | const openEditModal = useCallback( | ||||
| (defaultValues?: TimeEntry, recordDate?: string) => { | |||||
| (defaultValues?: TimeEntry, recordDate?: string, isHoliday?: boolean) => { | |||||
| setEditModalProps({ | setEditModalProps({ | ||||
| defaultValues, | defaultValues, | ||||
| recordDate, | recordDate, | ||||
| isHoliday, | |||||
| }); | }); | ||||
| setEditModalOpen(true); | setEditModalOpen(true); | ||||
| }, | }, | ||||
| @@ -105,6 +132,28 @@ const TimesheetAmendment: React.FC<Props> = ({ | |||||
| setEditModalOpen(false); | 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 | // calendar related | ||||
| const holidays = useMemo(() => { | const holidays = useMemo(() => { | ||||
| return [ | return [ | ||||
| @@ -123,11 +172,34 @@ const TimesheetAmendment: React.FC<Props> = ({ | |||||
| })); | })); | ||||
| }, [companyHolidays, theme.palette.error.main]); | }, [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( | const timeEntries = useMemo( | ||||
| () => | () => | ||||
| Object.keys(selectedStaff.timeEntries).flatMap((date, index) => { | Object.keys(selectedStaff.timeEntries).flatMap((date, index) => { | ||||
| return selectedStaff.timeEntries[date].map((entry) => ({ | return selectedStaff.timeEntries[date].map((entry) => ({ | ||||
| id: `${date}-${index}-entry-${entry.id}`, | |||||
| id: `${date}-${index}-time-${entry.id}`, | |||||
| date, | date, | ||||
| title: `${t("{{count}} hour", { | title: `${t("{{count}} hour", { | ||||
| ns: "common", | ns: "common", | ||||
| @@ -151,21 +223,41 @@ const TimesheetAmendment: React.FC<Props> = ({ | |||||
| const handleEventClick = useCallback( | const handleEventClick = useCallback( | ||||
| ({ event }: EventClickArg) => { | ({ event }: EventClickArg) => { | ||||
| const dayJsObj = dayjs(event.startStr); | |||||
| const holiday = getHolidayForDate(event.startStr, companyHolidays); | |||||
| const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; | |||||
| if ( | if ( | ||||
| event.extendedProps.calendar === "timeEntry" && | event.extendedProps.calendar === "timeEntry" && | ||||
| event.extendedProps.entry | 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( | const handleDateClick = useCallback( | ||||
| (e: { dateStr: string }) => { | (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( | const handleSave = useCallback( | ||||
| @@ -189,6 +281,26 @@ const TimesheetAmendment: React.FC<Props> = ({ | |||||
| [selectedStaff.id], | [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 ( | return ( | ||||
| <Stack spacing={2}> | <Stack spacing={2}> | ||||
| <Autocomplete | <Autocomplete | ||||
| @@ -207,7 +319,7 @@ const TimesheetAmendment: React.FC<Props> = ({ | |||||
| plugins={[dayGridPlugin, interactionPlugin]} | plugins={[dayGridPlugin, interactionPlugin]} | ||||
| initialView="dayGridMonth" | initialView="dayGridMonth" | ||||
| buttonText={{ today: t("Today") }} | buttonText={{ today: t("Today") }} | ||||
| events={[...holidays, ...timeEntries]} | |||||
| events={[...holidays, ...timeEntries, ...leaveEntries]} | |||||
| eventClick={handleEventClick} | eventClick={handleEventClick} | ||||
| dateClick={handleDateClick} | dateClick={handleDateClick} | ||||
| /> | /> | ||||
| @@ -220,6 +332,14 @@ const TimesheetAmendment: React.FC<Props> = ({ | |||||
| onSave={handleSave} | onSave={handleSave} | ||||
| {...editModalProps} | {...editModalProps} | ||||
| /> | /> | ||||
| <LeaveEditModal | |||||
| modalSx={{ maxWidth: 400 }} | |||||
| leaveTypes={leaveTypes} | |||||
| open={leaveEditModalOpen} | |||||
| onClose={closeLeaveEditModal} | |||||
| onSave={handleSaveLeave} | |||||
| {...leaveEditModalProps} | |||||
| /> | |||||
| </Stack> | </Stack> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -32,6 +32,8 @@ interface Props extends TimesheetAmendmentProps { | |||||
| export const TimesheetAmendmentModal: React.FC<Props> = ({ | export const TimesheetAmendmentModal: React.FC<Props> = ({ | ||||
| open, | open, | ||||
| onClose, | onClose, | ||||
| leaveTypes, | |||||
| teamLeaves, | |||||
| teamTimesheets, | teamTimesheets, | ||||
| companyHolidays, | companyHolidays, | ||||
| allProjects, | allProjects, | ||||
| @@ -42,6 +44,8 @@ export const TimesheetAmendmentModal: React.FC<Props> = ({ | |||||
| const title = t("Timesheet Amendment"); | const title = t("Timesheet Amendment"); | ||||
| const content = ( | const content = ( | ||||
| <TimesheetAmendment | <TimesheetAmendment | ||||
| leaveTypes={leaveTypes} | |||||
| teamLeaves={teamLeaves} | |||||
| companyHolidays={companyHolidays} | companyHolidays={companyHolidays} | ||||
| teamTimesheets={teamTimesheets} | teamTimesheets={teamTimesheets} | ||||
| allProjects={allProjects} | allProjects={allProjects} | ||||
| @@ -105,7 +105,7 @@ const TimesheetEditModal: React.FC<Props> = ({ | |||||
| const saveHandler = useCallback(async () => { | const saveHandler = useCallback(async () => { | ||||
| const valid = await trigger(); | const valid = await trigger(); | ||||
| if (valid) { | if (valid) { | ||||
| onSave(getValues(), recordDate); | |||||
| await onSave(getValues(), recordDate); | |||||
| reset({ id: Date.now() }); | reset({ id: Date.now() }); | ||||
| } | } | ||||
| }, [getValues, onSave, recordDate, reset, trigger]); | }, [getValues, onSave, recordDate, reset, trigger]); | ||||
| @@ -126,7 +126,11 @@ const TimesheetEditModal: React.FC<Props> = ({ | |||||
| <Modal open={open} onClose={closeHandler}> | <Modal open={open} onClose={closeHandler}> | ||||
| <Paper sx={{ ...modalSx, ...mSx }}> | <Paper sx={{ ...modalSx, ...mSx }}> | ||||
| {recordDate && ( | {recordDate && ( | ||||
| <Typography variant="h6" marginBlockEnd={2}> | |||||
| <Typography | |||||
| variant="h6" | |||||
| marginBlockEnd={2} | |||||
| color={isHoliday ? "error.main" : undefined} | |||||
| > | |||||
| {shortDateFormatter(language).format(new Date(recordDate))} | {shortDateFormatter(language).format(new Date(recordDate))} | ||||
| </Typography> | </Typography> | ||||
| )} | )} | ||||
| @@ -20,7 +20,7 @@ import { | |||||
| revalidateCacheAfterAmendment, | revalidateCacheAfterAmendment, | ||||
| } from "@/app/api/timesheets/actions"; | } from "@/app/api/timesheets/actions"; | ||||
| import LeaveModal from "../LeaveModal"; | import LeaveModal from "../LeaveModal"; | ||||
| import { LeaveType, TeamTimeSheets } from "@/app/api/timesheets"; | |||||
| import { LeaveType, TeamLeaves, TeamTimeSheets } from "@/app/api/timesheets"; | |||||
| import { CalendarIcon } from "@mui/x-date-pickers"; | import { CalendarIcon } from "@mui/x-date-pickers"; | ||||
| import PastEntryCalendarModal from "../PastEntryCalendar/PastEntryCalendarModal"; | import PastEntryCalendarModal from "../PastEntryCalendar/PastEntryCalendarModal"; | ||||
| import { HolidaysResult } from "@/app/api/holidays"; | import { HolidaysResult } from "@/app/api/holidays"; | ||||
| @@ -35,6 +35,7 @@ export interface Props { | |||||
| defaultTimesheets: RecordTimesheetInput; | defaultTimesheets: RecordTimesheetInput; | ||||
| holidays: HolidaysResult[]; | holidays: HolidaysResult[]; | ||||
| teamTimesheets: TeamTimeSheets; | teamTimesheets: TeamTimeSheets; | ||||
| teamLeaves: TeamLeaves; | |||||
| fastEntryEnabled?: boolean; | fastEntryEnabled?: boolean; | ||||
| } | } | ||||
| @@ -52,6 +53,7 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||||
| defaultTimesheets, | defaultTimesheets, | ||||
| holidays, | holidays, | ||||
| teamTimesheets, | teamTimesheets, | ||||
| teamLeaves, | |||||
| fastEntryEnabled, | fastEntryEnabled, | ||||
| }) => { | }) => { | ||||
| const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); | const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); | ||||
| @@ -201,7 +203,9 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||||
| {showTimesheetAmendment && ( | {showTimesheetAmendment && ( | ||||
| <TimesheetAmendmentModal | <TimesheetAmendmentModal | ||||
| allProjects={allProjects} | allProjects={allProjects} | ||||
| leaveTypes={leaveTypes} | |||||
| companyHolidays={holidays} | companyHolidays={holidays} | ||||
| teamLeaves={teamLeaves} | |||||
| teamTimesheets={teamTimesheets} | teamTimesheets={teamTimesheets} | ||||
| open={isTimesheetAmendmentVisible} | open={isTimesheetAmendmentVisible} | ||||
| onClose={handleAmendmentClose} | onClose={handleAmendmentClose} | ||||
| @@ -6,6 +6,7 @@ import UserWorkspacePage from "./UserWorkspacePage"; | |||||
| import { | import { | ||||
| fetchLeaveTypes, | fetchLeaveTypes, | ||||
| fetchLeaves, | fetchLeaves, | ||||
| fetchTeamMemberLeaves, | |||||
| fetchTeamMemberTimesheets, | fetchTeamMemberTimesheets, | ||||
| fetchTimesheets, | fetchTimesheets, | ||||
| } from "@/app/api/timesheets"; | } from "@/app/api/timesheets"; | ||||
| @@ -17,6 +18,7 @@ interface Props { | |||||
| const UserWorkspaceWrapper: React.FC<Props> = async ({ username }) => { | const UserWorkspaceWrapper: React.FC<Props> = async ({ username }) => { | ||||
| const [ | const [ | ||||
| teamLeaves, | |||||
| teamTimesheets, | teamTimesheets, | ||||
| assignedProjects, | assignedProjects, | ||||
| allProjects, | allProjects, | ||||
| @@ -25,6 +27,7 @@ const UserWorkspaceWrapper: React.FC<Props> = async ({ username }) => { | |||||
| leaveTypes, | leaveTypes, | ||||
| holidays, | holidays, | ||||
| ] = await Promise.all([ | ] = await Promise.all([ | ||||
| fetchTeamMemberLeaves(username), | |||||
| fetchTeamMemberTimesheets(username), | fetchTeamMemberTimesheets(username), | ||||
| fetchAssignedProjects(username), | fetchAssignedProjects(username), | ||||
| fetchProjectWithTasks(), | fetchProjectWithTasks(), | ||||
| @@ -36,6 +39,7 @@ const UserWorkspaceWrapper: React.FC<Props> = async ({ username }) => { | |||||
| return ( | return ( | ||||
| <UserWorkspacePage | <UserWorkspacePage | ||||
| teamLeaves={teamLeaves} | |||||
| teamTimesheets={teamTimesheets} | teamTimesheets={teamTimesheets} | ||||
| allProjects={allProjects} | allProjects={allProjects} | ||||
| assignedProjects={assignedProjects} | assignedProjects={assignedProjects} | ||||