Add weighted repartition preview

This commit is contained in:
wpetit 2020-04-23 09:15:27 +02:00
parent d1275cc1e4
commit 7e89fc43f9
3 changed files with 98 additions and 33 deletions

View File

@ -5,6 +5,7 @@ import TimePreview from "./time-preview";
import FinancialPreview from "./financial-preview"; import FinancialPreview from "./financial-preview";
import { addTask, updateTaskEstimation, removeTask, updateProjectLabel, updateTaskLabel, ProjectReducerActions } from "../../hooks/use-project-reducer"; import { addTask, updateTaskEstimation, removeTask, updateProjectLabel, updateTaskLabel, ProjectReducerActions } from "../../hooks/use-project-reducer";
import { Task, TaskID, EstimationConfidence } from "../../models/task"; import { Task, TaskID, EstimationConfidence } from "../../models/task";
import RepartitionPreview from "./repartition-preview";
export interface EstimationTabProps { export interface EstimationTabProps {
project: Project project: Project
@ -29,32 +30,40 @@ const EstimationTab: FunctionalComponent<EstimationTabProps> = ({ project, dispa
}; };
return ( return (
<Fragment> <Fragment>
<div class="columns">
<div class="column is-9">
<TaskTable
project={project}
onTaskAdd={onTaskAdd}
onTaskRemove={onTaskRemove}
onTaskLabelUpdate={onTaskLabelUpdate}
onEstimationChange={onEstimationChange} />
<div class="columns"> <div class="columns">
<div class="column is-9"> <div class="column is-5 is-offset-7">
<TaskTable <RepartitionPreview project={project} />
project={project} </div>
onTaskAdd={onTaskAdd}
onTaskRemove={onTaskRemove}
onTaskLabelUpdate={onTaskLabelUpdate}
onEstimationChange={onEstimationChange} />
</div>
<div class="column is-3">
<TimePreview project={project} />
<FinancialPreview project={project} />
</div>
</div> </div>
</div>
<div class="column is-3">
<TimePreview project={project} />
<FinancialPreview project={project} />
{ {
Object.keys(project.tasks).length <= 20 ? Object.keys(project.tasks).length <= 20 ?
<div class="message noPrint"> <div class="message noPrint">
<div class="message-body"> <div class="message-body">
<p><strong> Attention</strong></p> <p><strong> Attention</strong></p>
<p>Votre projet ne contient pas assez de tâches pour que les niveaux de confiance soient fiables. Un minimum de 20 tâches est conseillé pour obtenir une estimation pertinente.</p> <br />
</div> <p>Votre projet ne contient pas assez de tâches pour que les niveaux de confiance soient fiables.</p>
</div> : <br />
null <p>Un minimum de 20 tâches est conseillé pour obtenir une estimation pertinente.</p>
</div>
</div> :
null
} }
</Fragment> </div>
</div>
</Fragment>
); );
}; };

View File

@ -0,0 +1,48 @@
import { FunctionalComponent, h } from "preact";
import { Project } from "../../models/project";
import { useProjectEstimations } from "../../hooks/use-project-estimations";
import { getCurrency, getRoundUpEstimations } from "../../models/params";
import ProjectTimeUnit from "../../components/project-time-unit";
import { getTaskCategoryWeightedMean, getProjectWeightedMean } from "../../util/stat";
export interface RepartitionPreviewProps {
project: Project
}
const RepartitionPreview: FunctionalComponent<RepartitionPreviewProps> = ({ project }) => {
const projectMean = getProjectWeightedMean(project);
return (
<div class="table-container">
<table class="table is-bordered is-striped is-fullwidth">
<thead>
<tr>
<th colSpan={2}>Répartition moyenne pondérée</th>
</tr>
<tr>
<th>Catégorie</th>
<th>Temps</th>
</tr>
</thead>
<tbody>
{
Object.values(project.params.taskCategories).map(tc => {
let mean = getTaskCategoryWeightedMean(tc.id, project);
const percent = ((mean/projectMean) * 100).toFixed(0);
if (getRoundUpEstimations(project)) {
mean = Math.ceil(mean);
}
return (
<tr key={`task-category-${tc.id}`}>
<td>{tc.label}</td>
<td>~ {mean} <ProjectTimeUnit project={project} /> <span class="is-size-7 is-pulled-right">({percent} %)</span></td>
</tr>
);
})
}
</tbody>
</table>
</div>
);
};
export default RepartitionPreview;

View File

@ -1,24 +1,32 @@
import { Task } from "../models/task"; import { Task, TaskCategory, TaskCategoryID } from "../models/task";
import { Project } from "../models/project"; import { Project } from "../models/project";
import { TaskCategoriesTableProps } from "../routes/project/task-categories-table";
export function getTaskWeightedMean(t: Task): number { export function getTaskWeightedMean(t: Task): number {
return (t.estimations.optimistic + (4*t.estimations.likely) + t.estimations.pessimistic) / 6; return (t.estimations.optimistic + (4*t.estimations.likely) + t.estimations.pessimistic) / 6;
} }
export function getTaskStandardDeviation(t: Task): number { export function getTaskStandardDeviation(t: Task): number {
return (t.estimations.pessimistic - t.estimations.optimistic) / 6; return (t.estimations.pessimistic - t.estimations.optimistic) / 6;
} }
export function getProjectWeightedMean(p : Project): number { export function getProjectWeightedMean(p : Project): number {
return Object.values(p.tasks).reduce((sum: number, t: Task) => { return Object.values(p.tasks).reduce((sum: number, t: Task) => {
sum += getTaskWeightedMean(t); sum += getTaskWeightedMean(t);
return sum; return sum;
}, 0); }, 0);
}
export function getTaskCategoryWeightedMean(taskCategoryId: TaskCategoryID, p : Project): number {
return Object.values(p.tasks).filter(t => t.category === taskCategoryId).reduce((sum: number, t: Task) => {
sum += getTaskWeightedMean(t);
return sum;
}, 0);
} }
export function getProjectStandardDeviation(p : Project): number { export function getProjectStandardDeviation(p : Project): number {
return Math.sqrt(Object.values(p.tasks).reduce((sum: number, t: Task) => { return Math.sqrt(Object.values(p.tasks).reduce((sum: number, t: Task) => {
sum += Math.pow(getTaskStandardDeviation(t), 2); sum += Math.pow(getTaskStandardDeviation(t), 2);
return sum; return sum;
}, 0)); }, 0));
} }