From b4f077da34e8b4551a3668900fcceaaa3aecc8e6 Mon Sep 17 00:00:00 2001 From: Wayne Date: Sun, 26 May 2024 00:16:57 +0900 Subject: [PATCH 1/6] Fast time entry --- src/app/api/timesheets/utils.ts | 18 +- src/app/utils/manhourUtils.ts | 16 + .../TimesheetModal/TimesheetModal.tsx | 17 +- .../TimesheetTable/EntryInputTable.tsx | 130 +++++--- .../TimesheetTable/FastTimeEntryModal.tsx | 309 ++++++++++++++++++ .../TimesheetTable/MobileTimesheetEntry.tsx | 55 +++- .../TimesheetTable/MobileTimesheetTable.tsx | 9 +- .../TimesheetTable/ProjectSelect.tsx | 94 +++++- .../TimesheetTable/TimesheetEditModal.tsx | 5 + .../TimesheetTable/TimesheetTable.tsx | 4 +- .../UserWorkspacePage/UserWorkspacePage.tsx | 3 + .../UserWorkspaceWrapper.tsx | 2 + 12 files changed, 582 insertions(+), 80 deletions(-) create mode 100644 src/components/TimesheetTable/FastTimeEntryModal.tsx diff --git a/src/app/api/timesheets/utils.ts b/src/app/api/timesheets/utils.ts index 7a6dad3..81b543d 100644 --- a/src/app/api/timesheets/utils.ts +++ b/src/app/api/timesheets/utils.ts @@ -13,6 +13,10 @@ export type TimeEntryError = { [field in keyof TimeEntry]?: string; }; +interface TimeEntryValidationOptions { + skipTaskValidation?: boolean; +} + /** * @param entry - the time entry * @returns an object where the keys are the error fields and the values the error message, and undefined if there are no errors @@ -20,6 +24,7 @@ export type TimeEntryError = { export const validateTimeEntry = ( entry: Partial, isHoliday: boolean, + options: TimeEntryValidationOptions = {}, ): TimeEntryError | undefined => { // Test for errors const error: TimeEntryError = {}; @@ -41,10 +46,12 @@ export const validateTimeEntry = ( // If there is a project id, there should also be taskGroupId, taskId, inputHours if (entry.projectId) { - if (!entry.taskGroupId) { - error.taskGroupId = "Required"; - } else if (!entry.taskId) { - error.taskId = "Required"; + if (!options.skipTaskValidation) { + if (!entry.taskGroupId) { + error.taskGroupId = "Required"; + } else if (!entry.taskId) { + error.taskId = "Required"; + } } } else { if (!entry.remark) { @@ -71,6 +78,7 @@ export const validateTimesheet = ( timesheet: RecordTimesheetInput, leaveRecords: RecordLeaveInput, companyHolidays: HolidaysResult[], + options: TimeEntryValidationOptions = {}, ): { [date: string]: string } | undefined => { const errors: { [date: string]: string } = {}; @@ -86,7 +94,7 @@ export const validateTimesheet = ( // Check each entry for (const entry of timeEntries) { - const entryErrors = validateTimeEntry(entry, holidays.has(date)); + const entryErrors = validateTimeEntry(entry, holidays.has(date), options); if (entryErrors) { errors[date] = "There are errors in the entries"; diff --git a/src/app/utils/manhourUtils.ts b/src/app/utils/manhourUtils.ts index 37914ba..a1f681c 100644 --- a/src/app/utils/manhourUtils.ts +++ b/src/app/utils/manhourUtils.ts @@ -1,3 +1,19 @@ +import zipWith from "lodash/zipWith"; + export const roundToNearestQuarter = (n: number): number => { return Math.round(n / 0.25) * 0.25; }; + +export const distributeQuarters = (hours: number, parts: number): number[] => { + if (!parts) return []; + + const numQuarters = hours * 4; + const equalParts = Math.floor(numQuarters / parts); + const remainders = Array(numQuarters % parts).fill(1); + + return zipWith( + Array(parts).fill(equalParts), + remainders, + (a, b) => a + (b || 0), + ).map((quarters) => quarters / 4); +}; diff --git a/src/components/TimesheetModal/TimesheetModal.tsx b/src/components/TimesheetModal/TimesheetModal.tsx index 66ccd76..439f027 100644 --- a/src/components/TimesheetModal/TimesheetModal.tsx +++ b/src/components/TimesheetModal/TimesheetModal.tsx @@ -42,6 +42,7 @@ interface Props { defaultTimesheets?: RecordTimesheetInput; leaveRecords: RecordLeaveInput; companyHolidays: HolidaysResult[]; + fastEntryEnabled?: boolean; } const modalSx: SxProps = { @@ -63,6 +64,7 @@ const TimesheetModal: React.FC = ({ defaultTimesheets, leaveRecords, companyHolidays, + fastEntryEnabled, }) => { const { t } = useTranslation("home"); @@ -83,7 +85,9 @@ const TimesheetModal: React.FC = ({ const onSubmit = useCallback>( async (data) => { - const errors = validateTimesheet(data, leaveRecords, companyHolidays); + const errors = validateTimesheet(data, leaveRecords, companyHolidays, { + skipTaskValidation: fastEntryEnabled, + }); if (errors) { Object.keys(errors).forEach((date) => formProps.setError(date, { @@ -108,7 +112,14 @@ const TimesheetModal: React.FC = ({ formProps.reset(newFormValues); onClose(); }, - [companyHolidays, formProps, leaveRecords, onClose, username], + [ + companyHolidays, + fastEntryEnabled, + formProps, + leaveRecords, + onClose, + username, + ], ); const onCancel = useCallback(() => { @@ -165,6 +176,7 @@ const TimesheetModal: React.FC = ({ assignedProjects={assignedProjects} allProjects={allProjects} leaveRecords={leaveRecords} + fastEntryEnabled={fastEntryEnabled} /> {errorComponent} @@ -202,6 +214,7 @@ const TimesheetModal: React.FC = ({ {t("Timesheet Input")} = ({ allProjects, assignedProjects, isHoliday, + fastEntryEnabled, }) => { const { t } = useTranslation("home"); const taskGroupsByProject = useMemo(() => { @@ -114,7 +117,9 @@ const EntryInputTable: React.FC = ({ "", ) as TimeEntryRow; - const error = validateTimeEntry(row, isHoliday); + const error = validateTimeEntry(row, isHoliday, { + skipTaskValidation: fastEntryEnabled, + }); // Test for warnings let isPlanned; @@ -133,7 +138,7 @@ const EntryInputTable: React.FC = ({ apiRef.current.updateRows([{ id, _error: error, isPlanned }]); return !error; }, - [apiRef, day, isHoliday, milestonesByProject], + [apiRef, day, fastEntryEnabled, isHoliday, milestonesByProject], ); const handleCancel = useCallback( @@ -230,6 +235,7 @@ const EntryInputTable: React.FC = ({ renderEditCell(params: GridRenderEditCellParams) { return ( = ({ (entry) => entry.isPlanned !== undefined && !entry.isPlanned, ); + // Fast entry modal + const [fastEntryModalOpen, setFastEntryModalOpen] = useState(false); + const closeFastEntryModal = useCallback(() => { + setFastEntryModalOpen(false); + }, []); + const openFastEntryModal = useCallback(() => { + setFastEntryModalOpen(true); + }, []); + const onSaveFastEntry = useCallback(async (entries: TimeEntry[]) => { + setEntries((e) => [...e, ...entries]); + setFastEntryModalOpen(false); + }, []); + const footer = ( + {hasOutOfPlannedStages && ( {t("There are entries for stages out of planned dates!")} @@ -426,49 +454,61 @@ const EntryInputTable: React.FC = ({ ); return ( - ) => { - let classname = ""; - if (params.row._error?.[params.field as keyof TimeEntry]) { - classname = "hasError"; - } else if ( - params.field === "taskGroupId" && - params.row.isPlanned !== undefined && - !params.row.isPlanned - ) { - classname = "hasWarning"; - } - return classname; - }} - slots={{ - footer: FooterToolbar, - noRowsOverlay: NoRowsOverlay, - }} - slotProps={{ - footer: { child: footer }, - }} - /> + <> + ) => { + let classname = ""; + if (params.row._error?.[params.field as keyof TimeEntry]) { + classname = "hasError"; + } else if ( + params.field === "taskGroupId" && + params.row.isPlanned !== undefined && + !params.row.isPlanned + ) { + classname = "hasWarning"; + } + return classname; + }} + slots={{ + footer: FooterToolbar, + noRowsOverlay: NoRowsOverlay, + }} + slotProps={{ + footer: { child: footer }, + }} + /> + {fastEntryEnabled && ( + + )} + ); }; diff --git a/src/components/TimesheetTable/FastTimeEntryModal.tsx b/src/components/TimesheetTable/FastTimeEntryModal.tsx new file mode 100644 index 0000000..37ec8b6 --- /dev/null +++ b/src/components/TimesheetTable/FastTimeEntryModal.tsx @@ -0,0 +1,309 @@ +import { TimeEntry } from "@/app/api/timesheets/actions"; +import { Check, ExpandMore } from "@mui/icons-material"; +import { + Accordion, + AccordionDetails, + AccordionSummary, + Alert, + Box, + Button, + FormControl, + FormHelperText, + InputLabel, + Modal, + ModalProps, + Paper, + SxProps, + TextField, + Typography, +} from "@mui/material"; +import React, { useCallback, useMemo } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import ProjectSelect from "./ProjectSelect"; +import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; +import { + distributeQuarters, + roundToNearestQuarter, +} from "@/app/utils/manhourUtils"; +import { manhourFormatter, shortDateFormatter } from "@/app/utils/formatUtil"; +import { DAILY_NORMAL_MAX_HOURS } from "@/app/api/timesheets/utils"; +import zip from "lodash/zip"; + +export interface FastTimeEntryForm { + projectIds: TimeEntry["projectId"][]; + inputHours: TimeEntry["inputHours"]; + otHours: TimeEntry["otHours"]; + remark: TimeEntry["remark"]; +} + +export interface Props extends Omit { + onSave: (timeEntries: TimeEntry[], recordDate?: string) => Promise; + onDelete?: () => void; + allProjects: ProjectWithTasks[]; + assignedProjects: AssignedProject[]; + modalSx?: SxProps; + recordDate?: string; + isHoliday?: boolean; +} + +const modalSx: SxProps = { + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + width: "90%", + maxWidth: "sm", + maxHeight: "90%", + padding: 3, + display: "flex", + flexDirection: "column", + gap: 2, +}; + +let idOffset = Date.now(); +const getID = () => { + return ++idOffset; +}; + +const FastTimeEntryModal: React.FC = ({ + onSave, + open, + onClose, + allProjects, + assignedProjects, + modalSx: mSx, + recordDate, + isHoliday, +}) => { + const { + t, + i18n: { language }, + } = useTranslation("home"); + + const { register, control, reset, trigger, formState, watch } = + useForm({ + defaultValues: { + projectIds: [], + }, + }); + + const projectIds = watch("projectIds"); + const inputHours = watch("inputHours"); + const otHours = watch("otHours"); + const remark = watch("remark"); + + const selectedProjects = useMemo(() => { + return projectIds.map((id) => allProjects.find((p) => p.id === id)); + }, [allProjects, projectIds]); + + const normalHoursArray = distributeQuarters( + inputHours || 0, + selectedProjects.length, + ); + const otHoursArray = distributeQuarters( + otHours || 0, + selectedProjects.length, + ); + + const projectsWithHours = zip( + selectedProjects, + normalHoursArray, + otHoursArray, + ); + + const saveHandler = useCallback(async () => { + const valid = await trigger(); + if (valid) { + onSave( + projectsWithHours.map(([project, hour, othour]) => ({ + id: getID(), + projectId: project?.id, + inputHours: hour, + otHours: othour, + remark, + })), + recordDate, + ); + reset(); + } + }, [projectsWithHours, trigger, onSave, recordDate, reset, remark]); + + const closeHandler = useCallback>( + (...args) => { + onClose?.(...args); + reset(); + }, + [onClose, reset], + ); + + return ( + + + {recordDate && ( + + {shortDateFormatter(language).format(new Date(recordDate))} + + )} + + {t("Project Code and Name")} + ( + { + field.onChange( + newIds.map((id) => (id === "" ? undefined : id)), + ); + }} + /> + )} + rules={{ + validate: (value) => + value.length > 0 || t("Please choose at least 1 project."), + }} + /> + + {formState.errors.projectIds?.message || + t( + "The inputted time will be evenly distributed among the selected projects.", + )} + + + roundToNearestQuarter(parseFloat(value)), + validate: (value) => { + if (value) { + if (isHoliday) { + return t("Cannot input normal hours for holidays"); + } + + return ( + (0 < value && value <= DAILY_NORMAL_MAX_HOURS) || + t( + "Input hours should be between 0 and {{DAILY_NORMAL_MAX_HOURS}}", + { DAILY_NORMAL_MAX_HOURS }, + ) + ); + } else { + return Boolean(value || otHours) || t("Required"); + } + }, + })} + error={Boolean(formState.errors.inputHours)} + helperText={formState.errors.inputHours?.message} + /> + roundToNearestQuarter(parseFloat(value)), + validate: (value) => (value ? value > 0 : true), + })} + error={Boolean(formState.errors.otHours)} + /> + + projectIds.every((id) => id) || + value || + t("Required for non-billable tasks"), + })} + helperText={ + formState.errors.remark?.message || + t("The remark will be added to all selected projects") + } + /> + + }> + + {t("Hour distribution preview")} + + + + {projectIds.length > 0 ? ( + + ) : ( + + {t("Please select some projects.")} + + )} + + + + + + + + ); +}; + +const ProjectHourSummary: React.FC<{ + projectsWithHours: [ProjectWithTasks?, number?, number?][]; +}> = ({ projectsWithHours }) => { + const { t } = useTranslation("home"); + + return ( + + {projectsWithHours.map(([project, manhour, otManhour], index) => { + return ( + + + {project + ? `${project.code} - ${project.name}` + : t("Non-billable Task")} + + + + + {t("Hours")} + + + {manhourFormatter.format(manhour || 0)} + + + + + {t("Other Hours")} + + + {manhourFormatter.format(otManhour || 0)} + + + + + ); + })} + + ); +}; + +export default FastTimeEntryModal; diff --git a/src/components/TimesheetTable/MobileTimesheetEntry.tsx b/src/components/TimesheetTable/MobileTimesheetEntry.tsx index a5eab75..eeefbde 100644 --- a/src/components/TimesheetTable/MobileTimesheetEntry.tsx +++ b/src/components/TimesheetTable/MobileTimesheetEntry.tsx @@ -1,14 +1,7 @@ import { TimeEntry, RecordTimesheetInput } from "@/app/api/timesheets/actions"; -import { manhourFormatter, shortDateFormatter } from "@/app/utils/formatUtil"; -import { Add, Edit } from "@mui/icons-material"; -import { - Box, - Button, - Card, - CardContent, - IconButton, - Typography, -} from "@mui/material"; +import { shortDateFormatter } from "@/app/utils/formatUtil"; +import { Add } from "@mui/icons-material"; +import { Box, Button, Stack, Typography } from "@mui/material"; import dayjs from "dayjs"; import React, { useCallback, useMemo, useState } from "react"; import { useFormContext } from "react-hook-form"; @@ -20,12 +13,14 @@ import TimesheetEditModal, { import TimeEntryCard from "./TimeEntryCard"; import { HolidaysResult } from "@/app/api/holidays"; import { getHolidayForDate } from "@/app/utils/holidayUtils"; +import FastTimeEntryModal from "./FastTimeEntryModal"; interface Props { date: string; allProjects: ProjectWithTasks[]; assignedProjects: AssignedProject[]; companyHolidays: HolidaysResult[]; + fastEntryEnabled?: boolean; } const MobileTimesheetEntry: React.FC = ({ @@ -33,6 +28,7 @@ const MobileTimesheetEntry: React.FC = ({ allProjects, assignedProjects, companyHolidays, + fastEntryEnabled, }) => { const { t, @@ -51,7 +47,8 @@ const MobileTimesheetEntry: React.FC = ({ const holiday = getHolidayForDate(date, companyHolidays); const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; - const { watch, setValue, clearErrors } = useFormContext(); + const { watch, setValue, clearErrors } = + useFormContext(); const currentEntries = watch(date); // Edit modal @@ -103,6 +100,22 @@ const MobileTimesheetEntry: React.FC = ({ [clearErrors, currentEntries, date, setValue], ); + // Fast entry modal + const [fastEntryModalOpen, setFastEntryModalOpen] = useState(false); + const closeFastEntryModal = useCallback(() => { + setFastEntryModalOpen(false); + }, []); + const openFastEntryModal = useCallback(() => { + setFastEntryModalOpen(true); + }, []); + const onSaveFastEntry = useCallback( + async (entries: TimeEntry[]) => { + setValue(date, [...currentEntries, ...entries]); + setFastEntryModalOpen(false); + }, + [currentEntries, date, setValue], + ); + return ( <> = ({ {t("Add some time entries!")} )} - + - + {fastEntryEnabled && ( + + )} + = ({ onClose={closeEditModal} onSave={onSaveEntry} isHoliday={Boolean(isHoliday)} + fastEntryEnabled={fastEntryEnabled} {...editModalProps} /> + {fastEntryEnabled && ( + + )} ); diff --git a/src/components/TimesheetTable/MobileTimesheetTable.tsx b/src/components/TimesheetTable/MobileTimesheetTable.tsx index f09306f..bab4351 100644 --- a/src/components/TimesheetTable/MobileTimesheetTable.tsx +++ b/src/components/TimesheetTable/MobileTimesheetTable.tsx @@ -15,6 +15,7 @@ interface Props { leaveRecords: RecordLeaveInput; companyHolidays: HolidaysResult[]; errorComponent?: React.ReactNode; + fastEntryEnabled?: boolean; } const MobileTimesheetTable: React.FC = ({ @@ -23,6 +24,7 @@ const MobileTimesheetTable: React.FC = ({ leaveRecords, companyHolidays, errorComponent, + fastEntryEnabled, }) => { const { watch } = useFormContext(); const currentInput = watch(); @@ -35,7 +37,12 @@ const MobileTimesheetTable: React.FC = ({ leaveEntries={leaveRecords} timesheetEntries={currentInput} EntryComponent={MobileTimesheetEntry} - entryComponentProps={{ allProjects, assignedProjects, companyHolidays }} + entryComponentProps={{ + allProjects, + assignedProjects, + companyHolidays, + fastEntryEnabled, + }} errorComponent={errorComponent} /> ); diff --git a/src/components/TimesheetTable/ProjectSelect.tsx b/src/components/TimesheetTable/ProjectSelect.tsx index c79e89e..a90b120 100644 --- a/src/components/TimesheetTable/ProjectSelect.tsx +++ b/src/components/TimesheetTable/ProjectSelect.tsx @@ -1,6 +1,8 @@ import React, { useCallback, useMemo } from "react"; import { Autocomplete, + Checkbox, + Chip, ListSubheader, MenuItem, TextField, @@ -8,15 +10,30 @@ import { import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; import { useTranslation } from "react-i18next"; import differenceBy from "lodash/differenceBy"; +import intersectionWith from "lodash/intersectionWith"; import { TFunction } from "i18next"; -interface Props { +interface CommonProps { allProjects: ProjectWithTasks[]; assignedProjects: AssignedProject[]; + error?: boolean; + multiple?: boolean; +} + +interface SingleAutocompleteProps extends CommonProps { value: number | undefined; onProjectSelect: (projectId: number | string) => void; + multiple: false; } +interface MultiAutocompleteProps extends CommonProps { + value: (number | undefined)[]; + onProjectSelect: (projectIds: Array) => void; + multiple: true; +} + +type Props = SingleAutocompleteProps | MultiAutocompleteProps; + const getGroupName = (t: TFunction, groupName: string): string => { switch (groupName) { case "non-billable": @@ -37,6 +54,8 @@ const AutocompleteProjectSelect: React.FC = ({ assignedProjects, value, onProjectSelect, + error, + multiple, }) => { const { t } = useTranslation("home"); const nonAssignedProjects = useMemo(() => { @@ -63,17 +82,32 @@ const AutocompleteProjectSelect: React.FC = ({ ]; }, [assignedProjects, nonAssignedProjects, t]); - const currentValue = options.find((o) => o.value === value) || options[0]; + const currentValue = multiple + ? intersectionWith(options, value, (option, v) => { + return option.value === (v ?? ""); + }) + : options.find((o) => o.value === value) || options[0]; + // const currentValue = options.find((o) => o.value === value) || options[0]; const onChange = useCallback( - (event: React.SyntheticEvent, newValue: { value: number | string }) => { - onProjectSelect(newValue.value); + ( + event: React.SyntheticEvent, + newValue: { value: number | string } | { value: number | string }[], + ) => { + if (multiple) { + const multiNewValue = newValue as { value: number | string }[]; + onProjectSelect(multiNewValue.map(({ value }) => value)); + } else { + const singleNewVal = newValue as { value: number | string }; + onProjectSelect(singleNewVal.value); + } }, - [onProjectSelect], + [onProjectSelect, multiple], ); return ( = ({ groupBy={(option) => option.group} getOptionLabel={(option) => option.label} options={options} + disableCloseOnSelect={multiple} + renderTags={ + multiple + ? (value, getTagProps) => + value.map((option, index) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { key, ...chipProps } = getTagProps({ index }); + return ( + + ); + }) + : undefined + } renderGroup={(params) => ( - <> - - {getGroupName(t, params.group)} - + + {getGroupName(t, params.group)} {params.children} - + )} - renderOption={(params, option) => { + renderOption={( + params: React.HTMLAttributes & { key?: React.Key }, + option, + { selected }, + ) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { key, ...rest } = params; return ( - + + {multiple && ( + + )} {option.label} ); }} - renderInput={(params) => } + renderInput={(params) => } /> ); }; diff --git a/src/components/TimesheetTable/TimesheetEditModal.tsx b/src/components/TimesheetTable/TimesheetEditModal.tsx index 6fe33d5..3319bf4 100644 --- a/src/components/TimesheetTable/TimesheetEditModal.tsx +++ b/src/components/TimesheetTable/TimesheetEditModal.tsx @@ -34,6 +34,7 @@ export interface Props extends Omit { modalSx?: SxProps; recordDate?: string; isHoliday?: boolean; + fastEntryEnabled?: boolean; } const modalSx: SxProps = { @@ -59,6 +60,7 @@ const TimesheetEditModal: React.FC = ({ modalSx: mSx, recordDate, isHoliday, + fastEntryEnabled, }) => { const { t, @@ -135,6 +137,7 @@ const TimesheetEditModal: React.FC = ({ name="projectId" render={({ field }) => ( = ({ if (!projectId) { return !id; } + if (fastEntryEnabled) return true; const taskGroups = taskGroupsByProject[projectId]; return taskGroups.some((tg) => tg.value === id); }, @@ -202,6 +206,7 @@ const TimesheetEditModal: React.FC = ({ if (!projectId) { return !id; } + if (fastEntryEnabled) return true; const projectTasks = allProjects.find((p) => p.id === projectId) ?.tasks; return Boolean(projectTasks?.some((task) => task.id === id)); diff --git a/src/components/TimesheetTable/TimesheetTable.tsx b/src/components/TimesheetTable/TimesheetTable.tsx index 3b4bfff..02f91df 100644 --- a/src/components/TimesheetTable/TimesheetTable.tsx +++ b/src/components/TimesheetTable/TimesheetTable.tsx @@ -14,6 +14,7 @@ interface Props { assignedProjects: AssignedProject[]; leaveRecords: RecordLeaveInput; companyHolidays: HolidaysResult[]; + fastEntryEnabled?: boolean; } const TimesheetTable: React.FC = ({ @@ -21,6 +22,7 @@ const TimesheetTable: React.FC = ({ assignedProjects, leaveRecords, companyHolidays, + fastEntryEnabled, }) => { const { watch } = useFormContext(); const currentInput = watch(); @@ -33,7 +35,7 @@ const TimesheetTable: React.FC = ({ leaveEntries={leaveRecords} timesheetEntries={currentInput} EntryTableComponent={EntryInputTable} - entryTableProps={{ assignedProjects, allProjects }} + entryTableProps={{ assignedProjects, allProjects, fastEntryEnabled }} /> ); }; diff --git a/src/components/UserWorkspacePage/UserWorkspacePage.tsx b/src/components/UserWorkspacePage/UserWorkspacePage.tsx index 7bbf537..ff1030c 100644 --- a/src/components/UserWorkspacePage/UserWorkspacePage.tsx +++ b/src/components/UserWorkspacePage/UserWorkspacePage.tsx @@ -35,6 +35,7 @@ export interface Props { defaultTimesheets: RecordTimesheetInput; holidays: HolidaysResult[]; teamTimesheets: TeamTimeSheets; + fastEntryEnabled?: boolean; } const menuItemSx: SxProps = { @@ -51,6 +52,7 @@ const UserWorkspacePage: React.FC = ({ defaultTimesheets, holidays, teamTimesheets, + fastEntryEnabled, }) => { const [anchorEl, setAnchorEl] = useState(null); @@ -170,6 +172,7 @@ const UserWorkspacePage: React.FC = ({ leaveTypes={leaveTypes} /> = async ({ username }) => { defaultLeaveRecords={leaves} leaveTypes={leaveTypes} holidays={holidays} + // Change to access check + fastEntryEnabled={true} /> ); }; From 1b1e62a9ec6ce610fda2a1550879a6de5a1d2c42 Mon Sep 17 00:00:00 2001 From: "Mac\\David" Date: Sun, 26 May 2024 19:03:39 +0800 Subject: [PATCH 2/6] update dashboard --- src/app/api/cashflow/index.ts | 26 +++- .../ProjectCashFlow/ProjectCashFlow.tsx | 128 ++++++++++++++---- 2 files changed, 123 insertions(+), 31 deletions(-) diff --git a/src/app/api/cashflow/index.ts b/src/app/api/cashflow/index.ts index f71ba74..ed26b3a 100644 --- a/src/app/api/cashflow/index.ts +++ b/src/app/api/cashflow/index.ts @@ -11,12 +11,25 @@ export interface CashFlow { teamLeader: string; startDate: string; startDateFrom: string; - startDateTo: string; + startDateFromTo: string; targetEndDate: string; client: string; subsidiary: string; } +export interface CashFlowByMonthChartResult { + monthInvoice: string; + invoiceMonth: string; + income: number; + cumulativeIncome:number; + monthExpenditure:string; + recordMonth:string; + expenditure:number; + cumulativeExpenditure:number; + incomeList: any[]; + expenditureList: any[]; +} + export const preloadProjects = () => { fetchProjectsCashFlow(); }; @@ -24,3 +37,14 @@ export const preloadProjects = () => { export const fetchProjectsCashFlow = cache(async () => { return serverFetchJson(`${BASE_API_URL}/dashboard/searchCashFlowProject`); }); + +export const fetchProjectsCashFlowMonthlyChart = cache(async (projectIdList: number[], year: number) => { + if (projectIdList.length !== 0) { + const queryParams = new URLSearchParams(); + queryParams.append('projectIdList', projectIdList.join(',')); + return serverFetchJson(`${BASE_API_URL}/dashboard/searchCashFlowByMonth?${queryParams.toString()}&year=${year}`); + } else { + return []; + } + +}); diff --git a/src/components/ProjectCashFlow/ProjectCashFlow.tsx b/src/components/ProjectCashFlow/ProjectCashFlow.tsx index 4a78c62..14b40a0 100644 --- a/src/components/ProjectCashFlow/ProjectCashFlow.tsx +++ b/src/components/ProjectCashFlow/ProjectCashFlow.tsx @@ -19,9 +19,10 @@ import SearchBox, { Criterion } from "../SearchBox"; import ProgressByClientSearch from "@/components/ProgressByClientSearch"; import { Suspense } from "react"; import ProgressCashFlowSearch from "@/components/ProgressCashFlowSearch"; -import { fetchProjectsCashFlow} from "@/app/api/cashflow"; +import { fetchProjectsCashFlow,fetchProjectsCashFlowMonthlyChart} from "@/app/api/cashflow"; import { Input, Label } from "reactstrap"; import { CashFlow } from "@/app/api/cashflow"; +import dayjs from 'dayjs'; interface Props { projects: CashFlow[]; @@ -34,19 +35,78 @@ const ProjectCashFlow: React.FC = () => { const todayDate = new Date(); const [selectionModel, setSelectionModel]: any[] = React.useState([]); const [projectData, setProjectData]: any[] = React.useState([]); + const [filteredResult, setFilteredResult]:any[] = useState([]); + const [selectedProjectIdList, setSelectedProjectIdList]: any[] = React.useState([]); + const [monthlyIncomeList, setMonthlyIncomeList]: any[] = React.useState([]); + const [monthlyCumulativeIncomeList, setMonthlyCumulativeIncomeList]: any[] = React.useState([]); + const [monthlyExpenditureList, setMonthlyExpenditureList]: any[] = React.useState([]); + const [monthlyCumulativeExpenditureList, setMonthlyCumulativeExpenditureList]: any[] = React.useState([]); + const [monthlyChartLeftMax, setMonthlyChartLeftMax]: any[] = React.useState(0); + const [monthlyChartRightMax, setMonthlyChartRightMax]: any[] = React.useState(0); const [cashFlowYear, setCashFlowYear]: any[] = React.useState( todayDate.getFullYear(), ); const [anticipateCashFlowYear, setAnticipateCashFlowYear]: any[] = React.useState( todayDate.getFullYear(), ); + + const handleSelectionChange = (newSelectionModel: GridRowSelectionModel) => { + const selectedRowsData = projectData.filter((row: any) => + newSelectionModel.includes(row.id), + ); + const projectIdList = [] + for (var i=0; i { const cashFlowProject = await fetchProjectsCashFlow(); + console.log(cashFlowProject) setProjectData(cashFlowProject) + setFilteredResult(cashFlowProject) + } + const fetchChartData = async () => { + const cashFlowMonthlyChartData = await fetchProjectsCashFlowMonthlyChart(selectedProjectIdList,cashFlowYear); + console.log(cashFlowMonthlyChartData) + const monthlyIncome = [] + const cumulativeIncome = [] + const monthlyExpenditure = [] + const cumulativeExpenditure = [] + var leftMax = 0 + var rightMax = 0 + if (cashFlowMonthlyChartData.length !== 0) { + for (var i = 0; i < cashFlowMonthlyChartData[0].incomeList.length; i++) { + if (leftMax < cashFlowMonthlyChartData[0].incomeList[i].income || leftMax < cashFlowMonthlyChartData[0].expenditureList[i].expenditure){ + leftMax = Math.max(cashFlowMonthlyChartData[0].incomeList[i].income,cashFlowMonthlyChartData[0].expenditureList[i].expenditure) + } + monthlyIncome.push(cashFlowMonthlyChartData[0].incomeList[i].income) + cumulativeIncome.push(cashFlowMonthlyChartData[0].incomeList[i].cumulativeIncome) + } + for (var i = 0; i < cashFlowMonthlyChartData[0].expenditureList.length; i++) { + if (rightMax < cashFlowMonthlyChartData[0].incomeList[i].income || rightMax < cashFlowMonthlyChartData[0].expenditureList[i].expenditure){ + rightMax = Math.max(cashFlowMonthlyChartData[0].incomeList[i].income,cashFlowMonthlyChartData[0].expenditureList[i].expenditure) + } + monthlyExpenditure.push(cashFlowMonthlyChartData[0].expenditureList[i].expenditure) + cumulativeExpenditure.push(cashFlowMonthlyChartData[0].expenditureList[i].cumulativeExpenditure) + } + setMonthlyIncomeList(monthlyIncome) + setMonthlyCumulativeIncomeList(cumulativeIncome) + setMonthlyExpenditureList(monthlyExpenditure) + setMonthlyCumulativeExpenditureList(cumulativeExpenditure) + setMonthlyChartLeftMax(leftMax) + setMonthlyChartRightMax(rightMax) + } + } useEffect(() => { fetchData() }, []); + + useEffect(() => { + fetchChartData() + }, [cashFlowYear,selectedProjectIdList]); const columns = [ { id: "projectCode", @@ -170,7 +230,7 @@ const ProjectCashFlow: React.FC = () => { text: "Monthly Income and Expenditure(HKD)", }, min: 0, - max: 350000, + max: monthlyChartLeftMax, tickAmount: 5, labels: { formatter: function (val) { @@ -185,7 +245,7 @@ const ProjectCashFlow: React.FC = () => { text: "Monthly Expenditure (HKD)", }, min: 0, - max: 350000, + max: monthlyChartLeftMax, tickAmount: 5, }, { @@ -195,7 +255,7 @@ const ProjectCashFlow: React.FC = () => { text: "Cumulative Income and Expenditure(HKD)", }, min: 0, - max: 850000, + max: monthlyChartRightMax, tickAmount: 5, labels: { formatter: function (val) { @@ -211,7 +271,7 @@ const ProjectCashFlow: React.FC = () => { text: "Cumulative Expenditure (HKD)", }, min: 0, - max: 850000, + max: monthlyChartRightMax, tickAmount: 5, }, ], @@ -224,34 +284,25 @@ const ProjectCashFlow: React.FC = () => { name: "Monthly_Income", type: "column", color: "#ffde91", - data: [0, 110000, 0, 0, 185000, 0, 0, 189000, 0, 0, 300000, 0], + data: monthlyIncomeList, }, { name: "Monthly_Expenditure", type: "column", color: "#82b59a", - data: [ - 0, 160000, 120000, 120000, 55000, 55000, 55000, 55000, 55000, 70000, - 55000, 55000, - ], + data: monthlyExpenditureList, }, { name: "Cumulative_Income", type: "line", color: "#EE6D7A", - data: [ - 0, 100000, 100000, 100000, 300000, 300000, 300000, 500000, 500000, - 500000, 800000, 800000, - ], + data: monthlyCumulativeIncomeList, }, { name: "Cumulative_Expenditure", type: "line", color: "#7cd3f2", - data: [ - 0, 198000, 240000, 400000, 410000, 430000, 510000, 580000, 600000, - 710000, 730000, 790000, - ], + data: monthlyCumulativeExpenditureList, }, ], }; @@ -295,7 +346,7 @@ const ProjectCashFlow: React.FC = () => { text: "Anticipate Monthly Income and Expenditure(HKD)", }, min: 0, - max: 350000, + max: monthlyChartLeftMax, tickAmount: 5, labels: { formatter: function (val) { @@ -310,7 +361,7 @@ const ProjectCashFlow: React.FC = () => { text: "Monthly Expenditure (HKD)", }, min: 0, - max: 350000, + max: monthlyChartLeftMax, tickAmount: 5, }, { @@ -320,7 +371,7 @@ const ProjectCashFlow: React.FC = () => { text: "Cumulative Income and Expenditure(HKD)", }, min: 0, - max: 850000, + max: monthlyChartRightMax, tickAmount: 5, labels: { formatter: function (val) { @@ -336,7 +387,7 @@ const ProjectCashFlow: React.FC = () => { text: "Cumulative Expenditure (HKD)", }, min: 0, - max: 850000, + max: monthlyChartRightMax, tickAmount: 5, }, ], @@ -548,12 +599,6 @@ const ProjectCashFlow: React.FC = () => { }, ]; const [ledgerData, setLedgerData]: any[] = React.useState(ledgerRows); - const handleSelectionChange = (newSelectionModel: GridRowSelectionModel) => { - const selectedRowsData = projectData.filter((row: any) => - newSelectionModel.includes(row.id), - ); - console.log(selectedRowsData); - }; const searchCriteria: Criterion[] = useMemo( () => [ @@ -569,6 +614,19 @@ const ProjectCashFlow: React.FC = () => { [t], ); + function isDateInRange(dateToCheck: string, startDate: string, endDate: string): boolean { + console.log(startDate) + console.log(endDate) + if (!startDate || !endDate) { + return false; + } + const dateToCheckObj = new Date(dateToCheck); + const startDateObj = new Date(startDate); + const endDateObj = new Date(endDate); + console.log(dateToCheckObj) + return dateToCheckObj >= startDateObj && dateToCheckObj <= endDateObj; + } + return ( <> {/* }> @@ -577,11 +635,21 @@ const ProjectCashFlow: React.FC = () => { { - console.log(query); + console.log(query) + setFilteredResult( + projectData.filter( + (cp:any) => + cp.projectCode.toLowerCase().includes(query.projectCode.toLowerCase()) && + cp.projectName.toLowerCase().includes(query.projectName.toLowerCase()) && + (query.startDateFrom || query.startDateFromTo + ? isDateInRange(cp.startDate, query.startDateFrom, query.startDateFromTo) + : true) + ), + ); }} /> Date: Mon, 27 May 2024 00:11:09 +0800 Subject: [PATCH 3/6] dashboard call api --- src/app/api/cashflow/index.ts | 50 +++++++++++++++++ .../ProjectCashFlow/ProjectCashFlow.tsx | 55 ++++++++++++++++--- 2 files changed, 96 insertions(+), 9 deletions(-) diff --git a/src/app/api/cashflow/index.ts b/src/app/api/cashflow/index.ts index ed26b3a..4de997c 100644 --- a/src/app/api/cashflow/index.ts +++ b/src/app/api/cashflow/index.ts @@ -30,6 +30,34 @@ export interface CashFlowByMonthChartResult { expenditureList: any[]; } +export interface CashFlowReceivableAndExpenditure { + receivedPercentage: number; + expenditurePercentage: number; + totalInvoiced: number; + totalReceived: number; + receivable: number; + totalBudget: number; + totalExpenditure: number; + expenditureReceivable: number; +} +export interface CashFlowAnticipatedChartResult { + anticipateIncomeList: any[]; + anticipateExpenditure: any[]; + monthanticipateIncome: number; + anticipateIncomeDate: number; + anticipateIncome:number; + anticipateExpenditureList: any[]; + id: number; + name: string; + planStart: string; + planEnd: string; + startMonth: number; + Duration: number; + AverageManhours: number; + teamLead: number; + totalManhour: number; +} + export const preloadProjects = () => { fetchProjectsCashFlow(); }; @@ -48,3 +76,25 @@ export const fetchProjectsCashFlowMonthlyChart = cache(async (projectIdList: num } }); + +export const fetchProjectsCashFlowReceivableAndExpenditure = cache(async (projectIdList: number[]) => { + if (projectIdList.length !== 0) { + const queryParams = new URLSearchParams(); + queryParams.append('projectIdList', projectIdList.join(',')); + return serverFetchJson(`${BASE_API_URL}/dashboard/searchCashFlowReceivableAndExpenditure?${queryParams.toString()}`); + } else { + return []; + } + +}); + +export const fetchProjectsCashFlowAnticipate = cache(async (projectIdList: number[],year:number) => { + if (projectIdList.length !== 0) { + const queryParams = new URLSearchParams(); + queryParams.append('projectIdList', projectIdList.join(',')); + return serverFetchJson(`${BASE_API_URL}/dashboard/searchCashFlowAnticipate?${queryParams.toString()}&year=${year}`); + } else { + return []; + } + +}); diff --git a/src/components/ProjectCashFlow/ProjectCashFlow.tsx b/src/components/ProjectCashFlow/ProjectCashFlow.tsx index 14b40a0..7d422a2 100644 --- a/src/components/ProjectCashFlow/ProjectCashFlow.tsx +++ b/src/components/ProjectCashFlow/ProjectCashFlow.tsx @@ -19,7 +19,7 @@ import SearchBox, { Criterion } from "../SearchBox"; import ProgressByClientSearch from "@/components/ProgressByClientSearch"; import { Suspense } from "react"; import ProgressCashFlowSearch from "@/components/ProgressCashFlowSearch"; -import { fetchProjectsCashFlow,fetchProjectsCashFlowMonthlyChart} from "@/app/api/cashflow"; +import { fetchProjectsCashFlow,fetchProjectsCashFlowMonthlyChart,fetchProjectsCashFlowReceivableAndExpenditure,fetchProjectsCashFlowAnticipate} from "@/app/api/cashflow"; import { Input, Label } from "reactstrap"; import { CashFlow } from "@/app/api/cashflow"; import dayjs from 'dayjs'; @@ -43,6 +43,14 @@ const ProjectCashFlow: React.FC = () => { const [monthlyCumulativeExpenditureList, setMonthlyCumulativeExpenditureList]: any[] = React.useState([]); const [monthlyChartLeftMax, setMonthlyChartLeftMax]: any[] = React.useState(0); const [monthlyChartRightMax, setMonthlyChartRightMax]: any[] = React.useState(0); + const [receivedPercentage,setReceivedPercentage]: any[] = React.useState(0); + const [totalBudget,setTotalBudget]: any[] = React.useState(0); + const [totalInvoiced,setTotalInvoiced]: any[] = React.useState(0); + const [totalReceived,setTotalReceived]: any[] = React.useState(0); + const [receivable,setReceivable]: any[] = React.useState(0); + const [totalExpenditure,setTotalExpenditure]: any[] = React.useState(0); + const [expenditureReceivable,setExpenditureReceivable]: any[] = React.useState(0); + const [expenditurePercentage,setExpenditurePercentage]: any[] = React.useState(0); const [cashFlowYear, setCashFlowYear]: any[] = React.useState( todayDate.getFullYear(), ); @@ -100,12 +108,41 @@ const ProjectCashFlow: React.FC = () => { } } + const fetchReceivableAndExpenditureData = async () => { + const cashFlowReceivableAndExpenditureData = await fetchProjectsCashFlowReceivableAndExpenditure(selectedProjectIdList); + if(cashFlowReceivableAndExpenditureData.length !== 0){ + setReceivedPercentage(cashFlowReceivableAndExpenditureData[0].receivedPercentage) + setTotalInvoiced(cashFlowReceivableAndExpenditureData[0].totalInvoiced) + setTotalReceived(cashFlowReceivableAndExpenditureData[0].totalReceived) + setReceivable(cashFlowReceivableAndExpenditureData[0].receivable) + setExpenditurePercentage(cashFlowReceivableAndExpenditureData[0].expenditurePercentage) + setTotalBudget(cashFlowReceivableAndExpenditureData[0].totalBudget) + setTotalExpenditure(cashFlowReceivableAndExpenditureData[0].totalExpenditure) + setExpenditureReceivable(cashFlowReceivableAndExpenditureData[0].expenditureReceivable) + } + } + const fetchAnticipateData = async () => { + const cashFlowAnticipateData = await fetchProjectsCashFlowAnticipate(selectedProjectIdList,cashFlowYear); + const monthlyAnticipateIncome = [] + var anticipateLeftMax = 0 + if(cashFlowAnticipateData.length !== 0){ + for (var i = 0; i < cashFlowAnticipateData[0].anticipateIncomeList.length; i++) { + if (anticipateLeftMax < cashFlowAnticipateData[0].anticipateIncomeList[i].anticipateIncome){ + anticipateLeftMax = Math.max(cashFlowAnticipateData[0].anticipateIncomeList[i].anticipateIncome,cashFlowAnticipateData[0].anticipateIncomeList[i].anticipateIncome) + } + monthlyAnticipateIncome.push(cashFlowAnticipateData[0].anticipateIncomeList[i].anticipateIncome) + } + } + console.log(monthlyAnticipateIncome) + } useEffect(() => { fetchData() }, []); useEffect(() => { fetchChartData() + fetchReceivableAndExpenditureData() + fetchAnticipateData() }, [cashFlowYear,selectedProjectIdList]); const columns = [ { @@ -416,7 +453,7 @@ const ProjectCashFlow: React.FC = () => { const accountsReceivableOptions: ApexOptions = { colors: ["#20E647"], - series: [80], + series: [receivedPercentage], chart: { height: 350, type: "radialBar", @@ -465,7 +502,7 @@ const ProjectCashFlow: React.FC = () => { const expenditureOptions: ApexOptions = { colors: ["#20E647"], - series: [95], + series: [expenditurePercentage], chart: { height: 350, type: "radialBar", @@ -734,7 +771,7 @@ const ProjectCashFlow: React.FC = () => { className="text-lg font-medium ml-5" style={{ color: "#6b87cf" }} > - 1,000,000.00 + {totalInvoiced.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
{ className="text-lg font-medium ml-5" style={{ color: "#6b87cf" }} > - 800,000.00 + {totalReceived.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

{ className="text-lg font-medium ml-5 mb-2" style={{ color: "#6b87cf" }} > - 200,000.00 + {receivable.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
@@ -796,7 +833,7 @@ const ProjectCashFlow: React.FC = () => { className="text-lg font-medium ml-5" style={{ color: "#6b87cf" }} > - 800,000.00 + {totalBudget.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
{ className="text-lg font-medium ml-5" style={{ color: "#6b87cf" }} > - 760,000.00 + {totalExpenditure.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

{ className="text-lg font-medium ml-5 mb-2" style={{ color: "#6b87cf" }} > - 40,000.00 + {expenditureReceivable.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
From 598612c63ce628ca4d2c4c40d605036ee91b00d9 Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Mon, 27 May 2024 11:43:18 +0800 Subject: [PATCH 4/6] update --- .../CostAndExpenseReport.tsx | 1 + .../GenerateMonthlyWorkHoursReport.tsx | 1 + .../ProjectCompletionReport.tsx | 1 + .../ResourceOverconsumptionReport.tsx | 1 + src/components/SearchBox/SearchBox.tsx | 17 +---------------- 5 files changed, 5 insertions(+), 16 deletions(-) diff --git a/src/components/CostAndExpenseReport/CostAndExpenseReport.tsx b/src/components/CostAndExpenseReport/CostAndExpenseReport.tsx index 5957f94..ed5c159 100644 --- a/src/components/CostAndExpenseReport/CostAndExpenseReport.tsx +++ b/src/components/CostAndExpenseReport/CostAndExpenseReport.tsx @@ -49,6 +49,7 @@ const CostAndExpenseReport: React.FC = ({ team, customer }) => { return ( <> { let index = 0 diff --git a/src/components/GenerateMonthlyWorkHoursReport/GenerateMonthlyWorkHoursReport.tsx b/src/components/GenerateMonthlyWorkHoursReport/GenerateMonthlyWorkHoursReport.tsx index a858406..c5bffe3 100644 --- a/src/components/GenerateMonthlyWorkHoursReport/GenerateMonthlyWorkHoursReport.tsx +++ b/src/components/GenerateMonthlyWorkHoursReport/GenerateMonthlyWorkHoursReport.tsx @@ -46,6 +46,7 @@ const GenerateMonthlyWorkHoursReport: React.FC = ({ staffs }) => { return ( <> { const index = staffCombo.findIndex((staff) => staff === query.staff); diff --git a/src/components/ProjectCompletionReport/ProjectCompletionReport.tsx b/src/components/ProjectCompletionReport/ProjectCompletionReport.tsx index 37fd6d6..888d4b2 100644 --- a/src/components/ProjectCompletionReport/ProjectCompletionReport.tsx +++ b/src/components/ProjectCompletionReport/ProjectCompletionReport.tsx @@ -47,6 +47,7 @@ const ProjectCompletionReport: React.FC = ( return ( <> { console.log(query); diff --git a/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx b/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx index fb49dba..212a6ac 100644 --- a/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx +++ b/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx @@ -62,6 +62,7 @@ const ResourceOverconsumptionReport: React.FC = ({ team, customer }) => { return ( <> { let index = 0 diff --git a/src/components/SearchBox/SearchBox.tsx b/src/components/SearchBox/SearchBox.tsx index 3f849cd..84aa8a4 100644 --- a/src/components/SearchBox/SearchBox.tsx +++ b/src/components/SearchBox/SearchBox.tsx @@ -22,21 +22,6 @@ import { DatePicker } from "@mui/x-date-pickers/DatePicker"; import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import { Box, FormHelperText } from "@mui/material"; -import { DateCalendar } from "@mui/x-date-pickers"; -import { fetchLateStartReport } from "@/app/api/reports/actions"; -import { LateStartReportRequest } from "@/app/api/reports"; -import { fetchTeamCombo } from "@/app/api/team/actions"; -import { downloadFile } from "@/app/utils/commonUtil"; -import { - Unstable_NumberInput as BaseNumberInput, - NumberInputProps, - numberInputClasses, -} from "@mui/base/Unstable_NumberInput"; -import { - StyledButton, - StyledInputElement, - StyledInputRoot, -} from "@/theme/colorConst"; import { InputAdornment, NumberInput } from "../utils/numberInput"; interface BaseCriterion { @@ -216,7 +201,7 @@ function SearchBox({ )} {c.type === "number" && ( Date: Mon, 27 May 2024 16:21:19 +0800 Subject: [PATCH 5/6] update project potential delay report --- src/app/api/reports/index.ts | 4 +++ .../GenerateProjectPotentialDelayReport.tsx | 34 +++++++++++++++++-- src/components/SearchBox/SearchBox.tsx | 6 ++++ src/i18n/en/report.json | 3 ++ src/i18n/zh/report.json | 3 ++ 5 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/app/api/reports/index.ts b/src/app/api/reports/index.ts index c850dc0..555adbd 100644 --- a/src/app/api/reports/index.ts +++ b/src/app/api/reports/index.ts @@ -32,11 +32,15 @@ export interface ProjectCashFlowReportRequest { export interface ProjectPotentialDelayReportFilter { team: string[]; client: string[]; + numberOfDays: number; + projectCompletion: number; } export interface ProjectPotentialDelayReportRequest { teamId: number | "All"; clientId: number | "All"; + numberOfDays: number; + projectCompletion: number; } // - Monthly Work Hours Report diff --git a/src/components/GenerateProjectPotentialDelayReport/GenerateProjectPotentialDelayReport.tsx b/src/components/GenerateProjectPotentialDelayReport/GenerateProjectPotentialDelayReport.tsx index 5d0d34f..faaa66f 100644 --- a/src/components/GenerateProjectPotentialDelayReport/GenerateProjectPotentialDelayReport.tsx +++ b/src/components/GenerateProjectPotentialDelayReport/GenerateProjectPotentialDelayReport.tsx @@ -4,7 +4,7 @@ import React, { useMemo } from "react"; import SearchBox, { Criterion } from "../SearchBox"; import { useTranslation } from "react-i18next"; import { ProjectPotentialDelayReportFilter } from "@/app/api/reports"; -import { fetchProjectCashFlowReport, fetchProjectPotentialDelayReport } from "@/app/api/reports/actions"; +import { fetchProjectPotentialDelayReport } from "@/app/api/reports/actions"; import { downloadFile } from "@/app/utils/commonUtil"; import { TeamResult } from "@/app/api/team"; import { Customer } from "@/app/api/customer"; @@ -21,13 +21,19 @@ const GenerateProjectPotentialDelayReport: React.FC = ({ teams, clients } const { t } = useTranslation("report"); const teamCombo = teams.map(team => `${team.code} - ${team.name}`) const clientCombo = clients.map(client => `${client.code} - ${client.name}`) + const [errors, setErrors] = React.useState({ + numberOfDays: false, + projectCompletion: false, + }) const searchCriteria: Criterion[] = useMemo( () => [ { label: t("Team"), paramName: "team", type: "select", options: teamCombo }, { label: t("Client"), paramName: "client", type: "select", options: clientCombo }, + { label: t("Number Of Days"), paramName: "numberOfDays", type: "text", textType: "number", error: errors.numberOfDays, helperText: t("Can not be null and decimal, and should be >= 0") }, + { label: t("Project Completion (<= %)"), paramName: "projectCompletion", type: "text", textType: "number", error: errors.projectCompletion, helperText: t("Can not be null and decimal, and should be in range of 0 - 100") }, ], - [t], + [t, errors], ); return ( @@ -36,10 +42,32 @@ const GenerateProjectPotentialDelayReport: React.FC = ({ teams, clients } criteria={searchCriteria} onSearch={async (query) => { + let hasError = false + if (query.numberOfDays.length === 0 || !Number.isInteger(parseFloat(query.numberOfDays)) || parseInt(query.numberOfDays) < 0) { + setErrors((prev) => ({...prev, numberOfDays: true})) + hasError = true + } else { + setErrors((prev) => ({...prev, numberOfDays: false})) + } + + if (query.projectCompletion.length === 0 || !Number.isInteger(parseFloat(query.projectCompletion)) || parseInt(query.projectCompletion) < 0 || parseInt(query.projectCompletion) > 100) { + setErrors((prev) => ({...prev, projectCompletion: true})) + hasError = true + } else { + setErrors((prev) => ({...prev, projectCompletion: false})) + } + + if (hasError) return false + const teamIndex = teamCombo.findIndex(team => team === query.team) const clientIndex = clientCombo.findIndex(client => client === query.client) - const response = await fetchProjectPotentialDelayReport({ teamId: teams[teamIndex]?.id ?? "All", clientId: clients[clientIndex]?.id ?? "All" }) + const response = await fetchProjectPotentialDelayReport({ + teamId: teams[teamIndex]?.id ?? "All", + clientId: clients[clientIndex]?.id ?? "All", + numberOfDays: parseInt(query.numberOfDays), + projectCompletion: parseInt(query.projectCompletion) + }) if (response) { downloadFile(new Uint8Array(response.blobValue), response.filename!!) } diff --git a/src/components/SearchBox/SearchBox.tsx b/src/components/SearchBox/SearchBox.tsx index 84aa8a4..e3cf5e8 100644 --- a/src/components/SearchBox/SearchBox.tsx +++ b/src/components/SearchBox/SearchBox.tsx @@ -33,6 +33,9 @@ interface BaseCriterion { interface TextCriterion extends BaseCriterion { type: "text"; + textType?: React.HTMLInputTypeAttribute; + error?: boolean; + helperText?: React.ReactNode; } interface SelectCriterion extends BaseCriterion { @@ -175,9 +178,12 @@ function SearchBox({ {c.type === "text" && ( )} {c.type === "select" && ( diff --git a/src/i18n/en/report.json b/src/i18n/en/report.json index 541c75d..df2a3ae 100644 --- a/src/i18n/en/report.json +++ b/src/i18n/en/report.json @@ -1,4 +1,7 @@ { + "Number Of Days": "Number Of Days", + "Project Completion (<= %)": "Project Completion (<= %)", + "Project": "Project", "Date Type": "Date Type" } \ No newline at end of file diff --git a/src/i18n/zh/report.json b/src/i18n/zh/report.json index d15eec9..d85d97b 100644 --- a/src/i18n/zh/report.json +++ b/src/i18n/zh/report.json @@ -2,6 +2,9 @@ "Staff Monthly Work Hours Analysis Report": "Staff Monthly Work Hours Analysis Report", "Project Resource Overconsumption Report": "Project Resource Overconsumption Report", + "Number Of Days": "天數", + "Project Completion (<= %)": "項目完成度 (<= %)", + "Project": "項目", "Date Type": "日期類型", "Date": "日期", From 3366842544d8c6e358705fd6b078dc6f313d7302 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Mon, 27 May 2024 16:41:16 +0800 Subject: [PATCH 6/6] fix company --- src/app/utils/formatUtil.ts | 2 +- src/components/CreateCompany/CompanyDetails.tsx | 17 +++++++++-------- src/components/CreateCompany/CreateCompany.tsx | 2 +- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/app/utils/formatUtil.ts b/src/app/utils/formatUtil.ts index 150b350..ff9cc19 100644 --- a/src/app/utils/formatUtil.ts +++ b/src/app/utils/formatUtil.ts @@ -52,7 +52,7 @@ export const convertTimeArrayToString = ( format: string = OUTPUT_TIME_FORMAT, needTime: boolean = false, ) => { - let timeString = ""; + let timeString = null; if (timeArray !== null && timeArray !== undefined) { const hour = timeArray[0] || 0; diff --git a/src/components/CreateCompany/CompanyDetails.tsx b/src/components/CreateCompany/CompanyDetails.tsx index 347580e..893dadf 100644 --- a/src/components/CreateCompany/CompanyDetails.tsx +++ b/src/components/CreateCompany/CompanyDetails.tsx @@ -47,10 +47,10 @@ const CompanyDetails: React.FC = ({ // console.log(content) useEffect(() => { - setValue("normalHourFrom", convertTimeArrayToString(content.normalHourFrom, "HH:mm:ss", false)); - setValue("normalHourTo", convertTimeArrayToString(content.normalHourTo, "HH:mm:ss", false)); - setValue("otHourFrom", convertTimeArrayToString(content.otHourFrom, "HH:mm:ss", false)); - setValue("otHourTo", convertTimeArrayToString(content.otHourTo, "HH:mm:ss", false)); + setValue("normalHourFrom", convertTimeArrayToString(content.normalHourFrom, "HH:mm:ss", false) ?? "09:00:00"); + setValue("normalHourTo", convertTimeArrayToString(content.normalHourTo, "HH:mm:ss", false) ?? "18:00:00"); + setValue("otHourFrom", convertTimeArrayToString(content.otHourFrom, "HH:mm:ss", false) ?? "20:00:00"); + setValue("otHourTo", convertTimeArrayToString(content.otHourTo, "HH:mm:ss", false) ?? "08:00:00"); }, [content]) return ( @@ -125,10 +125,11 @@ const CompanyDetails: React.FC = ({ { + console.log(time?.format("HH:mm:ss")) if (!time) return; setValue("normalHourFrom", time.format("HH:mm:ss")); }} @@ -144,7 +145,7 @@ const CompanyDetails: React.FC = ({ { @@ -163,7 +164,7 @@ const CompanyDetails: React.FC = ({ { @@ -182,7 +183,7 @@ const CompanyDetails: React.FC = ({ { diff --git a/src/components/CreateCompany/CreateCompany.tsx b/src/components/CreateCompany/CreateCompany.tsx index 26aae87..7ae3d5b 100644 --- a/src/components/CreateCompany/CreateCompany.tsx +++ b/src/components/CreateCompany/CreateCompany.tsx @@ -69,7 +69,7 @@ const CreateCompany: React.FC = ({ contactName: company?.contactName, phone: company?.phone, otHourTo: "", - otHourFrom: "", + otHourFrom: "", normalHourTo: "", normalHourFrom: "", currency: company?.currency,