175 lines
7.6 KiB
TypeScript
175 lines
7.6 KiB
TypeScript
|
import { FunctionalComponent, h } from "preact";
|
|||
|
import { useState, useEffect } from "preact/hooks";
|
|||
|
import * as style from "./style.css";
|
|||
|
import { Project } from "../../models/project";
|
|||
|
import { newTask, Task, TaskID, EstimationConfidence } from "../../models/task";
|
|||
|
|
|||
|
|
|||
|
|
|||
|
export interface TaskTableProps {
|
|||
|
project: Project
|
|||
|
onTaskAdd: (task: Task) => void
|
|||
|
onTaskRemove: (taskId: TaskID) => void
|
|||
|
onEstimationChange: (taskId: TaskID, confidence: EstimationConfidence, value: number) => void
|
|||
|
}
|
|||
|
|
|||
|
export type EstimationTotals = { [confidence in EstimationConfidence]: number }
|
|||
|
|
|||
|
const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, onEstimationChange, onTaskRemove }) => {
|
|||
|
|
|||
|
const defaultTaskCategory = Object.keys(project.params.taskCategories)[0];
|
|||
|
const [ task, setTask ] = useState(newTask("", defaultTaskCategory));
|
|||
|
const [ totals, setTotals ] = useState({
|
|||
|
[EstimationConfidence.Optimistic]: 0,
|
|||
|
[EstimationConfidence.Likely]: 0,
|
|||
|
[EstimationConfidence.Pessimistic]: 0,
|
|||
|
} as EstimationTotals);
|
|||
|
|
|||
|
useEffect(() => {
|
|||
|
let optimistic = 0;
|
|||
|
let likely = 0;
|
|||
|
let pessimistic = 0;
|
|||
|
|
|||
|
Object.values(project.tasks).forEach(t => {
|
|||
|
optimistic += t.estimations.optimistic;
|
|||
|
likely += t.estimations.likely;
|
|||
|
pessimistic += t.estimations.pessimistic;
|
|||
|
});
|
|||
|
|
|||
|
setTotals({ optimistic, likely, pessimistic });
|
|||
|
}, [project.tasks]);
|
|||
|
|
|||
|
const onTaskLabelChange = (evt: Event) => {
|
|||
|
const value = (evt.currentTarget as HTMLInputElement).value;
|
|||
|
setTask({...task, label: value});
|
|||
|
};
|
|||
|
|
|||
|
const onTaskCategoryChange = (evt: Event) => {
|
|||
|
const value = (evt.currentTarget as HTMLInputElement).value;
|
|||
|
setTask({...task, category: value});
|
|||
|
};
|
|||
|
|
|||
|
const onAddTaskClick = (evt: Event) => {
|
|||
|
onTaskAdd(task);
|
|||
|
setTask(newTask("", defaultTaskCategory));
|
|||
|
};
|
|||
|
|
|||
|
const onTaskRemoveClick = (taskId: TaskID, evt: Event) => {
|
|||
|
onTaskRemove(taskId);
|
|||
|
};
|
|||
|
|
|||
|
const withEstimationChange = (confidence: EstimationConfidence, taskID: TaskID, evt: Event) => {
|
|||
|
const textValue = (evt.currentTarget as HTMLInputElement).value;
|
|||
|
const value = parseFloat(textValue);
|
|||
|
onEstimationChange(taskID, confidence, value);
|
|||
|
};
|
|||
|
|
|||
|
const onOptimisticChange = withEstimationChange.bind(null, EstimationConfidence.Optimistic);
|
|||
|
const onLikelyChange = withEstimationChange.bind(null, EstimationConfidence.Likely);
|
|||
|
const onPessimisticChange = withEstimationChange.bind(null, EstimationConfidence.Pessimistic);
|
|||
|
|
|||
|
return (
|
|||
|
<div class="table-container">
|
|||
|
<table class="table is-bordered is-striped is-hoverable is-fullwidth">
|
|||
|
<thead>
|
|||
|
<tr>
|
|||
|
<th class={style.noBorder} rowSpan={2}></th>
|
|||
|
<th class={style.mainColumn} rowSpan={2}>Tâche</th>
|
|||
|
<th rowSpan={2}>Catégorie</th>
|
|||
|
<th colSpan={3}>Estimation</th>
|
|||
|
</tr>
|
|||
|
<tr>
|
|||
|
<th>Optimiste</th>
|
|||
|
<th>Probable</th>
|
|||
|
<th>Pessimiste</th>
|
|||
|
</tr>
|
|||
|
</thead>
|
|||
|
<tbody>
|
|||
|
{
|
|||
|
Object.values(project.tasks).map(t => {
|
|||
|
const category = project.params.taskCategories[t.category];
|
|||
|
const categoryLabel = category ? category.label : '???';
|
|||
|
return (
|
|||
|
<tr key={`taks-${t.id}`}>
|
|||
|
<td class="is-narrow">
|
|||
|
<button
|
|||
|
onClick={onTaskRemoveClick.bind(null, t.id)}
|
|||
|
class="button is-danger is-small is-outlined">
|
|||
|
🗑️
|
|||
|
</button>
|
|||
|
</td>
|
|||
|
<td class={style.mainColumn}>{t.label}</td>
|
|||
|
<td>{ categoryLabel }</td>
|
|||
|
<td>
|
|||
|
<input class="input" type="number" value={t.estimations.optimistic}
|
|||
|
min={0}
|
|||
|
onChange={onOptimisticChange.bind(null, t.id)} />
|
|||
|
</td>
|
|||
|
<td>
|
|||
|
<input class="input" type="number" value={t.estimations.likely}
|
|||
|
min={0}
|
|||
|
onChange={onLikelyChange.bind(null, t.id)} />
|
|||
|
</td>
|
|||
|
<td>
|
|||
|
<input class="input" type="number" value={t.estimations.pessimistic}
|
|||
|
min={0}
|
|||
|
onChange={onPessimisticChange.bind(null, t.id)} />
|
|||
|
</td>
|
|||
|
</tr>
|
|||
|
)
|
|||
|
})
|
|||
|
}
|
|||
|
{
|
|||
|
Object.keys(project.tasks).length === 0 ?
|
|||
|
<tr>
|
|||
|
<td class={style.noBorder}></td>
|
|||
|
<td class={style.noTasks} colSpan={5}>Aucune tâche pour l'instant.</td>
|
|||
|
</tr> :
|
|||
|
null
|
|||
|
}
|
|||
|
</tbody>
|
|||
|
<tfoot>
|
|||
|
<tr>
|
|||
|
<td class={style.noBorder}></td>
|
|||
|
<td colSpan={2}>
|
|||
|
<div class="field has-addons">
|
|||
|
<p class="control is-expanded">
|
|||
|
<input class="input" type="text" placeholder="Nouvelle tâche"
|
|||
|
value={task.label} onChange={onTaskLabelChange} />
|
|||
|
</p>
|
|||
|
<p class="control">
|
|||
|
<span class="select">
|
|||
|
<select onChange={onTaskCategoryChange} value={task.category}>
|
|||
|
{
|
|||
|
Object.values(project.params.taskCategories).map(tc => {
|
|||
|
return (
|
|||
|
<option key={`task-category-${tc.id}`} value={tc.id}>{tc.label}</option>
|
|||
|
);
|
|||
|
})
|
|||
|
}
|
|||
|
</select>
|
|||
|
</span>
|
|||
|
</p>
|
|||
|
<p class="control">
|
|||
|
<a class="button is-primary" onClick={onAddTaskClick}>
|
|||
|
Ajouter
|
|||
|
</a>
|
|||
|
</p>
|
|||
|
</div>
|
|||
|
</td>
|
|||
|
<th colSpan={3}>Total</th>
|
|||
|
</tr>
|
|||
|
<tr>
|
|||
|
<td colSpan={3} class={style.noBorder}></td>
|
|||
|
<td>{totals.optimistic}</td>
|
|||
|
<td>{totals.likely}</td>
|
|||
|
<td>{totals.pessimistic}</td>
|
|||
|
</tr>
|
|||
|
</tfoot>
|
|||
|
</table>
|
|||
|
</div>
|
|||
|
);
|
|||
|
};
|
|||
|
|
|||
|
export default TaskTable;
|