Pārlūkot izejas kodu

Allow new events for time amendment

tags/Baseline_30082024_FRONTEND_UAT
Wayne pirms 1 gada
vecāks
revīzija
01581b4b91
6 mainītis faili ar 84 papildinājumiem un 29 dzēšanām
  1. +14
    -6
      src/app/api/timesheets/actions.ts
  2. +49
    -13
      src/components/TimesheetAmendment/TimesheetAmendment.tsx
  3. +0
    -3
      src/components/TimesheetAmendment/TimesheetAmendmentModal.tsx
  4. +3
    -3
      src/components/TimesheetTable/ProjectSelect.tsx
  5. +16
    -4
      src/components/TimesheetTable/TimesheetEditModal.tsx
  6. +2
    -0
      src/components/UserWorkspacePage/UserWorkspacePage.tsx

+ 14
- 6
src/app/api/timesheets/actions.ts Parādīt failu

@@ -4,7 +4,7 @@ import { serverFetchJson } from "@/app/utils/fetchUtil";
import { ProjectResult } from "../projects"; import { ProjectResult } from "../projects";
import { Task, TaskGroup } from "../tasks"; import { Task, TaskGroup } from "../tasks";
import { BASE_API_URL } from "@/config/api"; import { BASE_API_URL } from "@/config/api";
import { revalidateTag } from "next/cache";
import { revalidatePath, revalidateTag } from "next/cache";


