From 01581b4b91bbdff4907e44a47f6e1fbe86b047d1 Mon Sep 17 00:00:00 2001 From: Wayne Date: Sun, 19 May 2024 22:32:26 +0900 Subject: [PATCH] Allow new events for time amendment --- src/app/api/timesheets/actions.ts | 20 ++++-- .../TimesheetAmendment/TimesheetAmendment.tsx | 62 +++++++++++++++---- .../TimesheetAmendmentModal.tsx | 3 - .../TimesheetTable/ProjectSelect.tsx | 6 +- .../TimesheetTable/TimesheetEditModal.tsx | 20 ++++-- .../UserWorkspacePage/UserWorkspacePage.tsx | 2 + 6 files changed, 84 insertions(+), 29 deletions(-) diff --git a/src/app/api/timesheets/actions.ts b/src/app/api/timesheets/actions.ts index 49ebb4f..106e2d4 100644 --- a/src/app/api/timesheets/actions.ts +++ b/src/app/api/timesheets/actions.ts @@ -4,7 +4,7 @@ import { serverFetchJson } from "@/app/utils/fetchUtil"; import { ProjectResult } from "../projects"; import { Task, TaskGroup } from "../tasks"; import { BASE_API_URL } from "@/config/api"; -import { revalidateTag } from "next/cache"; +import { revalidatePath, revalidateTag } from "next/cache"; export interface TimeEntry { id: number; @@ -67,10 +67,18 @@ export const saveLeave = async (data: RecordLeaveInput, username: string) => { export const saveMemberEntry = async (data: { staffId: number; entry: TimeEntry; + recordDate?: string; }) => { - return serverFetchJson(`${BASE_API_URL}/timesheets/saveMemberEntry`, { - method: "POST", - body: JSON.stringify(data), - headers: { "Content-Type": "application/json" }, - }); + return serverFetchJson( + `${BASE_API_URL}/timesheets/saveMemberEntry`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); +}; + +export const revalidateCacheAfterAmendment = () => { + revalidatePath("/(main)/home"); }; diff --git a/src/components/TimesheetAmendment/TimesheetAmendment.tsx b/src/components/TimesheetAmendment/TimesheetAmendment.tsx index 237dc00..7ebcec8 100644 --- a/src/components/TimesheetAmendment/TimesheetAmendment.tsx +++ b/src/components/TimesheetAmendment/TimesheetAmendment.tsx @@ -1,8 +1,9 @@ -import React, { useCallback, useMemo, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import { HolidaysResult } from "@/app/api/holidays"; import { 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"; @@ -29,6 +30,7 @@ type MemberOption = TeamTimeSheets[0] & { id: string }; interface EventClickArg { event: { start: Date | null; + startStr: string; extendedProps: { calendar?: string; entry?: TimeEntry; @@ -54,10 +56,14 @@ const TimesheetAmendment: React.FC = ({ }, {}); }, [allProjects]); + // Use a local state to manage updates after a mutation + const [localTeamTimesheets, setLocalTeamTimesheets] = + useState(teamTimesheets); + // member select const allMembers = useMemo(() => { return transform( - teamTimesheets, + localTeamTimesheets, (acc, memberTimesheet, id) => { return acc.push({ ...memberTimesheet, @@ -66,10 +72,17 @@ const TimesheetAmendment: React.FC = ({ }, [], ); - }, [teamTimesheets]); + }, [localTeamTimesheets]); const [selectedStaff, setSelectedStaff] = useState( allMembers[0], ); + useEffect(() => { + setSelectedStaff( + (currentStaff) => + allMembers.find((member) => member.id === currentStaff.id) || + allMembers[0], + ); + }, [allMembers]); // edit modal related const [editModalProps, setEditModalProps] = useState< @@ -77,12 +90,16 @@ const TimesheetAmendment: React.FC = ({ >({}); const [editModalOpen, setEditModalOpen] = useState(false); - const openEditModal = useCallback((defaultValues?: TimeEntry) => { - setEditModalProps({ - defaultValues, - }); - setEditModalOpen(true); - }, []); + const openEditModal = useCallback( + (defaultValues?: TimeEntry, recordDate?: string) => { + setEditModalProps({ + defaultValues, + recordDate, + }); + setEditModalOpen(true); + }, + [], + ); const closeEditModal = useCallback(() => { setEditModalOpen(false); @@ -138,17 +155,35 @@ const TimesheetAmendment: React.FC = ({ event.extendedProps.calendar === "timeEntry" && event.extendedProps.entry ) { - openEditModal(event.extendedProps.entry); + openEditModal(event.extendedProps.entry, event.startStr); } }, [openEditModal], ); + const handleDateClick = useCallback( + (e: { dateStr: string }) => { + openEditModal(undefined, e.dateStr); + }, + [openEditModal], + ); + const handleSave = useCallback( - async (timeEntry: TimeEntry) => { + async (timeEntry: TimeEntry, recordDate?: string) => { // TODO: should be fine, but can handle parse error const intStaffId = parseInt(selectedStaff.id); - await saveMemberEntry({ staffId: intStaffId, entry: timeEntry }); + const newMemberTimesheets = await saveMemberEntry({ + staffId: intStaffId, + entry: timeEntry, + recordDate, + }); + setLocalTeamTimesheets((timesheets) => ({ + ...timesheets, + [intStaffId]: { + ...timesheets[intStaffId], + timeEntries: newMemberTimesheets, + }, + })); setEditModalOpen(false); }, [selectedStaff.id], @@ -169,11 +204,12 @@ const TimesheetAmendment: React.FC = ({ renderInput={(params) => } /> { return t("Assigned Projects"); case "non-assigned": return t("Non-assigned Projects"); + case "all-projects": + return t("All projects"); default: return t("Ungrouped"); } @@ -58,7 +58,7 @@ const AutocompleteProjectSelect: React.FC = ({ ...nonAssignedProjects.map((p) => ({ value: p.id, label: `${p.code} - ${p.name}`, - group: "non-assigned", + group: assignedProjects.length === 0 ? "all-projects" : "non-assigned", })), ]; }, [assignedProjects, nonAssignedProjects, t]); diff --git a/src/components/TimesheetTable/TimesheetEditModal.tsx b/src/components/TimesheetTable/TimesheetEditModal.tsx index 7e54eef..7a24f25 100644 --- a/src/components/TimesheetTable/TimesheetEditModal.tsx +++ b/src/components/TimesheetTable/TimesheetEditModal.tsx @@ -10,6 +10,7 @@ import { Paper, SxProps, TextField, + Typography, } from "@mui/material"; import React, { useCallback, useEffect, useMemo } from "react"; import { Controller, useForm } from "react-hook-form"; @@ -21,14 +22,16 @@ import TaskSelect from "./TaskSelect"; import { TaskGroup } from "@/app/api/tasks"; import uniqBy from "lodash/uniqBy"; import { roundToNearestQuarter } from "@/app/utils/manhourUtils"; +import { shortDateFormatter } from "@/app/utils/formatUtil"; export interface Props extends Omit { - onSave: (timeEntry: TimeEntry) => Promise; + onSave: (timeEntry: TimeEntry, recordDate?: string) => Promise; onDelete?: () => void; defaultValues?: Partial; allProjects: ProjectWithTasks[]; assignedProjects: AssignedProject[]; modalSx?: SxProps; + recordDate?: string; } const modalSx: SxProps = { @@ -52,8 +55,12 @@ const TimesheetEditModal: React.FC = ({ allProjects, assignedProjects, modalSx: mSx, + recordDate, }) => { - const { t } = useTranslation("home"); + const { + t, + i18n: { language }, + } = useTranslation("home"); const taskGroupsByProject = useMemo(() => { return allProjects.reduce<{ @@ -93,10 +100,10 @@ const TimesheetEditModal: React.FC = ({ const saveHandler = useCallback(async () => { const valid = await trigger(); if (valid) { - onSave(getValues()); + onSave(getValues(), recordDate); reset({ id: Date.now() }); } - }, [getValues, onSave, reset, trigger]); + }, [getValues, onSave, recordDate, reset, trigger]); const closeHandler = useCallback>( (...args) => { @@ -113,6 +120,11 @@ const TimesheetEditModal: React.FC = ({ return ( + {recordDate && ( + + {shortDateFormatter(language).format(new Date(recordDate))} + + )} {t("Project Code and Name")} = ({ const handleAmendmentClose = useCallback(() => { setisTimesheetAmendmentVisible(false); + revalidateCacheAfterAmendment(); }, []); return (