@@ -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; | ||||