diff --git a/src/app/(main)/projects/create/page.tsx b/src/app/(main)/projects/create/page.tsx
index ebe69cf..60ab586 100644
--- a/src/app/(main)/projects/create/page.tsx
+++ b/src/app/(main)/projects/create/page.tsx
@@ -1,5 +1,5 @@
import CreateProject from "@/components/CreateProject";
-import { getServerI18n } from "@/i18n";
+import { I18nProvider, getServerI18n } from "@/i18n";
import Typography from "@mui/material/Typography";
import { Metadata } from "next";
@@ -13,7 +13,9 @@ const Projects: React.FC = async () => {
return (
<>
{t("Create Project")}
-
+
+
+
>
);
};
diff --git a/src/app/api/projects/actions.ts b/src/app/api/projects/actions.ts
index edbaa24..f05ef40 100644
--- a/src/app/api/projects/actions.ts
+++ b/src/app/api/projects/actions.ts
@@ -22,9 +22,7 @@ export interface CreateProjectInputs {
// Tasks
tasks: {
[taskId: Task["id"]]: {
- manhourAllocation: {
- [gradeId: number]: number;
- };
+ manhourAllocation: ManhourAllocation;
};
};
@@ -41,6 +39,10 @@ export interface CreateProjectInputs {
};
}
+export interface ManhourAllocation {
+ [gradeId: number]: number;
+}
+
export interface PaymentInputs {
id: number;
description: string;
diff --git a/src/components/CreateProject/ResourceMilestone.tsx b/src/components/CreateProject/ResourceMilestone.tsx
index aa12789..0706851 100644
--- a/src/components/CreateProject/ResourceMilestone.tsx
+++ b/src/components/CreateProject/ResourceMilestone.tsx
@@ -30,7 +30,10 @@ export interface Props {
defaultManhourBreakdownByGrade?: { [gradeId: number]: number };
}
-const ResourceMilestone: React.FC = ({ allTasks }) => {
+const ResourceMilestone: React.FC = ({
+ allTasks,
+ defaultManhourBreakdownByGrade,
+}) => {
const { t } = useTranslation();
const { getValues } = useFormContext();
const tasks = useMemo(() => {
@@ -79,9 +82,7 @@ const ResourceMilestone: React.FC = ({ allTasks }) => {
{}}
- onAllocateManhours={() => {}}
+ manhourBreakdownByGrade={defaultManhourBreakdownByGrade}
/>
diff --git a/src/components/CreateProject/ResourceSection.tsx b/src/components/CreateProject/ResourceSection.tsx
index bea7295..9b60fc1 100644
--- a/src/components/CreateProject/ResourceSection.tsx
+++ b/src/components/CreateProject/ResourceSection.tsx
@@ -9,22 +9,40 @@ import {
ListItemText,
TextField,
} from "@mui/material";
-import { useState, useCallback, useEffect } from "react";
+import { useState, useCallback, useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Props as ResourceMilestoneProps } from "./ResourceMilestone";
+import StyledDataGrid from "../StyledDataGrid";
+import { useForm, useFormContext } from "react-hook-form";
+import { GridColDef, GridRowModel, useGridApiRef } from "@mui/x-data-grid";
+import {
+ CreateProjectInputs,
+ ManhourAllocation,
+} from "@/app/api/projects/actions";
+import isEmpty from "lodash/isEmpty";
+import _reduce from "lodash/reduce";
+
+const mockGrades = [1, 2, 3, 4, 5];
interface Props {
tasks: Task[];
- defaultManhourBreakdownByGrade: ResourceMilestoneProps["defaultManhourBreakdownByGrade"];
- onSetManhours: (hours: number, taskId: Task["id"]) => void;
- onAllocateManhours: () => void;
+ manhourBreakdownByGrade: ResourceMilestoneProps["defaultManhourBreakdownByGrade"];
}
+type Row = ManhourAllocation & { id: "manhourAllocation" };
+
+const parseValidManhours = (value: number | string): number => {
+ const inputNumber = Number(value);
+ return isNaN(inputNumber) || inputNumber < 0 ? 0 : inputNumber;
+};
+
const ResourceSection: React.FC = ({
tasks,
- onAllocateManhours,
- onSetManhours,
- defaultManhourBreakdownByGrade,
+ manhourBreakdownByGrade = mockGrades.reduce<
+ NonNullable
+ >((acc, grade) => {
+ return { ...acc, [grade]: 1 };
+ }, {}),
}) => {
const { t } = useTranslation();
const [selectedTaskId, setSelectedTaskId] = useState(tasks[0].id);
@@ -40,10 +58,116 @@ const ResourceSection: React.FC = ({
setSelectedTaskId(tasks[0].id);
}, [tasks]);
+ const { getValues, setValue } = useFormContext();
+
+ const updateTaskAllocations = useCallback(
+ (newAllocations: ManhourAllocation) => {
+ setValue("tasks", {
+ ...getValues("tasks"),
+ [selectedTaskId]: {
+ manhourAllocation: newAllocations,
+ },
+ });
+ },
+ [getValues, selectedTaskId, setValue],
+ );
+
+ const gridApiRef = useGridApiRef();
+ const columns = useMemo(() => {
+ return mockGrades.map((grade) => ({
+ field: grade.toString(),
+ editable: true,
+ sortable: false,
+ width: 120,
+ headerName: t("Grade {{grade}}", { grade }),
+ type: "number",
+ valueParser: parseValidManhours,
+ valueFormatter(params) {
+ return Number(params.value).toFixed(2);
+ },
+ }));
+ }, [t]);
+
+ const rows = useMemo(() => {
+ const initialAllocation =
+ getValues("tasks")[selectedTaskId].manhourAllocation;
+ if (!isEmpty(initialAllocation)) {
+ return [{ ...initialAllocation, id: "manhourAllocation" }];
+ }
+ return [
+ mockGrades.reduce(
+ (acc, grade) => {
+ return { ...acc, [grade]: 0 };
+ },
+ { id: "manhourAllocation" },
+ ),
+ ];
+ }, [getValues, selectedTaskId]);
+
+ const initialManhours = useMemo(() => {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { id, ...allocations } = rows[0];
+ return Object.values(allocations).reduce((acc, hours) => acc + hours, 0);
+ }, [rows]);
+
+ const {
+ register,
+ reset,
+ getValues: getManhourFormValues,
+ setValue: setManhourFormValue,
+ } = useForm<{ manhour: number }>({
+ defaultValues: {
+ manhour: initialManhours,
+ },
+ });
+
+ // Reset man hour input when task changes
+ useEffect(() => {
+ reset({ manhour: initialManhours });
+ }, [initialManhours, reset, selectedTaskId]);
+
+ const updateAllocation = useCallback(() => {
+ const inputHour = getManhourFormValues("manhour");
+ const ratioSum = Object.values(manhourBreakdownByGrade).reduce(
+ (acc, ratio) => acc + ratio,
+ 0,
+ );
+ const newAllocations = _reduce(
+ manhourBreakdownByGrade,
+ (acc, value, key) => {
+ return { ...acc, [key]: (inputHour / ratioSum) * value };
+ },
+ {},
+ );
+ gridApiRef.current.updateRows([
+ { id: "manhourAllocation", ...newAllocations },
+ ]);
+ updateTaskAllocations(newAllocations);
+ }, [
+ getManhourFormValues,
+ gridApiRef,
+ manhourBreakdownByGrade,
+ updateTaskAllocations,
+ ]);
+
+ const processRowUpdate = useCallback(
+ (newRow: GridRowModel) => {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { id: rowId, ...newAllocations } = newRow;
+ const totalHours = Object.values(
+ newAllocations as ManhourAllocation,
+ ).reduce((acc, hour) => acc + hour, 0);
+ setManhourFormValue("manhour", totalHours);
+ updateTaskAllocations(newAllocations);
+ return newRow;
+ },
+ [setManhourFormValue, updateTaskAllocations],
+ );
+
return (
- {t("Resource")}
+ {t("Task Breakdown")}
@@ -64,7 +188,35 @@ const ResourceSection: React.FC = ({
-
+
+
+
+
diff --git a/src/i18n/en/common.json b/src/i18n/en/common.json
new file mode 100644
index 0000000..2b2f3a3
--- /dev/null
+++ b/src/i18n/en/common.json
@@ -0,0 +1,3 @@
+{
+ "Grade {{grade}}": "Grade {{grade}}"
+}
\ No newline at end of file
diff --git a/src/i18n/en/projects.json b/src/i18n/en/projects.json
new file mode 100644
index 0000000..9e26dfe
--- /dev/null
+++ b/src/i18n/en/projects.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/src/i18n/en/translation.json b/src/i18n/en/translation.json
deleted file mode 100644
index 080318d..0000000
--- a/src/i18n/en/translation.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "test": "abc"
-}
\ No newline at end of file
diff --git a/src/i18n/zh/common.json b/src/i18n/zh/common.json
new file mode 100644
index 0000000..9e26dfe
--- /dev/null
+++ b/src/i18n/zh/common.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/src/i18n/zh/projects.json b/src/i18n/zh/projects.json
new file mode 100644
index 0000000..9e26dfe
--- /dev/null
+++ b/src/i18n/zh/projects.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/src/i18n/zh/translation.json b/src/i18n/zh/translation.json
deleted file mode 100644
index cd1d02f..0000000
--- a/src/i18n/zh/translation.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "test": "def"
-}
\ No newline at end of file