diff --git a/src/renderer/components/Kanban/Board/Board.tsx b/src/renderer/components/Kanban/Board/Board.tsx index ae9af8b..30634ba 100644 --- a/src/renderer/components/Kanban/Board/Board.tsx +++ b/src/renderer/components/Kanban/Board/Board.tsx @@ -117,6 +117,7 @@ export const Board: FC = (props: Props) => { key={listId} boardId={props.boardId} focused={listId === props.focusedList} + done={listId === props.doneList} /> ))} {provided.placeholder} diff --git a/src/renderer/components/Kanban/Board/Overview.tsx b/src/renderer/components/Kanban/Board/Overview.tsx new file mode 100644 index 0000000..b8e372f --- /dev/null +++ b/src/renderer/components/Kanban/Board/Overview.tsx @@ -0,0 +1,141 @@ +import React, { FC, useEffect, useState } from 'react'; +import { Table } from 'antd'; +import { RootState } from '../../../reducers'; +import { Dispatch } from 'redux'; +import { KanbanBoardState } from './action'; +import { ListsState } from '../List/action'; +import { connect } from 'react-redux'; +import { Card, CardsState } from '../Card/action'; +import { IdTrend } from '../../Visualization/ProjectTrend'; +import styled from 'styled-components'; +import { formatTime, formatTimeWithoutZero } from '../../../utils'; + +const Container = styled.div``; + +interface Props { + boards: KanbanBoardState; + lists: ListsState; + cards: CardsState; +} + +interface AggBoardInfo { + _id: string; + name: string; + estimatedLeftTimeSum: number; + actualTimeSum: number; + pomodoroCount: number; + meanPercentageError?: number; +} + +const columns = [ + { + title: 'Board Name', + dataIndex: 'name', + editable: true, + key: 'name' + }, + { + title: 'Estimated Left Time', + dataIndex: 'estimatedLeftTimeSum', + key: 'estimatedLeftTimeSum', + render: formatTimeWithoutZero, + sorter: (a: AggBoardInfo, b: AggBoardInfo) => + a.estimatedLeftTimeSum - b.estimatedLeftTimeSum + }, + { + title: 'Spent Time', + dataIndex: 'actualTimeSum', + key: 'actualTimeSum', + render: formatTimeWithoutZero, + sorter: (a: AggBoardInfo, b: AggBoardInfo) => a.actualTimeSum - b.actualTimeSum + }, + { + title: 'Pomodoros', + dataIndex: 'pomodoroCount', + key: 'pomodoroCount', + sorter: (a: AggBoardInfo, b: AggBoardInfo) => a.pomodoroCount - b.pomodoroCount + }, + { + title: 'Mean Estimate Error', + dataIndex: 'meanPercentageError', + key: 'meanPercentageError', + render: (text?: number) => { + if (text === undefined) { + return ``; + } + + return `${text.toFixed(2)}%`; + }, + sorter: (a: AggBoardInfo, b: AggBoardInfo) => { + const va = a.meanPercentageError === undefined ? 1e8 : a.meanPercentageError; + const vb = b.meanPercentageError === undefined ? 1e8 : b.meanPercentageError; + return va - vb; + } + }, + { + title: 'Trend', + dataIndex: 'trend', + key: 'trend', + render: (text: any, record: AggBoardInfo) => { + return ; + } + } +]; + +type NewCard = Card & { isDone?: boolean }; +const _Overview: FC = (props: Props) => { + const { boards, lists: listsById, cards: cardsById } = props; + const boardRows = Object.values(boards); + + const aggInfo: AggBoardInfo[] = boardRows.map(board => { + const { name, lists, relatedSessions, _id } = board; + const cards: NewCard[] = lists.reduce((l: NewCard[], listId) => { + for (const cardId of listsById[listId].cards) { + const card: NewCard = cardsById[cardId]; + card.isDone = listId === board.doneList; + l.push(card); + } + return l; + }, []); + const [estimatedLeftTimeSum, actualTimeSum, errorSum, n] = cards.reduce( + (l: number[], r: NewCard) => { + let err = 0; + const { actual, estimated } = r.spentTimeInHour; + if (r.isDone && actual !== 0 && estimated !== 0) { + err = (Math.abs(estimated - actual) / actual) * 100; + } + + return [ + l[0] + (r.isDone ? 0 : Math.max(0, estimated - actual)), + l[1] + actual, + l[2] + err, + l[3] + (r.isDone ? 1 : 0) + ]; + }, + [0, 0, 0, 0] + ); + return { + _id, + name, + estimatedLeftTimeSum, + actualTimeSum, + meanPercentageError: n ? errorSum / n : undefined, + pomodoroCount: relatedSessions.length + }; + }); + + return ( + + + + ); +}; + +export const Overview = connect( + (state: RootState) => ({ + boards: state.kanban.boards, + lists: state.kanban.lists, + cards: state.kanban.cards + }), + (dispatch: Dispatch) => ({}) +)(_Overview); diff --git a/src/renderer/components/Kanban/Board/action.ts b/src/renderer/components/Kanban/Board/action.ts index ab0569a..4904f70 100644 --- a/src/renderer/components/Kanban/Board/action.ts +++ b/src/renderer/components/Kanban/Board/action.ts @@ -24,6 +24,7 @@ export interface KanbanBoard { description: string; lists: ListId[]; // lists id in order focusedList: string; + doneList: string; relatedSessions: SessionId[]; aggInfo?: AggInfo; } @@ -33,6 +34,7 @@ const defaultBoard: KanbanBoard = { lists: [], name: '', focusedList: '', + doneList: '', description: '', relatedSessions: [], @@ -48,8 +50,9 @@ const addBoard = createActionCreator( name: string, description: string, lists: string[], - focusedList: string - ) => resolve({ _id, name, description, lists, focusedList }) + focusedList: string, + doneList: string + ) => resolve({ _id, name, description, lists, focusedList, doneList }) ); const setBoardMap = createActionCreator( @@ -95,17 +98,21 @@ const updateAggInfo = createActionCreator( ); export const boardReducer = createReducer({}, handle => [ - handle(addBoard, (state, { payload: { _id, name, description, lists, focusedList } }) => ({ - ...state, - [_id]: { - ...defaultBoard, - _id, - description, - name, - lists, - focusedList - } - })), + handle( + addBoard, + (state, { payload: { _id, name, description, lists, focusedList, doneList } }) => ({ + ...state, + [_id]: { + ...defaultBoard, + _id, + description, + name, + lists, + focusedList, + doneList + } + }) + ), handle(setBoardMap, (state, { payload }) => payload), handle(moveList, (state, { payload: { _id, fromIndex, toIndex } }) => { @@ -221,7 +228,7 @@ export const actions = { await listActions.addList(listId, name)(dispatch); lists.push(listId); } - dispatch(addBoard(_id, name, description, lists, lists[1])); + dispatch(addBoard(_id, name, description, lists, lists[1], lists[2])); await db.insert({ ...defaultBoard, @@ -229,7 +236,8 @@ export const actions = { description, name, lists, - focusedList: lists[1] + focusedList: lists[1], + doneList: lists[2] } as KanbanBoard); }, diff --git a/src/renderer/components/Kanban/Kanban.tsx b/src/renderer/components/Kanban/Kanban.tsx index a05806a..0a5c8f1 100644 --- a/src/renderer/components/Kanban/Kanban.tsx +++ b/src/renderer/components/Kanban/Kanban.tsx @@ -22,6 +22,7 @@ import styled from 'styled-components'; import { SelectParam } from 'antd/lib/menu'; import TextArea from 'antd/es/input/TextArea'; import { SearchBar } from './SearchBar'; +import { Overview } from './Board/Overview'; const { Option } = Select; const { Sider, Content } = Layout; @@ -205,7 +206,7 @@ export const Kanban: FunctionComponent = (props: Props) => { }} > {props.kanban.chosenBoardId === undefined ? ( - undefined + ) : ( )} diff --git a/src/renderer/components/Kanban/List/List.tsx b/src/renderer/components/Kanban/List/List.tsx index 7cf6d45..8eab59e 100644 --- a/src/renderer/components/Kanban/List/List.tsx +++ b/src/renderer/components/Kanban/List/List.tsx @@ -1,5 +1,6 @@ import { Draggable, Droppable } from 'react-beautiful-dnd'; import FocusIcon from '../../../../res/Focus.svg'; +import DoneIcon from '../../../../res/done.svg'; import React, { FC, useRef, useState } from 'react'; import { List as ListType, ListActionTypes } from './action'; import styled from 'styled-components'; @@ -72,6 +73,7 @@ export interface InputProps { index: number; boardId: string; focused?: boolean; + done?: boolean; } interface Props extends ListType, InputProps, ListActionTypes, KanbanActionTypes { @@ -80,7 +82,7 @@ interface Props extends ListType, InputProps, ListActionTypes, KanbanActionTypes } export const List: FC = (props: Props) => { - const { focused = false, searchReg, cards, cardsState } = props; + const { focused = false, searchReg, cards, cardsState, done = false } = props; const [estimatedTimeSum, actualTimeSum] = props.cards.reduce( (l: [number, number], r: string) => { return [ @@ -182,6 +184,15 @@ export const List: FC = (props: Props) => { ) : ( undefined )} + {done ? ( + + + + + + ) : ( + undefined + )} diff --git a/src/renderer/utils.ts b/src/renderer/utils.ts index f1b67aa..e9a9c64 100644 --- a/src/renderer/utils.ts +++ b/src/renderer/utils.ts @@ -53,6 +53,12 @@ export function formatTime(timeInHour: number) { return `${to2digits(hour)}h ${to2digits(minute)}m`; } +export function formatTimeWithoutZero(timeInHour: number) { + const hour = Math.floor(timeInHour); + const minute = Math.floor((timeInHour - hour) * 60 + 0.5); + return `${hour}h ${minute}m`; +} + export function parseTime(formattedTime: string) { const matchedH = formattedTime.match(/(\d+)h/); const matchedM = formattedTime.match(/(\d+)m/); diff --git a/src/res/done.svg b/src/res/done.svg new file mode 100644 index 0000000..023ec89 --- /dev/null +++ b/src/res/done.svg @@ -0,0 +1 @@ + \ No newline at end of file