Project parameters edition
This commit is contained in:
parent
69867b113f
commit
b0339d2ce0
|
@ -3148,6 +3148,11 @@
|
|||
"resolved": "https://registry.npmjs.org/bulma/-/bulma-0.8.2.tgz",
|
||||
"integrity": "sha512-vMM/ijYSxX+Sm+nD7Lmc1UgWDy2JcL2nTKqwgEqXuOMU+IGALbXd5MLt/BcjBAPLIx36TtzhzBcSnOP974gcqA=="
|
||||
},
|
||||
"bulma-switch": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bulma-switch/-/bulma-switch-2.0.0.tgz",
|
||||
"integrity": "sha512-myD38zeUfjmdduq+pXabhJEe3x2hQP48l/OI+Y0fO3HdDynZUY/VJygucvEAJKRjr4HxD5DnEm4yx+oDOBXpAA=="
|
||||
},
|
||||
"bytes": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
"@types/bs58": "^4.0.1",
|
||||
"bs58": "^4.0.1",
|
||||
"bulma": "^0.8.2",
|
||||
"bulma-switch": "^2.0.0",
|
||||
"preact": "^10.3.1",
|
||||
"preact-jsx-chai": "^3.0.0",
|
||||
"preact-markup": "^2.0.0",
|
||||
|
|
|
@ -8,7 +8,7 @@ export interface EditableTextProps {
|
|||
class?: string
|
||||
editIconClass?: string
|
||||
onChange?: (value: string) => void
|
||||
render: (value: string) => ComponentChild
|
||||
render?: (value: string) => ComponentChild
|
||||
}
|
||||
|
||||
const EditableText: FunctionalComponent<EditableTextProps> = ({ onChange, value, render, ...props }) => {
|
||||
|
@ -45,7 +45,7 @@ const EditableText: FunctionalComponent<EditableTextProps> = ({ onChange, value,
|
|||
</div>
|
||||
</div> :
|
||||
<Fragment>
|
||||
{ render(internalValue) }
|
||||
{ render ? render(internalValue) : <span>{internalValue}</span> }
|
||||
<i class={`${style.editIcon} icon ${props.editIconClass ? props.editIconClass : ''}`} onClick={onEditIconClick}>🖋️</i>
|
||||
</Fragment>
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.editableText {
|
||||
display: inherit;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.editableText > * {
|
||||
|
|
|
@ -9,13 +9,17 @@ export interface EstimationRangeProps {
|
|||
|
||||
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);
|
||||
let e = roundUp ? Math.ceil(estimation.e) : estimation.e;
|
||||
let sd = roundUp ? Math.ceil(estimation.sd * sdFactor) : (estimation.sd * sdFactor);
|
||||
const max = e+sd;
|
||||
const min = Math.max(e-sd, 0);
|
||||
if (!roundUp) {
|
||||
sd = sd.toFixed(2);
|
||||
e = e.toFixed(2);
|
||||
}
|
||||
return (
|
||||
<Fragment>
|
||||
<abbr title={`max: ${max}, min: ${min}`}>{`${e} ± ${sd}`}</abbr> <ProjectTimeUnit project={project} />
|
||||
<abbr title={`max: ${max.toFixed(2)}, min: ${min.toFixed(2)}`}>{`${e} ± ${sd}`}</abbr> <ProjectTimeUnit project={project} />
|
||||
</Fragment>
|
||||
|
||||
);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Project } from "../models/project";
|
||||
import { Task, TaskID, EstimationConfidence } from "../models/task";
|
||||
import { Task, TaskID, EstimationConfidence, TaskCategoryID, TaskCategory } from "../models/task";
|
||||
import { useReducer } from "preact/hooks";
|
||||
|
||||
export interface Action {
|
||||
|
@ -11,13 +11,19 @@ export type ProjectReducerActions =
|
|||
RemoveTask |
|
||||
UpdateTaskEstimation |
|
||||
UpdateProjectLabel |
|
||||
UpdateTaskLabel
|
||||
UpdateTaskLabel |
|
||||
UpdateParam |
|
||||
UpdateTaskCategoryLabel |
|
||||
UpdateTaskCategoryCost |
|
||||
AddTaskCategory |
|
||||
RemoveTaskCategory
|
||||
|
||||
export function useProjectReducer(project: Project) {
|
||||
return useReducer(projectReducer, project);
|
||||
}
|
||||
|
||||
export function projectReducer(project: Project, action: ProjectReducerActions): Project {
|
||||
console.log(action);
|
||||
switch(action.type) {
|
||||
case ADD_TASK:
|
||||
return handleAddTask(project, action as AddTask);
|
||||
|
@ -33,6 +39,21 @@ export function projectReducer(project: Project, action: ProjectReducerActions):
|
|||
|
||||
case UPDATE_TASK_LABEL:
|
||||
return handleUpdateTaskLabel(project, action as UpdateTaskLabel);
|
||||
|
||||
case UPDATE_PARAM:
|
||||
return handleUpdateParam(project, action as UpdateParam);
|
||||
|
||||
case ADD_TASK_CATEGORY:
|
||||
return handleAddTaskCategory(project, action as AddTaskCategory);
|
||||
|
||||
case REMOVE_TASK_CATEGORY:
|
||||
return handleRemoveTaskCategory(project, action as RemoveTaskCategory);
|
||||
|
||||
case UPDATE_TASK_CATEGORY_LABEL:
|
||||
return handleUpdateTaskCategoryLabel(project, action as UpdateTaskCategoryLabel);
|
||||
|
||||
case UPDATE_TASK_CATEGORY_COST:
|
||||
return handleUpdateTaskCategoryCost(project, action as UpdateTaskCategoryCost);
|
||||
}
|
||||
|
||||
return project;
|
||||
|
@ -156,4 +177,125 @@ export function handleUpdateTaskLabel(project: Project, action: UpdateTaskLabel)
|
|||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface UpdateParam extends Action {
|
||||
name: string
|
||||
value: any
|
||||
}
|
||||
|
||||
export const UPDATE_PARAM = "UPDATE_PARAM";
|
||||
|
||||
export function updateParam(name: string, value: any): UpdateParam {
|
||||
return { type: UPDATE_PARAM, name, value };
|
||||
}
|
||||
|
||||
export function handleUpdateParam(project: Project, action: UpdateParam): Project {
|
||||
return {
|
||||
...project,
|
||||
params: {
|
||||
...project.params,
|
||||
[action.name]: action.value,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface UpdateTaskCategoryLabel extends Action {
|
||||
categoryId: TaskCategoryID
|
||||
label: string
|
||||
}
|
||||
|
||||
export const UPDATE_TASK_CATEGORY_LABEL = "UPDATE_TASK_CATEGORY_LABEL";
|
||||
|
||||
export function updateTaskCategoryLabel(categoryId: TaskCategoryID, label: string): UpdateTaskCategoryLabel {
|
||||
return { type: UPDATE_TASK_CATEGORY_LABEL, categoryId, label };
|
||||
}
|
||||
|
||||
export function handleUpdateTaskCategoryLabel(project: Project, action: UpdateTaskCategoryLabel): Project {
|
||||
return {
|
||||
...project,
|
||||
params: {
|
||||
...project.params,
|
||||
taskCategories: {
|
||||
...project.params.taskCategories,
|
||||
[action.categoryId]: {
|
||||
...project.params.taskCategories[action.categoryId],
|
||||
label: action.label
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface UpdateTaskCategoryCost extends Action {
|
||||
categoryId: TaskCategoryID
|
||||
costPerTimeUnit: number
|
||||
}
|
||||
|
||||
export const UPDATE_TASK_CATEGORY_COST = "UPDATE_TASK_CATEGORY_COST";
|
||||
|
||||
export function updateTaskCategoryCost(categoryId: TaskCategoryID, costPerTimeUnit: number): UpdateTaskCategoryCost {
|
||||
return { type: UPDATE_TASK_CATEGORY_COST, categoryId, costPerTimeUnit };
|
||||
}
|
||||
|
||||
export function handleUpdateTaskCategoryCost(project: Project, action: UpdateTaskCategoryCost): Project {
|
||||
return {
|
||||
...project,
|
||||
params: {
|
||||
...project.params,
|
||||
taskCategories: {
|
||||
...project.params.taskCategories,
|
||||
[action.categoryId]: {
|
||||
...project.params.taskCategories[action.categoryId],
|
||||
costPerTimeUnit: action.costPerTimeUnit
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const ADD_TASK_CATEGORY = "ADD_TASK_CATEGORY";
|
||||
|
||||
export interface AddTaskCategory extends Action {
|
||||
taskCategory: TaskCategory
|
||||
}
|
||||
|
||||
export function addTaskCategory(taskCategory: TaskCategory): AddTaskCategory {
|
||||
return { type: ADD_TASK_CATEGORY, taskCategory };
|
||||
}
|
||||
|
||||
export function handleAddTaskCategory(project: Project, action: AddTaskCategory): Project {
|
||||
const taskCategory = { ...action.taskCategory };
|
||||
return {
|
||||
...project,
|
||||
params: {
|
||||
...project.params,
|
||||
taskCategories: {
|
||||
...project.params.taskCategories,
|
||||
[taskCategory.id]: taskCategory,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface RemoveTaskCategory extends Action {
|
||||
taskCategoryId: TaskCategoryID
|
||||
}
|
||||
|
||||
export const REMOVE_TASK_CATEGORY = "REMOVE_TASK_CATEGORY";
|
||||
|
||||
export function removeTaskCategory(taskCategoryId: TaskCategoryID): RemoveTaskCategory {
|
||||
return { type: REMOVE_TASK_CATEGORY, taskCategoryId };
|
||||
}
|
||||
|
||||
export function handleRemoveTaskCategory(project: Project, action: RemoveTaskCategory): Project {
|
||||
const taskCategories = { ...project.params.taskCategories };
|
||||
delete taskCategories[action.taskCategoryId];
|
||||
return {
|
||||
...project,
|
||||
params: {
|
||||
...project.params,
|
||||
taskCategories
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import "./style/index.css";
|
||||
import "bulma/css/bulma.css";
|
||||
import "bulma-switch/dist/css/bulma-switch.min.css";
|
||||
|
||||
import App from "./components/app.tsx";
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { TaskCategory, CategoryID } from "./task";
|
||||
import { TaskCategory, TaskCategoryID } from "./task";
|
||||
import { Project } from "./project";
|
||||
|
||||
export interface TaskCategoriesIndex {
|
||||
|
@ -41,6 +41,7 @@ export const defaults = {
|
|||
},
|
||||
roundUpEstimations: true,
|
||||
currency: "€ H.T.",
|
||||
costPerTimeUnit: 500,
|
||||
}
|
||||
|
||||
export function getTimeUnit(project: Project): TimeUnit {
|
||||
|
@ -57,4 +58,8 @@ export function getCurrency(project: Project): string {
|
|||
|
||||
export function getTaskCategories(project: Project): TaskCategoriesIndex {
|
||||
return project.params.taskCategories ? project.params.taskCategories : defaults.taskCategories;
|
||||
}
|
||||
|
||||
export function getTaskCategoryCost(taskCategory: TaskCategory): number {
|
||||
return taskCategory.hasOwnProperty("costPerTimeUnit") ? taskCategory.costPerTimeUnit : defaults.costPerTimeUnit;
|
||||
}
|
|
@ -24,12 +24,9 @@ export function newProject(id?: string): Project {
|
|||
tasks: {},
|
||||
params: {
|
||||
taskCategories: defaults.taskCategories,
|
||||
currency: "€",
|
||||
roundUpEstimations: true,
|
||||
timeUnit: {
|
||||
label: "Jour/homme",
|
||||
acronym: "j/h",
|
||||
}
|
||||
currency: defaults.currency,
|
||||
roundUpEstimations: defaults.roundUpEstimations,
|
||||
timeUnit: defaults.timeUnit,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -11,19 +11,19 @@ export enum EstimationConfidence {
|
|||
export interface Task {
|
||||
id: TaskID
|
||||
label: string
|
||||
category: CategoryID
|
||||
category: TaskCategoryID
|
||||
estimations: { [confidence in EstimationConfidence]: number }
|
||||
}
|
||||
|
||||
export type CategoryID = string
|
||||
export type TaskCategoryID = string
|
||||
|
||||
export interface TaskCategory {
|
||||
id: CategoryID
|
||||
id: TaskCategoryID
|
||||
label: string
|
||||
costPerTimeUnit: number
|
||||
}
|
||||
|
||||
export function newTask(label: string, category: CategoryID): Task {
|
||||
export function newTask(label: string, category: TaskCategoryID): Task {
|
||||
return {
|
||||
id: uuidV4(),
|
||||
label,
|
||||
|
|
|
@ -49,7 +49,7 @@ const EstimationTab: FunctionalComponent<EstimationTabProps> = ({ project, dispa
|
|||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div> :
|
||||
null
|
||||
|
|
|
@ -8,6 +8,7 @@ import { useLocalStorage } from "../../hooks/use-local-storage";
|
|||
import EditableText from "../../components/editable-text";
|
||||
import Tabs from "../../components/tabs";
|
||||
import EstimationTab from "./estimation-tab";
|
||||
import ParamsTab from "./params-tab";
|
||||
|
||||
export interface ProjectProps {
|
||||
projectId: string
|
||||
|
@ -44,7 +45,7 @@ const Project: FunctionalComponent<ProjectProps> = ({ projectId }) => {
|
|||
{
|
||||
label: 'Paramètres',
|
||||
icon: '⚙️',
|
||||
render: () => null
|
||||
render: () => <ParamsTab project={project} dispatch={dispatch} />
|
||||
},
|
||||
{
|
||||
label: 'Exporter',
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
import { FunctionalComponent, h, Fragment } from "preact";
|
||||
import { Project } from "../../models/project";
|
||||
import TaskTable from "./tasks-table";
|
||||
import TimePreview from "./time-preview";
|
||||
import FinancialPreview from "./financial-preview";
|
||||
import { addTask, updateTaskEstimation, removeTask, updateProjectLabel, updateTaskLabel, ProjectReducerActions, updateParam } from "../../hooks/use-project-reducer";
|
||||
import { getRoundUpEstimations, getCurrency, getTimeUnit } from "../../models/params";
|
||||
import TaskCategoriesTable from "./task-categories-table";
|
||||
|
||||
export interface ParamsTabProps {
|
||||
project: Project
|
||||
dispatch: (action: ProjectReducerActions) => void
|
||||
}
|
||||
|
||||
const ParamsTab: FunctionalComponent<ParamsTabProps> = ({ project, dispatch }) => {
|
||||
const onRoundUpChange = (evt: Event) => {
|
||||
const checked = (evt.currentTarget as HTMLInputElement).checked;
|
||||
dispatch(updateParam("roundUpEstimations", checked));
|
||||
};
|
||||
|
||||
const onCurrencyChange = (evt: Event) => {
|
||||
const value = (evt.currentTarget as HTMLInputElement).value;
|
||||
dispatch(updateParam("currency", value));
|
||||
};
|
||||
|
||||
const timeUnit = getTimeUnit(project);
|
||||
|
||||
const onTimeUnitLabelChange = (evt: Event) => {
|
||||
const value = (evt.currentTarget as HTMLInputElement).value;
|
||||
dispatch(updateParam("timeUnit", { ...timeUnit, label: value }));
|
||||
};
|
||||
|
||||
const onTimeUnitAcronymChange = (evt: Event) => {
|
||||
const value = (evt.currentTarget as HTMLInputElement).value;
|
||||
dispatch(updateParam("timeUnit", { ...timeUnit, acronym: value }));
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div class="field">
|
||||
<input type="checkbox"
|
||||
id="roundUpEstimations"
|
||||
name="roundUpEstimations"
|
||||
class="switch"
|
||||
onChange={onRoundUpChange}
|
||||
checked={getRoundUpEstimations(project)} />
|
||||
<label for="roundUpEstimations">Arrondir les estimations de temps à l'entier supérieur</label>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="field">
|
||||
<label class="label">Unité de temps</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text"
|
||||
onChange={onTimeUnitLabelChange}
|
||||
value={timeUnit.label} />
|
||||
</div>
|
||||
<label class="label">Acronyme</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text"
|
||||
onChange={onTimeUnitAcronymChange}
|
||||
value={timeUnit.acronym} />
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="field">
|
||||
<label class="label">Devise</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text"
|
||||
onChange={onCurrencyChange}
|
||||
value={getCurrency(project)} />
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<TaskCategoriesTable project={project} dispatch={dispatch} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default ParamsTab;
|
|
@ -0,0 +1,70 @@
|
|||
import { FunctionalComponent, h } from "preact";
|
||||
import { Project } from "../../models/project";
|
||||
import * as style from './style.css';
|
||||
import { projectReducer, ProjectReducerActions, updateTaskCategoryCost, updateTaskCategoryLabel, removeTaskCategory } from "../../hooks/use-project-reducer";
|
||||
import EditableText from "../../components/editable-text";
|
||||
import { TaskCategoryID } from "../../models/task";
|
||||
import ProjectTimeUnit from "../../components/project-time-unit";
|
||||
import { getCurrency, getTaskCategoryCost } from "../../models/params";
|
||||
|
||||
export interface TaskCategoriesTableProps {
|
||||
project: Project
|
||||
dispatch: (action: ProjectReducerActions) => void
|
||||
}
|
||||
|
||||
const TaskCategoriesTable: FunctionalComponent<TaskCategoriesTableProps> = ({ project, dispatch }) => {
|
||||
const onTaskCategoryRemove = (categoryId: TaskCategoryID) => {
|
||||
dispatch(removeTaskCategory(categoryId));
|
||||
};
|
||||
|
||||
const onTaskCategoryLabelChange = (categoryId: TaskCategoryID, value: string) => {
|
||||
dispatch(updateTaskCategoryLabel(categoryId, value));
|
||||
};
|
||||
|
||||
const onTaskCategoryCostChange = (categoryId: TaskCategoryID, value: string) => {
|
||||
const cost = parseFloat(value);
|
||||
dispatch(updateTaskCategoryCost(categoryId, cost));
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="table-container">
|
||||
<label class="label">Catégories de tâche</label>
|
||||
<table class={`table is-bordered is-striped" ${style.middleTable}`}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class={`${style.noBorder} is-narrow`}></th>
|
||||
<th>Catégorie</th>
|
||||
<th>Coût par unité de temps</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
Object.values(project.params.taskCategories).map(tc => {
|
||||
return (
|
||||
<tr key={`task-category-${tc.id}`}>
|
||||
<td>
|
||||
<button
|
||||
onClick={onTaskCategoryRemove.bind(null, tc.id)}
|
||||
class="button is-danger is-small is-outlined">
|
||||
🗑️
|
||||
</button>
|
||||
</td>
|
||||
<td><EditableText value={tc.label}
|
||||
onChange={onTaskCategoryLabelChange.bind(null, tc.id)} />
|
||||
</td>
|
||||
<td>
|
||||
<EditableText value={`${getTaskCategoryCost(tc)}`}
|
||||
render={value=> (<span>{value} {getCurrency(project)}</span>)}
|
||||
onChange={onTaskCategoryCostChange.bind(null, tc.id)} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskCategoriesTable;
|
|
@ -85,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 (en <ProjectTimeUnit project={project} />)</th>
|
||||
<th colSpan={3}>Estimation (en <ProjectTimeUnit project={project} />)</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Optimiste</th>
|
||||
|
|
Loading…
Reference in New Issue