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

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

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

return (
<FormProvider {...formProps}>
{matches ? (
{!matches ? (
// Desktop version
<Modal open={isOpen} onClose={onModalClose}>
<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 { register, control, reset, getValues, trigger, formState } =
useForm<LeaveEntry>();
useForm<LeaveEntry>({
defaultValues: {
leaveTypeId: leaveTypes[0].id,
},
});

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

const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>(
(...args) => {
onClose?.(...args);
reset();
reset({ id: Date.now() });
},
[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 { useTranslation } from "react-i18next";
import LeaveEditModal, { Props as LeaveEditModalProps } from "./LeaveEditModal";
import LeaveEntryCard from "./LeaveEntryCard";

interface Props {
date: string;
@@ -105,60 +106,12 @@ const MobileLeaveEntry: React.FC<Props> = ({ date, leaveTypes }) => {
{currentEntries.length ? (
currentEntries.map((entry, index) => {
return (
<Card
<LeaveEntryCard
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 { useTranslation } from "react-i18next";
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"> {
open: boolean;
handleClose: () => void;
leaveTypes: LeaveType[];
allProjects: ProjectWithTasks[];
}

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

@@ -45,42 +54,75 @@ const PastEntryCalendarModal: React.FC<Props> = ({
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}>
<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 && (
<DialogActions>
<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,
SxProps,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import TimesheetTable from "../TimesheetTable";
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 FullscreenModal from "../FullscreenModal";
import MobileTimesheetTable from "../TimesheetTable/MobileTimesheetTable";
import useIsMobile from "../utils/useIsMobile";

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

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

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


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

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


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

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

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


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

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

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

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;

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

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