Initial commit

This commit is contained in:
wpetit 2020-04-20 11:14:46 +02:00
commit 364840c665
48 changed files with 20852 additions and 0 deletions

36
.eslintrc.js Normal file
View File

@ -0,0 +1,36 @@
module.exports = {
env: {
browser: true
},
extends: [
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
"prettier/@typescript-eslint",
"plugin:prettier/recommended"
],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaFeatures: {
jsx: true
},
ecmaVersion: 2018,
sourceType: "module",
},
rules: {
"react/no-unknown-property": ["error", { ignore: ["class"] }],
},
settings: {
react: {
pragma: "h",
version: "detect"
},
},
overrides: [
{
files: ["*.js"],
rules: {
"@typescript-eslint/explicit-function-return-type": "off",
}
}
]
};

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules
/build
/*.log

4
.prettierignore Normal file
View File

@ -0,0 +1,4 @@
package.json
package-lock.json
yarn.lock
build

1
.prettierrc Normal file
View File

@ -0,0 +1 @@
tabWidth: 2

19
README.md Normal file
View File

@ -0,0 +1,19 @@
# guesstimate
## CLI Commands
* `npm install`: Installs dependencies
* `npm run start`: Runs `serve` or `dev`, depending on `NODE_ENV` value. Defaults to `dev server`
* `npm run dev`: Run a development, HMR server
* `npm run serve`: Run a production-like server
* `npm run build`: Production-ready build
* `npm run lint`: Pass TypeScript files using TSLint
* `npm run test`: Run Jest and [`preact-render-spy`](https://github.com/mzgoddard/preact-render-spy) for your tests
For detailed explanation on how things work, checkout the [CLI Readme](https://github.com/developit/preact-cli/blob/master/README.md).

33
jest.config.js Normal file
View File

@ -0,0 +1,33 @@
module.exports = {
transform: {
"^.+\\.tsx?$": "ts-jest"
},
verbose: true,
setupFiles: [
"<rootDir>/src/tests/__mocks__/browserMocks.js"
],
testURL: "http://localhost:8080",
moduleFileExtensions: [
"js",
"jsx",
"ts",
"tsx"
],
moduleDirectories: [
"node_modules"
],
testMatch: [
"**/__tests__/**/*.[jt]s?(x)",
"**/?(*.)(spec|test).[jt]s?(x)"
],
moduleNameMapper: {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/src/tests/__mocks__/fileMock.js",
"\\.(css|less|scss)$": "identity-obj-proxy",
"^./style$": "identity-obj-proxy",
"^preact$": "<rootDir>/node_modules/preact/dist/preact.min.js",
"^react$": "preact-compat",
"^react-dom$": "preact-compat",
"^create-react-class$": "preact-compat/lib/create-react-class",
"^react-addons-css-transition-group$": "preact-css-transition-group"
}
}

19794
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

60
package.json Normal file
View File

@ -0,0 +1,60 @@
{
"private": true,
"name": "guesstimate",
"version": "0.0.0",
"license": "MIT",
"scripts": {
"build": "preact build",
"serve": "sirv build --port 8080 --cors --single",
"dev": "preact watch",
"lint": "eslint src/**/*.{js,jsx,ts,tsx}",
"test": "jest ./tests"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{css,md,scss}": "prettier --write",
"*.{js,jsx,ts,tsx}": "eslint --fix"
},
"eslintIgnore": [
"build/*"
],
"dependencies": {
"@types/bs58": "^4.0.1",
"bs58": "^4.0.1",
"bulma": "^0.8.2",
"preact": "^10.3.1",
"preact-jsx-chai": "^3.0.0",
"preact-markup": "^2.0.0",
"preact-render-to-string": "^5.1.4",
"preact-router": "^3.2.1"
},
"devDependencies": {
"@types/jest": "^25.1.2",
"@types/webpack-env": "^1.15.1",
"@typescript-eslint/eslint-plugin": "^2.19.0",
"@typescript-eslint/parser": "^2.19.0",
"css-loader": "^1.0.1",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.10.0",
"eslint-plugin-prettier": "^3.1.2",
"eslint-plugin-react": "^7.18.3",
"husky": "^4.2.1",
"identity-obj-proxy": "^3.0.0",
"jest": "^25.1.0",
"lint-staged": "^10.0.7",
"node-sass": "^4.13.1",
"preact-cli": "^3.0.0-next.19",
"preact-render-spy": "^1.3.0",
"prettier": "^1.19.1",
"sass-loader": "^8.0.2",
"sirv-cli": "^1.0.0-next.3",
"ts-jest": "^25.2.0",
"ts-loader": "^6.2.1",
"typescript": "^3.7.5",
"typings-for-css-modules-loader": "^1.7.0"
}
}

