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

773 рядки
23 KiB

  1. "use client";
  2. import AutorenewIcon from "@mui/icons-material/Autorenew";
  3. import DoneIcon from "@mui/icons-material/Done";
  4. import Check from "@mui/icons-material/Check";
  5. import Close from "@mui/icons-material/Close";
  6. import Button, { ButtonProps } from "@mui/material/Button";
  7. import Stack from "@mui/material/Stack";
  8. import Tab from "@mui/material/Tab";
  9. import Tabs, { TabsProps } from "@mui/material/Tabs";
  10. import { useRouter } from "next/navigation";
  11. import React, {
  12. useCallback,
  13. useEffect,
  14. useMemo,
  15. useRef,
  16. useState,
  17. } from "react";
  18. import { useTranslation } from "react-i18next";
  19. import ProjectClientDetails from "./ProjectClientDetails";
  20. import TaskSetup from "./TaskSetup";
  21. import StaffAllocation from "./StaffAllocation";
  22. import Milestone from "./Milestone";
  23. import { Task, TaskTemplate } from "@/app/api/tasks";
  24. import {
  25. FieldErrors,
  26. FormProvider,
  27. SubmitErrorHandler,
  28. SubmitHandler,
  29. useForm,
  30. } from "react-hook-form";
  31. import {
  32. CreateProjectInputs,
  33. deleteProject,
  34. saveProject,
  35. } from "@/app/api/projects/actions";
  36. import { Delete, EditNote, Error, PlayArrow } from "@mui/icons-material";
  37. import {
  38. BuildingType,
  39. ContractType,
  40. FundingType,
  41. LocationType,
  42. MainProject,
  43. ProjectCategory,
  44. ServiceType,
  45. WorkNature,
  46. } from "@/app/api/projects";
  47. import { StaffResult } from "@/app/api/staff";
  48. import { Box, Grid, Typography } from "@mui/material";
  49. import { Grade } from "@/app/api/grades";
  50. import { Customer, CustomerType, Subsidiary } from "@/app/api/customer";
  51. import { isEmpty } from "lodash";
  52. import {
  53. deleteDialog,
  54. errorDialog,
  55. submitDialog,
  56. submitDialogWithWarning,
  57. successDialog,
  58. } from "../Swal/CustomAlerts";
  59. import dayjs from "dayjs";
  60. import { DELETE_PROJECT } from "@/middleware";
  61. import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
  62. import { deleteDraft, loadDraft, saveToLocalStorage } from "@/app/utils/draftUtils";
  63. export interface Props {
  64. isEditMode: boolean;
  65. draftId?: number;
  66. isSubProject: boolean;
  67. mainProjects?: MainProject[];
  68. defaultInputs?: CreateProjectInputs;
  69. allTasks: Task[];
  70. projectCategories: ProjectCategory[];
  71. taskTemplates: TaskTemplate[];
  72. teamLeads: StaffResult[];
  73. allCustomers: Customer[];
  74. allSubsidiaries: Subsidiary[];
  75. fundingTypes: FundingType[];
  76. serviceTypes: ServiceType[];
  77. contractTypes: ContractType[];
  78. locationTypes: LocationType[];
  79. buildingTypes: BuildingType[];
  80. workNatures: WorkNature[];
  81. allStaffs: StaffResult[];
  82. customerTypes: CustomerType[];
  83. grades: Grade[];
  84. abilities: string[];
  85. }
  86. const hasErrorsInTab = (
  87. tabIndex: number,
  88. errors: FieldErrors<CreateProjectInputs>,
  89. ) => {
  90. switch (tabIndex) {
  91. case 0:
  92. return (
  93. errors.projectName ||
  94. errors.projectDescription ||
  95. errors.clientId ||
  96. errors.projectCode ||
  97. errors.projectPlanStart ||
  98. errors.projectPlanEnd
  99. );
  100. case 2:
  101. return (
  102. errors.totalManhour ||
  103. errors.manhourPercentageByGrade ||
  104. errors.taskGroups ||
  105. errors.ratePerManhour
  106. );
  107. case 3:
  108. return errors.milestones;
  109. default:
  110. false;
  111. }
  112. };
  113. const CreateProject: React.FC<Props> = ({
  114. isEditMode,
  115. draftId,
  116. isSubProject,
  117. mainProjects,
  118. defaultInputs,
  119. allTasks,
  120. projectCategories,
  121. taskTemplates,
  122. teamLeads,
  123. grades,
  124. allCustomers,
  125. allSubsidiaries,
  126. contractTypes,
  127. fundingTypes,
  128. locationTypes,
  129. serviceTypes,
  130. buildingTypes,
  131. workNatures,
  132. allStaffs,
  133. customerTypes,
  134. abilities,
  135. }) => {
  136. const [serverError, setServerError] = useState("");
  137. const [tabIndex, setTabIndex] = useState(0);
  138. const { t } = useTranslation();
  139. const router = useRouter();
  140. const formProps = useForm<CreateProjectInputs>({
  141. defaultValues: {
  142. taskGroups: {},
  143. allocatedStaffIds: [],
  144. milestones: {},
  145. totalManhour: 0,
  146. taskTemplateId: "All",
  147. projectName:
  148. mainProjects !== undefined ? mainProjects[0].projectName : undefined,
  149. projectDescription:
  150. mainProjects !== undefined
  151. ? mainProjects[0].projectDescription
  152. : undefined,
  153. expectedProjectFee:
  154. mainProjects !== undefined
  155. ? mainProjects[0].expectedProjectFee
  156. : undefined,
  157. subContractFee:
  158. mainProjects !== undefined ? mainProjects[0].subContractFee : undefined,
  159. clientId: allCustomers !== undefined ? allCustomers[0].id : undefined,
  160. ratePerManhour: 250,
  161. ...defaultInputs,
  162. // manhourPercentageByGrade should have a sensible default
  163. manhourPercentageByGrade: isEmpty(defaultInputs?.manhourPercentageByGrade)
  164. ? grades.reduce((acc, grade) => {
  165. return { ...acc, [grade.id]: 100 / grades.length };
  166. }, {})
  167. : defaultInputs?.manhourPercentageByGrade,
  168. },
  169. });
  170. const projectName = formProps.watch("projectName");
  171. const projectDeleted = formProps.watch("projectDeleted");
  172. const projectStatus = formProps.watch("projectStatus") || "";
  173. const defaultBtn = {
  174. buttonName: "submit",
  175. title: t("Do you want to submit?"),
  176. confirmButtonText: t("Submit"),
  177. successTitle: t("Submit Success"),
  178. errorTitle: t("Submit Fail"),
  179. };
  180. const buttonData = useMemo<{
  181. buttonName: string;
  182. title: string;
  183. confirmButtonText: string;
  184. successTitle: string;
  185. errorTitle: string;
  186. buttonText: string;
  187. buttonIcon: React.ReactNode;
  188. buttonColor: ButtonProps["color"];
  189. }>(() => {
  190. //Button Parameters//
  191. switch (projectStatus) {
  192. case "pending to start":
  193. return {
  194. buttonName: "start",
  195. title: t("Do you want to start?"),
  196. confirmButtonText: t("Start"),
  197. successTitle: t("Start Success"),
  198. errorTitle: t("Start Fail"),
  199. buttonText: t("Start Project"),
  200. buttonIcon: <PlayArrow />,
  201. buttonColor: "success",
  202. };
  203. case "on-going":
  204. return {
  205. buttonName: "complete",
  206. title: t("Do you want to complete?"),
  207. confirmButtonText: t("Complete"),
  208. successTitle: t("Complete Success"),
  209. errorTitle: t("Complete Fail"),
  210. buttonText: t("Complete Project"),
  211. buttonIcon: <DoneIcon />,
  212. buttonColor: "info",
  213. };
  214. case "completed":
  215. return {
  216. buttonName: "reopen",
  217. title: t("Do you want to reopen?"),
  218. confirmButtonText: t("Reopen"),
  219. successTitle: t("Reopen Success"),
  220. errorTitle: t("Reopen Fail"),
  221. buttonText: t("Reopen Project"),
  222. buttonIcon: <AutorenewIcon />,
  223. buttonColor: "secondary",
  224. };
  225. default:
  226. return {
  227. buttonName: "submit",
  228. title: t("Do you want to submit?"),
  229. confirmButtonText: t("Submit"),
  230. successTitle: t("Submit Success"),
  231. errorTitle: t("Submit Fail"),
  232. buttonText: t("Submit Project"),
  233. buttonIcon: <Check />,
  234. buttonColor: "success",
  235. };
  236. }
  237. }, [projectStatus, t]);
  238. const handleCancel = () => {
  239. router.replace("/projects");
  240. };
  241. const handleDelete = () => {
  242. deleteDialog(async () => {
  243. await deleteProject(formProps.getValues("projectId")!);
  244. const clickSuccessDialog = await successDialog("Delete Success", t);
  245. if (clickSuccessDialog) {
  246. router.replace("/projects");
  247. }
  248. }, t);
  249. };
  250. const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
  251. (_e, newValue) => {
  252. setTabIndex(newValue);
  253. },
  254. [],
  255. );
  256. const onSubmit = useCallback<SubmitHandler<CreateProjectInputs>>(
  257. async (data, event) => {
  258. try {
  259. console.log(data);
  260. // detect errors
  261. let hasErrors = false;
  262. if(
  263. !data.projectPlanStart || !data.projectPlanEnd
  264. ){
  265. formProps.setError("projectPlanStart", {
  266. message: "projectPlanStart is not valid",
  267. type: "required",
  268. });
  269. setTabIndex(0);
  270. hasErrors = true;
  271. }
  272. // Tab - Staff Allocation and Resource
  273. if (
  274. data.totalManhour === null ||
  275. data.totalManhour <= 0 ||
  276. Number.isNaN(data.totalManhour)
  277. ) {
  278. formProps.setError("totalManhour", {
  279. message: "totalManhour value is not valid",
  280. type: "required",
  281. });
  282. setTabIndex(2);
  283. hasErrors = true;
  284. }
  285. if (
  286. data.ratePerManhour === null ||
  287. data.ratePerManhour <= 0 ||
  288. Number.isNaN(data.ratePerManhour)
  289. ) {
  290. formProps.setError("ratePerManhour", {
  291. message: "ratePerManhour value is not valid",
  292. type: "required",
  293. });
  294. setTabIndex(2);
  295. hasErrors = true;
  296. }
  297. const manhourPercentageByGradeKeys = Object.keys(
  298. data.manhourPercentageByGrade,
  299. );
  300. if (
  301. manhourPercentageByGradeKeys.filter(
  302. (k) => data.manhourPercentageByGrade[k as any] < 0,
  303. ).length > 0 ||
  304. manhourPercentageByGradeKeys.reduce(
  305. (acc, value) => acc + data.manhourPercentageByGrade[value as any],
  306. 0,
  307. ) !== 100
  308. ) {
  309. formProps.setError("manhourPercentageByGrade", {
  310. message: "manhourPercentageByGrade value is not valid",
  311. type: "invalid",
  312. });
  313. setTabIndex(2);
  314. hasErrors = true;
  315. }
  316. const taskGroupKeys = Object.keys(data.taskGroups);
  317. if (
  318. taskGroupKeys.filter(
  319. (k) => data.taskGroups[k as any].percentAllocation < 0,
  320. ).length > 0 ||
  321. taskGroupKeys.reduce(
  322. (acc, value) =>
  323. acc + data.taskGroups[value as any].percentAllocation,
  324. 0,
  325. ) !== 100
  326. ) {
  327. formProps.setError("taskGroups", {
  328. message: "Task Groups value is not invalid",
  329. type: "invalid",
  330. });
  331. setTabIndex(2);
  332. hasErrors = true;
  333. }
  334. // Tab - Milestone
  335. let projectTotal = 0;
  336. const milestonesKeys = Object.keys(data.milestones).filter((key) =>
  337. taskGroupKeys.includes(key),
  338. );
  339. milestonesKeys
  340. .filter((key) => Object.keys(data.taskGroups).includes(key))
  341. .forEach((key) => {
  342. const { startDate, endDate, payments } =
  343. data.milestones[parseFloat(key)];
  344. if (
  345. !Boolean(startDate) ||
  346. startDate === "Invalid Date" ||
  347. !Boolean(endDate) ||
  348. endDate === "Invalid Date"
  349. ){
  350. data.milestones[parseFloat(key)].startDate = null
  351. data.milestones[parseFloat(key)].endDate = null
  352. }
  353. // if (
  354. // !Boolean(startDate) ||
  355. // startDate === "Invalid Date" ||
  356. // !Boolean(endDate) ||
  357. // endDate === "Invalid Date" ||
  358. // new Date(startDate) > new Date(endDate)
  359. // ) {
  360. // formProps.setError("milestones", {
  361. // message: "milestones is not valid",
  362. // type: "invalid",
  363. // });
  364. // setTabIndex(3);
  365. // hasErrors = true;
  366. // }
  367. projectTotal += payments.reduce(
  368. (acc, payment) => acc + payment.amount,
  369. 0,
  370. );
  371. });
  372. if (
  373. projectTotal !== data.expectedProjectFee ||
  374. milestonesKeys.length !== taskGroupKeys.length
  375. ) {
  376. formProps.setError("milestones", {
  377. message: "milestones is not valid",
  378. type: "invalid",
  379. });
  380. setTabIndex(3);
  381. hasErrors = true;
  382. }
  383. if (hasErrors) return false;
  384. // save project
  385. setServerError("");
  386. const buttonName = (event?.nativeEvent as any).submitter.name;
  387. const handleSubmit = async () => {
  388. if (buttonName === "start") {
  389. data.projectActualStart = dayjs().format("YYYY-MM-DD");
  390. } else if (buttonName === "complete") {
  391. data.projectActualEnd = dayjs().format("YYYY-MM-DD");
  392. }
  393. data.taskTemplateId =
  394. data.taskTemplateId === "All" ? undefined : data.taskTemplateId;
  395. const response = await saveProject(data);
  396. if (
  397. response.id > 0 &&
  398. response.message?.toLowerCase() === "success" &&
  399. response.errorPosition === null
  400. ) {
  401. successDialog(
  402. buttonName === "submit"
  403. ? defaultBtn.successTitle
  404. : buttonData.successTitle,
  405. t,
  406. ).then(() => {
  407. if (draftId) {
  408. deleteDraft(draftId);
  409. }
  410. router.replace("/projects");
  411. });
  412. } else {
  413. errorDialog(
  414. response.message ??
  415. (buttonName === "submit"
  416. ? defaultBtn.errorTitle
  417. : buttonData.errorTitle),
  418. t,
  419. ).then(() => {
  420. if (
  421. response.errorPosition !== null &&
  422. response.errorPosition === "projectCode"
  423. ) {
  424. formProps.setError("projectCode", {
  425. message: response.message,
  426. type: "invalid",
  427. });
  428. setTabIndex(0);
  429. }
  430. return false;
  431. });
  432. }
  433. };
  434. if (buttonName === "complete") {
  435. submitDialogWithWarning(handleSubmit, t, {
  436. title: buttonData.title,
  437. confirmButtonText: buttonData.confirmButtonText,
  438. text: "<b style='color:red'>Completing project will restrict any further changes to the project, are you sure to proceed?</b>",
  439. });
  440. } else if (buttonName === "submit") {
  441. submitDialog(handleSubmit, t, {
  442. title: defaultBtn.title,
  443. confirmButtonText: defaultBtn.confirmButtonText,
  444. });
  445. } else {
  446. submitDialog(handleSubmit, t, {
  447. title: buttonData.title,
  448. confirmButtonText: buttonData.confirmButtonText,
  449. });
  450. }
  451. } catch (e) {
  452. setServerError(t("An error has occurred. Please try again later."));
  453. }
  454. },
  455. [router, t],
  456. );
  457. const onSubmitError = useCallback<SubmitErrorHandler<CreateProjectInputs>>(
  458. (errors) => {
  459. console.log(errors);
  460. // Set the tab so that the focus will go there
  461. if (
  462. errors.projectName ||
  463. errors.projectDescription ||
  464. errors.projectCode ||
  465. errors.clientId ||
  466. errors.projectPlanStart ||
  467. errors.projectPlanEnd
  468. ) {
  469. setTabIndex(0);
  470. } else if (
  471. errors.totalManhour ||
  472. errors.manhourPercentageByGrade ||
  473. errors.taskGroups ||
  474. errors.ratePerManhour
  475. ) {
  476. setTabIndex(2);
  477. } else if (errors.milestones) {
  478. setTabIndex(3);
  479. }
  480. },
  481. [],
  482. );
  483. const errors = formProps.formState.errors;
  484. // auto calculate the total project manhour
  485. const expectedProjectFee = formProps.watch("expectedProjectFee");
  486. const ratePerManhour = formProps.watch("ratePerManhour");
  487. const totalManhour = formProps.watch("totalManhour");
  488. const firstLoadedRef = useRef(false);
  489. useEffect(() => {
  490. if (
  491. firstLoadedRef.current &&
  492. expectedProjectFee > 0 &&
  493. ratePerManhour > 0
  494. ) {
  495. formProps.setValue(
  496. "totalManhour",
  497. Math.ceil(expectedProjectFee / ratePerManhour),
  498. );
  499. } else {
  500. firstLoadedRef.current = true;
  501. }
  502. }, [expectedProjectFee, ratePerManhour]);
  503. useEffect(() => {
  504. if (
  505. expectedProjectFee > 0 &&
  506. ratePerManhour > 0 &&
  507. (totalManhour === null || Number.isNaN(totalManhour) || totalManhour <= 0)
  508. ) {
  509. formProps.setValue(
  510. "totalManhour",
  511. Math.ceil(expectedProjectFee / ratePerManhour),
  512. );
  513. }
  514. }, [totalManhour]);
  515. const loading = isEditMode ? !Boolean(projectName) : false;
  516. const submitDisabled =
  517. loading ||
  518. projectDeleted === true ||
  519. projectStatus.toLowerCase() === "deleted" ||
  520. // !!formProps.getValues("projectActualStart") &&
  521. !!(projectStatus.toLowerCase() === "completed");
  522. useEffect(() => {
  523. const draftInputs = draftId ? loadDraft(draftId) : undefined;
  524. formProps.reset(draftInputs);
  525. }, [draftId, formProps]);
  526. const saveDraft = useCallback(() => {
  527. saveToLocalStorage(draftId || Date.now(), formProps.getValues());
  528. router.replace("/projects");
  529. }, [draftId, formProps, router]);
  530. return (
  531. <>
  532. <FormProvider {...formProps}>
  533. <Stack
  534. spacing={2}
  535. component="form"
  536. onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)}
  537. >
  538. {isEditMode &&
  539. !(formProps.getValues("projectDeleted") === true) &&
  540. !loading && (
  541. <Grid>
  542. <Typography mb={2} variant="h4">
  543. {t("Edit Project")}: {`<${defaultInputs?.projectCode}>`}
  544. </Typography>
  545. {(defaultInputs?.projectActualEnd ||
  546. defaultInputs?.projectActualStart) && (
  547. <Stack mb={2}>
  548. {defaultInputs?.projectActualStart && (
  549. <Typography variant="caption">
  550. {t("Project Start Date: {{date}}", {
  551. date: dayjs(defaultInputs.projectActualStart).format(
  552. OUTPUT_DATE_FORMAT,
  553. ),
  554. })}
  555. </Typography>
  556. )}
  557. {defaultInputs?.projectActualEnd && (
  558. <Typography variant="caption">
  559. {t("Project End Date: {{date}}", {
  560. date: dayjs(defaultInputs.projectActualEnd).format(
  561. OUTPUT_DATE_FORMAT,
  562. ),
  563. })}
  564. </Typography>
  565. )}
  566. </Stack>
  567. )}
  568. <Stack direction="row" gap={1}>
  569. {/* {!formProps.getValues("projectActualStart") && ( */}
  570. <Button
  571. name={buttonData.buttonName}
  572. type="submit"
  573. variant="contained"
  574. startIcon={buttonData.buttonIcon}
  575. color={buttonData.buttonColor}
  576. >
  577. {t(buttonData.buttonText)}
  578. </Button>
  579. {!(
  580. // formProps.getValues("projectActualStart") &&
  581. // formProps.getValues("projectActualEnd")
  582. (
  583. projectStatus.toLowerCase() === "completed" ||
  584. projectStatus.toLowerCase() === "deleted"
  585. )
  586. ) &&
  587. abilities.includes(DELETE_PROJECT) && (
  588. <Button
  589. variant="outlined"
  590. startIcon={<Delete />}
  591. color="error"
  592. onClick={handleDelete}
  593. >
  594. {t("Delete Project")}
  595. </Button>
  596. )}
  597. </Stack>
  598. </Grid>
  599. )}
  600. <Tabs
  601. value={tabIndex}
  602. onChange={handleTabChange}
  603. variant="scrollable"
  604. >
  605. <Tab
  606. label={t("Project and Client Details")}
  607. sx={{
  608. marginInlineEnd:
  609. !hasErrorsInTab(1, errors) &&
  610. (hasErrorsInTab(2, errors) || hasErrorsInTab(3, errors))
  611. ? 1
  612. : undefined,
  613. }}
  614. icon={
  615. hasErrorsInTab(0, errors) ? (
  616. <Error sx={{ marginInlineEnd: 1 }} color="error" />
  617. ) : undefined
  618. }
  619. iconPosition="end"
  620. />
  621. <Tab
  622. label={t("Project Task Setup")}
  623. sx={{
  624. marginInlineEnd:
  625. hasErrorsInTab(2, errors) || hasErrorsInTab(3, errors)
  626. ? 1
  627. : undefined,
  628. }}
  629. iconPosition="end"
  630. />
  631. <Tab
  632. label={t("Staff Allocation and Resource")}
  633. sx={{
  634. marginInlineEnd:
  635. !hasErrorsInTab(2, errors) && hasErrorsInTab(3, errors)
  636. ? 1
  637. : undefined,
  638. }}
  639. icon={
  640. hasErrorsInTab(2, errors) ? (
  641. <Error sx={{ marginInlineEnd: 1 }} color="error" />
  642. ) : undefined
  643. }
  644. iconPosition="end"
  645. />
  646. <Tab
  647. label={t("Milestone")}
  648. icon={
  649. hasErrorsInTab(3, errors) ? (
  650. <Error sx={{ marginInlineEnd: 1 }} color="error" />
  651. ) : undefined
  652. }
  653. iconPosition="end"
  654. />
  655. </Tabs>
  656. {
  657. <ProjectClientDetails
  658. isSubProject={isSubProject}
  659. mainProjects={mainProjects}
  660. buildingTypes={buildingTypes}
  661. workNatures={workNatures}
  662. contractTypes={contractTypes}
  663. fundingTypes={fundingTypes}
  664. locationTypes={locationTypes}
  665. serviceTypes={serviceTypes}
  666. allCustomers={allCustomers}
  667. allSubsidiaries={allSubsidiaries}
  668. projectCategories={projectCategories}
  669. customerTypes={customerTypes}
  670. teamLeads={teamLeads}
  671. isActive={tabIndex === 0}
  672. isEditMode={isEditMode}
  673. />
  674. }
  675. {
  676. <TaskSetup
  677. allTasks={allTasks}
  678. taskTemplates={taskTemplates}
  679. isActive={tabIndex === 1}
  680. />
  681. }
  682. {
  683. <StaffAllocation
  684. isActive={tabIndex === 2}
  685. allTasks={allTasks}
  686. grades={grades}
  687. allStaffs={allStaffs}
  688. teamLeads={teamLeads}
  689. />
  690. }
  691. {<Milestone allTasks={allTasks} isActive={tabIndex === 3} />}
  692. {serverError && (
  693. <Typography variant="body2" color="error" alignSelf="flex-end">
  694. {serverError}
  695. </Typography>
  696. )}
  697. <Stack direction="row" justifyContent="flex-end" gap={1}>
  698. {!isEditMode && (
  699. <>
  700. <Button
  701. variant="outlined"
  702. color="secondary"
  703. startIcon={<EditNote />}
  704. onClick={saveDraft}
  705. >
  706. {t("Save Draft")}
  707. </Button>
  708. <Box sx={{ flex: 1, pointerEvents: "none" }} />
  709. </>
  710. )}
  711. <Button
  712. variant="outlined"
  713. startIcon={<Close />}
  714. onClick={handleCancel}
  715. >
  716. {t("Cancel")}
  717. </Button>
  718. <Button
  719. name="submit"
  720. variant="contained"
  721. startIcon={<Check />}
  722. type="submit"
  723. disabled={submitDisabled}
  724. >
  725. {isEditMode ? t("Save") : t("Confirm")}
  726. </Button>
  727. </Stack>
  728. </Stack>
  729. </FormProvider>
  730. </>
  731. );
  732. };
  733. export default CreateProject;