Bladeren bron

Merge branch 'main' of https://git.2fi-solutions.com/wayne.lee/tsms

# Conflicts:
#	src/app/api/reports/actions.ts
tags/Baseline_30082024_FRONTEND_UAT
MSI\2Fi 1 jaar geleden
bovenliggende
commit
3d3c903ee9
44 gewijzigde bestanden met toevoegingen van 1200 en 844 verwijderingen
  1. +22
    -18
      src/app/(main)/analytics/ResourceOverconsumptionReport/page.tsx
  2. +1
    -1
      src/app/(main)/projects/create/sub/not-found.tsx
  3. +14
    -2
      src/app/(main)/projects/create/sub/page.tsx
  4. +17
    -0
      src/app/(main)/projects/edit/sub/not-found.tsx
  5. +76
    -0
      src/app/(main)/projects/edit/sub/page.tsx
  6. +27
    -34
      src/app/(main)/settings/changepassword/page.tsx
  7. +3
    -7
      src/app/(main)/settings/staff/user/page.tsx
  8. +2
    -2
      src/app/(main)/settings/user/edit/page.tsx
  9. +3
    -2
      src/app/api/projects/actions.ts
  10. +23
    -1
      src/app/api/projects/index.ts
  11. +0
    -42
      src/app/api/report3/index.ts
  12. +14
    -1
      src/app/api/reports/actions.ts
  13. +14
    -0
      src/app/api/reports/index.ts
  14. +2
    -0
      src/components/Breadcrumb/Breadcrumb.tsx
  15. +1
    -8
      src/components/ChangePassword/ChangePasswordForm.tsx
  16. +0
    -4
      src/components/ChangePassword/ChangePasswordWrapper.tsx
  17. +79
    -59
      src/components/ControlledAutoComplete/ControlledAutoComplete.tsx
  18. +11
    -8
      src/components/CreateProject/CreateProject.tsx
  19. +12
    -1
      src/components/CreateProject/CreateProjectWrapper.tsx
  20. +25
    -17
      src/components/CreateProject/Milestone.tsx
  21. +56
    -10
      src/components/CreateProject/ProjectClientDetails.tsx
  22. +22
    -16
      src/components/CreateProject/StaffAllocation.tsx
  23. +24
    -20
      src/components/CreateProject/TaskSetup.tsx
  24. +10
    -10
      src/components/EditStaff/EditStaff.tsx
  25. +10
    -14
      src/components/EditTeam/Allocation.tsx
  26. +100
    -96
      src/components/EditUser/AuthAllocation.tsx
  27. +66
    -47
      src/components/EditUser/EditUser.tsx
  28. +9
    -6
      src/components/EditUser/EditUserWrapper.tsx
  29. +39
    -2
      src/components/EditUser/UserDetail.tsx
  30. +3
    -2
      src/components/ProjectSearch/ProjectSearch.tsx
  31. +0
    -313
      src/components/Report/ReportSearchBox3/SearchBox3.tsx
  32. +0
    -3
      src/components/Report/ReportSearchBox3/index.ts
  33. +0
    -17
      src/components/Report/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx
  34. +0
    -2
      src/components/Report/ResourceOverconsumptionReport/index.ts
  35. +0
    -45
      src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGen.tsx
  36. +0
    -19
      src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenWrapper.tsx
  37. +0
    -2
      src/components/Report/ResourceOverconsumptionReportGen/index.ts
  38. +96
    -0
      src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx
  39. +2
    -2
      src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReportLoading.tsx
  40. +20
    -0
      src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReportWrapper.tsx
  41. +1
    -0
      src/components/ResourceOverconsumptionReport/index.ts
  42. +51
    -11
      src/components/SearchBox/SearchBox.tsx
  43. +192
    -0
      src/components/utils/numberInput.tsx
  44. +153
    -0
      src/theme/colorConst.js

+ 22
- 18
src/app/(main)/analytics/ResourceOverconsumptionReport/page.tsx Bestand weergeven

@@ -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 (
<I18nProvider namespaces={["analytics"]}>
<Typography variant="h4" marginInlineEnd={2}>
Resource Overconsumption Report
</Typography>
{/* <Suspense fallback={<ProgressCashFlowSearch.Loading />}>
<ProgressCashFlowSearch/>
</Suspense> */}
<ResourceOverconsumptionReportComponent />
</I18nProvider>
);
const StaffMonthlyWorkHoursAnalysisReport: React.FC = async () => {
const { t } = await getServerI18n("User Group");

return (
<>
<Typography variant="h4" marginInlineEnd={2}>
{t("Project Resource Overconsumption Report")}
</Typography>
<I18nProvider namespaces={["report", "common"]}>
<Suspense fallback={<ResourceOverconsumptionReport.Loading />}>
<ResourceOverconsumptionReport />
</Suspense>
</I18nProvider>
</>
);
};
export default ResourceOverconsumptionReport;

export default StaffMonthlyWorkHoursAnalysisReport;

+ 1
- 1
src/app/(main)/projects/create/sub/not-found.tsx Bestand weergeven

