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",
|
||||
"short_name": "guesstimate",
|
||||
"name": "Guesstimate",
|
||||
"short_name": "Guesstimate",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"orientation": "portrait",
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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",
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
}
|
||||
|
||||
.middleTable td {
|
||||
vertical-align: middle;
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
|
||||
.tabContainer {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
Loading…
Reference in New Issue