Use project parameters for calculations

This commit is contained in:
wpetit 2020-04-21 18:44:10 +02:00
parent 642b555b3d
commit 69867b113f
11 changed files with 145 additions and 48 deletions

View File

@ -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<EstimationRangeProps> = ({ 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 (
<Fragment>
<abbr title={`max: ${max}, min: ${min}`}>{`${e} ± ${sd}`}</abbr>&nbsp;<ProjectTimeUnit project={project} />
</Fragment>
);
}
export default EstimationRange;

View File

@ -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<ProjectTimeUnitProps> = ({ project }) => {
const timeUnit = getTimeUnit(project);
return (
<abbr title={timeUnit.label}>{timeUnit.acronym}</abbr>
);
};
export default ProjectTimeUnit;

View File

@ -1,6 +1,6 @@
{ {
"name": "guesstimate", "name": "Guesstimate",
"short_name": "guesstimate", "short_name": "Guesstimate",
"start_url": "/", "start_url": "/",
"display": "standalone", "display": "standalone",
"orientation": "portrait", "orientation": "portrait",

View File

@ -1,24 +1,60 @@
import { TaskCategory, CategoryID } from "./task"; import { TaskCategory, CategoryID } from "./task";
import { Project } from "./project";
export interface TaskCategoriesIndex { export interface TaskCategoriesIndex {
[id: string]: TaskCategory [id: string]: TaskCategory
} }
export interface Params { export interface TimeUnit {
taskCategories: TaskCategoriesIndex label: string
acronym: string
} }
export const DefaultTaskCategories = { export interface Params {
"7e92266f-0a7b-4728-8322-5fe05ff3b929": { taskCategories: TaskCategoriesIndex
id: "7e92266f-0a7b-4728-8322-5fe05ff3b929", timeUnit: TimeUnit
label: "Développement" 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": { timeUnit: {
id: "508a0925-a664-4426-8d40-6974156f0f00", label: "jour/homme",
label: "Conduite de projet" acronym: "j/h",
}, },
"7aab4d66-072e-4cc8-aae8-b62edd3237a8": { roundUpEstimations: true,
id: "7aab4d66-072e-4cc8-aae8-b62edd3237a8", currency: "€ H.T.",
label: "Recette" }
},
}; 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;
}

View File

@ -1,5 +1,5 @@
import { Task, TaskCategory, TaskID } from './task'; import { Task, TaskCategory, TaskID } from './task';
import { Params, DefaultTaskCategories } from "./params"; import { Params, defaults } from "./params";
import { uuidV4 } from "../util/uuid"; import { uuidV4 } from "../util/uuid";
export type ProjectID = string; export type ProjectID = string;
@ -23,7 +23,13 @@ export function newProject(id?: string): Project {
description: "", description: "",
tasks: {}, tasks: {},
params: { params: {
taskCategories: DefaultTaskCategories, taskCategories: defaults.taskCategories,
currency: "€",
roundUpEstimations: true,
timeUnit: {
label: "Jour/homme",
acronym: "j/h",
}
}, },
}; };
} }

View File

@ -20,6 +20,7 @@ export type CategoryID = string
export interface TaskCategory { export interface TaskCategory {
id: CategoryID id: CategoryID
label: string label: string
costPerTimeUnit: number
} }
export function newTask(label: string, category: CategoryID): Task { export function newTask(label: string, category: CategoryID): Task {

View File

@ -1,4 +1,4 @@
import { FunctionalComponent, h } from "preact"; import { FunctionalComponent, h, Fragment } from "preact";
import { Project } from "../../models/project"; import { Project } from "../../models/project";
import TaskTable from "./tasks-table"; import TaskTable from "./tasks-table";
import TimePreview from "./time-preview"; import TimePreview from "./time-preview";
@ -28,22 +28,33 @@ const EstimationTab: FunctionalComponent<EstimationTabProps> = ({ project, dispa
dispatch(updateTaskEstimation(taskId, confidence, value)); dispatch(updateTaskEstimation(taskId, confidence, value));
}; };
return ( return (
<div class="columns"> <Fragment>
<div class="column is-9"> <div class="columns">
<TaskTable <div class="column is-9">
project={project} <TaskTable
onTaskAdd={onTaskAdd} project={project}
onTaskRemove={onTaskRemove} onTaskAdd={onTaskAdd}
onTaskLabelUpdate={onTaskLabelUpdate} onTaskRemove={onTaskRemove}
onEstimationChange={onEstimationChange} /> onTaskLabelUpdate={onTaskLabelUpdate}
onEstimationChange={onEstimationChange} />
</div>
<div class="column is-3">
<TimePreview project={project} />
<FinancialPreview project={project} />
</div>
</div> </div>
<div class="column is-3"> {
<TimePreview project={project} /> Object.keys(project.tasks).length <= 20 ?
<FinancialPreview project={project} /> <div class="message noPrint">
</div> <div class="message-body">
</div> <p><strong> Attention</strong></p>
<p>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.</p>
</div>
</div> :
null
}
</Fragment>
); );
}; };

View File

