您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 

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