34
preact.config.js Normal file
View File

@ -0,0 +1,34 @@
import { resolve } from "path";
export default {
/**
* Function that mutates the original webpack config.
* Supports asynchronous changes when a promise is returned (or it's an async function).
*
* @param {object} config - original webpack config.
* @param {object} env - options passed to the CLI.
* @param {WebpackConfigHelpers} helpers - object with useful helpers for working with the webpack config.
* @param {object} options - this is mainly relevant for plugins (will always be empty in the config), default to an empty object
**/
webpack(config, env, helpers, options) {
// Switch css-loader for typings-for-css-modules-loader, which is a wrapper
// that automatically generates .d.ts files for loaded CSS
helpers.getLoadersByName(config, "css-loader").forEach(({ loader }) => {
loader.loader = "typings-for-css-modules-loader";
loader.options = Object.assign(loader.options, {
camelCase: true,
banner:
"// This file is automatically generated from your CSS. Any edits will be overwritten.",
namedExport: true,
silent: true
});
});
// Use any `index` file, not just index.js
config.resolve.alias["preact-cli-entrypoint"] = resolve(
process.cwd(),
"src",
"index"
);
}
};

5
src/.babelrc Normal file
View File

@ -0,0 +1,5 @@
{
"presets": [
["preact-cli/babel", { "modules": "commonjs" }]
]
}

BIN
src/assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 626 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

33
src/components/app.tsx Normal file
View File

@ -0,0 +1,33 @@
import { FunctionalComponent, h } from "preact";
import { Route, Router, RouterOnChangeArgs } from "preact-router";
import Home from "../routes/home";
import Project from "../routes/project";
import NotFoundPage from '../routes/notfound';
import Header from "./header";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((module as any).hot) {
// tslint:disable-next-line:no-var-requires
require("preact/debug");
}
const App: FunctionalComponent = () => {
let currentUrl: string;
const handleRoute = (e: RouterOnChangeArgs) => {
currentUrl = e.url;
};
return (
<div id="app">
<Header />
<Router onChange={handleRoute}>
<Route path="/" component={Home} />
<Route path="/p/:uuid" component={Project} />
<NotFoundPage default />
</Router>
</div>
);
};
export default App;

View File

@ -0,0 +1,28 @@
import { FunctionalComponent, h } from "preact";
import { Link } from "preact-router/match";
import * as style from "./style.css";
const Header: FunctionalComponent = () => {
return (
<div class="container">
<div class="columns">
<div class="column">
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="/">
<h1 class="title is-size-4"> Guesstimate</h1>
</a>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
</nav>
</div>
</div>
</div>
);
};
export default Header;

View File

3
src/components/header/style.css.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
// This file is automatically generated from your CSS. Any edits will be overwritten.
export const header: string;
export const active: string;

4
src/declaration.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
import { JSX } from "preact";
export = JSX;
export as namespace JSX;

View File

@ -0,0 +1,89 @@
import { Project } from "../models/project";
import { Task, TaskID, EstimationConfidence } from "../models/task";
export interface Action {
type: string
}
export type ProjectReducerActions =
AddTaskAction |
RemoveTaskAction |
UpdateTaskEstimation
export function projectReducer(project: Project, action: ProjectReducerActions): Project {
switch(action.type) {
case ADD_TASK:
const task = { ...(action as AddTaskAction).task };
return {
...project,
tasks: {
...project.tasks,
[task.id]: task,
}
};
case REMOVE_TASK:
action = action as RemoveTaskAction;
const tasks = { ...project.tasks };
delete tasks[action.id];
return {
...project,
tasks
};
case UPDATE_TASK_ESTIMATION:
action = action as UpdateTaskEstimation;
const estimations = {
...project.tasks[action.id].estimations,
[(action as UpdateTaskEstimation).confidence]: (action as UpdateTaskEstimation).value
};
if (estimations.likely <= estimations.optimistic) {
estimations.likely = estimations.optimistic + 1;
}
if (estimations.pessimistic <= estimations.likely) {
estimations.pessimistic = estimations.likely + 1;
}
return {
...project,
tasks: {
...project.tasks,
[action.id]: {
...project.tasks[action.id],
estimations: estimations,
}
}
};
}
return project;
}
export interface AddTaskAction extends Action {
task: Task
}
export const ADD_TASK = "ADD_TASK";
export function addTask(task: Task): AddTaskAction {
return { type: ADD_TASK, task };
}
export interface RemoveTaskAction extends Action {
id: TaskID
}
export const REMOVE_TASK = "REMOVE_TASK";
export function removeTask(id: TaskID): RemoveTaskAction {
return { type: REMOVE_TASK, id };
}
export interface UpdateTaskEstimation extends Action {
id: TaskID
confidence: string
value: number
}
export const UPDATE_TASK_ESTIMATION = "UPDATE_TASK_ESTIMATION";
export function updateTaskEstimation(id: TaskID, confidence: EstimationConfidence, value: number): UpdateTaskEstimation {
return { type: UPDATE_TASK_ESTIMATION, id, confidence, value };
}

