瀏覽代碼

Add project types

tags/Baseline_30082024_FRONTEND_UAT
Wayne 1 年之前
父節點
當前提交
3bc18a6176
共有 8 個檔案被更改,包括 539 行新增80 行删除
  1. +15
    -1
      src/app/(main)/projects/create/page.tsx
  2. +8
    -14
      src/app/api/customer/index.ts
  3. +13
    -7
      src/app/api/projects/actions.ts
  4. +81
    -0
      src/app/api/projects/index.ts
  5. +31
    -3
      src/components/CreateProject/CreateProject.tsx
  6. +42
    -8
      src/components/CreateProject/CreateProjectWrapper.tsx
  7. +348
    -46
      src/components/CreateProject/ProjectClientDetails.tsx
  8. +1
    -1
      src/components/CreateProject/ProjectTotalFee.tsx

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

@@ -1,4 +1,12 @@
import { fetchProjectCategories } from "@/app/api/projects";
import {
fetchProjectBuildingTypes,
fetchProjectCategories,
fetchProjectContractTypes,
fetchProjectFundingTypes,
fetchProjectLocationTypes,
fetchProjectServiceTypes,
fetchProjectWorkNatures,
} from "@/app/api/projects";
import { preloadStaff } from "@/app/api/staff";
import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks";
import CreateProject from "@/components/CreateProject";
@@ -17,6 +25,12 @@ const Projects: React.FC = async () => {
fetchAllTasks();
fetchTaskTemplates();
fetchProjectCategories();
fetchProjectContractTypes();
fetchProjectFundingTypes();
fetchProjectLocationTypes();
fetchProjectServiceTypes();
fetchProjectBuildingTypes();
fetchProjectWorkNatures();
preloadStaff();

return (


+ 8
- 14
src/app/api/customer/index.ts 查看文件

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

export interface SaveCustomerResponse {
@@ -53,19 +53,13 @@ export const fetchAllCustomers = cache(async () => {
});

export const fetchSubsidiaries = cache(async () => {
return serverFetchJson<Subsidiary[]>(
`${BASE_API_URL}/subsidiary`,
{
next: { tags: ["subsidiary"] },
},
);
return serverFetchJson<Subsidiary[]>(`${BASE_API_URL}/subsidiary`, {
next: { tags: ["subsidiary"] },
});
});

export const fetchCustomerTypes = cache(async () => {
return serverFetchJson<CustomerType[]>(
`${BASE_API_URL}/customer/types`,
{
next: { tags: ["customerTypes"] },
},
);
});
return serverFetchJson<CustomerType[]>(`${BASE_API_URL}/customer/types`, {
next: { tags: ["customerTypes"] },
});
});

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

@@ -3,6 +3,7 @@
import { serverFetchJson } from "@/app/utils/fetchUtil";
import { BASE_API_URL } from "@/config/api";
import { Task, TaskGroup } from "../tasks";
import { Customer } from "../customer";

export interface CreateProjectInputs {
// Project details
@@ -12,13 +13,18 @@ export interface CreateProjectInputs {
projectDescription: string;
projectLeadId: number;

// Project info
serviceTypeId: number;
fundingTypeId: number;
contractTypeId: number;
locationId: number;
buildingTypeIds: number[];
workNatureIds: number[];

// Client details
clientCode: string;
clientName: string;
clientContactName: string;
clientPhone: string;
clientEmail: string;
clientSubsidiary: string;
clientId: Customer["id"];
clientContactId: number;
clientSubsidiaryId?: number;

// Tasks
tasks: {
@@ -50,7 +56,7 @@ export interface CreateProjectInputs {
};

// Miscellaneous
expectedProjectFee: string;
expectedProjectFee: number;
}

export interface ManhourAllocation {


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

@@ -18,6 +18,36 @@ export interface ProjectCategory {
name: string;
}

export interface ServiceType {
id: number;
name: string;
}

export interface FundingType {
id: number;
name: string;
}

export interface ContractType {
id: number;
name: string;
}

export interface LocationType {
id: number;
name: string;
}

export interface BuildingType {
id: number;
name: string;
}

export interface WorkNature {
id: number;
name: string;
}

export interface AssignedProject {
id: number;
code: string;
@@ -44,3 +74,54 @@ export const fetchProjectCategories = cache(async () => {
},
);
});

export const fetchProjectServiceTypes = cache(async () => {
return serverFetchJson<ServiceType[]>(
`${BASE_API_URL}/projects/serviceTypes`,
{
next: { tags: ["projectServiceTypes"] },
},
);
});

export const fetchProjectFundingTypes = cache(async () => {
return serverFetchJson<FundingType[]>(
`${BASE_API_URL}/projects/fundingTypes`,
{
next: { tags: ["projectFundingTypes"] },
},
);
});

export const fetchProjectContractTypes = cache(async () => {
return serverFetchJson<ContractType[]>(
`${BASE_API_URL}/projects/contractTypes`,
{
next: { tags: ["projectContractTypes"] },
},
);
});

export const fetchProjectLocationTypes = cache(async () => {
return serverFetchJson<LocationType[]>(
`${BASE_API_URL}/projects/locationTypes`,
{
next: { tags: ["projectLocationTypes"] },
},
);
});

export const fetchProjectBuildingTypes = cache(async () => {
return serverFetchJson<BuildingType[]>(
`${BASE_API_URL}/projects/buildingTypes`,
{
next: { tags: ["projectBuildingTypes"] },
},
);
});

export const fetchProjectWorkNatures = cache(async () => {
return serverFetchJson<WorkNature[]>(`${BASE_API_URL}/projects/workNatures`, {
next: { tags: ["projectWorkNatures"] },
});
});

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

@@ -23,16 +23,32 @@ import {
} from "react-hook-form";
import { CreateProjectInputs, saveProject } from "@/app/api/projects/actions";
import { Error } from "@mui/icons-material";
import { ProjectCategory } from "@/app/api/projects";
import {
BuildingType,
ContractType,
FundingType,
LocationType,
ProjectCategory,
ServiceType,
WorkNature,
} from "@/app/api/projects";
import { StaffResult } from "@/app/api/staff";
import { Typography } from "@mui/material";
import { Grade } from "@/app/api/grades";
import { Customer } from "@/app/api/customer";

export interface Props {
allTasks: Task[];
projectCategories: ProjectCategory[];
taskTemplates: TaskTemplate[];
teamLeads: StaffResult[];
allCustomers: Customer[];
fundingTypes: FundingType[];
serviceTypes: ServiceType[];
contractTypes: ContractType[];
locationTypes: LocationType[];
buildingTypes: BuildingType[];
workNatures: WorkNature[];

// Mocked
grades: Grade[];
@@ -56,6 +72,13 @@ const CreateProject: React.FC<Props> = ({
taskTemplates,
teamLeads,
grades,
allCustomers,
contractTypes,
fundingTypes,
locationTypes,
serviceTypes,
buildingTypes,
workNatures,
}) => {
const [serverError, setServerError] = useState("");
const [tabIndex, setTabIndex] = useState(0);
@@ -107,8 +130,6 @@ const CreateProject: React.FC<Props> = ({
manhourPercentageByGrade: grades.reduce((acc, grade) => {
return { ...acc, [grade.id]: 1 / grades.length };
}, {}),
// TODO: Remove this
clientSubsidiary: "Test subsidiary",
},
});

@@ -137,6 +158,13 @@ const CreateProject: React.FC<Props> = ({
</Tabs>
{
<ProjectClientDetails
buildingTypes={buildingTypes}
workNatures={workNatures}
contractTypes={contractTypes}
fundingTypes={fundingTypes}
locationTypes={locationTypes}
serviceTypes={serviceTypes}
allCustomers={allCustomers}
projectCategories={projectCategories}
teamLeads={teamLeads}
isActive={tabIndex === 0}


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

@@ -1,16 +1,43 @@
import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks";
import CreateProject from "./CreateProject";
import { fetchProjectCategories } from "@/app/api/projects";
import {
fetchProjectBuildingTypes,
fetchProjectCategories,
fetchProjectContractTypes,
fetchProjectFundingTypes,
fetchProjectLocationTypes,
fetchProjectServiceTypes,
fetchProjectWorkNatures,
} from "@/app/api/projects";
import { fetchTeamLeads } from "@/app/api/staff";
import { fetchAllCustomers } from "@/app/api/customer";

const CreateProjectWrapper: React.FC = async () => {
const [tasks, taskTemplates, projectCategories, teamLeads] =
await Promise.all([
fetchAllTasks(),
fetchTaskTemplates(),
fetchProjectCategories(),
fetchTeamLeads(),
]);
const [
tasks,
taskTemplates,
projectCategories,
teamLeads,
allCustomers,
contractTypes,
fundingTypes,
locationTypes,
serviceTypes,
buildingTypes,
workNatures,
] = await Promise.all([
fetchAllTasks(),
fetchTaskTemplates(),
fetchProjectCategories(),
fetchTeamLeads(),
fetchAllCustomers(),
fetchProjectContractTypes(),
fetchProjectFundingTypes(),
fetchProjectLocationTypes(),
fetchProjectServiceTypes(),
fetchProjectBuildingTypes(),
fetchProjectWorkNatures(),
]);

return (
<CreateProject
@@ -18,6 +45,13 @@ const CreateProjectWrapper: React.FC = async () => {
projectCategories={projectCategories}
taskTemplates={taskTemplates}
teamLeads={teamLeads}
allCustomers={allCustomers}
contractTypes={contractTypes}
fundingTypes={fundingTypes}
locationTypes={locationTypes}
serviceTypes={serviceTypes}
buildingTypes={buildingTypes}
workNatures={workNatures}
// Mocks
grades={[
{ name: "Grade 1", id: 1, code: "1" },


+ 348
- 46
src/components/CreateProject/ProjectClientDetails.tsx 查看文件

@@ -17,27 +17,94 @@ import RestartAlt from "@mui/icons-material/RestartAlt";
import Button from "@mui/material/Button";
import { Controller, useFormContext } from "react-hook-form";
import { CreateProjectInputs } from "@/app/api/projects/actions";
import { ProjectCategory } from "@/app/api/projects";
import {
BuildingType,
ContractType,
FundingType,
LocationType,
ProjectCategory,
ServiceType,
WorkNature,
} from "@/app/api/projects";
import { StaffResult } from "@/app/api/staff";
import { Contact, Customer } from "@/app/api/customer";
import Link from "next/link";
import React, { useEffect, useMemo, useState } from "react";
import { fetchCustomer } from "@/app/api/customer/actions";
import { Checkbox, ListItemText } from "@mui/material";

interface Props {
isActive: boolean;
projectCategories: ProjectCategory[];
teamLeads: StaffResult[];
allCustomers: Customer[];
serviceTypes: ServiceType[];
contractTypes: ContractType[];
fundingTypes: FundingType[];
locationTypes: LocationType[];
buildingTypes: BuildingType[];
workNatures: WorkNature[];
}

const ProjectClientDetails: React.FC<Props> = ({
isActive,
projectCategories,
teamLeads
teamLeads,
allCustomers,
serviceTypes,
contractTypes,
fundingTypes,
locationTypes,
buildingTypes,
workNatures,
}) => {
const { t } = useTranslation();
const {
register,
formState: { errors },
watch,
control,
} = useFormContext<CreateProjectInputs>();

const selectedCustomerId = watch("clientId");
const selectedCustomer = useMemo(
() => allCustomers.find((c) => c.id === selectedCustomerId),
[allCustomers, selectedCustomerId],
);

const [customerContacts, setCustomerContacts] = useState<Contact[]>([]);
const [customerSubsidiaryIds, setCustomerSubsidiaryIds] = useState<number[]>(
[],
);

const selectedCustomerContactId = watch("clientContactId");
const selectedCustomerContact = useMemo(
() =>
customerContacts.find(
(contact) => contact.id === selectedCustomerContactId,
),
[customerContacts, selectedCustomerContactId],
);

useEffect(() => {
if (selectedCustomerId !== undefined) {
fetchCustomer(selectedCustomerId).then(({ contacts, subsidiaryIds }) => {
setCustomerContacts(contacts);
setCustomerSubsidiaryIds(subsidiaryIds);
});
}
}, [selectedCustomerId]);

const buildingTypeIdNameMap = buildingTypes.reduce<{ [id: number]: string }>(
(acc, building) => ({ ...acc, [building.id]: building.name }),
{},
);

const workNatureIdNameMap = workNatures.reduce<{ [id: number]: string }>(
(acc, wn) => ({ ...acc, [wn.id]: wn.name }),
{},
);

return (
<Card sx={{ display: isActive ? "block" : "none" }}>
<CardContent component={Stack} spacing={4}>
@@ -107,6 +174,146 @@ const ProjectClientDetails: React.FC<Props> = ({
/>
</FormControl>
</Grid>
<Grid item xs={6}>
<FormControl fullWidth>
<InputLabel>{t("Service Type")}</InputLabel>
<Controller
defaultValue={serviceTypes[0].id}
control={control}
name="serviceTypeId"
render={({ field }) => (
<Select label={t("Service Type")} {...field}>
{serviceTypes.map((type, index) => (
<MenuItem key={`${type.id}-${index}`} value={type.id}>
{type.name}
</MenuItem>
))}
</Select>
)}
/>
</FormControl>
</Grid>
<Grid item xs={6}>
<FormControl fullWidth>
<InputLabel>{t("Funding Type")}</InputLabel>
<Controller
defaultValue={fundingTypes[0].id}
control={control}
name="fundingTypeId"
render={({ field }) => (
<Select label={t("Funding Type")} {...field}>
{fundingTypes.map((type, index) => (
<MenuItem key={`${type.id}-${index}`} value={type.id}>
{type.name}
</MenuItem>
))}
</Select>
)}
/>
</FormControl>
</Grid>
<Grid item xs={6}>
<FormControl fullWidth>
<InputLabel>{t("Contract Type")}</InputLabel>
<Controller
defaultValue={contractTypes[0].id}
control={control}
name="contractTypeId"
render={({ field }) => (
<Select label={t("Contract Type")} {...field}>
{contractTypes.map((type, index) => (
<MenuItem key={`${type.id}-${index}`} value={type.id}>
{type.name}
</MenuItem>
))}
</Select>
)}
/>
</FormControl>
</Grid>
<Grid item xs={6}>
<FormControl fullWidth>
<InputLabel>{t("Location")}</InputLabel>
<Controller
defaultValue={locationTypes[0].id}
control={control}
name="locationId"
render={({ field }) => (
<Select label={t("Location")} {...field}>
{locationTypes.map((type, index) => (
<MenuItem key={`${type.id}-${index}`} value={type.id}>
{type.name}
</MenuItem>
))}
</Select>
)}
/>
</FormControl>
</Grid>
<Grid item xs={6}>
<FormControl fullWidth>
<InputLabel>{t("Building Types")}</InputLabel>
<Controller
defaultValue={[]}
control={control}
name="buildingTypeIds"
render={({ field }) => (
<Select
renderValue={(types) =>
types
.map((type) => buildingTypeIdNameMap[type])
.join(", ")
}
multiple
label={t("Building Types")}
{...field}
>
{buildingTypes.map((type, index) => (
<MenuItem key={`${type.id}-${index}`} value={type.id}>
<Checkbox
checked={field.value.indexOf(type.id) > -1}
/>
<ListItemText primary={type.name} />
</MenuItem>
))}
</Select>
)}
/>
</FormControl>
</Grid>

<Grid item xs={6}>
<FormControl fullWidth>
<InputLabel>{t("Work Nature")}</InputLabel>
<Controller
defaultValue={[]}
control={control}
name="workNatureIds"
render={({ field }) => (
<Select
renderValue={(types) =>
types
.map((type) => workNatureIdNameMap[type])
.join(", ")
}
multiple
label={t("Work Nature")}
{...field}
>
{workNatures.map((type, index) => (
<MenuItem key={`${type.id}-${index}`} value={type.id}>
<Checkbox
checked={field.value.indexOf(type.id) > -1}
/>
<ListItemText primary={type.name} />
</MenuItem>
))}
</Select>
)}
/>
</FormControl>
</Grid>

<Grid item xs={6}>
<TextField
label={t("Project Description")}
@@ -122,65 +329,160 @@ const ProjectClientDetails: React.FC<Props> = ({
label={t("Expected Total Project Fee")}
fullWidth
type="number"
{...register("expectedProjectFee")}
{...register("expectedProjectFee", { valueAsNumber: true })}
/>
</Grid>
</Grid>
</Box>

<Box>
<Typography variant="overline" display="block" marginBlockEnd={1}>
{t("Client Details")}
</Typography>
<Stack
direction="row"
alignItems="center"
marginBlockEnd={1}
spacing={2}
>
<Typography variant="overline" display="block">
{t("Client Details")}
</Typography>
<Button LinkComponent={Link} href="/settings/customer">
{t("Add or Edit Clients")}
</Button>
</Stack>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6}>
<TextField
label={t("Client Code")}
fullWidth
{...register("clientCode")}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Client Name")}
fullWidth
{...register("clientName")}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Client Lead Name")}
fullWidth
{...register("clientContactName")}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Client Lead Phone Number")}
fullWidth
{...register("clientPhone")}
/>
<FormControl fullWidth>
<InputLabel>{t("Client")}</InputLabel>
<Controller
defaultValue={allCustomers[0].id}
control={control}
name="clientId"
render={({ field }) => (
<Select label={t("Client")} {...field}>
{allCustomers.map((customer, index) => (
<MenuItem
key={`${customer.id}-${index}`}
value={customer.id}
>
{`${customer.code} - ${customer.name}`}
</MenuItem>
))}
</Select>
)}
/>
</FormControl>
</Grid>
<Grid item sx={{ display: { xs: "none", sm: "block" } }} />
<Grid item xs={6}>
<TextField
label={t("Client Lead Email")}
label={t("Client Type")}
InputProps={{
readOnly: true,
}}
fullWidth
{...register("clientEmail")}
value={selectedCustomer?.customerType.name || ""}
/>
</Grid>
<Grid item xs={6}>
<FormControl fullWidth>
<InputLabel>{t("Client Subsidiary")}</InputLabel>
<Select
label={t("Client Subsidiary")}
value={"Test Subsidiary"}
<Grid item sx={{ display: { xs: "none", sm: "block" } }} />
{customerContacts.length > 0 && (
<>
<Grid item xs={6}>
<FormControl
fullWidth
error={Boolean(errors.clientContactId)}
>
<InputLabel>{t("Client Lead")}</InputLabel>
<Controller
rules={{
validate: (value) => {
if (
!customerContacts.find(
(contact) => contact.id === value,
)
) {
return t("Please provide a valid contact");
} else return true;
},
}}
defaultValue={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>
))}
</Select>
)}
/>
</FormControl>
</Grid>
<Grid item sx={{ display: { xs: "none", sm: "block" } }} />
<Grid item xs={6}>
<TextField
label={t("Client Lead Phone Number")}
fullWidth
InputProps={{
readOnly: true,
}}
value={selectedCustomerContact?.phone || ""}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Client Lead Email")}
fullWidth
InputProps={{
readOnly: true,
}}
value={selectedCustomerContact?.email || ""}
/>
</Grid>
</>
)}
{customerSubsidiaryIds.length > 0 && (
<Grid item xs={6}>
<FormControl
fullWidth
error={Boolean(errors.clientSubsidiaryId)}
>
<MenuItem value={"Test Subsidiary"}>
{t("Test Subsidiary")}
</MenuItem>
</Select>
</FormControl>
</Grid>
<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.map((subsidiaryId, index) => (
<MenuItem
key={`${subsidiaryId}-${index}`}
value={subsidiaryId}
>
{subsidiaryId}
</MenuItem>
))}
</Select>
)}
/>
</FormControl>
</Grid>
)}
</Grid>
</Box>
<CardActions sx={{ justifyContent: "flex-end" }}>


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

@@ -14,7 +14,7 @@ const ProjectTotalFee: React.FC<Props> = ({ taskGroups }) => {
const { t } = useTranslation();
const { watch } = useFormContext<CreateProjectInputs>();
const milestones = watch("milestones");
const expectedTotalFee = Number(watch("expectedProjectFee"));
const expectedTotalFee = watch("expectedProjectFee");

let projectTotal = 0;



Loading…
取消
儲存