Bladeren bron

Past entries wip

tags/Baseline_30082024_FRONTEND_UAT
Wayne 1 jaar geleden
bovenliggende
commit
e421a25846
15 gewijzigde bestanden met toevoegingen van 637 en 310 verwijderingen
  1. +0
    -1
      src/app/api/projects/index.ts
  2. +65
    -24
      src/app/utils/formatUtil.ts
  3. +36
    -35
      src/components/Breadcrumb/Breadcrumb.tsx
  4. +26
    -25
      src/components/Breadcrumb/Clock.tsx
  5. +31
    -23
      src/components/CreateProject/MilestoneSection.tsx
  6. +106
    -0
      src/components/PastEntryCalendar/PastEntryCalendar.tsx
  7. +99
    -0
      src/components/PastEntryCalendar/PastEntryCalendarModal.tsx
  8. +1
    -0
      src/components/PastEntryCalendar/index.ts
  9. +4
    -0
      src/components/StyledDataGrid/StyledDataGrid.tsx
  10. +7
    -84
      src/components/TimesheetTable/MobileTimesheetEntry.tsx
  11. +135
    -94
      src/components/TimesheetTable/ProjectSelect.tsx
  12. +87
    -0
      src/components/TimesheetTable/TimeEntryCard.tsx
  13. +3
    -16
      src/components/UserWorkspacePage/ProjectGrid.tsx
  14. +27
    -8
      src/components/UserWorkspacePage/UserWorkspacePage.tsx
  15. +10
    -0
      src/theme/devias-material-kit/components.ts

+ 0
- 1
src/app/api/projects/index.ts Bestand weergeven

@@ -68,7 +68,6 @@ export interface AssignedProject extends ProjectWithTasks {
hoursSpent: number;
hoursSpentOther: number;
hoursAllocated: number;
hoursAllocatedOther: number;
}

