You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

468 regels
17 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 FormControl from "@mui/material/FormControl";
  7. import Grid from "@mui/material/Grid";
  8. import InputLabel from "@mui/material/InputLabel";
  9. import MenuItem from "@mui/material/MenuItem";
  10. import Select from "@mui/material/Select";
  11. import TextField from "@mui/material/TextField";
  12. import Typography from "@mui/material/Typography";
  13. import { useTranslation } from "react-i18next";
  14. import CardActions from "@mui/material/CardActions";
  15. import RestartAlt from "@mui/icons-material/RestartAlt";
  16. import Button from "@mui/material/Button";
  17. import { Controller, useFormContext } from "react-hook-form";
  18. import { CreateProjectInputs } from "@/app/api/projects/actions";
  19. import {
  20. BuildingType,
  21. ContractType,
  22. FundingType,
  23. LocationType,
  24. MainProject,
  25. ProjectCategory,
  26. ServiceType,
  27. WorkNature,
  28. } from "@/app/api/projects";
  29. import { StaffResult } from "@/app/api/staff";
  30. import { Contact, Customer, Subsidiary } 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 { Autocomplete, Checkbox, ListItemText } from "@mui/material";
  35. import uniq from "lodash/uniq";
  36. import ControlledAutoComplete from "../ControlledAutoComplete/ControlledAutoComplete";
  37. interface Props {
  38. isActive: boolean;
  39. isSubProject: boolean;
  40. isEditMode: boolean;
  41. mainProjects?: MainProject[];
  42. projectCategories: ProjectCategory[];
  43. teamLeads: StaffResult[];
  44. allCustomers: Customer[];
  45. allSubsidiaries: Subsidiary[];
  46. serviceTypes: ServiceType[];
  47. contractTypes: ContractType[];
  48. fundingTypes: FundingType[];
  49. locationTypes: LocationType[];
  50. buildingTypes: BuildingType[];
  51. workNatures: WorkNature[];
  52. }
  53. const ProjectClientDetails: React.FC<Props> = ({
  54. isActive,
  55. isSubProject,
  56. isEditMode,
  57. mainProjects,
  58. projectCategories,
  59. teamLeads,
  60. allCustomers,
  61. allSubsidiaries,
  62. serviceTypes,
  63. contractTypes,
  64. fundingTypes,
  65. locationTypes,
  66. buildingTypes,
  67. workNatures,
  68. }) => {
  69. const { t } = useTranslation();
  70. const {
  71. register,
  72. formState: { errors, defaultValues },
  73. watch,
  74. control,
  75. setValue,
  76. getValues,
  77. reset,
  78. resetField,
  79. } = useFormContext<CreateProjectInputs>();
  80. const subsidiaryMap = useMemo<{
  81. [id: Subsidiary["id"]]: Subsidiary;
  82. }>(
  83. () => allSubsidiaries.reduce((acc, sub) => ({ ...acc, [sub.id]: sub }), {}),
  84. [allSubsidiaries],
  85. );
  86. const selectedCustomerId = watch("clientId");
  87. const selectedCustomer = useMemo(
  88. () => allCustomers.find((c) => c.id === selectedCustomerId),
  89. [allCustomers, selectedCustomerId],
  90. );
  91. const [customerContacts, setCustomerContacts] = useState<Contact[]>([]);
  92. const [subsidiaryContacts, setSubsidiaryContacts] = useState<Contact[]>([]);
  93. const [customerSubsidiaryIds, setCustomerSubsidiaryIds] = useState<number[]>(
  94. [],
  95. );
  96. const selectedCustomerContactId = watch("clientContactId");
  97. const selectedCustomerContact = useMemo(
  98. () =>
  99. subsidiaryContacts.length > 0 ?
  100. subsidiaryContacts.find((contact) => contact.id === selectedCustomerContactId)
  101. : customerContacts.find(
  102. (contact) => contact.id === selectedCustomerContactId,
  103. ),
  104. [subsidiaryContacts, customerContacts, selectedCustomerContactId],
  105. );
  106. // get customer (client) contact combo
  107. const clientSubsidiaryId = watch("clientSubsidiaryId")
  108. const [firstCustomerLoaded, setFirstCustomerLoaded] = useState(false)
  109. useEffect(() => {
  110. if (selectedCustomerId !== undefined) {
  111. fetchCustomer(selectedCustomerId).then(({ contacts, subsidiaryIds }) => {
  112. setCustomerContacts(contacts);
  113. setCustomerSubsidiaryIds(subsidiaryIds);
  114. // if (subsidiaryIds.length > 0) setValue("clientSubsidiaryId", subsidiaryIds[0])
  115. // else
  116. if (isEditMode && !firstCustomerLoaded) { setFirstCustomerLoaded(true) }
  117. else if (subsidiaryIds.length > 0) setValue("clientSubsidiaryId", clientSubsidiaryId !== undefined && clientSubsidiaryId !== null ? subsidiaryIds.includes(clientSubsidiaryId) ? clientSubsidiaryId : null : null)
  118. // if (contacts.length > 0) setValue("clientContactId", contacts[0].id)
  119. // else setValue("clientContactId", undefined)
  120. });
  121. }
  122. }, [selectedCustomerId]);
  123. useEffect(() => {
  124. if (Boolean(clientSubsidiaryId)) {
  125. // get subsidiary contact combo
  126. const contacts = allSubsidiaries.find(subsidiary => subsidiary.id === clientSubsidiaryId)?.subsidiaryContacts!!
  127. setSubsidiaryContacts(() => contacts)
  128. setValue("clientContactId", selectedCustomerId === defaultValues?.clientId && Boolean(defaultValues?.clientSubsidiaryId) ? contacts.find(contact => contact.id === defaultValues.clientContactId)?.id ?? contacts[0].id : contacts[0].id)
  129. setValue("isSubsidiaryContact", true)
  130. } else if (customerContacts?.length > 0) {
  131. setSubsidiaryContacts(() => [])
  132. setValue("clientContactId", selectedCustomerId === defaultValues?.clientId && !Boolean(defaultValues?.clientSubsidiaryId) ? customerContacts.find(contact => contact.id === defaultValues.clientContactId)?.id ?? customerContacts[0].id : customerContacts[0].id)
  133. setValue("isSubsidiaryContact", false)
  134. }
  135. }, [customerContacts, clientSubsidiaryId, selectedCustomerId]);
  136. // Automatically add the team lead to the allocated staff list
  137. const selectedTeamLeadId = watch("projectLeadId");
  138. useEffect(() => {
  139. if (selectedTeamLeadId !== undefined) {
  140. const currentStaffIds = getValues("allocatedStaffIds");
  141. const newList = uniq([...currentStaffIds, selectedTeamLeadId]);
  142. setValue("allocatedStaffIds", newList);
  143. }
  144. }, [getValues, selectedTeamLeadId, setValue]);
  145. // Automatically update the project & client details whene select a main project
  146. const mainProjectId = watch("mainProjectId")
  147. useEffect(() => {
  148. if (mainProjectId !== undefined && mainProjects !== undefined && !isEditMode) {
  149. const mainProject = mainProjects.find(project => project.projectId === mainProjectId);
  150. if (mainProject !== undefined) {
  151. setValue("projectName", mainProject.projectName)
  152. setValue("projectCategoryId", mainProject.projectCategoryId)
  153. setValue("projectLeadId", mainProject.projectLeadId)
  154. setValue("serviceTypeId", mainProject.serviceTypeId)
  155. setValue("fundingTypeId", mainProject.fundingTypeId)
  156. setValue("contractTypeId", mainProject.contractTypeId)
  157. setValue("locationId", mainProject.locationId)
  158. setValue("buildingTypeIds", mainProject.buildingTypeIds)
  159. setValue("workNatureIds", mainProject.workNatureIds)
  160. setValue("projectDescription", mainProject.projectDescription)
  161. setValue("expectedProjectFee", mainProject.expectedProjectFee)
  162. setValue("isClpProject", mainProject.isClpProject)
  163. setValue("clientId", mainProject.clientId)
  164. setValue("clientSubsidiaryId", mainProject.clientSubsidiaryId)
  165. setValue("clientContactId", mainProject.clientContactId)
  166. }
  167. }
  168. }, [getValues, mainProjectId, setValue, isEditMode])
  169. // const buildingTypeIdNameMap = buildingTypes.reduce<{ [id: number]: string }>(
  170. // (acc, building) => ({ ...acc, [building.id]: building.name }),
  171. // {},
  172. // );
  173. // const workNatureIdNameMap = workNatures.reduce<{ [id: number]: string }>(
  174. // (acc, wn) => ({ ...acc, [wn.id]: wn.name }),
  175. // {},
  176. // );
  177. return (
  178. <Card sx={{ display: isActive ? "block" : "none" }}>
  179. <CardContent component={Stack} spacing={4}>
  180. <Box>
  181. <Typography variant="overline" display="block" marginBlockEnd={1}>
  182. {t("Project Details")}
  183. </Typography>
  184. <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
  185. {
  186. isSubProject && mainProjects !== undefined && <><Grid item xs={6}>
  187. <ControlledAutoComplete
  188. control={control}
  189. options={[...mainProjects.map(mainProject => ({ id: mainProject.projectId, label: `${mainProject.projectCode} - ${mainProject.projectName}` }))]}
  190. name="mainProjectId"
  191. label={t("Main Project")}
  192. noOptionsText={t("No Main Project")}
  193. disabled={isEditMode}
  194. />
  195. </Grid>
  196. <Grid item sx={{ display: { xs: "none", sm: "block" } }} /></>
  197. }
  198. <Grid item xs={6}>
  199. <TextField
  200. label={t("Project Code")}
  201. fullWidth
  202. disabled
  203. {...register("projectCode",
  204. // {
  205. // required: "Project code required!",
  206. // }
  207. )}
  208. // error={Boolean(errors.projectCode)}
  209. />
  210. </Grid>
  211. <Grid item xs={6}>
  212. <TextField
  213. label={t("Project Name")}
  214. fullWidth
  215. {...register("projectName", {
  216. required: "Project name required!",
  217. })}
  218. error={Boolean(errors.projectName)}
  219. />
  220. </Grid>
  221. <Grid item xs={6}>
  222. <ControlledAutoComplete
  223. control={control}
  224. options={projectCategories}
  225. name="projectCategoryId"
  226. label={t("Project Category")}
  227. noOptionsText={t("No Project Category")}
  228. />
  229. </Grid>
  230. <Grid item xs={6}>
  231. <ControlledAutoComplete
  232. control={control}
  233. options={teamLeads.map((staff) => ({ ...staff, label: `${staff.staffId} - ${staff.name} (${staff.team})` }))}
  234. name="projectLeadId"
  235. label={t("Team Lead")}
  236. noOptionsText={t("No Team Lead")}
  237. />
  238. </Grid>
  239. <Grid item xs={6}>
  240. <ControlledAutoComplete
  241. control={control}
  242. options={serviceTypes}
  243. name="serviceTypeId"
  244. label={t("Service Type")}
  245. noOptionsText={t("No Service Type")}
  246. />
  247. </Grid>
  248. <Grid item xs={6}>
  249. <ControlledAutoComplete
  250. control={control}
  251. options={fundingTypes}
  252. name="fundingTypeId"
  253. label={t("Funding Type")}
  254. noOptionsText={t("No Funding Type")}
  255. />
  256. </Grid>
  257. <Grid item xs={6}>
  258. <ControlledAutoComplete
  259. control={control}
  260. options={contractTypes}
  261. name="contractTypeId"
  262. label={t("Contract Type")}
  263. noOptionsText={t("No Contract Type")}
  264. />
  265. </Grid>
  266. <Grid item xs={6}>
  267. <ControlledAutoComplete
  268. control={control}
  269. options={locationTypes}
  270. name="locationId"
  271. label={t("Location")}
  272. noOptionsText={t("No Location")}
  273. />
  274. </Grid>
  275. <Grid item xs={6}>
  276. <ControlledAutoComplete
  277. control={control}
  278. options={buildingTypes}
  279. name="buildingTypeIds"
  280. label={t("Building Types")}
  281. noOptionsText={t("No Building Types")}
  282. isMultiple
  283. />
  284. </Grid>
  285. <Grid item xs={6}>
  286. <ControlledAutoComplete
  287. control={control}
  288. options={workNatures}
  289. name="workNatureIds"
  290. label={t("Work Nature")}
  291. noOptionsText={t("No Work Nature")}
  292. isMultiple
  293. />
  294. </Grid>
  295. <Grid item xs={6}>
  296. <TextField
  297. label={t("Project Description")}
  298. fullWidth
  299. {...register("projectDescription", {
  300. required: "Please enter a description",
  301. })}
  302. error={Boolean(errors.projectDescription)}
  303. />
  304. </Grid>
  305. <Grid item xs={6}>
  306. <TextField
  307. label={t("Expected Total Project Fee")}
  308. fullWidth
  309. type="number"
  310. inputProps={{ step: "0.01" }}
  311. {...register("expectedProjectFee", { valueAsNumber: true })}
  312. />
  313. </Grid>
  314. <Grid item xs={6}>
  315. <TextField
  316. label={t("Sub-Contract Fee")}
  317. fullWidth
  318. type="number"
  319. inputProps={{ step: "0.01" }}
  320. {...register("subContractFee", { valueAsNumber: true })}
  321. />
  322. </Grid>
  323. <Grid item xs={6}>
  324. <Checkbox
  325. {...register("isClpProject")}
  326. checked={Boolean(watch("isClpProject"))}
  327. />
  328. <Typography variant="overline" display="inline">
  329. {t("CLP Project")}
  330. </Typography>
  331. </Grid>
  332. </Grid>
  333. </Box>
  334. <Box>
  335. <Stack
  336. direction="row"
  337. alignItems="center"
  338. marginBlockEnd={1}
  339. spacing={2}
  340. >
  341. <Typography variant="overline" display="block">
  342. {t("Client Details")}
  343. </Typography>
  344. <Button LinkComponent={Link} href="/settings/customer">
  345. {t("Add or Edit Clients")}
  346. </Button>
  347. </Stack>
  348. <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
  349. <Grid item xs={6}>
  350. <ControlledAutoComplete
  351. control={control}
  352. options={allCustomers.map((customer) => ({ ...customer, label: `${customer.code} - ${customer.name}` }))}
  353. name="clientId"
  354. label={t("Client")}
  355. noOptionsText={t("No Client")}
  356. rules={{
  357. required: "Please select a client"
  358. }}
  359. />
  360. </Grid>
  361. <Grid item sx={{ display: { xs: "none", sm: "block" } }} />
  362. <Grid item xs={6}>
  363. <TextField
  364. label={t("Client Type")}
  365. InputProps={{
  366. readOnly: true,
  367. }}
  368. fullWidth
  369. value={selectedCustomer?.customerType.name || ""}
  370. />
  371. </Grid>
  372. <Grid item sx={{ display: { xs: "none", sm: "block" } }} />
  373. {customerContacts.length > 0 && (
  374. <>
  375. <Grid item xs={6}>
  376. <ControlledAutoComplete
  377. control={control}
  378. options={[{ label: t("No Subsidiary") }, ...customerSubsidiaryIds
  379. .filter((subId) => subsidiaryMap[subId])
  380. .map((subsidiaryId, index) => {
  381. const subsidiary = subsidiaryMap[subsidiaryId]
  382. return { id: subsidiary.id, label: `${subsidiary.code} - ${subsidiary.name}` }
  383. })]}
  384. name="clientSubsidiaryId"
  385. label={t("Client Subsidiary")}
  386. noOptionsText={t("No Client Subsidiary")}
  387. />
  388. </Grid>
  389. <Grid item xs={6}>
  390. <ControlledAutoComplete
  391. control={control}
  392. options={Boolean(watch("clientSubsidiaryId")) ? subsidiaryContacts : customerContacts}
  393. name="clientContactId"
  394. label={t("Client Lead")}
  395. noOptionsText={t("No Client Lead")}
  396. rules={{
  397. validate: (value) => {
  398. if (
  399. (customerContacts.length > 0 && !customerContacts.find(
  400. (contact) => contact.id === value,
  401. )) && (subsidiaryContacts?.length > 0 && !subsidiaryContacts.find(
  402. (contact) => contact.id === value,
  403. ))
  404. ) {
  405. return t("Please provide a valid contact");
  406. } else return true;
  407. },
  408. }}
  409. />
  410. </Grid>
  411. <Grid container sx={{ display: { xs: "none", sm: "block" } }} />
  412. <Grid item xs={6}>
  413. <TextField
  414. label={t("Client Lead Phone Number")}
  415. fullWidth
  416. InputProps={{
  417. readOnly: true,
  418. }}
  419. value={selectedCustomerContact?.phone || ""}
  420. />
  421. </Grid>
  422. <Grid item xs={6}>
  423. <TextField
  424. label={t("Client Lead Email")}
  425. fullWidth
  426. InputProps={{
  427. readOnly: true,
  428. }}
  429. value={selectedCustomerContact?.email || ""}
  430. />
  431. </Grid>
  432. </>
  433. )}
  434. </Grid>
  435. </Box>
  436. {/* <CardActions sx={{ justifyContent: "flex-end" }}>
  437. <Button variant="text" startIcon={<RestartAlt />}>
  438. {t("Reset")}
  439. </Button>
  440. </CardActions> */}
  441. </CardContent>
  442. </Card>
  443. );
  444. };
  445. export default ProjectClientDetails;