204 lines
6.9 KiB
TypeScript
204 lines
6.9 KiB
TypeScript
import React, { FunctionComponent, useState, useEffect, ChangeEvent, MouseEvent } from "react";
|
||
import style from "./style.module.css";
|
||
import { Project } from "../../types/project";
|
||
import { newTask, Task, TaskID, EstimationConfidence } from "../../types/task";
|
||
import EditableText from "../EditableText/EditableText";
|
||
import { usePrintMediaQuery } from "../../hooks/useMediaQuery";
|
||
import ProjectTimeUnit from "../ProjectTimeUnit";
|
||
|
||
export interface TaskTableProps {
|
||
project: Project
|
||
onTaskAdd: (task: Task) => void
|
||
onTaskRemove: (taskId: number) => void
|
||
onEstimationChange: (taskId: number, confidence: EstimationConfidence, value: number) => void
|
||
onTaskLabelUpdate: (taskId: number, label: string) => void
|
||
}
|
||
|
||
export type EstimationTotals = { [confidence in EstimationConfidence]: number }
|
||
|
||
const TaskTable: FunctionComponent<TaskTableProps> = ({ project, onTaskAdd, onEstimationChange, onTaskRemove, onTaskLabelUpdate }) => {
|
||
const [ task, setTask ] = useState(newTask("", null));
|
||
const [ totals, setTotals ] = useState({
|
||
[EstimationConfidence.Optimistic]: 0,
|
||
[EstimationConfidence.Likely]: 0,
|
||
[EstimationConfidence.Pessimistic]: 0,
|
||
} as EstimationTotals);
|
||
|
||
useEffect(() => {
|
||
if (project.taskCategories.length === 0) return;
|
||
setTask({...task, category: project.taskCategories[0]});
|
||
}, [project.taskCategories]);
|
||
|
||
const isPrint = usePrintMediaQuery();
|
||
|
||
useEffect(() => {
|
||
let optimistic = 0;
|
||
let likely = 0;
|
||
let pessimistic = 0;
|
||
|
||
project.tasks.forEach(t => {
|
||
optimistic += t.estimations.optimistic;
|
||
likely += t.estimations.likely;
|
||
pessimistic += t.estimations.pessimistic;
|
||
});
|
||
|
||
setTotals({ optimistic, likely, pessimistic });
|
||
}, [project.tasks]);
|
||
|
||
const onNewTaskLabelChange = (evt: ChangeEvent) => {
|
||
const value = (evt.currentTarget as HTMLInputElement).value;
|
||
setTask({...task, label: value});
|
||
};
|
||
|
||
const onNewTaskCategoryChange = (evt: ChangeEvent) => {
|
||
const value = (evt.currentTarget as HTMLInputElement).value;
|
||
const taskCategoryId = parseInt(value);
|
||
const category = project.taskCategories.find(tc => tc.id === taskCategoryId);
|
||
setTask({...task, category });
|
||
};
|
||
|
||
const onTaskLabelChange = (taskId: number, value: string) => {
|
||
onTaskLabelUpdate(taskId, value);
|
||
};
|
||
|
||
const onAddTaskClick = (evt: MouseEvent) => {
|
||
onTaskAdd(task);
|
||
setTask(newTask("", project.taskCategories[0]));
|
||
};
|
||
|
||
const onTaskRemoveClick = (taskId: number, evt: MouseEvent) => {
|
||
onTaskRemove(taskId);
|
||
};
|
||
|
||
const withEstimationChange = (confidence: EstimationConfidence, taskId: number, evt: ChangeEvent) => {
|
||
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 className="table-container">
|
||
<table className={`table is-bordered is-striped is-hoverable is-fullwidth ${style.middleTable}`}>
|
||
<thead>
|
||
<tr>
|
||
<th className={`${style.noBorder} noPrint`} rowSpan={2}></th>
|
||
<th className={style.mainColumn} rowSpan={2}>Tâche</th>
|
||
<th rowSpan={2}>Catégorie</th>
|
||
<th colSpan={3}>Estimation (en <ProjectTimeUnit project={project} />)</th>
|
||
</tr>
|
||
<tr>
|
||
<th>Optimiste</th>
|
||
<th>Probable</th>
|
||
<th>Pessimiste</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{
|
||
project.tasks.map((t,i) => {
|
||
const category = project.taskCategories.find(tc => tc.id === t.category.id);
|
||
const categoryLabel = category ? category.label : '???';
|
||
return (
|
||
<tr key={`tasks-${t.id}-${i}`}>
|
||
<td className={`is-narrow noPrint`}>
|
||
<button
|
||
onClick={onTaskRemoveClick.bind(null, t.id)}
|
||
className="button is-danger is-small is-outlined">
|
||
🗑️
|
||
</button>
|
||
</td>
|
||
<td className={style.mainColumn}>
|
||
<EditableText
|
||
render={(value) => (<span>{value}</span>)}
|
||
onChange={onTaskLabelChange.bind(null, t.id)}
|
||
value={t.label} />
|
||
</td>
|
||
<td>{ categoryLabel }</td>
|
||
<td>
|
||
{
|
||
isPrint ?
|
||
<span>{t.estimations.optimistic}</span> :
|
||
<input className="input" type="number" value={t.estimations.optimistic}
|
||
min={0}
|
||
onChange={onOptimisticChange.bind(null, t.id)} />
|
||
}
|
||
</td>
|
||
<td>
|
||
{
|
||
isPrint ?
|
||
<span>{t.estimations.likely}</span> :
|
||
<input className="input" type="number" value={t.estimations.likely}
|
||
min={0}
|
||
onChange={onLikelyChange.bind(null, t.id)} />
|
||
}
|
||
</td>
|
||
<td>
|
||
{
|
||
isPrint ?
|
||
<span>{t.estimations.pessimistic}</span> :
|
||
<input className="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 className={`${style.noBorder} noPrint`}></td>
|
||
<td className={style.noTasks} colSpan={5}>Aucune tâche pour l'instant.</td>
|
||
</tr> :
|
||
null
|
||
}
|
||
</tbody>
|
||
<tfoot>
|
||
<tr>
|
||
<td className={`${style.noBorder} noPrint`}></td>
|
||
<td colSpan={2} className={isPrint ? style.noBorder : ''}>
|
||
<div className="field has-addons noPrint">
|
||
<p className="control is-expanded">
|
||
<input className="input" type="text" placeholder="Nouvelle tâche"
|
||
value={task.label} onChange={onNewTaskLabelChange} />
|
||
</p>
|
||
<p className="control">
|
||
<span className="select">
|
||
<select onChange={onNewTaskCategoryChange} value={task.category ? task.category.id : -1}>
|
||
{
|
||
Object.values(project.taskCategories).map(tc => {
|
||
return (
|
||
<option key={`task-category-${tc.id}`} value={tc.id}>{tc.label}</option>
|
||
);
|
||
})
|
||
}
|
||
</select>
|
||
</span>
|
||
</p>
|
||
<p className="control">
|
||
<a className="button is-primary" onClick={onAddTaskClick}>
|
||
Ajouter
|
||
</a>
|
||
</p>
|
||
</div>
|
||
</td>
|
||
<th colSpan={3}>Total</th>
|
||
</tr>
|
||
<tr>
|
||
<td colSpan={isPrint ? 2 : 3} className={style.noBorder}></td>
|
||
<td>{totals.optimistic} <ProjectTimeUnit project={project} /></td>
|
||
<td>{totals.likely} <ProjectTimeUnit project={project} /></td>
|
||
<td>{totals.pessimistic} <ProjectTimeUnit project={project} /></td>
|
||
</tr>
|
||
</tfoot>
|
||
</table>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default TaskTable;
|