diff --git a/src/app/(main)/projects/create/page.tsx b/src/app/(main)/projects/create/page.tsx index c737430..f1672eb 100644 --- a/src/app/(main)/projects/create/page.tsx +++ b/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 ( diff --git a/src/app/api/customer/index.ts b/src/app/api/customer/index.ts index b35bac0..bb2f407 100644 --- a/src/app/api/customer/index.ts +++ b/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( - `${BASE_API_URL}/subsidiary`, - { - next: { tags: ["subsidiary"] }, - }, - ); + return serverFetchJson(`${BASE_API_URL}/subsidiary`, { + next: { tags: ["subsidiary"] }, + }); }); export const fetchCustomerTypes = cache(async () => { - return serverFetchJson( - `${BASE_API_URL}/customer/types`, - { - next: { tags: ["customerTypes"] }, - }, - ); -}); \ No newline at end of file + return serverFetchJson(`${BASE_API_URL}/customer/types`, { + next: { tags: ["customerTypes"] }, + }); +}); diff --git a/src/app/api/projects/actions.ts b/src/app/api/projects/actions.ts index cadc0c7..b745707 100644 --- a/src/app/api/projects/actions.ts +++ b/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 { diff --git a/src/app/api/projects/index.ts b/src/app/api/projects/index.ts index d55f1e9..e156c3c 100644 --- a/src/app/api/projects/index.ts +++ b/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( + `${BASE_API_URL}/projects/serviceTypes`, + { + next: { tags: ["projectServiceTypes"] }, + }, + ); +}); + +export const fetchProjectFundingTypes = cache(async () => { + return serverFetchJson( + `${BASE_API_URL}/projects/fundingTypes`, + { + next: { tags: ["projectFundingTypes"] }, + }, + ); +}); + +export const fetchProjectContractTypes = cache(async () => { + return serverFetchJson( + `${BASE_API_URL}/projects/contractTypes`, + { + next: { tags: ["projectContractTypes"] }, + }, + ); +}); + +export const fetchProjectLocationTypes = cache(async () => { + return serverFetchJson( + `${BASE_API_URL}/projects/locationTypes`, + { + next: { tags: ["projectLocationTypes"] }, + }, + ); +}); + +export const fetchProjectBuildingTypes = cache(async () => { + return serverFetchJson( + `${BASE_API_URL}/projects/buildingTypes`, + { + next: { tags: ["projectBuildingTypes"] }, + }, + ); +}); + +export const fetchProjectWorkNatures = cache(async () => { + return serverFetchJson(`${BASE_API_URL}/projects/workNatures`, { + next: { tags: ["projectWorkNatures"] }, + }); +}); diff --git a/src/components/CreateProject/CreateProject.tsx b/src/components/CreateProject/CreateProject.tsx index 2ec65cf..22299f2 100644 --- a/src/components/CreateProject/CreateProject.tsx +++ b/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 = ({ 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 = ({ 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 = ({ { { - 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 ( { 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" }, diff --git a/src/components/CreateProject/ProjectClientDetails.tsx b/src/components/CreateProject/ProjectClientDetails.tsx index abb2ba7..32db790 100644 --- a/src/components/CreateProject/ProjectClientDetails.tsx +++ b/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 = ({ isActive, projectCategories, - teamLeads + teamLeads, + allCustomers, + serviceTypes, + contractTypes, + fundingTypes, + locationTypes, + buildingTypes, + workNatures, }) => { const { t } = useTranslation(); const { register, formState: { errors }, + watch, control, } = useFormContext(); + const selectedCustomerId = watch("clientId"); + const selectedCustomer = useMemo( + () => allCustomers.find((c) => c.id === selectedCustomerId), + [allCustomers, selectedCustomerId], + ); + + const [customerContacts, setCustomerContacts] = useState([]); + const [customerSubsidiaryIds, setCustomerSubsidiaryIds] = useState( + [], + ); + + 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 ( @@ -107,6 +174,146 @@ const ProjectClientDetails: React.FC = ({ /> + + + {t("Service Type")} + ( + + )} + /> + + + + + {t("Funding Type")} + ( + + )} + /> + + + + + {t("Contract Type")} + ( + + )} + /> + + + + + {t("Location")} + ( + + )} + /> + + + + + {t("Building Types")} + ( + + )} + /> + + + + + + {t("Work Nature")} + ( + + )} + /> + + + = ({ label={t("Expected Total Project Fee")} fullWidth type="number" - {...register("expectedProjectFee")} + {...register("expectedProjectFee", { valueAsNumber: true })} /> - - {t("Client Details")} - + + + {t("Client Details")} + + + - - - - - - - - - - + + {t("Client")} + ( + + )} + /> + + - - - {t("Client Subsidiary")} - + {customerContacts.map((contact, index) => ( + + {contact.name} + + ))} + + )} + /> + + + + + + + + + + + )} + {customerSubsidiaryIds.length > 0 && ( + + - - {t("Test Subsidiary")} - - - - + {t("Client Subsidiary")} + { + 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 }) => ( + + )} + /> + + + )} diff --git a/src/components/CreateProject/ProjectTotalFee.tsx b/src/components/CreateProject/ProjectTotalFee.tsx index 97c61b9..a8809b9 100644 --- a/src/components/CreateProject/ProjectTotalFee.tsx +++ b/src/components/CreateProject/ProjectTotalFee.tsx @@ -14,7 +14,7 @@ const ProjectTotalFee: React.FC = ({ taskGroups }) => { const { t } = useTranslation(); const { watch } = useFormContext(); const milestones = watch("milestones"); - const expectedTotalFee = Number(watch("expectedProjectFee")); + const expectedTotalFee = watch("expectedProjectFee"); let projectTotal = 0;