export interface TimeEntry { export interface TimeEntry {
id: number; id: number;
@@ -67,10 +67,18 @@ export const saveLeave = async (data: RecordLeaveInput, username: string) => {
export const saveMemberEntry = async (data: { export const saveMemberEntry = async (data: {
staffId: number; staffId: number;
entry: TimeEntry; entry: TimeEntry;
recordDate?: string;
}) => { }) => {
return serverFetchJson<TimeEntry>(`${BASE_API_URL}/timesheets/saveMemberEntry`, {
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
return serverFetchJson<RecordTimesheetInput>(
`${BASE_API_URL}/timesheets/saveMemberEntry`,
{
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
},
);
};

export const revalidateCacheAfterAmendment = () => {
revalidatePath("/(main)/home");
}; };

+ 49
- 13
src/components/TimesheetAmendment/TimesheetAmendment.tsx Parādīt failu

@@ -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 { HolidaysResult } from "@/app/api/holidays";
import { TeamTimeSheets } from "@/app/api/timesheets"; import { TeamTimeSheets } from "@/app/api/timesheets";
import dayGridPlugin from "@fullcalendar/daygrid"; import dayGridPlugin from "@fullcalendar/daygrid";
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";
@@ -29,6 +30,7 @@ type MemberOption = TeamTimeSheets[0] & { id: string };
interface EventClickArg { interface EventClickArg {
event: { event: {
start: Date | null; start: Date | null;
startStr: string;
extendedProps: { extendedProps: {
calendar?: string; calendar?: string;
entry?: TimeEntry; entry?: TimeEntry;
@@ -54,10 +56,14 @@ const TimesheetAmendment: React.FC<Props> = ({
}, {}); }, {});
}, [allProjects]); }, [allProjects]);


// Use a local state to manage updates after a mutation
const [localTeamTimesheets, setLocalTeamTimesheets] =
useState(teamTimesheets);

// member select // member select
const allMembers = useMemo(() => { const allMembers = useMemo(() => {
return transform<TeamTimeSheets[0], MemberOption[]>( return transform<TeamTimeSheets[0], MemberOption[]>(
teamTimesheets,
localTeamTimesheets,
(acc, memberTimesheet, id) => { (acc, memberTimesheet, id) => {
return acc.push({ return acc.push({
...memberTimesheet, ...memberTimesheet,
@@ -66,10 +72,17 @@ const TimesheetAmendment: React.FC<Props> = ({
}, },
[], [],
); );
}, [teamTimesheets]);
}, [localTeamTimesheets]);
const [selectedStaff, setSelectedStaff] = useState<MemberOption>( const [selectedStaff, setSelectedStaff] = useState<MemberOption>(
allMembers[0], allMembers[0],
); );
useEffect(() => {
setSelectedStaff(
(currentStaff) =>
allMembers.find((member) => member.id === currentStaff.id) ||
allMembers[0],
);
}, [allMembers]);


// edit modal related // edit modal related
const [editModalProps, setEditModalProps] = useState< const [editModalProps, setEditModalProps] = useState<
@@ -77,12 +90,16 @@ const TimesheetAmendment: React.FC<Props> = ({
>({}); >({});
const [editModalOpen, setEditModalOpen] = useState(false); 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(() => { const closeEditModal = useCallback(() => {
setEditModalOpen(false); setEditModalOpen(false);
@@ -138,17 +155,35 @@ const TimesheetAmendment: React.FC<Props> = ({
event.extendedProps.calendar === "timeEntry" && event.extendedProps.calendar === "timeEntry" &&
event.extendedProps.entry event.extendedProps.entry
) { ) {
openEditModal(event.extendedProps.entry);
openEditModal(event.extendedProps.entry, event.startStr);
} }
}, },
[openEditModal], [openEditModal],
); );


const handleDateClick = useCallback(
(e: { dateStr: string }) => {
openEditModal(undefined, e.dateStr);
},
[openEditModal],
);

const handleSave = useCallback( const handleSave = useCallback(
async (timeEntry: TimeEntry) => {
async (timeEntry: TimeEntry, recordDate?: string) => {
// TODO: should be fine, but can handle parse error // TODO: should be fine, but can handle parse error
const intStaffId = parseInt(selectedStaff.id); 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); setEditModalOpen(false);
}, },
[selectedStaff.id], [selectedStaff.id],
@@ -169,11 +204,12 @@ const TimesheetAmendment: React.FC<Props> = ({
renderInput={(params) => <TextField {...params} />} renderInput={(params) => <TextField {...params} />}
/> />
<StyledFullCalendar <StyledFullCalendar
plugins={[dayGridPlugin]}
plugins={[dayGridPlugin, interactionPlugin]}
initialView="dayGridMonth" initialView="dayGridMonth"
buttonText={{ today: t("Today") }} buttonText={{ today: t("Today") }}
events={[...holidays, ...timeEntries]} events={[...holidays, ...timeEntries]}
eventClick={handleEventClick} eventClick={handleEventClick}
dateClick={handleDateClick}
/> />
<TimesheetEditModal <TimesheetEditModal
modalSx={{ maxWidth: 400 }} modalSx={{ maxWidth: 400 }}


+ 0
- 3
src/components/TimesheetAmendment/TimesheetAmendmentModal.tsx Parādīt failu

@@ -5,9 +5,6 @@ import {
Box, Box,
Card, Card,
CardContent, CardContent,
Dialog,
DialogContent,
DialogTitle,
Modal, Modal,
SxProps, SxProps,
Typography, Typography,


+ 3
- 3
src/components/TimesheetTable/ProjectSelect.tsx Parādīt failu

@@ -3,8 +3,6 @@ import {
Autocomplete, Autocomplete,
ListSubheader, ListSubheader,
MenuItem, MenuItem,
Select,
SelectChangeEvent,
TextField, TextField,
} from "@mui/material"; } from "@mui/material";
import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; import { AssignedProject, ProjectWithTasks } from "@/app/api/projects";
@@ -27,6 +25,8 @@ const getGroupName = (t: TFunction, groupName: string): string => {
return t("Assigned Projects"); return t("Assigned Projects");
case "non-assigned": case "non-assigned":
return t("Non-assigned Projects"); return t("Non-assigned Projects");
case "all-projects":
return t("All projects");
default: default:
return t("Ungrouped"); return t("Ungrouped");
} }
@@ -58,7 +58,7 @@ const AutocompleteProjectSelect: React.FC<Props> = ({
...nonAssignedProjects.map((p) => ({ ...nonAssignedProjects.map((p) => ({
value: p.id, value: p.id,
label: `${p.code} - ${p.name}`, label: `${p.code} - ${p.name}`,
group: "non-assigned",
group: assignedProjects.length === 0 ? "all-projects" : "non-assigned",
})), })),
]; ];
}, [assignedProjects, nonAssignedProjects, t]); }, [assignedProjects, nonAssignedProjects, t]);


+ 16
- 4
src/components/TimesheetTable/TimesheetEditModal.tsx Parādīt failu

@@ -10,6 +10,7 @@ import {
Paper, Paper,
SxProps, SxProps,
TextField, TextField,
Typography,
} from "@mui/material"; } from "@mui/material";
import React, { useCallback, useEffect, useMemo } from "react"; import React, { useCallback, useEffect, useMemo } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
@@ -21,14 +22,16 @@ import TaskSelect from "./TaskSelect";
import { TaskGroup } from "@/app/api/tasks"; import { TaskGroup } from "@/app/api/tasks";
import uniqBy from "lodash/uniqBy"; import uniqBy from "lodash/uniqBy";
import { roundToNearestQuarter } from "@/app/utils/manhourUtils"; import { roundToNearestQuarter } from "@/app/utils/manhourUtils";
import { shortDateFormatter } from "@/app/utils/formatUtil";


export interface Props extends Omit<ModalProps, "children"> { export interface Props extends Omit<ModalProps, "children"> {
onSave: (timeEntry: TimeEntry) => Promise<void>;
onSave: (timeEntry: TimeEntry, recordDate?: string) => Promise<void>;
onDelete?: () => void; onDelete?: () => void;
defaultValues?: Partial<TimeEntry>; defaultValues?: Partial<TimeEntry>;
allProjects: ProjectWithTasks[]; allProjects: ProjectWithTasks[];
assignedProjects: AssignedProject[]; assignedProjects: AssignedProject[];
modalSx?: SxProps; modalSx?: SxProps;
recordDate?: string;
} }


const modalSx: SxProps = { const modalSx: SxProps = {
@@ -52,8 +55,12 @@ const TimesheetEditModal: React.FC<Props> = ({
allProjects, allProjects,
assignedProjects, assignedProjects,
modalSx: mSx, modalSx: mSx,
recordDate,
}) => { }) => {
const { t } = useTranslation("home");
const {
t,
i18n: { language },
} = useTranslation("home");


const taskGroupsByProject = useMemo(() => { const taskGroupsByProject = useMemo(() => {
return allProjects.reduce<{ return allProjects.reduce<{
@@ -93,10 +100,10 @@ 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());
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) => {
@@ -113,6 +120,11 @@ const TimesheetEditModal: React.FC<Props> = ({
return ( return (
<Modal open={open} onClose={closeHandler}> <Modal open={open} onClose={closeHandler}>
<Paper sx={{ ...modalSx, ...mSx }}> <Paper sx={{ ...modalSx, ...mSx }}>
{recordDate && (
<Typography variant="h6" marginBlockEnd={2}>
{shortDateFormatter(language).format(new Date(recordDate))}
</Typography>
)}
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel shrink>{t("Project Code and Name")}</InputLabel> <InputLabel shrink>{t("Project Code and Name")}</InputLabel>
<Controller <Controller


+ 2
- 0
src/components/UserWorkspacePage/UserWorkspacePage.tsx Parādīt failu

@@ -17,6 +17,7 @@ import { AssignedProject, ProjectWithTasks } from "@/app/api/projects";
import { import {
RecordLeaveInput, RecordLeaveInput,
RecordTimesheetInput, RecordTimesheetInput,
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, TeamTimeSheets } from "@/app/api/timesheets";
@@ -106,6 +107,7 @@ const UserWorkspacePage: React.FC<Props> = ({


const handleAmendmentClose = useCallback(() => { const handleAmendmentClose = useCallback(() => {
setisTimesheetAmendmentVisible(false); setisTimesheetAmendmentVisible(false);
revalidateCacheAfterAmendment();
}, []); }, []);


return ( return (


Notiek ielāde…
Atcelt
Saglabāt