export const preloadProjects = () => {


+ 65
- 24
src/app/utils/formatUtil.ts Bestand weergeven

@@ -21,41 +21,52 @@ export const OUTPUT_DATE_FORMAT = "YYYY/MM/DD";

export const OUTPUT_TIME_FORMAT = "HH:mm:ss";

export const convertDateToString = (date: Date, format: string = OUTPUT_DATE_FORMAT) => {
return dayjs(date).format(format)
}
export const convertDateToString = (
date: Date,
format: string = OUTPUT_DATE_FORMAT,
) => {
return dayjs(date).format(format);
};

export const convertDateArrayToString = (dateArray: number[], format: string = OUTPUT_DATE_FORMAT, needTime: boolean = false) => {
export const convertDateArrayToString = (
dateArray: number[],
format: string = OUTPUT_DATE_FORMAT,
needTime: boolean = false,
) => {
if (dateArray.length === 6) {
if (!needTime) {
const dateString = `${dateArray[0]}-${dateArray[1]}-${dateArray[2]}`
return dayjs(dateString).format(format)
const dateString = `${dateArray[0]}-${dateArray[1]}-${dateArray[2]}`;
return dayjs(dateString).format(format);
}
}
if (dateArray.length === 3) {
if (!needTime) {
const dateString = `${dateArray[0]}-${dateArray[1]}-${dateArray[2]}`
return dayjs(dateString).format(format)
const dateString = `${dateArray[0]}-${dateArray[1]}-${dateArray[2]}`;
return dayjs(dateString).format(format);
}
}
}
};

export const convertTimeArrayToString = (timeArray: number[], format: string = OUTPUT_TIME_FORMAT, needTime: boolean = false) => {
let timeString = '';
export const convertTimeArrayToString = (
timeArray: number[],
format: string = OUTPUT_TIME_FORMAT,
needTime: boolean = false,
) => {
let timeString = "";

if (timeArray !== null && timeArray !== undefined) {
const hour = timeArray[0] || 0;
const minute = timeArray[1] || 0;
timeString = dayjs()
.set('hour', hour)
.set('minute', minute)
.set('second', 0)
.format('HH:mm:ss');
const hour = timeArray[0] || 0;
const minute = timeArray[1] || 0;
timeString = dayjs()
.set("hour", hour)
.set("minute", minute)
.set("second", 0)
.format("HH:mm:ss");
}
return timeString
}
return timeString;
};

const shortDateFormatter_en = new Intl.DateTimeFormat("en-HK", {
weekday: "short",
@@ -81,6 +92,36 @@ export const shortDateFormatter = (locale?: string) => {
}
};

const clockFormatOptions: Intl.DateTimeFormatOptions = {
year: "numeric",
month: "long",
day: "numeric",
weekday: "long",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: true,
};

const clockTimeFormatter_en = new Intl.DateTimeFormat(
"en-HK",
clockFormatOptions,
);
const clockTimeformatter_zh = new Intl.DateTimeFormat(
"zh-HK",
clockFormatOptions,
);

export const clockTimeFormatter = (locale?: string) => {
switch (locale) {
case "zh":
return clockTimeformatter_zh;
case "en":
default:
return clockTimeFormatter_en;
}
};

export function convertLocaleStringToNumber(numberString: string): number {
const numberWithoutCommas = numberString.replace(/,/g, "");
return parseFloat(numberWithoutCommas);
@@ -91,6 +132,6 @@ export function timestampToDateString(timestamp: string): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
console.log(`${year}-${month}-${day}`)
console.log(`${year}-${month}-${day}`);
return `${year}-${month}-${day}`;
}
}

+ 36
- 35
src/components/Breadcrumb/Breadcrumb.tsx Bestand weergeven

@@ -7,7 +7,7 @@ import MUILink from "@mui/material/Link";
import { usePathname } from "next/navigation";
import { useTranslation } from "react-i18next";
import Clock from "./Clock";
import { Grid } from "@mui/material";
import { Box, Grid } from "@mui/material";
import { I18nProvider } from "@/i18n";

const pathToLabelMap: { [path: string]: string } = {
@@ -46,42 +46,43 @@ const Breadcrumb = () => {

// const { t } = useTranslation("customer");


return (
<Grid container>
<Grid item xs={6}>
<Breadcrumbs>
{segments.map((segment, index) => {
const href = segments.slice(0, index + 1).join("/");
const label = pathToLabelMap[href] || segment;
<Box
display="flex"
flexDirection={{ xs: "column-reverse", sm: "row"}}
justifyContent={{ sm: "space-between" }}
>
<Breadcrumbs>
{segments.map((segment, index) => {
const href = segments.slice(0, index + 1).join("/");
const label = pathToLabelMap[href] || segment;

if (index === segments.length - 1) {
return (
<Typography key={index} color="text.primary">
{label}
{/* {t(label)} */}
</Typography>
);
} else {
return (
<MUILink
underline="hover"
color="inherit"
key={index}
component={Link}
href={href || "/"}
>
{label}
</MUILink>
);
}
})}
</Breadcrumbs>
</Grid>
<Grid item xs={6} sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<Clock />
</Grid>
</Grid>
if (index === segments.length - 1) {
return (
<Typography key={index} color="text.primary">
{label}
{/* {t(label)} */}
</Typography>
);
} else {
return (
<MUILink
underline="hover"
color="inherit"
key={index}
component={Link}
href={href || "/"}
>
{label}
</MUILink>
);
}
})}
</Breadcrumbs>
<Box width={{ xs: "100%", sm: "auto" }} marginBlockEnd={{ xs: 1, sm: 0 }}>
<Clock variant="body2" />
</Box>
</Box>
);
};



+ 26
- 25
src/components/Breadcrumb/Clock.tsx Bestand weergeven

@@ -1,32 +1,33 @@
"use client"
import { useState, useEffect, useLayoutEffect } from 'react';
import Typography from "@mui/material/Typography";
import { useTranslation } from 'react-i18next';
"use client";
import React, { useState, useLayoutEffect } from "react";
import Typography, { TypographyProps } from "@mui/material/Typography";
import { useTranslation } from "react-i18next";
import { clockTimeFormatter } from "@/app/utils/formatUtil";
import { NoSsr } from "@mui/material";

const Clock = () => {
const {
i18n: { language },
} = useTranslation();
const [currentDateTime, setCurrentDateTime] = useState(new Date());
const Clock: React.FC<TypographyProps> = (props) => {
const {
i18n: { language },
} = useTranslation();
const [currentDateTime, setCurrentDateTime] = useState(new Date());

useLayoutEffect(() => {
const timer = setInterval(() => {
setCurrentDateTime(new Date());
}, 1000);
useLayoutEffect(() => {
const timer = setInterval(() => {
setCurrentDateTime(new Date());
}, 1000);

return () => {
clearInterval(timer);
};
}, []);
return () => {
clearInterval(timer);
};
}, []);

const options: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true };
const formattedDateTime = new Intl.DateTimeFormat(language, options).format(currentDateTime)

return (
<Typography color="text.primary" suppressHydrationWarning>
{formattedDateTime}
</Typography>
);
return (
<NoSsr>
<Typography {...props}>
{clockTimeFormatter(language).format(currentDateTime)}
</Typography>
</NoSsr>
);
};

export default Clock;

+ 31
- 23
src/components/CreateProject/MilestoneSection.tsx Bestand weergeven

@@ -58,8 +58,8 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => {
const apiRef = useGridApiRef();
const addRow = useCallback(() => {
// const id = Date.now();
const minId = Math.min(...payments.map((payment) => payment.id!!));
const id = minId >= 0 ? -1 : minId - 1
const minId = Math.min(...payments.map((payment) => payment.id!));
const id = minId >= 0 ? -1 : minId - 1;
setPayments((p) => [...p, { id, _isNew: true }]);
setRowModesModel((model) => ({
...model,
@@ -241,26 +241,30 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => {
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs>
<FormControl fullWidth>
<DatePicker
label={t("Stage Start Date")}
value={startDate ? dayjs(startDate) : null}
onChange={(date) => {
if (!date) return;
const milestones = getValues("milestones");
setValue("milestones", {
...milestones,
[taskGroupId]: {
...milestones[taskGroupId],
startDate: date.format(INPUT_DATE_FORMAT),
},
});
}}
slotProps={{
textField: {
error: startDate === "Invalid Date" || new Date(startDate) > new Date(endDate) || (Boolean(formState.errors.milestones) && !Boolean(startDate)),
},
}}
/>
<DatePicker
label={t("Stage Start Date")}
value={startDate ? dayjs(startDate) : null}
onChange={(date) => {
if (!date) return;
const milestones = getValues("milestones");
setValue("milestones", {
...milestones,
[taskGroupId]: {
...milestones[taskGroupId],
startDate: date.format(INPUT_DATE_FORMAT),
},
});
}}
slotProps={{
textField: {
error:
startDate === "Invalid Date" ||
new Date(startDate) > new Date(endDate) ||
(Boolean(formState.errors.milestones) &&
!Boolean(startDate)),
},
}}
/>
</FormControl>
</Grid>
<Grid item xs>
@@ -281,7 +285,11 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => {
}}
slotProps={{
textField: {
error: endDate === "Invalid Date" || new Date(startDate) > new Date(endDate) || (Boolean(formState.errors.milestones) && !Boolean(endDate)),
error:
endDate === "Invalid Date" ||
new Date(startDate) > new Date(endDate) ||
(Boolean(formState.errors.milestones) &&
!Boolean(endDate)),
},
}}
/>


+ 106
- 0
src/components/PastEntryCalendar/PastEntryCalendar.tsx Bestand weergeven

@@ -0,0 +1,106 @@
import React from "react";
import {
RecordTimesheetInput,
RecordLeaveInput,
} from "@/app/api/timesheets/actions";
import {
DateCalendar,
LocalizationProvider,
PickersDay,
PickersDayProps,
} from "@mui/x-date-pickers";
import { useTranslation } from "react-i18next";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import dayjs, { Dayjs } from "dayjs";
import "dayjs/locale/zh-hk";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil";

dayjs.extend(utc);
dayjs.extend(timezone);

dayjs.tz.guess();

export interface Props {
timesheet: RecordTimesheetInput;
leaves: RecordLeaveInput;
onDateSelect: (date: string) => void;
}

const getColor = (
hasTimeInput: boolean,
hasLeave: boolean,
): string | undefined => {
if (hasTimeInput && hasLeave) {
return "success.light";
} else if (hasTimeInput) {
return "info.light";
} else if (hasLeave) {
return "warning.light";
} else {
return undefined;
}
};

const EntryDay: React.FC<PickersDayProps<Dayjs> & Props> = ({
timesheet,
leaves,
...pickerProps
}) => {
const timesheetDays = Object.keys(timesheet);
const leaveDays = Object.keys(leaves);

const hasTimesheetInput = timesheetDays.some((day) =>
dayjs(day).isSame(pickerProps.day, "day"),
);

const hasLeaveInput = leaveDays.some((day) =>
dayjs(day).isSame(pickerProps.day, "day"),
);

return (
<PickersDay
{...pickerProps}
disabled={!(hasTimesheetInput || hasLeaveInput)}
sx={{ backgroundColor: getColor(hasTimesheetInput, hasLeaveInput) }}
/>
);
};

const PastEntryCalendar: React.FC<Props> = ({
timesheet,
leaves,
onDateSelect,
}) => {
const {
i18n: { language },
} = useTranslation("home");

const onChange = (day: Dayjs) => {
onDateSelect(day.format(INPUT_DATE_FORMAT));
};

return (
<LocalizationProvider
dateAdapter={AdapterDayjs}
adapterLocale={`${language}-hk`}
>
<DateCalendar
onChange={onChange}
disableFuture
// eslint-disable-next-line @typescript-eslint/no-explicit-any
slots={{ day: EntryDay as any }}
slotProps={{
day: {
timesheet,
leaves,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any,
}}
/>
</LocalizationProvider>
);
};

export default PastEntryCalendar;

+ 99
- 0
src/components/PastEntryCalendar/PastEntryCalendarModal.tsx Bestand weergeven

@@ -0,0 +1,99 @@
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Stack,
Typography,
styled,
} from "@mui/material";
import PastEntryCalendar, {
Props as PastEntryCalendarProps,
} from "./PastEntryCalendar";
import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { ArrowBack } from "@mui/icons-material";

interface Props extends Omit<PastEntryCalendarProps, "onDateSelect"> {
open: boolean;
handleClose: () => void;
}

const Indicator = styled(Box)(() => ({
borderRadius: "50%",
width: "1rem",
height: "1rem",
}));

const PastEntryCalendarModal: React.FC<Props> = ({
handleClose,
open,
timesheet,
leaves,
}) => {
const { t } = useTranslation("home");

const [selectedDate, setSelectedDate] = useState("");

const clearDate = useCallback(() => {
setSelectedDate("");
}, []);

const onClose = useCallback(() => {
handleClose();
}, [handleClose]);

return (
<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>
{selectedDate && (
<DialogActions>
<Button
variant="outlined"
startIcon={<ArrowBack />}
onClick={clearDate}
>
{t("Back")}
</Button>
</DialogActions>
)}
</Dialog>
);
};

export default PastEntryCalendarModal;

+ 1
- 0
src/components/PastEntryCalendar/index.ts Bestand weergeven

@@ -0,0 +1 @@
export { default } from "./PastEntryCalendar";

+ 4
- 0
src/components/StyledDataGrid/StyledDataGrid.tsx Bestand weergeven

@@ -28,6 +28,10 @@ const StyledDataGrid = styled(DataGrid)(({ theme }) => ({
borderRadius: 0,
maxHeight: 50,
},
"& .MuiAutocomplete-root .MuiFilledInput-root": {
borderRadius: 0,
maxHeight: 50,
},
}));

export default StyledDataGrid;

+ 7
- 84
src/components/TimesheetTable/MobileTimesheetEntry.tsx Bestand weergeven

@@ -17,6 +17,7 @@ import { AssignedProject, ProjectWithTasks } from "@/app/api/projects";
import TimesheetEditModal, {
Props as TimesheetEditModalProps,
} from "./TimesheetEditModal";
import TimeEntryCard from "./TimeEntryCard";

interface Props {
date: string;
@@ -119,91 +120,13 @@ const MobileTimesheetEntry: React.FC<Props> = ({
const task = project?.tasks.find((t) => t.id === entry.taskId);

return (
<Card
<TimeEntryCard
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"
gap={2}
>
<Box>
<Typography
variant="body2"
component="div"
fontWeight="bold"
>
{project
? `${project.code} - ${project.name}`
: t("Non-billable Task")}
</Typography>
{task && (
<Typography variant="body2" component="div">
{task.name}
</Typography>
)}
</Box>
<IconButton
size="small"
color="primary"
onClick={openEditModal(entry)}
>
<Edit />
</IconButton>
</Box>
<Box display="flex" gap={2}>
<Box>
<Typography
variant="body2"
component="div"
fontWeight="bold"
>
{t("Hours")}
</Typography>
<Typography component="p">
{manhourFormatter.format(entry.inputHours || 0)}
</Typography>
</Box>
<Box>
<Typography
variant="body2"
component="div"
fontWeight="bold"
>
{t("Other Hours")}
</Typography>
<Typography component="p">
{manhourFormatter.format(entry.otHours || 0)}
</Typography>
</Box>
</Box>
{entry.remark && (
<Box>
<Typography
variant="body2"
component="div"
fontWeight="bold"
>
{t("Remark")}
</Typography>
<Typography component="p">{entry.remark}</Typography>
</Box>
)}
</CardContent>
</Card>
project={project}
task={task}
entry={entry}
onEdit={openEditModal(entry)}
/>
);
})
) : (


+ 135
- 94
src/components/TimesheetTable/ProjectSelect.tsx Bestand weergeven

@@ -10,6 +10,7 @@ import {
import { AssignedProject, ProjectWithTasks } from "@/app/api/projects";
import { useTranslation } from "react-i18next";
import differenceBy from "lodash/differenceBy";
import { TFunction } from "i18next";

interface Props {
allProjects: ProjectWithTasks[];
@@ -18,119 +19,159 @@ interface Props {
onProjectSelect: (projectId: number | string) => void;
}

// const AutocompleteProjectSelect: React.FC<Props> = ({
// allProjects,
// assignedProjects,
// value,
// onProjectSelect,
// }) => {
// const { t } = useTranslation("home");
// const nonAssignedProjects = useMemo(() => {
// return differenceBy(allProjects, assignedProjects, "id");
// }, [allProjects, assignedProjects]);

// const options = useMemo(() => {
// return [
// {
// value: "",
// label: t("None"),
// group: "non-billable",
// },
// ...assignedProjects.map((p) => ({
// value: p.id,
// label: `${p.code} - ${p.name}`,
// group: "assigned",
// })),
// ...nonAssignedProjects.map((p) => ({
// value: p.id,
// label: `${p.code} - ${p.name}`,
// group: "non-assigned",
// })),
// ];
// }, [assignedProjects, nonAssignedProjects, t]);

// return (
// <Autocomplete
// disableClearable
// fullWidth
// groupBy={(option) => option.group}
// getOptionLabel={(option) => option.label}
// options={options}
// renderInput={(params) => <TextField {...params} />}
// />
// );
// };
const getGroupName = (t: TFunction, groupName: string): string => {
switch (groupName) {
case "non-billable":
return t("Non-billable");
case "assigned":
return t("Assigned Projects");
case "non-assigned":
return t("Non-assigned Projects");
default:
return t("Ungrouped");
}
};

const ProjectSelect: React.FC<Props> = ({
const AutocompleteProjectSelect: React.FC<Props> = ({
allProjects,
assignedProjects,
value,
onProjectSelect,
}) => {
const { t } = useTranslation("home");

const nonAssignedProjects = useMemo(() => {
return differenceBy(allProjects, assignedProjects, "id");
}, [allProjects, assignedProjects]);

const options = useMemo(() => {
return [
{
value: "",
label: t("None"),
group: "non-billable",
},
...assignedProjects.map((p) => ({
value: p.id,
label: `${p.code} - ${p.name}`,
group: "assigned",
})),
...nonAssignedProjects.map((p) => ({
value: p.id,
label: `${p.code} - ${p.name}`,
group: "non-assigned",
})),
];
}, [assignedProjects, nonAssignedProjects, t]);

const currentValue = options.find((o) => o.value === value) || options[0];

const onChange = useCallback(
(event: SelectChangeEvent<number>) => {
const newValue = event.target.value;
onProjectSelect(newValue);
(event: React.SyntheticEvent, newValue: { value: number | string }) => {
onProjectSelect(newValue.value);
},
[onProjectSelect],
);

return (
<Select
displayEmpty
value={value || ""}
<Autocomplete
noOptionsText={t("No projects")}
disableClearable
fullWidth
value={currentValue}
onChange={onChange}
sx={{ width: "100%" }}
MenuProps={{
slotProps: {
paper: {
sx: { maxHeight: 400 },
},
},
anchorOrigin: {
vertical: "bottom",
horizontal: "left",
},
transformOrigin: {
vertical: "top",
horizontal: "left",
},
groupBy={(option) => option.group}
getOptionLabel={(option) => option.label}
options={options}
renderGroup={(params) => (
<>
<ListSubheader key={params.key}>
{getGroupName(t, params.group)}
</ListSubheader>
{params.children}
</>
)}
renderOption={(params, option) => {
return (
<MenuItem {...params} key={option.value} value={option.value}>
{option.label}
</MenuItem>
);
}}
>
<ListSubheader>{t("Non-billable")}</ListSubheader>
<MenuItem value={""}>{t("None")}</MenuItem>
{assignedProjects.length > 0 && [
<ListSubheader key="assignedProjectsSubHeader">
{t("Assigned Projects")}
</ListSubheader>,
...assignedProjects.map((project) => (
<MenuItem
key={project.id}
value={project.id}
sx={{ whiteSpace: "wrap" }}
>{`${project.code} - ${project.name}`}</MenuItem>
)),
]}
{nonAssignedProjects.length > 0 && [
<ListSubheader key="nonAssignedProjectsSubHeader">
{t("Non-assigned Projects")}
</ListSubheader>,
...nonAssignedProjects.map((project) => (
<MenuItem
key={project.id}
value={project.id}
sx={{ whiteSpace: "wrap" }}
>{`${project.code} - ${project.name}`}</MenuItem>
)),
]}
</Select>
renderInput={(params) => <TextField {...params} />}
/>
);
};

export default ProjectSelect;
// const ProjectSelect: React.FC<Props> = ({
// allProjects,
// assignedProjects,
// value,
// onProjectSelect,
// }) => {
// const { t } = useTranslation("home");

// const nonAssignedProjects = useMemo(() => {
// return differenceBy(allProjects, assignedProjects, "id");
// }, [allProjects, assignedProjects]);

// const onChange = useCallback(
// (event: SelectChangeEvent<number>) => {
// const newValue = event.target.value;
// onProjectSelect(newValue);
// },
// [onProjectSelect],
// );

// return (
// <Select
// displayEmpty
// value={value || ""}
// onChange={onChange}
// sx={{ width: "100%" }}
// MenuProps={{
// slotProps: {
// paper: {
// sx: { maxHeight: 400 },
// },
// },
// anchorOrigin: {
// vertical: "bottom",
// horizontal: "left",
// },
// transformOrigin: {
// vertical: "top",
// horizontal: "left",
// },
// }}
// >
// <ListSubheader>{t("Non-billable")}</ListSubheader>
// <MenuItem value={""}>{t("None")}</MenuItem>
// {assignedProjects.length > 0 && [
// <ListSubheader key="assignedProjectsSubHeader">
// {t("Assigned Projects")}
// </ListSubheader>,
// ...assignedProjects.map((project) => (
// <MenuItem
// key={project.id}
// value={project.id}
// sx={{ whiteSpace: "wrap" }}
// >{`${project.code} - ${project.name}`}</MenuItem>
// )),
// ]}
// {nonAssignedProjects.length > 0 && [
// <ListSubheader key="nonAssignedProjectsSubHeader">
// {t("Non-assigned Projects")}
// </ListSubheader>,
// ...nonAssignedProjects.map((project) => (
// <MenuItem
// key={project.id}
// value={project.id}
// sx={{ whiteSpace: "wrap" }}
// >{`${project.code} - ${project.name}`}</MenuItem>
// )),
// ]}
// </Select>
// );
// };

export default AutocompleteProjectSelect;

+ 87
- 0
src/components/TimesheetTable/TimeEntryCard.tsx Bestand weergeven

@@ -0,0 +1,87 @@
import { ProjectWithTasks } from "@/app/api/projects";
import { Task } from "@/app/api/tasks";
import { TimeEntry } 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 React from "react";
import { useTranslation } from "react-i18next";

interface Props {
project?: ProjectWithTasks;
task?: Task;
entry: TimeEntry;
onEdit?: () => void;
}

const TimeEntryCard: React.FC<Props> = ({ project, task, entry, onEdit }) => {
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"
gap={2}
>
<Box>
<Typography variant="body2" component="div" fontWeight="bold">
{project
? `${project.code} - ${project.name}`
: t("Non-billable Task")}
</Typography>
{task && (
<Typography variant="body2" component="div">
{task.name}
</Typography>
)}
</Box>
{onEdit && (
<IconButton size="small" color="primary" onClick={onEdit}>
<Edit />
</IconButton>
)}
</Box>
<Box display="flex" gap={2}>
<Box>
<Typography variant="body2" component="div" fontWeight="bold">
{t("Hours")}
</Typography>
<Typography component="p">
{manhourFormatter.format(entry.inputHours || 0)}
</Typography>
</Box>
<Box>
<Typography variant="body2" component="div" fontWeight="bold">
{t("Other Hours")}
</Typography>
<Typography component="p">
{manhourFormatter.format(entry.otHours || 0)}
</Typography>
</Box>
</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 TimeEntryCard;

+ 3
- 16
src/components/UserWorkspacePage/ProjectGrid.tsx Bestand weergeven

@@ -56,9 +56,6 @@ const ProjectGrid: React.FC<Props> = ({ projects }) => {
)})`}</Typography>
</Box>
{/* Hours Allocated */}
<Typography variant="subtitle2" sx={{ marginBlockStart: 2 }}>
{t("Hours Allocated:")}
</Typography>
<Box
sx={{
display: "flex",
@@ -66,23 +63,13 @@ const ProjectGrid: React.FC<Props> = ({ projects }) => {
alignItems: "baseline",
}}
>
<Typography variant="caption">{t("Normal")}</Typography>
<Typography variant="subtitle2" sx={{ marginBlockStart: 2 }}>
{t("Hours Allocated:")}
</Typography>
<Typography>
{manhourFormatter.format(project.hoursAllocated)}
</Typography>
</Box>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "baseline",
}}
>
<Typography variant="caption">{t("(Others)")}</Typography>
<Typography>{`(${manhourFormatter.format(
project.hoursAllocatedOther,
)})`}</Typography>
</Box>
</CardContent>
</Card>
</Grid>


+ 27
- 8
src/components/UserWorkspacePage/UserWorkspacePage.tsx Bestand weergeven

@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
import { Add } from "@mui/icons-material";
import { Typography } from "@mui/material";
import { Box, Typography } from "@mui/material";
import ButtonGroup from "@mui/material/ButtonGroup";
import AssignedProjects from "./AssignedProjects";
import TimesheetModal from "../TimesheetModal";
@@ -16,6 +16,8 @@ import {
} from "@/app/api/timesheets/actions";
import LeaveModal from "../LeaveModal";
import { LeaveType } from "@/app/api/timesheets";
import { CalendarIcon } from "@mui/x-date-pickers";
import PastEntryCalendarModal from "../PastEntryCalendar/PastEntryCalendarModal";

export interface Props {
leaveTypes: LeaveType[];
@@ -36,6 +38,7 @@ const UserWorkspacePage: React.FC<Props> = ({
}) => {
const [isTimeheetModalVisible, setTimeheetModalVisible] = useState(false);
const [isLeaveModalVisible, setLeaveModalVisible] = useState(false);
const [isPastEventModalVisible, setPastEventModalVisible] = useState(false);
const { t } = useTranslation("home");

const handleAddTimesheetButtonClick = useCallback(() => {
@@ -54,6 +57,14 @@ const UserWorkspacePage: React.FC<Props> = ({
setLeaveModalVisible(false);
}, []);

const handlePastEventClick = useCallback(() => {
setPastEventModalVisible(true);
}, []);

const handlePastEventClose = useCallback(() => {
setPastEventModalVisible(false);
}, []);

return (
<>
<Stack
@@ -65,12 +76,14 @@ const UserWorkspacePage: React.FC<Props> = ({
<Typography variant="h4" marginInlineEnd={2}>
{t("User Workspace")}
</Typography>
<Stack
direction="row"
justifyContent="right"
flexWrap="wrap"
spacing={2}
>
<Box display="flex" flexWrap="wrap" gap={2}>
<Button
startIcon={<CalendarIcon />}
variant="outlined"
onClick={handlePastEventClick}
>
{t("View Past Entries")}
</Button>
<ButtonGroup variant="contained">
<Button startIcon={<Add />} onClick={handleAddTimesheetButtonClick}>
{t("Enter Time")}
@@ -79,8 +92,14 @@ const UserWorkspacePage: React.FC<Props> = ({
{t("Record Leave")}
</Button>
</ButtonGroup>
</Stack>
</Box>
</Stack>
<PastEntryCalendarModal
open={isPastEventModalVisible}
handleClose={handlePastEventClose}
timesheet={defaultTimesheets}
leaves={defaultLeaveRecords}
/>
<TimesheetModal
isOpen={isTimeheetModalVisible}
onClose={handleCloseTimesheetModal}


+ 10
- 0
src/theme/devias-material-kit/components.ts Bestand weergeven

@@ -172,6 +172,16 @@ const components: ThemeOptions["components"] = {
},
},
},
MuiAutocomplete: {
styleOverrides: {
root: {
"& .MuiFilledInput-root": {
paddingTop: 8,
paddingBottom: 8,
},
},
},
},
MuiFilledInput: {
styleOverrides: {
root: {


Laden…
Annuleren
Opslaan