Browse Source

Past events

tags/Baseline_30082024_FRONTEND_UAT
Wayne 1 year ago
parent
commit
10fa56dcab
12 changed files with 281 additions and 107 deletions
  1. +3
    -5
      src/components/LeaveModal/LeaveModal.tsx
  2. +7
    -3
      src/components/LeaveTable/LeaveEditModal.tsx
  3. +63
    -0
      src/components/LeaveTable/LeaveEntryCard.tsx
  4. +6
    -53
      src/components/LeaveTable/MobileLeaveEntry.tsx
  5. +76
    -34
      src/components/PastEntryCalendar/PastEntryCalendarModal.tsx
  6. +101
    -0
      src/components/PastEntryCalendar/PastEntryList.tsx
  7. +3
    -5
      src/components/TimesheetModal/TimesheetModal.tsx
  8. +1
    -1
      src/components/TimesheetTable/MobileTimesheetEntry.tsx
  9. +2
    -2
      src/components/TimesheetTable/TimesheetEditModal.tsx
  10. +7
    -4
      src/components/TransferList/TransferListWrapper.tsx
  11. +2
    -0
      src/components/UserWorkspacePage/UserWorkspacePage.tsx
  12. +10
    -0
      src/components/utils/useIsMobile.ts

+ 3
- 5
src/components/LeaveModal/LeaveModal.tsx View File

@@ -9,8 +9,6 @@ import {
ModalProps, ModalProps,
SxProps, SxProps,
Typography, Typography,
useMediaQuery,
useTheme,
} from "@mui/material"; } from "@mui/material";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Check, Close } from "@mui/icons-material"; import { Check, Close } from "@mui/icons-material";
@@ -26,6 +24,7 @@ import LeaveTable from "../LeaveTable";
import { LeaveType } from "@/app/api/timesheets"; import { LeaveType } from "@/app/api/timesheets";
import FullscreenModal from "../FullscreenModal"; import FullscreenModal from "../FullscreenModal";
import MobileLeaveTable from "../LeaveTable/MobileLeaveTable"; import MobileLeaveTable from "../LeaveTable/MobileLeaveTable";
import useIsMobile from "../utils/useIsMobile";


