| @@ -1,4 +1,8 @@ | |||||
| import { fetchAllCustomers, fetchAllSubsidiaries, fetchCustomerTypes } from "@/app/api/customer"; | |||||
| import { | |||||
| fetchAllCustomers, | |||||
| fetchAllSubsidiaries, | |||||
| fetchCustomerTypes, | |||||
| } from "@/app/api/customer"; | |||||
| import { fetchGrades } from "@/app/api/grades"; | import { fetchGrades } from "@/app/api/grades"; | ||||
| import { | import { | ||||
| fetchProjectBuildingTypes, | fetchProjectBuildingTypes, | ||||
| @@ -16,6 +20,7 @@ import CreateProject from "@/components/CreateProject"; | |||||
| import { I18nProvider, getServerI18n } from "@/i18n"; | import { I18nProvider, getServerI18n } from "@/i18n"; | ||||
| import { MAINTAIN_PROJECT } from "@/middleware"; | import { MAINTAIN_PROJECT } from "@/middleware"; | ||||
| import Typography from "@mui/material/Typography"; | import Typography from "@mui/material/Typography"; | ||||
| import isString from "lodash/isString"; | |||||
| import { Metadata } from "next"; | import { Metadata } from "next"; | ||||
| import { notFound } from "next/navigation"; | import { notFound } from "next/navigation"; | ||||
| @@ -23,7 +28,11 @@ export const metadata: Metadata = { | |||||
| title: "Create Project", | title: "Create Project", | ||||
| }; | }; | ||||
| const Projects: React.FC = async () => { | |||||
| interface Props { | |||||
| searchParams: { [key: string]: string | string[] | undefined }; | |||||
| } | |||||
| const Projects: React.FC<Props> = async ({ searchParams }) => { | |||||
| const { t } = await getServerI18n("projects"); | const { t } = await getServerI18n("projects"); | ||||
| const abilities = await fetchUserAbilities(); | const abilities = await fetchUserAbilities(); | ||||
| @@ -32,6 +41,10 @@ const Projects: React.FC = async () => { | |||||
| notFound(); | notFound(); | ||||
| } | } | ||||
| const draftId = isString(searchParams["draftId"]) | |||||
| ? parseInt(searchParams["draftId"]) | |||||
| : undefined; | |||||
| // Preload necessary dependencies | // Preload necessary dependencies | ||||
| fetchAllTasks(); | fetchAllTasks(); | ||||
| fetchTaskTemplates(); | fetchTaskTemplates(); | ||||
| @@ -53,7 +66,7 @@ const Projects: React.FC = async () => { | |||||
| <> | <> | ||||
| <Typography variant="h4">{t("Create Project")}</Typography> | <Typography variant="h4">{t("Create Project")}</Typography> | ||||
| <I18nProvider namespaces={["projects"]}> | <I18nProvider namespaces={["projects"]}> | ||||
| <CreateProject isEditMode={false} /> | |||||
| <CreateProject isEditMode={false} draftId={draftId} /> | |||||
| </I18nProvider> | </I18nProvider> | ||||
| </> | </> | ||||
| ); | ); | ||||
| @@ -1,8 +1,10 @@ | |||||
| import { fetchProjectCategories, fetchProjects, preloadProjects } from "@/app/api/projects"; | |||||
| import { fetchAllCustomers } from "@/app/api/customer"; | |||||
| import { fetchProjectCategories, fetchProjects } from "@/app/api/projects"; | |||||
| import { fetchTeam } from "@/app/api/team"; | |||||
| import { fetchUserAbilities } from "@/app/utils/fetchUtil"; | import { fetchUserAbilities } from "@/app/utils/fetchUtil"; | ||||
| import ProjectSearch from "@/components/ProjectSearch"; | import ProjectSearch from "@/components/ProjectSearch"; | ||||
| import { getServerI18n, I18nProvider } from "@/i18n"; | import { getServerI18n, I18nProvider } from "@/i18n"; | ||||
| import { MAINTAIN_PROJECT, VIEW_PROJECT } from "@/middleware"; | |||||
| import { MAINTAIN_PROJECT } from "@/middleware"; | |||||
| import Add from "@mui/icons-material/Add"; | import Add from "@mui/icons-material/Add"; | ||||
| import Button from "@mui/material/Button"; | import Button from "@mui/material/Button"; | ||||
| import Stack from "@mui/material/Stack"; | import Stack from "@mui/material/Stack"; | ||||
| @@ -18,55 +20,62 @@ export const metadata: Metadata = { | |||||
| const Projects: React.FC = async () => { | const Projects: React.FC = async () => { | ||||
| const { t } = await getServerI18n("projects"); | const { t } = await getServerI18n("projects"); | ||||
| // preloadProjects(); | |||||
| fetchProjectCategories(); | fetchProjectCategories(); | ||||
| fetchTeam(); | |||||
| fetchAllCustomers(); | |||||
| const projects = await fetchProjects(); | const projects = await fetchProjects(); | ||||
| const abilities = await fetchUserAbilities() | |||||
| if (![MAINTAIN_PROJECT].some(ability => abilities.includes(ability))) { | |||||
| const abilities = await fetchUserAbilities(); | |||||
| if (![MAINTAIN_PROJECT].some((ability) => abilities.includes(ability))) { | |||||
| notFound(); | notFound(); | ||||
| } | } | ||||
| return ( | return ( | ||||
| <> | <> | ||||
| <I18nProvider namespaces={["projects","common"]}> | |||||
| <Stack | |||||
| direction="row" | |||||
| justifyContent="space-between" | |||||
| flexWrap="wrap" | |||||
| rowGap={2} | |||||
| > | |||||
| <Typography variant="h4" marginInlineEnd={2}> | |||||
| {t("Project Management")} | |||||
| </Typography> | |||||
| {abilities.includes(MAINTAIN_PROJECT) && <Stack | |||||
| <I18nProvider namespaces={["projects", "common"]}> | |||||
| <Stack | |||||
| direction="row" | direction="row" | ||||
| justifyContent="space-between" | justifyContent="space-between" | ||||
| flexWrap="wrap" | flexWrap="wrap" | ||||
| rowGap={2} | rowGap={2} | ||||
| spacing={1} | |||||
| > | > | ||||
| {projects.filter(project => project.status.toLowerCase() !== "deleted").length > 0 && <Button | |||||
| variant="contained" | |||||
| color="secondary" | |||||
| startIcon={<Add />} | |||||
| LinkComponent={Link} | |||||
| href="/projects/createSub" | |||||
| > | |||||
| {t("Create Sub Project")} | |||||
| </Button>} | |||||
| <Button | |||||
| variant="contained" | |||||
| startIcon={<Add />} | |||||
| LinkComponent={Link} | |||||
| href="/projects/create" | |||||
| > | |||||
| {t("Create Project")} | |||||
| </Button> | |||||
| </Stack >} | |||||
| </Stack> | |||||
| <Suspense fallback={<ProjectSearch.Loading />}> | |||||
| <ProjectSearch /> | |||||
| </Suspense> | |||||
| <Typography variant="h4" marginInlineEnd={2}> | |||||
| {t("Project Management")} | |||||
| </Typography> | |||||
| {abilities.includes(MAINTAIN_PROJECT) && ( | |||||
| <Stack | |||||
| direction="row" | |||||
| justifyContent="space-between" | |||||
| flexWrap="wrap" | |||||
| rowGap={2} | |||||
| spacing={1} | |||||
| > | |||||
| {projects.filter( | |||||
| (project) => project.status.toLowerCase() !== "deleted", | |||||
| ).length > 0 && ( | |||||
| <Button | |||||
| variant="contained" | |||||
| color="secondary" | |||||
| startIcon={<Add />} | |||||
| LinkComponent={Link} | |||||
| href="/projects/createSub" | |||||
| > | |||||
| {t("Create Sub Project")} | |||||
| </Button> | |||||
| )} | |||||
| <Button | |||||
| variant="contained" | |||||
| startIcon={<Add />} | |||||
| LinkComponent={Link} | |||||
| href="/projects/create" | |||||
| > | |||||
| {t("Create Project")} | |||||
| </Button> | |||||
| </Stack> | |||||
| )} | |||||
| </Stack> | |||||
| <Suspense fallback={<ProjectSearch.Loading />}> | |||||
| <ProjectSearch /> | |||||
| </Suspense> | |||||
| </I18nProvider> | </I18nProvider> | ||||
| </> | </> | ||||
| ); | ); | ||||
| @@ -0,0 +1,55 @@ | |||||
| import { CreateProjectInputs } from "../api/projects/actions"; | |||||
| const STORAGE_KEY = "draftProjects"; | |||||
| const getStorage = (): { | |||||
| [draftId: string]: CreateProjectInputs; | |||||
| } => { | |||||
| if (typeof window === "undefined") { | |||||
| return {}; | |||||
| } | |||||
| const storageString = localStorage.getItem(STORAGE_KEY); | |||||
| if (!storageString) { | |||||
| return {}; | |||||
| } | |||||
| try { | |||||
| return JSON.parse(storageString); | |||||
| } catch { | |||||
| return {}; | |||||
| } | |||||
| }; | |||||
| export const loadDrafts = (): [id: string, CreateProjectInputs][] => { | |||||
| return Object.entries(getStorage()); | |||||
| }; | |||||
| export const saveToLocalStorage = ( | |||||
| draftId: number, | |||||
| data: CreateProjectInputs, | |||||
| ) => { | |||||
| const storage = getStorage(); | |||||
| localStorage.setItem( | |||||
| STORAGE_KEY, | |||||
| JSON.stringify({ | |||||
| ...storage, | |||||
| [draftId]: data, | |||||
| }), | |||||
| ); | |||||
| }; | |||||
| export const loadDraft = (draftId: number): CreateProjectInputs | undefined => { | |||||
| const storage = getStorage(); | |||||
| const draft = storage[draftId]; | |||||
| return draft; | |||||
| }; | |||||
| export const deleteDraft = (draftId: number) => { | |||||
| const storage = getStorage(); | |||||
| delete storage[draftId]; | |||||
| localStorage.setItem(STORAGE_KEY, JSON.stringify(storage)); | |||||
| }; | |||||
| @@ -4,12 +4,18 @@ import AutorenewIcon from "@mui/icons-material/Autorenew"; | |||||
| import DoneIcon from "@mui/icons-material/Done"; | import DoneIcon from "@mui/icons-material/Done"; | ||||
| import Check from "@mui/icons-material/Check"; | import Check from "@mui/icons-material/Check"; | ||||
| import Close from "@mui/icons-material/Close"; | import Close from "@mui/icons-material/Close"; | ||||
| import Button from "@mui/material/Button"; | |||||
| import Button, { ButtonProps } from "@mui/material/Button"; | |||||
| import Stack from "@mui/material/Stack"; | import Stack from "@mui/material/Stack"; | ||||
| import Tab from "@mui/material/Tab"; | import Tab from "@mui/material/Tab"; | ||||
| import Tabs, { TabsProps } from "@mui/material/Tabs"; | import Tabs, { TabsProps } from "@mui/material/Tabs"; | ||||
| import { useRouter } from "next/navigation"; | import { useRouter } from "next/navigation"; | ||||
| import React, { useCallback, useEffect, useState } from "react"; | |||||
| import React, { | |||||
| useCallback, | |||||
| useEffect, | |||||
| useMemo, | |||||
| useRef, | |||||
| useState, | |||||
| } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import ProjectClientDetails from "./ProjectClientDetails"; | import ProjectClientDetails from "./ProjectClientDetails"; | ||||
| import TaskSetup from "./TaskSetup"; | import TaskSetup from "./TaskSetup"; | ||||
| @@ -28,7 +34,7 @@ import { | |||||
| deleteProject, | deleteProject, | ||||
| saveProject, | saveProject, | ||||
| } from "@/app/api/projects/actions"; | } from "@/app/api/projects/actions"; | ||||
| import { Delete, Error, PlayArrow } from "@mui/icons-material"; | |||||
| import { Delete, EditNote, Error, PlayArrow } from "@mui/icons-material"; | |||||
| import { | import { | ||||
| BuildingType, | BuildingType, | ||||
| ContractType, | ContractType, | ||||
| @@ -40,7 +46,7 @@ import { | |||||
| WorkNature, | WorkNature, | ||||
| } from "@/app/api/projects"; | } from "@/app/api/projects"; | ||||
| import { StaffResult } from "@/app/api/staff"; | import { StaffResult } from "@/app/api/staff"; | ||||
| import { Grid, Typography } from "@mui/material"; | |||||
| import { Box, Grid, Typography } from "@mui/material"; | |||||
| import { Grade } from "@/app/api/grades"; | import { Grade } from "@/app/api/grades"; | ||||
| import { Customer, CustomerType, Subsidiary } from "@/app/api/customer"; | import { Customer, CustomerType, Subsidiary } from "@/app/api/customer"; | ||||
| import { isEmpty } from "lodash"; | import { isEmpty } from "lodash"; | ||||
| @@ -54,9 +60,11 @@ import { | |||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| import { DELETE_PROJECT } from "@/middleware"; | import { DELETE_PROJECT } from "@/middleware"; | ||||
| import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | ||||
| import { deleteDraft, loadDraft, saveToLocalStorage } from "@/app/utils/draftUtils"; | |||||
| export interface Props { | export interface Props { | ||||
| isEditMode: boolean; | isEditMode: boolean; | ||||
| draftId?: number; | |||||
| isSubProject: boolean; | isSubProject: boolean; | ||||
| mainProjects?: MainProject[]; | mainProjects?: MainProject[]; | ||||
| defaultInputs?: CreateProjectInputs; | defaultInputs?: CreateProjectInputs; | ||||
| @@ -106,6 +114,7 @@ const hasErrorsInTab = ( | |||||
| const CreateProject: React.FC<Props> = ({ | const CreateProject: React.FC<Props> = ({ | ||||
| isEditMode, | isEditMode, | ||||
| draftId, | |||||
| isSubProject, | isSubProject, | ||||
| mainProjects, | mainProjects, | ||||
| defaultInputs, | defaultInputs, | ||||
| @@ -127,11 +136,46 @@ const CreateProject: React.FC<Props> = ({ | |||||
| abilities, | abilities, | ||||
| }) => { | }) => { | ||||
| const [serverError, setServerError] = useState(""); | const [serverError, setServerError] = useState(""); | ||||
| const [loading, setLoading] = useState(true); | |||||
| const [tabIndex, setTabIndex] = useState(0); | const [tabIndex, setTabIndex] = useState(0); | ||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const router = useRouter(); | 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 = { | const defaultBtn = { | ||||
| buttonName: "submit", | buttonName: "submit", | ||||
| title: t("Do you want to submit?"), | title: t("Do you want to submit?"), | ||||
| @@ -139,31 +183,64 @@ const CreateProject: React.FC<Props> = ({ | |||||
| successTitle: t("Submit Success"), | successTitle: t("Submit Success"), | ||||
| errorTitle: t("Submit Fail"), | errorTitle: t("Submit Fail"), | ||||
| }; | }; | ||||
| const [buttonData, setButtonData] = useState<{ | |||||
| const buttonData = useMemo<{ | |||||
| buttonName: string; | buttonName: string; | ||||
| title: string; | title: string; | ||||
| confirmButtonText: string; | confirmButtonText: string; | ||||
| successTitle: string; | successTitle: string; | ||||
| errorTitle: string; | errorTitle: string; | ||||
| buttonText: string; | buttonText: string; | ||||
| buttonIcon: any; | |||||
| buttonColor: any; | |||||
| }>({ | |||||
| ...defaultBtn, | |||||
| buttonText: t("Submit Project"), | |||||
| buttonIcon: <Check />, | |||||
| buttonColor: "success", | |||||
| }); | |||||
| const disableChecking = () => { | |||||
| return ( | |||||
| loading || | |||||
| formProps.getValues("projectDeleted") === true || | |||||
| formProps.getValues("projectStatus")?.toLowerCase() === "deleted" || | |||||
| // !!formProps.getValues("projectActualStart") && | |||||
| !!(formProps.getValues("projectStatus")?.toLowerCase() === "completed") | |||||
| ); | |||||
| }; | |||||
| 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: <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 = () => { | const handleCancel = () => { | ||||
| router.replace("/projects"); | router.replace("/projects"); | ||||
| @@ -333,6 +410,9 @@ const CreateProject: React.FC<Props> = ({ | |||||
| : buttonData.successTitle, | : buttonData.successTitle, | ||||
| t, | t, | ||||
| ).then(() => { | ).then(() => { | ||||
| if (draftId) { | |||||
| deleteDraft(draftId); | |||||
| } | |||||
| router.replace("/projects"); | router.replace("/projects"); | ||||
| }); | }); | ||||
| } else { | } else { | ||||
| @@ -408,58 +488,29 @@ const CreateProject: React.FC<Props> = ({ | |||||
| [], | [], | ||||
| ); | ); | ||||
| 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 errors = formProps.formState.errors; | const errors = formProps.formState.errors; | ||||
| // auto calculate the total project manhour | // auto calculate the total project manhour | ||||
| const expectedProjectFee = formProps.watch("expectedProjectFee"); | const expectedProjectFee = formProps.watch("expectedProjectFee"); | ||||
| const ratePerManhour = formProps.watch("ratePerManhour"); | const ratePerManhour = formProps.watch("ratePerManhour"); | ||||
| const totalManhour = formProps.watch("totalManhour"); | const totalManhour = formProps.watch("totalManhour"); | ||||
| const [firstLoaded, setFirstLoaded] = useState(false); | |||||
| React.useMemo(() => { | |||||
| if (firstLoaded && expectedProjectFee > 0 && ratePerManhour > 0) { | |||||
| console.log(ratePerManhour, formProps.watch("totalManhour")); | |||||
| const firstLoadedRef = useRef(false); | |||||
| useEffect(() => { | |||||
| if ( | |||||
| firstLoadedRef.current && | |||||
| expectedProjectFee > 0 && | |||||
| ratePerManhour > 0 | |||||
| ) { | |||||
| formProps.setValue( | formProps.setValue( | ||||
| "totalManhour", | "totalManhour", | ||||
| Math.ceil(expectedProjectFee / ratePerManhour), | Math.ceil(expectedProjectFee / ratePerManhour), | ||||
| ); | ); | ||||
| } else { | } else { | ||||
| setFirstLoaded(true); | |||||
| firstLoadedRef.current = true; | |||||
| } | } | ||||
| }, [expectedProjectFee, ratePerManhour]); | }, [expectedProjectFee, ratePerManhour]); | ||||
| React.useMemo(() => { | |||||
| useEffect(() => { | |||||
| if ( | if ( | ||||
| expectedProjectFee > 0 && | expectedProjectFee > 0 && | ||||
| ratePerManhour > 0 && | ratePerManhour > 0 && | ||||
| @@ -472,57 +523,24 @@ const CreateProject: React.FC<Props> = ({ | |||||
| } | } | ||||
| }, [totalManhour]); | }, [totalManhour]); | ||||
| const updateButtonData = () => { | |||||
| const status = formProps.getValues("projectStatus")?.toLowerCase(); | |||||
| const loading = isEditMode ? !Boolean(projectName) : false; | |||||
| //Button Parameters// | |||||
| switch (status) { | |||||
| case "pending to start": | |||||
| setButtonData({ | |||||
| 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", | |||||
| }); | |||||
| break; | |||||
| case "on-going": | |||||
| setButtonData({ | |||||
| 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", | |||||
| }); | |||||
| break; | |||||
| case "completed": | |||||
| setButtonData({ | |||||
| 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", | |||||
| }); | |||||
| } | |||||
| }; | |||||
| const submitDisabled = | |||||
| loading || | |||||
| projectDeleted === true || | |||||
| projectStatus.toLowerCase() === "deleted" || | |||||
| // !!formProps.getValues("projectActualStart") && | |||||
| !!(projectStatus.toLowerCase() === "completed"); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| if (!isEditMode) { | |||||
| setLoading(false); | |||||
| } else if (formProps?.getValues("projectName")) { | |||||
| setLoading(false); | |||||
| updateButtonData(); | |||||
| } | |||||
| }, [formProps]); | |||||
| 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 ( | return ( | ||||
| <> | <> | ||||
| @@ -577,10 +595,8 @@ const CreateProject: React.FC<Props> = ({ | |||||
| // formProps.getValues("projectActualStart") && | // formProps.getValues("projectActualStart") && | ||||
| // formProps.getValues("projectActualEnd") | // formProps.getValues("projectActualEnd") | ||||
| ( | ( | ||||
| formProps.getValues("projectStatus")?.toLowerCase() === | |||||
| "completed" || | |||||
| formProps.getValues("projectStatus")?.toLowerCase() === | |||||
| "deleted" | |||||
| projectStatus.toLowerCase() === "completed" || | |||||
| projectStatus.toLowerCase() === "deleted" | |||||
| ) | ) | ||||
| ) && | ) && | ||||
| abilities.includes(DELETE_PROJECT) && ( | abilities.includes(DELETE_PROJECT) && ( | ||||
| @@ -694,6 +710,19 @@ const CreateProject: React.FC<Props> = ({ | |||||
| </Typography> | </Typography> | ||||
| )} | )} | ||||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | <Stack direction="row" justifyContent="flex-end" gap={1}> | ||||
| {!isEditMode && ( | |||||
| <> | |||||
| <Button | |||||
| variant="outlined" | |||||
| color="secondary" | |||||
| startIcon={<EditNote />} | |||||
| onClick={saveDraft} | |||||
| > | |||||
| {t("Save Draft")} | |||||
| </Button> | |||||
| <Box sx={{ flex: 1, pointerEvents: "none" }} /> | |||||
| </> | |||||
| )} | |||||
| <Button | <Button | ||||
| variant="outlined" | variant="outlined" | ||||
| startIcon={<Close />} | startIcon={<Close />} | ||||
| @@ -706,7 +735,7 @@ const CreateProject: React.FC<Props> = ({ | |||||
| variant="contained" | variant="contained" | ||||
| startIcon={<Check />} | startIcon={<Check />} | ||||
| type="submit" | type="submit" | ||||
| disabled={disableChecking()} | |||||
| disabled={submitDisabled} | |||||
| > | > | ||||
| {isEditMode ? t("Save") : t("Confirm")} | {isEditMode ? t("Save") : t("Confirm")} | ||||
| </Button> | </Button> | ||||
| @@ -23,6 +23,7 @@ import { fetchUserAbilities, fetchUserStaff } from "@/app/utils/fetchUtil"; | |||||
| type CreateProjectProps = { | type CreateProjectProps = { | ||||
| isEditMode: false; | isEditMode: false; | ||||
| isSubProject?: boolean; | isSubProject?: boolean; | ||||
| draftId?: number; | |||||
| }; | }; | ||||
| interface EditProjectProps { | interface EditProjectProps { | ||||
| isEditMode: true; | isEditMode: true; | ||||
| @@ -68,9 +69,11 @@ const CreateProjectWrapper: React.FC<Props> = async (props) => { | |||||
| fetchCustomerTypes(), | fetchCustomerTypes(), | ||||
| fetchUserAbilities(), | fetchUserAbilities(), | ||||
| ]); | ]); | ||||
| const userStaff = await fetchUserStaff() | |||||
| const teamId = userStaff?.teamId | |||||
| const filteredTeamLeads = teamLeads.filter(teamLead => teamLead.teamId === teamId) | |||||
| const userStaff = await fetchUserStaff(); | |||||
| const teamId = userStaff?.teamId; | |||||
| const filteredTeamLeads = teamLeads.filter( | |||||
| (teamLead) => teamLead.teamId === teamId, | |||||
| ); | |||||
| const projectInfo = props.isEditMode | const projectInfo = props.isEditMode | ||||
| ? await fetchProjectDetails(props.projectId!) | ? await fetchProjectDetails(props.projectId!) | ||||
| : undefined; | : undefined; | ||||
| @@ -79,10 +82,10 @@ const CreateProjectWrapper: React.FC<Props> = async (props) => { | |||||
| ? await fetchMainProjects() | ? await fetchMainProjects() | ||||
| : undefined; | : undefined; | ||||
| console.log(projectInfo) | |||||
| return ( | return ( | ||||
| <CreateProject | <CreateProject | ||||
| isEditMode={props.isEditMode} | isEditMode={props.isEditMode} | ||||
| draftId={props.isEditMode ? undefined : props.draftId} | |||||
| isSubProject={Boolean(props.isSubProject)} | isSubProject={Boolean(props.isSubProject)} | ||||
| defaultInputs={projectInfo} | defaultInputs={projectInfo} | ||||
| allTasks={tasks} | allTasks={tasks} | ||||
| @@ -121,8 +121,8 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||||
| if (selectedCustomerId !== undefined) { | if (selectedCustomerId !== undefined) { | ||||
| fetchCustomer(selectedCustomerId).then( | fetchCustomer(selectedCustomerId).then( | ||||
| ({ contacts, subsidiaryIds, customer }) => { | ({ contacts, subsidiaryIds, customer }) => { | ||||
| console.log(contacts) | |||||
| console.log(subsidiaryIds) | |||||
| // console.log(contacts) | |||||
| // console.log(subsidiaryIds) | |||||
| setCustomerContacts(contacts); | setCustomerContacts(contacts); | ||||
| setCustomerSubsidiaryIds(subsidiaryIds); | setCustomerSubsidiaryIds(subsidiaryIds); | ||||
| setValue( | setValue( | ||||
| @@ -1,7 +1,7 @@ | |||||
| "use client"; | "use client"; | ||||
| import { ProjectCategory, ProjectResult } from "@/app/api/projects"; | import { ProjectCategory, ProjectResult } from "@/app/api/projects"; | ||||
| import React, { useCallback, useMemo, useState } from "react"; | |||||
| import React, { useCallback, useEffect, useMemo, useState } from "react"; | |||||
| import SearchBox, { Criterion } from "../SearchBox"; | import SearchBox, { Criterion } from "../SearchBox"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import SearchResults, { Column } from "../SearchResults"; | import SearchResults, { Column } from "../SearchResults"; | ||||
| @@ -9,23 +9,73 @@ import EditNote from "@mui/icons-material/EditNote"; | |||||
| import uniq from "lodash/uniq"; | import uniq from "lodash/uniq"; | ||||
| import { useRouter } from "next/navigation"; | import { useRouter } from "next/navigation"; | ||||
| import { MAINTAIN_PROJECT } from "@/middleware"; | import { MAINTAIN_PROJECT } from "@/middleware"; | ||||
| import { uniqBy } from "lodash"; | |||||
| import { reverse, uniqBy } from "lodash"; | |||||
| import { loadDrafts } from "@/app/utils/draftUtils"; | |||||
| import { TeamResult } from "@/app/api/team"; | |||||
| import { Customer } from "@/app/api/customer"; | |||||
| type ProjectResultOrDraft = ProjectResult & { isDraft?: boolean }; | |||||
| interface Props { | interface Props { | ||||
| projects: ProjectResult[]; | projects: ProjectResult[]; | ||||
| projectCategories: ProjectCategory[]; | projectCategories: ProjectCategory[]; | ||||
| abilities: string[] | |||||
| abilities: string[]; | |||||
| teams: TeamResult[]; | |||||
| customers: Customer[]; | |||||
| } | } | ||||
| type SearchQuery = Partial<Omit<ProjectResult, "id">>; | type SearchQuery = Partial<Omit<ProjectResult, "id">>; | ||||
| type SearchParamNames = keyof SearchQuery; | type SearchParamNames = keyof SearchQuery; | ||||
| const ProjectSearch: React.FC<Props> = ({ projects, projectCategories, abilities }) => { | |||||
| const ProjectSearch: React.FC<Props> = ({ | |||||
| projects, | |||||
| projectCategories, | |||||
| abilities, | |||||
| teams, | |||||
| customers, | |||||
| }) => { | |||||
| const router = useRouter(); | const router = useRouter(); | ||||
| const { t } = useTranslation("projects"); | const { t } = useTranslation("projects"); | ||||
| const [draftProjects, setDraftProjects] = useState<ProjectResultOrDraft[]>( | |||||
| [], | |||||
| ); | |||||
| useEffect(() => { | |||||
| const drafts = reverse(loadDrafts()); | |||||
| setDraftProjects( | |||||
| drafts.map(([id, inputs]) => { | |||||
| const team = teams.find( | |||||
| (team) => team.teamLead === inputs.projectLeadId, | |||||
| ); | |||||
| return { | |||||
| isDraft: true, | |||||
| id: parseInt(id), | |||||
| code: inputs.projectCode || "", | |||||
| name: inputs.projectName || t("Draft Project"), | |||||
| category: | |||||
| projectCategories.find((cat) => cat.id === inputs.projectCategoryId) | |||||
| ?.name || "", | |||||
| team: team?.code || "", | |||||
| client: | |||||
| customers.find((customer) => customer.id === inputs.clientId) | |||||
| ?.name || "", | |||||
| status: t("Draft"), | |||||
| teamCodeName: team?.code || "", | |||||
| teamId: team?.teamId || 0, | |||||
| mainProject: "", | |||||
| }; | |||||
| }), | |||||
| ); | |||||
| }, [projectCategories, t, teams, customers]); | |||||
| const [filteredProjects, setFilteredProjects] = useState(projects); | const [filteredProjects, setFilteredProjects] = useState(projects); | ||||
| const draftAndFilterdProjects = useMemo<ProjectResultOrDraft[]>( | |||||
| () => [...draftProjects, ...filteredProjects], | |||||
| [draftProjects, filteredProjects], | |||||
| ); | |||||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | ||||
| () => [ | () => [ | ||||
| { label: t("Project Code"), paramName: "code", type: "text" }, | { label: t("Project Code"), paramName: "code", type: "text" }, | ||||
| @@ -34,7 +84,13 @@ const ProjectSearch: React.FC<Props> = ({ projects, projectCategories, abilities | |||||
| label: t("Client Name"), | label: t("Client Name"), | ||||
| paramName: "client", | paramName: "client", | ||||
| type: "autocomplete", | type: "autocomplete", | ||||
| options: uniqBy(projects.map((project) => ({value: project.client, label: project.client})), "value").sort((a, b) => a.value >= b.value ? 1 : -1), | |||||
| options: uniqBy( | |||||
| projects.map((project) => ({ | |||||
| value: project.client, | |||||
| label: project.client, | |||||
| })), | |||||
| "value", | |||||
| ).sort((a, b) => (a.value >= b.value ? 1 : -1)), | |||||
| }, | }, | ||||
| { | { | ||||
| label: t("Project Category"), | label: t("Project Category"), | ||||
| @@ -63,8 +119,10 @@ const ProjectSearch: React.FC<Props> = ({ projects, projectCategories, abilities | |||||
| }, [projects]); | }, [projects]); | ||||
| const onProjectClick = useCallback( | const onProjectClick = useCallback( | ||||
| (project: ProjectResult) => { | |||||
| if (Boolean(project.mainProject)) { | |||||
| (project: ProjectResultOrDraft) => { | |||||
| if (project.isDraft && project.id) { | |||||
| router.push(`/projects/create?draftId=${project.id}`); | |||||
| } else if (Boolean(project.mainProject)) { | |||||
| router.push(`/projects/editSub?id=${project.id}`); | router.push(`/projects/editSub?id=${project.id}`); | ||||
| } else router.push(`/projects/edit?id=${project.id}`); | } else router.push(`/projects/edit?id=${project.id}`); | ||||
| }, | }, | ||||
| @@ -103,7 +161,8 @@ const ProjectSearch: React.FC<Props> = ({ projects, projectCategories, abilities | |||||
| (query.client === "All" || p.client === query.client) && | (query.client === "All" || p.client === query.client) && | ||||
| (query.category === "All" || p.category === query.category) && | (query.category === "All" || p.category === query.category) && | ||||
| // (query.team === "All" || p.team === query.team) && | // (query.team === "All" || p.team === query.team) && | ||||
| (query.team === "All" || query.team.toLowerCase().includes(p.team.toLowerCase())) && | |||||
| (query.team === "All" || | |||||
| query.team.toLowerCase().includes(p.team.toLowerCase())) && | |||||
| (query.status === "All" || p.status === query.status), | (query.status === "All" || p.status === query.status), | ||||
| ), | ), | ||||
| ); | ); | ||||
| @@ -111,7 +170,7 @@ const ProjectSearch: React.FC<Props> = ({ projects, projectCategories, abilities | |||||
| onReset={onReset} | onReset={onReset} | ||||
| /> | /> | ||||
| <SearchResults<ProjectResult> | <SearchResults<ProjectResult> | ||||
| items={filteredProjects} | |||||
| items={draftAndFilterdProjects} | |||||
| columns={columns} | columns={columns} | ||||
| /> | /> | ||||
| </> | </> | ||||
| @@ -6,6 +6,8 @@ import { fetchUserAbilities, fetchUserStaff } from "@/app/utils/fetchUtil"; | |||||
| import { authOptions } from "@/config/authConfig"; | import { authOptions } from "@/config/authConfig"; | ||||
| import { getServerSession } from "next-auth"; | import { getServerSession } from "next-auth"; | ||||
| import { VIEW_ALL_PROJECTS } from "@/middleware"; | import { VIEW_ALL_PROJECTS } from "@/middleware"; | ||||
| import { fetchTeam } from "@/app/api/team"; | |||||
| import { fetchAllCustomers } from "@/app/api/customer"; | |||||
| interface SubComponents { | interface SubComponents { | ||||
| Loading: typeof ProjectSearchLoading; | Loading: typeof ProjectSearchLoading; | ||||
| @@ -13,20 +15,31 @@ interface SubComponents { | |||||
| const ProjectSearchWrapper: React.FC & SubComponents = async () => { | const ProjectSearchWrapper: React.FC & SubComponents = async () => { | ||||
| const projectCategories = await fetchProjectCategories(); | const projectCategories = await fetchProjectCategories(); | ||||
| const userStaff = await fetchUserStaff() | |||||
| const teamId = userStaff?.teamId | |||||
| const userStaff = await fetchUserStaff(); | |||||
| const teamId = userStaff?.teamId; | |||||
| const projects = await fetchProjects(); | const projects = await fetchProjects(); | ||||
| const teams = await fetchTeam(); | |||||
| const customers = await fetchAllCustomers(); | |||||
| const abilities = await fetchUserAbilities() | |||||
| const isViewAllProjectRight = [VIEW_ALL_PROJECTS].some((ability) => abilities.includes(ability)) | |||||
| const abilities = await fetchUserAbilities(); | |||||
| const isViewAllProjectRight = [VIEW_ALL_PROJECTS].some((ability) => | |||||
| abilities.includes(ability), | |||||
| ); | |||||
| let filteredProjects = projects | |||||
| let filteredProjects = projects; | |||||
| if (!isViewAllProjectRight) { | if (!isViewAllProjectRight) { | ||||
| filteredProjects = projects.filter(project => project.teamId === teamId) | |||||
| filteredProjects = projects.filter((project) => project.teamId === teamId); | |||||
| } | } | ||||
| return <ProjectSearch projects={filteredProjects} projectCategories={projectCategories} abilities={abilities}/>; | |||||
| return ( | |||||
| <ProjectSearch | |||||
| projects={filteredProjects} | |||||
| projectCategories={projectCategories} | |||||
| abilities={abilities} | |||||
| teams={teams} | |||||
| customers={customers} | |||||
| /> | |||||
| ); | |||||
| }; | }; | ||||
| ProjectSearchWrapper.Loading = ProjectSearchLoading; | ProjectSearchWrapper.Loading = ProjectSearchLoading; | ||||