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.
 
 

430 lines
13 KiB

  1. "use client";
  2. import DoneIcon from "@mui/icons-material/Done";
  3. import Check from "@mui/icons-material/Check";
  4. import Close from "@mui/icons-material/Close";
  5. import Button from "@mui/material/Button";
  6. import Stack from "@mui/material/Stack";
  7. import Tab from "@mui/material/Tab";
  8. import Tabs, { TabsProps } from "@mui/material/Tabs";
  9. import { useRouter } from "next/navigation";
  10. import React, { useCallback, useState } from "react";
  11. import { useTranslation } from "react-i18next";
  12. import ProjectClientDetails from "./ProjectClientDetails";
  13. import TaskSetup from "./TaskSetup";
  14. import StaffAllocation from "./StaffAllocation";
  15. import Milestone from "./Milestone";
  16. import { Task, TaskTemplate } from "@/app/api/tasks";
  17. import {
  18. FieldErrors,
  19. FormProvider,
  20. SubmitErrorHandler,
  21. SubmitHandler,
  22. useForm,
  23. } from "react-hook-form";
  24. import {
  25. CreateProjectInputs,
  26. deleteProject,
  27. saveProject,
  28. } from "@/app/api/projects/actions";
  29. import { Delete, Error, PlayArrow } from "@mui/icons-material";
  30. import {
  31. BuildingType,
  32. ContractType,
  33. FundingType,
  34. LocationType,
  35. ProjectCategory,
  36. ServiceType,
  37. WorkNature,
  38. } from "@/app/api/projects";
  39. import { StaffResult } from "@/app/api/staff";
  40. import { Typography } from "@mui/material";
  41. import { Grade } from "@/app/api/grades";
  42. import { Customer, Subsidiary } from "@/app/api/customer";
  43. import { isEmpty } from "lodash";
  44. import {
  45. deleteDialog,
  46. errorDialog,
  47. submitDialog,
  48. successDialog,
  49. } from "../Swal/CustomAlerts";
  50. import dayjs from "dayjs";
  51. export interface Props {
  52. isEditMode: boolean;
  53. defaultInputs?: CreateProjectInputs;
  54. allTasks: Task[];
  55. projectCategories: ProjectCategory[];
  56. taskTemplates: TaskTemplate[];
  57. teamLeads: StaffResult[];
  58. allCustomers: Customer[];
  59. allSubsidiaries: Subsidiary[];
  60. fundingTypes: FundingType[];
  61. serviceTypes: ServiceType[];
  62. contractTypes: ContractType[];
  63. locationTypes: LocationType[];
  64. buildingTypes: BuildingType[];
  65. workNatures: WorkNature[];
  66. allStaffs: StaffResult[];
  67. grades: Grade[];
  68. }
  69. const hasErrorsInTab = (
  70. tabIndex: number,
  71. errors: FieldErrors<CreateProjectInputs>,
  72. ) => {
  73. switch (tabIndex) {
  74. case 0:
  75. return (
  76. errors.projectName || errors.projectCode || errors.projectDescription
  77. );
  78. case 2:
  79. return (
  80. errors.totalManhour || errors.manhourPercentageByGrade || errors.taskGroups
  81. );
  82. case 3:
  83. return (
  84. errors.milestones
  85. )
  86. default:
  87. false;
  88. }
  89. };
  90. const CreateProject: React.FC<Props> = ({
  91. isEditMode,
  92. defaultInputs,
  93. allTasks,
  94. projectCategories,
  95. taskTemplates,
  96. teamLeads,
  97. grades,
  98. allCustomers,
  99. allSubsidiaries,
  100. contractTypes,
  101. fundingTypes,
  102. locationTypes,
  103. serviceTypes,
  104. buildingTypes,
  105. workNatures,
  106. allStaffs,
  107. }) => {
  108. const [serverError, setServerError] = useState("");
  109. const [tabIndex, setTabIndex] = useState(0);
  110. const { t } = useTranslation();
  111. const router = useRouter();
  112. const handleCancel = () => {
  113. router.replace("/projects");
  114. };
  115. const handleDelete = () => {
  116. deleteDialog(async () => {
  117. await deleteProject(formProps.getValues("projectId")!);
  118. const clickSuccessDialog = await successDialog("Delete Success", t);
  119. if (clickSuccessDialog) {
  120. router.replace("/projects");
  121. }
  122. }, t);
  123. };
  124. const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
  125. (_e, newValue) => {
  126. setTabIndex(newValue);
  127. },
  128. [],
  129. );
  130. const onSubmit = useCallback<SubmitHandler<CreateProjectInputs>>(
  131. async (data, event) => {
  132. try {
  133. console.log(data);
  134. // detect errors
  135. let hasErrors = false
  136. // Tab - Staff Allocation and Resource
  137. if (data.totalManhour === null || data.totalManhour <= 0) {
  138. formProps.setError("totalManhour", { message: "totalManhour value is not valid", type: "required" })
  139. setTabIndex(2)
  140. hasErrors = true
  141. }
  142. const manhourPercentageByGradeKeys = Object.keys(data.manhourPercentageByGrade)
  143. if (manhourPercentageByGradeKeys.filter(k => data.manhourPercentageByGrade[k as any] < 0).length > 0 ||
  144. manhourPercentageByGradeKeys.reduce((acc, value) => acc + data.manhourPercentageByGrade[value as any], 0) !== 100) {
  145. formProps.setError("manhourPercentageByGrade", { message: "manhourPercentageByGrade value is not valid", type: "invalid" })
  146. setTabIndex(2)
  147. hasErrors = true
  148. }
  149. const taskGroupKeys = Object.keys(data.taskGroups)
  150. if (taskGroupKeys.filter(k => data.taskGroups[k as any].percentAllocation < 0).length > 0 ||
  151. taskGroupKeys.reduce((acc, value) => acc + data.taskGroups[value as any].percentAllocation, 0) !== 100) {
  152. formProps.setError("taskGroups", { message: "Task Groups value is not invalid", type: "invalid" })
  153. setTabIndex(2)
  154. hasErrors = true
  155. }
  156. // Tab - Milestone
  157. let projectTotal = 0
  158. const milestonesKeys = Object.keys(data.milestones)
  159. milestonesKeys.forEach(key => {
  160. const { startDate, endDate, payments } = data.milestones[parseFloat(key)]
  161. if (!Boolean(startDate) || startDate === "Invalid Date" || !Boolean(endDate) || endDate === "Invalid Date" || new Date(startDate) > new Date(endDate)) {
  162. formProps.setError("milestones", {message: "milestones is not valid", type: "invalid"})
  163. setTabIndex(3)
  164. hasErrors = true
  165. }
  166. projectTotal += payments.reduce((acc, payment) => acc + payment.amount, 0)
  167. })
  168. if (projectTotal !== data.expectedProjectFee) {
  169. formProps.setError("milestones", {message: "milestones is not valid", type: "invalid"})
  170. setTabIndex(3)
  171. hasErrors = true
  172. }
  173. if (hasErrors) return false
  174. // save project
  175. setServerError("");
  176. let title = t("Do you want to submit?");
  177. let confirmButtonText = t("Submit");
  178. let successTitle = t("Submit Success");
  179. let errorTitle = t("Submit Fail");
  180. const buttonName = (event?.nativeEvent as any).submitter.name;
  181. if (buttonName === "start") {
  182. title = t("Do you want to start?");
  183. confirmButtonText = t("Start");
  184. successTitle = t("Start Success");
  185. errorTitle = t("Start Fail");
  186. } else if (buttonName === "complete") {
  187. title = t("Do you want to complete?");
  188. confirmButtonText = t("Complete");
  189. successTitle = t("Complete Success");
  190. errorTitle = t("Complete Fail");
  191. }
  192. submitDialog(
  193. async () => {
  194. if (buttonName === "start") {
  195. data.projectActualStart = dayjs().format("YYYY-MM-DD");
  196. } else if (buttonName === "complete") {
  197. data.projectActualEnd = dayjs().format("YYYY-MM-DD");
  198. }
  199. const response = await saveProject(data);
  200. if (response.id > 0) {
  201. successDialog(successTitle, t).then(() => {
  202. router.replace("/projects");
  203. });
  204. } else {
  205. errorDialog(errorTitle, t).then(() => {
  206. return false;
  207. });
  208. }
  209. },
  210. t,
  211. { title: title, confirmButtonText: confirmButtonText },
  212. );
  213. } catch (e) {
  214. setServerError(t("An error has occurred. Please try again later."));
  215. }
  216. },
  217. [router, t],
  218. );
  219. const onSubmitError = useCallback<SubmitErrorHandler<CreateProjectInputs>>(
  220. (errors) => {
  221. console.log(errors)
  222. // Set the tab so that the focus will go there
  223. if (
  224. errors.projectName ||
  225. errors.projectDescription ||
  226. errors.projectCode
  227. ) {
  228. setTabIndex(0);
  229. } else if (errors.totalManhour || errors.manhourPercentageByGrade || errors.taskGroups) {
  230. setTabIndex(2)
  231. } else if (errors.milestones) {
  232. setTabIndex(3)
  233. }
  234. },
  235. [],
  236. );
  237. const formProps = useForm<CreateProjectInputs>({
  238. defaultValues: {
  239. taskGroups: {},
  240. allocatedStaffIds: [],
  241. milestones: {},
  242. totalManhour: 0,
  243. ...defaultInputs,
  244. // manhourPercentageByGrade should have a sensible default
  245. manhourPercentageByGrade: isEmpty(defaultInputs?.manhourPercentageByGrade)
  246. ? grades.reduce((acc, grade) => {
  247. return { ...acc, [grade.id]: 100 / grades.length };
  248. }, {})
  249. : defaultInputs?.manhourPercentageByGrade,
  250. },
  251. });
  252. const errors = formProps.formState.errors;
  253. return (
  254. <>
  255. <FormProvider {...formProps}>
  256. <Stack
  257. spacing={2}
  258. component="form"
  259. onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)}
  260. >
  261. {isEditMode && !(formProps.getValues("projectDeleted") === true) && (
  262. <Stack direction="row" gap={1}>
  263. {!formProps.getValues("projectActualStart") && (
  264. <Button
  265. name="start"
  266. type="submit"
  267. variant="contained"
  268. startIcon={<PlayArrow />}
  269. color="success"
  270. >
  271. {t("Start Project")}
  272. </Button>
  273. )}
  274. {formProps.getValues("projectActualStart") &&
  275. !formProps.getValues("projectActualEnd") && (
  276. <Button
  277. name="complete"
  278. type="submit"
  279. variant="contained"
  280. startIcon={<DoneIcon />}
  281. color="info"
  282. >
  283. {t("Complete Project")}
  284. </Button>
  285. )}
  286. {!(
  287. formProps.getValues("projectActualStart") &&
  288. formProps.getValues("projectActualEnd")
  289. ) && (
  290. <Button
  291. variant="outlined"
  292. startIcon={<Delete />}
  293. color="error"
  294. onClick={handleDelete}
  295. >
  296. {t("Delete Project")}
  297. </Button>
  298. )}
  299. </Stack>
  300. )}
  301. <Tabs
  302. value={tabIndex}
  303. onChange={handleTabChange}
  304. variant="scrollable"
  305. >
  306. <Tab
  307. label={t("Project and Client Details")}
  308. sx={{ marginInlineEnd: !hasErrorsInTab(1, errors) && (hasErrorsInTab(2, errors) || hasErrorsInTab(3, errors)) ? 1 : undefined }}
  309. icon={
  310. hasErrorsInTab(0, errors) ? (
  311. <Error sx={{ marginInlineEnd: 1 }} color="error" />
  312. ) : undefined
  313. }
  314. iconPosition="end"
  315. />
  316. <Tab
  317. label={t("Project Task Setup")}
  318. sx={{ marginInlineEnd: hasErrorsInTab(2, errors) || hasErrorsInTab(3, errors) ? 1 : undefined }}
  319. iconPosition="end" />
  320. <Tab
  321. label={t("Staff Allocation and Resource")}
  322. sx={{ marginInlineEnd: !hasErrorsInTab(2, errors) && hasErrorsInTab(3, errors) ? 1 : undefined }}
  323. icon={
  324. hasErrorsInTab(2, errors) ? (
  325. <Error sx={{ marginInlineEnd: 1 }} color="error" />
  326. ) : undefined
  327. }
  328. iconPosition="end"
  329. />
  330. <Tab label={t("Milestone")}
  331. icon={
  332. hasErrorsInTab(3, errors) ? (
  333. <Error sx={{ marginInlineEnd: 1 }} color="error" />)
  334. : undefined}
  335. iconPosition="end" />
  336. </Tabs>
  337. {
  338. <ProjectClientDetails
  339. buildingTypes={buildingTypes}
  340. workNatures={workNatures}
  341. contractTypes={contractTypes}
  342. fundingTypes={fundingTypes}
  343. locationTypes={locationTypes}
  344. serviceTypes={serviceTypes}
  345. allCustomers={allCustomers}
  346. allSubsidiaries={allSubsidiaries}
  347. projectCategories={projectCategories}
  348. teamLeads={teamLeads}
  349. isActive={tabIndex === 0}
  350. />
  351. }
  352. {
  353. <TaskSetup
  354. allTasks={allTasks}
  355. taskTemplates={taskTemplates}
  356. isActive={tabIndex === 1}
  357. />
  358. }
  359. {
  360. <StaffAllocation
  361. isActive={tabIndex === 2}
  362. allTasks={allTasks}
  363. grades={grades}
  364. allStaffs={allStaffs}
  365. />
  366. }
  367. {<Milestone allTasks={allTasks} isActive={tabIndex === 3} />}
  368. {serverError && (
  369. <Typography variant="body2" color="error" alignSelf="flex-end">
  370. {serverError}
  371. </Typography>
  372. )}
  373. <Stack direction="row" justifyContent="flex-end" gap={1}>
  374. <Button
  375. variant="outlined"
  376. startIcon={<Close />}
  377. onClick={handleCancel}
  378. >
  379. {t("Cancel")}
  380. </Button>
  381. <Button
  382. variant="contained"
  383. startIcon={<Check />}
  384. type="submit"
  385. disabled={
  386. formProps.getValues("projectDeleted") === true ||
  387. (!!formProps.getValues("projectActualStart") &&
  388. !!formProps.getValues("projectActualEnd"))
  389. }
  390. >
  391. {isEditMode ? t("Save") : t("Confirm")}
  392. </Button>
  393. </Stack>
  394. </Stack>
  395. </FormProvider>
  396. </>
  397. );
  398. };
  399. export default CreateProject;