5
src/index.js Normal file
View File

@ -0,0 +1,5 @@
import "./style/index.scss";
import "bulma/bulma.sass";
import App from "./components/app.tsx";
export default App;

21
src/manifest.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "guesstimate",
"short_name": "guesstimate",
"start_url": "/",
"display": "standalone",
"orientation": "portrait",
"background_color": "#fff",
"theme_color": "#673ab8",
"icons": [
{
"src": "/assets/icons/android-chrome-192x192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "/assets/icons/android-chrome-512x512.png",
"type": "image/png",
"sizes": "512x512"
}
]
}

24
src/models/params.ts Normal file
View File

@ -0,0 +1,24 @@
import { TaskCategory, CategoryID } from "./task";
export interface TaskCategoriesIndex {
[id: string]: TaskCategory
}
export interface Params {
taskCategories: TaskCategoriesIndex
}
export const DefaultTaskCategories = {
"7e92266f-0a7b-4728-8322-5fe05ff3b929": {
id: "7e92266f-0a7b-4728-8322-5fe05ff3b929",
label: "Développement"
},
"508a0925-a664-4426-8d40-6974156f0f00": {
id: "508a0925-a664-4426-8d40-6974156f0f00",
label: "Conduite de projet"
},
"7aab4d66-072e-4cc8-aae8-b62edd3237a8": {
id: "7aab4d66-072e-4cc8-aae8-b62edd3237a8",
label: "Recette"
},
};

30
src/models/project.ts Normal file
View File

@ -0,0 +1,30 @@
import { Project } from "./project";
import { Task, TaskCategory, TaskID } from './task';
import { Params, DefaultTaskCategories } from "./params";
import { uuidV4 } from "../util/uuid";
export type ProjectID = string;
export interface Project {
id: ProjectID
label: string
description: string
tasks: Tasks
params: Params
}
export interface Tasks {
[id: string]: Task
}
export function newProject(): Project {
return {
id: uuidV4(),
label: "",
description: "",
tasks: {},
params: {
taskCategories: DefaultTaskCategories,
},
};
}

36
src/models/task.ts Normal file
View File

@ -0,0 +1,36 @@
import { uuidV4 } from "../util/uuid"
export type TaskID = string
export enum EstimationConfidence {
Optimistic = "optimistic",
Likely = "likely",
Pessimistic = "pessimistic"
}
export interface Task {
id: TaskID
label: string
category: CategoryID
estimations: { [confidence in EstimationConfidence]: number }
}
export type CategoryID = string
export interface TaskCategory {
id: CategoryID
label: string
}
export function newTask(label: string, category: CategoryID): Task {
return {
id: uuidV4(),
label,
category,
estimations: {
[EstimationConfidence.Optimistic]: 0,
[EstimationConfidence.Likely]: 0,
[EstimationConfidence.Pessimistic]: 0,
}
};
}

44
src/routes/home/index.tsx Normal file
View File

@ -0,0 +1,44 @@
import { FunctionalComponent, h } from "preact";
import * as style from "./style.css";
import { route } from 'preact-router';
import { base58UUID } from '../../util/uuid';
const Home: FunctionalComponent = () => {
const openNewProject = () => {
const uuid = base58UUID();
route(`/p/${uuid}`);
};
return (
<div class={`container ${style.home}`}>
<div class="columns">
<div class="column">
<div class="buttons is-right">
<button class="button is-primary"
onClick={openNewProject}>
<strong>+</strong>&nbsp;&nbsp;Nouveau projet
</button>
</div>
<div class="panel">
<p class="panel-heading">
Mes projets
</p>
<div class="panel-block">
<p class="control has-icons-left">
<input class="input" type="text" placeholder="Search" />
<span class="icon is-left">🔍</span>
</p>
</div>
<a class="panel-block">
<span class="panel-icon">🗒</span>
Projet #1
</a>
</div>
</div>
</div>
</div>
);
};
export default Home;

