@@ -2,6 +2,7 @@ import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
import { BASE_API_URL } from "@/config/api"; | import { BASE_API_URL } from "@/config/api"; | ||||
import { cache } from "react"; | import { cache } from "react"; | ||||
import "server-only"; | import "server-only"; | ||||
import { Task } from "../tasks"; | |||||
export interface ProjectResult { | export interface ProjectResult { | ||||
id: number; | id: number; | ||||
@@ -17,6 +18,13 @@ export interface ProjectCategory { | |||||
name: string; | name: string; | ||||
} | } | ||||
export interface AssignedProject { | |||||
id: number; | |||||
code: string; | |||||
name: string; | |||||
tasks: Task[]; | |||||
} | |||||
export const preloadProjects = () => { | export const preloadProjects = () => { | ||||
fetchProjectCategories(); | fetchProjectCategories(); | ||||
fetchProjects(); | fetchProjects(); | ||||
@@ -0,0 +1,15 @@ | |||||
"use server"; | |||||
import { ProjectResult } from "../projects"; | |||||
import { Task, TaskGroup } from "../tasks"; | |||||
export interface TimeEntry { | |||||
projectId: ProjectResult["id"]; | |||||
taskGroupId: TaskGroup["id"]; | |||||
taskId: Task["id"]; | |||||
inputHours: number; | |||||
} | |||||
export interface RecordTimesheetInput { | |||||
[date: string]: TimeEntry[]; | |||||
} |
@@ -12,3 +12,29 @@ export const percentFormatter = new Intl.NumberFormat("en-HK", { | |||||
style: "percent", | style: "percent", | ||||
maximumFractionDigits: 2, | maximumFractionDigits: 2, | ||||
}); | }); | ||||
export const INPUT_DATE_FORMAT = "YYYY-MM-DD"; | |||||
const shortDateFormatter_en = new Intl.DateTimeFormat("en-HK", { | |||||
weekday: "short", | |||||
year: "numeric", | |||||
month: "short", | |||||
day: "numeric", | |||||
}); | |||||
const shortDateFormatter_zh = new Intl.DateTimeFormat("zh-HK", { | |||||
weekday: "long", | |||||
year: "numeric", | |||||
month: "numeric", | |||||
day: "numeric", | |||||
}); | |||||
export const shortDateFormatter = (locale?: string) => { | |||||
switch (locale) { | |||||
case "zh": | |||||
return shortDateFormatter_zh; | |||||
case "en": | |||||
default: | |||||
return shortDateFormatter_en; | |||||
} | |||||
}; |
@@ -45,7 +45,10 @@ declare module "@mui/x-data-grid" { | |||||
type PaymentRow = Partial<PaymentInputs & { _isNew: boolean; _error: string }>; | type PaymentRow = Partial<PaymentInputs & { _isNew: boolean; _error: string }>; | ||||
const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => { | const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => { | ||||
const { t } = useTranslation(); | |||||
const { | |||||
t, | |||||
i18n: { language }, | |||||
} = useTranslation(); | |||||
const { getValues, setValue } = useFormContext<CreateProjectInputs>(); | const { getValues, setValue } = useFormContext<CreateProjectInputs>(); | ||||
const [payments, setPayments] = useState<PaymentRow[]>( | const [payments, setPayments] = useState<PaymentRow[]>( | ||||
getValues("milestones")[taskGroupId]?.payments || [], | getValues("milestones")[taskGroupId]?.payments || [], | ||||
@@ -220,7 +223,10 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => { | |||||
<Typography variant="overline" display="block" marginBlockEnd={1}> | <Typography variant="overline" display="block" marginBlockEnd={1}> | ||||
{t("Stage Milestones")} | {t("Stage Milestones")} | ||||
</Typography> | </Typography> | ||||
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="zh-hk"> | |||||
<LocalizationProvider | |||||
dateAdapter={AdapterDayjs} | |||||
adapterLocale={`${language}-hk`} | |||||
> | |||||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | ||||
<Grid item xs> | <Grid item xs> | ||||
<FormControl fullWidth> | <FormControl fullWidth> | ||||
@@ -17,6 +17,10 @@ const StyledDataGrid = styled(DataGrid)(({ theme }) => ({ | |||||
"& .MuiDataGrid-columnSeparator": { | "& .MuiDataGrid-columnSeparator": { | ||||
color: theme.palette.primary.main, | color: theme.palette.primary.main, | ||||
}, | }, | ||||
"& .MuiOutlinedInput-root": { | |||||
borderRadius: 0, | |||||
maxHeight: 50, | |||||
}, | |||||
})); | })); | ||||
export default StyledDataGrid; | export default StyledDataGrid; |
@@ -0,0 +1,102 @@ | |||||
import React, { useCallback, useMemo } from "react"; | |||||
import { | |||||
Box, | |||||
Button, | |||||
Card, | |||||
CardActions, | |||||
CardContent, | |||||
Modal, | |||||
SxProps, | |||||
Typography, | |||||
} from "@mui/material"; | |||||
import TimesheetTable from "../TimesheetTable"; | |||||
import { useTranslation } from "react-i18next"; | |||||
import { Check, Close } from "@mui/icons-material"; | |||||
import { FormProvider, useForm } from "react-hook-form"; | |||||
import { RecordTimesheetInput } from "@/app/api/timesheets/actions"; | |||||
import dayjs from "dayjs"; | |||||
import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||||
interface Props { | |||||
isOpen: boolean; | |||||
onClose: () => void; | |||||
timesheetType: "time" | "leave"; | |||||
} | |||||
const modalSx: SxProps = { | |||||
position: "absolute", | |||||
top: "50%", | |||||
left: "50%", | |||||
transform: "translate(-50%, -50%)", | |||||
width: { xs: "calc(100% - 2rem)", sm: "90%" }, | |||||
maxHeight: "90%", | |||||
maxWidth: 1200, | |||||
}; | |||||
const TimesheetModal: React.FC<Props> = ({ | |||||
isOpen, | |||||
onClose, | |||||
timesheetType, | |||||
}) => { | |||||
const { t } = useTranslation("home"); | |||||
const defaultValues = useMemo(() => { | |||||
const today = dayjs(); | |||||
return Array(7) | |||||
.fill(undefined) | |||||
.reduce<RecordTimesheetInput>((acc, _, index) => { | |||||
return { | |||||
...acc, | |||||
[today.subtract(index, "day").format(INPUT_DATE_FORMAT)]: [], | |||||
}; | |||||
}, {}); | |||||
}, []); | |||||
const formProps = useForm<RecordTimesheetInput>({ defaultValues }); | |||||
const onCancel = useCallback(() => { | |||||
formProps.reset(defaultValues); | |||||
onClose(); | |||||
}, [defaultValues, formProps, onClose]); | |||||
return ( | |||||
<Modal open={isOpen} onClose={onClose}> | |||||
<Card sx={modalSx}> | |||||
<FormProvider {...formProps}> | |||||
<CardContent> | |||||
<Typography variant="overline" display="block" marginBlockEnd={1}> | |||||
{t(timesheetType === "time" ? "Timesheet Input" : "Record Leave")} | |||||
</Typography> | |||||
<Box | |||||
sx={{ | |||||
marginInline: -3, | |||||
marginBlock: 4, | |||||
}} | |||||
> | |||||
<TimesheetTable /> | |||||
</Box> | |||||
<CardActions sx={{ justifyContent: "flex-end" }}> | |||||
<Button | |||||
variant="outlined" | |||||
startIcon={<Close />} | |||||
onClick={onCancel} | |||||
> | |||||
{t("Cancel")} | |||||
</Button> | |||||
<Button | |||||
onClick={onClose} | |||||
variant="contained" | |||||
startIcon={<Check />} | |||||
type="submit" | |||||
> | |||||
{t("Confirm")} | |||||
</Button> | |||||
</CardActions> | |||||
</CardContent> | |||||
</FormProvider> | |||||
</Card> | |||||
</Modal> | |||||
); | |||||
}; | |||||
export default TimesheetModal; |
@@ -0,0 +1 @@ | |||||
export { default } from "./TimesheetModal"; |
@@ -0,0 +1,430 @@ | |||||
import { Add, Check, Close, Delete } from "@mui/icons-material"; | |||||
import { Box, Button, Typography } from "@mui/material"; | |||||
import { | |||||
FooterPropsOverrides, | |||||
GridActionsCellItem, | |||||
GridColDef, | |||||
GridEventListener, | |||||
GridRowId, | |||||
GridRowModel, | |||||
GridRowModes, | |||||
GridRowModesModel, | |||||
GridToolbarContainer, | |||||
useGridApiRef, | |||||
} from "@mui/x-data-grid"; | |||||
import { useTranslation } from "react-i18next"; | |||||
import StyledDataGrid from "../StyledDataGrid"; | |||||
import React, { useCallback, useEffect, useMemo, useState } from "react"; | |||||
import { useFormContext } from "react-hook-form"; | |||||
import { RecordTimesheetInput, TimeEntry } from "@/app/api/timesheets/actions"; | |||||
import { manhourFormatter } from "@/app/utils/formatUtil"; | |||||
import { AssignedProject } from "@/app/api/projects"; | |||||
import uniqBy from "lodash/uniqBy"; | |||||
import { TaskGroup } from "@/app/api/tasks"; | |||||
const mockProjects: AssignedProject[] = [ | |||||
{ | |||||
id: 1, | |||||
name: "Consultancy Project A", | |||||
code: "M1001 (C)", | |||||
tasks: [ | |||||
{ | |||||
id: 1, | |||||
name: "1.1 Preparation of preliminary Cost Estimate / Cost Plan including Revised & Refined", | |||||
description: null, | |||||
taskGroup: { | |||||
id: 1, | |||||
name: "1. Design & Cost Planning / Estimating", | |||||
}, | |||||
}, | |||||
{ | |||||
id: 6, | |||||
name: "2.1 Advise on tendering & contractual arrangement", | |||||
description: null, | |||||
taskGroup: { | |||||
id: 2, | |||||
name: "2. Tender Documentation", | |||||
}, | |||||
}, | |||||
], | |||||
}, | |||||
{ | |||||
id: 2, | |||||
name: "Consultancy Project B", | |||||
code: "M1354 (C)", | |||||
tasks: [ | |||||
{ | |||||
id: 1, | |||||
name: "1.1 Preparation of preliminary Cost Estimate / Cost Plan including Revised & Refined", | |||||
description: null, | |||||
taskGroup: { | |||||
id: 1, | |||||
name: "1. Design & Cost Planning / Estimating", | |||||
}, | |||||
}, | |||||
{ | |||||
id: 10, | |||||
name: "3.5 Attend tender interviews", | |||||
description: null, | |||||
taskGroup: { | |||||
id: 3, | |||||
name: "3. Tender Analysis & Report & Contract Documentation", | |||||
}, | |||||
}, | |||||
], | |||||
}, | |||||
{ | |||||
id: 3, | |||||
name: "Consultancy Project C", | |||||
code: "M1973 (C)", | |||||
tasks: [ | |||||
{ | |||||
id: 1, | |||||
name: "1.1 Preparation of preliminary Cost Estimate / Cost Plan including Revised & Refined", | |||||
description: null, | |||||
taskGroup: { | |||||
id: 1, | |||||
name: "1. Design & Cost Planning / Estimating", | |||||
}, | |||||
}, | |||||
{ | |||||
id: 20, | |||||
name: "4.10 Preparation of Statement of Final Account", | |||||
description: null, | |||||
taskGroup: { | |||||
id: 4, | |||||
name: "4. Construction / Post Construction", | |||||
}, | |||||
}, | |||||
], | |||||
}, | |||||
]; | |||||
type TimeEntryRow = Partial< | |||||
TimeEntry & { | |||||
_isNew: boolean; | |||||
_error: string; | |||||
id: string; | |||||
taskGroupId: number; | |||||
} | |||||
>; | |||||
const EntryInputTable: React.FC<{ day: string }> = ({ day }) => { | |||||
const { t } = useTranslation("home"); | |||||
const taskGroupsByProject = useMemo(() => { | |||||
return mockProjects.reduce<{ | |||||
[projectId: AssignedProject["id"]]: { | |||||
value: TaskGroup["id"]; | |||||
label: string; | |||||
}[]; | |||||
}>((acc, project) => { | |||||
return { | |||||
...acc, | |||||
[project.id]: uniqBy( | |||||
project.tasks.map((t) => ({ | |||||
value: t.taskGroup.id, | |||||
label: t.taskGroup.name, | |||||
})), | |||||
"value", | |||||
), | |||||
}; | |||||
}, {}); | |||||
}, []); | |||||
const { getValues, setValue } = useFormContext<RecordTimesheetInput>(); | |||||
const currentEntries = getValues(day); | |||||
const [entries, setEntries] = useState<TimeEntryRow[]>( | |||||
currentEntries.map((e, index) => ({ ...e, id: `${day}-${index}` })) || [], | |||||
); | |||||
const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); | |||||
const apiRef = useGridApiRef(); | |||||
const addRow = useCallback(() => { | |||||
const id = `${day}-${Date.now()}`; | |||||
setEntries((e) => [...e, { id, _isNew: true }]); | |||||
setRowModesModel((model) => ({ | |||||
...model, | |||||
[id]: { mode: GridRowModes.Edit, fieldToFocus: "projectId" }, | |||||
})); | |||||
}, [day]); | |||||
const validateRow = useCallback( | |||||
(id: GridRowId) => { | |||||
const row = apiRef.current.getRowWithUpdatedValues( | |||||
id, | |||||
"", | |||||
) as TimeEntryRow; | |||||
let error: keyof TimeEntry | "taskGroupId" | "" = ""; | |||||
if (!row.projectId) { | |||||
error = "projectId"; | |||||
} else if (!row.taskGroupId) { | |||||
error = "taskGroupId"; | |||||
} else if (!row.taskId) { | |||||
error = "taskId"; | |||||
} else if (!row.inputHours || !(row.inputHours >= 0)) { | |||||
error = "inputHours"; | |||||
} | |||||
apiRef.current.updateRows([{ id, _error: error }]); | |||||
return !error; | |||||
}, | |||||
[apiRef], | |||||
); | |||||
const handleCancel = useCallback( | |||||
(id: GridRowId) => () => { | |||||
setRowModesModel((model) => ({ | |||||
...model, | |||||
[id]: { mode: GridRowModes.View, ignoreModifications: true }, | |||||
})); | |||||
const editedRow = entries.find((entry) => entry.id === id); | |||||
if (editedRow?._isNew) { | |||||
setEntries((es) => es.filter((e) => e.id !== id)); | |||||
} | |||||
}, | |||||
[entries], | |||||
); | |||||
const handleDelete = useCallback( | |||||
(id: GridRowId) => () => { | |||||
setEntries((es) => es.filter((e) => e.id !== id)); | |||||
}, | |||||
[], | |||||
); | |||||
const handleSave = useCallback( | |||||
(id: GridRowId) => () => { | |||||
if (validateRow(id)) { | |||||
setRowModesModel((model) => ({ | |||||
...model, | |||||
[id]: { mode: GridRowModes.View }, | |||||
})); | |||||
} | |||||
}, | |||||
[validateRow], | |||||
); | |||||
const handleEditStop = useCallback<GridEventListener<"rowEditStop">>( | |||||
(params, event) => { | |||||
if (!validateRow(params.id)) { | |||||
event.defaultMuiPrevented = true; | |||||
} | |||||
}, | |||||
[validateRow], | |||||
); | |||||
const processRowUpdate = useCallback((newRow: GridRowModel) => { | |||||
const updatedRow = { ...newRow, _isNew: false }; | |||||
setEntries((es) => es.map((e) => (e.id === newRow.id ? updatedRow : e))); | |||||
return updatedRow; | |||||
}, []); | |||||
const columns = useMemo<GridColDef[]>( | |||||
() => [ | |||||
{ | |||||
type: "actions", | |||||
field: "actions", | |||||
headerName: t("Actions"), | |||||
getActions: ({ id }) => { | |||||
if (rowModesModel[id]?.mode === GridRowModes.Edit) { | |||||
return [ | |||||
<GridActionsCellItem | |||||
key="accpet-action" | |||||
icon={<Check />} | |||||
label={t("Save")} | |||||
onClick={handleSave(id)} | |||||
/>, | |||||
<GridActionsCellItem | |||||
key="cancel-action" | |||||
icon={<Close />} | |||||
label={t("Cancel")} | |||||
onClick={handleCancel(id)} | |||||
/>, | |||||
]; | |||||
} | |||||
return [ | |||||
<GridActionsCellItem | |||||
key="delete-action" | |||||
icon={<Delete />} | |||||
label={t("Remove")} | |||||
onClick={handleDelete(id)} | |||||
/>, | |||||
]; | |||||
}, | |||||
}, | |||||
{ | |||||
field: "projectId", | |||||
headerName: t("Project Code and Name"), | |||||
width: 200, | |||||
editable: true, | |||||
type: "singleSelect", | |||||
valueOptions() { | |||||
return mockProjects.map((p) => ({ value: p.id, label: p.name })); | |||||
}, | |||||
}, | |||||
{ | |||||
field: "taskGroupId", | |||||
headerName: t("Stage"), | |||||
width: 200, | |||||
editable: true, | |||||
type: "singleSelect", | |||||
valueOptions(params) { | |||||
const updatedRow = params.id | |||||
? apiRef.current.getRowWithUpdatedValues(params.id, "") | |||||
: null; | |||||
if (!updatedRow) { | |||||
return []; | |||||
} | |||||
const projectInfo = mockProjects.find( | |||||
(p) => p.id === updatedRow.projectId, | |||||
); | |||||
if (!projectInfo) { | |||||
return []; | |||||
} | |||||
return taskGroupsByProject[projectInfo.id]; | |||||
}, | |||||
}, | |||||
{ | |||||
field: "taskId", | |||||
headerName: t("Task"), | |||||
width: 200, | |||||
editable: true, | |||||
type: "singleSelect", | |||||
valueOptions(params) { | |||||
const updatedRow = params.id | |||||
? apiRef.current.getRowWithUpdatedValues(params.id, "") | |||||
: null; | |||||
if (!updatedRow) { | |||||
return []; | |||||
} | |||||
const projectInfo = mockProjects.find( | |||||
(p) => p.id === updatedRow.projectId, | |||||
); | |||||
if (!projectInfo) { | |||||
return []; | |||||
} | |||||
return projectInfo.tasks | |||||
.filter((t) => t.taskGroup.id === updatedRow.taskGroupId) | |||||
.map((t) => ({ | |||||
value: t.id, | |||||
label: t.name, | |||||
})); | |||||
}, | |||||
}, | |||||
{ | |||||
field: "inputHours", | |||||
headerName: t("Hours"), | |||||
width: 100, | |||||
editable: true, | |||||
type: "number", | |||||
valueFormatter(params) { | |||||
return manhourFormatter.format(params.value); | |||||
}, | |||||
}, | |||||
], | |||||
[ | |||||
t, | |||||
rowModesModel, | |||||
handleDelete, | |||||
handleSave, | |||||
handleCancel, | |||||
apiRef, | |||||
taskGroupsByProject, | |||||
], | |||||
); | |||||
useEffect(() => { | |||||
setValue(day, [ | |||||
...entries | |||||
.filter( | |||||
(e) => | |||||
!e._isNew && | |||||
!e._error && | |||||
e.inputHours && | |||||
e.projectId && | |||||
e.taskId && | |||||
e.taskGroupId, | |||||
) | |||||
.map((e) => ({ | |||||
inputHours: e.inputHours!, | |||||
projectId: e.projectId!, | |||||
taskId: e.taskId!, | |||||
taskGroupId: e.taskGroupId!, | |||||
})), | |||||
]); | |||||
}, [getValues, entries, setValue, day]); | |||||
return ( | |||||
<StyledDataGrid | |||||
apiRef={apiRef} | |||||
autoHeight | |||||
sx={{ | |||||
"--DataGrid-overlayHeight": "100px", | |||||
".MuiDataGrid-row .MuiDataGrid-cell.hasError": { | |||||
border: "1px solid", | |||||
borderColor: "error.main", | |||||
}, | |||||
}} | |||||
disableColumnMenu | |||||
editMode="row" | |||||
rows={entries} | |||||
rowModesModel={rowModesModel} | |||||
onRowModesModelChange={setRowModesModel} | |||||
onRowEditStop={handleEditStop} | |||||
processRowUpdate={processRowUpdate} | |||||
columns={columns} | |||||
getCellClassName={(params) => { | |||||
return params.row._error === params.field ? "hasError" : ""; | |||||
}} | |||||
slots={{ | |||||
footer: FooterToolbar, | |||||
noRowsOverlay: NoRowsOverlay, | |||||
}} | |||||
slotProps={{ | |||||
footer: { onAdd: addRow }, | |||||
}} | |||||
/> | |||||
); | |||||
}; | |||||
const NoRowsOverlay: React.FC = () => { | |||||
const { t } = useTranslation("home"); | |||||
return ( | |||||
<Box | |||||
display="flex" | |||||
justifyContent="center" | |||||
alignItems="center" | |||||
height="100%" | |||||
> | |||||
<Typography variant="caption">{t("Add some time entries!")}</Typography> | |||||
</Box> | |||||
); | |||||
}; | |||||
const FooterToolbar: React.FC<FooterPropsOverrides> = ({ onAdd }) => { | |||||
const { t } = useTranslation(); | |||||
return ( | |||||
<GridToolbarContainer sx={{ p: 2 }}> | |||||
<Button | |||||
disableRipple | |||||
variant="outlined" | |||||
startIcon={<Add />} | |||||
onClick={onAdd} | |||||
size="small" | |||||
> | |||||
{t("Record time")} | |||||
</Button> | |||||
</GridToolbarContainer> | |||||
); | |||||
}; | |||||
export default EntryInputTable; |
@@ -0,0 +1,106 @@ | |||||
import { RecordTimesheetInput, TimeEntry } from "@/app/api/timesheets/actions"; | |||||
import { manhourFormatter, shortDateFormatter } from "@/app/utils/formatUtil"; | |||||
import { KeyboardArrowDown, KeyboardArrowUp } from "@mui/icons-material"; | |||||
import { | |||||
Box, | |||||
Collapse, | |||||
IconButton, | |||||
Table, | |||||
TableBody, | |||||
TableCell, | |||||
TableContainer, | |||||
TableHead, | |||||
TableRow, | |||||
} from "@mui/material"; | |||||
import dayjs from "dayjs"; | |||||
import React, { useState } from "react"; | |||||
import { useFormContext } from "react-hook-form"; | |||||
import { useTranslation } from "react-i18next"; | |||||
import EntryInputTable from "./EntryInputTable"; | |||||
const TimesheetTable: React.FC = () => { | |||||
const { t } = useTranslation("home"); | |||||
const { watch } = useFormContext<RecordTimesheetInput>(); | |||||
const currentInput = watch(); | |||||
const days = Object.keys(currentInput); | |||||
return ( | |||||
<TableContainer sx={{ maxHeight: 400 }}> | |||||
<Table stickyHeader> | |||||
<TableHead> | |||||
<TableRow> | |||||
<TableCell /> | |||||
<TableCell>{t("Date")}</TableCell> | |||||
<TableCell>{t("Daily Total Hours")}</TableCell> | |||||
</TableRow> | |||||
</TableHead> | |||||
<TableBody> | |||||
{days.map((day, index) => { | |||||
const entries = currentInput[day]; | |||||
return ( | |||||
<DayRow key={`${day}${index}`} day={day} entries={entries} /> | |||||
); | |||||
})} | |||||
</TableBody> | |||||
</Table> | |||||
</TableContainer> | |||||
); | |||||
}; | |||||
const DayRow: React.FC<{ day: string; entries: TimeEntry[] }> = ({ | |||||
day, | |||||
entries, | |||||
}) => { | |||||
const { | |||||
i18n: { language }, | |||||
} = useTranslation("home"); | |||||
const dayJsObj = dayjs(day); | |||||
const [open, setOpen] = useState(false); | |||||
const totalHours = entries.reduce((acc, entry) => acc + entry.inputHours, 0); | |||||
return ( | |||||
<> | |||||
<TableRow> | |||||
<TableCell align="center" width={70}> | |||||
<IconButton | |||||
disableRipple | |||||
aria-label="expand row" | |||||
size="small" | |||||
onClick={() => setOpen(!open)} | |||||
> | |||||
{open ? <KeyboardArrowUp /> : <KeyboardArrowDown />} | |||||
</IconButton> | |||||
</TableCell> | |||||
<TableCell | |||||
sx={{ color: dayJsObj.day() === 0 ? "error.main" : undefined }} | |||||
> | |||||
{shortDateFormatter(language).format(dayJsObj.toDate())} | |||||
</TableCell> | |||||
<TableCell sx={{ color: totalHours > 20 ? "error.main" : undefined }}> | |||||
{manhourFormatter.format(totalHours)} | |||||
</TableCell> | |||||
</TableRow> | |||||
<TableRow> | |||||
<TableCell | |||||
sx={{ | |||||
p: 0, | |||||
border: "none", | |||||
outline: open ? "1px solid" : undefined, | |||||
outlineColor: "primary.main", | |||||
}} | |||||
colSpan={3} | |||||
> | |||||
<Collapse in={open} timeout="auto" unmountOnExit> | |||||
<Box> | |||||
<EntryInputTable day={day} /> | |||||
</Box> | |||||
</Collapse> | |||||
</TableCell> | |||||
</TableRow> | |||||
</> | |||||
); | |||||
}; | |||||
export default TimesheetTable; |
@@ -0,0 +1 @@ | |||||
export { default } from "./TimesheetTable"; |
@@ -11,6 +11,7 @@ import EnterLeaveModal from "../EnterLeave/EnterLeaveModal"; | |||||
import ButtonGroup from "@mui/material/ButtonGroup"; | import ButtonGroup from "@mui/material/ButtonGroup"; | ||||
import AssignedProjects from "./AssignedProjects"; | import AssignedProjects from "./AssignedProjects"; | ||||
import { ProjectHours } from "./UserWorkspaceWrapper"; | import { ProjectHours } from "./UserWorkspaceWrapper"; | ||||
import TimesheetModal from "../TimesheetModal"; | |||||
export interface Props { | export interface Props { | ||||
allProjects: ProjectHours[]; | allProjects: ProjectHours[]; | ||||
@@ -68,7 +69,12 @@ const UserWorkspacePage: React.FC<Props> = ({ allProjects }) => { | |||||
isOpen={isTimeheetModalVisible} | isOpen={isTimeheetModalVisible} | ||||
onClose={handleCloseTimesheetModal} | onClose={handleCloseTimesheetModal} | ||||
/> | /> | ||||
<EnterLeaveModal | |||||
{/* <EnterLeaveModal | |||||
isOpen={isLeaveModalVisible} | |||||
onClose={handleCloseLeaveModal} | |||||
/> */} | |||||
<TimesheetModal | |||||
timesheetType="leave" | |||||
isOpen={isLeaveModalVisible} | isOpen={isLeaveModalVisible} | ||||
onClose={handleCloseLeaveModal} | onClose={handleCloseLeaveModal} | ||||
/> | /> | ||||