浏览代码

update project

tags/Baseline_30082024_FRONTEND_UAT
cyril.tsui 1年前
父节点
当前提交
8479f6fd10
共有 9 个文件被更改,包括 198 次插入92 次删除
  1. +7
    -4
      src/app/(main)/layout.tsx
  2. +3
    -0
      src/app/api/customer/index.ts
  3. +6
    -1
      src/app/api/projects/actions.ts
  4. +1
    -0
      src/app/api/projects/index.ts
  5. +6
    -2
      src/app/api/subsidiary/index.ts
  6. +17
    -8
      src/components/CreateProject/CreateProject.tsx
  7. +117
    -66
      src/components/CreateProject/ProjectClientDetails.tsx
  8. +32
    -10
      src/components/CreateProject/TaskSetup.tsx
  9. +9
    -1
      src/components/ProjectSearch/ProjectSearch.tsx

+ 7
- 4
src/app/(main)/layout.tsx 查看文件

@@ -6,6 +6,7 @@ import Box from "@mui/material/Box";
import { NAVIGATION_CONTENT_WIDTH } from "@/config/uiConfig";
import Stack from "@mui/material/Stack";
import Breadcrumb from "@/components/Breadcrumb";
import { I18nProvider } from "@/i18n";

export default async function MainLayout({
children,
@@ -31,10 +32,12 @@ export default async function MainLayout({
padding: { xs: "1rem", sm: "1.5rem", lg: "3rem" },
}}
>
<Stack spacing={2}>
<Breadcrumb />
{children}
</Stack>
<I18nProvider namespaces={["common"]}>
<Stack spacing={2}>
<Breadcrumb />
{children}
</Stack>
</I18nProvider>
</Box>
</>
);


+ 3
- 0
src/app/api/customer/index.ts 查看文件

@@ -11,6 +11,8 @@ export interface Customer {
address: string | null;
district: string | null;
customerType: CustomerType;

contacts: Contact[];
}

export interface SaveCustomerResponse {
@@ -40,6 +42,7 @@ export interface Subsidiary {
district: string | null;
email: string | null;
subsidiaryType: SubsidiaryType;
subsidiaryContacts: Contact[];
}

export interface SubsidiaryTable {


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

@@ -20,6 +20,8 @@ export interface CreateProjectInputs {
projectLeadId: number;
projectActualStart: string;
projectActualEnd: string;
projectStatus: string;
isClpProject: boolean;

// Project info
serviceTypeId: number;
@@ -28,11 +30,14 @@ export interface CreateProjectInputs {
locationId: number;
buildingTypeIds: number[];
workNatureIds: number[];
taskTemplateId?: number | "All";

// Client details
clientId: Customer["id"];
clientContactId: number;
clientContactId?: number;
clientSubsidiaryId?: number;
subsidiaryContactId: number;
isSubsidiaryContact?: boolean;

// Allocation
totalManhour: number;


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

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

export interface ProjectCategory {


+ 6
- 2
src/app/api/subsidiary/index.ts 查看文件

@@ -10,7 +10,9 @@ export interface Customer {
brNo: string | null;
address: string | null;
district: string | null;
customerType: CustomerType
customerType: CustomerType;
contacts: Contact[];
}

export interface CustomerTable {
@@ -40,7 +42,9 @@ export interface Subsidiary {
brNo: string | null;
address: string | null;
district: string | null;
subsidiaryType: SubsidiaryType
subsidiaryType: SubsidiaryType;

contacts: Contact[];
}

export interface SaveSubsidiaryResponse {


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

@@ -76,7 +76,7 @@ const hasErrorsInTab = (
switch (tabIndex) {
case 0:
return (
errors.projectName || errors.projectCode || errors.projectDescription
errors.projectName || errors.projectDescription || errors.clientId
);
case 2:
return (
@@ -114,6 +114,8 @@ const CreateProject: React.FC<Props> = ({
const { t } = useTranslation();
const router = useRouter();

console.log(defaultInputs)

const handleCancel = () => {
router.replace("/projects");
};
@@ -219,6 +221,7 @@ const CreateProject: React.FC<Props> = ({
data.projectActualEnd = dayjs().format("YYYY-MM-DD");
}

data.taskTemplateId = data.taskTemplateId === "All" ? undefined : data.taskTemplateId;
const response = await saveProject(data);

if (response.id > 0) {
@@ -248,7 +251,8 @@ const CreateProject: React.FC<Props> = ({
if (
errors.projectName ||
errors.projectDescription ||
errors.projectCode
// errors.projectCode ||
errors.clientId
) {
setTabIndex(0);
} else if (errors.totalManhour || errors.manhourPercentageByGrade || errors.taskGroups) {
@@ -266,6 +270,7 @@ const CreateProject: React.FC<Props> = ({
allocatedStaffIds: [],
milestones: {},
totalManhour: 0,
taskTemplateId: "All",
...defaultInputs,

// manhourPercentageByGrade should have a sensible default
@@ -289,7 +294,8 @@ const CreateProject: React.FC<Props> = ({
>
{isEditMode && !(formProps.getValues("projectDeleted") === true) && (
<Stack direction="row" gap={1}>
{!formProps.getValues("projectActualStart") && (
{/* {!formProps.getValues("projectActualStart") && ( */}
{formProps.getValues("projectStatus") === "Pending to Start" && (
<Button
name="start"
type="submit"
@@ -300,8 +306,9 @@ const CreateProject: React.FC<Props> = ({
{t("Start Project")}
</Button>
)}
{formProps.getValues("projectActualStart") &&
!formProps.getValues("projectActualEnd") && (
{/* {formProps.getValues("projectActualStart") &&
!formProps.getValues("projectActualEnd") && ( */}
{formProps.getValues("projectStatus") === "On-going" && (
<Button
name="complete"
type="submit"
@@ -313,8 +320,10 @@ const CreateProject: React.FC<Props> = ({
</Button>
)}
{!(
formProps.getValues("projectActualStart") &&
formProps.getValues("projectActualEnd")
// formProps.getValues("projectActualStart") &&
// formProps.getValues("projectActualEnd")
formProps.getValues("projectStatus") === "Completed" ||
formProps.getValues("projectStatus") === "Deleted"
) && (
<Button
variant="outlined"
@@ -412,7 +421,7 @@ const CreateProject: React.FC<Props> = ({
startIcon={<Check />}
type="submit"
disabled={
formProps.getValues("projectDeleted") === true ||
formProps.getValues("projectDeleted") === true || formProps.getValues("projectStatus") === "Deleted" ||
(!!formProps.getValues("projectActualStart") &&
!!formProps.getValues("projectActualEnd"))
}


+ 117
- 66
src/components/CreateProject/ProjectClientDetails.tsx 查看文件

@@ -85,6 +85,7 @@ const ProjectClientDetails: React.FC<Props> = ({
);

const [customerContacts, setCustomerContacts] = useState<Contact[]>([]);
const [subsidiaryContacts, setSubsidiaryContacts] = useState<Contact[]>([]);
const [customerSubsidiaryIds, setCustomerSubsidiaryIds] = useState<number[]>(
[],
);
@@ -92,21 +93,44 @@ const ProjectClientDetails: React.FC<Props> = ({
const selectedCustomerContactId = watch("clientContactId");
const selectedCustomerContact = useMemo(
() =>
customerContacts.find(
(contact) => contact.id === selectedCustomerContactId,
),
[customerContacts, selectedCustomerContactId],
subsidiaryContacts.length > 0 ?
subsidiaryContacts.find((contact) => contact.id === selectedCustomerContactId)
: customerContacts.find(
(contact) => contact.id === selectedCustomerContactId,
),
[subsidiaryContacts, customerContacts, selectedCustomerContactId],
);

// get customer (client) contact combo
useEffect(() => {
if (selectedCustomerId !== undefined) {
fetchCustomer(selectedCustomerId).then(({ contacts, subsidiaryIds }) => {
setCustomerContacts(contacts);
setCustomerSubsidiaryIds(subsidiaryIds);

if (subsidiaryIds.length > 0) setValue("clientSubsidiaryId", subsidiaryIds[0])
else setValue("clientSubsidiaryId", undefined)
// if (contacts.length > 0) setValue("clientContactId", contacts[0].id)
// else setValue("clientContactId", undefined)
});
}
}, [selectedCustomerId]);

const clientSubsidiaryId = watch("clientSubsidiaryId")
useEffect(() => {
if (Boolean(clientSubsidiaryId)) {
// get subsidiary contact combo
const contacts = allSubsidiaries.find(subsidiary => subsidiary.id === clientSubsidiaryId)?.subsidiaryContacts!!
setSubsidiaryContacts(contacts)
setValue("clientContactId", contacts[0].id)
setValue("isSubsidiaryContact", true)
} else if (customerContacts?.length > 0) {
setSubsidiaryContacts([])
setValue("clientContactId", customerContacts[0].id)
setValue("isSubsidiaryContact", false)
}
}, [customerContacts, clientSubsidiaryId]);

// Automatically add the team lead to the allocated staff list
const selectedTeamLeadId = watch("projectLeadId");
useEffect(() => {
@@ -139,10 +163,13 @@ const ProjectClientDetails: React.FC<Props> = ({
<TextField
label={t("Project Code")}
fullWidth
{...register("projectCode", {
required: "Project code required!",
})}
error={Boolean(errors.projectCode)}
disabled
{...register("projectCode",
// {
// required: "Project code required!",
// }
)}
// error={Boolean(errors.projectCode)}
/>
</Grid>
<Grid item xs={6}>
@@ -354,6 +381,16 @@ const ProjectClientDetails: React.FC<Props> = ({
{...register("expectedProjectFee", { valueAsNumber: true })}
/>
</Grid>

<Grid item xs={6}>
<Checkbox
{...register("isClpProject")}
defaultChecked={watch("isClpProject")}
/>
<Typography variant="overline" display="inline">
{t("CLP Project")}
</Typography>
</Grid>
</Grid>
</Box>

@@ -373,12 +410,15 @@ const ProjectClientDetails: React.FC<Props> = ({
</Stack>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6}>
<FormControl fullWidth>
<FormControl fullWidth error={Boolean(errors.clientId)}>
<InputLabel>{t("Client")}</InputLabel>
<Controller
defaultValue={allCustomers[0].id}
defaultValue={allCustomers[0]?.id}
control={control}
name="clientId"
rules={{
required: "Please select a client",
}}
render={({ field }) => (
<Select label={t("Client")} {...field}>
{allCustomers.map((customer, index) => (
@@ -408,6 +448,50 @@ const ProjectClientDetails: React.FC<Props> = ({
<Grid item sx={{ display: { xs: "none", sm: "block" } }} />
{customerContacts.length > 0 && (
<>
{customerSubsidiaryIds.length > 0 && (
<Grid item xs={6}>
<FormControl
fullWidth
error={Boolean(errors.clientSubsidiaryId)}
>
<InputLabel>{t("Client Subsidiary")}</InputLabel>
<Controller
// rules={{
// validate: (value) => {
// if (
// !customerSubsidiaryIds.find(
// (subsidiaryId) => subsidiaryId === value,
// )
// ) {
// return t("Please choose a valid subsidiary");
// } else return true;
// },
// }}
defaultValue={customerSubsidiaryIds[0]}
control={control}
name="clientSubsidiaryId"
render={({ field }) => (
<Select label={t("Client Subsidiary")} {...field}>
{customerSubsidiaryIds
.filter((subId) => subsidiaryMap[subId])
.map((subsidiaryId, index) => {
const subsidiary = subsidiaryMap[subsidiaryId];

return (
<MenuItem
key={`${subsidiaryId}-${index}`}
value={subsidiaryId}
>
{`${subsidiary.code} - ${subsidiary.name}`}
</MenuItem>
);
})}
</Select>
)}
/>
</FormControl>
</Grid>
)}
<Grid item xs={6}>
<FormControl
fullWidth
@@ -418,33 +502,44 @@ const ProjectClientDetails: React.FC<Props> = ({
rules={{
validate: (value) => {
if (
!customerContacts.find(
(customerContacts.length > 0 && !customerContacts.find(
(contact) => contact.id === value,
)) && (subsidiaryContacts?.length > 0 && !subsidiaryContacts.find(
(contact) => contact.id === value,
)
))
) {
return t("Please provide a valid contact");
} else return true;
},
}}
defaultValue={customerContacts[0].id}
defaultValue={subsidiaryContacts?.length > 0 ? subsidiaryContacts[0].id : customerContacts[0].id}
control={control}
name="clientContactId"
render={({ field }) => (
<Select label={t("Client Lead")} {...field}>
{customerContacts.map((contact, index) => (
<MenuItem
key={`${contact.id}-${index}`}
value={contact.id}
>
{contact.name}
</MenuItem>
))}
{subsidiaryContacts?.length > 0 ?
subsidiaryContacts.map((contact, index) => (
<MenuItem
key={`${contact.id}-${index}`}
value={contact.id}
>
{contact.name}
</MenuItem>
))
: customerContacts.map((contact, index) => (
<MenuItem
key={`${contact.id}-${index}`}
value={contact.id}
>
{contact.name}
</MenuItem>
))}
</Select>
)}
/>
</FormControl>
</Grid>
<Grid item sx={{ display: { xs: "none", sm: "block" } }} />
<Grid container sx={{ display: { xs: "none", sm: "block" } }} />
<Grid item xs={6}>
<TextField
label={t("Client Lead Phone Number")}
@@ -467,50 +562,6 @@ const ProjectClientDetails: React.FC<Props> = ({
</Grid>
</>
)}
{customerSubsidiaryIds.length > 0 && (
<Grid item xs={6}>
<FormControl
fullWidth
error={Boolean(errors.clientSubsidiaryId)}
>
<InputLabel>{t("Client Subsidiary")}</InputLabel>
<Controller
// rules={{
// validate: (value) => {
// if (
// !customerSubsidiaryIds.find(
// (subsidiaryId) => subsidiaryId === value,
// )
// ) {
// return t("Please choose a valid subsidiary");
// } else return true;
// },
// }}
defaultValue={customerSubsidiaryIds[0]}
control={control}
name="clientSubsidiaryId"
render={({ field }) => (
<Select label={t("Client Lead")} {...field}>
{customerSubsidiaryIds
.filter((subId) => subsidiaryMap[subId])
.map((subsidiaryId, index) => {
const subsidiary = subsidiaryMap[subsidiaryId];

return (
<MenuItem
key={`${subsidiaryId}-${index}`}
value={subsidiaryId}
>
{`${subsidiary.code} - ${subsidiary.name}`}
</MenuItem>
);
})}
</Select>
)}
/>
</FormControl>
</Grid>
)}
</Grid>
</Box>
<CardActions sx={{ justifyContent: "flex-end" }}>


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

@@ -33,7 +33,7 @@ const TaskSetup: React.FC<Props> = ({
isActive,
}) => {
const { t } = useTranslation();
const { setValue, watch, clearErrors, setError } = useFormContext<CreateProjectInputs>();
const { setValue, watch, clearErrors, setError, formState: { defaultValues } } = useFormContext<CreateProjectInputs>();
const currentTaskGroups = watch("taskGroups");
const currentTaskIds = Object.values(currentTaskGroups).reduce<Task["id"][]>(
(acc, group) => {
@@ -48,7 +48,7 @@ const TaskSetup: React.FC<Props> = ({

const [selectedTaskTemplateId, setSelectedTaskTemplateId] = useState<
"All" | number
>("All");
>(watch("taskTemplateId") ?? "All");
const onSelectTaskTemplate = useCallback(
(e: SelectChangeEvent<number | "All">) => {
if (e.target.value === "All" || isNumber(e.target.value)) {
@@ -64,7 +64,8 @@ const TaskSetup: React.FC<Props> = ({
(template) => template.id === selectedTaskTemplateId,
)

if (selectedTaskTemplateId !== "All") {
if (selectedTaskTemplateId !== "All" && selectedTaskTemplateId !== watch("taskTemplateId")) {

// update the "manhour allocation by grade" by task template
const updatedManhourPercentageByGrade: ManhourAllocation = watch("manhourPercentageByGrade")
selectedTaskTemplate?.gradeAllocations.forEach((gradeAllocation) => {
@@ -73,28 +74,30 @@ const TaskSetup: React.FC<Props> = ({

setValue("manhourPercentageByGrade", updatedManhourPercentageByGrade)
if (Object.values(updatedManhourPercentageByGrade).reduce((acc, value) => acc + value, 0) === 100) clearErrors("manhourPercentageByGrade")
else setError("manhourPercentageByGrade", {message: "manhourPercentageByGrade value is not valid", type: "invalid"})
else setError("manhourPercentageByGrade", { message: "manhourPercentageByGrade value is not valid", type: "invalid" })

// update the "manhour allocation by grade by stage" by task template
const updatedTaskGroups = watch("taskGroups")
const taskGroupsKeys = Object.keys(updatedTaskGroups)
selectedTaskTemplate?.groupAllocations.forEach((groupAllocation) => {
const taskGroupId = groupAllocation.taskGroup.id
if(taskGroupsKeys.includes(taskGroupId.toString())) {
updatedTaskGroups[taskGroupId] = {...updatedTaskGroups[taskGroupId], percentAllocation: groupAllocation?.percentage}
if (taskGroupsKeys.includes(taskGroupId.toString())) {
updatedTaskGroups[taskGroupId] = { ...updatedTaskGroups[taskGroupId], percentAllocation: groupAllocation?.percentage }
}
})

const percentageToZeroGroupIds = difference(taskGroupsKeys.map(key => parseFloat(key)), selectedTaskTemplate?.groupAllocations.map(groupAllocation => groupAllocation.taskGroup.id)!!)
percentageToZeroGroupIds.forEach((percentageToZeroGroupId) => {
updatedTaskGroups[percentageToZeroGroupId] = {...updatedTaskGroups[percentageToZeroGroupId], percentAllocation: 0}
updatedTaskGroups[percentageToZeroGroupId] = { ...updatedTaskGroups[percentageToZeroGroupId], percentAllocation: 0 }
})
setValue("taskGroups", updatedTaskGroups)
if (Object.values(updatedTaskGroups).reduce((acc, value) => acc + value.percentAllocation, 0) === 100) clearErrors("taskGroups")
else setError("taskGroups", {message: "Task Groups value is not invalid", type: "invalid"})
else setError("taskGroups", { message: "Task Groups value is not invalid", type: "invalid" })
}

setValue("taskTemplateId", selectedTaskTemplateId)

const taskList =
selectedTaskTemplateId === "All"
? tasks
@@ -176,7 +179,26 @@ const TaskSetup: React.FC<Props> = ({
};
}, {});

setValue("taskGroups", newTaskGroups);
// update the "manhour allocation by grade by stage" by task template
const taskGroupsKeys = Object.keys(newTaskGroups)
const selectedTaskTemplate = taskTemplates.find(
(template) => template.id === selectedTaskTemplateId,
)
selectedTaskTemplate?.groupAllocations.forEach((groupAllocation) => {
const taskGroupId = groupAllocation.taskGroup.id
if (taskGroupsKeys.includes(taskGroupId.toString())) {
newTaskGroups[taskGroupId] = { ...newTaskGroups[taskGroupId], percentAllocation: groupAllocation?.percentage }
}
})

const percentageToZeroGroupIds = difference(taskGroupsKeys.map(key => parseFloat(key)), selectedTaskTemplate?.groupAllocations.map(groupAllocation => groupAllocation.taskGroup.id)!!)
percentageToZeroGroupIds.forEach((percentageToZeroGroupId) => {
newTaskGroups[percentageToZeroGroupId] = { ...newTaskGroups[percentageToZeroGroupId], percentAllocation: 0 }
})

setValue("taskGroups", newTaskGroups)
if (Object.values(newTaskGroups).reduce((acc, value) => acc + value.percentAllocation, 0) === 100) clearErrors("taskGroups")
else setError("taskGroups", { message: "Task Groups value is not invalid", type: "invalid" })
}}
allItemsLabel={t("Task Pool")}
selectedItemsLabel={t("Project Task List")}


+ 9
- 1
src/components/ProjectSearch/ProjectSearch.tsx 查看文件

@@ -46,6 +46,12 @@ const ProjectSearch: React.FC<Props> = ({ projects, projectCategories }) => {
type: "select",
options: uniq(projects.map((project) => project.team)),
},
{
label: t("Status"),
paramName: "status",
type: "select",
options: uniq(projects.map((project) => project.status)),
},
],
[t, projectCategories, projects],
);
@@ -74,6 +80,7 @@ const ProjectSearch: React.FC<Props> = ({ projects, projectCategories }) => {
{ name: "category", label: t("Project Category") },
{ name: "team", label: t("Team") },
{ name: "client", label: t("Client") },
{ name: "status", label: t("Status") },
],
[t, onProjectClick],
);
@@ -90,7 +97,8 @@ const ProjectSearch: React.FC<Props> = ({ projects, projectCategories }) => {
p.name.toLowerCase().includes(query.name.toLowerCase()) &&
(query.client === "All" || p.client === query.client) &&
(query.category === "All" || p.category === query.category) &&
(query.team === "All" || p.team === query.team),
(query.team === "All" || p.team === query.team) &&
(query.status === "All" || p.status === query.status),
),
);
}}


正在加载...
取消
保存