@@ -8,7 +8,7 @@ export default async function NotFound() { | |||
return ( | |||
<Stack spacing={2}> | |||
<Typography variant="h4">{t("Not Found")}</Typography> | |||
<Typography variant="body1">{t("The sub project was not found or there was no any main projects!")}</Typography> | |||
<Typography variant="body1">{t("There was no any main projects!")}</Typography> | |||
<Link href="/projects" component={NextLink} variant="body2"> | |||
{t("Return to all projects")} | |||
</Link> | |||
@@ -21,7 +21,7 @@ import { Metadata } from "next"; | |||
import { notFound } from "next/navigation"; | |||
export const metadata: Metadata = { | |||
title: "Create Project", | |||
title: "Create Sub Project", | |||
}; | |||
const Projects: React.FC = async () => { | |||
@@ -0,0 +1,17 @@ | |||
import { getServerI18n } from "@/i18n"; | |||
import { Stack, Typography, Link } from "@mui/material"; | |||
import NextLink from "next/link"; | |||
export default async function NotFound() { | |||
const { t } = await getServerI18n("projects", "common"); | |||
return ( | |||
<Stack spacing={2}> | |||
<Typography variant="h4">{t("Not Found")}</Typography> | |||
<Typography variant="body1">{t("The sub project was not found!")}</Typography> | |||
<Link href="/projects" component={NextLink} variant="body2"> | |||
{t("Return to all projects")} | |||
</Link> | |||
</Stack> | |||
); | |||
} |
@@ -0,0 +1,76 @@ | |||
import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer"; | |||
import { fetchGrades } from "@/app/api/grades"; | |||
import { | |||
fetchMainProjects, | |||
fetchProjectBuildingTypes, | |||
fetchProjectCategories, | |||
fetchProjectContractTypes, | |||
fetchProjectDetails, | |||
fetchProjectFundingTypes, | |||
fetchProjectLocationTypes, | |||
fetchProjectServiceTypes, | |||
fetchProjectWorkNatures, | |||
} from "@/app/api/projects"; | |||
import { preloadStaff, preloadTeamLeads } from "@/app/api/staff"; | |||
import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks"; | |||
import CreateProject from "@/components/CreateProject"; | |||
import { I18nProvider, getServerI18n } from "@/i18n"; | |||
import Typography from "@mui/material/Typography"; | |||
import { isArray } from "lodash"; | |||
import { Metadata } from "next"; | |||
import { notFound } from "next/navigation"; | |||
interface Props { | |||
searchParams: { [key: string]: string | string[] | undefined }; | |||
} | |||
export const metadata: Metadata = { | |||
title: "Edit Sub Project", | |||
}; | |||
const Projects: React.FC<Props> = async ({ searchParams }) => { | |||
const { t } = await getServerI18n("projects"); | |||
const projectId = searchParams["id"]; | |||
if (!projectId || isArray(projectId)) { | |||
notFound(); | |||
} | |||
// Preload necessary dependencies | |||
fetchAllTasks(); | |||
fetchTaskTemplates(); | |||
fetchProjectCategories(); | |||
fetchProjectContractTypes(); | |||
fetchProjectFundingTypes(); | |||
fetchProjectLocationTypes(); | |||
fetchProjectServiceTypes(); | |||
fetchProjectBuildingTypes(); | |||
fetchProjectWorkNatures(); | |||
fetchAllCustomers(); | |||
fetchAllSubsidiaries(); | |||
fetchGrades(); | |||
preloadTeamLeads(); | |||
preloadStaff(); | |||
try { | |||
await fetchProjectDetails(projectId); | |||
const data = await fetchMainProjects(); | |||
if (!Boolean(data) || data.length === 0) { | |||
notFound(); | |||
} | |||
} catch (e) { | |||
notFound(); | |||
} | |||
return ( | |||
<> | |||
<Typography variant="h4">{t("Edit Sub Project")}</Typography> | |||
<I18nProvider namespaces={["projects"]}> | |||
<CreateProject isEditMode isSubProject projectId={projectId}/> | |||
</I18nProvider> | |||
</> | |||
); | |||
}; | |||
export default Projects; |
@@ -36,8 +36,8 @@ export interface CreateProjectInputs { | |||
// Client details | |||
clientId: Customer["id"]; | |||
clientContactId?: number; | |||
clientSubsidiaryId?: number; | |||
subsidiaryContactId: number; | |||
clientSubsidiaryId?: number | null; | |||
subsidiaryContactId?: number; | |||
isSubsidiaryContact?: boolean; | |||
// Allocation | |||
@@ -13,6 +13,7 @@ export interface ProjectResult { | |||
team: string; | |||
client: string; | |||
status: string; | |||
mainProject: string; | |||
} | |||
export interface MainProject { | |||
@@ -1,6 +1,6 @@ | |||
"use client" | |||
import { Autocomplete, MenuItem, TextField, Checkbox } from "@mui/material"; | |||
import { Autocomplete, MenuItem, TextField, Checkbox, Chip } from "@mui/material"; | |||
import { Controller, FieldValues, Path, Control, RegisterOptions } from "react-hook-form"; | |||
import { useTranslation } from "react-i18next"; | |||
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank'; | |||
@@ -18,6 +18,7 @@ interface Props<T extends { id?: number | string | null; label?: string; name?: | |||
noOptionsText?: string, | |||
isMultiple?: boolean, | |||
rules?: RegisterOptions<FieldValues> | |||
disabled?: boolean, | |||
} | |||
function ControlledAutoComplete< | |||
@@ -27,11 +28,10 @@ function ControlledAutoComplete< | |||
props: Props<T, TField> | |||
) { | |||
const { t } = useTranslation() | |||
const { control, options, name, label, noOptionsText, isMultiple, rules } = props; | |||
const { control, options, name, label, noOptionsText, isMultiple, rules, disabled } = props; | |||
// set default value if value is null | |||
if (!Boolean(isMultiple) && !Boolean(control._formValues[name])) { | |||
console.log(name, control._formValues[name]) | |||
control._formValues[name] = options[0]?.id ?? undefined | |||
} else if (Boolean(isMultiple) && !Boolean(control._formValues[name])) { | |||
control._formValues[name] = [] | |||
@@ -42,7 +42,6 @@ function ControlledAutoComplete< | |||
name={name} | |||
control={control} | |||
rules={rules} | |||
render={({ field, fieldState, formState }) => { | |||
return ( | |||
@@ -51,7 +50,8 @@ function ControlledAutoComplete< | |||
multiple | |||
disableClearable | |||
disableCloseOnSelect | |||
disablePortal | |||
// disablePortal | |||
disabled={disabled} | |||
noOptionsText={noOptionsText ?? t("No Options")} | |||
value={options.filter(option => { | |||
return field.value?.includes(option.id) | |||
@@ -61,7 +61,7 @@ function ControlledAutoComplete< | |||
isOptionEqualToValue={(option, value) => option.id === value.id} | |||
renderOption={(params, option, { selected }) => { | |||
return ( | |||
<li {...params} key={option.id}> | |||
<li {...params} key={option?.id}> | |||
<Checkbox | |||
icon={icon} | |||
checkedIcon={checkedIcon} | |||
@@ -72,6 +72,11 @@ function ControlledAutoComplete< | |||
</li> | |||
); | |||
}} | |||
// renderTags={(tagValue, getTagProps) => { | |||
// return tagValue.map((option, index) => ( | |||
// <Chip {...getTagProps({ index })} key={option?.id} label={option.label ?? option.name} /> | |||
// )) | |||
// }} | |||
onChange={(event, value) => { | |||
field.onChange(value?.map(v => v.id)) | |||
}} | |||
@@ -80,7 +85,8 @@ function ControlledAutoComplete< | |||
: | |||
<Autocomplete | |||
disableClearable | |||
disablePortal | |||
// disablePortal | |||
disabled={disabled} | |||
noOptionsText={noOptionsText ?? t("No Options")} | |||
value={options.find(option => option.id === field.value) ?? options[0]} | |||
options={options} | |||
@@ -88,13 +94,18 @@ function ControlledAutoComplete< | |||
isOptionEqualToValue={(option, value) => option?.id === value?.id} | |||
renderOption={(params, option) => { | |||
return ( | |||
<MenuItem {...params} key={option.id} value={option.id}> | |||
<MenuItem {...params} key={option?.id} value={option.id}> | |||
{option.label ?? option.name} | |||
</MenuItem> | |||
); | |||
}} | |||
// renderTags={(tagValue, getTagProps) => { | |||
// return tagValue.map((option, index) => ( | |||
// <Chip {...getTagProps({ index })} key={option?.id} label={option.label ?? option.name} /> | |||
// )) | |||
// }} | |||
onChange={(event, value) => { | |||
field.onChange(value?.id) | |||
field.onChange(value?.id ?? null) | |||
}} | |||
renderInput={(params) => <TextField {...params} error={Boolean(formState.errors[name])} variant="outlined" label={label} />} | |||
/>) | |||
@@ -393,6 +393,7 @@ const CreateProject: React.FC<Props> = ({ | |||
projectCategories={projectCategories} | |||
teamLeads={teamLeads} | |||
isActive={tabIndex === 0} | |||
isEditMode={isEditMode} | |||
/> | |||
} | |||
{ | |||
@@ -4,16 +4,18 @@ import Card from "@mui/material/Card"; | |||
import CardContent from "@mui/material/CardContent"; | |||
import { useTranslation } from "react-i18next"; | |||
import Button from "@mui/material/Button"; | |||
import React, { useCallback, useEffect, useMemo, useState } from "react"; | |||
import React, { SyntheticEvent, useCallback, useEffect, useMemo, useState } from "react"; | |||
import CardActions from "@mui/material/CardActions"; | |||
import RestartAlt from "@mui/icons-material/RestartAlt"; | |||
import { | |||
Alert, | |||
Autocomplete, | |||
FormControl, | |||
InputLabel, | |||
MenuItem, | |||
Select, | |||
SelectChangeEvent, | |||
TextField, | |||
} from "@mui/material"; | |||
import { Task, TaskGroup } from "@/app/api/tasks"; | |||
import uniqBy from "lodash/uniqBy"; | |||
@@ -49,8 +51,8 @@ const Milestone: React.FC<Props> = ({ allTasks, isActive }) => { | |||
taskGroups[0].id, | |||
); | |||
const onSelectTaskGroup = useCallback( | |||
(event: SelectChangeEvent<TaskGroup["id"]>) => { | |||
const id = event.target.value; | |||
(event: SyntheticEvent<Element, Event>, value: NonNullable<TaskGroup>) => { | |||
const id = value.id; | |||
const newTaksGroupId = typeof id === "string" ? parseInt(id) : id; | |||
setCurrentTaskGroupId(newTaksGroupId); | |||
}, | |||
@@ -81,7 +83,7 @@ const Milestone: React.FC<Props> = ({ allTasks, isActive }) => { | |||
} | |||
// console.log(Object.keys(milestones).reduce((acc, key) => acc + milestones[parseFloat(key)].payments.reduce((acc2, value) => acc2 + value.amount, 0), 0)) | |||
if (hasError) { | |||
setError("milestones", {message: "milestones is not valid", type: "invalid"}) | |||
setError("milestones", { message: "milestones is not valid", type: "invalid" }) | |||
} else { | |||
clearErrors("milestones") | |||
} | |||
@@ -92,26 +94,32 @@ const Milestone: React.FC<Props> = ({ allTasks, isActive }) => { | |||
<Card sx={{ display: isActive ? "block" : "none" }}> | |||
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 2 }}> | |||
<FormControl> | |||
<InputLabel>{t("Task Stage")}</InputLabel> | |||
<Select | |||
label={t("Task Stage")} | |||
<Autocomplete | |||
disableClearable | |||
// disablePortal | |||
noOptionsText={t("No Task Stage")} | |||
value={taskGroups.find(taskGroup => taskGroup.id === currentTaskGroupId)} | |||
options={taskGroups} | |||
getOptionLabel={(taskGroup) => taskGroup.name} | |||
isOptionEqualToValue={(option, value) => option.id === value.id} | |||
renderOption={(params, option) => { | |||
return ( | |||
<MenuItem {...params} key={option.id} value={option.id}> | |||
{option.name} | |||
</MenuItem> | |||
); | |||
}} | |||
onChange={onSelectTaskGroup} | |||
value={currentTaskGroupId} | |||
> | |||
{taskGroups.map((taskGroup) => ( | |||
<MenuItem key={taskGroup.id} value={taskGroup.id}> | |||
{taskGroup.name} | |||
</MenuItem> | |||
))} | |||
</Select> | |||
renderInput={(params) => <TextField {...params} variant="outlined" label={t("Task Stage")} />} | |||
/> | |||
</FormControl> | |||
{/* MUI X-Grid will throw an error if it is rendered without any dimensions; so not rendering when not active */} | |||
{isActive && <MilestoneSection taskGroupId={currentTaskGroupId} />} | |||
<CardActions sx={{ justifyContent: "flex-end" }}> | |||
{/* <CardActions sx={{ justifyContent: "flex-end" }}> | |||
<Button variant="text" startIcon={<RestartAlt />}> | |||
{t("Reset")} | |||
</Button> | |||
</CardActions> | |||
</CardActions> */} | |||
</CardContent> | |||
</Card> | |||
<Card sx={{ display: isActive ? "block" : "none" }}> | |||
@@ -39,6 +39,7 @@ import ControlledAutoComplete from "../ControlledAutoComplete/ControlledAutoComp | |||
interface Props { | |||
isActive: boolean; | |||
isSubProject: boolean; | |||
isEditMode: boolean; | |||
mainProjects?: MainProject[]; | |||
projectCategories: ProjectCategory[]; | |||
teamLeads: StaffResult[]; | |||
@@ -55,6 +56,7 @@ interface Props { | |||
const ProjectClientDetails: React.FC<Props> = ({ | |||
isActive, | |||
isSubProject, | |||
isEditMode, | |||
mainProjects, | |||
projectCategories, | |||
teamLeads, | |||
@@ -110,6 +112,7 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||
); | |||
// get customer (client) contact combo | |||
const [firstCustomerLoaded, setFirstCustomerLoaded] = useState(false) | |||
useEffect(() => { | |||
if (selectedCustomerId !== undefined) { | |||
fetchCustomer(selectedCustomerId).then(({ contacts, subsidiaryIds }) => { | |||
@@ -118,7 +121,7 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||
// if (subsidiaryIds.length > 0) setValue("clientSubsidiaryId", subsidiaryIds[0]) | |||
// else | |||
setValue("clientSubsidiaryId", undefined) | |||
if (isEditMode && !firstCustomerLoaded) { setFirstCustomerLoaded(true) } else setValue("clientSubsidiaryId", null) | |||
// if (contacts.length > 0) setValue("clientContactId", contacts[0].id) | |||
// else setValue("clientContactId", undefined) | |||
}); | |||
@@ -130,11 +133,11 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||
if (Boolean(clientSubsidiaryId)) { | |||
// get subsidiary contact combo | |||
const contacts = allSubsidiaries.find(subsidiary => subsidiary.id === clientSubsidiaryId)?.subsidiaryContacts!! | |||
setSubsidiaryContacts(contacts) | |||
setSubsidiaryContacts(() => contacts) | |||
setValue("clientContactId", selectedCustomerId === defaultValues?.clientId && Boolean(defaultValues?.clientSubsidiaryId) ? contacts.find(contact => contact.id === defaultValues.clientContactId)?.id ?? contacts[0].id : contacts[0].id) | |||
setValue("isSubsidiaryContact", true) | |||
} else if (customerContacts?.length > 0) { | |||
setSubsidiaryContacts([]) | |||
setSubsidiaryContacts(() => []) | |||
setValue("clientContactId", selectedCustomerId === defaultValues?.clientId && !Boolean(defaultValues?.clientSubsidiaryId) ? customerContacts.find(contact => contact.id === defaultValues.clientContactId)?.id ?? customerContacts[0].id : customerContacts[0].id) | |||
setValue("isSubsidiaryContact", false) | |||
} | |||
@@ -153,7 +156,7 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||
// Automatically update the project & client details whene select a main project | |||
const mainProjectId = watch("mainProjectId") | |||
useEffect(() => { | |||
if (mainProjectId !== undefined && mainProjects !== undefined) { | |||
if (mainProjectId !== undefined && mainProjects !== undefined && !isEditMode) { | |||
const mainProject = mainProjects.find(project => project.projectId === mainProjectId); | |||
if (mainProject !== undefined) { | |||
@@ -174,7 +177,7 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||
setValue("clientContactId", mainProject.clientContactId) | |||
} | |||
} | |||
}, [getValues, mainProjectId, setValue]) | |||
}, [getValues, mainProjectId, setValue, isEditMode]) | |||
// const buildingTypeIdNameMap = buildingTypes.reduce<{ [id: number]: string }>( | |||
// (acc, building) => ({ ...acc, [building.id]: building.name }), | |||
@@ -202,6 +205,7 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||
name="mainProjectId" | |||
label={t("Main Project")} | |||
noOptionsText={t("No Main Project")} | |||
disabled={isEditMode} | |||
/> | |||
</Grid> | |||
<Grid item sx={{ display: { xs: "none", sm: "block" } }} /></> | |||
@@ -438,11 +442,11 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||
)} | |||
</Grid> | |||
</Box> | |||
<CardActions sx={{ justifyContent: "flex-end" }}> | |||
{/* <CardActions sx={{ justifyContent: "flex-end" }}> | |||
<Button variant="text" startIcon={<RestartAlt />}> | |||
{t("Reset")} | |||
</Button> | |||
</CardActions> | |||
</CardActions> */} | |||
</CardContent> | |||
</Card> | |||
); | |||
@@ -1,7 +1,7 @@ | |||
"use client"; | |||
import { useTranslation } from "react-i18next"; | |||
import React, { useEffect, useMemo } from "react"; | |||
import React, { SyntheticEvent, useEffect, useMemo } from "react"; | |||
import RestartAlt from "@mui/icons-material/RestartAlt"; | |||
import SearchResults, { Column } from "../SearchResults"; | |||
import { Search, Clear, PersonAdd, PersonRemove } from "@mui/icons-material"; | |||
@@ -160,8 +160,8 @@ const StaffAllocation: React.FC<Props> = ({ | |||
}, [columnFilters]); | |||
const [filters, setFilters] = React.useState(defaultFilterValues); | |||
const makeFilterSelect = React.useCallback( | |||
(filter: keyof StaffResult) => (event: SelectChangeEvent<string>) => { | |||
setFilters((f) => ({ ...f, [filter]: event.target.value })); | |||
(filter: keyof StaffResult) => (event: SyntheticEvent<Element, Event>, value: NonNullable<string>) => { | |||
setFilters((f) => ({ ...f, [filter]: value })); | |||
}, | |||
[], | |||
); | |||
@@ -239,20 +239,25 @@ const StaffAllocation: React.FC<Props> = ({ | |||
return ( | |||
<Grid key={`${filter.toString()}-${idx}`} item xs={3}> | |||
<FormControl fullWidth> | |||
<InputLabel size="small">{label}</InputLabel> | |||
<Select | |||
label={label} | |||
<Autocomplete | |||
disableClearable | |||
// disablePortal | |||
size="small" | |||
noOptionsText={t(`No ${label}`)} | |||
value={filters[filter]} | |||
options={["All", ...(filterValues[filter] ?? [])]} | |||
getOptionLabel={(filterValue) => filterValue} | |||
isOptionEqualToValue={(option, value) => option === value} | |||
renderOption={(params, option) => { | |||
return ( | |||
<MenuItem {...params} key={option} value={option}> | |||
{option} | |||
</MenuItem> | |||
); | |||
}} | |||
onChange={makeFilterSelect(filter)} | |||
> | |||
<MenuItem value={"All"}>{t("All")}</MenuItem> | |||
{filterValues[filter]?.map((option, index) => ( | |||
<MenuItem key={`${option}-${index}`} value={option}> | |||
{option} | |||
</MenuItem> | |||
))} | |||
</Select> | |||
renderInput={(params) => <TextField {...params} variant="outlined" label={t(label)} />} | |||
/> | |||
</FormControl> | |||
</Grid> | |||
); | |||
@@ -289,11 +294,11 @@ const StaffAllocation: React.FC<Props> = ({ | |||
)} | |||
</Box> | |||
</Stack> | |||
<CardActions sx={{ justifyContent: "flex-end" }}> | |||
{/* <CardActions sx={{ justifyContent: "flex-end" }}> | |||
<Button variant="text" startIcon={<RestartAlt />} onClick={reset}> | |||
{t("Reset")} | |||
</Button> | |||
</CardActions> | |||
</CardActions> */} | |||
</CardContent> | |||
</Card> | |||
{/* MUI X-Grid will throw an error if it is rendered without any dimensions; so not rendering when not active */} | |||
@@ -135,7 +135,7 @@ const TaskSetup: React.FC<Props> = ({ | |||
<Grid item xs={6}> | |||
<Autocomplete | |||
disableClearable | |||
disablePortal | |||
// disablePortal | |||
noOptionsText={t("No Task List Source")} | |||
value={taskTemplates.find(taskTemplate => taskTemplate.id === selectedTaskTemplateId)} | |||
options={[{id: "All", name: t("All tasks")}, ...taskTemplates.map(taskTemplate => ({id: taskTemplate.id, name: taskTemplate.name}))]} | |||
@@ -207,11 +207,11 @@ const TaskSetup: React.FC<Props> = ({ | |||
allItemsLabel={t("Task Pool")} | |||
selectedItemsLabel={t("Project Task List")} | |||
/> | |||
<CardActions sx={{ justifyContent: "flex-end" }}> | |||
{/* <CardActions sx={{ justifyContent: "flex-end" }}> | |||
<Button variant="text" startIcon={<RestartAlt />} onClick={onReset}> | |||
{t("Reset")} | |||
</Button> | |||
</CardActions> | |||
</CardActions> */} | |||
</CardContent> | |||
</Card> | |||
); | |||
@@ -20,7 +20,6 @@ type SearchParamNames = keyof SearchQuery; | |||
const ProjectSearch: React.FC<Props> = ({ projects, projectCategories }) => { | |||
const router = useRouter(); | |||
const { t } = useTranslation("projects"); | |||
console.log(projects) | |||
const [filteredProjects, setFilteredProjects] = useState(projects); | |||
@@ -62,7 +61,9 @@ const ProjectSearch: React.FC<Props> = ({ projects, projectCategories }) => { | |||
const onProjectClick = useCallback( | |||
(project: ProjectResult) => { | |||
router.push(`/projects/edit?id=${project.id}`); | |||
if (Boolean(project.mainProject)) { | |||
router.push(`/projects/edit/sub?id=${project.id}`); | |||
} else router.push(`/projects/edit?id=${project.id}`); | |||
}, | |||
[router], | |||
); | |||