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