From 69867b113f5cc364e2ff6a26659cee685187fb00 Mon Sep 17 00:00:00 2001 From: William Petit Date: Tue, 21 Apr 2020 18:44:10 +0200 Subject: [PATCH] Use project parameters for calculations --- src/components/estimation-range.tsx | 24 +++++++++ src/components/project-time-unit.tsx | 16 ++++++ src/manifest.json | 4 +- src/models/params.ts | 64 ++++++++++++++++++------ src/models/project.ts | 10 +++- src/models/task.ts | 1 + src/routes/project/estimation-tab.tsx | 41 +++++++++------ src/routes/project/financial-preview.tsx | 7 +-- src/routes/project/style.css | 2 +- src/routes/project/tasks-table.tsx | 10 ++-- src/routes/project/time-preview.tsx | 14 +++--- 11 files changed, 145 insertions(+), 48 deletions(-) create mode 100644 src/components/estimation-range.tsx create mode 100644 src/components/project-time-unit.tsx diff --git a/src/components/estimation-range.tsx b/src/components/estimation-range.tsx new file mode 100644 index 0000000..671cfe0 --- /dev/null +++ b/src/components/estimation-range.tsx @@ -0,0 +1,24 @@ +import ProjectTimeUnit from "./project-time-unit"; +import { defaults, getRoundUpEstimations } from "../models/params"; + +export interface EstimationRangeProps { + project: Project, + estimation: Estimation + sdFactor: number +} + +export const EstimationRange: FunctionalComponent = ({ project, estimation, sdFactor }) => { + const roundUp = getRoundUpEstimations(project); + const e = roundUp ? Math.ceil(estimation.e) : estimation.e; + const sd = roundUp ? Math.ceil(estimation.sd * sdFactor) : (estimation.sd * sdFactor); + const max = e+sd; + const min = Math.max(e-sd, 0); + return ( + + {`${e} ± ${sd}`}  + + + ); +} + +export default EstimationRange; \ No newline at end of file diff --git a/src/components/project-time-unit.tsx b/src/components/project-time-unit.tsx new file mode 100644 index 0000000..45ddc01 --- /dev/null +++ b/src/components/project-time-unit.tsx @@ -0,0 +1,16 @@ +import { FunctionalComponent, h } from "preact"; +import { Project } from "../models/project"; +import { getTimeUnit } from "../models/params"; + +export interface ProjectTimeUnitProps { + project: Project +} + +const ProjectTimeUnit: FunctionalComponent = ({ project }) => { + const timeUnit = getTimeUnit(project); + return ( + {timeUnit.acronym} + ); +}; + +export default ProjectTimeUnit; diff --git a/src/manifest.json b/src/manifest.json index 1c76037..e981481 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -1,6 +1,6 @@ { - "name": "guesstimate", - "short_name": "guesstimate", + "name": "Guesstimate", + "short_name": "Guesstimate", "start_url": "/", "display": "standalone", "orientation": "portrait", diff --git a/src/models/params.ts b/src/models/params.ts index dacb6f9..bfd01da 100644 --- a/src/models/params.ts +++ b/src/models/params.ts @@ -1,24 +1,60 @@ import { TaskCategory, CategoryID } from "./task"; +import { Project } from "./project"; export interface TaskCategoriesIndex { [id: string]: TaskCategory } -export interface Params { - taskCategories: TaskCategoriesIndex +export interface TimeUnit { + label: string + acronym: string } -export const DefaultTaskCategories = { - "7e92266f-0a7b-4728-8322-5fe05ff3b929": { - id: "7e92266f-0a7b-4728-8322-5fe05ff3b929", - label: "Développement" +export interface Params { + taskCategories: TaskCategoriesIndex + timeUnit: TimeUnit + currency: string + roundUpEstimations: boolean +} + +export const defaults = { + taskCategories: { + "7e92266f-0a7b-4728-8322-5fe05ff3b929": { + id: "7e92266f-0a7b-4728-8322-5fe05ff3b929", + label: "Développement", + costPerTimeUnit: 500, + }, + "508a0925-a664-4426-8d40-6974156f0f00": { + id: "508a0925-a664-4426-8d40-6974156f0f00", + label: "Conduite de projet", + costPerTimeUnit: 500, + }, + "7aab4d66-072e-4cc8-aae8-b62edd3237a8": { + id: "7aab4d66-072e-4cc8-aae8-b62edd3237a8", + label: "Recette", + costPerTimeUnit: 500, + }, }, - "508a0925-a664-4426-8d40-6974156f0f00": { - id: "508a0925-a664-4426-8d40-6974156f0f00", - label: "Conduite de projet" + timeUnit: { + label: "jour/homme", + acronym: "j/h", }, - "7aab4d66-072e-4cc8-aae8-b62edd3237a8": { - id: "7aab4d66-072e-4cc8-aae8-b62edd3237a8", - label: "Recette" - }, -}; \ No newline at end of file + roundUpEstimations: true, + currency: "€ H.T.", +} + +export function getTimeUnit(project: Project): TimeUnit { + return project.params.timeUnit ? project.params.timeUnit : defaults.timeUnit; +} + +export function getRoundUpEstimations(project: Project): boolean { + return project.params.hasOwnProperty("roundUpEstimations") ? project.params.roundUpEstimations : defaults.roundUpEstimations; +} + +export function getCurrency(project: Project): string { + return project.params.currency ? project.params.currency : defaults.currency; +} + +export function getTaskCategories(project: Project): TaskCategoriesIndex { + return project.params.taskCategories ? project.params.taskCategories : defaults.taskCategories; +} \ No newline at end of file diff --git a/src/models/project.ts b/src/models/project.ts index 843ae40..0fe4878 100644 --- a/src/models/project.ts +++ b/src/models/project.ts @@ -1,5 +1,5 @@ import { Task, TaskCategory, TaskID } from './task'; -import { Params, DefaultTaskCategories } from "./params"; +import { Params, defaults } from "./params"; import { uuidV4 } from "../util/uuid"; export type ProjectID = string; @@ -23,7 +23,13 @@ export function newProject(id?: string): Project { description: "", tasks: {}, params: { - taskCategories: DefaultTaskCategories, + taskCategories: defaults.taskCategories, + currency: "€", + roundUpEstimations: true, + timeUnit: { + label: "Jour/homme", + acronym: "j/h", + } }, }; } \ No newline at end of file diff --git a/src/models/task.ts b/src/models/task.ts index c8ac553..3e60b1f 100644 --- a/src/models/task.ts +++ b/src/models/task.ts @@ -20,6 +20,7 @@ export type CategoryID = string export interface TaskCategory { id: CategoryID label: string + costPerTimeUnit: number } export function newTask(label: string, category: CategoryID): Task { diff --git a/src/routes/project/estimation-tab.tsx b/src/routes/project/estimation-tab.tsx index 2bf99ee..bf061be 100644 --- a/src/routes/project/estimation-tab.tsx +++ b/src/routes/project/estimation-tab.tsx @@ -1,4 +1,4 @@ -import { FunctionalComponent, h } from "preact"; +import { FunctionalComponent, h, Fragment } from "preact"; import { Project } from "../../models/project"; import TaskTable from "./tasks-table"; import TimePreview from "./time-preview"; @@ -28,22 +28,33 @@ const EstimationTab: FunctionalComponent = ({ project, dispa dispatch(updateTaskEstimation(taskId, confidence, value)); }; - return ( -
-
- + +
+
+ +
+
+ + +
-
- - -
-
+ { + Object.keys(project.tasks).length <= 20 ? +
+
+

⚠️ Attention

+

Votre projet ne contient pas assez de tâches actuellement pour que les niveaux de confiance soient fiables. Un minimum de 20 tâches est conseillé pour obtenir une estimation pertinente.

+
+
: + null + } + ); }; diff --git a/src/routes/project/financial-preview.tsx b/src/routes/project/financial-preview.tsx index 8e37d46..800e663 100644 --- a/src/routes/project/financial-preview.tsx +++ b/src/routes/project/financial-preview.tsx @@ -2,6 +2,7 @@ import { FunctionalComponent, h } from "preact"; import * as style from "./style.css"; import { Project } from "../../models/project"; import { useProjectEstimations } from "../../hooks/use-project-estimations"; +import { getCurrency } from "../../models/params"; export interface FinancialPreviewProps { project: Project @@ -11,7 +12,7 @@ const FinancialPreview: FunctionalComponent = ({ project const estimations = useProjectEstimations(project); const costPerTimeUnit = 500; const maxCost = Math.ceil((estimations.p99.e + estimations.p99.sd) * costPerTimeUnit); - const minCost = Math.ceil((estimations.p99.e - estimations.p99.sd) * costPerTimeUnit); + const minCost = Math.max(Math.ceil((estimations.p99.e - estimations.p99.sd) * costPerTimeUnit), 0); return (
@@ -27,11 +28,11 @@ const FinancialPreview: FunctionalComponent = ({ project - + - +
Maximum~ {maxCost} €{maxCost} {getCurrency(project)}
Minimum~ {minCost} €{minCost} {getCurrency(project)}
diff --git a/src/routes/project/style.css b/src/routes/project/style.css index 2dd0933..915d7ed 100644 --- a/src/routes/project/style.css +++ b/src/routes/project/style.css @@ -16,7 +16,7 @@ } .middleTable td { - vertical-align: middle; + vertical-align: middle !important; } .tabContainer { diff --git a/src/routes/project/tasks-table.tsx b/src/routes/project/tasks-table.tsx index bc88264..77bc374 100644 --- a/src/routes/project/tasks-table.tsx +++ b/src/routes/project/tasks-table.tsx @@ -5,6 +5,8 @@ import { Project } from "../../models/project"; import { newTask, Task, TaskID, EstimationConfidence } from "../../models/task"; import EditableText from "../../components/editable-text"; import { usePrintMediaQuery } from "../../hooks/use-media-query"; +import { defaults, getTimeUnit } from "../../models/params"; +import ProjectTimeUnit from "../../components/project-time-unit"; export interface TaskTableProps { project: Project @@ -83,7 +85,7 @@ const TaskTable: FunctionalComponent = ({ project, onTaskAdd, on Tâche Catégorie - Estimation + Estimation (en ) Optimiste @@ -185,9 +187,9 @@ const TaskTable: FunctionalComponent = ({ project, onTaskAdd, on - {totals.optimistic} - {totals.likely} - {totals.pessimistic} + {totals.optimistic} + {totals.likely} + {totals.pessimistic} diff --git a/src/routes/project/time-preview.tsx b/src/routes/project/time-preview.tsx index fbceb0c..19928e3 100644 --- a/src/routes/project/time-preview.tsx +++ b/src/routes/project/time-preview.tsx @@ -1,6 +1,7 @@ -import { FunctionalComponent, h } from "preact"; +import { FunctionalComponent, h, Fragment } from "preact"; import { Project } from "../../models/project"; -import { useProjectEstimations } from "../../hooks/use-project-estimations"; +import { useProjectEstimations, Estimation } from "../../hooks/use-project-estimations"; +import EstimationRange from "../../components/estimation-range"; export interface TimePreviewProps { project: Project @@ -8,7 +9,6 @@ export interface TimePreviewProps { const TimePreview: FunctionalComponent = ({ project }) => { const estimations = useProjectEstimations(project); - return (
@@ -24,15 +24,15 @@ const TimePreview: FunctionalComponent = ({ project }) => { - + - + - + @@ -45,6 +45,6 @@ const TimePreview: FunctionalComponent = ({ project }) => {
>= 99.7%{`${estimations.p99.e.toPrecision(2)} ± ${estimations.p99.sd.toPrecision(2)} j/h`}
>= 90%{`${estimations.p90.e.toPrecision(2)} ± ${estimations.p90.sd.toPrecision(2)} j/h`}
>= 68%{`${estimations.p68.e.toPrecision(2)} ± ${estimations.p68.sd.toPrecision(2)} j/h`}
); -}; +}; export default TimePreview;