feat(ui+backend): base of data persistence

This commit is contained in:
2020-09-11 09:19:18 +02:00
parent c7cea6e46b
commit 7fc1a7f3af
37 changed files with 1298 additions and 195 deletions

View File

@ -11,7 +11,6 @@ export interface TimeUnit {
}
export interface Params {
taskCategories: TaskCategoriesIndex
timeUnit: TimeUnit
currency: string
roundUpEstimations: boolean
@ -19,53 +18,38 @@ export interface Params {
}
export const defaults = {
taskCategories: {
"RQ15CD3iX1Ey2f9kat7tfLGZmUx9GGc15nS6A7fYtZv76SnS4": {
id: "RQ15CD3iX1Ey2f9kat7tfLGZmUx9GGc15nS6A7fYtZv76SnS4",
label: "Développement",
costPerTimeUnit: 500,
},
"QRdGS5Pr5si9SSjU84WAq19cjxQ3rUL71jKh8oHSMZSY4bBH9": {
id: "QRdGS5Pr5si9SSjU84WAq19cjxQ3rUL71jKh8oHSMZSY4bBH9",
label: "Conduite de projet",
costPerTimeUnit: 500,
},
"RPcqFMLdQrgBSomv7Sao7EQSb7on6rtjfDQK5JZNhNSg9DwEo": {
id: "RPcqFMLdQrgBSomv7Sao7EQSb7on6rtjfDQK5JZNhNSg9DwEo",
label: "Recette",
costPerTimeUnit: 500,
},
},
params: {
timeUnit: {
label: "jour/homme",
acronym: "j/h",
label: "jour/homme",
acronym: "j/h",
},
roundUpEstimations: true,
currency: "€ H.T.",
costPerTimeUnit: 500,
hideFinancialPreviewOnPrint: false,
}
}
export function getTimeUnit(project: Project): TimeUnit {
return project.params.timeUnit ? project.params.timeUnit : defaults.timeUnit;
return project.params.timeUnit ? project.params.timeUnit : defaults.params.timeUnit;
}
export function getRoundUpEstimations(project: Project): boolean {
return project.params.hasOwnProperty("roundUpEstimations") ? project.params.roundUpEstimations : defaults.roundUpEstimations;
return project.params.hasOwnProperty("roundUpEstimations") ? project.params.roundUpEstimations : defaults.params.roundUpEstimations;
}
export function getCurrency(project: Project): string {
return project.params.currency ? project.params.currency : defaults.currency;
return project.params.currency ? project.params.currency : defaults.params.currency;
}
export function getTaskCategories(project: Project): TaskCategoriesIndex {
return project.params.taskCategories ? project.params.taskCategories : defaults.taskCategories;
export function getTaskCategories(project: Project): TaskCategory[] {
return project.taskCategories ? project.taskCategories : [];
}
export function getTaskCategoryCost(taskCategory: TaskCategory): number {
return taskCategory.hasOwnProperty("costPerTimeUnit") ? taskCategory.costPerTimeUnit : defaults.costPerTimeUnit;
return taskCategory.hasOwnProperty("costPerTimeUnit") ? taskCategory.costPerTimeUnit : defaults.params.costPerTimeUnit;
}
export function getHideFinancialPreviewOnPrint(project: Project): boolean {
return project.params.hasOwnProperty("hideFinancialPreviewOnPrint") ? project.params.hideFinancialPreviewOnPrint : defaults.hideFinancialPreviewOnPrint;
return project.params.hasOwnProperty("hideFinancialPreviewOnPrint") ? project.params.hideFinancialPreviewOnPrint : defaults.params.hideFinancialPreviewOnPrint;
}

View File

@ -1,29 +1,27 @@
import { Task, TaskCategory, TaskID, getTaskWeightedMean, TaskCategoryID, getTaskStandardDeviation } from './task';
import { Params, defaults, getTaskCategoryCost } from "./params";
import { Params, defaults, getTaskCategoryCost, TaskCategoriesIndex } from "./params";
import { Estimation } from '../hooks/useProjectEstimations';
export type ProjectID = string;
export type ProjectID = number;
export interface Project {
id: ProjectID
label: string
title: string
description: string
tasks: Tasks
taskCategories: TaskCategory[]
tasks: Task[]
params: Params
}
export interface Tasks {
[id: string]: Task
}
export function newProject(): Project {
export function newProject(id: number|undefined = undefined): Project {
return {
id: "",
label: "",
id: id,
title: "",
description: "",
tasks: {},
tasks: [],
taskCategories: [],
params: {
...defaults
...defaults.params
},
};
}
@ -36,7 +34,7 @@ export function getProjectWeightedMean(p : Project): number {
}
export function getTaskCategoryWeightedMean(taskCategoryId: TaskCategoryID, p : Project): number {
return Object.values(p.tasks).filter(t => t.category === taskCategoryId).reduce((sum: number, t: Task) => {
return Object.values(p.tasks).filter(t => t.category && t.category.id === taskCategoryId).reduce((sum: number, t: Task) => {
sum += getTaskWeightedMean(t);
return sum;
}, 0);
@ -58,7 +56,7 @@ export function getTaskCategoriesMeanRepartition(project: Project): MeanRepartit
const repartition: MeanRepartition = {};
Object.values(project.params.taskCategories).forEach(tc => {
Object.values(project.taskCategories).forEach(tc => {
repartition[tc.id] = getTaskCategoryWeightedMean(tc.id, project) / projectMean;
if (Number.isNaN(repartition[tc.id])) repartition[tc.id] = 0;
});
@ -74,34 +72,54 @@ export interface MinMaxCost {
export interface Cost {
totalCost: number
totalTime: number
details: { [taskCategoryId: string]: { time: number, cost: number } }
details: CostDetails[]
}
export interface CostDetails {
time: number
cost: number
taskCategoryId: TaskCategoryID
}
export function getMinMaxCosts(project: Project, estimation: Estimation): MinMaxCost {
const max: Cost = {totalCost: 0, totalTime: 0, details: {}};
const min: Cost = {totalCost: 0, totalTime: 0, details: {}};
const max: Cost = {totalCost: 0, totalTime: 0, details: []};
const min: Cost = {totalCost: 0, totalTime: 0, details: []};
const repartition = getTaskCategoriesMeanRepartition(project);
Object.values(project.params.taskCategories).forEach(tc => {
Object.values(project.taskCategories).forEach(tc => {
const cost = getTaskCategoryCost(tc);
const maxTime = Math.round((estimation.e + estimation.sd) * repartition[tc.id]);
max.details[tc.id] = {
time: maxTime,
cost: Math.ceil(maxTime) * cost,
};
max.totalTime += max.details[tc.id].time;
max.totalCost += max.details[tc.id].cost;
let maxDetails = getDetailsForTaskCategory(max.details, tc.id);
if (!maxDetails) {
maxDetails = { taskCategoryId: tc.id, time: 0, cost: 0 };
max.details.push(maxDetails);
}
maxDetails.time = maxTime;
maxDetails.cost = Math.ceil(maxTime) * cost;
max.totalTime += maxDetails.time;
max.totalCost += maxDetails.cost;
const minTime = Math.round((estimation.e - estimation.sd) * repartition[tc.id]);
min.details[tc.id] = {
time: minTime,
cost: Math.ceil(minTime) * cost,
};
min.totalTime += min.details[tc.id].time;
min.totalCost += min.details[tc.id].cost;
let minDetails = getDetailsForTaskCategory(min.details, tc.id);
if (!minDetails) {
minDetails = { taskCategoryId: tc.id, time: 0, cost: 0 };
min.details.push(minDetails);
}
minDetails.time = minTime;
minDetails.cost = Math.ceil(minTime) * cost;
min.totalTime += minDetails.time;
min.totalCost += minDetails.cost;
});
return { max, min };
}
function getDetailsForTaskCategory(details: CostDetails[], id: TaskCategoryID): CostDetails|void {
for (let d, i = 0; (d = details[i]); ++i) {
if (d.taskCategoryId === id) return d;
}
}

View File

@ -1,6 +1,7 @@
import { defaults } from "./params";
import { uuid } from "../util/uuid";
export type TaskID = string
export type TaskID = number
export enum EstimationConfidence {
Optimistic = "optimistic",
@ -11,21 +12,21 @@ export enum EstimationConfidence {
export interface Task {
id: TaskID
label: string
category: TaskCategoryID
category: TaskCategory
estimations: { [confidence in EstimationConfidence]: number }
}
export type TaskCategoryID = string
export type TaskCategoryID = number
export interface TaskCategory {
id: TaskCategoryID
id: number
label: string
costPerTimeUnit: number
}
export function newTask(label: string, category: TaskCategoryID): Task {
export function newTask(label: string, category: TaskCategory): Task {
return {
id: '',
id: null,
label,
category,
estimations: {
@ -38,8 +39,8 @@ export function newTask(label: string, category: TaskCategoryID): Task {
export function createTaskCategory(): TaskCategory {
return {
id: '',
costPerTimeUnit: defaults.costPerTimeUnit,
id: null,
costPerTimeUnit: defaults.params.costPerTimeUnit,
label: ""
};
}