diff --git a/package-lock.json b/package-lock.json index b9e5be8..e90e15a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 4e47b60..1761179 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/editable-text/index.tsx b/src/components/editable-text/index.tsx index 010e345..82dadab 100644 --- a/src/components/editable-text/index.tsx +++ b/src/components/editable-text/index.tsx @@ -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 = ({ onChange, value, render, ...props }) => { @@ -45,7 +45,7 @@ const EditableText: FunctionalComponent = ({ onChange, value, : - { render(internalValue) } + { render ? render(internalValue) : {internalValue} } 🖋️ } diff --git a/src/components/editable-text/style.css b/src/components/editable-text/style.css index 6349e65..11cb78c 100644 --- a/src/components/editable-text/style.css +++ b/src/components/editable-text/style.css @@ -1,5 +1,5 @@ .editableText { - display: inherit; + display: inline-block; } .editableText > * { diff --git a/src/components/estimation-range.tsx b/src/components/estimation-range.tsx index 671cfe0..efe9a42 100644 --- a/src/components/estimation-range.tsx +++ b/src/components/estimation-range.tsx @@ -9,13 +9,17 @@ export interface EstimationRangeProps { export const EstimationRange: FunctionalComponent = ({ 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 ( - {`${e} ± ${sd}`}  + {`${e} ± ${sd}`}  ); diff --git a/src/hooks/use-project-reducer.ts b/src/hooks/use-project-reducer.ts index be89e1e..2756357 100644 --- a/src/hooks/use-project-reducer.ts +++ b/src/hooks/use-project-reducer.ts @@ -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 + } + }; } \ No newline at end of file diff --git a/src/index.js b/src/index.js index f6dae11..10781ad 100644 --- a/src/index.js +++ b/src/index.js @@ -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"; diff --git a/src/models/params.ts b/src/models/params.ts index bfd01da..f754757 100644 --- a/src/models/params.ts +++ b/src/models/params.ts @@ -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; } \ No newline at end of file diff --git a/src/models/project.ts b/src/models/project.ts index 0fe4878..dbaef7c 100644 --- a/src/models/project.ts +++ b/src/models/project.ts @@ -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, }, }; } \ No newline at end of file diff --git a/src/models/task.ts b/src/models/task.ts index 3e60b1f..363ade2 100644 --- a/src/models/task.ts +++ b/src/models/task.ts @@ -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, diff --git a/src/routes/project/estimation-tab.tsx b/src/routes/project/estimation-tab.tsx index bf061be..46b16c8 100644 --- a/src/routes/project/estimation-tab.tsx +++ b/src/routes/project/estimation-tab.tsx @@ -49,7 +49,7 @@ const EstimationTab: FunctionalComponent = ({ project, dispa

⚠️ Attention

-

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.

+

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.

: null diff --git a/src/routes/project/index.tsx b/src/routes/project/index.tsx index 3462284..10ee68a 100644 --- a/src/routes/project/index.tsx +++ b/src/routes/project/index.tsx @@ -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 = ({ projectId }) => { { label: 'Paramètres', icon: '⚙️', - render: () => null + render: () => }, { label: 'Exporter', diff --git a/src/routes/project/params-tab.tsx b/src/routes/project/params-tab.tsx new file mode 100644 index 0000000..2f4dd4b --- /dev/null +++ b/src/routes/project/params-tab.tsx @@ -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 = ({ 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 ( + +
+ + +
+
+
+ +
+ +
+ +
+ +
+
+
+
+ +
+ +
+
+
+ +
+ ); +}; + +export default ParamsTab; diff --git a/src/routes/project/task-categories-table.tsx b/src/routes/project/task-categories-table.tsx new file mode 100644 index 0000000..89f4935 --- /dev/null +++ b/src/routes/project/task-categories-table.tsx @@ -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 = ({ 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 ( +
+ + + + + + + + + + + { + Object.values(project.params.taskCategories).map(tc => { + return ( + + + + + + ); + }) + } + +
CatégorieCoût par unité de temps
+ + + + ({value} {getCurrency(project)})} + onChange={onTaskCategoryCostChange.bind(null, tc.id)} /> +
+
+ ); +}; + +export default TaskCategoriesTable; diff --git a/src/routes/project/tasks-table.tsx b/src/routes/project/tasks-table.tsx index 77bc374..8de4261 100644 --- a/src/routes/project/tasks-table.tsx +++ b/src/routes/project/tasks-table.tsx @@ -85,7 +85,7 @@ const TaskTable: FunctionalComponent = ({ project, onTaskAdd, on Tâche Catégorie - Estimation (en ) + Estimation (en ) Optimiste