|
- "use client";
-
- import Stack from "@mui/material/Stack";
- import Box from "@mui/material/Box";
- import Card from "@mui/material/Card";
- import CardContent from "@mui/material/CardContent";
- import Grid from "@mui/material/Grid";
- import TextField from "@mui/material/TextField";
- import { NumericFormat } from 'react-number-format';
- import Typography from "@mui/material/Typography";
- import { useTranslation } from "react-i18next";
- import Button from "@mui/material/Button";
- import { Controller, useFormContext } from "react-hook-form";
- import { CreateProjectInputs } from "@/app/api/projects/actions";
- import {
- BuildingType,
- ContractType,
- FundingType,
- LocationType,
- MainProject,
- ProjectCategory,
- ServiceType,
- WorkNature,
- } from "@/app/api/projects";
- import { StaffResult } from "@/app/api/staff";
- import {
- Contact,
- Customer,
- CustomerType,
- Subsidiary,
- } from "@/app/api/customer";
- import Link from "next/link";
- import React, { useEffect, useMemo, useState } from "react";
- import { fetchCustomer } from "@/app/api/customer/actions";
- import uniq from "lodash/uniq";
- import ControlledAutoComplete from "../ControlledAutoComplete/ControlledAutoComplete";
- import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
- import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
- import dayjs, { Dayjs } from 'dayjs';
- import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
-
- interface Props {
- isActive: boolean;
- isSubProject: boolean;
- isEditMode: boolean;
- mainProjects?: MainProject[];
- projectCategories: ProjectCategory[];
- teamLeads: StaffResult[];
- allCustomers: Customer[];
- allSubsidiaries: Subsidiary[];
- serviceTypes: ServiceType[];
- contractTypes: ContractType[];
- fundingTypes: FundingType[];
- locationTypes: LocationType[];
- buildingTypes: BuildingType[];
- workNatures: WorkNature[];
- customerTypes: CustomerType[];
- }
-
- const ProjectClientDetails: React.FC<Props> = ({
- isActive,
- isSubProject,
- isEditMode,
- mainProjects,
- projectCategories,
- teamLeads,
- allCustomers,
- allSubsidiaries,
- serviceTypes,
- contractTypes,
- fundingTypes,
- locationTypes,
- buildingTypes,
- customerTypes,
- workNatures,
- }) => {
- const {
- t,
- i18n: { language },
- } = useTranslation();
- const {
- register,
- formState: { errors, defaultValues, touchedFields },
- watch,
- control,
- setValue,
- getValues,
- reset,
- resetField,
- setError,
- clearErrors
- } = useFormContext<CreateProjectInputs>();
-
- const subsidiaryMap = useMemo<{
- [id: Subsidiary["id"]]: Subsidiary;
- }>(
- () => allSubsidiaries.reduce((acc, sub) => ({ ...acc, [sub.id]: sub }), {}),
- [allSubsidiaries],
- );
-
- const selectedCustomerId = watch("clientId");
- const [customerContacts, setCustomerContacts] = useState<Contact[]>([]);
- const [subsidiaryContacts, setSubsidiaryContacts] = useState<Contact[]>([]);
- const [customerSubsidiaryIds, setCustomerSubsidiaryIds] = useState<number[]>(
- [],
- );
-
- const selectedCustomerContactId = watch("clientContactId");
- const selectedCustomerContact = useMemo(
- () =>
- subsidiaryContacts.length > 0
- ? subsidiaryContacts.find(
- (contact) => contact.id === selectedCustomerContactId,
- )
- : customerContacts.find(
- (contact) => contact.id === selectedCustomerContactId,
- ),
- [subsidiaryContacts, customerContacts, selectedCustomerContactId],
- );
-
- // get customer (client) contact combo
- const clientSubsidiaryId = watch("clientSubsidiaryId");
- useEffect(() => {
- if (selectedCustomerId !== undefined) {
- fetchCustomer(selectedCustomerId).then(
- ({ contacts, subsidiaryIds, customer }) => {
- // console.log(contacts)
- // console.log(subsidiaryIds)
- setCustomerContacts(contacts);
- setCustomerSubsidiaryIds(subsidiaryIds);
- setValue(
- "clientTypeId",
- touchedFields["clientTypeId"]
- ? customer.customerType.id
- : defaultValues?.clientTypeId || customer.customerType.id,
- {
- shouldTouch: isEditMode,
- },
- );
- if (subsidiaryIds.length > 0)
- setValue(
- "clientSubsidiaryId",
- clientSubsidiaryId !== undefined && clientSubsidiaryId !== null
- ? subsidiaryIds.includes(clientSubsidiaryId)
- ? clientSubsidiaryId
- : null
- : null,
- );
- // if (contacts.length > 0) setValue("clientContactId", contacts[0].id)
- // else setValue("clientContactId", undefined)
- },
- );
- }
- }, [selectedCustomerId]);
-
- useEffect(() => {
- if (Boolean(clientSubsidiaryId)) {
- // get subsidiary contact combo
- const contacts = allSubsidiaries.find(
- (subsidiary) => subsidiary.id === clientSubsidiaryId,
- )!.subsidiaryContacts;
- 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(() => []);
- 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);
- }
- }, [customerContacts, clientSubsidiaryId, selectedCustomerId]);
-
- // Automatically add the team lead to the allocated staff list
- const selectedTeamLeadId = watch("projectLeadId");
- useEffect(() => {
- if (selectedTeamLeadId !== undefined) {
- const currentStaffIds = getValues("allocatedStaffIds");
- const newList = uniq([...currentStaffIds, selectedTeamLeadId]);
- setValue("allocatedStaffIds", newList);
- }
- }, [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) {
-
- const teamLeadIds = teamLeads.map((teamLead) => teamLead.id)
- setValue("projectName", mainProject.projectName);
- setValue("projectCategoryId", mainProject.projectCategoryId);
-
- // set project lead id to the first team lead id if the main project lead id is not in the team lead list
- setValue("projectLeadId", teamLeadIds.find((id) => id === mainProject.projectLeadId) ? mainProject.projectLeadId : teamLeadIds[0] ?? 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("subContractFee", mainProject.subContractFee);
- 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 }),
- // {},
- // );
-
- // const workNatureIdNameMap = workNatures.reduce<{ [id: number]: string }>(
- // (acc, wn) => ({ ...acc, [wn.id]: wn.name }),
- // {},
- // );
- const planStart = watch("projectPlanStart")
- const planEnd = watch("projectPlanEnd")
-
- useEffect(() => {
- let hasErrors = false
- if(
- !planStart || new Date(planStart) > new Date(planEnd)
- ){
- hasErrors = true;
- }
- if(
- !planEnd || new Date(planStart) > new Date(planEnd)
- ){
- hasErrors = true;
- }
- if(hasErrors){
- setError("projectPlanStart", {
- message: "Project Plan Start date is not valid",
- type: "required",
- });
- setError("projectPlanEnd", {
- message: "Project Plan End date is not valid",
- type: "required",
- });
- }else{
- clearErrors("projectPlanStart")
- clearErrors("projectPlanEnd")
- }
- },[planStart, planEnd])
-
-
- return (
- <Card sx={{ display: isActive ? "block" : "none" }}>
- <CardContent component={Stack} spacing={4}>
- <Box>
- <Typography variant="overline" display="block" marginBlockEnd={1}>
- {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")}
- fullWidth
- // disabled={isSubProject && mainProjects !== undefined}
- {...register("projectCode", {
- required:
- !(isSubProject && mainProjects !== undefined) &&
- "Project code required!",
- })}
- error={Boolean(errors.projectCode)}
- />
- </Grid>
- <Grid item xs={6}>
- <TextField
- label={t("Project Name")}
- fullWidth
- {...register("projectName", {
- required: "Project name required!",
- })}
- error={Boolean(errors.projectName)}
- />
- </Grid>
- <Grid item xs={3}>
- <LocalizationProvider
- dateAdapter={AdapterDayjs}
- adapterLocale={`${language}-hk`}
- >
- <DatePicker
- sx={{ width: "100%" }}
- label={t("Plan Start")}
- format="YYYY/MM/DD"
- value={planStart ? dayjs(planStart) : null}
- onChange={(date) => {
- if (!date) return;
- setValue("projectPlanStart", date.format(INPUT_DATE_FORMAT));
- }}
- slotProps={{
- textField: {
- // required: true,
- error:
- // Boolean(errors.projectPlanStart)
- // ||
- new Date(planStart) > new Date(planEnd)
- || Boolean(errors.projectPlanStart)
- ,
- },
- }}
- />
- </LocalizationProvider>
- </Grid>
- <Grid item xs={3}>
- <LocalizationProvider
- dateAdapter={AdapterDayjs}
- adapterLocale={`${language}-hk`}
- >
- <DatePicker
- sx={{ width: "100%" }}
- label={t("Plan End")}
- format="YYYY/MM/DD"
- value={planEnd ? dayjs(planEnd) : null}
- onChange={(date) => {
- if (!date) return;
- setValue("projectPlanEnd", date.format(INPUT_DATE_FORMAT));
- }}
- slotProps={{
- textField: {
- // required: true,
- error:
- // Boolean(errors.projectPlanEnd)
- // ||
- new Date(planStart) > new Date(planEnd)
- || Boolean(errors.projectPlanEnd)
- ,
- },
- }}
- />
- </LocalizationProvider>
- </Grid>
- <Grid item xs={6}>
- <ControlledAutoComplete
- control={control}
- options={projectCategories}
- name="projectCategoryId"
- label={t("Project Category")}
- noOptionsText={t("No Project Category")}
- />
- </Grid>
- <Grid item xs={6}>
- <ControlledAutoComplete
- control={control}
- options={teamLeads.map((staff) => ({
- ...staff,
- label: `${staff.staffId} - ${staff.name} (${staff.team})`,
- }))}
- name="projectLeadId"
- label={t("Team Lead")}
- noOptionsText={t("No Team Lead")}
- />
- </Grid>
- <Grid item xs={6}>
- <ControlledAutoComplete
- control={control}
- options={serviceTypes}
- name="serviceTypeId"
- label={t("Service Type")}
- noOptionsText={t("No Service Type")}
- />
- </Grid>
- <Grid item xs={6}>
- <ControlledAutoComplete
- control={control}
- options={fundingTypes}
- name="fundingTypeId"
- label={t("Funding Type")}
- noOptionsText={t("No Funding Type")}
- />
- </Grid>
- <Grid item xs={6}>
- <ControlledAutoComplete
- control={control}
- options={contractTypes}
- name="contractTypeId"
- label={t("Contract Type")}
- noOptionsText={t("No Contract Type")}
- />
- </Grid>
- <Grid item xs={6}>
- <ControlledAutoComplete
- control={control}
- options={locationTypes}
- name="locationId"
- label={t("Location")}
- noOptionsText={t("No Location")}
- />
- </Grid>
- <Grid item xs={6}>
- <ControlledAutoComplete
- control={control}
- options={buildingTypes}
- name="buildingTypeIds"
- label={t("Building Types")}
- noOptionsText={t("No Building Types")}
- isMultiple
- />
- </Grid>
-
- <Grid item xs={6}>
- <ControlledAutoComplete
- control={control}
- options={workNatures}
- name="workNatureIds"
- label={t("Work Nature")}
- noOptionsText={t("No Work Nature")}
- isMultiple
- />
- </Grid>
-
- <Grid item xs={6}>
- <TextField
- label={t("Project Description")}
- fullWidth
- {...register("projectDescription", {
- required: "Please enter a description",
- })}
- error={Boolean(errors.projectDescription)}
- />
- </Grid>
- <Grid item xs={6}>
- <Controller
- control={control}
- name="expectedProjectFee"
- render={({ field: { onChange, onBlur, name, value, ref } }) => (
- <NumericFormat
- label={t("Expected Total Project Fee")}
- fullWidth
- prefix="HK$"
- onValueChange={(values) => {
- // console.log(values)
- onChange(values.floatValue)
- }}
- customInput={TextField}
- thousandSeparator
- valueIsNumericString
- decimalScale={2}
- fixedDecimalScale
- name={name}
- value={value}
- onBlur={onBlur}
- inputRef={ref}
- />
- )}
- />
- {/* <TextField
- label={t("Expected Total Project Fee")}
- fullWidth
- type="number"
- inputProps={{
- step: "0.01",
- }}
- {...register("expectedProjectFee", { valueAsNumber: true })}
- /> */}
- </Grid>
-
- <Grid item xs={6}>
- <Controller
- control={control}
- name="subContractFee"
- render={({ field: { onChange, onBlur, name, value, ref } }) => (
- <NumericFormat
- label={t("Sub-Contract Fee")}
- fullWidth
- prefix="HK$"
- onValueChange={(values) => {
- // console.log(values)
- onChange(values.floatValue)
- }}
- customInput={TextField}
- thousandSeparator
- valueIsNumericString
- decimalScale={2}
- fixedDecimalScale
- name={name}
- value={value}
- onBlur={onBlur}
- inputRef={ref}
- />
- )}
- />
- {/* <TextField
- label={t("Sub-Contract Fee")}
- fullWidth
- type="number"
- inputProps={{ step: "0.01" }}
- // InputLabelProps={{
- // shrink: Boolean(watch("subContractFee")),
- // }}
- {...register("subContractFee", { valueAsNumber: true })}
- /> */}
- </Grid>
-
- {/* <Grid item xs={6}>
- <Checkbox
- {...register("isClpProject")}
- checked={Boolean(watch("isClpProject"))}
- disabled={isSubProject && mainProjects !== undefined}
- />
- <Typography variant="overline" display="inline">
- {t("CLP Project")}
- </Typography>
- </Grid> */}
- </Grid>
- </Box>
-
- <Box>
- <Stack
- direction="row"
- alignItems="center"
- marginBlockEnd={1}
- spacing={2}
- >
- <Typography variant="overline" display="block">
- {t("Client Details")}
- </Typography>
- {/* <Button LinkComponent={Link} href="/settings/customer">
- {t("Add or Edit Clients")}
- </Button> */}
- </Stack>
- <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
- <Grid item xs={6}>
- <ControlledAutoComplete
- control={control}
- options={allCustomers.map((customer) => ({
- ...customer,
- label: `${customer.name}`,
- // label: `${customer.code} - ${customer.name}`,
- }))}
- name="clientId"
- label={t("Client")}
- noOptionsText={t("No Client")}
- rules={{
- required: "Please select a client",
- }}
- />
- </Grid>
- {/* <Grid item sx={{ display: { xs: "none", sm: "block" } }} /> */}
- <Grid item xs={6}>
- <ControlledAutoComplete
- control={control}
- options={customerTypes}
- name="clientTypeId"
- label={t("Client Type")}
- noOptionsText={t("No Client Type")}
- rules={{
- required: "Please select a client type",
- }}
- />
- </Grid>
- </Grid>
- <Grid item sx={{ display: { xs: "none", sm: "block" } }} />
- {customerContacts.length > 0 && (
- <Box>
- <Stack
- direction="row"
- alignItems="center"
- marginBlockEnd={1}
- spacing={2}
- >
- <Typography variant="overline" display="block">
- {t("Subsidiary Details")}
- </Typography>
- </Stack>
- <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
- <Grid item xs={6}>
- <ControlledAutoComplete
- control={control}
- options={[
- { label: t("No Subsidiary") },
- ...customerSubsidiaryIds
- .filter((subId) => subsidiaryMap[subId])
- .map((subsidiaryId, index) => {
- const subsidiary = subsidiaryMap[subsidiaryId];
- return {
- id: subsidiary.id,
- label: `${subsidiary.name}`,
- // label: `${subsidiary.code} - ${subsidiary.name}`,
- };
- }),
- ]}
- name="clientSubsidiaryId"
- label={t("Client Subsidiary")}
- noOptionsText={t("No Client Subsidiary")}
- />
- </Grid>
- <Grid item xs={6}>
- <ControlledAutoComplete
- control={control}
- options={
- Boolean(watch("clientSubsidiaryId"))
- ? subsidiaryContacts
- : customerContacts
- }
- name="clientContactId"
- label={t("Client Lead")}
- noOptionsText={t("No Client Lead")}
- rules={{
- validate: (value) => {
- if (
- customerContacts.length > 0 &&
- !customerContacts.find(
- (contact) => contact.id === value,
- ) &&
- subsidiaryContacts?.length > 0 &&
- !subsidiaryContacts.find(
- (contact) => contact.id === value,
- )
- ) {
- return t("Please provide a valid contact");
- } else return true;
- },
- }}
- />
- </Grid>
- <Grid container sx={{ display: { xs: "none", sm: "block" } }} />
- <Grid item xs={6}>
- <TextField
- label={t("Client Lead Phone Number")}
- fullWidth
- InputProps={{
- readOnly: true,
- }}
- value={selectedCustomerContact?.phone || ""}
- />
- </Grid>
- <Grid item xs={6}>
- <TextField
- label={t("Client Lead Email")}
- fullWidth
- InputProps={{
- readOnly: true,
- }}
- value={selectedCustomerContact?.email || ""}
- />
- </Grid>
- </Grid>
- </Box>
- )}
- {/* </Grid> */}
- </Box>
- {/* <CardActions sx={{ justifyContent: "flex-end" }}>
- <Button variant="text" startIcon={<RestartAlt />}>
- {t("Reset")}
- </Button>
- </CardActions> */}
- </CardContent>
- </Card>
- );
- };
-
- export default ProjectClientDetails;
|