diff --git a/src/app/api/timesheets/index.ts b/src/app/api/timesheets/index.ts index ff0f5ad..9f0a0f3 100644 --- a/src/app/api/timesheets/index.ts +++ b/src/app/api/timesheets/index.ts @@ -13,6 +13,7 @@ export type TeamTimeSheets = { timeEntries: RecordTimesheetInput; staffId: string; name: string; + employType: string | null; }; }; @@ -21,6 +22,7 @@ export type TeamLeaves = { leaveEntries: RecordLeaveInput; staffId: string; name: string; + employType: string | null; }; }; diff --git a/src/app/api/timesheets/utils.ts b/src/app/api/timesheets/utils.ts index 714b1c8..10b128b 100644 --- a/src/app/api/timesheets/utils.ts +++ b/src/app/api/timesheets/utils.ts @@ -1,14 +1,9 @@ import { getPublicHolidaysForNYears } from "@/app/utils/holidayUtils"; import { HolidaysResult } from "../holidays"; -import { - LeaveEntry, - RecordLeaveInput, - RecordTimeLeaveInput, - RecordTimesheetInput, - TimeEntry, -} from "./actions"; +import { LeaveEntry, RecordTimeLeaveInput, TimeEntry } from "./actions"; import { convertDateArrayToString } from "@/app/utils/formatUtil"; import compact from "lodash/compact"; +import dayjs from "dayjs"; export type TimeEntryError = { [field in keyof TimeEntry]?: string; @@ -83,85 +78,10 @@ export const validateLeaveEntry = ( return Object.keys(error).length > 0 ? error : undefined; }; -export const validateTimesheet = ( - timesheet: RecordTimesheetInput, - leaveRecords: RecordLeaveInput, - companyHolidays: HolidaysResult[], -): { [date: string]: string } | undefined => { - const errors: { [date: string]: string } = {}; - - const holidays = new Set( - compact([ - ...getPublicHolidaysForNYears(2).map((h) => h.date), - ...companyHolidays.map((h) => convertDateArrayToString(h.date)), - ]), - ); - - Object.keys(timesheet).forEach((date) => { - const timeEntries = timesheet[date]; - - // Check each entry - for (const entry of timeEntries) { - const entryErrors = validateTimeEntry(entry, holidays.has(date)); - - if (entryErrors) { - errors[date] = "There are errors in the entries"; - return; - } - } - - // Check total hours - const leaves = leaveRecords[date] || []; - const totalHourError = checkTotalHours(timeEntries, leaves); - if (totalHourError) { - errors[date] = totalHourError; - } - }); - - return Object.keys(errors).length > 0 ? errors : undefined; -}; - -export const validateLeaveRecord = ( - leaveRecords: RecordLeaveInput, - timesheet: RecordTimesheetInput, - companyHolidays: HolidaysResult[], -): { [date: string]: string } | undefined => { - const errors: { [date: string]: string } = {}; - - const holidays = new Set( - compact([ - ...getPublicHolidaysForNYears(2).map((h) => h.date), - ...companyHolidays.map((h) => convertDateArrayToString(h.date)), - ]), - ); - - Object.keys(leaveRecords).forEach((date) => { - const leaves = leaveRecords[date]; - - // Check each leave entry - for (const entry of leaves) { - const entryError = validateLeaveEntry(entry, holidays.has(date)); - if (entryError) { - errors[date] = "There are errors in the entries"; - return; - } - } - - // Check total hours - const timeEntries = timesheet[date] || []; - - const totalHourError = checkTotalHours(timeEntries, leaves); - if (totalHourError) { - errors[date] = totalHourError; - } - }); - - return Object.keys(errors).length > 0 ? errors : undefined; -}; - export const validateTimeLeaveRecord = ( records: RecordTimeLeaveInput, companyHolidays: HolidaysResult[], + isFullTime?: boolean, ): { [date: string]: string } | undefined => { const errors: { [date: string]: string } = {}; @@ -173,14 +93,18 @@ export const validateTimeLeaveRecord = ( ); Object.keys(records).forEach((date) => { + const dayJsObj = dayjs(date); + const isHoliday = + holidays.has(date) || dayJsObj.day() === 0 || dayJsObj.day() === 6; + const entries = records[date]; // Check each entry for (const entry of entries) { let entryError; if (entry.type === "leaveEntry") { - entryError = validateLeaveEntry(entry, holidays.has(date)); + entryError = validateLeaveEntry(entry, isHoliday); } else { - entryError = validateTimeEntry(entry, holidays.has(date)); + entryError = validateTimeEntry(entry, isHoliday); } if (entryError) { @@ -191,8 +115,10 @@ export const validateTimeLeaveRecord = ( // Check total hours const totalHourError = checkTotalHours( - entries.filter((e) => e.type === "timeEntry"), - entries.filter((e) => e.type === "leaveEntry"), + entries.filter((e) => e.type === "timeEntry") as TimeEntry[], + entries.filter((e) => e.type === "leaveEntry") as LeaveEntry[], + isHoliday, + isFullTime, ); if (totalHourError) { @@ -206,6 +132,8 @@ export const validateTimeLeaveRecord = ( export const checkTotalHours = ( timeEntries: TimeEntry[], leaves: LeaveEntry[], + isHoliday?: boolean, + isFullTime?: boolean, ): string | undefined => { const leaveHours = leaves.reduce((acc, entry) => acc + entry.inputHours, 0); @@ -219,6 +147,12 @@ export const checkTotalHours = ( if (totalInputHours + leaveHours > DAILY_NORMAL_MAX_HOURS) { return "The daily normal hours (timesheet hours + leave hours) cannot be more than {{DAILY_NORMAL_MAX_HOURS}}. Please use other hours for exceeding hours or decrease the leave hours."; + } else if ( + isFullTime && + !isHoliday && + totalInputHours + leaveHours !== DAILY_NORMAL_MAX_HOURS + ) { + return "The daily normal hours (timesheet hours + leave hours) for full-time staffs should be {{DAILY_NORMAL_MAX_HOURS}}."; } else if ( totalInputHours + totalOtHours + leaveHours > TIMESHEET_DAILY_MAX_HOURS diff --git a/src/app/utils/formatUtil.ts b/src/app/utils/formatUtil.ts index 8ecc9b8..fff7d45 100644 --- a/src/app/utils/formatUtil.ts +++ b/src/app/utils/formatUtil.ts @@ -134,8 +134,8 @@ export function convertLocaleStringToNumber(numberString: string): number { } export function timestampToDateString(timestamp: string): string { - if (timestamp === "0" || timestamp === null) { - return "-"; + if (timestamp === null){ + return "-" } const date = new Date(timestamp); const year = date.getFullYear(); diff --git a/src/components/CreateProject/ProjectClientDetails.tsx b/src/components/CreateProject/ProjectClientDetails.tsx index e0333d4..7413f0a 100644 --- a/src/components/CreateProject/ProjectClientDetails.tsx +++ b/src/components/CreateProject/ProjectClientDetails.tsx @@ -344,7 +344,7 @@ const ProjectClientDetails: React.FC = ({ /> - + {/* = ({ {t("CLP Project")} - + */} diff --git a/src/components/EnterLeave/EnterLeaveModal.tsx b/src/components/EnterLeave/EnterLeaveModal.tsx deleted file mode 100644 index 97f0d3c..0000000 --- a/src/components/EnterLeave/EnterLeaveModal.tsx +++ /dev/null @@ -1,109 +0,0 @@ -"use client"; - -import { useState } from "react"; -import Button from "@mui/material/Button"; -import { Card, Modal } from "@mui/material"; -import TimesheetInputGrid from "./LeaveInputGrid"; - -// import { fetchLeaves } from "@/app/api/leave"; - -interface EnterTimesheetModalProps { - isOpen: boolean; - onClose: () => void; - modalStyle?: any; -} - -const EnterTimesheetModal: React.FC = ({ - ...props -}) => { - const [lockConfirm, setLockConfirm] = useState(false); - const columns = [ - { - id: "projectCode", - field: "projectCode", - headerName: "Project Code and Name", - flex: 1, - }, - { - id: "task", - field: "task", - headerName: "Task", - flex: 1, - }, - ]; - - const rows = [ - { - id: 1, - projectCode: "M1001", - task: "1.2", - }, - { - id: 2, - projectCode: "M1301", - task: "1.1", - }, - ]; - - const fetchTimesheet = async () => { - // fetchLeaves(); - // const res = await fetch(`http://localhost:8090/api/timesheets`, { - // // const res = await fetch(`${BASE_API_URL}/timesheets`, { - // method: "GET", - // mode: 'no-cors', - // }); - - // console.log(res.json); - }; - - return ( - -
- {/* -
- Record Leave -
-
*/} - - - -
- - -
-
-
-
- ); -}; - -export default EnterTimesheetModal; diff --git a/src/components/EnterLeave/LeaveInputGrid.tsx b/src/components/EnterLeave/LeaveInputGrid.tsx deleted file mode 100644 index 03eeaab..0000000 --- a/src/components/EnterLeave/LeaveInputGrid.tsx +++ /dev/null @@ -1,548 +0,0 @@ -"use client"; -import Grid from "@mui/material/Grid"; -import Paper from "@mui/material/Paper"; -import { useState, useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import PageTitle from "../PageTitle/PageTitle"; -import { Suspense } from "react"; -import Button from "@mui/material/Button"; -import Stack from "@mui/material/Stack"; -import Link from "next/link"; -import { t } from "i18next"; -import { - Box, - Container, - Modal, - Select, - SelectChangeEvent, - Typography, -} from "@mui/material"; -import { Close } from "@mui/icons-material"; -import AddIcon from "@mui/icons-material/Add"; -import EditIcon from "@mui/icons-material/Edit"; -import DeleteIcon from "@mui/icons-material/DeleteOutlined"; -import SaveIcon from "@mui/icons-material/Save"; -import CancelIcon from "@mui/icons-material/Close"; -import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; -import ArrowBackIcon from "@mui/icons-material/ArrowBack"; -import Swal from "sweetalert2"; -import { msg } from "../Swal/CustomAlerts"; -import React from "react"; -import { DatePicker } from "@mui/x-date-pickers/DatePicker"; -import { - GridRowsProp, - GridRowModesModel, - GridRowModes, - DataGrid, - GridColDef, - GridToolbarContainer, - GridFooterContainer, - GridActionsCellItem, - GridEventListener, - GridRowId, - GridRowModel, - GridRowEditStopReasons, - GridEditInputCell, - GridValueSetterParams, -} from "@mui/x-data-grid"; -import { LocalizationProvider } from "@mui/x-date-pickers"; -import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; -import dayjs from "dayjs"; -import { Props } from "react-intl/src/components/relative"; - -const weekdays = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]; - -interface BottomBarProps { - getHoursTotal: (column: string) => number; - setLockConfirm: (newLock: (oldLock: boolean) => boolean) => void; - setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; - setRowModesModel: ( - newModel: (oldModel: GridRowModesModel) => GridRowModesModel, - ) => void; -} - -interface EditToolbarProps { - // setDay: (newDay : dayjs.Dayjs) => void; - setDay: (newDay: (oldDay: dayjs.Dayjs) => dayjs.Dayjs) => void; - setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; - setRowModesModel: ( - newModel: (oldModel: GridRowModesModel) => GridRowModesModel, - ) => void; -} - -interface EditFooterProps { - setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; - setRowModesModel: ( - newModel: (oldModel: GridRowModesModel) => GridRowModesModel, - ) => void; -} - -const EditToolbar = (props: EditToolbarProps) => { - const { setDay } = props; - const [selectedDate, setSelectedDate] = useState(dayjs()); - - const handleClickLeft = () => { - if (selectedDate) { - const newDate = selectedDate.add(-7, "day"); - setSelectedDate(newDate); - } - }; - const handleClickRight = () => { - if (selectedDate) { - const newDate = - selectedDate.add(7, "day") > dayjs() - ? dayjs() - : selectedDate.add(7, "day"); - setSelectedDate(newDate); - } - }; - - const handleDateChange = (date: dayjs.Dayjs | Date | null) => { - const newDate = dayjs(date); - setSelectedDate(newDate); - }; - - useEffect(() => { - setDay((oldDay) => selectedDate); - }, [selectedDate]); - - return ( - -
- - Record Leave - - - - -
-
- ); -}; - -const BottomBar = (props: BottomBarProps) => { - const { setRows, setRowModesModel, getHoursTotal, setLockConfirm } = props; - // const getHoursTotal = props.getHoursTotal; - const [newId, setNewId] = useState(-1); - const [invalidDays, setInvalidDays] = useState(0); - - const handleAddClick = () => { - const id = newId; - setNewId(newId - 1); - setRows((oldRows) => [ - ...oldRows, - { id, projectCode: "", task: "", isNew: true }, - ]); - setRowModesModel((oldModel) => ({ - ...oldModel, - [id]: { mode: GridRowModes.Edit, fieldToFocus: "projectCode" }, - })); - }; - - const totalColDef = { - flex: 1, - // style: {color:getHoursTotal('mon')>24?"red":"black"} - }; - - const TotalCell = ({ value }: Props) => { - const [invalid, setInvalid] = useState(false); - - useEffect(() => { - const newInvalid = (value ?? 0) > 24; - setInvalid(newInvalid); - }, [value]); - - return ( - - {value} - - ); - }; - - const checkUnlockConfirmBtn = () => { - // setLockConfirm((oldLock)=> valid); - setLockConfirm((oldLock) => - weekdays.every((weekday) => { - getHoursTotal(weekday) <= 24; - }), - ); - }; - - return ( -
-
- - Total: - - - - - - - - -
- -
- ); -}; - -const EditFooter = (props: EditFooterProps) => { - return ( -
- - Total: - - ssss -
- ); -}; - -interface TimesheetInputGridProps { - setLockConfirm: (newLock: (oldLock: boolean) => boolean) => void; - onClose?: () => void; -} - -const initialRows: GridRowsProp = [ - { - id: 1, - projectCode: "M1001", - task: "1.2", - mon: 2.5, - }, - { - id: 2, - projectCode: "M1002", - task: "1.3", - mon: 3.25, - }, -]; - -const options = ["M1001", "M1301", "M1354", "M1973"]; -const options2 = [ - "1.1 - Preparation of preliminary Cost Estimate / Cost Plan", - "1.2 - Cash flow forecast", - "1.3 - Cost studies fo alterative design solutions", - "1.4 = Attend design co-ordination / project review meetings", - "1.5 - Prepare / Review RIC", -]; - -const getDateForHeader = (date: dayjs.Dayjs, weekday: number) => { - if (date.day() == 0) { - return date.add(weekday - date.day() - 7, "day").format("DD MMM"); - } else { - return date.add(weekday - date.day(), "day").format("DD MMM"); - } -}; - -const TimesheetInputGrid: React.FC = ({ - ...props -}) => { - const [rows, setRows] = useState(initialRows); - const [day, setDay] = useState(dayjs()); - const [rowModesModel, setRowModesModel] = React.useState( - {}, - ); - const { setLockConfirm } = props; - - const handleRowEditStop: GridEventListener<"rowEditStop"> = ( - params, - event, - ) => { - if (params.reason === GridRowEditStopReasons.rowFocusOut) { - event.defaultMuiPrevented = true; - } - }; - - const handleEditClick = (id: GridRowId) => () => { - setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } }); - }; - - const handleSaveClick = (id: GridRowId) => () => { - setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } }); - }; - - const handleDeleteClick = (id: GridRowId) => () => { - setRows(rows.filter((row) => row.id !== id)); - }; - - const handleCancelClick = (id: GridRowId) => () => { - setRowModesModel({ - ...rowModesModel, - [id]: { mode: GridRowModes.View, ignoreModifications: true }, - }); - - const editedRow = rows.find((row) => row.id === id); - if (editedRow!.isNew) { - setRows(rows.filter((row) => row.id !== id)); - } - }; - - const processRowUpdate = (newRow: GridRowModel) => { - const updatedRow = { ...newRow, isNew: false }; - setRows(rows.map((row) => (row.id === newRow.id ? updatedRow : row))); - return updatedRow; - }; - - const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => { - setRowModesModel(newRowModesModel); - }; - - const getHoursTotal = (column: any) => { - let sum = 0; - rows.forEach((row) => { - sum += row[column] ?? 0; - }); - return sum; - }; - - const weekdayColConfig: any = { - type: "number", - // sortable: false, - //width: 100, - flex: 1, - align: "left", - headerAlign: "left", - editable: true, - renderEditCell: (value: any) => ( - - ), - }; - - const columns: GridColDef[] = [ - { - field: "actions", - type: "actions", - headerName: "Actions", - width: 100, - cellClassName: "actions", - getActions: ({ id }) => { - const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; - - if (isInEditMode) { - return [ - } - title="Save" - label="Save" - sx={{ - color: "primary.main", - }} - onClick={handleSaveClick(id)} - />, - } - title="Cancel" - label="Cancel" - className="textPrimary" - onClick={handleCancelClick(id)} - color="inherit" - />, - ]; - } - - return [ - } - title="Edit" - label="Edit" - className="textPrimary" - onClick={handleEditClick(id)} - color="inherit" - />, - } - onClick={handleDeleteClick(id)} - sx={{ color: "red" }} - />, - ]; - }, - }, - { - field: "projectCode", - headerName: "Project Code", - // width: 220, - flex: 2, - editable: true, - type: "singleSelect", - valueOptions: options, - }, - { - field: "task", - headerName: "Task", - // width: 220, - flex: 3, - editable: true, - type: "singleSelect", - valueOptions: options2, - }, - { - // Mon - field: "mon", - ...weekdayColConfig, - renderHeader: () => { - return
Mon - {getDateForHeader(day, 1)}
; - }, - }, - { - // Tue - field: "tue", - ...weekdayColConfig, - renderHeader: () => { - return
Tue - {getDateForHeader(day, 2)}
; - }, - }, - { - // Wed - field: "wed", - ...weekdayColConfig, - renderHeader: () => { - return
Wed - {getDateForHeader(day, 3)}
; - }, - }, - { - // Thu - field: "thu", - ...weekdayColConfig, - renderHeader: () => { - return
Thu - {getDateForHeader(day, 4)}
; - }, - }, - { - // Fri - field: "fri", - ...weekdayColConfig, - renderHeader: () => { - return
Fri - {getDateForHeader(day, 5)}
; - }, - }, - { - // Sat - field: "sat", - ...weekdayColConfig, - renderHeader: () => { - return
Sat - {getDateForHeader(day, 6)}
; - }, - }, - { - // Sun - field: "sun", - ...weekdayColConfig, - renderHeader: () => { - return ( -
Sun - {getDateForHeader(day, 7)}
- ); - }, - }, - // { - // field: 'joinDate', - // headerName: 'Join date', - // type: 'date', - // width: 180, - // editable: true, - // }, - ]; - - return ( - - - - - - ); -}; - -export default TimesheetInputGrid; diff --git a/src/components/EnterLeave/index.ts b/src/components/EnterLeave/index.ts deleted file mode 100644 index 33541f2..0000000 --- a/src/components/EnterLeave/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./EnterLeaveModal"; diff --git a/src/components/EnterTimesheet/EnterTimesheetModal.tsx b/src/components/EnterTimesheet/EnterTimesheetModal.tsx deleted file mode 100644 index d8854a1..0000000 --- a/src/components/EnterTimesheet/EnterTimesheetModal.tsx +++ /dev/null @@ -1,109 +0,0 @@ -"use client"; - -import { useState } from "react"; -import Button from "@mui/material/Button"; -import { Card, Modal } from "@mui/material"; -import TimesheetInputGrid from "./TimesheetInputGrid"; - -// import { fetchTimesheets } from "@/app/api/timesheets"; - -interface EnterTimesheetModalProps { - isOpen: boolean; - onClose: () => void; - modalStyle?: any; -} - -const EnterTimesheetModal: React.FC = ({ - ...props -}) => { - const [lockConfirm, setLockConfirm] = useState(false); - const columns = [ - { - id: "projectCode", - field: "projectCode", - headerName: "Project Code and Name", - flex: 1, - }, - { - id: "task", - field: "task", - headerName: "Task", - flex: 1, - }, - ]; - - const rows = [ - { - id: 1, - projectCode: "M1001", - task: "1.2", - }, - { - id: 2, - projectCode: "M1301", - task: "1.1", - }, - ]; - - const fetchTimesheet = async () => { - // fetchTimesheets(); - // const res = await fetch(`http://localhost:8090/api/timesheets`, { - // // const res = await fetch(`${BASE_API_URL}/timesheets`, { - // method: "GET", - // mode: 'no-cors', - // }); - - // console.log(res.json); - }; - - return ( - -
- {/* -
- Timesheet Input -
-
*/} - - - -
- - -
-
-
-
- ); -}; - -export default EnterTimesheetModal; diff --git a/src/components/EnterTimesheet/TimesheetInputGrid.tsx b/src/components/EnterTimesheet/TimesheetInputGrid.tsx deleted file mode 100644 index bc64c50..0000000 --- a/src/components/EnterTimesheet/TimesheetInputGrid.tsx +++ /dev/null @@ -1,548 +0,0 @@ -"use client"; -import Grid from "@mui/material/Grid"; -import Paper from "@mui/material/Paper"; -import { useState, useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import PageTitle from "../PageTitle/PageTitle"; -import { Suspense } from "react"; -import Button from "@mui/material/Button"; -import Stack from "@mui/material/Stack"; -import Link from "next/link"; -import { t } from "i18next"; -import { - Box, - Container, - Modal, - Select, - SelectChangeEvent, - Typography, -} from "@mui/material"; -import { Close } from "@mui/icons-material"; -import AddIcon from "@mui/icons-material/Add"; -import EditIcon from "@mui/icons-material/Edit"; -import DeleteIcon from "@mui/icons-material/DeleteOutlined"; -import SaveIcon from "@mui/icons-material/Save"; -import CancelIcon from "@mui/icons-material/Close"; -import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; -import ArrowBackIcon from "@mui/icons-material/ArrowBack"; -import Swal from "sweetalert2"; -import { msg } from "../Swal/CustomAlerts"; -import React from "react"; -import { DatePicker } from "@mui/x-date-pickers/DatePicker"; -import { - GridRowsProp, - GridRowModesModel, - GridRowModes, - DataGrid, - GridColDef, - GridToolbarContainer, - GridFooterContainer, - GridActionsCellItem, - GridEventListener, - GridRowId, - GridRowModel, - GridRowEditStopReasons, - GridEditInputCell, - GridValueSetterParams, -} from "@mui/x-data-grid"; -import { LocalizationProvider } from "@mui/x-date-pickers"; -import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; -import dayjs from "dayjs"; -import { Props } from "react-intl/src/components/relative"; - -const weekdays = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]; - -interface BottomBarProps { - getHoursTotal: (column: string) => number; - setLockConfirm: (newLock: (oldLock: boolean) => boolean) => void; - setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; - setRowModesModel: ( - newModel: (oldModel: GridRowModesModel) => GridRowModesModel, - ) => void; -} - -interface EditToolbarProps { - // setDay: (newDay : dayjs.Dayjs) => void; - setDay: (newDay: (oldDay: dayjs.Dayjs) => dayjs.Dayjs) => void; - setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; - setRowModesModel: ( - newModel: (oldModel: GridRowModesModel) => GridRowModesModel, - ) => void; -} - -interface EditFooterProps { - setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; - setRowModesModel: ( - newModel: (oldModel: GridRowModesModel) => GridRowModesModel, - ) => void; -} - -const EditToolbar = (props: EditToolbarProps) => { - const { setDay } = props; - const [selectedDate, setSelectedDate] = useState(dayjs()); - - const handleClickLeft = () => { - if (selectedDate) { - const newDate = selectedDate.add(-7, "day"); - setSelectedDate(newDate); - } - }; - const handleClickRight = () => { - if (selectedDate) { - const newDate = - selectedDate.add(7, "day") > dayjs() - ? dayjs() - : selectedDate.add(7, "day"); - setSelectedDate(newDate); - } - }; - - const handleDateChange = (date: dayjs.Dayjs | Date | null) => { - const newDate = dayjs(date); - setSelectedDate(newDate); - }; - - useEffect(() => { - setDay((oldDay) => selectedDate); - }, [selectedDate]); - - return ( - -
- - Timesheet Input - - - - -
-
- ); -}; - -const BottomBar = (props: BottomBarProps) => { - const { setRows, setRowModesModel, getHoursTotal, setLockConfirm } = props; - // const getHoursTotal = props.getHoursTotal; - const [newId, setNewId] = useState(-1); - const [invalidDays, setInvalidDays] = useState(0); - - const handleAddClick = () => { - const id = newId; - setNewId(newId - 1); - setRows((oldRows) => [ - ...oldRows, - { id, projectCode: "", task: "", isNew: true }, - ]); - setRowModesModel((oldModel) => ({ - ...oldModel, - [id]: { mode: GridRowModes.Edit, fieldToFocus: "projectCode" }, - })); - }; - - const totalColDef = { - flex: 1, - // style: {color:getHoursTotal('mon')>24?"red":"black"} - }; - - const TotalCell = ({ value }: Props) => { - const [invalid, setInvalid] = useState(false); - - useEffect(() => { - const newInvalid = (value ?? 0) > 24; - setInvalid(newInvalid); - }, [value]); - - return ( - - {value} - - ); - }; - - const checkUnlockConfirmBtn = () => { - // setLockConfirm((oldLock)=> valid); - setLockConfirm((oldLock) => - weekdays.every((weekday) => { - getHoursTotal(weekday) <= 24; - }), - ); - }; - - return ( -
-
- - Total: - - - - - - - - -
- -
- ); -}; - -const EditFooter = (props: EditFooterProps) => { - return ( -
- - Total: - - ssss -
- ); -}; - -interface TimesheetInputGridProps { - setLockConfirm: (newLock: (oldLock: boolean) => boolean) => void; - onClose?: () => void; -} - -const initialRows: GridRowsProp = [ - { - id: 1, - projectCode: "M1001", - task: "1.2", - mon: 2.5, - }, - { - id: 2, - projectCode: "M1002", - task: "1.3", - mon: 3.25, - }, -]; - -const options = ["M1001", "M1301", "M1354", "M1973"]; -const options2 = [ - "1.1 - Preparation of preliminary Cost Estimate / Cost Plan", - "1.2 - Cash flow forecast", - "1.3 - Cost studies fo alterative design solutions", - "1.4 = Attend design co-ordination / project review meetings", - "1.5 - Prepare / Review RIC", -]; - -const getDateForHeader = (date: dayjs.Dayjs, weekday: number) => { - if (date.day() == 0) { - return date.add(weekday - date.day() - 7, "day").format("DD MMM"); - } else { - return date.add(weekday - date.day(), "day").format("DD MMM"); - } -}; - -const TimesheetInputGrid: React.FC = ({ - ...props -}) => { - const [rows, setRows] = useState(initialRows); - const [day, setDay] = useState(dayjs()); - const [rowModesModel, setRowModesModel] = React.useState( - {}, - ); - const { setLockConfirm } = props; - - const handleRowEditStop: GridEventListener<"rowEditStop"> = ( - params, - event, - ) => { - if (params.reason === GridRowEditStopReasons.rowFocusOut) { - event.defaultMuiPrevented = true; - } - }; - - const handleEditClick = (id: GridRowId) => () => { - setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } }); - }; - - const handleSaveClick = (id: GridRowId) => () => { - setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } }); - }; - - const handleDeleteClick = (id: GridRowId) => () => { - setRows(rows.filter((row) => row.id !== id)); - }; - - const handleCancelClick = (id: GridRowId) => () => { - setRowModesModel({ - ...rowModesModel, - [id]: { mode: GridRowModes.View, ignoreModifications: true }, - }); - - const editedRow = rows.find((row) => row.id === id); - if (editedRow!.isNew) { - setRows(rows.filter((row) => row.id !== id)); - } - }; - - const processRowUpdate = (newRow: GridRowModel) => { - const updatedRow = { ...newRow, isNew: false }; - setRows(rows.map((row) => (row.id === newRow.id ? updatedRow : row))); - return updatedRow; - }; - - const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => { - setRowModesModel(newRowModesModel); - }; - - const getHoursTotal = (column: any) => { - let sum = 0; - rows.forEach((row) => { - sum += row[column] ?? 0; - }); - return sum; - }; - - const weekdayColConfig: any = { - type: "number", - // sortable: false, - //width: 100, - flex: 1, - align: "left", - headerAlign: "left", - editable: true, - renderEditCell: (value: any) => ( - - ), - }; - - const columns: GridColDef[] = [ - { - field: "actions", - type: "actions", - headerName: "Actions", - width: 100, - cellClassName: "actions", - getActions: ({ id }) => { - const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; - - if (isInEditMode) { - return [ - } - title="Save" - label="Save" - sx={{ - color: "primary.main", - }} - onClick={handleSaveClick(id)} - />, - } - title="Cancel" - label="Cancel" - className="textPrimary" - onClick={handleCancelClick(id)} - color="inherit" - />, - ]; - } - - return [ - } - title="Edit" - label="Edit" - className="textPrimary" - onClick={handleEditClick(id)} - color="inherit" - />, - } - onClick={handleDeleteClick(id)} - sx={{ color: "red" }} - />, - ]; - }, - }, - { - field: "projectCode", - headerName: "Project Code", - // width: 220, - flex: 2, - editable: true, - type: "singleSelect", - valueOptions: options, - }, - { - field: "task", - headerName: "Task", - // width: 220, - flex: 3, - editable: true, - type: "singleSelect", - valueOptions: options2, - }, - { - // Mon - field: "mon", - ...weekdayColConfig, - renderHeader: () => { - return
Mon - {getDateForHeader(day, 1)}
; - }, - }, - { - // Tue - field: "tue", - ...weekdayColConfig, - renderHeader: () => { - return
Tue - {getDateForHeader(day, 2)}
; - }, - }, - { - // Wed - field: "wed", - ...weekdayColConfig, - renderHeader: () => { - return
Wed - {getDateForHeader(day, 3)}
; - }, - }, - { - // Thu - field: "thu", - ...weekdayColConfig, - renderHeader: () => { - return
Thu - {getDateForHeader(day, 4)}
; - }, - }, - { - // Fri - field: "fri", - ...weekdayColConfig, - renderHeader: () => { - return
Fri - {getDateForHeader(day, 5)}
; - }, - }, - { - // Sat - field: "sat", - ...weekdayColConfig, - renderHeader: () => { - return
Sat - {getDateForHeader(day, 6)}
; - }, - }, - { - // Sun - field: "sun", - ...weekdayColConfig, - renderHeader: () => { - return ( -
Sun - {getDateForHeader(day, 7)}
- ); - }, - }, - // { - // field: 'joinDate', - // headerName: 'Join date', - // type: 'date', - // width: 180, - // editable: true, - // }, - ]; - - return ( - - - - - - ); -}; - -export default TimesheetInputGrid; diff --git a/src/components/EnterTimesheet/index.ts b/src/components/EnterTimesheet/index.ts deleted file mode 100644 index e070291..0000000 --- a/src/components/EnterTimesheet/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./EnterTimesheetModal"; diff --git a/src/components/InvoiceSearch/InvoiceSearch.tsx b/src/components/InvoiceSearch/InvoiceSearch.tsx index 74fba52..614f1cd 100644 --- a/src/components/InvoiceSearch/InvoiceSearch.tsx +++ b/src/components/InvoiceSearch/InvoiceSearch.tsx @@ -100,7 +100,7 @@ const InvoiceSearch: React.FC = ({ issuedInvoice, receivedInvoice, invoic formData.append('multipartFileList', file); const response = await importIssuedInovice(formData); - console.log(response); + // response: status, message, projectList, emptyRowList, invoiceList if (response.status) { successDialog(t("Import Success"), t).then(() => { diff --git a/src/components/LeaveModal/LeaveCalendar.tsx b/src/components/LeaveModal/LeaveCalendar.tsx new file mode 100644 index 0000000..447f4a7 --- /dev/null +++ b/src/components/LeaveModal/LeaveCalendar.tsx @@ -0,0 +1,285 @@ +import React, { useCallback, useMemo, useState } from "react"; + +import { HolidaysResult } from "@/app/api/holidays"; +import { LeaveType } from "@/app/api/timesheets"; +import dayGridPlugin from "@fullcalendar/daygrid"; +import interactionPlugin from "@fullcalendar/interaction"; +import { Box, useTheme } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { + getHolidayForDate, + getPublicHolidaysForNYears, +} from "@/app/utils/holidayUtils"; +import { + INPUT_DATE_FORMAT, + convertDateArrayToString, +} from "@/app/utils/formatUtil"; +import StyledFullCalendar from "../StyledFullCalendar"; +import { ProjectWithTasks } from "@/app/api/projects"; +import { + LeaveEntry, + RecordLeaveInput, + RecordTimesheetInput, + saveLeave, +} from "@/app/api/timesheets/actions"; +import { Props as LeaveEditModalProps } from "../LeaveTable/LeaveEditModal"; +import LeaveEditModal from "../LeaveTable/LeaveEditModal"; +import dayjs from "dayjs"; +import { checkTotalHours } from "@/app/api/timesheets/utils"; +import unionBy from "lodash/unionBy"; + +export interface Props { + leaveTypes: LeaveType[]; + companyHolidays: HolidaysResult[]; + allProjects: ProjectWithTasks[]; + leaveRecords: RecordLeaveInput; + timesheetRecords: RecordTimesheetInput; + isFullTime: boolean; +} + +interface EventClickArg { + event: { + start: Date | null; + startStr: string; + extendedProps: { + calendar?: string; + entry?: LeaveEntry; + }; + }; +} + +const LeaveCalendar: React.FC = ({ + companyHolidays, + allProjects, + leaveTypes, + timesheetRecords, + leaveRecords, + isFullTime, +}) => { + const { t } = useTranslation(["home", "common"]); + + const theme = useTheme(); + + const projectMap = useMemo(() => { + return allProjects.reduce<{ + [id: ProjectWithTasks["id"]]: ProjectWithTasks; + }>((acc, project) => { + return { ...acc, [project.id]: project }; + }, {}); + }, [allProjects]); + + const leaveMap = useMemo(() => { + return leaveTypes.reduce<{ [id: LeaveType["id"]]: string }>( + (acc, leaveType) => ({ ...acc, [leaveType.id]: leaveType.name }), + {}, + ); + }, [leaveTypes]); + + const [localLeaveRecords, setLocalLeaveEntries] = useState(leaveRecords); + + // leave edit modal related + const [leaveEditModalProps, setLeaveEditModalProps] = useState< + Partial + >({}); + const [leaveEditModalOpen, setLeaveEditModalOpen] = useState(false); + + const openLeaveEditModal = useCallback( + (defaultValues?: LeaveEntry, recordDate?: string, isHoliday?: boolean) => { + setLeaveEditModalProps({ + defaultValues: defaultValues ? { ...defaultValues } : undefined, + recordDate, + isHoliday, + onDelete: defaultValues + ? async () => { + if (!recordDate || !leaveRecords[recordDate]) { + return; + } + const leaveEntriesAtDate = leaveRecords[recordDate]; + const newLeaveRecords = { + ...leaveRecords, + [recordDate!]: leaveEntriesAtDate.filter( + (e) => e.id !== defaultValues.id, + ), + }; + const savedLeaveRecords = await saveLeave(newLeaveRecords); + setLocalLeaveEntries(savedLeaveRecords); + setLeaveEditModalOpen(false); + } + : undefined, + }); + setLeaveEditModalOpen(true); + }, + [leaveRecords], + ); + + const closeLeaveEditModal = useCallback(() => { + setLeaveEditModalOpen(false); + }, []); + + // calendar related + const holidays = useMemo(() => { + return [ + ...getPublicHolidaysForNYears(2), + ...companyHolidays.map((h) => ({ + title: h.name, + date: convertDateArrayToString(h.date, INPUT_DATE_FORMAT), + extendedProps: { + calender: "holiday", + }, + })), + ].map((e) => ({ + ...e, + backgroundColor: theme.palette.error.main, + borderColor: theme.palette.error.main, + })); + }, [companyHolidays, theme.palette.error.main]); + + const leaveEntries = useMemo( + () => + Object.keys(localLeaveRecords).flatMap((date, index) => { + return localLeaveRecords[date].map((entry) => ({ + id: `${date}-${index}-leave-${entry.id}`, + date, + title: `${t("{{count}} hour", { + ns: "common", + count: entry.inputHours || 0, + })} (${leaveMap[entry.leaveTypeId]})`, + backgroundColor: theme.palette.warning.light, + borderColor: theme.palette.warning.light, + textColor: theme.palette.text.primary, + extendedProps: { + calendar: "leaveEntry", + entry, + }, + })); + }), + [leaveMap, localLeaveRecords, t, theme], + ); + + const timeEntries = useMemo( + () => + Object.keys(timesheetRecords).flatMap((date, index) => { + return timesheetRecords[date].map((entry) => ({ + id: `${date}-${index}-time-${entry.id}`, + date, + title: `${t("{{count}} hour", { + ns: "common", + count: (entry.inputHours || 0) + (entry.otHours || 0), + })} (${ + entry.projectId + ? projectMap[entry.projectId].code + : t("Non-billable task") + })`, + backgroundColor: theme.palette.info.main, + borderColor: theme.palette.info.main, + extendedProps: { + calendar: "timeEntry", + entry, + }, + })); + }), + [projectMap, timesheetRecords, t, theme], + ); + + const handleEventClick = useCallback( + ({ event }: EventClickArg) => { + const dayJsObj = dayjs(event.startStr); + const holiday = getHolidayForDate(event.startStr, companyHolidays); + const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; + + if ( + event.extendedProps.calendar === "leaveEntry" && + event.extendedProps.entry + ) { + openLeaveEditModal( + event.extendedProps.entry as LeaveEntry, + event.startStr, + Boolean(isHoliday), + ); + } + }, + [companyHolidays, openLeaveEditModal], + ); + + const handleDateClick = useCallback( + (e: { dateStr: string; dayEl: HTMLElement }) => { + const dayJsObj = dayjs(e.dateStr); + const holiday = getHolidayForDate(e.dateStr, companyHolidays); + const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; + + openLeaveEditModal(undefined, e.dateStr, Boolean(isHoliday)); + }, + [companyHolidays, openLeaveEditModal], + ); + + const checkTotalHoursForDate = useCallback( + (newEntry: LeaveEntry, date?: string) => { + if (!date) { + throw Error("Invalid date"); + } + const dayJsObj = dayjs(date); + const holiday = getHolidayForDate(date, companyHolidays); + const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; + + const leaves = localLeaveRecords[date] || []; + const timesheets = timesheetRecords[date] || []; + + const leavesWithNewEntry = unionBy( + [newEntry as LeaveEntry], + leaves, + "id", + ); + + const totalHourError = checkTotalHours( + timesheets, + leavesWithNewEntry, + Boolean(isHoliday), + isFullTime, + ); + + if (totalHourError) throw Error(totalHourError); + }, + [companyHolidays, isFullTime, localLeaveRecords, timesheetRecords], + ); + + const handleSaveLeave = useCallback( + async (leaveEntry: LeaveEntry, recordDate?: string) => { + checkTotalHoursForDate(leaveEntry, recordDate); + const leaveEntriesAtDate = leaveRecords[recordDate!] || []; + const newLeaveRecords = { + ...leaveRecords, + [recordDate!]: [ + ...leaveEntriesAtDate.filter((e) => e.id !== leaveEntry.id), + leaveEntry, + ], + }; + const savedLeaveRecords = await saveLeave(newLeaveRecords); + setLocalLeaveEntries(savedLeaveRecords); + setLeaveEditModalOpen(false); + }, + [checkTotalHoursForDate, leaveRecords], + ); + + return ( + + + + + ); +}; + +export default LeaveCalendar; diff --git a/src/components/LeaveModal/LeaveModal.tsx b/src/components/LeaveModal/LeaveModal.tsx index 531b1aa..3739bd7 100644 --- a/src/components/LeaveModal/LeaveModal.tsx +++ b/src/components/LeaveModal/LeaveModal.tsx @@ -1,46 +1,16 @@ -import React, { useCallback, useEffect, useMemo } from "react"; +import useIsMobile from "@/app/utils/useIsMobile"; +import React from "react"; +import FullscreenModal from "../FullscreenModal"; import { Box, - Button, Card, - CardActions, CardContent, Modal, - ModalProps, SxProps, Typography, } from "@mui/material"; import { useTranslation } from "react-i18next"; -import { Check, Close } from "@mui/icons-material"; -import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; -import { - RecordLeaveInput, - RecordTimesheetInput, - saveLeave, -} from "@/app/api/timesheets/actions"; -import dayjs from "dayjs"; -import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; -import LeaveTable from "../LeaveTable"; -import { LeaveType } from "@/app/api/timesheets"; -import FullscreenModal from "../FullscreenModal"; -import MobileLeaveTable from "../LeaveTable/MobileLeaveTable"; -import useIsMobile from "@/app/utils/useIsMobile"; -import { HolidaysResult } from "@/app/api/holidays"; -import { - DAILY_NORMAL_MAX_HOURS, - TIMESHEET_DAILY_MAX_HOURS, - validateLeaveRecord, -} from "@/app/api/timesheets/utils"; -import ErrorAlert from "../ErrorAlert"; - -interface Props { - isOpen: boolean; - onClose: () => void; - defaultLeaveRecords?: RecordLeaveInput; - leaveTypes: LeaveType[]; - timesheetRecords: RecordTimesheetInput; - companyHolidays: HolidaysResult[]; -} +import LeaveCalendar, { Props as LeaveCalendarProps } from "./LeaveCalendar"; const modalSx: SxProps = { position: "absolute", @@ -52,167 +22,58 @@ const modalSx: SxProps = { maxWidth: 1400, }; +interface Props extends LeaveCalendarProps { + open: boolean; + onClose: () => void; +} + const LeaveModal: React.FC = ({ - isOpen, + open, onClose, - defaultLeaveRecords, - timesheetRecords, leaveTypes, companyHolidays, + allProjects, + leaveRecords, + timesheetRecords, + isFullTime, }) => { const { t } = useTranslation("home"); + const isMobile = useIsMobile(); - const defaultValues = useMemo(() => { - const today = dayjs(); - return Array(7) - .fill(undefined) - .reduce((acc, _, index) => { - const date = today.subtract(index, "day").format(INPUT_DATE_FORMAT); - return { - ...acc, - [date]: defaultLeaveRecords?.[date] ?? [], - }; - }, {}); - }, [defaultLeaveRecords]); - - const formProps = useForm({ defaultValues }); - useEffect(() => { - formProps.reset(defaultValues); - }, [defaultValues, formProps]); - - const onSubmit = useCallback>( - async (data) => { - const errors = validateLeaveRecord( - data, - timesheetRecords, - companyHolidays, - ); - if (errors) { - Object.keys(errors).forEach((date) => - formProps.setError(date, { - message: errors[date], - }), - ); - return; - } - const savedRecords = await saveLeave(data); - - const today = dayjs(); - const newFormValues = Array(7) - .fill(undefined) - .reduce((acc, _, index) => { - const date = today.subtract(index, "day").format(INPUT_DATE_FORMAT); - return { - ...acc, - [date]: savedRecords[date] ?? [], - }; - }, {}); - - formProps.reset(newFormValues); - onClose(); - }, - [companyHolidays, formProps, onClose, timesheetRecords], - ); - - const onCancel = useCallback(() => { - formProps.reset(defaultValues); - onClose(); - }, [defaultValues, formProps, onClose]); - - const onModalClose = useCallback>( - (_, reason) => { - if (reason !== "backdropClick") { - onCancel(); - } - }, - [onCancel], - ); - - const errorComponent = ( - { - const error = formProps.formState.errors[date]?.message; - return error - ? `${date}: ${t(error, { - TIMESHEET_DAILY_MAX_HOURS, - DAILY_NORMAL_MAX_HOURS, - })}` - : undefined; - })} + const title = t("Record leave"); + const content = ( + ); - const matches = useIsMobile(); - - return ( - - {!matches ? ( - // Desktop version - - - - - {t("Record Leave")} - - - - - {errorComponent} - - - - - - - - ) : ( - // Mobile version - - - - {t("Record Leave")} - - + return isMobile ? ( + + + + {title} + + {content} + + + ) : ( + + + + + {title} + + + {content} - - )} - + + + ); }; diff --git a/src/components/Logo/Logo.tsx b/src/components/Logo/Logo.tsx index cc00180..53f65b6 100644 --- a/src/components/Logo/Logo.tsx +++ b/src/components/Logo/Logo.tsx @@ -1,5 +1,3 @@ -import { Grid } from "@mui/material"; - interface Props { width?: number; height?: number; @@ -16,7 +14,7 @@ const Logo: React.FC = ({ width, height }) => {