@@ -8,7 +8,7 @@ export default async function NotFound() {
return (
<Stack spacing={2}>
<Typography variant="h4">{t("Not Found")}</Typography>
<Typography variant="body1">{t("The sub project was not found or there was no any main projects!")}</Typography>
<Typography variant="body1">{t("There was no any main projects!")}</Typography>
<Link href="/projects" component={NextLink} variant="body2">
{t("Return to all projects")}
</Link>


+ 14
- 2
src/app/(main)/projects/create/sub/page.tsx Bestand weergeven

@@ -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 (
<>
<Typography variant="h4">{t("Create Sub Project")}</Typography>
<I18nProvider namespaces={["projects"]}>
<CreateProject isEditMode={false}/>
<CreateProject isEditMode={false} isSubProject={true} />
</I18nProvider>
</>
);


+ 17
- 0
src/app/(main)/projects/edit/sub/not-found.tsx Bestand weergeven

@@ -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 (
<Stack spacing={2}>
<Typography variant="h4">{t("Not Found")}</Typography>
<Typography variant="body1">{t("The sub project was not found!")}</Typography>
<Link href="/projects" component={NextLink} variant="body2">
{t("Return to all projects")}
</Link>
</Stack>
);
}

+ 76
- 0
src/app/(main)/projects/edit/sub/page.tsx Bestand weergeven

@@ -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<Props> = 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 (
<>
<Typography variant="h4">{t("Edit Sub Project")}</Typography>
<I18nProvider namespaces={["projects"]}>
<CreateProject isEditMode isSubProject projectId={projectId}/>
</I18nProvider>
</>
);
};

export default Projects;

+ 27
- 34
src/app/(main)/settings/changepassword/page.tsx Bestand weergeven

@@ -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 (
<>
<Stack
direction="row"
justifyContent="space-between"
flexWrap="wrap"
rowGap={2}
>
<Typography variant="h4" marginInlineEnd={2}>
{t("Change Password")}
</Typography>
</Stack>
<I18nProvider namespaces={["User Group", "common"]}>
<Suspense fallback={<ChangePassword.Loading />}>
<ChangePassword />
</Suspense>
</I18nProvider>
</>
);
};

const ChangePasswordPage: React.FC = async () => {
const { t } = await getServerI18n("User Group");
// preloadTeamLeads();
// preloadStaff();
return (
<>
<Stack
direction="row"
justifyContent="space-between"
flexWrap="wrap"
rowGap={2}
>
<Typography variant="h4" marginInlineEnd={2}>
{t("Change Password")}
</Typography>
</Stack>
{/* <I18nProvider namespaces={["User Group", "common"]}>
<Suspense fallback={<UserGroupSearch.Loading />}>
<UserGroupSearch />
</Suspense>
</I18nProvider> */}
<I18nProvider namespaces={["User Group", "common"]}>
<Suspense fallback={<ChangePassword.Loading />}>
<ChangePassword />
</Suspense>
</I18nProvider>
</>
);
};
export default ChangePasswordPage;
export default ChangePasswordPage;

+ 3
- 7
src/app/(main)/settings/staff/user/page.tsx Bestand weergeven

@@ -7,19 +7,15 @@ import { Suspense } from "react";
import { preloadUser } from "@/app/api/user";
import { searchParamsProps } from "@/app/utils/fetchUtil";

