浏览代码

Update project

tags/Baseline_30082024_FRONTEND_UAT
cyril.tsui 1年前
父节点
当前提交
e93c59551d
共有 13 个文件被更改,包括 182 次插入58 次删除
  1. +1
    -1
      src/app/(main)/projects/create/sub/not-found.tsx
  2. +1
    -1
      src/app/(main)/projects/create/sub/page.tsx
  3. +17
    -0
      src/app/(main)/projects/edit/sub/not-found.tsx
  4. +76
    -0
      src/app/(main)/projects/edit/sub/page.tsx
  5. +2
    -2
      src/app/api/projects/actions.ts
  6. +1
    -0
      src/app/api/projects/index.ts
  7. +20
    -9
      src/components/ControlledAutoComplete/ControlledAutoComplete.tsx
  8. +1
    -0
      src/components/CreateProject/CreateProject.tsx
  9. +25
    -17
      src/components/CreateProject/Milestone.tsx
  10. +11
    -7
      src/components/CreateProject/ProjectClientDetails.tsx
  11. +21
    -16
      src/components/CreateProject/StaffAllocation.tsx
  12. +3
    -3
      src/components/CreateProject/TaskSetup.tsx
  13. +3
    -2
      src/components/ProjectSearch/ProjectSearch.tsx

+ 1
- 1
src/app/(main)/projects/create/sub/not-found.tsx 查看文件

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


+ 1
- 1
src/app/(main)/projects/create/sub/page.tsx 查看文件

@@ -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 () => {


+ 17
- 0
src/app/(main)/projects/edit/sub/not-found.tsx 查看文件

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

+ 76
- 0
src/app/(main)/projects/edit/sub/page.tsx 查看文件

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

+ 2
- 2
src/app/api/projects/actions.ts 查看文件

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


+ 1
- 0
src/app/api/projects/index.ts 查看文件

@@ -13,6 +13,7 @@ export interface ProjectResult {
team: string;
client: string;
status: string;
mainProject: string;
}

export interface MainProject {


+ 20
- 9
src/components/ControlledAutoComplete/ControlledAutoComplete.tsx 查看文件

@@ -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} />}
/>)


+ 1
- 0
src/components/CreateProject/CreateProject.tsx 查看文件

@@ -393,6 +393,7 @@ const CreateProject: React.FC<Props> = ({
projectCategories={projectCategories}
teamLeads={teamLeads}
isActive={tabIndex === 0}
isEditMode={isEditMode}
/>
}
{


+ 25
- 17
src/components/CreateProject/Milestone.tsx 查看文件

@@ -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" }}>


+ 11
- 7
src/components/CreateProject/ProjectClientDetails.tsx 查看文件

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


+ 21
- 16
src/components/CreateProject/StaffAllocation.tsx 查看文件

@@ -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 */}


+ 3
- 3
src/components/CreateProject/TaskSetup.tsx 查看文件

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


+ 3
- 2
src/components/ProjectSearch/ProjectSearch.tsx 查看文件

@@ -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],
);


正在加载...
取消
保存