Use project parameters for calculations
This commit is contained in:
parent
642b555b3d
commit
69867b113f
|
@ -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> <ProjectTimeUnit project={project} />
|
||||||
|
</Fragment>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EstimationRange;
|
|
@ -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;
|
|
@ -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",
|
||||||
|
|
|
@ -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 {
|
||||||
|
taskCategories: TaskCategoriesIndex
|
||||||
|
timeUnit: TimeUnit
|
||||||
|
currency: string
|
||||||
|
roundUpEstimations: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaults = {
|
||||||
|
taskCategories: {
|
||||||
"7e92266f-0a7b-4728-8322-5fe05ff3b929": {
|
"7e92266f-0a7b-4728-8322-5fe05ff3b929": {
|
||||||
id: "7e92266f-0a7b-4728-8322-5fe05ff3b929",
|
id: "7e92266f-0a7b-4728-8322-5fe05ff3b929",
|
||||||
label: "Développement"
|
label: "Développement",
|
||||||
|
costPerTimeUnit: 500,
|
||||||
},
|
},
|
||||||
"508a0925-a664-4426-8d40-6974156f0f00": {
|
"508a0925-a664-4426-8d40-6974156f0f00": {
|
||||||
id: "508a0925-a664-4426-8d40-6974156f0f00",
|
id: "508a0925-a664-4426-8d40-6974156f0f00",
|
||||||
label: "Conduite de projet"
|
label: "Conduite de projet",
|
||||||
|
costPerTimeUnit: 500,
|
||||||
},
|
},
|
||||||
"7aab4d66-072e-4cc8-aae8-b62edd3237a8": {
|
"7aab4d66-072e-4cc8-aae8-b62edd3237a8": {
|
||||||
id: "7aab4d66-072e-4cc8-aae8-b62edd3237a8",
|
id: "7aab4d66-072e-4cc8-aae8-b62edd3237a8",
|
||||||
label: "Recette"
|
label: "Recette",
|
||||||
|
costPerTimeUnit: 500,
|
||||||
},
|
},
|
||||||
};
|
},
|
||||||
|
timeUnit: {
|
||||||
|
label: "jour/homme",
|
||||||
|
acronym: "j/h",
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
}
|
|
@ -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",
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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,8 +28,8 @@ const EstimationTab: FunctionalComponent<EstimationTabProps> = ({ project, dispa
|
||||||
dispatch(updateTaskEstimation(taskId, confidence, value));
|
dispatch(updateTaskEstimation(taskId, confidence, value));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Fragment>
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-9">
|
<div class="column is-9">
|
||||||
<TaskTable
|
<TaskTable
|
||||||
|
@ -44,6 +44,17 @@ const EstimationTab: FunctionalComponent<EstimationTabProps> = ({ project, dispa
|
||||||
<FinancialPreview project={project} />
|
<FinancialPreview project={project} />
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.middleTable td {
|
.middleTable td {
|
||||||
vertical-align: middle;
|
vertical-align: middle !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabContainer {
|
.tabContainer {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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">
|
||||||
|
|
Loading…
Reference in New Issue