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",
"short_name": "guesstimate",
"name": "Guesstimate",
"short_name": "Guesstimate",
"start_url": "/",
"display": "standalone",
"orientation": "portrait",

View File

@ -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"
},
};
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;
}

View File

@ -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",
}
},
};
}

View File

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

View File

@ -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<EstimationTabProps> = ({ project, dispa
dispatch(updateTaskEstimation(taskId, confidence, value));
};
return (
<div class="columns">
<div class="column is-9">
<TaskTable
project={project}
onTaskAdd={onTaskAdd}
onTaskRemove={onTaskRemove}
onTaskLabelUpdate={onTaskLabelUpdate}
onEstimationChange={onEstimationChange} />
<Fragment>
<div class="columns">
<div class="column is-9">
<TaskTable
project={project}
onTaskAdd={onTaskAdd}
onTaskRemove={onTaskRemove}
onTaskLabelUpdate={onTaskLabelUpdate}
onEstimationChange={onEstimationChange} />
</div>
<div class="column is-3">
<TimePreview project={project} />
<FinancialPreview project={project} />
</div>
</div>
<div class="column is-3">
<TimePreview project={project} />
<FinancialPreview project={project} />
</div>
</div>
{
Object.keys(project.tasks).length <= 20 ?
<div class="message noPrint">
<div class="message-body">
<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 { 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<FinancialPreviewProps> = ({ 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 (
<div class="table-container">
<table class="table is-bordered is-striped is-fullwidth">
@ -27,11 +28,11 @@ const FinancialPreview: FunctionalComponent<FinancialPreviewProps> = ({ project
<tbody>
<tr>
<td class="is-narrow">Maximum</td>
<td>~ {maxCost} </td>
<td>{maxCost} {getCurrency(project)}</td>
</tr>
<tr>
<td class="is-narrow">Minimum</td>
<td>~ {minCost} </td>
<td>{minCost} {getCurrency(project)}</td>
</tr>
</tbody>
</table>

View File

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

View File

@ -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<TaskTableProps> = ({ project, onTaskAdd, on
<th class={`${style.noBorder} noPrint`} rowSpan={2}></th>
<th class={style.mainColumn} rowSpan={2}>Tâche</th>
<th rowSpan={2}>Catégorie</th>
<th colSpan={3}>Estimation</th>
<th colSpan={3}>Estimation (en <ProjectTimeUnit project={project} />)</th>
</tr>
<tr>
<th>Optimiste</th>
@ -185,9 +187,9 @@ const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, on
</tr>
<tr>
<td colSpan={isPrint ? 2 : 3} class={style.noBorder}></td>
<td>{totals.optimistic}</td>
<td>{totals.likely}</td>
<td>{totals.pessimistic}</td>
<td>{totals.optimistic} <ProjectTimeUnit project={project} /></td>
<td>{totals.likely} <ProjectTimeUnit project={project} /></td>
<td>{totals.pessimistic} <ProjectTimeUnit project={project} /></td>
</tr>
</tfoot>
</table>

View File

@ -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<TimePreviewProps> = ({ project }) => {
const estimations = useProjectEstimations(project);
return (
<div class="table-container">
<table class="table is-bordered is-striped is-fullwidth">
@ -24,15 +24,15 @@ const TimePreview: FunctionalComponent<TimePreviewProps> = ({ project }) => {
<tbody>
<tr>
<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>
<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>
<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>
</tbody>
<tfoot class="noPrint">
@ -45,6 +45,6 @@ const TimePreview: FunctionalComponent<TimePreviewProps> = ({ project }) => {
</table>
</div>
);
};
};
export default TimePreview;