Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.
 
 

704 рядки
24 KiB

  1. "use client";
  2. import Stack from "@mui/material/Stack";
  3. import Box from "@mui/material/Box";
  4. import Card from "@mui/material/Card";
  5. import CardContent from "@mui/material/CardContent";
  6. import Grid from "@mui/material/Grid";
  7. import TextField from "@mui/material/TextField";
  8. import { NumericFormat } from 'react-number-format';
  9. import Typography from "@mui/material/Typography";
  10. import { useTranslation } from "react-i18next";
  11. import Button from "@mui/material/Button";
  12. import { Controller, useFormContext } from "react-hook-form";
  13. import { CreateProjectInputs } from "@/app/api/projects/actions";
  14. import {
  15. BuildingType,
  16. ContractType,
  17. FundingType,
  18. LocationType,
  19. MainProject,
  20. ProjectCategory,
  21. ServiceType,
  22. WorkNature,
  23. } from "@/app/api/projects";
  24. import { StaffResult } from "@/app/api/staff";
  25. import {
  26. Contact,
  27. Customer,
  28. CustomerType,
  29. Subsidiary,
  30. } from "@/app/api/customer";
  31. import Link from "next/link";
  32. import React, { useEffect, useMemo, useState } from "react";
  33. import { fetchCustomer } from "@/app/api/customer/actions";
  34. import uniq from "lodash/uniq";
  35. import ControlledAutoComplete from "../ControlledAutoComplete/ControlledAutoComplete";
  36. import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
  37. import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
  38. import dayjs, { Dayjs } from 'dayjs';
  39. import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
  40. interface Props {
  41. isActive: boolean;
  42. isSubProject: boolean;
  43. isEditMode: boolean;
  44. mainProjects?: MainProject[];
  45. projectCategories: ProjectCategory[];
  46. teamLeads: StaffResult[];
  47. allCustomers: Customer[];
  48. allSubsidiaries: Subsidiary[];
  49. serviceTypes: ServiceType[];
  50. contractTypes: ContractType[];
  51. fundingTypes: FundingType[];
  52. locationTypes: LocationType[];
  53. buildingTypes: BuildingType[];
  54. workNatures: WorkNature[];
  55. customerTypes: CustomerType[];
  56. }
  57. const ProjectClientDetails: React.FC<Props> = ({
  58. isActive,
  59. isSubProject,
  60. isEditMode,
  61. mainProjects,
  62. projectCategories,
  63. teamLeads,
  64. allCustomers,
  65. allSubsidiaries,
  66. serviceTypes,
  67. contractTypes,
  68. fundingTypes,
  69. locationTypes,
  70. buildingTypes,
  71. customerTypes,
  72. workNatures,
  73. }) => {
  74. const {
  75. t,
  76. i18n: { language },
  77. } = useTranslation();
  78. const {
  79. register,
  80. formState: { errors, defaultValues, touchedFields },
  81. watch,
  82. control,
  83. setValue,
  84. getValues,
  85. reset,
  86. resetField,
  87. setError,
  88. clearErrors
  89. } = useFormContext<CreateProjectInputs>();
  90. const subsidiaryMap = useMemo<{
  91. [id: Subsidiary["id"]]: Subsidiary;
  92. }>(
  93. () => allSubsidiaries.reduce((acc, sub) => ({ ...acc, [sub.id]: sub }), {}),
  94. [allSubsidiaries],
  95. );
  96. const selectedCustomerId = watch("clientId");
  97. const [customerContacts, setCustomerContacts] = useState<Contact[]>([]);
  98. const [subsidiaryContacts, setSubsidiaryContacts] = useState<Contact[]>([]);
  99. const [customerSubsidiaryIds, setCustomerSubsidiaryIds] = useState<number[]>(
  100. [],
  101. );
  102. const selectedCustomerContactId = watch("clientContactId");
  103. const selectedCustomerContact = useMemo(
  104. () =>
  105. subsidiaryContacts.length > 0
  106. ? subsidiaryContacts.find(
  107. (contact) => contact.id === selectedCustomerContactId,
  108. )
  109. : customerContacts.find(
  110. (contact) => contact.id === selectedCustomerContactId,
  111. ),
  112. [subsidiaryContacts, customerContacts, selectedCustomerContactId],
  113. );
  114. // get customer (client) contact combo
  115. const clientSubsidiaryId = watch("clientSubsidiaryId");
  116. useEffect(() => {
  117. if (selectedCustomerId !== undefined) {
  118. fetchCustomer(selectedCustomerId).then(
  119. ({ contacts, subsidiaryIds, customer }) => {
  120. // console.log(contacts)
  121. // console.log(subsidiaryIds)
  122. setCustomerContacts(contacts);
  123. setCustomerSubsidiaryIds(subsidiaryIds);
  124. setValue(
  125. "clientTypeId",
  126. touchedFields["clientTypeId"]
  127. ? customer.customerType.id
  128. : defaultValues?.clientTypeId || customer.customerType.id,
  129. {
  130. shouldTouch: isEditMode,
  131. },
  132. );
  133. if (subsidiaryIds.length > 0)
  134. setValue(
  135. "clientSubsidiaryId",
  136. clientSubsidiaryId !== undefined && clientSubsidiaryId !== null
  137. ? subsidiaryIds.includes(clientSubsidiaryId)
  138. ? clientSubsidiaryId
  139. : null
  140. : null,
  141. );
  142. // if (contacts.length > 0) setValue("clientContactId", contacts[0].id)
  143. // else setValue("clientContactId", undefined)
  144. },
  145. );
  146. }
  147. }, [selectedCustomerId]);
  148. useEffect(() => {
  149. if (Boolean(clientSubsidiaryId)) {
  150. // get subsidiary contact combo
  151. const contacts = allSubsidiaries.find(
  152. (subsidiary) => subsidiary.id === clientSubsidiaryId,
  153. )!.subsidiaryContacts;
  154. setSubsidiaryContacts(() => contacts);
  155. setValue(
  156. "clientContactId",
  157. selectedCustomerId === defaultValues?.clientId &&
  158. Boolean(defaultValues?.clientSubsidiaryId)
  159. ? contacts.find(
  160. (contact) => contact?.id === defaultValues?.clientContactId,
  161. )?.id ?? contacts[0]?.id
  162. : contacts[0]?.id,
  163. );
  164. setValue("isSubsidiaryContact", true);
  165. } else if (customerContacts?.length > 0) {
  166. setSubsidiaryContacts(() => []);
  167. setValue(
  168. "clientContactId",
  169. selectedCustomerId === defaultValues?.clientId &&
  170. !Boolean(defaultValues?.clientSubsidiaryId)
  171. ? customerContacts.find(
  172. (contact) => contact.id === defaultValues.clientContactId,
  173. )?.id ?? customerContacts[0].id
  174. : customerContacts[0].id,
  175. );
  176. setValue("isSubsidiaryContact", false);
  177. }
  178. }, [customerContacts, clientSubsidiaryId, selectedCustomerId]);
  179. // Automatically add the team lead to the allocated staff list
  180. const selectedTeamLeadId = watch("projectLeadId");
  181. useEffect(() => {
  182. if (selectedTeamLeadId !== undefined) {
  183. const currentStaffIds = getValues("allocatedStaffIds");
  184. const newList = uniq([...currentStaffIds, selectedTeamLeadId]);
  185. setValue("allocatedStaffIds", newList);
  186. }
  187. }, [getValues, selectedTeamLeadId, setValue]);
  188. // Automatically update the project & client details whene select a main project
  189. const mainProjectId = watch("mainProjectId");
  190. useEffect(() => {
  191. if (
  192. mainProjectId !== undefined &&
  193. mainProjects !== undefined &&
  194. !isEditMode
  195. ) {
  196. const mainProject = mainProjects.find(
  197. (project) => project.projectId === mainProjectId,
  198. );
  199. if (mainProject !== undefined) {
  200. const teamLeadIds = teamLeads.map((teamLead) => teamLead.id)
  201. setValue("projectName", mainProject.projectName);
  202. setValue("projectCategoryId", mainProject.projectCategoryId);
  203. // set project lead id to the first team lead id if the main project lead id is not in the team lead list
  204. setValue("projectLeadId", teamLeadIds.find((id) => id === mainProject.projectLeadId) ? mainProject.projectLeadId : teamLeadIds[0] ?? mainProject.projectLeadId);
  205. setValue("serviceTypeId", mainProject.serviceTypeId);
  206. setValue("fundingTypeId", mainProject.fundingTypeId);
  207. setValue("contractTypeId", mainProject.contractTypeId);
  208. setValue("locationId", mainProject.locationId);
  209. setValue("buildingTypeIds", mainProject.buildingTypeIds);
  210. setValue("workNatureIds", mainProject.workNatureIds);
  211. setValue("projectDescription", mainProject.projectDescription);
  212. setValue("expectedProjectFee", mainProject.expectedProjectFee);
  213. setValue("subContractFee", mainProject.subContractFee);
  214. setValue("isClpProject", mainProject.isClpProject);
  215. setValue("clientId", mainProject.clientId);
  216. setValue("clientSubsidiaryId", mainProject.clientSubsidiaryId);
  217. setValue("clientContactId", mainProject.clientContactId);
  218. }
  219. }
  220. }, [getValues, mainProjectId, setValue, isEditMode]);
  221. // const buildingTypeIdNameMap = buildingTypes.reduce<{ [id: number]: string }>(
  222. // (acc, building) => ({ ...acc, [building.id]: building.name }),
  223. // {},
  224. // );
  225. // const workNatureIdNameMap = workNatures.reduce<{ [id: number]: string }>(
  226. // (acc, wn) => ({ ...acc, [wn.id]: wn.name }),
  227. // {},
  228. // );
  229. const planStart = watch("projectPlanStart")
  230. const planEnd = watch("projectPlanEnd")
  231. useEffect(() => {
  232. let hasErrors = false
  233. if(
  234. !planStart || new Date(planStart) > new Date(planEnd)
  235. ){
  236. hasErrors = true;
  237. }
  238. if(
  239. !planEnd || new Date(planStart) > new Date(planEnd)
  240. ){
  241. hasErrors = true;
  242. }
  243. if(hasErrors){
  244. setError("projectPlanStart", {
  245. message: "Project Plan Start date is not valid",
  246. type: "required",
  247. });
  248. setError("projectPlanEnd", {
  249. message: "Project Plan End date is not valid",
  250. type: "required",
  251. });
  252. }else{
  253. clearErrors("projectPlanStart")
  254. clearErrors("projectPlanEnd")
  255. }
  256. },[planStart, planEnd])
  257. return (
  258. <Card sx={{ display: isActive ? "block" : "none" }}>
  259. <CardContent component={Stack} spacing={4}>
  260. <Box>
  261. <Typography variant="overline" display="block" marginBlockEnd={1}>
  262. {t("Project Details")}
  263. </Typography>
  264. <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
  265. {isSubProject && mainProjects !== undefined && (
  266. <>
  267. <Grid item xs={6}>
  268. <ControlledAutoComplete
  269. control={control}
  270. options={[
  271. ...mainProjects.map((mainProject) => ({
  272. id: mainProject.projectId,
  273. label: `${mainProject.projectCode} - ${mainProject.projectName}`,
  274. })),
  275. ]}
  276. name="mainProjectId"
  277. label={t("Main Project")}
  278. noOptionsText={t("No Main Project")}
  279. disabled={isEditMode}
  280. />
  281. </Grid>
  282. <Grid item sx={{ display: { xs: "none", sm: "block" } }} />
  283. </>
  284. )}
  285. <Grid item xs={6}>
  286. <TextField
  287. label={t("Project Code")}
  288. fullWidth
  289. // disabled={isSubProject && mainProjects !== undefined}
  290. {...register("projectCode", {
  291. required:
  292. !(isSubProject && mainProjects !== undefined) &&
  293. "Project code required!",
  294. })}
  295. error={Boolean(errors.projectCode)}
  296. />
  297. </Grid>
  298. <Grid item xs={6}>
  299. <TextField
  300. label={t("Project Name")}
  301. fullWidth
  302. {...register("projectName", {
  303. required: "Project name required!",
  304. })}
  305. error={Boolean(errors.projectName)}
  306. />
  307. </Grid>
  308. <Grid item xs={3}>
  309. <LocalizationProvider
  310. dateAdapter={AdapterDayjs}
  311. adapterLocale={`${language}-hk`}
  312. >
  313. <DatePicker
  314. sx={{ width: "100%" }}
  315. label={t("Plan Start")}
  316. format="YYYY/MM/DD"
  317. value={planStart ? dayjs(planStart) : null}
  318. onChange={(date) => {
  319. if (!date) return;
  320. setValue("projectPlanStart", date.format(INPUT_DATE_FORMAT));
  321. }}
  322. slotProps={{
  323. textField: {
  324. // required: true,
  325. error:
  326. // Boolean(errors.projectPlanStart)
  327. // ||
  328. new Date(planStart) > new Date(planEnd)
  329. || Boolean(errors.projectPlanStart)
  330. ,
  331. },
  332. }}
  333. />
  334. </LocalizationProvider>
  335. </Grid>
  336. <Grid item xs={3}>
  337. <LocalizationProvider
  338. dateAdapter={AdapterDayjs}
  339. adapterLocale={`${language}-hk`}
  340. >
  341. <DatePicker
  342. sx={{ width: "100%" }}
  343. label={t("Plan End")}
  344. format="YYYY/MM/DD"
  345. value={planEnd ? dayjs(planEnd) : null}
  346. onChange={(date) => {
  347. if (!date) return;
  348. setValue("projectPlanEnd", date.format(INPUT_DATE_FORMAT));
  349. }}
  350. slotProps={{
  351. textField: {
  352. // required: true,
  353. error:
  354. // Boolean(errors.projectPlanEnd)
  355. // ||
  356. new Date(planStart) > new Date(planEnd)
  357. || Boolean(errors.projectPlanEnd)
  358. ,
  359. },
  360. }}
  361. />
  362. </LocalizationProvider>
  363. </Grid>
  364. <Grid item xs={6}>
  365. <ControlledAutoComplete
  366. control={control}
  367. options={projectCategories}
  368. name="projectCategoryId"
  369. label={t("Project Category")}
  370. noOptionsText={t("No Project Category")}
  371. />
  372. </Grid>
  373. <Grid item xs={6}>
  374. <ControlledAutoComplete
  375. control={control}
  376. options={teamLeads.map((staff) => ({
  377. ...staff,
  378. label: `${staff.staffId} - ${staff.name} (${staff.team})`,
  379. }))}
  380. name="projectLeadId"
  381. label={t("Team Lead")}
  382. noOptionsText={t("No Team Lead")}
  383. />
  384. </Grid>
  385. <Grid item xs={6}>
  386. <ControlledAutoComplete
  387. control={control}
  388. options={serviceTypes}
  389. name="serviceTypeId"
  390. label={t("Service Type")}
  391. noOptionsText={t("No Service Type")}
  392. />
  393. </Grid>
  394. <Grid item xs={6}>
  395. <ControlledAutoComplete
  396. control={control}
  397. options={fundingTypes}
  398. name="fundingTypeId"
  399. label={t("Funding Type")}
  400. noOptionsText={t("No Funding Type")}
  401. />
  402. </Grid>
  403. <Grid item xs={6}>
  404. <ControlledAutoComplete
  405. control={control}
  406. options={contractTypes}
  407. name="contractTypeId"
  408. label={t("Contract Type")}
  409. noOptionsText={t("No Contract Type")}
  410. />
  411. </Grid>
  412. <Grid item xs={6}>
  413. <ControlledAutoComplete
  414. control={control}
  415. options={locationTypes}
  416. name="locationId"
  417. label={t("Location")}
  418. noOptionsText={t("No Location")}
  419. />
  420. </Grid>
  421. <Grid item xs={6}>
  422. <ControlledAutoComplete
  423. control={control}
  424. options={buildingTypes}
  425. name="buildingTypeIds"
  426. label={t("Building Types")}
  427. noOptionsText={t("No Building Types")}
  428. isMultiple
  429. />
  430. </Grid>
  431. <Grid item xs={6}>
  432. <ControlledAutoComplete
  433. control={control}
  434. options={workNatures}
  435. name="workNatureIds"
  436. label={t("Work Nature")}
  437. noOptionsText={t("No Work Nature")}
  438. isMultiple
  439. />
  440. </Grid>
  441. <Grid item xs={6}>
  442. <TextField
  443. label={t("Project Description")}
  444. fullWidth
  445. {...register("projectDescription", {
  446. required: "Please enter a description",
  447. })}
  448. error={Boolean(errors.projectDescription)}
  449. />
  450. </Grid>
  451. <Grid item xs={6}>
  452. <Controller
  453. control={control}
  454. name="expectedProjectFee"
  455. render={({ field: { onChange, onBlur, name, value, ref } }) => (
  456. <NumericFormat
  457. label={t("Expected Total Project Fee")}
  458. fullWidth
  459. prefix="HK$"
  460. onValueChange={(values) => {
  461. // console.log(values)
  462. onChange(values.floatValue)
  463. }}
  464. customInput={TextField}
  465. thousandSeparator
  466. valueIsNumericString
  467. decimalScale={2}
  468. fixedDecimalScale
  469. name={name}
  470. value={value}
  471. onBlur={onBlur}
  472. inputRef={ref}
  473. />
  474. )}
  475. />
  476. {/* <TextField
  477. label={t("Expected Total Project Fee")}
  478. fullWidth
  479. type="number"
  480. inputProps={{
  481. step: "0.01",
  482. }}
  483. {...register("expectedProjectFee", { valueAsNumber: true })}
  484. /> */}
  485. </Grid>
  486. <Grid item xs={6}>
  487. <Controller
  488. control={control}
  489. name="subContractFee"
  490. render={({ field: { onChange, onBlur, name, value, ref } }) => (
  491. <NumericFormat
  492. label={t("Sub-Contract Fee")}
  493. fullWidth
  494. prefix="HK$"
  495. onValueChange={(values) => {
  496. // console.log(values)
  497. onChange(values.floatValue)
  498. }}
  499. customInput={TextField}
  500. thousandSeparator
  501. valueIsNumericString
  502. decimalScale={2}
  503. fixedDecimalScale
  504. name={name}
  505. value={value}
  506. onBlur={onBlur}
  507. inputRef={ref}
  508. />
  509. )}
  510. />
  511. {/* <TextField
  512. label={t("Sub-Contract Fee")}
  513. fullWidth
  514. type="number"
  515. inputProps={{ step: "0.01" }}
  516. // InputLabelProps={{
  517. // shrink: Boolean(watch("subContractFee")),
  518. // }}
  519. {...register("subContractFee", { valueAsNumber: true })}
  520. /> */}
  521. </Grid>
  522. {/* <Grid item xs={6}>
  523. <Checkbox
  524. {...register("isClpProject")}
  525. checked={Boolean(watch("isClpProject"))}
  526. disabled={isSubProject && mainProjects !== undefined}
  527. />
  528. <Typography variant="overline" display="inline">
  529. {t("CLP Project")}
  530. </Typography>
  531. </Grid> */}
  532. </Grid>
  533. </Box>
  534. <Box>
  535. <Stack
  536. direction="row"
  537. alignItems="center"
  538. marginBlockEnd={1}
  539. spacing={2}
  540. >
  541. <Typography variant="overline" display="block">
  542. {t("Client Details")}
  543. </Typography>
  544. {/* <Button LinkComponent={Link} href="/settings/customer">
  545. {t("Add or Edit Clients")}
  546. </Button> */}
  547. </Stack>
  548. <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
  549. <Grid item xs={6}>
  550. <ControlledAutoComplete
  551. control={control}
  552. options={allCustomers.map((customer) => ({
  553. ...customer,
  554. label: `${customer.name}`,
  555. // label: `${customer.code} - ${customer.name}`,
  556. }))}
  557. name="clientId"
  558. label={t("Client")}
  559. noOptionsText={t("No Client")}
  560. rules={{
  561. required: "Please select a client",
  562. }}
  563. />
  564. </Grid>
  565. {/* <Grid item sx={{ display: { xs: "none", sm: "block" } }} /> */}
  566. <Grid item xs={6}>
  567. <ControlledAutoComplete
  568. control={control}
  569. options={customerTypes}
  570. name="clientTypeId"
  571. label={t("Client Type")}
  572. noOptionsText={t("No Client Type")}
  573. rules={{
  574. required: "Please select a client type",
  575. }}
  576. />
  577. </Grid>
  578. </Grid>
  579. <Grid item sx={{ display: { xs: "none", sm: "block" } }} />
  580. {customerContacts.length > 0 && (
  581. <Box>
  582. <Stack
  583. direction="row"
  584. alignItems="center"
  585. marginBlockEnd={1}
  586. spacing={2}
  587. >
  588. <Typography variant="overline" display="block">
  589. {t("Subsidiary Details")}
  590. </Typography>
  591. </Stack>
  592. <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
  593. <Grid item xs={6}>
  594. <ControlledAutoComplete
  595. control={control}
  596. options={[
  597. { label: t("No Subsidiary") },
  598. ...customerSubsidiaryIds
  599. .filter((subId) => subsidiaryMap[subId])
  600. .map((subsidiaryId, index) => {
  601. const subsidiary = subsidiaryMap[subsidiaryId];
  602. return {
  603. id: subsidiary.id,
  604. label: `${subsidiary.name}`,
  605. // label: `${subsidiary.code} - ${subsidiary.name}`,
  606. };
  607. }),
  608. ]}
  609. name="clientSubsidiaryId"
  610. label={t("Client Subsidiary")}
  611. noOptionsText={t("No Client Subsidiary")}
  612. />
  613. </Grid>
  614. <Grid item xs={6}>
  615. <ControlledAutoComplete
  616. control={control}
  617. options={
  618. Boolean(watch("clientSubsidiaryId"))
  619. ? subsidiaryContacts
  620. : customerContacts
  621. }
  622. name="clientContactId"
  623. label={t("Client Lead")}
  624. noOptionsText={t("No Client Lead")}
  625. rules={{
  626. validate: (value) => {
  627. if (
  628. customerContacts.length > 0 &&
  629. !customerContacts.find(
  630. (contact) => contact.id === value,
  631. ) &&
  632. subsidiaryContacts?.length > 0 &&
  633. !subsidiaryContacts.find(
  634. (contact) => contact.id === value,
  635. )
  636. ) {
  637. return t("Please provide a valid contact");
  638. } else return true;
  639. },
  640. }}
  641. />
  642. </Grid>
  643. <Grid container sx={{ display: { xs: "none", sm: "block" } }} />
  644. <Grid item xs={6}>
  645. <TextField
  646. label={t("Client Lead Phone Number")}
  647. fullWidth
  648. InputProps={{
  649. readOnly: true,
  650. }}
  651. value={selectedCustomerContact?.phone || ""}
  652. />
  653. </Grid>
  654. <Grid item xs={6}>
  655. <TextField
  656. label={t("Client Lead Email")}
  657. fullWidth
  658. InputProps={{
  659. readOnly: true,
  660. }}
  661. value={selectedCustomerContact?.email || ""}
  662. />
  663. </Grid>
  664. </Grid>
  665. </Box>
  666. )}
  667. {/* </Grid> */}
  668. </Box>
  669. {/* <CardActions sx={{ justifyContent: "flex-end" }}>
  670. <Button variant="text" startIcon={<RestartAlt />}>
  671. {t("Reset")}
  672. </Button>
  673. </CardActions> */}
  674. </CardContent>
  675. </Card>
  676. );
  677. };
  678. export default ProjectClientDetails;