From 07d56e9d71002706d898846e3651c89087a5fcf7 Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Tue, 21 May 2024 14:02:06 +0800 Subject: [PATCH 1/8] update --- src/components/EditStaff/EditStaff.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/EditStaff/EditStaff.tsx b/src/components/EditStaff/EditStaff.tsx index 4864504..b33ad85 100644 --- a/src/components/EditStaff/EditStaff.tsx +++ b/src/components/EditStaff/EditStaff.tsx @@ -56,11 +56,11 @@ const EditStaff: React.FC = ({ Staff, combos }) => { name: Staff.name, companyId: Staff.company.id, teamId: Staff.team?.id, - departmentId: Staff.department.id, - gradeId: Staff.department.id, + departmentId: Staff.department?.id, + gradeId: Staff.grade?.id, skillSetId: defaultSkillset, // removeSkillSetId: [], - currentPositionId: Staff.currentPosition.id, + currentPositionId: Staff.currentPosition?.id, salaryId: Staff.salary.id, employType: Staff.employType, email: Staff.email, @@ -69,7 +69,7 @@ const EditStaff: React.FC = ({ Staff, combos }) => { emergContactName: Staff.emergContactName, emergContactPhone: Staff.emergContactPhone, joinDate: dayjs(Staff.joinDate).toString() || "", - joinPositionId: Staff.joinPosition.id, + joinPositionId: Staff.joinPosition?.id, departDate: dayjs(Staff.departDate).toString() || "", departReason: Staff.departReason, remark: Staff.remark, @@ -188,11 +188,11 @@ const EditStaff: React.FC = ({ Staff, combos }) => { name: Staff.name, companyId: Staff.company.id, teamId: Staff.team?.id, - departmentId: Staff.department.id, - gradeId: Staff.department.id, + departmentId: Staff.department?.id, + gradeId: Staff.grade?.id, skillSetId: defaultSkillset, // removeSkillSetId: [], - currentPositionId: Staff.currentPosition.id, + currentPositionId: Staff.currentPosition?.id, salaryId: Staff.salary.id, employType: Staff.employType, email: Staff.email, @@ -201,7 +201,7 @@ const EditStaff: React.FC = ({ Staff, combos }) => { emergContactName: Staff.emergContactName, emergContactPhone: Staff.emergContactPhone, joinDate: dayjs(Staff.joinDate).format(INPUT_DATE_FORMAT) || "", - joinPositionId: Staff.joinPosition.id, + joinPositionId: Staff.joinPosition?.id, departDate: !Staff.departDate ? "" : dayjs(Staff.departDate).format(INPUT_DATE_FORMAT), departReason: Staff.departReason, remark: Staff.remark, From 0ed0d5f5cce8e7f143927de1130097c3fe355695 Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Tue, 21 May 2024 14:12:52 +0800 Subject: [PATCH 2/8] update --- src/components/EditStaff/EditStaff.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/EditStaff/EditStaff.tsx b/src/components/EditStaff/EditStaff.tsx index b33ad85..fbe4323 100644 --- a/src/components/EditStaff/EditStaff.tsx +++ b/src/components/EditStaff/EditStaff.tsx @@ -61,7 +61,7 @@ const EditStaff: React.FC = ({ Staff, combos }) => { skillSetId: defaultSkillset, // removeSkillSetId: [], currentPositionId: Staff.currentPosition?.id, - salaryId: Staff.salary.id, + salaryId: Staff.salary.salaryPoint, employType: Staff.employType, email: Staff.email, phone1: Staff.phone1, @@ -193,7 +193,7 @@ const EditStaff: React.FC = ({ Staff, combos }) => { skillSetId: defaultSkillset, // removeSkillSetId: [], currentPositionId: Staff.currentPosition?.id, - salaryId: Staff.salary.id, + salaryId: Staff.salary.salaryPoint, employType: Staff.employType, email: Staff.email, phone1: Staff.phone1, From 1ab7d7223a5d76fbac4b3161b70c64482dd1e166 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Tue, 21 May 2024 18:41:07 +0800 Subject: [PATCH 3/8] add "create sup project" --- src/app/(main)/projects/create/sub/page.tsx | 14 +- src/app/api/projects/actions.ts | 1 + src/app/api/projects/index.ts | 23 +++- src/components/Breadcrumb/Breadcrumb.tsx | 2 + .../ControlledAutoComplete.tsx | 125 ++++++++++-------- .../CreateProject/CreateProject.tsx | 18 +-- .../CreateProject/CreateProjectWrapper.tsx | 13 +- .../CreateProject/ProjectClientDetails.tsx | 52 +++++++- .../CreateProject/StaffAllocation.tsx | 1 + src/components/CreateProject/TaskSetup.tsx | 40 +++--- 10 files changed, 197 insertions(+), 92 deletions(-) diff --git a/src/app/(main)/projects/create/sub/page.tsx b/src/app/(main)/projects/create/sub/page.tsx index bd5837e..1a8fab7 100644 --- a/src/app/(main)/projects/create/sub/page.tsx +++ b/src/app/(main)/projects/create/sub/page.tsx @@ -1,6 +1,7 @@ import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer"; import { fetchGrades } from "@/app/api/grades"; import { + fetchMainProjects, fetchProjectBuildingTypes, fetchProjectCategories, fetchProjectContractTypes, @@ -12,10 +13,12 @@ import { } from "@/app/api/projects"; import { preloadStaff, preloadTeamLeads } from "@/app/api/staff"; import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks"; +import { ServerFetchError } from "@/app/utils/fetchUtil"; import CreateProject from "@/components/CreateProject"; import { I18nProvider, getServerI18n } from "@/i18n"; import Typography from "@mui/material/Typography"; import { Metadata } from "next"; +import { notFound } from "next/navigation"; export const metadata: Metadata = { title: "Create Project", @@ -39,12 +42,21 @@ const Projects: React.FC = async () => { fetchGrades(); preloadTeamLeads(); preloadStaff(); + try { + const data = await fetchMainProjects(); + + if (!Boolean(data) || data.length === 0) { + notFound(); + } + } catch (e) { + notFound(); + } return ( <> {t("Create Sub Project")} - + ); diff --git a/src/app/api/projects/actions.ts b/src/app/api/projects/actions.ts index c1be476..97cb34e 100644 --- a/src/app/api/projects/actions.ts +++ b/src/app/api/projects/actions.ts @@ -22,6 +22,7 @@ export interface CreateProjectInputs { projectActualEnd: string; projectStatus: string; isClpProject: boolean; + mainProjectId?: number | null; // Project info serviceTypeId: number; diff --git a/src/app/api/projects/index.ts b/src/app/api/projects/index.ts index 7a64ea1..d25ba16 100644 --- a/src/app/api/projects/index.ts +++ b/src/app/api/projects/index.ts @@ -15,6 +15,27 @@ export interface ProjectResult { status: string; } +export interface MainProject { + projectId: number; + projectCode: string; + projectName: string; + projectCategoryId: number; + projectDescription: string; + projectLeadId: number; + projectStatus: string; + isClpProject: boolean; + serviceTypeId: number; + fundingTypeId: number; + contractTypeId: number; + locationId: number; + buildingTypeIds: number[]; + workNatureIds: number[]; + clientId: number; + clientContactId: number; + clientSubsidiaryId: number; + expectedProjectFee: number; +} + export interface ProjectCategory { id: number; name: string; @@ -82,7 +103,7 @@ export const fetchProjects = cache(async () => { }); export const fetchMainProjects = cache(async () => { - return serverFetchJson(`${BASE_API_URL}/projects/main`, { + return serverFetchJson(`${BASE_API_URL}/projects/main`, { next: { tags: ["projects"] }, }); }); diff --git a/src/components/Breadcrumb/Breadcrumb.tsx b/src/components/Breadcrumb/Breadcrumb.tsx index 516aca2..8c1b9b0 100644 --- a/src/components/Breadcrumb/Breadcrumb.tsx +++ b/src/components/Breadcrumb/Breadcrumb.tsx @@ -15,7 +15,9 @@ const pathToLabelMap: { [path: string]: string } = { "/home": "User Workspace", "/projects": "Projects", "/projects/create": "Create Project", + "/projects/create/sub": "Sub Project", "/projects/edit": "Edit Project", + "/projects/edit/sub": "Sub Project", "/tasks": "Task Template", "/tasks/create": "Create Task Template", "/staffReimbursement": "Staff Reimbursement", diff --git a/src/components/ControlledAutoComplete/ControlledAutoComplete.tsx b/src/components/ControlledAutoComplete/ControlledAutoComplete.tsx index d8a2962..e37f348 100644 --- a/src/components/ControlledAutoComplete/ControlledAutoComplete.tsx +++ b/src/components/ControlledAutoComplete/ControlledAutoComplete.tsx @@ -10,7 +10,7 @@ const icon = ; const checkedIcon = ; // label -> e.g. code - name -> 001 - WL // name -> WL -interface Props { +interface Props { control: Control, options: T[], name: Path, // register name @@ -18,7 +18,6 @@ interface Props - error?: boolean, } function ControlledAutoComplete< @@ -28,68 +27,78 @@ function ControlledAutoComplete< props: Props ) { const { t } = useTranslation() - const { control, options, name, label, noOptionsText, isMultiple, rules, error } = props; + const { control, options, name, label, noOptionsText, isMultiple, rules } = props; + + // set default value if value is null + if (!Boolean(isMultiple) && !Boolean(control._formValues[name])) { + console.log(name, control._formValues[name]) + control._formValues[name] = options[0]?.id ?? undefined + } else if (Boolean(isMultiple) && !Boolean(control._formValues[name])) { + control._formValues[name] = [] + } return ( ( - isMultiple ? - { - // console.log(field.value) - return field.value?.includes(option.id) - })} - options={options} - getOptionLabel={(option) => option.label ?? option.name!!} - isOptionEqualToValue={(option, value) => option.id === value.id} - renderOption={(params, option, { selected }) => { - return ( -
  • - - {option.label ?? option.name} -
  • - ); - }} - onChange={(event, value) => { - field.onChange(value?.map(v => v.id)) - }} - renderInput={(params) => } - /> - : - option.id === field.value) ?? options[0]} - options={options} - getOptionLabel={(option) => option.label ?? option.name!!} - isOptionEqualToValue={(option, value) => option.id === value.id} - renderOption={(params, option) => { - return ( - - {option.label ?? option.name} - - ); - }} - onChange={(event, value) => { - field.onChange(value?.id) - }} - renderInput={(params) => } - /> - )} + + render={({ field, fieldState, formState }) => { + + return ( + isMultiple ? + { + return field.value?.includes(option.id) + })} + options={options} + getOptionLabel={(option) => option.label ?? option.name!!} + isOptionEqualToValue={(option, value) => option.id === value.id} + renderOption={(params, option, { selected }) => { + return ( +
  • + + {option.label ?? option.name} +
  • + ); + }} + onChange={(event, value) => { + field.onChange(value?.map(v => v.id)) + }} + renderInput={(params) => } + /> + : + option.id === field.value) ?? options[0]} + options={options} + getOptionLabel={(option) => option.label ?? option.name!!} + isOptionEqualToValue={(option, value) => option?.id === value?.id} + renderOption={(params, option) => { + return ( + + {option.label ?? option.name} + + ); + }} + onChange={(event, value) => { + field.onChange(value?.id) + }} + renderInput={(params) => } + />) + }} /> ) } diff --git a/src/components/CreateProject/CreateProject.tsx b/src/components/CreateProject/CreateProject.tsx index 937bac5..5f35919 100644 --- a/src/components/CreateProject/CreateProject.tsx +++ b/src/components/CreateProject/CreateProject.tsx @@ -33,6 +33,7 @@ import { ContractType, FundingType, LocationType, + MainProject, ProjectCategory, ServiceType, WorkNature, @@ -52,6 +53,8 @@ import dayjs from "dayjs"; export interface Props { isEditMode: boolean; + isSubProject: boolean; + mainProjects?: MainProject[]; defaultInputs?: CreateProjectInputs; allTasks: Task[]; projectCategories: ProjectCategory[]; @@ -93,6 +96,8 @@ const hasErrorsInTab = ( const CreateProject: React.FC = ({ isEditMode, + isSubProject, + mainProjects, defaultInputs, allTasks, projectCategories, @@ -269,14 +274,9 @@ const CreateProject: React.FC = ({ milestones: {}, totalManhour: 0, taskTemplateId: "All", - projectCategoryId: projectCategories.length > 0 ? projectCategories[0].id : undefined, - projectLeadId: teamLeads.length > 0 ? teamLeads[0].id : undefined, - serviceTypeId: serviceTypes.length > 0 ? serviceTypes[0].id : undefined, - fundingTypeId: fundingTypes.length > 0 ? fundingTypes[0].id : undefined, - contractTypeId: contractTypes.length > 0 ? contractTypes[0].id : undefined, - locationId: locationTypes.length > 0 ? locationTypes[0].id : undefined, - clientSubsidiaryId: undefined, - clientId: allCustomers.length > 0 ? allCustomers[0].id : undefined, + projectName: mainProjects !== undefined ? mainProjects[0].projectName : undefined, + projectDescription: mainProjects !== undefined ? mainProjects[0].projectDescription : undefined, + expectedProjectFee: mainProjects !== undefined ? mainProjects[0].expectedProjectFee : undefined, ...defaultInputs, // manhourPercentageByGrade should have a sensible default @@ -380,6 +380,8 @@ const CreateProject: React.FC = ({ { = async (props) => { ? await fetchProjectDetails(props.projectId!) : undefined; + const mainProjects = Boolean(props.isSubProject) + ? await fetchMainProjects() + : undefined; + return ( = async (props) => { workNatures={workNatures} allStaffs={allStaffs} grades={grades} + mainProjects={mainProjects} /> ); }; diff --git a/src/components/CreateProject/ProjectClientDetails.tsx b/src/components/CreateProject/ProjectClientDetails.tsx index d77defe..8fb7fab 100644 --- a/src/components/CreateProject/ProjectClientDetails.tsx +++ b/src/components/CreateProject/ProjectClientDetails.tsx @@ -22,6 +22,7 @@ import { ContractType, FundingType, LocationType, + MainProject, ProjectCategory, ServiceType, WorkNature, @@ -37,6 +38,8 @@ import ControlledAutoComplete from "../ControlledAutoComplete/ControlledAutoComp interface Props { isActive: boolean; + isSubProject: boolean; + mainProjects?: MainProject[]; projectCategories: ProjectCategory[]; teamLeads: StaffResult[]; allCustomers: Customer[]; @@ -51,6 +54,8 @@ interface Props { const ProjectClientDetails: React.FC = ({ isActive, + isSubProject, + mainProjects, projectCategories, teamLeads, allCustomers, @@ -70,6 +75,8 @@ const ProjectClientDetails: React.FC = ({ control, setValue, getValues, + reset, + resetField, } = useFormContext(); const subsidiaryMap = useMemo<{ @@ -136,7 +143,6 @@ const ProjectClientDetails: React.FC = ({ // Automatically add the team lead to the allocated staff list const selectedTeamLeadId = watch("projectLeadId"); useEffect(() => { - console.log(selectedTeamLeadId) if (selectedTeamLeadId !== undefined) { const currentStaffIds = getValues("allocatedStaffIds"); const newList = uniq([...currentStaffIds, selectedTeamLeadId]); @@ -144,6 +150,32 @@ const ProjectClientDetails: React.FC = ({ } }, [getValues, selectedTeamLeadId, setValue]); + // Automatically update the project & client details whene select a main project + const mainProjectId = watch("mainProjectId") + useEffect(() => { + if (mainProjectId !== undefined && mainProjects !== undefined) { + const mainProject = mainProjects.find(project => project.projectId === mainProjectId); + + if (mainProject !== undefined) { + setValue("projectName", mainProject.projectName) + setValue("projectCategoryId", mainProject.projectCategoryId) + setValue("projectLeadId", mainProject.projectLeadId) + setValue("serviceTypeId", mainProject.serviceTypeId) + setValue("fundingTypeId", mainProject.fundingTypeId) + setValue("contractTypeId", mainProject.contractTypeId) + setValue("locationId", mainProject.locationId) + setValue("buildingTypeIds", mainProject.buildingTypeIds) + setValue("workNatureIds", mainProject.workNatureIds) + setValue("projectDescription", mainProject.projectDescription) + setValue("expectedProjectFee", mainProject.expectedProjectFee) + setValue("isClpProject", mainProject.isClpProject) + setValue("clientId", mainProject.clientId) + setValue("clientSubsidiaryId", mainProject.clientSubsidiaryId) + setValue("clientContactId", mainProject.clientContactId) + } + } + }, [getValues, mainProjectId, setValue]) + // const buildingTypeIdNameMap = buildingTypes.reduce<{ [id: number]: string }>( // (acc, building) => ({ ...acc, [building.id]: building.name }), // {}, @@ -162,6 +194,18 @@ const ProjectClientDetails: React.FC = ({ {t("Project Details")} + { + isSubProject && mainProjects !== undefined && <> + ({ id: mainProject.projectId, label: `${mainProject.projectCode} - ${mainProject.projectName}` }))]} + name="mainProjectId" + label={t("Main Project")} + noOptionsText={t("No Main Project")} + /> + + + } = ({ {t("CLP Project")} @@ -317,7 +361,6 @@ const ProjectClientDetails: React.FC = ({ rules={{ required: "Please select a client" }} - error={Boolean(errors.clientId)} /> @@ -337,7 +380,7 @@ const ProjectClientDetails: React.FC = ({ subsidiaryMap[subId]) .map((subsidiaryId, index) => { const subsidiary = subsidiaryMap[subsidiaryId] @@ -368,7 +411,6 @@ const ProjectClientDetails: React.FC = ({ } else return true; }, }} - error={Boolean(errors.clientContactId)} /> diff --git a/src/components/CreateProject/StaffAllocation.tsx b/src/components/CreateProject/StaffAllocation.tsx index bd699ec..81d3c97 100644 --- a/src/components/CreateProject/StaffAllocation.tsx +++ b/src/components/CreateProject/StaffAllocation.tsx @@ -25,6 +25,7 @@ import { Tab, Tabs, SelectChangeEvent, + Autocomplete, } from "@mui/material"; import differenceWith from "lodash/differenceWith"; import intersectionWith from "lodash/intersectionWith"; diff --git a/src/components/CreateProject/TaskSetup.tsx b/src/components/CreateProject/TaskSetup.tsx index dac0698..fe02f37 100644 --- a/src/components/CreateProject/TaskSetup.tsx +++ b/src/components/CreateProject/TaskSetup.tsx @@ -7,7 +7,7 @@ import Typography from "@mui/material/Typography"; import { useTranslation } from "react-i18next"; import TransferList from "../TransferList"; import Button from "@mui/material/Button"; -import React, { useCallback, useMemo, useState } from "react"; +import React, { SyntheticEvent, useCallback, useMemo, useState } from "react"; import CardActions from "@mui/material/CardActions"; import RestartAlt from "@mui/icons-material/RestartAlt"; import FormControl from "@mui/material/FormControl"; @@ -20,6 +20,7 @@ import { CreateProjectInputs, ManhourAllocation } from "@/app/api/projects/actio import isNumber from "lodash/isNumber"; import intersectionWith from "lodash/intersectionWith"; import { difference } from "lodash"; +import { Autocomplete, TextField } from "@mui/material"; interface Props { allTasks: Task[]; @@ -50,9 +51,9 @@ const TaskSetup: React.FC = ({ "All" | number >(watch("taskTemplateId") ?? "All"); const onSelectTaskTemplate = useCallback( - (e: SelectChangeEvent) => { - if (e.target.value === "All" || isNumber(e.target.value)) { - setSelectedTaskTemplateId(e.target.value); + (event: SyntheticEvent, value: NonNullable<{id: number | string, name: string}>) => { + if (value.id === "All" || isNumber(value.id)) { + setSelectedTaskTemplateId(value.id); // onReset(); } }, @@ -132,21 +133,24 @@ const TaskSetup: React.FC = ({ marginBlockEnd={1} > - - {t("Task List Source")} - - label={t("Task List Source")} - value={selectedTaskTemplateId} - onChange={onSelectTaskTemplate} - > - {t("All tasks")} - {taskTemplates.map((template, index) => ( - - {template.name} + taskTemplate.id === selectedTaskTemplateId)} + options={[{id: "All", name: t("All tasks")}, ...taskTemplates.map(taskTemplate => ({id: taskTemplate.id, name: taskTemplate.name}))]} + getOptionLabel={(taskTemplate) => taskTemplate.name} + isOptionEqualToValue={(option, value) => option?.id === value?.id} + renderOption={(params, option) => { + return ( + + {option.name} - ))} - - + ); + }} + onChange={onSelectTaskTemplate} + renderInput={(params) => } + /> Date: Wed, 22 May 2024 13:31:14 +0800 Subject: [PATCH 4/8] update overconsumption report --- .../ResourceOverconsumptionReport/page.tsx | 40 ++- src/app/api/report3/index.ts | 42 --- src/app/api/reports/actions.ts | 15 +- src/app/api/reports/index.ts | 14 + .../Report/ReportSearchBox3/SearchBox3.tsx | 313 ------------------ .../Report/ReportSearchBox3/index.ts | 3 - .../ResourceOverconsumptionReportGen.tsx | 45 --- ...esourceOverconsumptionReportGenWrapper.tsx | 19 -- .../ResourceOverconsumptionReportGen/index.ts | 2 - .../ResourceOverconsumptionReport.tsx | 96 ++++++ .../ResourceOverconsumptionReportLoading.tsx} | 4 +- .../ResourceOverconsumptionReportWrapper.tsx | 20 ++ .../ResourceOverconsumptionReport/index.ts | 1 + src/components/SearchBox/SearchBox.tsx | 62 +++- src/components/utils/numberInput.tsx | 192 +++++++++++ src/theme/colorConst.js | 153 +++++++++ 16 files changed, 565 insertions(+), 456 deletions(-) delete mode 100644 src/app/api/report3/index.ts delete mode 100644 src/components/Report/ReportSearchBox3/SearchBox3.tsx delete mode 100644 src/components/Report/ReportSearchBox3/index.ts delete mode 100644 src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGen.tsx delete mode 100644 src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenWrapper.tsx delete mode 100644 src/components/Report/ResourceOverconsumptionReportGen/index.ts create mode 100644 src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx rename src/components/{Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenLoading.tsx => ResourceOverconsumptionReport/ResourceOverconsumptionReportLoading.tsx} (89%) create mode 100644 src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReportWrapper.tsx create mode 100644 src/components/ResourceOverconsumptionReport/index.ts create mode 100644 src/components/utils/numberInput.tsx diff --git a/src/app/(main)/analytics/ResourceOverconsumptionReport/page.tsx b/src/app/(main)/analytics/ResourceOverconsumptionReport/page.tsx index a1751be..771d21f 100644 --- a/src/app/(main)/analytics/ResourceOverconsumptionReport/page.tsx +++ b/src/app/(main)/analytics/ResourceOverconsumptionReport/page.tsx @@ -1,24 +1,28 @@ -//src\app\(main)\analytics\ResourceOvercomsumptionReport\page.tsx import { Metadata } from "next"; -import { I18nProvider } from "@/i18n"; -import Typography from "@mui/material/Typography"; -import ResourceOverconsumptionReportComponent from "@/components/Report/ResourceOverconsumptionReport"; +import { Suspense } from "react"; +import { I18nProvider, getServerI18n } from "@/i18n"; +import ResourceOverconsumptionReport from "@/components/ResourceOverconsumptionReport"; +import { Typography } from "@mui/material"; export const metadata: Metadata = { - title: "Resource Overconsumption Report", + title: "Staff Monthly Work Hours Analysis Report", }; -const ResourceOverconsumptionReport: React.FC = () => { - return ( - - - Resource Overconsumption Report - - {/* }> - - */} - - - ); +const StaffMonthlyWorkHoursAnalysisReport: React.FC = async () => { + const { t } = await getServerI18n("User Group"); + + return ( + <> + + {t("Project Resource Overconsumption Report")} + + + }> + + + + + ); }; -export default ResourceOverconsumptionReport; + +export default StaffMonthlyWorkHoursAnalysisReport; diff --git a/src/app/api/report3/index.ts b/src/app/api/report3/index.ts deleted file mode 100644 index b2c8751..0000000 --- a/src/app/api/report3/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -//src\app\api\report\index.ts -import { cache } from "react"; - -export interface ResourceOverconsumption { - id: number; - projectCode: string; - projectName: string; - team: string; - teamLeader: string; - startDate: string; - startDateFrom: string; - startDateTo: string; - targetEndDate: string; - client: string; - subsidiary: string; - status: string; -} - -export const preloadProjects = () => { - fetchProjectsResourceOverconsumption(); -}; - -export const fetchProjectsResourceOverconsumption = cache(async () => { - return mockProjects; -}); - -const mockProjects: ResourceOverconsumption[] = [ - { - id: 1, - projectCode: "CUST-001", - projectName: "Client A", - team: "N/A", - teamLeader: "N/A", - startDate: "1/2/2024", - startDateFrom: "1/2/2024", - startDateTo: "1/2/2024", - targetEndDate: "30/3/2024", - client: "ss", - subsidiary: "sus", - status: "1", - }, -]; diff --git a/src/app/api/reports/actions.ts b/src/app/api/reports/actions.ts index 3d49a16..a4c2b85 100644 --- a/src/app/api/reports/actions.ts +++ b/src/app/api/reports/actions.ts @@ -1,7 +1,7 @@ "use server"; import { serverFetchBlob, serverFetchJson } from "@/app/utils/fetchUtil"; -import { MonthlyWorkHoursReportRequest, ProjectCashFlowReportRequest,LateStartReportRequest } from "."; +import { MonthlyWorkHoursReportRequest, ProjectCashFlowReportRequest,LateStartReportRequest, ProjectResourceOverconsumptionReportRequest } from "."; import { BASE_API_URL } from "@/config/api"; export interface FileResponse { @@ -35,6 +35,19 @@ export const fetchMonthlyWorkHoursReport = async (data: MonthlyWorkHoursReportRe return reportBlob }; +export const fetchProjectResourceOverconsumptionReport = async (data: ProjectResourceOverconsumptionReportRequest) => { + const reportBlob = await serverFetchBlob( + `${BASE_API_URL}/reports/ProjectResourceOverconsumptionReport`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); + + return reportBlob +}; + // export const fetchLateStartReport = async (data: LateStartReportRequest) => { // const response = await serverFetchBlob( // `${BASE_API_URL}/reports/downloadLateStartReport`, diff --git a/src/app/api/reports/index.ts b/src/app/api/reports/index.ts index dd1afb8..f1fa613 100644 --- a/src/app/api/reports/index.ts +++ b/src/app/api/reports/index.ts @@ -25,6 +25,20 @@ export interface MonthlyWorkHoursReportRequest { id: number; yearMonth: string; } +// - Project Resource Overconsumption Report +export interface ProjectResourceOverconsumptionReportFilter { + team: string[]; + customer: string[]; + status: string[]; + lowerLimit: number; +} + +export interface ProjectResourceOverconsumptionReportRequest { + teamId?: number + custId?: number + status: "All" | "Within Budget" | "Potential Overconsumption" | "Overconsumption" + lowerLimit: number +} export interface LateStartReportFilter { remainedDays: number; diff --git a/src/components/Report/ReportSearchBox3/SearchBox3.tsx b/src/components/Report/ReportSearchBox3/SearchBox3.tsx deleted file mode 100644 index aafa7d0..0000000 --- a/src/components/Report/ReportSearchBox3/SearchBox3.tsx +++ /dev/null @@ -1,313 +0,0 @@ -//src\components\ReportSearchBox3\SearchBox3.tsx -"use client"; - -import Grid from "@mui/material/Grid"; -import Card from "@mui/material/Card"; -import CardContent from "@mui/material/CardContent"; -import Typography from "@mui/material/Typography"; -import React, { useCallback, useMemo, useState } from "react"; -import { useTranslation } from "react-i18next"; -import TextField from "@mui/material/TextField"; -import FormControl from "@mui/material/FormControl"; -import InputLabel from "@mui/material/InputLabel"; -import Select, { SelectChangeEvent } from "@mui/material/Select"; -import MenuItem from "@mui/material/MenuItem"; -import CardActions from "@mui/material/CardActions"; -import Button from "@mui/material/Button"; -import RestartAlt from "@mui/icons-material/RestartAlt"; -import Search from "@mui/icons-material/Search"; -import dayjs from "dayjs"; -import "dayjs/locale/zh-hk"; -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 } from "@mui/material"; -import * as XLSX from 'xlsx-js-style'; -//import { DownloadReportButton } from '../LateStartReportGen/DownloadReportButton'; - -interface BaseCriterion { - label: string; - label2?: string; - paramName: T; - paramName2?: T; -} - -interface TextCriterion extends BaseCriterion { - type: "text"; -} - -interface SelectCriterion extends BaseCriterion { - type: "select"; - options: string[]; -} - -interface DateRangeCriterion extends BaseCriterion { - type: "dateRange"; -} - -export type Criterion = - | TextCriterion - | SelectCriterion - | DateRangeCriterion; - -interface Props { - criteria: Criterion[]; - onSearch: (inputs: Record) => void; - onReset?: () => void; -} - -function SearchBox({ - criteria, - onSearch, - onReset, -}: Props) { - const { t } = useTranslation("common"); - const defaultInputs = useMemo( - () => - criteria.reduce>( - (acc, c) => { - return { ...acc, [c.paramName]: c.type === "select" ? "All" : "" }; - }, - {} as Record, - ), - [criteria], - ); - const [inputs, setInputs] = useState(defaultInputs); - - const makeInputChangeHandler = useCallback( - (paramName: T): React.ChangeEventHandler => { - return (e) => { - setInputs((i) => ({ ...i, [paramName]: e.target.value })); - }; - }, - [], - ); - - const makeSelectChangeHandler = useCallback((paramName: T) => { - return (e: SelectChangeEvent) => { - setInputs((i) => ({ ...i, [paramName]: e.target.value })); - }; - }, []); - - const makeDateChangeHandler = useCallback((paramName: T) => { - return (e: any) => { - setInputs((i) => ({ ...i, [paramName]: dayjs(e).format("YYYY-MM-DD") })); - }; - }, []); - - const makeDateToChangeHandler = useCallback((paramName: T) => { - return (e: any) => { - setInputs((i) => ({ - ...i, - [paramName + "To"]: dayjs(e).format("YYYY-MM-DD"), - })); - }; - }, []); - - const handleReset = () => { - setInputs(defaultInputs); - onReset?.(); - }; - - const handleSearch = () => { - onSearch(inputs); - - }; - - const handleDownload = async () => { - //setIsLoading(true); - - try { - const response = await fetch('/temp/AR03_Resource Overconsumption.xlsx', { - headers: { - 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - }, - }); - if (!response.ok) throw new Error('Network response was not ok.'); - - const data = await response.blob(); - const reader = new FileReader(); - reader.onload = (e) => { - if (e.target && e.target.result) { - const ab = e.target.result as ArrayBuffer; - const workbook = XLSX.read(ab, { type: 'array' }); - const firstSheetName = workbook.SheetNames[0]; - const worksheet = workbook.Sheets[firstSheetName]; - - // Add the current date to cell C2 - const cellAddress = 'C2'; - const date = new Date().toISOString().split('T')[0]; // Format YYYY-MM-DD - const formattedDate = date.replace(/-/g, '/'); // Change format to YYYY/MM/DD - XLSX.utils.sheet_add_aoa(worksheet, [[formattedDate]], { origin: cellAddress }); - - // Style for cell A1: Font size 16 and bold - if (worksheet['A1']) { - worksheet['A1'].s = { - font: { - bold: true, - sz: 16, // Font size 16 - //name: 'Times New Roman' // Specify font - } - }; - } - - // Apply styles from A2 to A4 (bold) - ['A2', 'A3', 'A4'].forEach(cell => { - if (worksheet[cell]) { - worksheet[cell].s = { font: { bold: true } }; - } - }); - - // Formatting from A6 to L6 - // Apply styles from A6 to L6 (bold, bottom border, center alignment) - for (let col = 0; col < 12; col++) { // Columns A to K - const cellRef = XLSX.utils.encode_col(col) + '6'; - if (worksheet[cellRef]) { - worksheet[cellRef].s = { - font: { bold: true }, - alignment: { horizontal: 'center' }, - border: { - bottom: { style: 'thin', color: { auto: 1 } } - } - }; - } - } - - const firstTableData = [ - ['Column1', 'Column2', 'Column3'], // Row 1 - ['Data1', 'Data2', 'Data3'], // Row 2 - // ... more rows as needed - ]; - // Find the last row of the first table - let lastRowOfFirstTable = 6; // Starting row for data in the first table - while (worksheet[XLSX.utils.encode_cell({ c: 0, r: lastRowOfFirstTable })]) { - lastRowOfFirstTable++; - } - - // Calculate the maximum length of content in each column and set column width - const colWidths: number[] = []; - - const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "", blankrows: true }) as (string | number)[][]; - jsonData.forEach((row: (string | number)[]) => { - row.forEach((cell: string | number, index: number) => { - const valueLength = cell.toString().length; - colWidths[index] = Math.max(colWidths[index] || 0, valueLength); - }); - }); - - // Apply calculated widths to each column, skipping column A - worksheet['!cols'] = colWidths.map((width, index) => { - if (index === 0) { - return { wch: 8 }; // Set default or specific width for column A if needed - } - return { wch: width + 2 }; // Add padding to width - }); - - // Format filename with date - const today = new Date().toISOString().split('T')[0].replace(/-/g, '_'); // Get current date and format as YYYY_MM_DD - const filename = `AR03_Resource_Overconsumption_${today}.xlsx`; // Append formatted date to the filename - - // Convert workbook back to XLSX file - XLSX.writeFile(workbook, filename); - } else { - throw new Error('Failed to load file'); - } - }; - reader.readAsArrayBuffer(data); - } catch (error) { - console.error('Error downloading the file: ', error); - } - - //setIsLoading(false); - }; - return ( - - - {t("Search Criteria")} - - {criteria.map((c) => { - return ( - - {c.type === "text" && ( - - )} - {c.type === "select" && ( - - {c.label} - - - )} - {c.type === "dateRange" && ( - - - - - - - {"-"} - - - - - - - )} - - ); - })} - - - - - - - - ); -} - -export default SearchBox; diff --git a/src/components/Report/ReportSearchBox3/index.ts b/src/components/Report/ReportSearchBox3/index.ts deleted file mode 100644 index d481fbd..0000000 --- a/src/components/Report/ReportSearchBox3/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -//src\components\SearchBox\index.ts -export { default } from "./SearchBox3"; -export type { Criterion } from "./SearchBox3"; diff --git a/src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGen.tsx b/src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGen.tsx deleted file mode 100644 index a6ec216..0000000 --- a/src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGen.tsx +++ /dev/null @@ -1,45 +0,0 @@ -//src\components\LateStartReportGen\LateStartReportGen.tsx -"use client"; -import React, { useMemo, useState } from "react"; -import SearchBox, { Criterion } from "../ReportSearchBox3"; -import { useTranslation } from "react-i18next"; -import { ResourceOverconsumption } from "@/app/api/report3"; - -interface Props { - projects: ResourceOverconsumption[]; -} -type SearchQuery = Partial>; -type SearchParamNames = keyof SearchQuery; - -const ProgressByClientSearch: React.FC = ({ projects }) => { - const { t } = useTranslation("projects"); - - const searchCriteria: Criterion[] = useMemo( - () => [ - { label: "Team", paramName: "team", type: "select", options: ["AAA", "BBB", "CCC"] }, - { label: "Client", paramName: "client", type: "select", options: ["Cust A", "Cust B", "Cust C"] }, - { label: "Status", paramName: "status", type: "select", options: ["Overconsumption", "Potential Overconsumption"] }, - // { - // label: "Status", - // label2: "Remained Date To", - // paramName: "targetEndDate", - // type: "dateRange", - // }, - ], - [t], - ); - - return ( - <> - { - console.log(query); - }} - /> - {/* */} - - ); -}; - -export default ProgressByClientSearch; diff --git a/src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenWrapper.tsx b/src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenWrapper.tsx deleted file mode 100644 index a93f64b..0000000 --- a/src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenWrapper.tsx +++ /dev/null @@ -1,19 +0,0 @@ -//src\components\LateStartReportGen\LateStartReportGenWrapper.tsx -import { fetchProjectsResourceOverconsumption } from "@/app/api/report3"; -import React from "react"; -import ResourceOvercomsumptionReportGen from "./ResourceOverconsumptionReportGen"; -import ResourceOvercomsumptionReportGenLoading from "./ResourceOverconsumptionReportGenLoading"; - -interface SubComponents { - Loading: typeof ResourceOvercomsumptionReportGenLoading; -} - -const ResourceOvercomsumptionReportGenWrapper: React.FC & SubComponents = async () => { - const clentprojects = await fetchProjectsResourceOverconsumption(); - - return ; -}; - -ResourceOvercomsumptionReportGenWrapper.Loading = ResourceOvercomsumptionReportGenLoading; - -export default ResourceOvercomsumptionReportGenWrapper; \ No newline at end of file diff --git a/src/components/Report/ResourceOverconsumptionReportGen/index.ts b/src/components/Report/ResourceOverconsumptionReportGen/index.ts deleted file mode 100644 index 82fb633..0000000 --- a/src/components/Report/ResourceOverconsumptionReportGen/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -//src\components\DelayReportGen\index.ts -export { default } from "./ResourceOverconsumptionReportGenWrapper"; diff --git a/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx b/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx new file mode 100644 index 0000000..5e6e76a --- /dev/null +++ b/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx @@ -0,0 +1,96 @@ +"use client"; +import React, { useMemo } from "react"; +import SearchBox, { Criterion } from "../SearchBox"; +import { useTranslation } from "react-i18next"; +import { ProjectResult } from "@/app/api/projects"; +import { fetchMonthlyWorkHoursReport, fetchProjectCashFlowReport, fetchProjectResourceOverconsumptionReport } from "@/app/api/reports/actions"; +import { downloadFile } from "@/app/utils/commonUtil"; +import { BASE_API_URL } from "@/config/api"; +import { ProjectResourceOverconsumptionReportFilter, ProjectResourceOverconsumptionReportRequest } from "@/app/api/reports"; +import { StaffResult } from "@/app/api/staff"; +import { TeamResult } from "@/app/api/team"; +import { Customer } from "@/app/api/customer"; + +interface Props { + team: TeamResult[] + customer: Customer[] +} + +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +const ResourceOverconsumptionReport: React.FC = ({ team, customer }) => { + const { t } = useTranslation(); + const teamCombo = team.map(t => `${t.name} - ${t.code}`) + const custCombo = customer.map(c => `${c.name} - ${c.code}`) + const statusCombo = ["Within Budget, Overconsumption", "Potential Overconsumption"] + // const staffCombo = staffs.map(staff => `${staff.name} - ${staff.staffId}`) + // console.log(staffs) + + const searchCriteria: Criterion[] = useMemo( + () => [ + { + label: t("Team"), + paramName: "team", + type: "select", + options: teamCombo, + needAll: true + }, + { + label: t("Client"), + paramName: "customer", + type: "select", + options: custCombo, + needAll: true + }, + { + label: t("Status"), + paramName: "status", + type: "select", + options: statusCombo, + needAll: true + }, + { + label: t("lowerLimit"), + paramName: "lowerLimit", + type: "number", + }, + ], + [t], + ); + +return ( + <> + { + let index = 0 + let postData: ProjectResourceOverconsumptionReportRequest = { + status: "All", + lowerLimit: 0.9 + } + if (query.team.length > 0 && query.team.toLocaleLowerCase() !== "all") { + index = teamCombo.findIndex(team => team === query.team) + postData.teamId = team[index].id + } + if (query.customer.length > 0 && query.customer.toLocaleLowerCase() !== "all") { + index = custCombo.findIndex(customer => customer === query.customer) + postData.custId = customer[index].id + } + if (Boolean(query.lowerLimit)) { + postData.lowerLimit = query.lowerLimit/100 + } + postData.status = query.status + console.log(postData) + const response = await fetchProjectResourceOverconsumptionReport(postData) + if (response) { + downloadFile(new Uint8Array(response.blobValue), response.filename!!) + } + } + } + /> + + ) +} + +export default ResourceOverconsumptionReport \ No newline at end of file diff --git a/src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenLoading.tsx b/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReportLoading.tsx similarity index 89% rename from src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenLoading.tsx rename to src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReportLoading.tsx index 9ae7417..945d13c 100644 --- a/src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenLoading.tsx +++ b/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReportLoading.tsx @@ -6,7 +6,7 @@ import Stack from "@mui/material/Stack"; import React from "react"; // Can make this nicer -export const ResourceOvercomsumptionReportGenLoading: React.FC = () => { +export const ResourceOvercomsumptionReportLoading: React.FC = () => { return ( <> @@ -38,4 +38,4 @@ export const ResourceOvercomsumptionReportGenLoading: React.FC = () => { ); }; -export default ResourceOvercomsumptionReportGenLoading; +export default ResourceOvercomsumptionReportLoading; diff --git a/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReportWrapper.tsx b/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReportWrapper.tsx new file mode 100644 index 0000000..1ab9d24 --- /dev/null +++ b/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReportWrapper.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import ResourceOvercomsumptionReportLoading from "./ResourceOverconsumptionReportLoading"; +import ResourceOverconsumptionReport from "./ResourceOverconsumptionReport"; +import { fetchAllCustomers } from "@/app/api/customer"; +import { fetchTeam } from "@/app/api/team"; + +interface SubComponents { + Loading: typeof ResourceOvercomsumptionReportLoading; +} + +const ResourceOvercomsumptionReportWrapper: React.FC & SubComponents = async () => { + const customers = await fetchAllCustomers() + const teams = await fetchTeam () + + return ; +}; + +ResourceOvercomsumptionReportWrapper.Loading = ResourceOvercomsumptionReportLoading; + +export default ResourceOvercomsumptionReportWrapper; \ No newline at end of file diff --git a/src/components/ResourceOverconsumptionReport/index.ts b/src/components/ResourceOverconsumptionReport/index.ts new file mode 100644 index 0000000..b5f20e2 --- /dev/null +++ b/src/components/ResourceOverconsumptionReport/index.ts @@ -0,0 +1 @@ +export { default } from "./ResourceOverconsumptionReportWrapper"; \ No newline at end of file diff --git a/src/components/SearchBox/SearchBox.tsx b/src/components/SearchBox/SearchBox.tsx index 686eb8d..2ab52b0 100644 --- a/src/components/SearchBox/SearchBox.tsx +++ b/src/components/SearchBox/SearchBox.tsx @@ -4,7 +4,7 @@ import Grid from "@mui/material/Grid"; import Card from "@mui/material/Card"; import CardContent from "@mui/material/CardContent"; import Typography from "@mui/material/Typography"; -import React, { useCallback, useMemo, useState } from "react"; +import React, { FocusEvent, KeyboardEvent, PointerEvent, useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import TextField from "@mui/material/TextField"; import FormControl from "@mui/material/FormControl"; @@ -15,7 +15,7 @@ import CardActions from "@mui/material/CardActions"; import Button from "@mui/material/Button"; import RestartAlt from "@mui/icons-material/RestartAlt"; import Search from "@mui/icons-material/Search"; -import FileDownload from '@mui/icons-material/FileDownload'; +import FileDownload from "@mui/icons-material/FileDownload"; import dayjs from "dayjs"; import "dayjs/locale/zh-hk"; import { DatePicker } from "@mui/x-date-pickers/DatePicker"; @@ -23,6 +23,17 @@ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import { Box } from "@mui/material"; import { DateCalendar } from "@mui/x-date-pickers"; +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 { label: string; @@ -49,17 +60,22 @@ interface MonthYearCriterion extends BaseCriterion { type: "monthYear"; } +interface NumberCriterion extends BaseCriterion { + type: "number"; +} + export type Criterion = | TextCriterion | SelectCriterion | DateRangeCriterion - | MonthYearCriterion; + | MonthYearCriterion + | NumberCriterion; interface Props { criteria: Criterion[]; onSearch: (inputs: Record) => void; onReset?: () => void; - formType?: String, + formType?: String; } function SearchBox({ @@ -75,10 +91,14 @@ function SearchBox({ (acc, c) => { return { ...acc, - [c.paramName]: c.type === "select" ? - !(c.needAll === false) ? "All" : - c.options.length > 0 ? c.options[0] : "" - : "" + [c.paramName]: + c.type === "select" + ? !(c.needAll === false) + ? "All" + : c.options.length > 0 + ? c.options[0] + : "" + : "", }; }, {} as Record @@ -86,7 +106,7 @@ function SearchBox({ [criteria] ); const [inputs, setInputs] = useState(defaultInputs); - + const makeInputChangeHandler = useCallback( (paramName: T): React.ChangeEventHandler => { return (e) => { @@ -95,6 +115,15 @@ function SearchBox({ }, [] ); + + const makeNumberChangeHandler = useCallback( + (paramName: T): (event: FocusEvent | PointerEvent | KeyboardEvent, value: number | null) => void => { + return (event, value) => { + setInputs((i) => ({ ...i, [paramName]: value })); + }; + }, + [] + ); const makeSelectChangeHandler = useCallback((paramName: T) => { return (e: SelectChangeEvent) => { @@ -110,7 +139,7 @@ function SearchBox({ const makeMonthYearChangeHandler = useCallback((paramName: T) => { return (e: any) => { - console.log(dayjs(e).format("YYYY-MM")) + console.log(dayjs(e).format("YYYY-MM")); setInputs((i) => ({ ...i, [paramName]: dayjs(e).format("YYYY-MM") })); }; }, []); @@ -168,6 +197,15 @@ function SearchBox({ )} + {c.type === "number" && ( + %} + /> + )} {c.type === "monthYear" && ( ({ - + */} diff --git a/src/components/CreateProject/ProjectClientDetails.tsx b/src/components/CreateProject/ProjectClientDetails.tsx index 8fb7fab..9e4ac7f 100644 --- a/src/components/CreateProject/ProjectClientDetails.tsx +++ b/src/components/CreateProject/ProjectClientDetails.tsx @@ -39,6 +39,7 @@ import ControlledAutoComplete from "../ControlledAutoComplete/ControlledAutoComp interface Props { isActive: boolean; isSubProject: boolean; + isEditMode: boolean; mainProjects?: MainProject[]; projectCategories: ProjectCategory[]; teamLeads: StaffResult[]; @@ -55,6 +56,7 @@ interface Props { const ProjectClientDetails: React.FC = ({ isActive, isSubProject, + isEditMode, mainProjects, projectCategories, teamLeads, @@ -110,6 +112,7 @@ const ProjectClientDetails: React.FC = ({ ); // get customer (client) contact combo + const [firstCustomerLoaded, setFirstCustomerLoaded] = useState(false) useEffect(() => { if (selectedCustomerId !== undefined) { fetchCustomer(selectedCustomerId).then(({ contacts, subsidiaryIds }) => { @@ -118,7 +121,7 @@ const ProjectClientDetails: React.FC = ({ // if (subsidiaryIds.length > 0) setValue("clientSubsidiaryId", subsidiaryIds[0]) // else - setValue("clientSubsidiaryId", undefined) + if (isEditMode && !firstCustomerLoaded) { setFirstCustomerLoaded(true) } else setValue("clientSubsidiaryId", null) // if (contacts.length > 0) setValue("clientContactId", contacts[0].id) // else setValue("clientContactId", undefined) }); @@ -130,11 +133,11 @@ const ProjectClientDetails: React.FC = ({ if (Boolean(clientSubsidiaryId)) { // get subsidiary contact combo const contacts = allSubsidiaries.find(subsidiary => subsidiary.id === clientSubsidiaryId)?.subsidiaryContacts!! - setSubsidiaryContacts(contacts) + setSubsidiaryContacts(() => contacts) setValue("clientContactId", selectedCustomerId === defaultValues?.clientId && Boolean(defaultValues?.clientSubsidiaryId) ? contacts.find(contact => contact.id === defaultValues.clientContactId)?.id ?? contacts[0].id : contacts[0].id) setValue("isSubsidiaryContact", true) } else if (customerContacts?.length > 0) { - setSubsidiaryContacts([]) + setSubsidiaryContacts(() => []) setValue("clientContactId", selectedCustomerId === defaultValues?.clientId && !Boolean(defaultValues?.clientSubsidiaryId) ? customerContacts.find(contact => contact.id === defaultValues.clientContactId)?.id ?? customerContacts[0].id : customerContacts[0].id) setValue("isSubsidiaryContact", false) } @@ -153,7 +156,7 @@ const ProjectClientDetails: React.FC = ({ // Automatically update the project & client details whene select a main project const mainProjectId = watch("mainProjectId") useEffect(() => { - if (mainProjectId !== undefined && mainProjects !== undefined) { + if (mainProjectId !== undefined && mainProjects !== undefined && !isEditMode) { const mainProject = mainProjects.find(project => project.projectId === mainProjectId); if (mainProject !== undefined) { @@ -174,7 +177,7 @@ const ProjectClientDetails: React.FC = ({ setValue("clientContactId", mainProject.clientContactId) } } - }, [getValues, mainProjectId, setValue]) + }, [getValues, mainProjectId, setValue, isEditMode]) // const buildingTypeIdNameMap = buildingTypes.reduce<{ [id: number]: string }>( // (acc, building) => ({ ...acc, [building.id]: building.name }), @@ -202,6 +205,7 @@ const ProjectClientDetails: React.FC = ({ name="mainProjectId" label={t("Main Project")} noOptionsText={t("No Main Project")} + disabled={isEditMode} /> @@ -438,11 +442,11 @@ const ProjectClientDetails: React.FC = ({ )} - + {/* - + */} ); diff --git a/src/components/CreateProject/StaffAllocation.tsx b/src/components/CreateProject/StaffAllocation.tsx index 81d3c97..e61f995 100644 --- a/src/components/CreateProject/StaffAllocation.tsx +++ b/src/components/CreateProject/StaffAllocation.tsx @@ -1,7 +1,7 @@ "use client"; import { useTranslation } from "react-i18next"; -import React, { useEffect, useMemo } from "react"; +import React, { SyntheticEvent, useEffect, useMemo } from "react"; import RestartAlt from "@mui/icons-material/RestartAlt"; import SearchResults, { Column } from "../SearchResults"; import { Search, Clear, PersonAdd, PersonRemove } from "@mui/icons-material"; @@ -160,8 +160,8 @@ const StaffAllocation: React.FC = ({ }, [columnFilters]); const [filters, setFilters] = React.useState(defaultFilterValues); const makeFilterSelect = React.useCallback( - (filter: keyof StaffResult) => (event: SelectChangeEvent) => { - setFilters((f) => ({ ...f, [filter]: event.target.value })); + (filter: keyof StaffResult) => (event: SyntheticEvent, value: NonNullable) => { + setFilters((f) => ({ ...f, [filter]: value })); }, [], ); @@ -239,20 +239,25 @@ const StaffAllocation: React.FC = ({ return ( - {label} - + renderInput={(params) => } + /> ); @@ -289,11 +294,11 @@ const StaffAllocation: React.FC = ({ )} - + {/* - + */} {/* MUI X-Grid will throw an error if it is rendered without any dimensions; so not rendering when not active */} diff --git a/src/components/CreateProject/TaskSetup.tsx b/src/components/CreateProject/TaskSetup.tsx index fe02f37..759b05f 100644 --- a/src/components/CreateProject/TaskSetup.tsx +++ b/src/components/CreateProject/TaskSetup.tsx @@ -135,7 +135,7 @@ const TaskSetup: React.FC = ({ taskTemplate.id === selectedTaskTemplateId)} options={[{id: "All", name: t("All tasks")}, ...taskTemplates.map(taskTemplate => ({id: taskTemplate.id, name: taskTemplate.name}))]} @@ -207,11 +207,11 @@ const TaskSetup: React.FC = ({ allItemsLabel={t("Task Pool")} selectedItemsLabel={t("Project Task List")} /> - + {/* - + */} ); diff --git a/src/components/ProjectSearch/ProjectSearch.tsx b/src/components/ProjectSearch/ProjectSearch.tsx index 79ee51b..bbb1d67 100644 --- a/src/components/ProjectSearch/ProjectSearch.tsx +++ b/src/components/ProjectSearch/ProjectSearch.tsx @@ -20,7 +20,6 @@ type SearchParamNames = keyof SearchQuery; const ProjectSearch: React.FC = ({ projects, projectCategories }) => { const router = useRouter(); const { t } = useTranslation("projects"); - console.log(projects) const [filteredProjects, setFilteredProjects] = useState(projects); @@ -62,7 +61,9 @@ const ProjectSearch: React.FC = ({ projects, projectCategories }) => { const onProjectClick = useCallback( (project: ProjectResult) => { - router.push(`/projects/edit?id=${project.id}`); + if (Boolean(project.mainProject)) { + router.push(`/projects/edit/sub?id=${project.id}`); + } else router.push(`/projects/edit?id=${project.id}`); }, [router], ); From b91acd66758870f55af6ff5db545d5e4b6259f8f Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Wed, 22 May 2024 16:05:31 +0800 Subject: [PATCH 7/8] update --- src/components/EditTeam/Allocation.tsx | 24 ++- src/components/EditUser/AuthAllocation.tsx | 196 +++++++++++---------- src/components/EditUser/EditUser.tsx | 100 +++++++---- src/components/EditUser/UserDetail.tsx | 41 ++++- 4 files changed, 214 insertions(+), 147 deletions(-) diff --git a/src/components/EditTeam/Allocation.tsx b/src/components/EditTeam/Allocation.tsx index 2376ece..b9762f5 100644 --- a/src/components/EditTeam/Allocation.tsx +++ b/src/components/EditTeam/Allocation.tsx @@ -60,24 +60,20 @@ const Allocation: React.FC = ({ allStaffs: staff, teamLead }) => { return rearrangedStaff.filter((s) => getValues("addStaffIds")?.includes(s.id)) } ); - const [seletedTeamLead, setSeletedTeamLead] = useState(); const [deletedStaffIds, setDeletedStaffIds] = useState([]); // Adding / Removing staff const addStaff = useCallback((staff: StaffResult) => { setSelectedStaff((s) => [...s, staff]); - // setDeletedStaffIds((s) => s.filter((s) => s === selectedStaff.id)) }, []); const removeStaff = useCallback((staff: StaffResult) => { setSelectedStaff((s) => s.filter((s) => s.id !== staff.id)); - // setDeletedStaffIds((s) => s) setDeletedStaffIds((prevIds) => [...prevIds, staff.id]); }, []); const setTeamLead = useCallback( (staff: StaffResult) => { - setSeletedTeamLead(staff.id); const rearrangedList = getValues("addStaffIds").reduce( (acc, num, index) => { if (num === staff.id && index !== 0) { @@ -171,16 +167,16 @@ const Allocation: React.FC = ({ allStaffs: staff, teamLead }) => { }, []); React.useEffect(() => { - // setFilteredStaff( - // initialStaffs.filter((s) => { - // const q = query.toLowerCase(); - // // s.staffId.toLowerCase().includes(q) - // // const q = query.toLowerCase(); - // // return s.name.toLowerCase().includes(q); - // // s.code.toString().includes(q) || - // // (s.brNo != null && s.brNo.toLowerCase().includes(q)) - // }) - // ); + setFilteredStaff( + initialStaffs.filter((i) => { + const q = query.toLowerCase(); + return ( + i.staffId.toLowerCase().includes(q) || + i.name.toLowerCase().includes(q) || + i.currentPosition.toLowerCase().includes(q) + ); + }) + ); }, [staff, query]); useEffect(() => { diff --git a/src/components/EditUser/AuthAllocation.tsx b/src/components/EditUser/AuthAllocation.tsx index afb44d5..fe6c5a9 100644 --- a/src/components/EditUser/AuthAllocation.tsx +++ b/src/components/EditUser/AuthAllocation.tsx @@ -1,93 +1,97 @@ "use client"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import { useRouter, useSearchParams } from "next/navigation"; -import { Add, Clear, PersonAdd, PersonRemove, Remove, Search } from "@mui/icons-material"; +import { + Add, + Clear, + PersonAdd, + PersonRemove, + Remove, + Search, +} from "@mui/icons-material"; import { useTranslation } from "react-i18next"; import { - FieldErrors, - FormProvider, - SubmitErrorHandler, - SubmitHandler, - useForm, - useFormContext, - } from "react-hook-form"; + FieldErrors, + FormProvider, + SubmitErrorHandler, + SubmitHandler, + useForm, + useFormContext, +} from "react-hook-form"; import { - Box, - Card, - CardContent, - Grid, - IconButton, - InputAdornment, - Stack, - Tab, - Tabs, - TabsProps, - TextField, - Typography, - } from "@mui/material"; - import { differenceBy } from "lodash"; + Box, + Card, + CardContent, + Grid, + IconButton, + InputAdornment, + Stack, + Tab, + Tabs, + TabsProps, + TextField, + Typography, +} from "@mui/material"; +import { differenceBy } from "lodash"; import { UserInputs } from "@/app/api/user/actions"; import { auth } from "@/app/api/group/actions"; import SearchResults, { Column } from "../SearchResults"; export interface Props { - auths: auth[] - - } + auths: auth[]; +} const AuthAllocation: React.FC = ({ auths }) => { - const { t } = useTranslation(); - const searchParams = useSearchParams(); - const id = parseInt(searchParams.get("id") || "0"); - const { - setValue, - getValues, - formState: { defaultValues }, - reset, - resetField, - } = useFormContext(); - const initialAuths = auths.map((u) => ({ ...u })).sort((a, b) => a.id - b.id); - const [filteredAuths, setFilteredAuths] = useState(initialAuths); - const [selectedAuths, setSelectedAuths] = useState( - () => { - return filteredAuths.filter( - (s) => getValues("addAuthIds")?.includes(s.id) - ); - } + const { t } = useTranslation(); + const searchParams = useSearchParams(); + const id = parseInt(searchParams.get("id") || "0"); + const { + setValue, + getValues, + formState: { defaultValues }, + reset, + resetField, + } = useFormContext(); + const initialAuths = auths.map((u) => ({ ...u })).sort((a, b) => a.id - b.id); + const [filteredAuths, setFilteredAuths] = useState(initialAuths); + const [selectedAuths, setSelectedAuths] = useState( + () => { + return filteredAuths.filter( + (s) => getValues("addAuthIds")?.includes(s.id) ); - const [removeAuthIds, setRemoveAuthIds] = useState([]); + } + ); + const [removeAuthIds, setRemoveAuthIds] = useState([]); - // Adding / Removing Auth - const addAuth = useCallback((auth: auth) => { - setSelectedAuths((a) => [...a, auth]); - }, []); - const removeAuth = useCallback((auth: auth) => { - setSelectedAuths((a) => a.filter((a) => a.id !== auth.id)); - setRemoveAuthIds((prevIds) => [...prevIds, auth.id]); - }, []); + // Adding / Removing Auth + const addAuth = useCallback((auth: auth) => { + setSelectedAuths((a) => [...a, auth]); + }, []); + const removeAuth = useCallback((auth: auth) => { + setSelectedAuths((a) => a.filter((a) => a.id !== auth.id)); + setRemoveAuthIds((prevIds) => [...prevIds, auth.id]); + }, []); - const clearAuth = useCallback(() => { - if (defaultValues !== undefined) { - resetField("addAuthIds"); - setSelectedAuths( - initialAuths.filter((auth) => defaultValues.addAuthIds?.includes(auth.id)) - ); - } - }, [defaultValues]); + const clearAuth = useCallback(() => { + if (defaultValues !== undefined) { + resetField("addAuthIds"); + setSelectedAuths( + initialAuths.filter( + (auth) => defaultValues.addAuthIds?.includes(auth.id) + ) + ); + } + }, [defaultValues]); - // Sync with form + // Sync with form useEffect(() => { setValue( "addAuthIds", selectedAuths.map((a) => a.id) ); - setValue( - "removeAuthIds", - removeAuthIds - ); + setValue("removeAuthIds", removeAuthIds); }, [selectedAuths, removeAuthIds, setValue]); - const AuthPoolColumns = useMemo[]>( () => [ { @@ -97,8 +101,7 @@ const AuthAllocation: React.FC = ({ auths }) => { buttonIcon: , }, { label: t("authority"), name: "authority" }, - { label: t("Auth Name"), name: "name" }, - // { label: t("Current Position"), name: "currentPosition" }, + { label: t("description"), name: "name" }, ], [addAuth, t] ); @@ -109,10 +112,10 @@ const AuthAllocation: React.FC = ({ auths }) => { label: t("Remove"), name: "id", onClick: removeAuth, - buttonIcon: , + buttonIcon: , }, { label: t("authority"), name: "authority" }, - { label: t("Auth Name"), name: "name" }, + { label: t("description"), name: "name" }, ], [removeAuth, selectedAuths, t] ); @@ -128,16 +131,14 @@ const AuthAllocation: React.FC = ({ auths }) => { }, []); React.useEffect(() => { - // setFilteredStaff( - // initialStaffs.filter((s) => { - // const q = query.toLowerCase(); - // // s.staffId.toLowerCase().includes(q) - // // const q = query.toLowerCase(); - // // return s.name.toLowerCase().includes(q); - // // s.code.toString().includes(q) || - // // (s.brNo != null && s.brNo.toLowerCase().includes(q)) - // }) - // ); + setFilteredAuths( + initialAuths.filter((a) => + ( + a.authority.toLowerCase().includes(query.toLowerCase()) || + a.name?.toLowerCase().includes(query.toLowerCase()) + ) + ) + ); }, [auths, query]); const resetAuth = React.useCallback(() => { @@ -147,16 +148,16 @@ const AuthAllocation: React.FC = ({ auths }) => { const formProps = useForm({}); - // Tab related - const [tabIndex, setTabIndex] = React.useState(0); - const handleTabChange = React.useCallback>( - (_e, newValue) => { - setTabIndex(newValue); - }, - [] - ); + // Tab related + const [tabIndex, setTabIndex] = React.useState(0); + const handleTabChange = React.useCallback>( + (_e, newValue) => { + setTabIndex(newValue); + }, + [] + ); -return ( + return ( <> @@ -175,7 +176,9 @@ return ( fullWidth onChange={onQueryInputChange} value={query} - placeholder={t("Search by staff ID, name or position.")} + placeholder={t( + "Search by Authority or description or position." + )} InputProps={{ endAdornment: query && ( @@ -191,18 +194,20 @@ return ( - {tabIndex === 0 && ( + {tabIndex === 0 && ( )} - {tabIndex === 1 && ( + {tabIndex === 1 && ( ); - -} -export default AuthAllocation \ No newline at end of file +}; +export default AuthAllocation; diff --git a/src/components/EditUser/EditUser.tsx b/src/components/EditUser/EditUser.tsx index bbbd174..947a116 100644 --- a/src/components/EditUser/EditUser.tsx +++ b/src/components/EditUser/EditUser.tsx @@ -1,6 +1,12 @@ "use client"; import { useRouter, useSearchParams } from "next/navigation"; -import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from "react"; +import React, { + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useState, +} from "react"; import SearchResults, { Column } from "../SearchResults"; // import { TeamResult } from "@/app/api/team"; import { useTranslation } from "react-i18next"; @@ -26,23 +32,24 @@ import { } from "react-hook-form"; import { Check, Close, Error, RestartAlt } from "@mui/icons-material"; import { StaffResult } from "@/app/api/staff"; -import { UserInputs, adminChangePassword, editUser, fetchUserDetails } from "@/app/api/user/actions"; +import { + UserInputs, + adminChangePassword, + editUser, + fetchUserDetails, +} from "@/app/api/user/actions"; import UserDetail from "./UserDetail"; import { UserResult, passwordRule } from "@/app/api/user"; import { auth, fetchAuth } from "@/app/api/group/actions"; import AuthAllocation from "./AuthAllocation"; interface Props { - user: UserResult, - rules: passwordRule, - auths: auth[] -} + user: UserResult; + rules: passwordRule; + auths: auth[]; +} -const EditUser: React.FC = async ({ - user, - rules, - auths -}) => { +const EditUser: React.FC = async ({ user, rules, auths }) => { const { t } = useTranslation(); const formProps = useForm(); const searchParams = useSearchParams(); @@ -50,6 +57,13 @@ const EditUser: React.FC = async ({ const [tabIndex, setTabIndex] = useState(0); const router = useRouter(); const [serverError, setServerError] = useState(""); + const addAuthIds = + auths && auths.length > 0 + ? auths + .filter((item) => item.v === 1) + .map((item) => item.id) + .sort((a, b) => a - b) + : []; const handleTabChange = useCallback>( (_e, newValue) => { @@ -60,22 +74,27 @@ const EditUser: React.FC = async ({ const errors = formProps.formState.errors; - useEffect(() => { + const resetForm = React.useCallback(() => { + console.log("triggerred"); + console.log(addAuthIds); try { - const addAuthIds = auths && auths.length > 0 - ? auths.filter((item) => item.v === 1).map((item) => item.id).sort((a, b) => a - b) - : [] - formProps.reset({ name: user.username, email: user.email, - addAuthIds: addAuthIds + addAuthIds: addAuthIds, + removeAuthIds: [], + password: "", }); + console.log(formProps.formState.defaultValues); } catch (error) { console.log(error); setServerError(t("An error has occurred. Please try again later.")); } - }, [user, auths]); + }, [auths, user]); + + useEffect(() => { + resetForm(); + }, []); const hasErrorsInTab = ( tabIndex: number, @@ -96,22 +115,33 @@ const EditUser: React.FC = async ({ const onSubmit = useCallback>( async (data) => { try { - let haveError = false - let regex_pw = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*])[A-Za-z\d!@#$%^&*]{8,20}$/ - let pw = '' + let haveError = false; + let regex_pw = + /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*])[A-Za-z\d!@#$%^&*]{8,20}$/; + let pw = ""; if (data.password && data.password.length > 0) { - pw = data.password + pw = data.password; if (pw.length < rules.min) { - haveError = true - formProps.setError("password", { message: t("The password requires 8-20 characters."), type: "required" }) + haveError = true; + formProps.setError("password", { + message: t("The password requires 8-20 characters."), + type: "required", + }); } if (pw.length > rules.max) { - haveError = true - formProps.setError("password", { message: t("The password requires 8-20 characters."), type: "required" }) + haveError = true; + formProps.setError("password", { + message: t("The password requires 8-20 characters."), + type: "required", + }); } if (!regex_pw.test(pw)) { - haveError = true - formProps.setError("password", { message: "A combination of uppercase letters, lowercase letters, numbers, and symbols is required.", type: "required" }) + haveError = true; + formProps.setError("password", { + message: + "A combination of uppercase letters, lowercase letters, numbers, and symbols is required.", + type: "required", + }); } } const userData = { @@ -119,16 +149,16 @@ const EditUser: React.FC = async ({ locked: false, addAuthIds: data.addAuthIds || [], removeAuthIds: data.removeAuthIds || [], - } + }; const pwData = { id: id, password: "", - newPassword: pw - } + newPassword: pw, + }; if (haveError) { - return + return; } - console.log("passed") + console.log("passed"); await editUser(id, userData); if (data.password && data.password.length > 0) { await adminChangePassword(pwData); @@ -185,12 +215,12 @@ const EditUser: React.FC = async ({ {tabIndex == 0 && } - {tabIndex === 1 && } + {tabIndex === 1 && } diff --git a/src/components/EditUser/UserDetail.tsx b/src/components/EditUser/UserDetail.tsx index fc2f419..1d251c4 100644 --- a/src/components/EditUser/UserDetail.tsx +++ b/src/components/EditUser/UserDetail.tsx @@ -9,13 +9,13 @@ import { Stack, TextField, Typography, + makeStyles, } from "@mui/material"; import { useFormContext } from "react-hook-form"; import { useTranslation } from "react-i18next"; - - const UserDetail: React.FC = () => { + const { t } = useTranslation(); const { register, @@ -45,6 +45,30 @@ const UserDetail: React.FC = () => { label={t("password")} fullWidth {...register("password")} + // helperText={ + // Boolean(errors.password) && + // (errors.password?.message + // ? t(errors.password.message) + // : + // (<> + // - 8-20 characters + //
    + // - Uppercase letters + //
    + // - Lowercase letters + //
    + // - Numbers + //
    + // - Symbols + // ) + // ) + // } + helperText={ + Boolean(errors.password) && + (errors.password?.message + ? t(errors.password.message) + : t("Please input correct password")) + } error={Boolean(errors.password)} />
    @@ -55,3 +79,16 @@ const UserDetail: React.FC = () => { }; export default UserDetail; + + +{/* <> + - 8-20 characters +
    + - Uppercase letters +
    + - Lowercase letters +
    + - Numbers +
    + - Symbols + */} \ No newline at end of file From 812dca7afe12bbefaeacf13b88372631e14165f6 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Wed, 22 May 2024 16:09:11 +0800 Subject: [PATCH 8/8] update --- .../ResourceOverconsumptionReport.tsx | 17 ----------------- .../ResourceOverconsumptionReport/index.ts | 2 -- 2 files changed, 19 deletions(-) delete mode 100644 src/components/Report/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx delete mode 100644 src/components/Report/ResourceOverconsumptionReport/index.ts diff --git a/src/components/Report/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx b/src/components/Report/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx deleted file mode 100644 index 345b2f2..0000000 --- a/src/components/Report/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx +++ /dev/null @@ -1,17 +0,0 @@ -//src\components\DelayReport\DelayReport.tsx -"use client"; -import * as React from "react"; -import "../../../app/global.css"; -import { Suspense } from "react"; -import ResourceOverconsumptionReportGen from "@/components/Report/ResourceOverconsumptionReportGen"; - -const ResourceOverconsumptionReport: React.FC = () => { - - return ( - }> - - - ); -}; - -export default ResourceOverconsumptionReport; \ No newline at end of file diff --git a/src/components/Report/ResourceOverconsumptionReport/index.ts b/src/components/Report/ResourceOverconsumptionReport/index.ts deleted file mode 100644 index ce20324..0000000 --- a/src/components/Report/ResourceOverconsumptionReport/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -//src\components\LateStartReport\index.ts -export { default } from "./ResourceOverconsumptionReport";