View File

@ -0,0 +1,3 @@
.home {
height: 100%;
}

2
src/routes/home/style.css.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
// This file is automatically generated from your CSS. Any edits will be overwritten.
export const home: string;

View File

@ -0,0 +1,15 @@
import { FunctionalComponent, h } from "preact";
import { Link } from 'preact-router/match';
import * as style from "./style.css";
const Notfound: FunctionalComponent = () => {
return (
<div class={style.notfound}>
<h1>Error 404</h1>
<p>That page doesn't exist.</p>
<Link href="/"><h4>Back to Home</h4></Link>
</div>
);
};
export default Notfound;

View File

2
src/routes/notfound/style.css.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
// This file is automatically generated from your CSS. Any edits will be overwritten.
export {};

View File

@ -0,0 +1,37 @@
import { FunctionalComponent, h } from "preact";
import * as style from "./style.css";
import { Project } from "../../models/project";
export interface FinancialPreviewProps {
project: Project
}
const FinancialPreview: FunctionalComponent<FinancialPreviewProps> = ({ project }) => {
return (
<div class="table-container">
<table class="table is-bordered is-striped is-fullwidth">
<thead>
<tr>
<th colSpan={2}>Prévisionnel financier</th>
</tr>
<tr>
<th>Temps</th>
<th>Coût</th>
</tr>
</thead>
<tbody>
<tr>
<td>Maximum</td>
<td></td>
</tr>
<tr>
<td>Minimum</td>
<td></td>
</tr>
</tbody>
</table>
</div>
);
};
export default FinancialPreview;

View File

@ -0,0 +1,61 @@
import { FunctionalComponent, h } from "preact";
import { useReducer } from "preact/hooks";
import * as style from "./style.css";
import { newProject } from "../../models/project";
import TaskTable from "./tasks-table";
import TimePreview from "./time-preview";
import FinancialPreview from "./financial-preview";
import { projectReducer, addTask, updateTaskEstimation, removeTask } from "../../hooks/project-reducer";
import { Task, TaskID, EstimationConfidence } from "../../models/task";
const Project: FunctionalComponent = () => {
const [project, dispatch] = useReducer(projectReducer, newProject());
const onTaskAdd = (task: Task) => {
dispatch(addTask(task));
};
const onTaskRemove = (taskId: TaskID) => {
dispatch(removeTask(taskId));
}
const onEstimationChange = (taskId: TaskID, confidence: EstimationConfidence, value: number) => {
dispatch(updateTaskEstimation(taskId, confidence, value));
};
return (
<div class={`container ${style.estimation}`}>
<div class="tabs">
<ul>
<li class="is-active">
<a>
<span class="icon is-small">📋</span>
Estimation
</a>
</li>
{/* <li>
<a disabled>
<span class="icon is-small"></span>
Paramètres
</a>
</li> */}
</ul>
</div>
<div class="columns">
<div class="column is-9">
<TaskTable
project={project}
onTaskAdd={onTaskAdd}
onTaskRemove={onTaskRemove}
onEstimationChange={onEstimationChange} />
</div>
<div class="column is-3">
<TimePreview project={project} />
<FinancialPreview project={project} />
</div>
</div>
</div>
);
};
export default Project;

View File

@ -0,0 +1,16 @@
.estimation {
height: 100%;
}
.noTasks {
text-align: center !important;
font-style: italic;
}
.noBorder {
border: none !important;
}
.mainColumn {
width: 100%;
}

5
src/routes/project/style.css.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
// This file is automatically generated from your CSS. Any edits will be overwritten.
export const estimation: string;
export const noTasks: string;
export const noBorder: string;
export const mainColumn: string;

View File

@ -0,0 +1,174 @@
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;

View File

