Ver a proveniência

Add task breakdown

tags/Baseline_30082024_FRONTEND_UAT
Wayne há 1 ano
ascendente
cometimento
046c584c58
10 ficheiros alterados com 181 adições e 24 eliminações
  1. +4
    -2
      src/app/(main)/projects/create/page.tsx
  2. +5
    -3
      src/app/api/projects/actions.ts
  3. +5
    -4
      src/components/CreateProject/ResourceMilestone.tsx
  4. +161
    -9
      src/components/CreateProject/ResourceSection.tsx
  5. +3
    -0
      src/i18n/en/common.json
  6. +1
    -0
      src/i18n/en/projects.json
  7. +0
    -3
      src/i18n/en/translation.json
  8. +1
    -0
      src/i18n/zh/common.json
  9. +1
    -0
      src/i18n/zh/projects.json
  10. +0
    -3
      src/i18n/zh/translation.json

+ 4
- 2
src/app/(main)/projects/create/page.tsx Ver ficheiro

@@ -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 (
<>
<Typography variant="h4">{t("Create Project")}</Typography>
<CreateProject />
<I18nProvider namespaces={["projects"]}>
<CreateProject />
</I18nProvider>
</>
);
};


+ 5
- 3
src/app/api/projects/actions.ts Ver ficheiro

@@ -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;


+ 5
- 4
src/components/CreateProject/ResourceMilestone.tsx Ver ficheiro

@@ -30,7 +30,10 @@ export interface Props {
defaultManhourBreakdownByGrade?: { [gradeId: number]: number };
}

const ResourceMilestone: React.FC<Props> = ({ allTasks }) => {
const ResourceMilestone: React.FC<Props> = ({
allTasks,
defaultManhourBreakdownByGrade,
}) => {
const { t } = useTranslation();
const { getValues } = useFormContext<CreateProjectInputs>();
const tasks = useMemo(() => {
@@ -79,9 +82,7 @@ const ResourceMilestone: React.FC<Props> = ({ allTasks }) => {
</FormControl>
<ResourceSection
tasks={currentTasks}
defaultManhourBreakdownByGrade={{}}
onSetManhours={() => {}}
onAllocateManhours={() => {}}
manhourBreakdownByGrade={defaultManhourBreakdownByGrade}
/>
<MilestoneSection taskGroupId={currentTaskGroupId} />
<CardActions sx={{ justifyContent: "flex-end" }}>


+ 161
- 9
src/components/CreateProject/ResourceSection.tsx Ver ficheiro

@@ -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<Props> = ({
tasks,
onAllocateManhours,
onSetManhours,
defaultManhourBreakdownByGrade,
manhourBreakdownByGrade = mockGrades.reduce<
NonNullable<Props["manhourBreakdownByGrade"]>
>((acc, grade) => {
return { ...acc, [grade]: 1 };
}, {}),
}) => {
const { t } = useTranslation();
const [selectedTaskId, setSelectedTaskId] = useState(tasks[0].id);
@@ -40,10 +58,116 @@ const ResourceSection: React.FC<Props> = ({
setSelectedTaskId(tasks[0].id);
}, [tasks]);

const { getValues, setValue } = useFormContext<CreateProjectInputs>();

const updateTaskAllocations = useCallback(
(newAllocations: ManhourAllocation) => {
setValue("tasks", {
...getValues("tasks"),
[selectedTaskId]: {
manhourAllocation: newAllocations,
},
});
},
[getValues, selectedTaskId, setValue],
);

const gridApiRef = useGridApiRef();
const columns = useMemo(() => {
return mockGrades.map<GridColDef>((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<Row[]>(() => {
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<number>((acc, hour) => acc + hour, 0);
setManhourFormValue("manhour", totalHours);
updateTaskAllocations(newAllocations);
return newRow;
},
[setManhourFormValue, updateTaskAllocations],
);

return (
<Box marginBlock={4}>
<Typography variant="overline" display="block" marginBlockEnd={1}>
{t("Resource")}
{t("Task Breakdown")}
</Typography>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6}>
@@ -64,7 +188,35 @@ const ResourceSection: React.FC<Props> = ({
</Paper>
</Grid>
<Grid item xs={6}>
<TextField label={t("Mahours Allocated to Task")} fullWidth />
<TextField
label={t("Mahours Allocated to Task")}
fullWidth
type="number"
{...register("manhour", {
valueAsNumber: true,
onBlur: updateAllocation,
})}
/>
<Paper
elevation={2}
sx={{
marginBlockStart: 2,
".MuiDataGrid-root .MuiDataGrid-columnHeader:focus-within": {
outlineOffset: -2,
},
}}
>
<StyledDataGrid
apiRef={gridApiRef}
disableColumnMenu
hideFooter
disableRowSelectionOnClick
rows={rows}
columns={columns}
processRowUpdate={processRowUpdate}
sx={{ paddingBlockEnd: 2 }}
/>
</Paper>
</Grid>
</Grid>
</Box>


+ 3
- 0
src/i18n/en/common.json Ver ficheiro

@@ -0,0 +1,3 @@
{
"Grade {{grade}}": "Grade {{grade}}"
}

+ 1
- 0
src/i18n/en/projects.json Ver ficheiro

@@ -0,0 +1 @@
{}

+ 0
- 3
src/i18n/en/translation.json Ver ficheiro

@@ -1,3 +0,0 @@
{
"test": "abc"
}

+ 1
- 0
src/i18n/zh/common.json Ver ficheiro

@@ -0,0 +1 @@
{}

+ 1
- 0
src/i18n/zh/projects.json Ver ficheiro

@@ -0,0 +1 @@
{}

+ 0
- 3
src/i18n/zh/translation.json Ver ficheiro

@@ -1,3 +0,0 @@
{
"test": "def"
}

Carregando…
Cancelar
Guardar