|
- "use client";
-
- import AutorenewIcon from "@mui/icons-material/Autorenew";
- import DoneIcon from "@mui/icons-material/Done";
- import Check from "@mui/icons-material/Check";
- import Close from "@mui/icons-material/Close";
- import Button, { ButtonProps } from "@mui/material/Button";
- import Stack from "@mui/material/Stack";
- import Tab from "@mui/material/Tab";
- import Tabs, { TabsProps } from "@mui/material/Tabs";
- import { useRouter } from "next/navigation";
- import React, {
- useCallback,
- useEffect,
- useMemo,
- useRef,
- useState,
- } from "react";
- import { useTranslation } from "react-i18next";
- import ProjectClientDetails from "./ProjectClientDetails";
- import TaskSetup from "./TaskSetup";
- import StaffAllocation from "./StaffAllocation";
- import Milestone from "./Milestone";
- import { Task, TaskTemplate } from "@/app/api/tasks";
- import {
- FieldErrors,
- FormProvider,
- SubmitErrorHandler,
- SubmitHandler,
- useForm,
- } from "react-hook-form";
- import {
- CreateProjectInputs,
- deleteProject,
- saveProject,
- } from "@/app/api/projects/actions";
- import { Delete, EditNote, Error, PlayArrow } from "@mui/icons-material";
- import {
- BuildingType,
- ContractType,
- FundingType,
- LocationType,
- MainProject,
- ProjectCategory,
- ServiceType,
- WorkNature,
- } from "@/app/api/projects";
- import { StaffResult } from "@/app/api/staff";
- import { Box, Grid, Typography } from "@mui/material";
- import { Grade } from "@/app/api/grades";
- import { Customer, CustomerType, Subsidiary } from "@/app/api/customer";
- import { isEmpty } from "lodash";
- import {
- deleteDialog,
- errorDialog,
- submitDialog,
- submitDialogWithWarning,
- successDialog,
- } from "../Swal/CustomAlerts";
- import dayjs from "dayjs";
- import { DELETE_PROJECT } from "@/middleware";
- import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
- import { deleteDraft, loadDraft, saveToLocalStorage } from "@/app/utils/draftUtils";
-
- export interface Props {
- isEditMode: boolean;
- isCopyMode: boolean;
- draftId?: number;
- isSubProject: boolean;
- mainProjects?: MainProject[];
- defaultInputs?: CreateProjectInputs;
- allTasks: Task[];
- projectCategories: ProjectCategory[];
- taskTemplates: TaskTemplate[];
- teamLeads: StaffResult[];
- allCustomers: Customer[];
- allSubsidiaries: Subsidiary[];
- fundingTypes: FundingType[];
- serviceTypes: ServiceType[];
- contractTypes: ContractType[];
- locationTypes: LocationType[];
- buildingTypes: BuildingType[];
- workNatures: WorkNature[];
- allStaffs: StaffResult[];
- customerTypes: CustomerType[];
- grades: Grade[];
- abilities: string[];
- }
-
- const hasErrorsInTab = (
- tabIndex: number,
- errors: FieldErrors<CreateProjectInputs>,
- ) => {
- switch (tabIndex) {
- case 0:
- return (
- errors.projectName ||
- errors.projectDescription ||
- errors.clientId ||
- errors.projectCode ||
- errors.projectPlanStart ||
- errors.projectPlanEnd
- );
- case 2:
- return (
- errors.totalManhour ||
- errors.manhourPercentageByGrade ||
- errors.taskGroups ||
- errors.ratePerManhour
- );
- case 3:
- return errors.milestones;
- default:
- false;
- }
- };
-
- const CreateProject: React.FC<Props> = ({
- isEditMode,
- isCopyMode,
- draftId,
- isSubProject,
- mainProjects,
- defaultInputs,
- allTasks,
- projectCategories,
- taskTemplates,
- teamLeads,
- grades,
- allCustomers,
- allSubsidiaries,
- contractTypes,
- fundingTypes,
- locationTypes,
- serviceTypes,
- buildingTypes,
- workNatures,
- allStaffs,
- customerTypes,
- abilities,
- }) => {
- const [serverError, setServerError] = useState("");
- const [tabIndex, setTabIndex] = useState(0);
- const { t } = useTranslation();
- const router = useRouter();
-
- const formProps = useForm<CreateProjectInputs>({
- defaultValues: {
- taskGroups: {},
- allocatedStaffIds: [],
- milestones: {},
- totalManhour: 0,
- taskTemplateId: "All",
- projectName:
- mainProjects !== undefined ? mainProjects[0].projectName : undefined,
- projectDescription:
- mainProjects !== undefined
- ? mainProjects[0].projectDescription
- : undefined,
- expectedProjectFee:
- mainProjects !== undefined
- ? mainProjects[0].expectedProjectFee
- : undefined,
- subContractFee:
- mainProjects !== undefined ? mainProjects[0].subContractFee : undefined,
- clientId: allCustomers !== undefined ? allCustomers[0].id : undefined,
- ratePerManhour: 250,
- ...defaultInputs,
-
- // manhourPercentageByGrade should have a sensible default
- manhourPercentageByGrade: isEmpty(defaultInputs?.manhourPercentageByGrade)
- ? grades.reduce((acc, grade) => {
- return { ...acc, [grade.id]: 100 / grades.length };
- }, {})
- : defaultInputs?.manhourPercentageByGrade,
- },
- });
-
- const projectName = formProps.watch("projectName");
- const projectDeleted = formProps.watch("projectDeleted");
- const projectStatus = formProps.watch("projectStatus") || "";
-
- const defaultBtn = {
- buttonName: "submit",
- title: t("Do you want to submit?"),
- confirmButtonText: t("Submit"),
- successTitle: t("Submit Success"),
- errorTitle: t("Submit Fail"),
- };
- const buttonData = useMemo<{
- buttonName: string;
- title: string;
- confirmButtonText: string;
- successTitle: string;
- errorTitle: string;
- buttonText: string;
- buttonIcon: React.ReactNode;
- buttonColor: ButtonProps["color"];
- }>(() => {
- //Button Parameters//
- switch (projectStatus.toLowerCase()) {
- case "pending to start":
- return {
- buttonName: "start",
- title: t("Do you want to start?"),
- confirmButtonText: t("Start"),
- successTitle: t("Start Success"),
- errorTitle: t("Start Fail"),
- buttonText: t("Start Project"),
- buttonIcon: <PlayArrow />,
- buttonColor: "success",
- };
- case "on-going":
- return {
- buttonName: "complete",
- title: t("Do you want to complete?"),
- confirmButtonText: t("Complete"),
- successTitle: t("Complete Success"),
- errorTitle: t("Complete Fail"),
- buttonText: t("Complete Project"),
- buttonIcon: <DoneIcon />,
- buttonColor: "info",
- };
- case "completed":
- return {
- buttonName: "reopen",
- title: t("Do you want to reopen?"),
- confirmButtonText: t("Reopen"),
- successTitle: t("Reopen Success"),
- errorTitle: t("Reopen Fail"),
- buttonText: t("Reopen Project"),
- buttonIcon: <AutorenewIcon />,
- buttonColor: "secondary",
- };
- default:
- return {
- buttonName: "submit",
- title: t("Do you want to submit?"),
- confirmButtonText: t("Submit"),
- successTitle: t("Submit Success"),
- errorTitle: t("Submit Fail"),
- buttonText: t("Submit Project"),
- buttonIcon: <Check />,
- buttonColor: "success",
- };
- }
- }, [projectStatus, t]);
-
- const handleCancel = () => {
- router.replace("/projects");
- };
-
- const handleDelete = () => {
- deleteDialog(async () => {
- await deleteProject(formProps.getValues("projectId")!);
-
- const clickSuccessDialog = await successDialog("Delete Success", t);
-
- if (clickSuccessDialog) {
- router.replace("/projects");
- }
- }, t);
- };
-
- const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
- (_e, newValue) => {
- setTabIndex(newValue);
- },
- [],
- );
-
- const onSubmit = useCallback<SubmitHandler<CreateProjectInputs>>(
- async (data, event) => {
- try {
- console.log(data);
-
- // detect errors
- let hasErrors = false;
-
- if(
- !data.projectPlanStart || !data.projectPlanEnd
- ){
- formProps.setError("projectPlanStart", {
- message: "projectPlanStart is not valid",
- type: "required",
- });
- setTabIndex(0);
- hasErrors = true;
- }
-
- // Tab - Staff Allocation and Resource
- if (
- data.totalManhour === null ||
- data.totalManhour <= 0 ||
- Number.isNaN(data.totalManhour)
- ) {
- formProps.setError("totalManhour", {
- message: "totalManhour value is not valid",
- type: "required",
- });
- setTabIndex(2);
- hasErrors = true;
- }
-
- if (
- data.ratePerManhour === null ||
- data.ratePerManhour <= 0 ||
- Number.isNaN(data.ratePerManhour)
- ) {
- formProps.setError("ratePerManhour", {
- message: "ratePerManhour value is not valid",
- type: "required",
- });
- setTabIndex(2);
- hasErrors = true;
- }
-
- const manhourPercentageByGradeKeys = Object.keys(
- data.manhourPercentageByGrade,
- );
- if (
- manhourPercentageByGradeKeys.filter(
- (k) => data.manhourPercentageByGrade[k as any] < 0,
- ).length > 0 ||
- manhourPercentageByGradeKeys.reduce(
- (acc, value) => acc + data.manhourPercentageByGrade[value as any],
- 0,
- ) !== 100
- ) {
- formProps.setError("manhourPercentageByGrade", {
- message: "manhourPercentageByGrade value is not valid",
- type: "invalid",
- });
- setTabIndex(2);
- hasErrors = true;
- }
-
- const taskGroupKeys = Object.keys(data.taskGroups);
- if (
- taskGroupKeys.filter(
- (k) => data.taskGroups[k as any].percentAllocation < 0,
- ).length > 0 ||
- taskGroupKeys.reduce(
- (acc, value) =>
- acc + data.taskGroups[value as any].percentAllocation,
- 0,
- ) !== 100
- ) {
- formProps.setError("taskGroups", {
- message: "Task Groups value is not invalid",
- type: "invalid",
- });
- setTabIndex(2);
- hasErrors = true;
- }
-
- // Tab - Milestone
- let projectTotal = 0;
- const milestonesKeys = Object.keys(data.milestones).filter((key) =>
- taskGroupKeys.includes(key),
- );
- milestonesKeys
- .filter((key) => Object.keys(data.taskGroups).includes(key))
- .forEach((key) => {
- const { startDate, endDate, payments } =
- data.milestones[parseFloat(key)];
- if (
- !Boolean(startDate) ||
- startDate === "Invalid Date" ||
- !Boolean(endDate) ||
- endDate === "Invalid Date"
- ){
- data.milestones[parseFloat(key)].startDate = null
- data.milestones[parseFloat(key)].endDate = null
- }
- // if (
- // !Boolean(startDate) ||
- // startDate === "Invalid Date" ||
- // !Boolean(endDate) ||
- // endDate === "Invalid Date" ||
- // new Date(startDate) > new Date(endDate)
- // ) {
- // formProps.setError("milestones", {
- // message: "milestones is not valid",
- // type: "invalid",
- // });
- // setTabIndex(3);
- // hasErrors = true;
- // }
-
- projectTotal += payments.reduce(
- (acc, payment) => acc + payment.amount,
- 0,
- );
- });
- console.log(milestonesKeys)
-
- if (
- projectTotal !== data.expectedProjectFee
- // || milestonesKeys.length !== taskGroupKeys.length
- ) {
- formProps.setError("milestones", {
- message: "milestones is not valid",
- type: "invalid",
- });
- setTabIndex(3);
- hasErrors = true;
- }
-
- if (hasErrors) return false;
- // save project
- setServerError("");
-
- const buttonName = (event?.nativeEvent as any).submitter.name;
-
- const handleSubmit = async () => {
- if (buttonName === "start") {
- data.projectActualStart = dayjs().format("YYYY-MM-DD");
- } else if (buttonName === "complete") {
- data.projectActualEnd = dayjs().format("YYYY-MM-DD");
- }
- data.taskTemplateId =
- data.taskTemplateId === "All" ? undefined : data.taskTemplateId;
- const response = await saveProject(data);
-
- if (
- response.id > 0 &&
- response.message?.toLowerCase() === "success" &&
- response.errorPosition === null
- ) {
- successDialog(
- buttonName === "submit"
- ? defaultBtn.successTitle
- : buttonData.successTitle,
- t,
- ).then(() => {
- if (draftId) {
- deleteDraft(draftId);
- }
- router.replace("/projects");
- });
- } else {
- errorDialog(
- response.message ??
- (buttonName === "submit"
- ? defaultBtn.errorTitle
- : buttonData.errorTitle),
- t,
- ).then(() => {
- if (
- response.errorPosition !== null &&
- response.errorPosition === "projectCode"
- ) {
- formProps.setError("projectCode", {
- message: response.message,
- type: "invalid",
- });
- setTabIndex(0);
- }
-
- return false;
- });
- }
- };
-
- if (buttonName === "complete") {
- submitDialogWithWarning(handleSubmit, t, {
- title: buttonData.title,
- confirmButtonText: buttonData.confirmButtonText,
- text: "<b style='color:red'>Completing project will restrict any further changes to the project, are you sure to proceed?</b>",
- });
- } else if (buttonName === "submit") {
- submitDialog(handleSubmit, t, {
- title: defaultBtn.title,
- confirmButtonText: defaultBtn.confirmButtonText,
- });
- } else {
- submitDialog(handleSubmit, t, {
- title: buttonData.title,
- confirmButtonText: buttonData.confirmButtonText,
- });
- }
- } catch (e) {
- setServerError(t("An error has occurred. Please try again later."));
- }
- },
- [router, t],
- );
-
- const onSubmitError = useCallback<SubmitErrorHandler<CreateProjectInputs>>(
- (errors) => {
- console.log(errors);
- // Set the tab so that the focus will go there
- if (
- errors.projectName ||
- errors.projectDescription ||
- errors.projectCode ||
- errors.clientId ||
- errors.projectPlanStart ||
- errors.projectPlanEnd
- ) {
- setTabIndex(0);
- } else if (
- errors.totalManhour ||
- errors.manhourPercentageByGrade ||
- errors.taskGroups ||
- errors.ratePerManhour
- ) {
- setTabIndex(2);
- } else if (errors.milestones) {
- setTabIndex(3);
- }
- },
- [],
- );
-
- const errors = formProps.formState.errors;
-
- // auto calculate the total project manhour
- const expectedProjectFee = formProps.watch("expectedProjectFee");
- const ratePerManhour = formProps.watch("ratePerManhour");
- const totalManhour = formProps.watch("totalManhour");
- const firstLoadedRef = useRef(false);
- useEffect(() => {
- if (
- firstLoadedRef.current &&
- expectedProjectFee > 0 &&
- ratePerManhour > 0
- ) {
- formProps.setValue(
- "totalManhour",
- Math.ceil(expectedProjectFee / ratePerManhour),
- );
- } else {
- firstLoadedRef.current = true;
- }
- }, [expectedProjectFee, ratePerManhour]);
-
- useEffect(() => {
- if (
- expectedProjectFee > 0 &&
- ratePerManhour > 0 &&
- (totalManhour === null || Number.isNaN(totalManhour) || totalManhour <= 0)
- ) {
- formProps.setValue(
- "totalManhour",
- Math.ceil(expectedProjectFee / ratePerManhour),
- );
- }
- }, [totalManhour]);
-
- const loading = isEditMode || isCopyMode ? !Boolean(projectName) : false;
-
- const submitDisabled =
- loading ||
- projectDeleted === true ||
- projectStatus.toLowerCase() === "deleted" ||
- // !!formProps.getValues("projectActualStart") &&
- !!(projectStatus.toLowerCase() === "completed");
-
- useEffect(() => {
- const draftInputs = draftId ? loadDraft(draftId) : undefined;
- formProps.reset(draftInputs);
- }, [draftId, formProps]);
-
- const saveDraft = useCallback(() => {
- saveToLocalStorage(draftId || Date.now(), formProps.getValues());
- router.replace("/projects");
- }, [draftId, formProps, router]);
-
- const handleDeleteDraft = useCallback(() => {
- deleteDialog(async () => {
- deleteDraft(Number(draftId));
-
- const clickSuccessDialog = await successDialog("Delete Success", t);
-
- if (clickSuccessDialog) {
- router.replace("/projects");
- }
- }, t);
- }, [draftId, router]);
-
- return (
- <>
- <FormProvider {...formProps}>
- <Stack
- spacing={2}
- component="form"
- onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)}
- >
- {isEditMode &&
- !(formProps.getValues("projectDeleted") === true) &&
- !loading && (
- <Grid>
- <Typography mb={2} variant="h4">
- {t("Edit Project")}: {`<${defaultInputs?.projectCode}>`}
- </Typography>
- {(defaultInputs?.projectActualEnd ||
- defaultInputs?.projectActualStart) && (
- <Stack mb={2}>
- {defaultInputs?.projectActualStart && (
- <Typography variant="caption">
- {t("Project Start Date: {{date}}", {
- date: dayjs(defaultInputs.projectActualStart).format(
- OUTPUT_DATE_FORMAT,
- ),
- })}
- </Typography>
- )}
- {defaultInputs?.projectActualEnd && (
- <Typography variant="caption">
- {t("Project End Date: {{date}}", {
- date: dayjs(defaultInputs.projectActualEnd).format(
- OUTPUT_DATE_FORMAT,
- ),
- })}
- </Typography>
- )}
- </Stack>
- )}
- <Stack direction="row" gap={1}>
- {/* {!formProps.getValues("projectActualStart") && ( */}
- <Button
- name={buttonData.buttonName}
- type="submit"
- variant="contained"
- startIcon={buttonData.buttonIcon}
- color={buttonData.buttonColor}
- >
- {t(buttonData.buttonText)}
- </Button>
- {!(
- // formProps.getValues("projectActualStart") &&
- // formProps.getValues("projectActualEnd")
- (
- projectStatus.toLowerCase() === "completed" ||
- projectStatus.toLowerCase() === "deleted"
- )
- ) &&
- abilities.includes(DELETE_PROJECT) && (
- <Button
- variant="outlined"
- startIcon={<Delete />}
- color="error"
- onClick={handleDelete}
- >
- {t("Delete Project")}
- </Button>
- )}
- </Stack>
- </Grid>
- )}
- <Tabs
- value={tabIndex}
- onChange={handleTabChange}
- variant="scrollable"
- >
- <Tab
- label={t("Project and Client Details")}
- sx={{
- marginInlineEnd:
- !hasErrorsInTab(1, errors) &&
- (hasErrorsInTab(2, errors) || hasErrorsInTab(3, errors))
- ? 1
- : undefined,
- }}
- icon={
- hasErrorsInTab(0, errors) ? (
- <Error sx={{ marginInlineEnd: 1 }} color="error" />
- ) : undefined
- }
- iconPosition="end"
- />
- <Tab
- label={t("Project Task Setup")}
- sx={{
- marginInlineEnd:
- hasErrorsInTab(2, errors) || hasErrorsInTab(3, errors)
- ? 1
- : undefined,
- }}
- iconPosition="end"
- />
- <Tab
- label={t("Staff Allocation and Resource")}
- sx={{
- marginInlineEnd:
- !hasErrorsInTab(2, errors) && hasErrorsInTab(3, errors)
- ? 1
- : undefined,
- }}
- icon={
- hasErrorsInTab(2, errors) ? (
- <Error sx={{ marginInlineEnd: 1 }} color="error" />
- ) : undefined
- }
- iconPosition="end"
- />
- <Tab
- label={t("Milestone")}
- icon={
- hasErrorsInTab(3, errors) ? (
- <Error sx={{ marginInlineEnd: 1 }} color="error" />
- ) : undefined
- }
- iconPosition="end"
- />
- </Tabs>
- {
- <ProjectClientDetails
- isSubProject={isSubProject}
- mainProjects={mainProjects}
- buildingTypes={buildingTypes}
- workNatures={workNatures}
- contractTypes={contractTypes}
- fundingTypes={fundingTypes}
- locationTypes={locationTypes}
- serviceTypes={serviceTypes}
- allCustomers={allCustomers}
- allSubsidiaries={allSubsidiaries}
- projectCategories={projectCategories}
- customerTypes={customerTypes}
- teamLeads={teamLeads}
- isActive={tabIndex === 0}
- isEditMode={isEditMode}
- />
- }
- {
- <TaskSetup
- allTasks={allTasks}
- taskTemplates={taskTemplates}
- isActive={tabIndex === 1}
- />
- }
- {
- <StaffAllocation
- isActive={tabIndex === 2}
- allTasks={allTasks}
- grades={grades}
- allStaffs={allStaffs}
- teamLeads={teamLeads}
- />
- }
- {<Milestone allTasks={allTasks} isActive={tabIndex === 3} />}
- {serverError && (
- <Typography variant="body2" color="error" alignSelf="flex-end">
- {serverError}
- </Typography>
- )}
- <Stack direction="row" justifyContent="flex-end" gap={1}>
- {!isEditMode && (
- <>
- <Button
- variant="outlined"
- color="secondary"
- startIcon={<EditNote />}
- onClick={saveDraft}
- >
- {t("Save Draft")}
- </Button>
- {draftId &&
- <Button
- variant="outlined"
- color="error"
- startIcon={<Delete />}
- onClick={handleDeleteDraft}
- >
- {t("Delete Draft")}
- </Button>
- }
- <Box sx={{ flex: 1, pointerEvents: "none" }} />
- </>
- )}
- <Button
- variant="outlined"
- startIcon={<Close />}
- onClick={handleCancel}
- >
- {t("Cancel")}
- </Button>
- <Button
- name="submit"
- variant="contained"
- startIcon={<Check />}
- type="submit"
- disabled={submitDisabled}
- >
- {isEditMode ? t("Save") : t("Confirm")}
- </Button>
- </Stack>
- </Stack>
- </FormProvider>
- </>
- );
- };
-
- export default CreateProject;
|