@ -0,0 +1,59 @@
import { FunctionalComponent, h } from "preact";
import { Project } from "../../models/project";
import { Task } from "../../models/task";
import { useState, useEffect } from "preact/hooks";
import { getProjectWeightedMean, getProjectStandardDeviation } from "../../util/stat";
export interface TimePreviewProps {
project: Project
}
const TimePreview: FunctionalComponent<TimePreviewProps> = ({ project }) => {
const [ estimations, setEstimations ] = useState({
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 (
<div class="table-container">
<table class="table is-bordered is-striped is-fullwidth">
<thead>
<tr>
<th colSpan={2}>Prévisionnel temps</th>
</tr>
<tr>
<th>Niveau de confiance</th>
<th>Estimation</th>
</tr>
</thead>
<tbody>
<tr>
<td>>= 99.7%</td>
<td>{`${estimations.p99.e} ± ${estimations.p99.sd} j/h`}</td>
</tr>
<tr>
<td>>= 90%</td>
<td>{`${estimations.p90.e} ± ${estimations.p90.sd} j/h`}</td>
</tr>
<tr>
<td>>= 68%</td>
<td>{`${estimations.p68.e} ± ${estimations.p68.sd} j/h`}</td>
</tr>
</tbody>
</table>
</div>
);
};
export default TimePreview;

2
src/style/index.css.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
// This file is automatically generated from your CSS. Any edits will be overwritten.
export const app: string;

0
src/style/index.scss Normal file
View File

View File

@ -0,0 +1,21 @@
// Mock Browser API's which are not supported by JSDOM, e.g. ServiceWorker, LocalStorage
/**
* An example how to mock localStorage is given below 👇
*/
/*
// Mocks localStorage
const localStorageMock = (function() {
let store = {};
return {
getItem: (key) => store[key] || null,
setItem: (key, value) => store[key] = value.toString(),
clear: () => store = {}
};
})();
Object.defineProperty(window, 'localStorage', {
value: localStorageMock
}); */

View File

@ -0,0 +1,3 @@
// This fixed an error related to the CSS and loading gif breaking my Jest test
// See https://facebook.github.io/jest/docs/en/webpack.html#handling-static-assets
export default "test-file-stub";

12
src/tests/header.test.tsx Normal file
View File

@ -0,0 +1,12 @@
import { h } from "preact";
// See: https://github.com/mzgoddard/preact-render-spy
import { shallow } from "preact-render-spy";
import Header from "../components/header";
describe("Initial Test of the Header", () => {
test("Header renders 3 nav items", () => {
const context = shallow(<Header />);
expect(context.find("h1").text()).toBe("Preact App");
expect(context.find("Link").length).toBe(3);
});
});

24
src/util/stat.ts Normal file
View File

@ -0,0 +1,24 @@
import { Task } from "../models/task";
import { Project } from "../models/project";
export function getTaskWeightedMean(t: Task): number {
return (t.estimations.optimistic + (4*t.estimations.likely) + t.estimations.pessimistic) / 6;
}
export function getTaskStandardDeviation(t: Task): number {
return (t.estimations.pessimistic - t.estimations.optimistic) / 6;
}
export function getProjectWeightedMean(p : Project): number {
return Object.values(p.tasks).reduce((sum: number, t: Task) => {
sum += getTaskWeightedMean(t);
return sum;
}, 0);
}
export function getProjectStandardDeviation(p : Project): number {
return Math.sqrt(Object.values(p.tasks).reduce((sum: number, t: Task) => {
sum += Math.pow(getTaskStandardDeviation(t), 2);
return sum;
}, 0));
}

53
src/util/uuid.ts Normal file
View File

@ -0,0 +1,53 @@
import bs58 from 'bs58';
const hex: string[] = [];
for (var i = 0; i < 256; i++) {
hex[i] = (i < 16 ? '0' : '') + (i).toString(16);
}
export function uuidV4(): string {
const r = crypto.getRandomValues(new Uint8Array(16));
r[6] = r[6] & 0x0f | 0x40;
r[8] = r[8] & 0x3f | 0x80;
return (
hex[r[0]] +
hex[r[1]] +
hex[r[2]] +
hex[r[3]] +
"-" +
hex[r[4]] +
hex[r[5]] +
"-" +
hex[r[6]] +
hex[r[7]] +
"-" +
hex[r[8]] +
hex[r[9]] +
"-" +
hex[r[10]] +
hex[r[11]] +
hex[r[12]] +
hex[r[13]] +
hex[r[14]] +
hex[r[15]]
);
}
export function toUTF8Bytes(str: string): number[] {
var utf8 = unescape(encodeURIComponent(str));
var arr: number[] = [];
for (var i = 0; i < utf8.length; i++) {
arr.push(utf8.charCodeAt(i));
}
return arr
}
export function base58UUID(): string {
const uuid = uuidV4();
return bs58.encode(toUTF8Bytes(uuid));
}

57
tsconfig.json Normal file
View File

@ -0,0 +1,57 @@
{
"compilerOptions": {
/* Basic Options */
"target": "ES5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
"module": "ESNext", /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation: */
"allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
"jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
"jsxFactory": "h", /* Specify the JSX factory function to use when targeting react JSX emit, e.g. React.createElement or h. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
// "outDir": "./", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
/* Source Map Options */
// "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
"esModuleInterop": true
},
"include": ["src/**/*.tsx", "src/**/*.ts"]
}