| @@ -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 { | |||
| fetchProjectBuildingTypes, | |||
| @@ -16,6 +20,7 @@ import CreateProject from "@/components/CreateProject"; | |||
| import { I18nProvider, getServerI18n } from "@/i18n"; | |||
| import { MAINTAIN_PROJECT } from "@/middleware"; | |||
| import Typography from "@mui/material/Typography"; | |||
| import isString from "lodash/isString"; | |||
| import { Metadata } from "next"; | |||
| import { notFound } from "next/navigation"; | |||
| @@ -23,7 +28,11 @@ export const metadata: Metadata = { | |||
| 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 abilities = await fetchUserAbilities(); | |||
| @@ -32,6 +41,10 @@ const Projects: React.FC = async () => { | |||
| notFound(); | |||
| } | |||
| const draftId = isString(searchParams["draftId"]) | |||
| ? parseInt(searchParams["draftId"]) | |||
| : undefined; | |||
| // Preload necessary dependencies | |||
| fetchAllTasks(); | |||
| fetchTaskTemplates(); | |||
| @@ -53,7 +66,7 @@ const Projects: React.FC = async () => { | |||
| <> | |||
| <Typography variant="h4">{t("Create Project")}</Typography> | |||
| <I18nProvider namespaces={["projects"]}> | |||
| <CreateProject isEditMode={false} /> | |||
| <CreateProject isEditMode={false} draftId={draftId} /> | |||
| </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 ProjectSearch from "@/components/ProjectSearch"; | |||
| 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 Button from "@mui/material/Button"; | |||
| import Stack from "@mui/material/Stack"; | |||
| @@ -18,55 +20,62 @@ export const metadata: Metadata = { | |||
| const Projects: React.FC = async () => { | |||
| const { t } = await getServerI18n("projects"); | |||
| // preloadProjects(); | |||
| fetchProjectCategories(); | |||
| fetchTeam(); | |||
| fetchAllCustomers(); | |||
| 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(); | |||
| } | |||
| 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" | |||
| 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> | |||
| <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> | |||
| </> | |||
| ); | |||
| @@ -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 Check from "@mui/icons-material/Check"; | |||
| 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 Tab from "@mui/material/Tab"; | |||
| import Tabs, { TabsProps } from "@mui/material/Tabs"; | |||
| 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 ProjectClientDetails from "./ProjectClientDetails"; | |||
| import TaskSetup from "./TaskSetup"; | |||
| @@ -28,7 +34,7 @@ import { | |||
| deleteProject, | |||
| saveProject, | |||
| } from "@/app/api/projects/actions"; | |||
| import { Delete, Error, PlayArrow } from "@mui/icons-material"; | |||
| import { Delete, EditNote, Error, PlayArrow } from "@mui/icons-material"; | |||
| import { | |||
| BuildingType, | |||
| ContractType, | |||
| @@ -40,7 +46,7 @@ import { | |||
| WorkNature, | |||
| } from "@/app/api/projects"; | |||
| 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 { Customer, CustomerType, Subsidiary } from "@/app/api/customer"; | |||
| import { isEmpty } from "lodash"; | |||
| @@ -54,9 +60,11 @@ import { | |||
| 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; | |||
| @@ -106,6 +114,7 @@ const hasErrorsInTab = ( | |||
| const CreateProject: React.FC<Props> = ({ | |||
| isEditMode, | |||
| draftId, | |||
| isSubProject, | |||
| mainProjects, | |||
| defaultInputs, | |||
| @@ -127,11 +136,46 @@ const CreateProject: React.FC<Props> = ({ | |||
| abilities, | |||
| }) => { | |||
| const [serverError, setServerError] = useState(""); | |||
| const [loading, setLoading] = useState(true); | |||
| 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?"), | |||
| @@ -139,31 +183,64 @@ const CreateProject: React.FC<Props> = ({ | |||
| successTitle: t("Submit Success"), | |||
| errorTitle: t("Submit Fail"), | |||
| }; | |||
| const [buttonData, setButtonData] = useState<{ | |||
| const buttonData = useMemo<{ | |||
| buttonName: string; | |||
| title: string; | |||
| confirmButtonText: string; | |||
| successTitle: string; | |||
| errorTitle: 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 = () => { | |||
| router.replace("/projects"); | |||
| @@ -333,6 +410,9 @@ const CreateProject: React.FC<Props> = ({ | |||
| : buttonData.successTitle, | |||
| t, | |||
| ).then(() => { | |||
| if (draftId) { | |||
| deleteDraft(draftId); | |||
| } | |||
| router.replace("/projects"); | |||
| }); | |||
| } 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; | |||
| // auto calculate the total project manhour | |||
| const expectedProjectFee = formProps.watch("expectedProjectFee"); | |||
| const ratePerManhour = formProps.watch("ratePerManhour"); | |||
| 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( | |||
| "totalManhour", | |||
| Math.ceil(expectedProjectFee / ratePerManhour), | |||
| ); | |||
| } else { | |||
| setFirstLoaded(true); | |||
| firstLoadedRef.current = true; | |||
| } | |||
| }, [expectedProjectFee, ratePerManhour]); | |||
| React.useMemo(() => { | |||
| useEffect(() => { | |||
| if ( | |||
| expectedProjectFee > 0 && | |||
| ratePerManhour > 0 && | |||
| @@ -472,57 +523,24 @@ const CreateProject: React.FC<Props> = ({ | |||
| } | |||
| }, [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(() => { | |||
| 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 ( | |||
| <> | |||
| @@ -577,10 +595,8 @@ const CreateProject: React.FC<Props> = ({ | |||
| // formProps.getValues("projectActualStart") && | |||
| // formProps.getValues("projectActualEnd") | |||
| ( | |||
| formProps.getValues("projectStatus")?.toLowerCase() === | |||
| "completed" || | |||
| formProps.getValues("projectStatus")?.toLowerCase() === | |||
| "deleted" | |||
| projectStatus.toLowerCase() === "completed" || | |||
| projectStatus.toLowerCase() === "deleted" | |||
| ) | |||
| ) && | |||
| abilities.includes(DELETE_PROJECT) && ( | |||
| @@ -694,6 +710,19 @@ const CreateProject: React.FC<Props> = ({ | |||
| </Typography> | |||
| )} | |||
| <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 | |||
| variant="outlined" | |||
| startIcon={<Close />} | |||
| @@ -706,7 +735,7 @@ const CreateProject: React.FC<Props> = ({ | |||
| variant="contained" | |||
| startIcon={<Check />} | |||
| type="submit" | |||
| disabled={disableChecking()} | |||
| disabled={submitDisabled} | |||
| > | |||
| {isEditMode ? t("Save") : t("Confirm")} | |||
| </Button> | |||
| @@ -23,6 +23,7 @@ import { fetchUserAbilities, fetchUserStaff } from "@/app/utils/fetchUtil"; | |||
| type CreateProjectProps = { | |||
| isEditMode: false; | |||
| isSubProject?: boolean; | |||
| draftId?: number; | |||
| }; | |||
| interface EditProjectProps { | |||
| isEditMode: true; | |||
| @@ -68,9 +69,11 @@ const CreateProjectWrapper: React.FC<Props> = async (props) => { | |||
| fetchCustomerTypes(), | |||
| 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 | |||
| ? await fetchProjectDetails(props.projectId!) | |||
| : undefined; | |||
| @@ -79,10 +82,10 @@ const CreateProjectWrapper: React.FC<Props> = async (props) => { | |||
| ? await fetchMainProjects() | |||
| : undefined; | |||
| console.log(projectInfo) | |||
| return ( | |||
| <CreateProject | |||
| isEditMode={props.isEditMode} | |||
| draftId={props.isEditMode ? undefined : props.draftId} | |||
| isSubProject={Boolean(props.isSubProject)} | |||
| defaultInputs={projectInfo} | |||
| allTasks={tasks} | |||
| @@ -121,8 +121,8 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||
| if (selectedCustomerId !== undefined) { | |||
| fetchCustomer(selectedCustomerId).then( | |||
| ({ contacts, subsidiaryIds, customer }) => { | |||
| console.log(contacts) | |||
| console.log(subsidiaryIds) | |||
| // console.log(contacts) | |||
| // console.log(subsidiaryIds) | |||
| setCustomerContacts(contacts); | |||
| setCustomerSubsidiaryIds(subsidiaryIds); | |||
| setValue( | |||
| @@ -1,7 +1,7 @@ | |||
| "use client"; | |||
| 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 { useTranslation } from "react-i18next"; | |||
| import SearchResults, { Column } from "../SearchResults"; | |||
| @@ -9,23 +9,73 @@ import EditNote from "@mui/icons-material/EditNote"; | |||
| import uniq from "lodash/uniq"; | |||
| import { useRouter } from "next/navigation"; | |||
| 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 { | |||
| projects: ProjectResult[]; | |||
| projectCategories: ProjectCategory[]; | |||
| abilities: string[] | |||
| abilities: string[]; | |||
| teams: TeamResult[]; | |||
| customers: Customer[]; | |||
| } | |||
| type SearchQuery = Partial<Omit<ProjectResult, "id">>; | |||
| 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 { 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 draftAndFilterdProjects = useMemo<ProjectResultOrDraft[]>( | |||
| () => [...draftProjects, ...filteredProjects], | |||
| [draftProjects, filteredProjects], | |||
| ); | |||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | |||
| () => [ | |||
| { label: t("Project Code"), paramName: "code", type: "text" }, | |||
| @@ -34,7 +84,13 @@ const ProjectSearch: React.FC<Props> = ({ projects, projectCategories, abilities | |||
| label: t("Client Name"), | |||
| paramName: "client", | |||
| 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"), | |||
| @@ -63,8 +119,10 @@ const ProjectSearch: React.FC<Props> = ({ projects, projectCategories, abilities | |||
| }, [projects]); | |||
| 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}`); | |||
| } 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.category === "All" || p.category === query.category) && | |||
| // (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), | |||
| ), | |||
| ); | |||
| @@ -111,7 +170,7 @@ const ProjectSearch: React.FC<Props> = ({ projects, projectCategories, abilities | |||
| onReset={onReset} | |||
| /> | |||
| <SearchResults<ProjectResult> | |||
| items={filteredProjects} | |||
| items={draftAndFilterdProjects} | |||
| columns={columns} | |||
| /> | |||
| </> | |||
| @@ -6,6 +6,8 @@ import { fetchUserAbilities, fetchUserStaff } from "@/app/utils/fetchUtil"; | |||
| import { authOptions } from "@/config/authConfig"; | |||
| import { getServerSession } from "next-auth"; | |||
| import { VIEW_ALL_PROJECTS } from "@/middleware"; | |||
| import { fetchTeam } from "@/app/api/team"; | |||
| import { fetchAllCustomers } from "@/app/api/customer"; | |||
| interface SubComponents { | |||
| Loading: typeof ProjectSearchLoading; | |||
| @@ -13,20 +15,31 @@ interface SubComponents { | |||
| const ProjectSearchWrapper: React.FC & SubComponents = async () => { | |||
| 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 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) { | |||
| 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; | |||