const User: React.FC<searchParamsProps> = async ({
searchParams
}) => {
const User: React.FC<searchParamsProps> = async ({ searchParams }) => {
const { t } = await getServerI18n("user");
preloadUser()
preloadUser();
return (
<>
<Typography variant="h4">{t("Edit User")}</Typography>
<I18nProvider namespaces={["user", "common"]}>
<Suspense fallback={<EditUser.Loading />}>
<EditUser
// id={parseInt(searchParams.id as string)}
/>
<EditUser searchParams={searchParams} />
</Suspense>
</I18nProvider>
</>


+ 2
- 2
src/app/(main)/settings/user/edit/page.tsx Bestand weergeven

@@ -20,10 +20,10 @@ const EditUserPage: React.FC<searchParamsProps> = async ({

return (
<>
<I18nProvider namespaces={["team", "common"]}>
<I18nProvider namespaces={["user", "common"]}>
<Suspense fallback={<EditUser.Loading />}>
<EditUser
// id={id}
searchParams={searchParams}
/>
</Suspense>
</I18nProvider>


+ 3
- 2
src/app/api/projects/actions.ts Bestand weergeven

@@ -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


+ 23
- 1
src/app/api/projects/index.ts Bestand weergeven

@@ -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<ProjectResult[]>(`${BASE_API_URL}/projects/main`, {
return serverFetchJson<MainProject[]>(`${BASE_API_URL}/projects/main`, {
next: { tags: ["projects"] },
});
});


+ 0
- 42
src/app/api/report3/index.ts Bestand weergeven

@@ -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",
},
];

+ 14
- 1
src/app/api/reports/actions.ts Bestand weergeven

@@ -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<FileResponse>(
`${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<FileResponse>(
// `${BASE_API_URL}/reports/downloadLateStartReport`,


+ 14
- 0
src/app/api/reports/index.ts Bestand weergeven

@@ -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;


+ 2
- 0
src/components/Breadcrumb/Breadcrumb.tsx Bestand weergeven

@@ -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",


+ 1
- 8
src/components/ChangePassword/ChangePasswordForm.tsx Bestand weergeven

@@ -33,19 +33,12 @@ const ChagnePasswordForm: React.FC = () => {
setValue,
} = useFormContext<PasswordInputs>();

// const resetGroup = useCallback(() => {
// console.log(defaultValues);
// if (defaultValues !== undefined) {
// resetField("description");
// }
// }, [defaultValues]);

return (
<Card sx={{ display: "block" }}>
<CardContent component={Stack} spacing={4}>
<Box>
<Typography variant="overline" display="block" marginBlockEnd={1}>
{t("Group Info")}
{t("Please Fill in all the Fields")}
</Typography>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6}>


+ 0
- 4
src/components/ChangePassword/ChangePasswordWrapper.tsx Bestand weergeven

@@ -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 <ChangePassword />;
};


+ 79
- 59
src/components/ControlledAutoComplete/ControlledAutoComplete.tsx Bestand weergeven

@@ -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 = <CheckBoxOutlineBlankIcon fontSize="medium" />;
const checkedIcon = <CheckBoxIcon fontSize="medium" />;
// label -> e.g. code - name -> 001 - WL
// name -> WL
interface Props<T extends { id?: number | string; label?: string; name?: string }, TField extends FieldValues> {
interface Props<T extends { id?: number | string | null; label?: string; name?: string }, TField extends FieldValues> {
control: Control<TField>,
options: T[],
name: Path<TField>, // register name
@@ -18,7 +18,7 @@ interface Props<T extends { id?: number | string; label?: string; name?: string
noOptionsText?: string,
isMultiple?: boolean,
rules?: RegisterOptions<FieldValues>
error?: boolean,
disabled?: boolean,
}

function ControlledAutoComplete<
@@ -28,68 +28,88 @@ function ControlledAutoComplete<
props: Props<T, TField>
) {
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 (
<Controller
name={name}
control={control}
rules={rules}
render={({ field }) => (
isMultiple ?
<Autocomplete
multiple
disableClearable
disableCloseOnSelect
disablePortal
noOptionsText={noOptionsText ?? t("No Options")}
value={options.filter(option => {
// 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 (
<li {...params}>
<Checkbox
icon={icon}
checkedIcon={checkedIcon}
checked={selected}
style={{ marginRight: 8 }}
/>
{option.label ?? option.name}
</li>
);
}}
onChange={(event, value) => {
field.onChange(value?.map(v => v.id))
}}
renderInput={(params) => <TextField {...params} error={error} variant="outlined" label={label} />}
/>
:
<Autocomplete
disableClearable
disablePortal
noOptionsText={noOptionsText ?? t("No Options")}
value={options.find(option => 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 (
<MenuItem {...params} key={option.id} value={option.id}>
{option.label ?? option.name}
</MenuItem>
);
}}
onChange={(event, value) => {
field.onChange(value?.id)
}}
renderInput={(params) => <TextField {...params} error={error} variant="outlined" label={label} />}
/>
)}
render={({ field, fieldState, formState }) => {

return (
isMultiple ?
<Autocomplete
multiple
disableClearable
disableCloseOnSelect
// disablePortal
disabled={disabled}
noOptionsText={noOptionsText ?? t("No Options")}
value={options.filter(option => {
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 (
<li {...params} key={option?.id}>
<Checkbox
icon={icon}
checkedIcon={checkedIcon}
checked={selected}
style={{ marginRight: 8 }}
/>
{option.label ?? option.name}
</li>
);
}}
// renderTags={(tagValue, getTagProps) => {
// return tagValue.map((option, index) => (
// <Chip {...getTagProps({ index })} key={option?.id} label={option.label ?? option.name} />
// ))
// }}
onChange={(event, value) => {
field.onChange(value?.map(v => v.id))
}}
renderInput={(params) => <TextField {...params} error={Boolean(formState.errors[name])} variant="outlined" label={label} />}
/>
:
<Autocomplete
disableClearable
// disablePortal
disabled={disabled}
noOptionsText={noOptionsText ?? t("No Options")}
value={options.find(option => 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 (
<MenuItem {...params} key={option?.id} value={option.id}>
{option.label ?? option.name}
</MenuItem>
);
}}
// renderTags={(tagValue, getTagProps) => {
// return tagValue.map((option, index) => (
// <Chip {...getTagProps({ index })} key={option?.id} label={option.label ?? option.name} />
// ))
// }}
onChange={(event, value) => {
field.onChange(value?.id ?? null)
}}
renderInput={(params) => <TextField {...params} error={Boolean(formState.errors[name])} variant="outlined" label={label} />}
/>)
}}
/>
)
}


+ 11
- 8
src/components/CreateProject/CreateProject.tsx Bestand weergeven

@@ -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<Props> = ({
isEditMode,
isSubProject,
mainProjects,
defaultInputs,
allTasks,
projectCategories,
@@ -269,14 +274,9 @@ const CreateProject: React.FC<Props> = ({
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<Props> = ({
</Tabs>
{
<ProjectClientDetails
isSubProject={isSubProject}
mainProjects={mainProjects}
buildingTypes={buildingTypes}
workNatures={workNatures}
contractTypes={contractTypes}
@@ -391,6 +393,7 @@ const CreateProject: React.FC<Props> = ({
projectCategories={projectCategories}
teamLeads={teamLeads}
isActive={tabIndex === 0}
isEditMode={isEditMode}
/>
}
{


+ 12
- 1
src/components/CreateProject/CreateProjectWrapper.tsx Bestand weergeven

@@ -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<Props> = async (props) => {
? await fetchProjectDetails(props.projectId!)
: undefined;

const mainProjects = Boolean(props.isSubProject)
? await fetchMainProjects()
: undefined;

return (
<CreateProject
isEditMode={props.isEditMode}
isSubProject={Boolean(props.isSubProject)}
defaultInputs={projectInfo}
allTasks={tasks}
projectCategories={projectCategories}
@@ -77,6 +87,7 @@ const CreateProjectWrapper: React.FC<Props> = async (props) => {
workNatures={workNatures}
allStaffs={allStaffs}
grades={grades}
mainProjects={mainProjects}
/>
);
};


+ 25
- 17
src/components/CreateProject/Milestone.tsx Bestand weergeven

@@ -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<Props> = ({ allTasks, isActive }) => {
taskGroups[0].id,
);
const onSelectTaskGroup = useCallback(
(event: SelectChangeEvent<TaskGroup["id"]>) => {
const id = event.target.value;
(event: SyntheticEvent<Element, Event>, value: NonNullable<TaskGroup>) => {
const id = value.id;
const newTaksGroupId = typeof id === "string" ? parseInt(id) : id;
setCurrentTaskGroupId(newTaksGroupId);
},
@@ -81,7 +83,7 @@ const Milestone: React.FC<Props> = ({ 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<Props> = ({ allTasks, isActive }) => {
<Card sx={{ display: isActive ? "block" : "none" }}>
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
<FormControl>
<InputLabel>{t("Task Stage")}</InputLabel>
<Select
label={t("Task Stage")}
<Autocomplete
disableClearable
// disablePortal
noOptionsText={t("No Task Stage")}
value={taskGroups.find(taskGroup => taskGroup.id === currentTaskGroupId)}
options={taskGroups}
getOptionLabel={(taskGroup) => taskGroup.name}
isOptionEqualToValue={(option, value) => option.id === value.id}
renderOption={(params, option) => {
return (
<MenuItem {...params} key={option.id} value={option.id}>
{option.name}
</MenuItem>
);
}}
onChange={onSelectTaskGroup}
value={currentTaskGroupId}
>
{taskGroups.map((taskGroup) => (
<MenuItem key={taskGroup.id} value={taskGroup.id}>
{taskGroup.name}
</MenuItem>
))}
</Select>
renderInput={(params) => <TextField {...params} variant="outlined" label={t("Task Stage")} />}
/>
</FormControl>
{/* MUI X-Grid will throw an error if it is rendered without any dimensions; so not rendering when not active */}
{isActive && <MilestoneSection taskGroupId={currentTaskGroupId} />}
<CardActions sx={{ justifyContent: "flex-end" }}>
{/* <CardActions sx={{ justifyContent: "flex-end" }}>
<Button variant="text" startIcon={<RestartAlt />}>
{t("Reset")}
</Button>
</CardActions>
</CardActions> */}
</CardContent>
</Card>
<Card sx={{ display: isActive ? "block" : "none" }}>


+ 56
- 10
src/components/CreateProject/ProjectClientDetails.tsx Bestand weergeven

@@ -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<Props> = ({
isActive,
isSubProject,
isEditMode,
mainProjects,
projectCategories,
teamLeads,
allCustomers,
@@ -70,6 +77,8 @@ const ProjectClientDetails: React.FC<Props> = ({
control,
setValue,
getValues,
reset,
resetField,
} = useFormContext<CreateProjectInputs>();

const subsidiaryMap = useMemo<{
@@ -103,6 +112,7 @@ const ProjectClientDetails: React.FC<Props> = ({
);

// 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<Props> = ({

// 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<Props> = ({
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<Props> = ({
// 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<Props> = ({
}
}, [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<Props> = ({
{t("Project Details")}
</Typography>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
{
isSubProject && mainProjects !== undefined && <><Grid item xs={6}>
<ControlledAutoComplete
control={control}
options={[...mainProjects.map(mainProject => ({ id: mainProject.projectId, label: `${mainProject.projectCode} - ${mainProject.projectName}` }))]}
name="mainProjectId"
label={t("Main Project")}
noOptionsText={t("No Main Project")}
disabled={isEditMode}
/>
</Grid>
<Grid item sx={{ display: { xs: "none", sm: "block" } }} /></>
}
<Grid item xs={6}>
<TextField
label={t("Project Code")}
@@ -283,7 +331,7 @@ const ProjectClientDetails: React.FC<Props> = ({
<Grid item xs={6}>
<Checkbox
{...register("isClpProject")}
defaultChecked={watch("isClpProject")}
checked={Boolean(watch("isClpProject"))}
/>
<Typography variant="overline" display="inline">
{t("CLP Project")}
@@ -317,7 +365,6 @@ const ProjectClientDetails: React.FC<Props> = ({
rules={{
required: "Please select a client"
}}
error={Boolean(errors.clientId)}
/>
</Grid>
<Grid item sx={{ display: { xs: "none", sm: "block" } }} />
@@ -337,7 +384,7 @@ const ProjectClientDetails: React.FC<Props> = ({
<Grid item xs={6}>
<ControlledAutoComplete
control={control}
options={[{ id: undefined, label: t("No Subsidiary") }, ...customerSubsidiaryIds
options={[{ label: t("No Subsidiary") }, ...customerSubsidiaryIds
.filter((subId) => subsidiaryMap[subId])
.map((subsidiaryId, index) => {
const subsidiary = subsidiaryMap[subsidiaryId]
@@ -368,7 +415,6 @@ const ProjectClientDetails: React.FC<Props> = ({
} else return true;
},
}}
error={Boolean(errors.clientContactId)}
/>
</Grid>
<Grid container sx={{ display: { xs: "none", sm: "block" } }} />
@@ -396,11 +442,11 @@ const ProjectClientDetails: React.FC<Props> = ({
)}
</Grid>
</Box>
<CardActions sx={{ justifyContent: "flex-end" }}>
{/* <CardActions sx={{ justifyContent: "flex-end" }}>
<Button variant="text" startIcon={<RestartAlt />}>
{t("Reset")}
</Button>
</CardActions>
</CardActions> */}
</CardContent>
</Card>
);


+ 22
- 16
src/components/CreateProject/StaffAllocation.tsx Bestand weergeven

@@ -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<Props> = ({
}, [columnFilters]);
const [filters, setFilters] = React.useState(defaultFilterValues);
const makeFilterSelect = React.useCallback(
(filter: keyof StaffResult) => (event: SelectChangeEvent<string>) => {
setFilters((f) => ({ ...f, [filter]: event.target.value }));
(filter: keyof StaffResult) => (event: SyntheticEvent<Element, Event>, value: NonNullable<string>) => {
setFilters((f) => ({ ...f, [filter]: value }));
},
[],
);
@@ -238,20 +239,25 @@ const StaffAllocation: React.FC<Props> = ({
return (
<Grid key={`${filter.toString()}-${idx}`} item xs={3}>
<FormControl fullWidth>
<InputLabel size="small">{label}</InputLabel>
<Select
label={label}
<Autocomplete
disableClearable
// disablePortal
size="small"
noOptionsText={t(`No ${label}`)}
value={filters[filter]}
options={["All", ...(filterValues[filter] ?? [])]}
getOptionLabel={(filterValue) => filterValue}
isOptionEqualToValue={(option, value) => option === value}
renderOption={(params, option) => {
return (
<MenuItem {...params} key={option} value={option}>
{option}
</MenuItem>
);
}}
onChange={makeFilterSelect(filter)}
>
<MenuItem value={"All"}>{t("All")}</MenuItem>
{filterValues[filter]?.map((option, index) => (
<MenuItem key={`${option}-${index}`} value={option}>
{option}
</MenuItem>
))}
</Select>
renderInput={(params) => <TextField {...params} variant="outlined" label={t(label)} />}
/>
</FormControl>
</Grid>
);
@@ -288,11 +294,11 @@ const StaffAllocation: React.FC<Props> = ({
)}
</Box>
</Stack>
<CardActions sx={{ justifyContent: "flex-end" }}>
{/* <CardActions sx={{ justifyContent: "flex-end" }}>
<Button variant="text" startIcon={<RestartAlt />} onClick={reset}>
{t("Reset")}
</Button>
</CardActions>
</CardActions> */}
</CardContent>
</Card>
{/* MUI X-Grid will throw an error if it is rendered without any dimensions; so not rendering when not active */}


+ 24
- 20
src/components/CreateProject/TaskSetup.tsx Bestand weergeven

@@ -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<Props> = ({
"All" | number
>(watch("taskTemplateId") ?? "All");
const onSelectTaskTemplate = useCallback(
(e: SelectChangeEvent<number | "All">) => {
if (e.target.value === "All" || isNumber(e.target.value)) {
setSelectedTaskTemplateId(e.target.value);
(event: SyntheticEvent<Element, Event>, 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<Props> = ({
marginBlockEnd={1}
>
<Grid item xs={6}>
<FormControl fullWidth>
<InputLabel>{t("Task List Source")}</InputLabel>
<Select<"All" | number>
label={t("Task List Source")}
value={selectedTaskTemplateId}
onChange={onSelectTaskTemplate}
>
<MenuItem value={"All"}>{t("All tasks")}</MenuItem>
{taskTemplates.map((template, index) => (
<MenuItem key={`${template.id}-${index}`} value={template.id}>
{template.name}
<Autocomplete
disableClearable
// disablePortal
noOptionsText={t("No Task List Source")}
value={taskTemplates.find(taskTemplate => 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 (
<MenuItem {...params} key={option.id} value={option.id}>
{option.name}
</MenuItem>
))}
</Select>
</FormControl>
);
}}
onChange={onSelectTaskTemplate}
renderInput={(params) => <TextField {...params} variant="outlined" label={t("Task List Source")} />}
/>
</Grid>
</Grid>
<TransferList
@@ -203,11 +207,11 @@ const TaskSetup: React.FC<Props> = ({
allItemsLabel={t("Task Pool")}
selectedItemsLabel={t("Project Task List")}
/>
<CardActions sx={{ justifyContent: "flex-end" }}>
{/* <CardActions sx={{ justifyContent: "flex-end" }}>
<Button variant="text" startIcon={<RestartAlt />} onClick={onReset}>
{t("Reset")}
</Button>
</CardActions>
</CardActions> */}
</CardContent>
</Card>
);


+ 10
- 10
src/components/EditStaff/EditStaff.tsx Bestand weergeven

@@ -56,12 +56,12 @@ const EditStaff: React.FC<formProps> = ({ 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<formProps> = ({ 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<formProps> = ({ 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<formProps> = ({ 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,


+ 10
- 14
src/components/EditTeam/Allocation.tsx Bestand weergeven

@@ -60,24 +60,20 @@ const Allocation: React.FC<Props> = ({ allStaffs: staff, teamLead }) => {
return rearrangedStaff.filter((s) => getValues("addStaffIds")?.includes(s.id))
}
);
const [seletedTeamLead, setSeletedTeamLead] = useState<number>();
const [deletedStaffIds, setDeletedStaffIds] = useState<number[]>([]);

// 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<number[]>(
(acc, num, index) => {
if (num === staff.id && index !== 0) {
@@ -171,16 +167,16 @@ const Allocation: React.FC<Props> = ({ 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(() => {


+ 100
- 96
src/components/EditUser/AuthAllocation.tsx Bestand weergeven

@@ -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<Props> = ({ auths }) => {
const { t } = useTranslation();
const searchParams = useSearchParams();
const id = parseInt(searchParams.get("id") || "0");
const {
setValue,
getValues,
formState: { defaultValues },
reset,
resetField,
} = useFormContext<UserInputs>();
const initialAuths = auths.map((u) => ({ ...u })).sort((a, b) => a.id - b.id);
const [filteredAuths, setFilteredAuths] = useState(initialAuths);
const [selectedAuths, setSelectedAuths] = useState<typeof filteredAuths>(
() => {
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<UserInputs>();
const initialAuths = auths.map((u) => ({ ...u })).sort((a, b) => a.id - b.id);
const [filteredAuths, setFilteredAuths] = useState(initialAuths);
const [selectedAuths, setSelectedAuths] = useState<typeof filteredAuths>(
() => {
return filteredAuths.filter(
(s) => getValues("addAuthIds")?.includes(s.id)
);
const [removeAuthIds, setRemoveAuthIds] = useState<number[]>([]);
}
);
const [removeAuthIds, setRemoveAuthIds] = useState<number[]>([]);

// 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<Column<auth>[]>(
() => [
{
@@ -97,8 +101,7 @@ const AuthAllocation: React.FC<Props> = ({ auths }) => {
buttonIcon: <Add />,
},
{ 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<Props> = ({ auths }) => {
label: t("Remove"),
name: "id",
onClick: removeAuth,
buttonIcon: <Remove color="warning"/>,
buttonIcon: <Remove color="warning" />,
},
{ 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<Props> = ({ 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<Props> = ({ auths }) => {

const formProps = useForm({});

// Tab related
const [tabIndex, setTabIndex] = React.useState(0);
const handleTabChange = React.useCallback<NonNullable<TabsProps["onChange"]>>(
(_e, newValue) => {
setTabIndex(newValue);
},
[]
);
// Tab related
const [tabIndex, setTabIndex] = React.useState(0);
const handleTabChange = React.useCallback<NonNullable<TabsProps["onChange"]>>(
(_e, newValue) => {
setTabIndex(newValue);
},
[]
);

return (
return (
<>
<FormProvider {...formProps}>
<Card sx={{ display: "block" }}>
@@ -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 && (
<InputAdornment position="end">
@@ -191,18 +194,20 @@ return (
<Tabs value={tabIndex} onChange={handleTabChange}>
<Tab label={t("Authority Pool")} />
<Tab
label={`${t("Allocated Authority")} (${selectedAuths.length})`}
label={`${t("Allocated Authority")} (${
selectedAuths.length
})`}
/>
</Tabs>
<Box sx={{ marginInline: -3 }}>
{tabIndex === 0 && (
{tabIndex === 0 && (
<SearchResults
noWrapper
items={differenceBy(filteredAuths, selectedAuths, "id")}
columns={AuthPoolColumns}
/>
)}
{tabIndex === 1 && (
{tabIndex === 1 && (
<SearchResults
noWrapper
items={selectedAuths}
@@ -216,6 +221,5 @@ return (
</FormProvider>
</>
);

}
export default AuthAllocation
};
export default AuthAllocation;

+ 66
- 47
src/components/EditUser/EditUser.tsx Bestand weergeven

@@ -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<Props> = async ({
rules
}) => {
const EditUser: React.FC<Props> = async ({ user, rules, auths }) => {
const { t } = useTranslation();
const formProps = useForm<UserInputs>();
const searchParams = useSearchParams();
@@ -46,8 +57,13 @@ const EditUser: React.FC<Props> = async ({
const [tabIndex, setTabIndex] = useState(0);
const router = useRouter();
const [serverError, setServerError] = useState("");
const [data, setData] = useState<UserResult>();
const [auths, setAuths] = useState<auth[]>();
const addAuthIds =
auths && auths.length > 0
? auths
.filter((item) => item.v === 1)
.map((item) => item.id)
.sort((a, b) => a - b)
: [];

const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
(_e, newValue) => {
@@ -57,33 +73,27 @@ const EditUser: React.FC<Props> = 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<SubmitHandler<UserInputs>>(
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")
</Tabs>
</Stack>
{tabIndex == 0 && <UserDetail />}
{tabIndex === 1 && <AuthAllocation auths={auths!}/>}
{tabIndex === 1 && <AuthAllocation auths={auths!} />}
<Stack direction="row" justifyContent="flex-end" gap={1}>
<Button
variant="text"
startIcon={<RestartAlt />}
// onClick={() => console.log("asdasd")}
onClick={resetForm}
>
{t("Reset")}
</Button>


+ 9
- 6
src/components/EditUser/EditUserWrapper.tsx Bestand weergeven

@@ -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<Props> & SubComponents = async ({
// id
const EditUserWrapper: React.FC<searchParamsProps> & 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 <EditUser rules={pwRule} />
return <EditUser user={user.data} rules={pwRule} auths={auths.records}/>
};

EditUserWrapper.Loading = EditUserLoading;


+ 39
- 2
src/components/EditUser/UserDetail.tsx Bestand weergeven

@@ -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
// <br/>
// - Uppercase letters
// <br/>
// - Lowercase letters
// <br/>
// - Numbers
// <br/>
// - Symbols
// </>)
// )
// }
helperText={
Boolean(errors.password) &&
(errors.password?.message
? t(errors.password.message)
: t("Please input correct password"))
}
error={Boolean(errors.password)}
/>
</Grid>
@@ -55,3 +79,16 @@ const UserDetail: React.FC = () => {
};

export default UserDetail;


{/* <>
- 8-20 characters
<br/>
- Uppercase letters
<br/>
- Lowercase letters
<br/>
- Numbers
<br/>
- Symbols
</> */}

+ 3
- 2
src/components/ProjectSearch/ProjectSearch.tsx Bestand weergeven

@@ -20,7 +20,6 @@ type SearchParamNames = keyof SearchQuery;
const ProjectSearch: React.FC<Props> = ({ 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<Props> = ({ 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],
);


+ 0
- 313
src/components/Report/ReportSearchBox3/SearchBox3.tsx Bestand weergeven

@@ -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<T extends string> {
label: string;
label2?: string;
paramName: T;
paramName2?: T;
}

interface TextCriterion<T extends string> extends BaseCriterion<T> {
type: "text";
}

interface SelectCriterion<T extends string> extends BaseCriterion<T> {
type: "select";
options: string[];
}

interface DateRangeCriterion<T extends string> extends BaseCriterion<T> {
type: "dateRange";
}

export type Criterion<T extends string> =
| TextCriterion<T>
| SelectCriterion<T>
| DateRangeCriterion<T>;

interface Props<T extends string> {
criteria: Criterion<T>[];
onSearch: (inputs: Record<T, string>) => void;
onReset?: () => void;
}

function SearchBox<T extends string>({
criteria,
onSearch,
onReset,
}: Props<T>) {
const { t } = useTranslation("common");
const defaultInputs = useMemo(
() =>
criteria.reduce<Record<T, string>>(
(acc, c) => {
return { ...acc, [c.paramName]: c.type === "select" ? "All" : "" };
},
{} as Record<T, string>,
),
[criteria],
);
const [inputs, setInputs] = useState(defaultInputs);

const makeInputChangeHandler = useCallback(
(paramName: T): React.ChangeEventHandler<HTMLInputElement> => {
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 (
<Card>
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
<Typography variant="overline">{t("Search Criteria")}</Typography>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
{criteria.map((c) => {
return (
<Grid key={c.paramName} item xs={6}>
{c.type === "text" && (
<TextField
label={c.label}
fullWidth
onChange={makeInputChangeHandler(c.paramName)}
value={inputs[c.paramName]}
/>
)}
{c.type === "select" && (
<FormControl fullWidth>
<InputLabel>{c.label}</InputLabel>
<Select
label={c.label}
onChange={makeSelectChangeHandler(c.paramName)}
value={inputs[c.paramName]}
>
<MenuItem value={"All"}>{t("All")}</MenuItem>
{c.options.map((option, index) => (
<MenuItem key={`${option}-${index}`} value={option}>
{option}
</MenuItem>
))}
</Select>
</FormControl>
)}
{c.type === "dateRange" && (
<LocalizationProvider
dateAdapter={AdapterDayjs}
// TODO: Should maybe use a custom adapterLocale here to support YYYY-MM-DD
adapterLocale="zh-hk"
>
<Box display="flex">
<FormControl fullWidth>
<DatePicker
label={c.label}
onChange={makeDateChangeHandler(c.paramName)}
value={inputs[c.paramName] ? dayjs(inputs[c.paramName]) : null}
/>
</FormControl>
<Box
display="flex"
alignItems="center"
justifyContent="center"
marginInline={2}
>
{"-"}
</Box>
<FormControl fullWidth>
<DatePicker
label={c.label2}
onChange={makeDateToChangeHandler(c.paramName)}
value={inputs[c.paramName.concat("To") as T] ? dayjs(inputs[c.paramName.concat("To") as T]) : null}
/>
</FormControl>
</Box>
</LocalizationProvider>
)}
</Grid>
);
})}
</Grid>
<CardActions sx={{ justifyContent: "flex-end" }}>
<Button
variant="text"
startIcon={<RestartAlt />}
onClick={handleReset}
>
{t("Reset")}
</Button>
<Button
variant="outlined"
startIcon={<Search />}
onClick={handleDownload}
>
{t("Download")}
</Button>
</CardActions>
</CardContent>
</Card>
);
}

export default SearchBox;

+ 0
- 3
src/components/Report/ReportSearchBox3/index.ts Bestand weergeven

@@ -1,3 +0,0 @@
//src\components\SearchBox\index.ts
export { default } from "./SearchBox3";
export type { Criterion } from "./SearchBox3";

+ 0
- 17
src/components/Report/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx Bestand weergeven

@@ -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 (
<Suspense fallback={<ResourceOverconsumptionReportGen.Loading />}>
<ResourceOverconsumptionReportGen />
</Suspense>
);
};

export default ResourceOverconsumptionReport;

+ 0
- 2
src/components/Report/ResourceOverconsumptionReport/index.ts Bestand weergeven

@@ -1,2 +0,0 @@
//src\components\LateStartReport\index.ts
export { default } from "./ResourceOverconsumptionReport";

+ 0
- 45
src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGen.tsx Bestand weergeven

@@ -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<Omit<ResourceOverconsumption, "id">>;
type SearchParamNames = keyof SearchQuery;

const ProgressByClientSearch: React.FC<Props> = ({ projects }) => {
const { t } = useTranslation("projects");

const searchCriteria: Criterion<SearchParamNames>[] = 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 (
<>
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
console.log(query);
}}
/>
{/* <DownloadReportButton /> */}
</>
);
};

export default ProgressByClientSearch;

+ 0
- 19
src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenWrapper.tsx Bestand weergeven

@@ -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 <ResourceOvercomsumptionReportGen projects={clentprojects} />;
};

ResourceOvercomsumptionReportGenWrapper.Loading = ResourceOvercomsumptionReportGenLoading;

export default ResourceOvercomsumptionReportGenWrapper;

+ 0
- 2
src/components/Report/ResourceOverconsumptionReportGen/index.ts Bestand weergeven

@@ -1,2 +0,0 @@
//src\components\DelayReportGen\index.ts
export { default } from "./ResourceOverconsumptionReportGenWrapper";

+ 96
- 0
src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx Bestand weergeven

@@ -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<Omit<ProjectResourceOverconsumptionReportFilter, "id">>;
type SearchParamNames = keyof SearchQuery;

const ResourceOverconsumptionReport: React.FC<Props> = ({ 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<SearchParamNames>[] = 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 (
<>
<SearchBox
criteria={searchCriteria}
onSearch={async (query: any) => {
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

src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenLoading.tsx → src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReportLoading.tsx Bestand weergeven

@@ -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 (
<>
<Card>
@@ -38,4 +38,4 @@ export const ResourceOvercomsumptionReportGenLoading: React.FC = () => {
);
};

export default ResourceOvercomsumptionReportGenLoading;
export default ResourceOvercomsumptionReportLoading;

+ 20
- 0
src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReportWrapper.tsx Bestand weergeven

@@ -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 <ResourceOverconsumptionReport team={teams} customer={customers}/>;
};

ResourceOvercomsumptionReportWrapper.Loading = ResourceOvercomsumptionReportLoading;

export default ResourceOvercomsumptionReportWrapper;

+ 1
- 0
src/components/ResourceOverconsumptionReport/index.ts Bestand weergeven

@@ -0,0 +1 @@
export { default } from "./ResourceOverconsumptionReportWrapper";

+ 51
- 11
src/components/SearchBox/SearchBox.tsx Bestand weergeven

@@ -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<T extends string> {
label: string;
@@ -50,17 +61,22 @@ interface MonthYearCriterion<T extends string> extends BaseCriterion<T> {
type: "monthYear";
}

interface NumberCriterion<T extends string> extends BaseCriterion<T> {
type: "number";
}

export type Criterion<T extends string> =
| TextCriterion<T>
| SelectCriterion<T>
| DateRangeCriterion<T>
| MonthYearCriterion<T>;
| MonthYearCriterion<T>
| NumberCriterion<T>;

interface Props<T extends string> {
criteria: Criterion<T>[];
onSearch: (inputs: Record<T, string>) => void;
onReset?: () => void;
formType?: String,
formType?: String;
}

function SearchBox<T extends string>({
@@ -76,10 +92,14 @@ function SearchBox<T extends string>({
(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<T, string>
@@ -87,7 +107,7 @@ function SearchBox<T extends string>({
[criteria]
);
const [inputs, setInputs] = useState(defaultInputs);
const makeInputChangeHandler = useCallback(
(paramName: T): React.ChangeEventHandler<HTMLInputElement> => {
return (e) => {
@@ -96,6 +116,15 @@ function SearchBox<T extends string>({
},
[]
);
const makeNumberChangeHandler = useCallback(
(paramName: T): (event: FocusEvent<HTMLInputElement, Element> | PointerEvent<Element> | KeyboardEvent<Element>, 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<T extends string>({

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<T extends string>({
</Select>
</FormControl>
)}
{c.type === "number" && (
<NumberInput
// defaultValue={90}
min={50}
max={99}
onChange={makeNumberChangeHandler(c.paramName)}
endAdornment={<InputAdornment>%</InputAdornment>}
/>
)}
{c.type === "monthYear" && (
<LocalizationProvider
dateAdapter={AdapterDayjs}
@@ -260,7 +298,9 @@ function SearchBox<T extends string>({
</Button>
<Button
variant="outlined"
startIcon={(formType === "download" && <FileDownload />) || <Search />}
startIcon={
(formType === "download" && <FileDownload />) || <Search />
}
onClick={handleSearch}
>
{(formType === "download" && t("Download")) || t("Search")}


+ 192
- 0
src/components/utils/numberInput.tsx Bestand weergeven

@@ -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<HTMLInputElement, Element> | PointerEvent<Element> | KeyboardEvent<Element>
export const NumberInput = React.forwardRef(function CustomNumberInput(
props: NumberInputProps,
ref: React.ForwardedRef<HTMLDivElement>,
) {
return (
<BaseNumberInput
slots={{
root: StyledInputRoot,
input: StyledInputElement,
incrementButton: StyledButton,
decrementButton: StyledButton,
}}
slotProps={{
incrementButton: {
children: '▴',
},
decrementButton: {
children: '▾',
},
}}
{...props}
ref={ref}
/>
);
});
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<number | null>(null);
return (
<NumberInput
aria-label="Demo number input"
placeholder="Type a number…"
value={value}
onChange={(event, val) => 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);
}
`,
);

+ 153
- 0
src/theme/colorConst.js Bestand weergeven

@@ -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<number | null>(null);
return (
<NumberInput
aria-label="Demo number input"
placeholder="Type a number…"
value={value}
onChange={(event, val) => 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);
}
`,
);

Laden…
Annuleren
Opslaan