@ -2,6 +2,7 @@ import { FunctionalComponent, h } from "preact";
import * as style from "./style.css"; import * as style from "./style.css";
import { Project } from "../../models/project"; import { Project } from "../../models/project";
import { useProjectEstimations } from "../../hooks/use-project-estimations"; import { useProjectEstimations } from "../../hooks/use-project-estimations";
import { getCurrency } from "../../models/params";
export interface FinancialPreviewProps { export interface FinancialPreviewProps {
project: Project project: Project
@ -11,7 +12,7 @@ const FinancialPreview: FunctionalComponent<FinancialPreviewProps> = ({ project
const estimations = useProjectEstimations(project); const estimations = useProjectEstimations(project);
const costPerTimeUnit = 500; const costPerTimeUnit = 500;
const maxCost = Math.ceil((estimations.p99.e + estimations.p99.sd) * costPerTimeUnit); 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 ( return (
<div class="table-container"> <div class="table-container">
<table class="table is-bordered is-striped is-fullwidth"> <table class="table is-bordered is-striped is-fullwidth">
@ -27,11 +28,11 @@ const FinancialPreview: FunctionalComponent<FinancialPreviewProps> = ({ project
<tbody> <tbody>
<tr> <tr>
<td class="is-narrow">Maximum</td> <td class="is-narrow">Maximum</td>
<td>~ {maxCost} </td> <td>{maxCost} {getCurrency(project)}</td>
</tr> </tr>
<tr> <tr>
<td class="is-narrow">Minimum</td> <td class="is-narrow">Minimum</td>
<td>~ {minCost} </td> <td>{minCost} {getCurrency(project)}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -16,7 +16,7 @@
} }
.middleTable td { .middleTable td {
vertical-align: middle; vertical-align: middle !important;
} }
.tabContainer { .tabContainer {

View File

@ -5,6 +5,8 @@ import { Project } from "../../models/project";
import { newTask, Task, TaskID, EstimationConfidence } from "../../models/task"; import { newTask, Task, TaskID, EstimationConfidence } from "../../models/task";
import EditableText from "../../components/editable-text"; import EditableText from "../../components/editable-text";
import { usePrintMediaQuery } from "../../hooks/use-media-query"; import { usePrintMediaQuery } from "../../hooks/use-media-query";
import { defaults, getTimeUnit } from "../../models/params";
import ProjectTimeUnit from "../../components/project-time-unit";
export interface TaskTableProps { export interface TaskTableProps {
project: Project project: Project
@ -83,7 +85,7 @@ const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, on
<th class={`${style.noBorder} noPrint`} rowSpan={2}></th> <th class={`${style.noBorder} noPrint`} rowSpan={2}></th>
<th class={style.mainColumn} rowSpan={2}>Tâche</th> <th class={style.mainColumn} rowSpan={2}>Tâche</th>
<th rowSpan={2}>Catégorie</th> <th rowSpan={2}>Catégorie</th>
<th colSpan={3}>Estimation</th> <th colSpan={3}>Estimation (en <ProjectTimeUnit project={project} />)</th>
</tr> </tr>
<tr> <tr>
<th>Optimiste</th> <th>Optimiste</th>
@ -185,9 +187,9 @@ const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, on
</tr> </tr>
<tr> <tr>
<td colSpan={isPrint ? 2 : 3} class={style.noBorder}></td> <td colSpan={isPrint ? 2 : 3} class={style.noBorder}></td>
<td>{totals.optimistic}</td> <td>{totals.optimistic} <ProjectTimeUnit project={project} /></td>
<td>{totals.likely}</td> <td>{totals.likely} <ProjectTimeUnit project={project} /></td>
<td>{totals.pessimistic}</td> <td>{totals.pessimistic} <ProjectTimeUnit project={project} /></td>
</tr> </tr>
</tfoot> </tfoot>
</table> </table>

View File

@ -1,6 +1,7 @@
import { FunctionalComponent, h } from "preact"; import { FunctionalComponent, h, Fragment } from "preact";
import { Project } from "../../models/project"; 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 { export interface TimePreviewProps {
project: Project project: Project
@ -8,7 +9,6 @@ export interface TimePreviewProps {
const TimePreview: FunctionalComponent<TimePreviewProps> = ({ project }) => { const TimePreview: FunctionalComponent<TimePreviewProps> = ({ project }) => {
const estimations = useProjectEstimations(project); const estimations = useProjectEstimations(project);
return ( return (
<div class="table-container"> <div class="table-container">
<table class="table is-bordered is-striped is-fullwidth"> <table class="table is-bordered is-striped is-fullwidth">
@ -24,15 +24,15 @@ const TimePreview: FunctionalComponent<TimePreviewProps> = ({ project }) => {
<tbody> <tbody>
<tr> <tr>
<td class="is-narrow">>= 99.7%</td> <td class="is-narrow">>= 99.7%</td>
<td>{`${estimations.p99.e.toPrecision(2)} ± ${estimations.p99.sd.toPrecision(2)} j/h`}</td> <td><EstimationRange project={project} estimation={estimations.p99} sdFactor={3} /></td>
</tr> </tr>
<tr> <tr>
<td class="is-narrow">>= 90%</td> <td class="is-narrow">>= 90%</td>
<td>{`${estimations.p90.e.toPrecision(2)} ± ${estimations.p90.sd.toPrecision(2)} j/h`}</td> <td><EstimationRange project={project} estimation={estimations.p90} sdFactor={1.645} /></td>
</tr> </tr>
<tr> <tr>
<td class="is-narrow">>= 68%</td> <td class="is-narrow">>= 68%</td>
<td>{`${estimations.p68.e.toPrecision(2)} ± ${estimations.p68.sd.toPrecision(2)} j/h`}</td> <td><EstimationRange project={project} estimation={estimations.p68} sdFactor={1} /></td>
</tr> </tr>
</tbody> </tbody>
<tfoot class="noPrint"> <tfoot class="noPrint">
@ -45,6 +45,6 @@ const TimePreview: FunctionalComponent<TimePreviewProps> = ({ project }) => {
</table> </table>
</div> </div>
); );
}; };
export default TimePreview; export default TimePreview;