"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; 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, ) => { 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 = ({ isEditMode, 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({ 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) { 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: , 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: , 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: , 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: , 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>( (_e, newValue) => { setTabIndex(newValue); }, [], ); const onSubmit = useCallback>( 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, ); }); 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: "Completing project will restrict any further changes to the project, are you sure to proceed?", }); } 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>( (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 ? !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]); return ( <> {isEditMode && !(formProps.getValues("projectDeleted") === true) && !loading && ( {t("Edit Project")}: {`<${defaultInputs?.projectCode}>`} {(defaultInputs?.projectActualEnd || defaultInputs?.projectActualStart) && ( {defaultInputs?.projectActualStart && ( {t("Project Start Date: {{date}}", { date: dayjs(defaultInputs.projectActualStart).format( OUTPUT_DATE_FORMAT, ), })} )} {defaultInputs?.projectActualEnd && ( {t("Project End Date: {{date}}", { date: dayjs(defaultInputs.projectActualEnd).format( OUTPUT_DATE_FORMAT, ), })} )} )} {/* {!formProps.getValues("projectActualStart") && ( */} {!( // formProps.getValues("projectActualStart") && // formProps.getValues("projectActualEnd") ( projectStatus.toLowerCase() === "completed" || projectStatus.toLowerCase() === "deleted" ) ) && abilities.includes(DELETE_PROJECT) && ( )} )} ) : undefined } iconPosition="end" /> ) : undefined } iconPosition="end" /> ) : undefined } iconPosition="end" /> { } { } { } {} {serverError && ( {serverError} )} {!isEditMode && ( <> )} ); }; export default CreateProject;