Better print style + base tabs
This commit is contained in:
parent
6e0ccd5575
commit
642b555b3d
|
@ -20,7 +20,7 @@ const App: FunctionalComponent = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<Header />
|
<Header class="noPrint" />
|
||||||
<Router onChange={handleRoute}>
|
<Router onChange={handleRoute}>
|
||||||
<Route path="/" component={Home} />
|
<Route path="/" component={Home} />
|
||||||
<Route path="/p/:projectId" component={Project} />
|
<Route path="/p/:projectId" component={Project} />
|
||||||
|
|
|
@ -2,9 +2,13 @@ import { FunctionalComponent, h } from "preact";
|
||||||
import { Link } from "preact-router/match";
|
import { Link } from "preact-router/match";
|
||||||
import * as style from "./style.css";
|
import * as style from "./style.css";
|
||||||
|
|
||||||
const Header: FunctionalComponent = () => {
|
export interface HeaderProps {
|
||||||
|
class?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Header: FunctionalComponent<HeaderProps> = ({ ...props}) => {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class={`container ${props.class ? props.class : ''}`}>
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<nav class="navbar" role="navigation" aria-label="main navigation">
|
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { FunctionalComponent, h, ComponentChild } from "preact";
|
||||||
|
import * as style from "./style.css";
|
||||||
|
import { useState } from "preact/hooks";
|
||||||
|
|
||||||
|
export interface TabItem {
|
||||||
|
label: string
|
||||||
|
icon?: string
|
||||||
|
render: () => ComponentChild
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TabsProps {
|
||||||
|
class?: string
|
||||||
|
items: TabItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const Tabs: FunctionalComponent<TabsProps> = ({ items, ...props }) => {
|
||||||
|
const [ selectedTabIndex, setSelectedTabIndex ] = useState(0);
|
||||||
|
|
||||||
|
const onTabClick = (tabIndex: number) => {
|
||||||
|
setSelectedTabIndex(tabIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedTab = items[selectedTabIndex];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={`${style.tabs} ${props.class}`}>
|
||||||
|
<div class="tabs">
|
||||||
|
<ul class={`noPrint`}>
|
||||||
|
{
|
||||||
|
items.map((tabItem, tabIndex) => (
|
||||||
|
<li key={`tab-${tabIndex}`}
|
||||||
|
onClick={onTabClick.bind(null, tabIndex)}
|
||||||
|
class={`${selectedTabIndex === tabIndex ? 'is-active' : ''}`}>
|
||||||
|
<a>
|
||||||
|
<span class="icon is-small">{tabItem.icon}</span>
|
||||||
|
{tabItem.label}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class={style.tabContent}>
|
||||||
|
{ selectedTab.render() }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Tabs;
|
|
@ -0,0 +1,8 @@
|
||||||
|
.tabs {
|
||||||
|
display: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabContent {
|
||||||
|
padding-top: 1em;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
// This file is automatically generated from your CSS. Any edits will be overwritten.
|
||||||
|
export const tabs: string;
|
||||||
|
export const tabContent: string;
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
|
export function useMediaQuery(query: string): boolean {
|
||||||
|
const media = window.matchMedia(query);
|
||||||
|
const [ matches, setMatches ] = useState(media.matches);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const listener = (evt: MediaQueryListEvent) => {
|
||||||
|
setMatches(evt.matches);
|
||||||
|
};
|
||||||
|
media.addListener(listener);
|
||||||
|
return () => {media.removeListener(listener)}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePrintMediaQuery(): boolean {
|
||||||
|
const isMediaQueryPrint = useMediaQuery("print");
|
||||||
|
const [ isPrint, setIsPrint ] = useState(false);
|
||||||
|
|
||||||
|
// Firefox/IE compatibility layer
|
||||||
|
useEffect(() => {
|
||||||
|
const beforePrint = () => {
|
||||||
|
setIsPrint(true);
|
||||||
|
};
|
||||||
|
const afterPrint = () => {
|
||||||
|
setIsPrint(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('beforeprint', beforePrint);
|
||||||
|
window.addEventListener('afterprint', afterPrint);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('beforeprint', beforePrint);
|
||||||
|
window.removeEventListener('afterprint', afterPrint);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return isMediaQueryPrint || isPrint;
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { Project } from "../models/project";
|
||||||
|
import { useState, useEffect } from "preact/hooks";
|
||||||
|
import { getProjectWeightedMean, getProjectStandardDeviation } from "../util/stat";
|
||||||
|
|
||||||
|
export interface Estimation {
|
||||||
|
e: number
|
||||||
|
sd: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProjetEstimations {
|
||||||
|
p99: Estimation
|
||||||
|
p90: Estimation
|
||||||
|
p68: Estimation
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useProjectEstimations(p :Project): ProjetEstimations {
|
||||||
|
const [ estimations, setEstimations ] = useState({
|
||||||
|
p99: { e: 0, sd: 0 },
|
||||||
|
p90: { e: 0, sd: 0 },
|
||||||
|
p68: { e: 0, sd: 0 },
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const projectWeightedMean = getProjectWeightedMean(p)
|
||||||
|
const projectStandardDeviation = getProjectStandardDeviation(p);
|
||||||
|
|
||||||
|
setEstimations({
|
||||||
|
p99: { e: projectWeightedMean, sd: (projectStandardDeviation * 3) },
|
||||||
|
p90: { e: projectWeightedMean, sd: (projectStandardDeviation * 1.645) },
|
||||||
|
p68: { e: projectWeightedMean, sd: (projectStandardDeviation) },
|
||||||
|
})
|
||||||
|
}, [p.tasks]);
|
||||||
|
|
||||||
|
return estimations;
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
import "./style/index.css";
|
import "./style/index.css";
|
||||||
|
|
||||||
import "bulma/css/bulma.css";
|
import "bulma/css/bulma.css";
|
||||||
|
|
||||||
import App from "./components/app.tsx";
|
import App from "./components/app.tsx";
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { FunctionalComponent, h } 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 } from "../../hooks/use-project-reducer";
|
||||||
|
import { Task, TaskID, EstimationConfidence } from "../../models/task";
|
||||||
|
|
||||||
|
export interface EstimationTabProps {
|
||||||
|
project: Project
|
||||||
|
dispatch: (action: ProjectReducerActions) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const EstimationTab: FunctionalComponent<EstimationTabProps> = ({ project, dispatch }) => {
|
||||||
|
const onTaskAdd = (task: Task) => {
|
||||||
|
dispatch(addTask(task));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTaskRemove = (taskId: TaskID) => {
|
||||||
|
dispatch(removeTask(taskId));
|
||||||
|
}
|
||||||
|
|
||||||
|
const onTaskLabelUpdate = (taskId: TaskID, label: string) => {
|
||||||
|
dispatch(updateTaskLabel(taskId, label));
|
||||||
|
}
|
||||||
|
|
||||||
|
const onEstimationChange = (taskId: TaskID, confidence: EstimationConfidence, value: number) => {
|
||||||
|
dispatch(updateTaskEstimation(taskId, confidence, value));
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-9">
|
||||||
|
<TaskTable
|
||||||
|
project={project}
|
||||||
|
onTaskAdd={onTaskAdd}
|
||||||
|
onTaskRemove={onTaskRemove}
|
||||||
|
onTaskLabelUpdate={onTaskLabelUpdate}
|
||||||
|
onEstimationChange={onEstimationChange} />
|
||||||
|
</div>
|
||||||
|
<div class="column is-3">
|
||||||
|
<TimePreview project={project} />
|
||||||
|
<FinancialPreview project={project} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EstimationTab;
|
|
@ -1,12 +1,17 @@
|
||||||
import { FunctionalComponent, h } from "preact";
|
import { FunctionalComponent, h } from "preact";
|
||||||
import * as style from "./style.css";
|
import * as style from "./style.css";
|
||||||
import { Project } from "../../models/project";
|
import { Project } from "../../models/project";
|
||||||
|
import { useProjectEstimations } from "../../hooks/use-project-estimations";
|
||||||
|
|
||||||
export interface FinancialPreviewProps {
|
export interface FinancialPreviewProps {
|
||||||
project: Project
|
project: Project
|
||||||
}
|
}
|
||||||
|
|
||||||
const FinancialPreview: FunctionalComponent<FinancialPreviewProps> = ({ project }) => {
|
const FinancialPreview: FunctionalComponent<FinancialPreviewProps> = ({ project }) => {
|
||||||
|
const estimations = useProjectEstimations(project);
|
||||||
|
const costPerTimeUnit = 500;
|
||||||
|
const maxCost = Math.ceil((estimations.p99.e + estimations.p99.sd) * costPerTimeUnit);
|
||||||
|
const minCost = Math.ceil((estimations.p99.e - estimations.p99.sd) * costPerTimeUnit);
|
||||||
return (
|
return (
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table class="table is-bordered is-striped is-fullwidth">
|
<table class="table is-bordered is-striped is-fullwidth">
|
||||||
|
@ -15,18 +20,18 @@ const FinancialPreview: FunctionalComponent<FinancialPreviewProps> = ({ project
|
||||||
<th colSpan={2}>Prévisionnel financier</th>
|
<th colSpan={2}>Prévisionnel financier</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Temps</th>
|
<th class="is-narrow">Temps</th>
|
||||||
<th>Coût</th>
|
<th>Coût</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Maximum</td>
|
<td class="is-narrow">Maximum</td>
|
||||||
<td></td>
|
<td>~ {maxCost} €</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Minimum</td>
|
<td class="is-narrow">Minimum</td>
|
||||||
<td></td>
|
<td>~ {minCost} €</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -2,14 +2,12 @@ import { FunctionalComponent, h } from "preact";
|
||||||
import { useEffect } from "preact/hooks";
|
import { useEffect } from "preact/hooks";
|
||||||
import * as style from "./style.css";
|
import * as style from "./style.css";
|
||||||
import { newProject } from "../../models/project";
|
import { newProject } from "../../models/project";
|
||||||
import TaskTable from "./tasks-table";
|
import { useProjectReducer, updateProjectLabel } from "../../hooks/use-project-reducer";
|
||||||
import TimePreview from "./time-preview";
|
|
||||||
import FinancialPreview from "./financial-preview";
|
|
||||||
import { useProjectReducer, addTask, updateTaskEstimation, removeTask, updateProjectLabel, updateTaskLabel } from "../../hooks/use-project-reducer";
|
|
||||||
import { Task, TaskID, EstimationConfidence } from "../../models/task";
|
|
||||||
import { getProjectStorageKey } from "../../util/storage";
|
import { getProjectStorageKey } from "../../util/storage";
|
||||||
import { useLocalStorage } from "../../hooks/use-local-storage";
|
import { useLocalStorage } from "../../hooks/use-local-storage";
|
||||||
import EditableText from "../../components/editable-text";
|
import EditableText from "../../components/editable-text";
|
||||||
|
import Tabs from "../../components/tabs";
|
||||||
|
import EstimationTab from "./estimation-tab";
|
||||||
|
|
||||||
export interface ProjectProps {
|
export interface ProjectProps {
|
||||||
projectId: string
|
projectId: string
|
||||||
|
@ -20,22 +18,6 @@ const Project: FunctionalComponent<ProjectProps> = ({ projectId }) => {
|
||||||
const [ storedProject, storeProject ] = useLocalStorage(projectStorageKey, newProject(projectId));
|
const [ storedProject, storeProject ] = useLocalStorage(projectStorageKey, newProject(projectId));
|
||||||
const [ project, dispatch ] = useProjectReducer(storedProject);
|
const [ project, dispatch ] = useProjectReducer(storedProject);
|
||||||
|
|
||||||
const onTaskAdd = (task: Task) => {
|
|
||||||
dispatch(addTask(task));
|
|
||||||
};
|
|
||||||
|
|
||||||
const onTaskRemove = (taskId: TaskID) => {
|
|
||||||
dispatch(removeTask(taskId));
|
|
||||||
}
|
|
||||||
|
|
||||||
const onTaskLabelUpdate = (taskId: TaskID, label: string) => {
|
|
||||||
dispatch(updateTaskLabel(taskId, label));
|
|
||||||
}
|
|
||||||
|
|
||||||
const onEstimationChange = (taskId: TaskID, confidence: EstimationConfidence, value: number) => {
|
|
||||||
dispatch(updateTaskEstimation(taskId, confidence, value));
|
|
||||||
};
|
|
||||||
|
|
||||||
const onProjectLabelChange = (projectLabel: string) => {
|
const onProjectLabelChange = (projectLabel: string) => {
|
||||||
dispatch(updateProjectLabel(projectLabel));
|
dispatch(updateProjectLabel(projectLabel));
|
||||||
};
|
};
|
||||||
|
@ -49,38 +31,28 @@ const Project: FunctionalComponent<ProjectProps> = ({ projectId }) => {
|
||||||
<div class={`container ${style.estimation}`}>
|
<div class={`container ${style.estimation}`}>
|
||||||
<EditableText
|
<EditableText
|
||||||
editIconClass="is-size-4"
|
editIconClass="is-size-4"
|
||||||
render={(value) => (<h3 class="is-size-3">{value}</h3>)}
|
render={(value) => (<h2 class="is-size-3">{value}</h2>)}
|
||||||
onChange={onProjectLabelChange}
|
onChange={onProjectLabelChange}
|
||||||
value={project.label ? project.label : "Projet sans nom"} />
|
value={project.label ? project.label : "Projet sans nom"} />
|
||||||
<div class="tabs">
|
<div class={style.tabContainer}>
|
||||||
<ul>
|
<Tabs items={[
|
||||||
<li class="is-active">
|
{
|
||||||
<a>
|
label: 'Estimation',
|
||||||
<span class="icon is-small">📋</span>
|
icon: '📋',
|
||||||
Estimation
|
render: () => <EstimationTab project={project} dispatch={dispatch} />
|
||||||
</a>
|
},
|
||||||
</li>
|
{
|
||||||
{/* <li>
|
label: 'Paramètres',
|
||||||
<a disabled>
|
icon: '⚙️',
|
||||||
<span class="icon is-small">⚙️</span>
|
render: () => null
|
||||||
Paramètres
|
},
|
||||||
</a>
|
{
|
||||||
</li> */}
|
label: 'Exporter',
|
||||||
</ul>
|
icon: '↗️',
|
||||||
</div>
|
render: () => null
|
||||||
<div class="columns">
|
}
|
||||||
<div class="column is-9">
|
]}
|
||||||
<TaskTable
|
/>
|
||||||
project={project}
|
|
||||||
onTaskAdd={onTaskAdd}
|
|
||||||
onTaskRemove={onTaskRemove}
|
|
||||||
onTaskLabelUpdate={onTaskLabelUpdate}
|
|
||||||
onEstimationChange={onEstimationChange} />
|
|
||||||
</div>
|
|
||||||
<div class="column is-3">
|
|
||||||
<TimePreview project={project} />
|
|
||||||
<FinancialPreview project={project} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,3 +14,11 @@
|
||||||
.mainColumn {
|
.mainColumn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.middleTable td {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabContainer {
|
||||||
|
padding-top: 1em;
|
||||||
|
}
|
|
@ -3,3 +3,5 @@ export const estimation: string;
|
||||||
export const noTasks: string;
|
export const noTasks: string;
|
||||||
export const noBorder: string;
|
export const noBorder: string;
|
||||||
export const mainColumn: string;
|
export const mainColumn: string;
|
||||||
|
export const middleTable: string;
|
||||||
|
export const tabContainer: string;
|
||||||
|
|
|
@ -4,8 +4,7 @@ import * as style from "./style.css";
|
||||||
import { Project } from "../../models/project";
|
import { Project } from "../../models/project";
|
||||||
import { newTask, Task, TaskID, EstimationConfidence } from "../../models/task";
|
import { newTask, Task, TaskID, EstimationConfidence } from "../../models/task";
|
||||||
import EditableText from "../../components/editable-text";
|
import EditableText from "../../components/editable-text";
|
||||||
|
import { usePrintMediaQuery } from "../../hooks/use-media-query";
|
||||||
|
|
||||||
|
|
||||||
export interface TaskTableProps {
|
export interface TaskTableProps {
|
||||||
project: Project
|
project: Project
|
||||||
|
@ -27,6 +26,8 @@ const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, on
|
||||||
[EstimationConfidence.Pessimistic]: 0,
|
[EstimationConfidence.Pessimistic]: 0,
|
||||||
} as EstimationTotals);
|
} as EstimationTotals);
|
||||||
|
|
||||||
|
const isPrint = usePrintMediaQuery();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let optimistic = 0;
|
let optimistic = 0;
|
||||||
let likely = 0;
|
let likely = 0;
|
||||||
|
@ -76,10 +77,10 @@ const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, on
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table class="table is-bordered is-striped is-hoverable is-fullwidth">
|
<table class={`table is-bordered is-striped is-hoverable is-fullwidth ${style.middleTable}`}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class={style.noBorder} rowSpan={2}></th>
|
<th class={`${style.noBorder} noPrint`} rowSpan={2}></th>
|
||||||
<th class={style.mainColumn} rowSpan={2}>Tâche</th>
|
<th class={style.mainColumn} rowSpan={2}>Tâche</th>
|
||||||
<th rowSpan={2}>Catégorie</th>
|
<th rowSpan={2}>Catégorie</th>
|
||||||
<th colSpan={3}>Estimation</th>
|
<th colSpan={3}>Estimation</th>
|
||||||
|
@ -97,7 +98,7 @@ const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, on
|
||||||
const categoryLabel = category ? category.label : '???';
|
const categoryLabel = category ? category.label : '???';
|
||||||
return (
|
return (
|
||||||
<tr key={`taks-${t.id}`}>
|
<tr key={`taks-${t.id}`}>
|
||||||
<td class="is-narrow">
|
<td class={`is-narrow noPrint`}>
|
||||||
<button
|
<button
|
||||||
onClick={onTaskRemoveClick.bind(null, t.id)}
|
onClick={onTaskRemoveClick.bind(null, t.id)}
|
||||||
class="button is-danger is-small is-outlined">
|
class="button is-danger is-small is-outlined">
|
||||||
|
@ -112,19 +113,31 @@ const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, on
|
||||||
</td>
|
</td>
|
||||||
<td>{ categoryLabel }</td>
|
<td>{ categoryLabel }</td>
|
||||||
<td>
|
<td>
|
||||||
|
{
|
||||||
|
isPrint ?
|
||||||
|
<span>{t.estimations.optimistic}</span> :
|
||||||
<input class="input" type="number" value={t.estimations.optimistic}
|
<input class="input" type="number" value={t.estimations.optimistic}
|
||||||
min={0}
|
min={0}
|
||||||
onChange={onOptimisticChange.bind(null, t.id)} />
|
onChange={onOptimisticChange.bind(null, t.id)} />
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
{
|
||||||
|
isPrint ?
|
||||||
|
<span>{t.estimations.likely}</span> :
|
||||||
<input class="input" type="number" value={t.estimations.likely}
|
<input class="input" type="number" value={t.estimations.likely}
|
||||||
min={0}
|
min={0}
|
||||||
onChange={onLikelyChange.bind(null, t.id)} />
|
onChange={onLikelyChange.bind(null, t.id)} />
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
{
|
||||||
|
isPrint ?
|
||||||
|
<span>{t.estimations.pessimistic}</span> :
|
||||||
<input class="input" type="number" value={t.estimations.pessimistic}
|
<input class="input" type="number" value={t.estimations.pessimistic}
|
||||||
min={0}
|
min={0}
|
||||||
onChange={onPessimisticChange.bind(null, t.id)} />
|
onChange={onPessimisticChange.bind(null, t.id)} />
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
|
@ -133,7 +146,7 @@ const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, on
|
||||||
{
|
{
|
||||||
Object.keys(project.tasks).length === 0 ?
|
Object.keys(project.tasks).length === 0 ?
|
||||||
<tr>
|
<tr>
|
||||||
<td class={style.noBorder}></td>
|
<td class={`${style.noBorder} noPrint`}></td>
|
||||||
<td class={style.noTasks} colSpan={5}>Aucune tâche pour l'instant.</td>
|
<td class={style.noTasks} colSpan={5}>Aucune tâche pour l'instant.</td>
|
||||||
</tr> :
|
</tr> :
|
||||||
null
|
null
|
||||||
|
@ -141,9 +154,9 @@ const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, on
|
||||||
</tbody>
|
</tbody>
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr>
|
<tr>
|
||||||
<td class={style.noBorder}></td>
|
<td class={`${style.noBorder} noPrint`}></td>
|
||||||
<td colSpan={2}>
|
<td colSpan={2} class={isPrint ? style.noBorder : ''}>
|
||||||
<div class="field has-addons">
|
<div class="field has-addons noPrint">
|
||||||
<p class="control is-expanded">
|
<p class="control is-expanded">
|
||||||
<input class="input" type="text" placeholder="Nouvelle tâche"
|
<input class="input" type="text" placeholder="Nouvelle tâche"
|
||||||
value={task.label} onChange={onNewTaskLabelChange} />
|
value={task.label} onChange={onNewTaskLabelChange} />
|
||||||
|
@ -171,7 +184,7 @@ const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, on
|
||||||
<th colSpan={3}>Total</th>
|
<th colSpan={3}>Total</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={3} class={style.noBorder}></td>
|
<td colSpan={isPrint ? 2 : 3} class={style.noBorder}></td>
|
||||||
<td>{totals.optimistic}</td>
|
<td>{totals.optimistic}</td>
|
||||||
<td>{totals.likely}</td>
|
<td>{totals.likely}</td>
|
||||||
<td>{totals.pessimistic}</td>
|
<td>{totals.pessimistic}</td>
|
||||||
|
|
|
@ -1,29 +1,13 @@
|
||||||
import { FunctionalComponent, h } from "preact";
|
import { FunctionalComponent, h } from "preact";
|
||||||
import { Project } from "../../models/project";
|
import { Project } from "../../models/project";
|
||||||
import { Task } from "../../models/task";
|
import { useProjectEstimations } from "../../hooks/use-project-estimations";
|
||||||
import { useState, useEffect } from "preact/hooks";
|
|
||||||
import { getProjectWeightedMean, getProjectStandardDeviation } from "../../util/stat";
|
|
||||||
|
|
||||||
export interface TimePreviewProps {
|
export interface TimePreviewProps {
|
||||||
project: Project
|
project: Project
|
||||||
}
|
}
|
||||||
|
|
||||||
const TimePreview: FunctionalComponent<TimePreviewProps> = ({ project }) => {
|
const TimePreview: FunctionalComponent<TimePreviewProps> = ({ project }) => {
|
||||||
const [ estimations, setEstimations ] = useState({
|
const estimations = useProjectEstimations(project);
|
||||||
p99: { e: '0', sd: '0' },
|
|
||||||
p90: { e: '0', sd: '0' },
|
|
||||||
p68: { e: '0', sd: '0' },
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const projectWeightedMean = getProjectWeightedMean(project).toFixed(2);
|
|
||||||
const projectStandardDeviation = getProjectStandardDeviation(project);
|
|
||||||
setEstimations({
|
|
||||||
p99: { e: projectWeightedMean, sd: (projectStandardDeviation * 3).toFixed(2) },
|
|
||||||
p90: { e: projectWeightedMean, sd: (projectStandardDeviation * 1.645).toFixed(2) },
|
|
||||||
p68: { e: projectWeightedMean, sd: (projectStandardDeviation).toFixed(2) },
|
|
||||||
})
|
|
||||||
}, [project.tasks]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
|
@ -33,25 +17,25 @@ const TimePreview: FunctionalComponent<TimePreviewProps> = ({ project }) => {
|
||||||
<th colSpan={2}>Prévisionnel temps</th>
|
<th colSpan={2}>Prévisionnel temps</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Niveau de confiance</th>
|
<th class="is-narrow">Confiance</th>
|
||||||
<th>Estimation</th>
|
<th>Estimation</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>>= 99.7%</td>
|
<td class="is-narrow">>= 99.7%</td>
|
||||||
<td>{`${estimations.p99.e} ± ${estimations.p99.sd} j/h`}</td>
|
<td>{`${estimations.p99.e.toPrecision(2)} ± ${estimations.p99.sd.toPrecision(2)} j/h`}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>>= 90%</td>
|
<td class="is-narrow">>= 90%</td>
|
||||||
<td>{`${estimations.p90.e} ± ${estimations.p90.sd} j/h`}</td>
|
<td>{`${estimations.p90.e.toPrecision(2)} ± ${estimations.p90.sd.toPrecision(2)} j/h`}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>>= 68%</td>
|
<td class="is-narrow">>= 68%</td>
|
||||||
<td>{`${estimations.p68.e} ± ${estimations.p68.sd} j/h`}</td>
|
<td>{`${estimations.p68.e.toPrecision(2)} ± ${estimations.p68.sd.toPrecision(2)} j/h`}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
<tfoot>
|
<tfoot class="noPrint">
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={2}>
|
<td colSpan={2}>
|
||||||
<a class="is-small is-pulled-right" href="https://en.wikipedia.org/wiki/Three-point_estimation" target="_blank">❓ Estimation à 3 points</a>
|
<a class="is-small is-pulled-right" href="https://en.wikipedia.org/wiki/Three-point_estimation" target="_blank">❓ Estimation à 3 points</a>
|
||||||
|
|
|
@ -1,3 +1,10 @@
|
||||||
#app {
|
#app {
|
||||||
display: inherit;
|
display: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media print
|
||||||
|
{
|
||||||
|
.noPrint, .noPrint * {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,2 +1 @@
|
||||||
// This file is automatically generated from your CSS. Any edits will be overwritten.
|
|
||||||
export const app: string;
|
export const app: string;
|
Loading…
Reference in New Issue