瀏覽代碼

Add project apis

tags/Baseline_30082024_FRONTEND_UAT
Wayne 1 年之前
父節點
當前提交
23e3faddd5
共有 13 個檔案被更改,包括 158 行新增34 行删除
  1. +2
    -0
      src/app/(main)/projects/create/page.tsx
  2. +4
    -0
      src/app/api/projects/actions.ts
  3. +42
    -9
      src/app/api/projects/index.ts
  4. +24
    -0
      src/app/api/staff/index.ts
  5. +1
    -0
      src/app/utils/fetchUtil.ts
  6. +27
    -4
      src/components/CreateProject/CreateProject.tsx
  7. +9
    -3
      src/components/CreateProject/CreateProjectWrapper.tsx
  8. +35
    -9
      src/components/CreateProject/ProjectClientDetails.tsx
  9. +6
    -0
      src/components/CreateProject/ProjectTotalFee.tsx
  10. +3
    -4
      src/components/CreateProject/ResourceMilestone.tsx
  11. +1
    -1
      src/components/CreateProject/ResourceSection.tsx
  12. +2
    -2
      src/components/ProjectSearch/ProjectSearch.tsx
  13. +2
    -2
      src/components/SearchBox/SearchBox.tsx

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

@@ -1,4 +1,5 @@
import { fetchProjectCategories } from "@/app/api/projects";
import { preloadStaff } from "@/app/api/staff";
import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks";
import CreateProject from "@/components/CreateProject";
import { I18nProvider, getServerI18n } from "@/i18n";
@@ -16,6 +17,7 @@ const Projects: React.FC = async () => {
fetchAllTasks();
fetchTaskTemplates();
fetchProjectCategories();
preloadStaff();

return (
<>


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

@@ -10,6 +10,7 @@ export interface CreateProjectInputs {
projectName: string;
projectCategoryId: number;
projectDescription: string;
projectLeadId: number;

// Client details
clientCode: string;
@@ -37,6 +38,9 @@ export interface CreateProjectInputs {
payments: PaymentInputs[];
};
};

// Miscellaneous
expectedProjectFee: string;
}

export interface ManhourAllocation {


+ 42
- 9
src/app/api/projects/index.ts 查看文件

@@ -1,18 +1,34 @@
import { serverFetchJson } from "@/app/utils/fetchUtil";
import { BASE_API_URL } from "@/config/api";
import { cache } from "react";
import "server-only";
import { Staff } from "../staff";

interface Project {
id: number;
code: string;
name: string;
projectCategory: {
name: string;
};
teamLead: Staff;
customer: {
name: string;
};
}

export interface ProjectResult {
id: number;
code: string;
name: string;
category: "Confirmed Project" | "Project to be bidded";
category: string;
team: string;
client: string;
}

export interface ProjectCategory {
id: number;
label: string;
name: string;
}

export const preloadProjects = () => {
@@ -21,18 +37,35 @@ export const preloadProjects = () => {
};

export const fetchProjects = cache(async () => {
return mockProjects;
const projects = await serverFetchJson<Project[]>(
`${BASE_API_URL}/projects`,
{
next: { tags: ["projects"] },
},
);

// TODO: Replace this with a project
return projects.map<ProjectResult>(
({ id, code, name, projectCategory, teamLead, customer }) => ({
id,
code,
name,
category: projectCategory.name,
team: teamLead.team.code,
client: customer.name,
}),
);
});

export const fetchProjectCategories = cache(async () => {
return mockProjectCategories;
return serverFetchJson<ProjectCategory[]>(
`${BASE_API_URL}/projects/categories`,
{
next: { tags: ["projectCategories"] },
},
);
});

const mockProjectCategories: ProjectCategory[] = [
{ id: 1, label: "Confirmed Project" },
{ id: 2, label: "Project to be bidded" },
];

const mockProjects: ProjectResult[] = [
{
id: 1,


+ 24
- 0
src/app/api/staff/index.ts 查看文件

@@ -0,0 +1,24 @@
import { serverFetchJson } from "@/app/utils/fetchUtil";
import { BASE_API_URL } from "@/config/api";
import { cache } from "react";
import "server-only";

export interface Staff {
id: number;
name: string;
staffId: string;
team: {
name: string;
code: string;
};
}

export const preloadStaff = () => {
fetchTeamLeads();
};

export const fetchTeamLeads = cache(async () => {
return serverFetchJson<Staff[]>(`${BASE_API_URL}/staffs/teamLeads`, {
next: { tags: ["teamLeads"] },
});
});

+ 1
- 0
src/app/utils/fetchUtil.ts 查看文件

@@ -32,6 +32,7 @@ export async function serverFetchJson<T>(...args: FetchParams) {
case 401:
signOutUser();
default:
console.error(await response.text());
throw Error("Something went wrong fetching data in server.");
}
}


+ 27
- 4
src/components/CreateProject/CreateProject.tsx 查看文件

@@ -21,14 +21,17 @@ import {
SubmitHandler,
useForm,
} from "react-hook-form";
import { CreateProjectInputs } from "@/app/api/projects/actions";
import { CreateProjectInputs, saveProject } from "@/app/api/projects/actions";
import { Error } from "@mui/icons-material";
import { ProjectCategory } from "@/app/api/projects";
import { Staff } from "@/app/api/staff";
import { Typography } from "@mui/material";

export interface Props {
allTasks: Task[];
projectCategories: ProjectCategory[];
taskTemplates: TaskTemplate[];
teamLeads: Staff[];
}

const hasErrorsInTab = (
@@ -47,7 +50,9 @@ const CreateProject: React.FC<Props> = ({
allTasks,
projectCategories,
taskTemplates,
teamLeads,
}) => {
const [serverError, setServerError] = useState("");
const [tabIndex, setTabIndex] = useState(0);
const { t } = useTranslation();
const router = useRouter();
@@ -63,9 +68,19 @@ const CreateProject: React.FC<Props> = ({
[],
);

const onSubmit = useCallback<SubmitHandler<CreateProjectInputs>>((data) => {
console.log(data);
}, []);
const onSubmit = useCallback<SubmitHandler<CreateProjectInputs>>(
async (data) => {
try {
console.log(data);
setServerError("");
await saveProject(data);
router.replace("/projects");
} catch (e) {
setServerError(t("An error has occurred. Please try again later."));
}
},
[router, t],
);

const onSubmitError = useCallback<SubmitErrorHandler<CreateProjectInputs>>(
(errors) => {
@@ -82,6 +97,8 @@ const CreateProject: React.FC<Props> = ({
tasks: {},
allocatedStaffIds: [],
milestones: {},
// TODO: Remove this
clientSubsidiary: "Test subsidiary",
},
});

@@ -111,6 +128,7 @@ const CreateProject: React.FC<Props> = ({
{
<ProjectClientDetails
projectCategories={projectCategories}
teamLeads={teamLeads}
isActive={tabIndex === 0}
/>
}
@@ -123,6 +141,11 @@ const CreateProject: React.FC<Props> = ({
}
{<StaffAllocation isActive={tabIndex === 2} />}
{<ResourceMilestone allTasks={allTasks} isActive={tabIndex === 3} />}
{serverError && (
<Typography variant="body2" color="error" alignSelf="flex-end">
{serverError}
</Typography>
)}
<Stack direction="row" justifyContent="flex-end" gap={1}>
<Button
variant="outlined"


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

@@ -1,17 +1,23 @@
import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks";
import CreateProject from "./CreateProject";
import { fetchProjectCategories } from "@/app/api/projects";
import { fetchTeamLeads } from "@/app/api/staff";

const CreateProjectWrapper: React.FC = async () => {
const tasks = await fetchAllTasks();
const taskTemplates = await fetchTaskTemplates();
const projectCategories = await fetchProjectCategories();
const [tasks, taskTemplates, projectCategories, teamLeads] =
await Promise.all([
fetchAllTasks(),
fetchTaskTemplates(),
fetchProjectCategories(),
fetchTeamLeads(),
]);

return (
<CreateProject
allTasks={tasks}
projectCategories={projectCategories}
taskTemplates={taskTemplates}
teamLeads={teamLeads}
/>
);
};


+ 35
- 9
src/components/CreateProject/ProjectClientDetails.tsx 查看文件

@@ -18,15 +18,18 @@ 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 { Staff } from "@/app/api/staff";

interface Props {
isActive: boolean;
projectCategories: ProjectCategory[];
teamLeads: Staff[];
}

const ProjectClientDetails: React.FC<Props> = ({
isActive,
projectCategories,
teamLeads
}) => {
const { t } = useTranslation();
const {
@@ -47,7 +50,10 @@ const ProjectClientDetails: React.FC<Props> = ({
<TextField
label={t("Project Code")}
fullWidth
{...register("projectCode")}
{...register("projectCode", {
required: "Project code required!",
})}
error={Boolean(errors.projectCode)}
/>
</Grid>
<Grid item xs={6}>
@@ -74,7 +80,7 @@ const ProjectClientDetails: React.FC<Props> = ({
key={`${category.id}-${index}`}
value={category.id}
>
{t(category.label)}
{t(category.name)}
</MenuItem>
))}
</Select>
@@ -85,18 +91,38 @@ const ProjectClientDetails: React.FC<Props> = ({
<Grid item xs={6}>
<FormControl fullWidth>
<InputLabel>{t("Team Lead")}</InputLabel>
<Select label={t("Team Lead")} value={"00539 - Ming CHAN (MC)"}>
<MenuItem value={"00539 - Ming CHAN (MC)"}>
{"00539 - Ming CHAN (MC)"}
</MenuItem>
</Select>
<Controller
defaultValue={teamLeads[0].id}
control={control}
name="projectLeadId"
render={({ field }) => (
<Select label={t("Team Lead")} {...field}>
{teamLeads.map((staff, index) => (
<MenuItem key={`${staff.id}-${index}`} value={staff.id}>
{`${staff.staffId} - ${staff.name} (${staff.team.code})`}
</MenuItem>
))}
</Select>
)}
/>
</FormControl>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Project Description")}
fullWidth
{...register("projectDescription")}
{...register("projectDescription", {
required: "Please enter a description",
})}
error={Boolean(errors.projectDescription)}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Expected Total Project Fee")}
fullWidth
type="number"
{...register("expectedProjectFee")}
/>
</Grid>
</Grid>
@@ -116,7 +142,7 @@ const ProjectClientDetails: React.FC<Props> = ({
</Grid>
<Grid item xs={6}>
<TextField
label={t("ClientName")}
label={t("Client Name")}
fullWidth
{...register("clientName")}
/>


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

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

let projectTotal = 0;

@@ -40,6 +41,11 @@ const ProjectTotalFee: React.FC<Props> = ({ taskGroups }) => {
<Typography variant="h6">{t("Project Total Fee")}</Typography>
<Typography>{moneyFormatter.format(projectTotal)}</Typography>
</Stack>
{projectTotal > expectedTotalFee && (
<Typography variant="caption" color="warning.main" alignSelf="flex-end">
{t("Project total fee is larger than the expected total fee!")}
</Typography>
)}
</Stack>
);
};


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

@@ -36,10 +36,9 @@ const ResourceMilestone: React.FC<Props> = ({
isActive,
}) => {
const { t } = useTranslation();
const { getValues } = useFormContext<CreateProjectInputs>();
const tasks = useMemo(() => {
return allTasks.filter((task) => getValues("tasks")[task.id]);
}, [allTasks, getValues]);
const { watch } = useFormContext<CreateProjectInputs>();

const tasks = allTasks.filter((task) => watch("tasks")[task.id]);

const taskGroups = useMemo(() => {
return uniqBy(


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

@@ -90,7 +90,7 @@ const ResourceSection: React.FC<Props> = ({

const rows = useMemo<Row[]>(() => {
const initialAllocation =
getValues("tasks")[selectedTaskId].manhourAllocation;
getValues("tasks")[selectedTaskId]?.manhourAllocation;
if (!isEmpty(initialAllocation)) {
return [{ ...initialAllocation, id: "manhourAllocation" }];
}


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

@@ -6,7 +6,7 @@ import SearchBox, { Criterion } from "../SearchBox";
import { useTranslation } from "react-i18next";
import SearchResults, { Column } from "../SearchResults";
import EditNote from "@mui/icons-material/EditNote";
import uniq from 'lodash/uniq';
import uniq from "lodash/uniq";

interface Props {
projects: ProjectResult[];
@@ -35,7 +35,7 @@ const ProjectSearch: React.FC<Props> = ({ projects, projectCategories }) => {
label: t("Project category"),
paramName: "category",
type: "select",
options: projectCategories.map((category) => category.label),
options: projectCategories.map((category) => category.name),
},
{
label: t("Team"),


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

@@ -135,8 +135,8 @@ function SearchBox<T extends string>({
value={inputs[c.paramName]}
>
<MenuItem value={"All"}>{t("All")}</MenuItem>
{c.options.map((option) => (
<MenuItem key={option} value={option}>
{c.options.map((option, index) => (
<MenuItem key={`${option}-${index}`} value={option}>
{option}
</MenuItem>
))}


Loading…
取消
儲存