interface Props { interface Props {
isOpen: boolean; isOpen: boolean;
@@ -106,12 +105,11 @@ const LeaveModal: React.FC<Props> = ({
[onCancel], [onCancel],
); );


const theme = useTheme();
const matches = useMediaQuery(theme.breakpoints.up("sm"));
const matches = useIsMobile();


return ( return (
<FormProvider {...formProps}> <FormProvider {...formProps}>
{matches ? (
{!matches ? (
// Desktop version // Desktop version
<Modal open={isOpen} onClose={onModalClose}> <Modal open={isOpen} onClose={onModalClose}>
<Card sx={modalSx}> <Card sx={modalSx}>


+ 7
- 3
src/components/LeaveTable/LeaveEditModal.tsx View File

@@ -47,7 +47,11 @@ const LeaveEditModal: React.FC<Props> = ({
}) => { }) => {
const { t } = useTranslation("home"); const { t } = useTranslation("home");
const { register, control, reset, getValues, trigger, formState } = const { register, control, reset, getValues, trigger, formState } =
useForm<LeaveEntry>();
useForm<LeaveEntry>({
defaultValues: {
leaveTypeId: leaveTypes[0].id,
},
});


useEffect(() => { useEffect(() => {
reset(defaultValues ?? { leaveTypeId: leaveTypes[0].id, id: Date.now() }); reset(defaultValues ?? { leaveTypeId: leaveTypes[0].id, id: Date.now() });
@@ -57,14 +61,14 @@ const LeaveEditModal: React.FC<Props> = ({
const valid = await trigger(); const valid = await trigger();
if (valid) { if (valid) {
onSave(getValues()); onSave(getValues());
reset();
reset({ id: Date.now() });
} }
}, [getValues, onSave, reset, trigger]); }, [getValues, onSave, reset, trigger]);


const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>(
(...args) => { (...args) => {
onClose?.(...args); onClose?.(...args);
reset();
reset({ id: Date.now() });
}, },
[onClose, reset], [onClose, reset],
); );


+ 63
- 0
src/components/LeaveTable/LeaveEntryCard.tsx View File

@@ -0,0 +1,63 @@
import { LeaveType } from "@/app/api/timesheets";
import { LeaveEntry } from "@/app/api/timesheets/actions";
import { manhourFormatter } from "@/app/utils/formatUtil";
import { Edit } from "@mui/icons-material";
import { Box, Card, CardContent, IconButton, Typography } from "@mui/material";
import { useTranslation } from "react-i18next";

interface Props {
entry: LeaveEntry;
onEdit?: () => void;
leaveTypeMap: {
[id: number]: LeaveType;
};
}

const LeaveEntryCard: React.FC<Props> = ({ entry, onEdit, leaveTypeMap }) => {
const { t } = useTranslation("home");
return (
<Card sx={{ marginInline: 1, overflow: "visible" }}>
<CardContent
sx={{
padding: 2,
display: "flex",
flexDirection: "column",
gap: 2,
"&:last-child": {
paddingBottom: 2,
},
}}
>
<Box
display="flex"
justifyContent="space-between"
alignItems="flex-start"
>
<Box>
<Typography variant="body2" component="div" fontWeight="bold">
{leaveTypeMap[entry.leaveTypeId].name}
</Typography>
<Typography component="p">
{manhourFormatter.format(entry.inputHours)}
</Typography>
</Box>
{onEdit && (
<IconButton size="small" color="primary" onClick={onEdit}>
<Edit />
</IconButton>
)}
</Box>
{entry.remark && (
<Box>
<Typography variant="body2" component="div" fontWeight="bold">
{t("Remark")}
</Typography>
<Typography component="p">{entry.remark}</Typography>
</Box>
)}
</CardContent>
</Card>
);
};

export default LeaveEntryCard;

+ 6
- 53
src/components/LeaveTable/MobileLeaveEntry.tsx View File

@@ -15,6 +15,7 @@ import React, { useCallback, useMemo, useState } from "react";
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import LeaveEditModal, { Props as LeaveEditModalProps } from "./LeaveEditModal"; import LeaveEditModal, { Props as LeaveEditModalProps } from "./LeaveEditModal";
import LeaveEntryCard from "./LeaveEntryCard";


interface Props { interface Props {
date: string; date: string;
@@ -105,60 +106,12 @@ const MobileLeaveEntry: React.FC<Props> = ({ date, leaveTypes }) => {
{currentEntries.length ? ( {currentEntries.length ? (
currentEntries.map((entry, index) => { currentEntries.map((entry, index) => {
return ( return (
<Card
<LeaveEntryCard
key={`${entry.id}-${index}`} key={`${entry.id}-${index}`}
sx={{ marginInline: 1, overflow: "visible" }}
>
<CardContent
sx={{
padding: 2,
display: "flex",
flexDirection: "column",
gap: 2,
"&:last-child": {
paddingBottom: 2,
},
}}
>
<Box
display="flex"
justifyContent="space-between"
alignItems="flex-start"
>
<Box>
<Typography
variant="body2"
component="div"
fontWeight="bold"
>
{leaveTypeMap[entry.leaveTypeId].name}
</Typography>
<Typography component="p">
{manhourFormatter.format(entry.inputHours)}
</Typography>
</Box>
<IconButton
size="small"
color="primary"
onClick={openEditModal(entry)}
>
<Edit />
</IconButton>
</Box>
{entry.remark && (
<Box>
<Typography
variant="body2"
component="div"
fontWeight="bold"
>
{t("Remark")}
</Typography>
<Typography component="p">{entry.remark}</Typography>
</Box>
)}
</CardContent>
</Card>
entry={entry}
onEdit={openEditModal(entry)}
leaveTypeMap={leaveTypeMap}
/>
); );
}) })
) : ( ) : (


+ 76
- 34
src/components/PastEntryCalendar/PastEntryCalendarModal.tsx View File

@@ -15,10 +15,17 @@ import PastEntryCalendar, {
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ArrowBack } from "@mui/icons-material"; import { ArrowBack } from "@mui/icons-material";
import PastEntryList from "./PastEntryList";
import { ProjectWithTasks } from "@/app/api/projects";
import { LeaveType } from "@/app/api/timesheets";
import useIsMobile from "../utils/useIsMobile";
import FullscreenModal from "../FullscreenModal";


interface Props extends Omit<PastEntryCalendarProps, "onDateSelect"> { interface Props extends Omit<PastEntryCalendarProps, "onDateSelect"> {
open: boolean; open: boolean;
handleClose: () => void; handleClose: () => void;
leaveTypes: LeaveType[];
allProjects: ProjectWithTasks[];
} }


const Indicator = styled(Box)(() => ({ const Indicator = styled(Box)(() => ({
@@ -32,6 +39,8 @@ const PastEntryCalendarModal: React.FC<Props> = ({
open, open,
timesheet, timesheet,
leaves, leaves,
leaveTypes,
allProjects,
}) => { }) => {
const { t } = useTranslation("home"); const { t } = useTranslation("home");


@@ -45,42 +54,75 @@ const PastEntryCalendarModal: React.FC<Props> = ({
handleClose(); handleClose();
}, [handleClose]); }, [handleClose]);


return (
const content = selectedDate ? (
<>
<PastEntryList
date={selectedDate}
timesheet={timesheet}
leaves={leaves}
allProjects={allProjects}
leaveTypes={leaveTypes}
/>
</>
) : (
<>
<Stack marginBlockEnd={2}>
<Box display="flex" alignItems="center" gap={1}>
<Indicator sx={{ backgroundColor: "info.light" }} />
<Typography variant="caption">{t("Has timesheet entry")}</Typography>
</Box>
<Box display="flex" alignItems="center" gap={1}>
<Indicator sx={{ backgroundColor: "warning.light" }} />
<Typography variant="caption">{t("Has leave entry")}</Typography>
</Box>
<Box display="flex" alignItems="center" gap={1}>
<Indicator sx={{ backgroundColor: "success.light" }} />
<Typography variant="caption">
{t("Has both timesheet and leave entry")}
</Typography>
</Box>
</Stack>
<PastEntryCalendar
timesheet={timesheet}
leaves={leaves}
onDateSelect={setSelectedDate}
/>
</>
);

const isMobile = useIsMobile();

return isMobile ? (
<FullscreenModal open={open} onClose={onClose} closeModal={onClose}>
<Box display="flex" flexDirection="column" gap={2} height="100%">
<Typography variant="h6" flex="none" paddingInline={2}>
{t("Past Entries")}
</Typography>
<Box
flex={1}
paddingInline={2}
overflow="hidden"
display="flex"
flexDirection="column"
sx={{ overflow: "scroll" }}
>
{content}
</Box>
<Box padding={2} display="flex" justifyContent="flex-end">
<Button
variant="outlined"
startIcon={<ArrowBack />}
onClick={clearDate}
>
{t("Back")}
</Button>
</Box>
</Box>
</FullscreenModal>
) : (
<Dialog onClose={onClose} open={open}> <Dialog onClose={onClose} open={open}>
<DialogTitle>{t("Past Entries")}</DialogTitle> <DialogTitle>{t("Past Entries")}</DialogTitle>
<DialogContent>
{selectedDate ? (
<Box>{selectedDate}</Box>
) : (
<Box>
<Stack>
<Box display="flex" alignItems="center" gap={1}>
<Indicator sx={{ backgroundColor: "info.light" }} />
<Typography variant="caption">
{t("Has timesheet entry")}
</Typography>
</Box>
<Box display="flex" alignItems="center" gap={1}>
<Indicator sx={{ backgroundColor: "warning.light" }} />
<Typography variant="caption">
{t("Has leave entry")}
</Typography>
</Box>
<Box display="flex" alignItems="center" gap={1}>
<Indicator sx={{ backgroundColor: "success.light" }} />
<Typography variant="caption">
{t("Has both timesheet and leave entry")}
</Typography>
</Box>
</Stack>
<PastEntryCalendar
timesheet={timesheet}
leaves={leaves}
onDateSelect={setSelectedDate}
/>
</Box>
)}
</DialogContent>
<DialogContent>{content}</DialogContent>
{selectedDate && ( {selectedDate && (
<DialogActions> <DialogActions>
<Button <Button


+ 101
- 0
src/components/PastEntryCalendar/PastEntryList.tsx View File

@@ -0,0 +1,101 @@
import {
RecordTimesheetInput,
RecordLeaveInput,
} from "@/app/api/timesheets/actions";
import { shortDateFormatter } from "@/app/utils/formatUtil";
import { Box, Stack, Typography } from "@mui/material";
import dayjs from "dayjs";
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import TimeEntryCard from "../TimesheetTable/TimeEntryCard";
import LeaveEntryCard from "../LeaveTable/LeaveEntryCard";
import { ProjectWithTasks } from "@/app/api/projects";
import { LeaveType } from "@/app/api/timesheets";

interface Props {
date: string;
timesheet: RecordTimesheetInput;
leaves: RecordLeaveInput;
leaveTypes: LeaveType[];
allProjects: ProjectWithTasks[];
}

const PastEntryList: React.FC<Props> = ({
date,
timesheet,
leaves,
leaveTypes,
allProjects,
}) => {
const {
i18n: { language },
} = useTranslation("home");

const timeEntries = timesheet[date] || [];
const leaveEntries = leaves[date] || [];

const leaveTypeMap = useMemo<{ [id: LeaveType["id"]]: LeaveType }>(() => {
return leaveTypes.reduce(
(acc, leaveType) => ({ ...acc, [leaveType.id]: leaveType }),
{},
);
}, [leaveTypes]);

const projectMap = useMemo(() => {
return allProjects.reduce<{
[id: ProjectWithTasks["id"]]: ProjectWithTasks;
}>((acc, project) => {
return { ...acc, [project.id]: project };
}, {});
}, [allProjects]);

if (!(timeEntries.length || leaveEntries.length)) {
return null;
}

const dayJsObj = dayjs(date);

return (
<Stack gap={2} marginBlockEnd={2} minWidth={{ sm: 375 }}>
<Typography
variant="overline"
color={dayJsObj.day() === 0 ? "error.main" : undefined}
>
{shortDateFormatter(language).format(dayJsObj.toDate())}
</Typography>
<Box
flex={1}
display="flex"
flexDirection="column"
gap={2}
sx={{ overflowY: "scroll" }}
>
{timeEntries.map((entry, index) => {
const project = entry.projectId
? projectMap[entry.projectId]
: undefined;

const task = project?.tasks.find((t) => t.id === entry.taskId);

return (
<TimeEntryCard
key={`${entry.id}-${index}`}
project={project}
task={task}
entry={entry}
/>
);
})}
{leaveEntries.map((entry, index) => (
<LeaveEntryCard
key={`${entry.id}-${index}`}
entry={entry}
leaveTypeMap={leaveTypeMap}
/>
))}
</Box>
</Stack>
);
};

export default PastEntryList;

+ 3
- 5
src/components/TimesheetModal/TimesheetModal.tsx View File

@@ -9,8 +9,6 @@ import {
ModalProps, ModalProps,
SxProps, SxProps,
Typography, Typography,
useMediaQuery,
useTheme,
} from "@mui/material"; } from "@mui/material";
import TimesheetTable from "../TimesheetTable"; import TimesheetTable from "../TimesheetTable";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -26,6 +24,7 @@ import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; import { AssignedProject, ProjectWithTasks } from "@/app/api/projects";
import FullscreenModal from "../FullscreenModal"; import FullscreenModal from "../FullscreenModal";
import MobileTimesheetTable from "../TimesheetTable/MobileTimesheetTable"; import MobileTimesheetTable from "../TimesheetTable/MobileTimesheetTable";
import useIsMobile from "../utils/useIsMobile";


interface Props { interface Props {
isOpen: boolean; isOpen: boolean;
@@ -108,12 +107,11 @@ const TimesheetModal: React.FC<Props> = ({
[onClose], [onClose],
); );


const theme = useTheme();
const matches = useMediaQuery(theme.breakpoints.up("sm"));
const matches = useIsMobile();


return ( return (
<FormProvider {...formProps}> <FormProvider {...formProps}>
{matches ? (
{!matches ? (
// Desktop version // Desktop version
<Modal open={isOpen} onClose={onModalClose}> <Modal open={isOpen} onClose={onModalClose}>
<Card sx={modalSx}> <Card sx={modalSx}>


+ 1
- 1
src/components/TimesheetTable/MobileTimesheetEntry.tsx View File

@@ -136,7 +136,7 @@ const MobileTimesheetEntry: React.FC<Props> = ({
)} )}
<Box> <Box>
<Button startIcon={<Add />} onClick={openEditModal()}> <Button startIcon={<Add />} onClick={openEditModal()}>
{t("Record leave")}
{t("Record time")}
</Button> </Button>
</Box> </Box>
<TimesheetEditModal <TimesheetEditModal


+ 2
- 2
src/components/TimesheetTable/TimesheetEditModal.tsx View File

@@ -91,14 +91,14 @@ const TimesheetEditModal: React.FC<Props> = ({
const valid = await trigger(); const valid = await trigger();
if (valid) { if (valid) {
onSave(getValues()); onSave(getValues());
reset();
reset({ id: Date.now() });
} }
}, [getValues, onSave, reset, trigger]); }, [getValues, onSave, reset, trigger]);


const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>(
(...args) => { (...args) => {
onClose?.(...args); onClose?.(...args);
reset();
reset({ id: Date.now() });
}, },
[onClose, reset], [onClose, reset],
); );


+ 7
- 4
src/components/TransferList/TransferListWrapper.tsx View File

@@ -2,14 +2,17 @@


import React from "react"; import React from "react";
import TransferList, { TransferListProps } from "./TransferList"; import TransferList, { TransferListProps } from "./TransferList";
import { useMediaQuery, useTheme } from "@mui/material";
import MultiSelectList from "./MultiSelectList"; import MultiSelectList from "./MultiSelectList";
import useIsMobile from "../utils/useIsMobile";


const TransferListWrapper: React.FC<TransferListProps> = (props) => { const TransferListWrapper: React.FC<TransferListProps> = (props) => {
const theme = useTheme();
const matches = useMediaQuery(theme.breakpoints.up("sm"));
const matches = useIsMobile();


return matches ? <TransferList {...props} /> : <MultiSelectList {...props} />;
return !matches ? (
<TransferList {...props} />
) : (
<MultiSelectList {...props} />
);
}; };


export default TransferListWrapper; export default TransferListWrapper;

+ 2
- 0
src/components/UserWorkspacePage/UserWorkspacePage.tsx View File

@@ -99,6 +99,8 @@ const UserWorkspacePage: React.FC<Props> = ({
handleClose={handlePastEventClose} handleClose={handlePastEventClose}
timesheet={defaultTimesheets} timesheet={defaultTimesheets}
leaves={defaultLeaveRecords} leaves={defaultLeaveRecords}
allProjects={allProjects}
leaveTypes={leaveTypes}
/> />
<TimesheetModal <TimesheetModal
isOpen={isTimeheetModalVisible} isOpen={isTimeheetModalVisible}


+ 10
- 0
src/components/utils/useIsMobile.ts View File

@@ -0,0 +1,10 @@
import { Breakpoint, useMediaQuery, useTheme } from "@mui/material";

const useIsMobile = (breakpoint: Breakpoint = "sm") => {
const theme = useTheme();
const matches = useMediaQuery(theme.breakpoints.up(breakpoint));

return !matches;
};

export default useIsMobile;

Loading…
Cancel
Save