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/(main)/projects/create/sub/not-found.tsx b/src/app/(main)/projects/create/sub/not-found.tsx
index 1cc4df3..9b28f5d 100644
--- a/src/app/(main)/projects/create/sub/not-found.tsx
+++ b/src/app/(main)/projects/create/sub/not-found.tsx
@@ -8,7 +8,7 @@ export default async function NotFound() {
return (
{t("Not Found")}
- {t("The sub project was not found or there was no any main projects!")}
+ {t("There was no any main projects!")}
{t("Return to all projects")}
diff --git a/src/app/(main)/projects/create/sub/page.tsx b/src/app/(main)/projects/create/sub/page.tsx
index bd5837e..3f474de 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,13 +13,15 @@ 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",
+ title: "Create Sub Project",
};
const Projects: React.FC = async () => {
@@ -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/(main)/projects/edit/sub/not-found.tsx b/src/app/(main)/projects/edit/sub/not-found.tsx
new file mode 100644
index 0000000..234e436
--- /dev/null
+++ b/src/app/(main)/projects/edit/sub/not-found.tsx
@@ -0,0 +1,17 @@
+import { getServerI18n } from "@/i18n";
+import { Stack, Typography, Link } from "@mui/material";
+import NextLink from "next/link";
+
+export default async function NotFound() {
+ const { t } = await getServerI18n("projects", "common");
+
+ return (
+
+ {t("Not Found")}
+ {t("The sub project was not found!")}
+
+ {t("Return to all projects")}
+
+
+ );
+}
diff --git a/src/app/(main)/projects/edit/sub/page.tsx b/src/app/(main)/projects/edit/sub/page.tsx
new file mode 100644
index 0000000..eb4f5c6
--- /dev/null
+++ b/src/app/(main)/projects/edit/sub/page.tsx
@@ -0,0 +1,76 @@
+import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer";
+import { fetchGrades } from "@/app/api/grades";
+import {
+ fetchMainProjects,
+ fetchProjectBuildingTypes,
+ fetchProjectCategories,
+ fetchProjectContractTypes,
+ fetchProjectDetails,
+ fetchProjectFundingTypes,
+ fetchProjectLocationTypes,
+ fetchProjectServiceTypes,
+ fetchProjectWorkNatures,
+} from "@/app/api/projects";
+import { preloadStaff, preloadTeamLeads } from "@/app/api/staff";
+import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks";
+import CreateProject from "@/components/CreateProject";
+import { I18nProvider, getServerI18n } from "@/i18n";
+import Typography from "@mui/material/Typography";
+import { isArray } from "lodash";
+import { Metadata } from "next";
+import { notFound } from "next/navigation";
+
+interface Props {
+ searchParams: { [key: string]: string | string[] | undefined };
+}
+
+export const metadata: Metadata = {
+ title: "Edit Sub Project",
+};
+
+const Projects: React.FC = async ({ searchParams }) => {
+ const { t } = await getServerI18n("projects");
+ const projectId = searchParams["id"];
+
+ if (!projectId || isArray(projectId)) {
+ notFound();
+ }
+
+ // Preload necessary dependencies
+ fetchAllTasks();
+ fetchTaskTemplates();
+ fetchProjectCategories();
+ fetchProjectContractTypes();
+ fetchProjectFundingTypes();
+ fetchProjectLocationTypes();
+ fetchProjectServiceTypes();
+ fetchProjectBuildingTypes();
+ fetchProjectWorkNatures();
+ fetchAllCustomers();
+ fetchAllSubsidiaries();
+ fetchGrades();
+ preloadTeamLeads();
+ preloadStaff();
+
+ try {
+ await fetchProjectDetails(projectId);
+ const data = await fetchMainProjects();
+
+ if (!Boolean(data) || data.length === 0) {
+ notFound();
+ }
+ } catch (e) {
+ notFound();
+ }
+
+ return (
+ <>
+ {t("Edit Sub Project")}
+
+
+
+ >
+ );
+};
+
+export default Projects;
diff --git a/src/app/(main)/settings/changepassword/page.tsx b/src/app/(main)/settings/changepassword/page.tsx
index b6b9a41..e470ca7 100644
--- a/src/app/(main)/settings/changepassword/page.tsx
+++ b/src/app/(main)/settings/changepassword/page.tsx
@@ -14,40 +14,33 @@ import { Metadata } from "next";
import Link from "next/link";
import { Suspense } from "react";
-
export const metadata: Metadata = {
- title: "Change Password",
- };
+ title: "Change Password",
+};
+const ChangePasswordPage: React.FC = async () => {
+ const { t } = await getServerI18n("User Group");
+ // preloadTeamLeads();
+ // preloadStaff();
+ return (
+ <>
+
+
+ {t("Change Password")}
+
+
+
+ }>
+
+
+
+ >
+ );
+};
- const ChangePasswordPage: React.FC = async () => {
- const { t } = await getServerI18n("User Group");
- // preloadTeamLeads();
- // preloadStaff();
- return (
- <>
-
-
- {t("Change Password")}
-
-
- {/*
- }>
-
-
- */}
-
- }>
-
-
-
- >
- );
- };
-
- export default ChangePasswordPage;
\ No newline at end of file
+export default ChangePasswordPage;
diff --git a/src/app/(main)/settings/staff/user/page.tsx b/src/app/(main)/settings/staff/user/page.tsx
index 86373bb..b45dc04 100644
--- a/src/app/(main)/settings/staff/user/page.tsx
+++ b/src/app/(main)/settings/staff/user/page.tsx
@@ -7,19 +7,15 @@ import { Suspense } from "react";
import { preloadUser } from "@/app/api/user";
import { searchParamsProps } from "@/app/utils/fetchUtil";
-const User: React.FC = async ({
- searchParams
-}) => {
+const User: React.FC = async ({ searchParams }) => {
const { t } = await getServerI18n("user");
- preloadUser()
+ preloadUser();
return (
<>
{t("Edit User")}
}>
-
+
>
diff --git a/src/app/(main)/settings/user/edit/page.tsx b/src/app/(main)/settings/user/edit/page.tsx
index fc36425..f820520 100644
--- a/src/app/(main)/settings/user/edit/page.tsx
+++ b/src/app/(main)/settings/user/edit/page.tsx
@@ -20,10 +20,10 @@ const EditUserPage: React.FC = async ({
return (
<>
-
+
}>
diff --git a/src/app/api/projects/actions.ts b/src/app/api/projects/actions.ts
index c1be476..31ab2c9 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;
@@ -35,8 +36,8 @@ export interface CreateProjectInputs {
// Client details
clientId: Customer["id"];
clientContactId?: number;
- clientSubsidiaryId?: number;
- subsidiaryContactId: number;
+ clientSubsidiaryId?: number | null;
+ subsidiaryContactId?: number;
isSubsidiaryContact?: boolean;
// Allocation
diff --git a/src/app/api/projects/index.ts b/src/app/api/projects/index.ts
index 7a64ea1..6833571 100644
--- a/src/app/api/projects/index.ts
+++ b/src/app/api/projects/index.ts
@@ -13,6 +13,28 @@ export interface ProjectResult {
team: string;
client: string;
status: string;
+ mainProject: 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 {
@@ -82,7 +104,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/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 ddcab13..3b94fca 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, ProjectPandLReportRequest } from ".";
+import { MonthlyWorkHoursReportRequest, ProjectCashFlowReportRequest,LateStartReportRequest, ProjectResourceOverconsumptionReportRequest, ProjectPandLReportRequest } 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 e633084..491797a 100644
--- a/src/app/api/reports/index.ts
+++ b/src/app/api/reports/index.ts
@@ -39,6 +39,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/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/ChangePassword/ChangePasswordForm.tsx b/src/components/ChangePassword/ChangePasswordForm.tsx
index 19e2a29..9801ec7 100644
--- a/src/components/ChangePassword/ChangePasswordForm.tsx
+++ b/src/components/ChangePassword/ChangePasswordForm.tsx
@@ -33,19 +33,12 @@ const ChagnePasswordForm: React.FC = () => {
setValue,
} = useFormContext();
- // const resetGroup = useCallback(() => {
- // console.log(defaultValues);
- // if (defaultValues !== undefined) {
- // resetField("description");
- // }
- // }, [defaultValues]);
-
return (
- {t("Group Info")}
+ {t("Please Fill in all the Fields")}
diff --git a/src/components/ChangePassword/ChangePasswordWrapper.tsx b/src/components/ChangePassword/ChangePasswordWrapper.tsx
index 30acb9d..2de12db 100644
--- a/src/components/ChangePassword/ChangePasswordWrapper.tsx
+++ b/src/components/ChangePassword/ChangePasswordWrapper.tsx
@@ -7,10 +7,6 @@ interface SubComponents {
}
const ChangePasswordWrapper: React.FC & SubComponents = async () => {
- // const records = await fetchAuth()
- // const users = await fetchUser()
- // console.log(users)
- // const auth = records.records as auth[]
return ;
};
diff --git a/src/components/ControlledAutoComplete/ControlledAutoComplete.tsx b/src/components/ControlledAutoComplete/ControlledAutoComplete.tsx
index d8a2962..36e7542 100644
--- a/src/components/ControlledAutoComplete/ControlledAutoComplete.tsx
+++ b/src/components/ControlledAutoComplete/ControlledAutoComplete.tsx
@@ -1,6 +1,6 @@
"use client"
-import { Autocomplete, MenuItem, TextField, Checkbox } from "@mui/material";
+import { Autocomplete, MenuItem, TextField, Checkbox, Chip } from "@mui/material";
import { Controller, FieldValues, Path, Control, RegisterOptions } from "react-hook-form";
import { useTranslation } from "react-i18next";
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
@@ -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,7 @@ interface Props
- error?: boolean,
+ disabled?: boolean,
}
function ControlledAutoComplete<
@@ -28,68 +28,88 @@ 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, disabled } = props;
+
+ // set default value if value is null
+ if (!Boolean(isMultiple) && !Boolean(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 (
-
- );
- }}
- 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}
+
+ );
+ }}
+ // renderTags={(tagValue, getTagProps) => {
+ // return tagValue.map((option, index) => (
+ //
+ // ))
+ // }}
+ 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 (
+
+ );
+ }}
+ // renderTags={(tagValue, getTagProps) => {
+ // return tagValue.map((option, index) => (
+ //
+ // ))
+ // }}
+ onChange={(event, value) => {
+ field.onChange(value?.id ?? null)
+ }}
+ renderInput={(params) => }
+ />)
+ }}
/>
)
}
diff --git a/src/components/CreateProject/CreateProject.tsx b/src/components/CreateProject/CreateProject.tsx
index 937bac5..d115f3a 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 = ({
{
= ({
projectCategories={projectCategories}
teamLeads={teamLeads}
isActive={tabIndex === 0}
+ isEditMode={isEditMode}
/>
}
{
diff --git a/src/components/CreateProject/CreateProjectWrapper.tsx b/src/components/CreateProject/CreateProjectWrapper.tsx
index 6ce6242..3d70225 100644
--- a/src/components/CreateProject/CreateProjectWrapper.tsx
+++ b/src/components/CreateProject/CreateProjectWrapper.tsx
@@ -1,6 +1,7 @@
import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks";
import CreateProject from "./CreateProject";
import {
+ fetchMainProjects,
fetchProjectBuildingTypes,
fetchProjectCategories,
fetchProjectContractTypes,
@@ -14,10 +15,14 @@ import { fetchStaff, fetchTeamLeads } from "@/app/api/staff";
import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer";
import { fetchGrades } from "@/app/api/grades";
-type CreateProjectProps = { isEditMode: false };
+type CreateProjectProps = {
+ isEditMode: false;
+ isSubProject?: boolean;
+};
interface EditProjectProps {
isEditMode: true;
projectId?: string;
+ isSubProject?: boolean;
}
type Props = CreateProjectProps | EditProjectProps;
@@ -59,9 +64,14 @@ const CreateProjectWrapper: 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/Milestone.tsx b/src/components/CreateProject/Milestone.tsx
index c91c0fa..f2e62ce 100644
--- a/src/components/CreateProject/Milestone.tsx
+++ b/src/components/CreateProject/Milestone.tsx
@@ -4,16 +4,18 @@ import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import { useTranslation } from "react-i18next";
import Button from "@mui/material/Button";
-import React, { useCallback, useEffect, useMemo, useState } from "react";
+import React, { SyntheticEvent, useCallback, useEffect, useMemo, useState } from "react";
import CardActions from "@mui/material/CardActions";
import RestartAlt from "@mui/icons-material/RestartAlt";
import {
Alert,
+ Autocomplete,
FormControl,
InputLabel,
MenuItem,
Select,
SelectChangeEvent,
+ TextField,
} from "@mui/material";
import { Task, TaskGroup } from "@/app/api/tasks";
import uniqBy from "lodash/uniqBy";
@@ -49,8 +51,8 @@ const Milestone: React.FC = ({ allTasks, isActive }) => {
taskGroups[0].id,
);
const onSelectTaskGroup = useCallback(
- (event: SelectChangeEvent) => {
- const id = event.target.value;
+ (event: SyntheticEvent, value: NonNullable) => {
+ const id = value.id;
const newTaksGroupId = typeof id === "string" ? parseInt(id) : id;
setCurrentTaskGroupId(newTaksGroupId);
},
@@ -81,7 +83,7 @@ const Milestone: React.FC = ({ allTasks, isActive }) => {
}
// console.log(Object.keys(milestones).reduce((acc, key) => acc + milestones[parseFloat(key)].payments.reduce((acc2, value) => acc2 + value.amount, 0), 0))
if (hasError) {
- setError("milestones", {message: "milestones is not valid", type: "invalid"})
+ setError("milestones", { message: "milestones is not valid", type: "invalid" })
} else {
clearErrors("milestones")
}
@@ -92,26 +94,32 @@ const Milestone: React.FC = ({ allTasks, isActive }) => {
- {t("Task Stage")}
-
+ renderInput={(params) => }
+ />
{/* MUI X-Grid will throw an error if it is rendered without any dimensions; so not rendering when not active */}
{isActive && }
-
+ {/*
}>
{t("Reset")}
-
+ */}
diff --git a/src/components/CreateProject/ProjectClientDetails.tsx b/src/components/CreateProject/ProjectClientDetails.tsx
index d77defe..9e4ac7f 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,9 @@ import ControlledAutoComplete from "../ControlledAutoComplete/ControlledAutoComp
interface Props {
isActive: boolean;
+ isSubProject: boolean;
+ isEditMode: boolean;
+ mainProjects?: MainProject[];
projectCategories: ProjectCategory[];
teamLeads: StaffResult[];
allCustomers: Customer[];
@@ -51,6 +55,9 @@ interface Props {
const ProjectClientDetails: React.FC = ({
isActive,
+ isSubProject,
+ isEditMode,
+ mainProjects,
projectCategories,
teamLeads,
allCustomers,
@@ -70,6 +77,8 @@ const ProjectClientDetails: React.FC = ({
control,
setValue,
getValues,
+ reset,
+ resetField,
} = useFormContext();
const subsidiaryMap = useMemo<{
@@ -103,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 }) => {
@@ -111,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)
});
@@ -123,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)
}
@@ -136,7 +146,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 +153,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 && !isEditMode) {
+ 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, isEditMode])
+
// const buildingTypeIdNameMap = buildingTypes.reduce<{ [id: number]: string }>(
// (acc, building) => ({ ...acc, [building.id]: building.name }),
// {},
@@ -162,6 +197,19 @@ 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")}
+ disabled={isEditMode}
+ />
+
+ >
+ }
= ({
{t("CLP Project")}
@@ -317,7 +365,6 @@ const ProjectClientDetails: React.FC = ({
rules={{
required: "Please select a client"
}}
- error={Boolean(errors.clientId)}
/>
@@ -337,7 +384,7 @@ const ProjectClientDetails: React.FC = ({
subsidiaryMap[subId])
.map((subsidiaryId, index) => {
const subsidiary = subsidiaryMap[subsidiaryId]
@@ -368,7 +415,6 @@ const ProjectClientDetails: React.FC = ({
} else return true;
},
}}
- error={Boolean(errors.clientContactId)}
/>
@@ -396,11 +442,11 @@ const ProjectClientDetails: React.FC = ({
)}
-
+ {/*
}>
{t("Reset")}
-
+ */}
);
diff --git a/src/components/CreateProject/StaffAllocation.tsx b/src/components/CreateProject/StaffAllocation.tsx
index bd699ec..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";
@@ -25,6 +25,7 @@ import {
Tab,
Tabs,
SelectChangeEvent,
+ Autocomplete,
} from "@mui/material";
import differenceWith from "lodash/differenceWith";
import intersectionWith from "lodash/intersectionWith";
@@ -159,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 }));
},
[],
);
@@ -238,20 +239,25 @@ const StaffAllocation: React.FC = ({
return (
- {label}
-
+ renderInput={(params) => }
+ />
);
@@ -288,11 +294,11 @@ const StaffAllocation: React.FC = ({
)}
-
+ {/*
} onClick={reset}>
{t("Reset")}
-
+ */}
{/* 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 dac0698..759b05f 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")}
-
-
+ );
+ }}
+ onChange={onSelectTaskTemplate}
+ renderInput={(params) => }
+ />
= ({
allItemsLabel={t("Task Pool")}
selectedItemsLabel={t("Project Task List")}
/>
-
+ {/*
} onClick={onReset}>
{t("Reset")}
-
+ */}
);
diff --git a/src/components/EditStaff/EditStaff.tsx b/src/components/EditStaff/EditStaff.tsx
index 4864504..fbe4323 100644
--- a/src/components/EditStaff/EditStaff.tsx
+++ b/src/components/EditStaff/EditStaff.tsx
@@ -56,12 +56,12 @@ 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,
- salaryId: Staff.salary.id,
+ currentPositionId: Staff.currentPosition?.id,
+ salaryId: Staff.salary.salaryPoint,
employType: Staff.employType,
email: Staff.email,
phone1: Staff.phone1,
@@ -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,12 +188,12 @@ 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,
- salaryId: Staff.salary.id,
+ currentPositionId: Staff.currentPosition?.id,
+ salaryId: Staff.salary.salaryPoint,
employType: Staff.employType,
email: Staff.email,
phone1: Staff.phone1,
@@ -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,
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 58067b9..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,19 +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 {
- rules: passwordRule
-}
+ user: UserResult;
+ rules: passwordRule;
+ auths: auth[];
+}
-const EditUser: React.FC = async ({
- rules
-}) => {
+const EditUser: React.FC = async ({ user, rules, auths }) => {
const { t } = useTranslation();
const formProps = useForm();
const searchParams = useSearchParams();
@@ -46,8 +57,13 @@ const EditUser: React.FC = async ({
const [tabIndex, setTabIndex] = useState(0);
const router = useRouter();
const [serverError, setServerError] = useState("");
- const [data, setData] = useState();
- const [auths, setAuths] = 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) => {
@@ -57,33 +73,27 @@ const EditUser: React.FC = async ({
);
const errors = formProps.formState.errors;
-console.log("asdasd")
- const fetchUserDetail = async () => {
- try {
- // fetch user info
- const userDetail = await fetchUserDetails(id);
- console.log(userDetail);
- const _data = userDetail.data as UserResult;
- console.log(_data);
- setData(_data);
- //fetch user auths
- const authDetail = await fetchAuth("user", id);
- setAuths(authDetail.records)
- const addAuthIds = authDetail.records.filter((item) => item.v === 1).map((item) => item.id).sort((a, b) => a - b);
+ const resetForm = React.useCallback(() => {
+ console.log("triggerred");
+ console.log(addAuthIds);
+ try {
formProps.reset({
- name: _data.username,
- email: _data.email,
- addAuthIds: addAuthIds || []
+ name: user.username,
+ email: user.email,
+ addAuthIds: addAuthIds,
+ removeAuthIds: [],
+ password: "",
});
+ console.log(formProps.formState.defaultValues);
} catch (error) {
console.log(error);
setServerError(t("An error has occurred. Please try again later."));
}
- }
+ }, [auths, user]);
useEffect(() => {
- fetchUserDetail();
+ resetForm();
}, []);
const hasErrorsInTab = (
@@ -92,10 +102,8 @@ console.log("asdasd")
) => {
switch (tabIndex) {
case 0:
- console.log("yolo")
return Object.keys(errors).length > 0;
default:
- console.log("yolo")
false;
}
};
@@ -107,39 +115,50 @@ console.log("asdasd")
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 = {
name: data.name,
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);
@@ -196,12 +215,12 @@ console.log("asdasd")
{tabIndex == 0 && }
- {tabIndex === 1 && }
+ {tabIndex === 1 && }
}
- // onClick={() => console.log("asdasd")}
+ onClick={resetForm}
>
{t("Reset")}
diff --git a/src/components/EditUser/EditUserWrapper.tsx b/src/components/EditUser/EditUserWrapper.tsx
index 9273d4a..f7d77eb 100644
--- a/src/components/EditUser/EditUserWrapper.tsx
+++ b/src/components/EditUser/EditUserWrapper.tsx
@@ -6,20 +6,23 @@ import { useSearchParams } from "next/navigation";
import { fetchTeam, fetchTeamDetail } from "@/app/api/team";
import { fetchStaff } from "@/app/api/staff";
import { fetchPwRules, fetchUser, fetchUserDetail } from "@/app/api/user";
+import { searchParamsProps } from "@/app/utils/fetchUtil";
+import { fetchUserDetails } from "@/app/api/user/actions";
+import { fetchAuth } from "@/app/api/group/actions";
interface SubComponents {
Loading: typeof EditUserLoading;
}
-interface Props {
- // id: number
-}
-const EditUserWrapper: React.FC & SubComponents = async ({
- // id
+const EditUserWrapper: React.FC & SubComponents = async ({
+ searchParams
}) => {
+ const id = parseInt(searchParams.id as string)
const pwRule = await fetchPwRules()
+ const user = await fetchUserDetails(id);
+ const auths = await fetchAuth("user", id);
- return
+ return
};
EditUserWrapper.Loading = EditUserLoading;
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
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],
);
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" && (
-
-
-
-
-
-
- {"-"}
-
-
-
-
-
-
- )}
-
- );
- })}
-
-
- }
- onClick={handleReset}
- >
- {t("Reset")}
-
- }
- onClick={handleDownload}
- >
- {t("Download")}
-
-
-
-
- );
-}
-
-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/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";
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 cd28040..0f342d0 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, FormHelperText } 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;
@@ -50,17 +61,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({
@@ -76,10 +92,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
@@ -87,7 +107,7 @@ function SearchBox({
[criteria]
);
const [inputs, setInputs] = useState(defaultInputs);
-
+
const makeInputChangeHandler = useCallback(
(paramName: T): React.ChangeEventHandler => {
return (e) => {
@@ -96,6 +116,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) => {
@@ -116,7 +145,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") }));
};
}, []);
@@ -181,6 +210,15 @@ function SearchBox({
)}
+ {c.type === "number" && (
+ %}
+ />
+ )}
{c.type === "monthYear" && (
({
) || }
+ startIcon={
+ (formType === "download" && ) ||
+ }
onClick={handleSearch}
>
{(formType === "download" && t("Download")) || t("Search")}
diff --git a/src/components/utils/numberInput.tsx b/src/components/utils/numberInput.tsx
new file mode 100644
index 0000000..2ccaa6b
--- /dev/null
+++ b/src/components/utils/numberInput.tsx
@@ -0,0 +1,192 @@
+import * as React from 'react';
+import {
+ Unstable_NumberInput as BaseNumberInput,
+ NumberInputProps,
+ numberInputClasses,
+} from '@mui/base/Unstable_NumberInput';
+import { styled } from '@mui/system';
+// FocusEvent | PointerEvent | KeyboardEvent
+export const NumberInput = React.forwardRef(function CustomNumberInput(
+ props: NumberInputProps,
+ ref: React.ForwardedRef,
+) {
+ return (
+
+ );
+});
+export const InputAdornment = styled('div')(
+ ({ theme }) => `
+ margin: 8px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ grid-row: 1/3;
+ color: ${theme.palette.mode === 'dark' ? grey[500] : grey[700]};
+ `,
+ );
+
+export default function NumberInputBasic() {
+ const [value, setValue] = React.useState(null);
+ return (
+ setValue(val)}
+ />
+ );
+}
+
+const blue = {
+ 100: '#DAECFF',
+ 200: '#80BFFF',
+ 400: '#3399FF',
+ 500: '#007FFF',
+ 600: '#0072E5',
+};
+
+const grey = {
+ 50: '#F3F6F9',
+ 100: '#E5EAF2',
+ 200: '#DAE2ED',
+ 300: '#C7D0DD',
+ 400: '#B0B8C4',
+ 500: '#9DA8B7',
+ 600: '#6B7A90',
+ 700: '#434D5B',
+ 800: '#303740',
+ 900: '#1C2025',
+};
+
+const StyledInputRoot = styled('div')(
+ ({ theme }) => `
+ font-family: 'IBM Plex Sans', sans-serif;
+ font-weight: 400;
+ border-radius: 8px;
+ color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]};
+ background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'};
+ border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[200]};
+ box-shadow: 0px 2px 2px ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ display: grid;
+ grid-template-columns: 1fr 19px;
+ grid-template-rows: 1fr 1fr;
+ overflow: hidden;
+ column-gap: 8px;
+ padding: 4px;
+
+ &.${numberInputClasses.focused} {
+ border-color: ${blue[400]};
+ box-shadow: 0 0 0 3px ${theme.palette.mode === 'dark' ? blue[600] : blue[200]};
+ }
+
+ &:hover {
+ border-color: ${blue[400]};
+ }
+
+ // firefox
+ &:focus-visible {
+ outline: 0;
+ }
+`,
+);
+
+const StyledInputElement = styled('input')(
+ ({ theme }) => `
+ font-size: 0.875rem;
+ font-family: inherit;
+ font-weight: 400;
+ line-height: 1.5;
+ grid-column: 1/2;
+ grid-row: 1/3;
+ color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]};
+ background: inherit;
+ border: none;
+ border-radius: inherit;
+ padding: 8px 12px;
+ outline: 0;
+`,
+);
+
+const StyledButton = styled('button')(
+ ({ theme }) => `
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: center;
+ align-items: center;
+ appearance: none;
+ padding: 0;
+ width: 19px;
+ height: 19px;
+ font-family: system-ui, sans-serif;
+ font-size: 0.875rem;
+ line-height: 1;
+ box-sizing: border-box;
+ background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'};
+ border: 0;
+ color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]};
+ transition-property: all;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 120ms;
+
+ &:hover {
+ background: ${theme.palette.mode === 'dark' ? grey[800] : grey[50]};
+ border-color: ${theme.palette.mode === 'dark' ? grey[600] : grey[300]};
+ cursor: pointer;
+ }
+
+ &.${numberInputClasses.incrementButton} {
+ grid-column: 2/3;
+ grid-row: 1/2;
+ border-top-left-radius: 4px;
+ border-top-right-radius: 4px;
+ border: 1px solid;
+ border-bottom: 0;
+ &:hover {
+ cursor: pointer;
+ background: ${blue[400]};
+ color: ${grey[50]};
+ }
+
+ border-color: ${theme.palette.mode === 'dark' ? grey[800] : grey[200]};
+ background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ color: ${theme.palette.mode === 'dark' ? grey[200] : grey[900]};
+ }
+
+ &.${numberInputClasses.decrementButton} {
+ grid-column: 2/3;
+ grid-row: 2/3;
+ border-bottom-left-radius: 4px;
+ border-bottom-right-radius: 4px;
+ border: 1px solid;
+ &:hover {
+ cursor: pointer;
+ background: ${blue[400]};
+ color: ${grey[50]};
+ }
+
+ border-color: ${theme.palette.mode === 'dark' ? grey[800] : grey[200]};
+ background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ color: ${theme.palette.mode === 'dark' ? grey[200] : grey[900]};
+ }
+ & .arrow {
+ transform: translateY(-1px);
+ }
+`,
+);
\ No newline at end of file
diff --git a/src/theme/colorConst.js b/src/theme/colorConst.js
index 0db2053..8055843 100644
--- a/src/theme/colorConst.js
+++ b/src/theme/colorConst.js
@@ -1,5 +1,11 @@
import { createTheme } from "@mui/material";
import { aborted } from "util";
+import { styled } from '@mui/system';
+import {
+ Unstable_NumberInput as BaseNumberInput,
+ NumberInputProps,
+ numberInputClasses,
+} from '@mui/base/Unstable_NumberInput';
// - - - - - - WORK IN PROGRESS - - - - - - //
@@ -415,3 +421,150 @@ export const TSMS_LONG_BUTTON_THEME = createTheme({
},
},
});
+
+export default function NumberInputBasic() {
+ const [value, setValue] = React.useState(null);
+ return (
+ setValue(val)}
+ />
+ );
+}
+const blue = {
+ 100: '#DAECFF',
+ 200: '#80BFFF',
+ 400: '#3399FF',
+ 500: '#007FFF',
+ 600: '#0072E5',
+};
+
+const grey = {
+ 50: '#F3F6F9',
+ 100: '#E5EAF2',
+ 200: '#DAE2ED',
+ 300: '#C7D0DD',
+ 400: '#B0B8C4',
+ 500: '#9DA8B7',
+ 600: '#6B7A90',
+ 700: '#434D5B',
+ 800: '#303740',
+ 900: '#1C2025',
+};
+export const StyledInputRoot = styled('div')(
+ ({ theme }) => `
+ font-family: 'IBM Plex Sans', sans-serif;
+ font-weight: 400;
+ border-radius: 8px;
+ color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]};
+ background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'};
+ border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[200]};
+ box-shadow: 0px 2px 2px ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ display: grid;
+ grid-template-columns: 1fr 19px;
+ grid-template-rows: 1fr 1fr;
+ overflow: hidden;
+ column-gap: 8px;
+ padding: 4px;
+
+ &.${numberInputClasses.focused} {
+ border-color: ${blue[400]};
+ box-shadow: 0 0 0 3px ${theme.palette.mode === 'dark' ? blue[600] : blue[200]};
+ }
+
+ &:hover {
+ border-color: ${blue[400]};
+ }
+
+ // firefox
+ &:focus-visible {
+ outline: 0;
+ }
+`,
+);
+
+export const StyledInputElement = styled('input')(
+ ({ theme }) => `
+ font-size: 0.875rem;
+ font-family: inherit;
+ font-weight: 400;
+ line-height: 1.5;
+ grid-column: 1/2;
+ grid-row: 1/3;
+ color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]};
+ background: inherit;
+ border: none;
+ border-radius: inherit;
+ padding: 8px 12px;
+ outline: 0;
+`,
+);
+
+export const StyledButton = styled('button')(
+ ({ theme }) => `
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: center;
+ align-items: center;
+ appearance: none;
+ padding: 0;
+ width: 19px;
+ height: 19px;
+ font-family: system-ui, sans-serif;
+ font-size: 0.875rem;
+ line-height: 1;
+ box-sizing: border-box;
+ background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'};
+ border: 0;
+ color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]};
+ transition-property: all;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 120ms;
+
+ &:hover {
+ background: ${theme.palette.mode === 'dark' ? grey[800] : grey[50]};
+ border-color: ${theme.palette.mode === 'dark' ? grey[600] : grey[300]};
+ cursor: pointer;
+ }
+
+ &.${numberInputClasses.incrementButton} {
+ grid-column: 2/3;
+ grid-row: 1/2;
+ border-top-left-radius: 4px;
+ border-top-right-radius: 4px;
+ border: 1px solid;
+ border-bottom: 0;
+ &:hover {
+ cursor: pointer;
+ background: ${blue[400]};
+ color: ${grey[50]};
+ }
+
+ border-color: ${theme.palette.mode === 'dark' ? grey[800] : grey[200]};
+ background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ color: ${theme.palette.mode === 'dark' ? grey[200] : grey[900]};
+ }
+
+ &.${numberInputClasses.decrementButton} {
+ grid-column: 2/3;
+ grid-row: 2/3;
+ border-bottom-left-radius: 4px;
+ border-bottom-right-radius: 4px;
+ border: 1px solid;
+ &:hover {
+ cursor: pointer;
+ background: ${blue[400]};
+ color: ${grey[50]};
+ }
+
+ border-color: ${theme.palette.mode === 'dark' ? grey[800] : grey[200]};
+ background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ color: ${theme.palette.mode === 'dark' ? grey[200] : grey[900]};
+ }
+ & .arrow {
+ transform: translateY(-1px);
+ }
+`,
+);
\ No newline at end of file