From a4cbab72a275114341641f3692875a4cd4000651 Mon Sep 17 00:00:00 2001 From: Marco Kellershoff Date: Tue, 14 May 2024 11:11:18 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Redux=20is=20working=20=F0=9F=8D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ' | 36 -- index.old.html | 394 -------------- package-lock.json | 23 +- package.json | 15 +- src/components/ActiveTasks.tsx | 113 +++- src/components/EditTaskDefinitionModal.tsx | 53 ++ src/components/EditTaskModal.tsx | 86 +++ src/components/GUI.tsx | 12 +- src/components/Navigation.tsx | 160 ++++++ src/components/PDFDocument.tsx | 97 ++++ src/components/PDFExport.tsx | 174 ++++++ src/components/Projects.tsx | 30 +- src/components/Store/index.tsx | 9 +- src/components/Store/slices/activeTasks.ts | 78 +++ src/components/Store/slices/pdfDocument.ts | 38 ++ src/components/Store/slices/projects.ts | 2 - src/components/Store/slices/selectedPanel.ts | 38 ++ .../Store/slices/selectedProject.ts | 18 +- src/components/Store/slices/selectedTask.ts | 20 +- .../Store/slices/selectedTaskDefinition.ts | 16 +- .../Store/slices/taskDefinitions.ts | 4 +- src/components/Store/slices/tasks.ts | 11 +- src/components/TaskDefinitions.tsx | 251 +++++++-- src/components/Tasks.tsx | 381 ++++++++++--- src/components/TimeInputComponent.tsx | 58 ++ src/components/TimerComponent.tsx | 29 + src/countup.ts | 43 +- src/database.ts | 54 +- src/global.d.ts | 97 +++- src/index.css | 4 + src/lib/Datafetcher.ts | 38 +- src/lib/Utils.ts | 38 ++ src/main.ts | 226 +++++--- src/preload.ts | 26 +- src/renderer.ts | 515 ------------------ 35 files changed, 1844 insertions(+), 1343 deletions(-) delete mode 100644 ' delete mode 100644 index.old.html create mode 100644 src/components/EditTaskDefinitionModal.tsx create mode 100644 src/components/EditTaskModal.tsx create mode 100644 src/components/Navigation.tsx create mode 100644 src/components/PDFDocument.tsx create mode 100644 src/components/PDFExport.tsx create mode 100644 src/components/Store/slices/activeTasks.ts create mode 100644 src/components/Store/slices/pdfDocument.ts create mode 100644 src/components/Store/slices/selectedPanel.ts create mode 100644 src/components/TimeInputComponent.tsx create mode 100644 src/components/TimerComponent.tsx delete mode 100644 src/renderer.ts diff --git a/' b/' deleted file mode 100644 index dc369c0..0000000 --- a/' +++ /dev/null @@ -1,36 +0,0 @@ -import { createSlice } from '@reduxjs/toolkit' - -interface SelectedProjectState { - value: HTMLDivElement -} - -// Define the initial state using that type -const initialState: SelectedProjectState = { - value: null -} - -interface State { - value: HTMLDivElement -} - -interface Action { - payload: HTMLDivElement -} - -export const selectedProjectSlice = createSlice({ - name: 'selectedProject', - initialState, - reducers: { - setSelectedProject: (state: State, action: Action) => { - state.value = action.payload - }, - removeSelectedProject: (state: State) => { - state.value = null - } - }, -}) - -// Action creators are generated for each case reducer function -export const { replaceProjects, appendProject, removeProject } = selectedProjectSlice.actions - -export const selectedProjectReducer = selectedProjectSlice.reducer; diff --git a/index.old.html b/index.old.html deleted file mode 100644 index 37c0279..0000000 --- a/index.old.html +++ /dev/null @@ -1,394 +0,0 @@ - - - - - - - TimeTrack.Desktop - - - -
- - - -
- - - - - diff --git a/package-lock.json b/package-lock.json index 8563802..11bbe3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,15 +10,16 @@ "license": "MIT", "dependencies": { "@fortawesome/fontawesome-free": "6.5.2", - "@reduxjs/toolkit": "^2.2.4", + "@reduxjs/toolkit": "2.2.4", "bulma": "1.0.0", - "clsx": "^2.1.1", + "clsx": "2.1.1", "i": "0.3.7", + "moment": "^2.30.1", "npm": "10.7.0", "react": "18.3.1", "react-dom": "18.3.1", - "react-redux": "^9.1.2", - "remove": "^0.1.5", + "react-redux": "9.1.2", + "remove": "0.1.5", "sqlite": "5.1.1", "sqlite3": "5.1.7" }, @@ -39,15 +40,15 @@ "@typescript-eslint/parser": "5.62.0", "@vitejs/plugin-react": "4.2.1", "electron": "30.0.3", - "electron-squirrel-startup": "^1.0.1", + "electron-squirrel-startup": "1.0.1", "eslint": "8.57.0", "eslint-import-resolver-typescript": "3.6.1", "eslint-plugin-import": "2.29.1", "ts-node": "10.9.2", - "tsconfig-paths": "^4.2.0", + "tsconfig-paths": "4.2.0", "typescript": "4.5.4", "vite": "5.2.11", - "vite-tsconfig-paths": "^4.3.2" + "vite-tsconfig-paths": "4.3.2" } }, "node_modules/@ampproject/remapping": { @@ -8616,6 +8617,14 @@ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/package.json b/package.json index 59f556b..fbe0e3f 100644 --- a/package.json +++ b/package.json @@ -42,27 +42,28 @@ "@typescript-eslint/parser": "5.62.0", "@vitejs/plugin-react": "4.2.1", "electron": "30.0.3", - "electron-squirrel-startup": "^1.0.1", + "electron-squirrel-startup": "1.0.1", "eslint": "8.57.0", "eslint-import-resolver-typescript": "3.6.1", "eslint-plugin-import": "2.29.1", "ts-node": "10.9.2", - "tsconfig-paths": "^4.2.0", + "tsconfig-paths": "4.2.0", "typescript": "4.5.4", "vite": "5.2.11", - "vite-tsconfig-paths": "^4.3.2" + "vite-tsconfig-paths": "4.3.2" }, "dependencies": { "@fortawesome/fontawesome-free": "6.5.2", - "@reduxjs/toolkit": "^2.2.4", + "@reduxjs/toolkit": "2.2.4", "bulma": "1.0.0", - "clsx": "^2.1.1", + "clsx": "2.1.1", "i": "0.3.7", + "moment": "^2.30.1", "npm": "10.7.0", "react": "18.3.1", "react-dom": "18.3.1", - "react-redux": "^9.1.2", - "remove": "^0.1.5", + "react-redux": "9.1.2", + "remove": "0.1.5", "sqlite": "5.1.1", "sqlite3": "5.1.7" } diff --git a/src/components/ActiveTasks.tsx b/src/components/ActiveTasks.tsx index 335cfdf..4b49237 100644 --- a/src/components/ActiveTasks.tsx +++ b/src/components/ActiveTasks.tsx @@ -1,33 +1,96 @@ -import React, { FC, ReactNode } from 'react'; +import { FC, useEffect } from 'react'; +import { connect } from 'react-redux'; +import type { RootState } from './Store' +import { useAppDispatch } from './Store/hooks' +import { removeActiveTask, replaceActiveTasks } from './Store/slices/activeTasks' +import { TimerComponent } from './TimerComponent'; interface BaseLayoutProps { - children?: ReactNode; + activeTasks: ActiveTask[] } -export const ActiveTasks: FC = ({ children }) => { +const Component: FC = ({ activeTasks }) => { + const dispatch = useAppDispatch(); + + const fetchActiveTasks = async () => { + const rpcResult = await window.electron.getActiveTasks(); + if (rpcResult && rpcResult.length) { + dispatch(replaceActiveTasks(rpcResult)); + } + } + + useEffect(() => { + fetchActiveTasks(); + }, []); + + const stopTask = async (evt: React.MouseEvent) => { + evt.preventDefault(); + const target = evt.target as HTMLButtonElement; + const root = target.closest('tr'); + const rpcResult = await window.electron.stopActiveTask({ + project_name: root.dataset.projectName, + name: root.dataset.name, + date: root.dataset.date + }); + if (rpcResult.success) { + dispatch(removeActiveTask({ + name: root.dataset.name, + project_name: root.dataset.projectName, + date: root.dataset.date + })); + // TODO fix + // dirty hack to force a reload of the task definitions + window.location.reload(); + } + } return <> -
- -
+ { activeTasks.length ? +
+ +
+ : null } ; }; +const mapStateToProps = (state: RootState) => { + return { activeTasks: state.activeTasks.value } +} +const connected = connect(mapStateToProps)(Component); +export const ActiveTasks = connected diff --git a/src/components/EditTaskDefinitionModal.tsx b/src/components/EditTaskDefinitionModal.tsx new file mode 100644 index 0000000..0fcda38 --- /dev/null +++ b/src/components/EditTaskDefinitionModal.tsx @@ -0,0 +1,53 @@ +import React, { FC, ReactNode } from 'react'; + +interface BaseLayoutProps { + children?: ReactNode; + name: string; + useRef: React.RefObject; + callback?: (status: boolean) => void; +} + +export const EditTaskDefinitionModal: FC = ({ callback, name, useRef }) => { + const onEditButtonClick = (evt: React.MouseEvent) => { + evt.preventDefault(); + if (callback) { + callback(true); + } + } + + const onCancelButtonClick = (evt: React.MouseEvent) => { + evt.preventDefault(); + if (callback) { + callback(false); + } + } + + return <> +
+
+
+
+
+

Edit Task Definition

+
+
+
+ +
+ + +
+
+
+
+
+ + +
+
+
+
+
+ ; +}; + diff --git a/src/components/EditTaskModal.tsx b/src/components/EditTaskModal.tsx new file mode 100644 index 0000000..36db2af --- /dev/null +++ b/src/components/EditTaskModal.tsx @@ -0,0 +1,86 @@ +import React, { FC } from 'react'; +import { connect } from 'react-redux'; +import { RootState } from './Store'; +import { TimeInputComponent } from './TimeInputComponent'; + +interface BaseLayoutProps { + activeTasks: ActiveTask[]; + task: DBTask; + useRef: React.RefObject; + callback: (status: boolean) => void; +} + +const Component: FC = ({ activeTasks, callback, task, useRef }) => { + const onEditButtonClick = (evt: React.MouseEvent) => { + evt.preventDefault(); + if (callback) { + callback(true); + } + } + + const onCancelButtonClick = (evt: React.MouseEvent) => { + evt.preventDefault(); + if (callback) { + callback(false); + } + } + + const activeTask = activeTasks.find(t => t.name === task.name && t.date === task.date && t.project_name === task.project_name) + + return <> +
+
+
+
+
+

Edit Task

+
+
+
+ +
+ +
+
+
+ +
+ { (activeTask !== undefined) ? +
+
+

Warning

+
+
+ Editing the duration of an active task is not allowed. + You can stop the task first, and then edit the duration. +
+
+ + +
+
+ : + } +
+
+
+
+
+ + +
+
+
+
+
+ ; +}; + +const mapStateToProps = (state: RootState) => { + return { + activeTasks: state.activeTasks.value + } +} +const connected = connect(mapStateToProps)(Component); +export const EditTaskModal = connected + diff --git a/src/components/GUI.tsx b/src/components/GUI.tsx index f2e0ac0..fe7ee6e 100644 --- a/src/components/GUI.tsx +++ b/src/components/GUI.tsx @@ -1,20 +1,14 @@ -import React, { FC, useState, useEffect, useContext } from 'react'; +import { FC } from 'react'; import { Provider } from 'react-redux' import { Container } from './Container'; -import { ActiveTasks } from './ActiveTasks'; -import { Projects } from './Projects'; -import { Tasks } from './Tasks'; -import { TaskDefinitions } from './TaskDefinitions'; +import { Navigation } from './Navigation'; import { store } from './Store' export const GUI: FC = () => { return <> - - - - + ; diff --git a/src/components/Navigation.tsx b/src/components/Navigation.tsx new file mode 100644 index 0000000..865cde3 --- /dev/null +++ b/src/components/Navigation.tsx @@ -0,0 +1,160 @@ +import { FC } from 'react'; +import { connect } from 'react-redux'; +import type { RootState } from './Store' +import { ActiveTasks } from './ActiveTasks'; +import { Projects } from './Projects'; +import { Tasks } from './Tasks'; +import { TaskDefinitions } from './TaskDefinitions'; +import { PDFExport } from './PDFExport'; +import { PDFDocument } from './PDFDocument'; +import { useAppDispatch } from './Store/hooks' +import { setSelectedPanel } from './Store/slices/selectedPanel' + +type Props = { + selectedPanel: string, +} + +const Component: FC = ({ selectedPanel }) => { + const dispatch = useAppDispatch(); + + const handleTopButtonsClick = (evt: React.MouseEvent) => { + evt.preventDefault(); + const target = evt.target as HTMLButtonElement; + const root = target.closest('button'); + switch (root.dataset.action) { + case 'reportABug': + root.classList.add('is-loading'); + setTimeout(() => { + root.classList.remove('is-loading'); + }, 3000); + window.open('https://github.com/mistweaverco/timetrack.desktop/issues/new'); + break; + case 'seeTheCode': + root.classList.add('is-loading'); + setTimeout(() => { + root.classList.remove('is-loading'); + }, 3000); + window.open('https://github.com/mistweaverco/timetrack.desktop'); + break; + default: + break; + } + } + + const handlePanelClick = (evt: React.MouseEvent) => { + evt.preventDefault(); + const target = evt.target as HTMLButtonElement; + const root = target.closest('a') as HTMLAnchorElement; + const nav = root.closest('nav'); + const heading = nav.querySelector('.panel-heading'); + const items = nav.querySelectorAll('a') as NodeListOf; + items.forEach((item) => { + item.classList.remove('is-active'); + }); + root.classList.add('is-active'); + switch (root.dataset.action) { + case 'Overview': + heading.textContent = 'Overview'; + dispatch(setSelectedPanel({ name: 'Overview' })); + break; + case 'Search': + heading.textContent = 'Search'; + dispatch(setSelectedPanel({ name: 'Search' })); + break; + case 'PDFExport': + heading.textContent = 'PDF Export'; + dispatch(setSelectedPanel({ name: 'PDFExport' })); + break; + default: + break; + } + } + + const OverviewComponent: FC = () => { + return <> + + + + + ; + } + + const ActiveComponent: FC = () => { + return <> + { selectedPanel === 'Search' &&
} + { selectedPanel === 'PDFExport' && } + { selectedPanel === 'PDFDocument' && } + { selectedPanel === 'Overview' && } + ; + } + + return <> + { selectedPanel !== "PDFDocument" ? + <> + + + + : null } + + ; +} + + +const mapStateToProps = (state: RootState) => { + return { + selectedPanel: state.selectedPanel.value.name, + } +} +const connected = connect(mapStateToProps)(Component); + +export const Navigation = connected; diff --git a/src/components/PDFDocument.tsx b/src/components/PDFDocument.tsx new file mode 100644 index 0000000..3cde7f0 --- /dev/null +++ b/src/components/PDFDocument.tsx @@ -0,0 +1,97 @@ +import React, { FC } from 'react'; +import { connect } from 'react-redux'; +import type { RootState } from './Store' +import { getHMSStringFromSeconds } from './../lib/Utils'; + +type TotalViewProps = { + pdfDocument: PDFQueryResult[], +} + +const TotalView: FC = ({ pdfDocument }) => { + const total: PDFTotalObject = {} + pdfDocument.forEach((item: PDFQueryResult) => { + if (!total[item.project_name]) { + total[item.project_name] = {} + } + if (!total[item.project_name][item.name]) { + total[item.project_name][item.name] = 0 + } + total[item.project_name][item.name] += item.seconds + }) + return <> +
+
+

Total Time

+

Total time spent on projects and tasks combined.

+
+
+ {Object.keys(total).map((projectName: string, idx: number) => ( +
+
+

{projectName}

+
+
+
+ {Object.keys(total[projectName]).map((taskName: string, idx: number) => ( +
+

{taskName} {getHMSStringFromSeconds(total[projectName][taskName])}

+
+ ))} +
+
+
+ ))} + +} + +const BasicView: FC = (pdfDocument: PDFQueryResult[]) => { + return <> +
+
+

timetrack.desktop

+

Simple desktop 🖥️ application to track your time ⏰ spent on different projects 🎉.
+
+ { pdfDocument.map((item: PDFQueryResult, idx: number) => ( +
+
+

{item.project_name}

+
+
+
+

{item.description}

+
+ +
+
+
+

{getHMSStringFromSeconds(item.seconds)}

+
+
+ ))} + + ; +} + +type Props = { + pdfDocument: PDFQueryResult[], +} + +const Component: FC = ({ pdfDocument }) => { + if (!pdfDocument.length) { + return null; + } + return ( +
+ {BasicView(pdfDocument)} +
+ ) +} + +const mapStateToProps = (state: RootState) => { + return { + pdfDocument: state.pdfDocument.value.name, + } +} +const connected = connect(mapStateToProps)(Component); + +export const PDFDocument = connected; diff --git a/src/components/PDFExport.tsx b/src/components/PDFExport.tsx new file mode 100644 index 0000000..86013c0 --- /dev/null +++ b/src/components/PDFExport.tsx @@ -0,0 +1,174 @@ +import React, { FC, useEffect, useRef, useState, ReactNode } from 'react'; +import moment from 'moment'; +import { getHMSStringFromSeconds } from './../lib/Utils'; +import { Datafetcher } from './../lib/Datafetcher'; +import { useAppDispatch, useAppSelector } from './Store/hooks' +import { setPDFDocument, removePDFDocument } from './Store/slices/pdfDocument' +import { setSelectedPanel } from './Store/slices/selectedPanel' + +type ProjectTaskDefintion = { + project_name: string, + name: string, +} + +const TasksComponent: FC<{ tasks: ProjectTaskDefintion[] }> = ({ tasks }) => { + if (!tasks.length) { + return null; + } + return tasks.map((task, idx: number) => { + return ( +
+ +
+ ) + }) +} + +const ProjectsComponent: FC<{ projects: DBProject[], taskdefs: ProjectTaskDefintion[] }> = ({ projects, taskdefs }) => { + if (!projects.length) { + return null; + } + if (!taskdefs.length) { + return null; + } + return projects.map((project, idx: number) => { + const tasks = taskdefs.filter((td) => td.project_name === project.name); + return ( +
+
+

{project.name}

+
+
+
+ +
+
+
+ ) + }) +} + +const Component: FC = () => { + const [projects, setProjects] = useState([]); + const [tasksDefinitions, setTaskDefinitions] = useState([]); + const fromRef = useRef(null) + const toRef = useRef(null) + const genRef = useRef(null) + const dispatch = useAppDispatch(); + + const onFormSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const form = e.currentTarget as HTMLFormElement; + const formData = new FormData(form); + const rawfrom = formData.get('from') as string; + const rawto = formData.get('to') as string; + const from = moment(rawfrom).format('YYYY-MM-DD'); + const to = moment(rawto).format('YYYY-MM-DD'); + const tasks = form.querySelectorAll('input[name="task"]:checked'); + const query: PDFQuery = { from, to, tasks: [] }; + tasks.forEach((task: HTMLInputElement) => { + const project_name = task.dataset.projectName as string; + const name = task.value; + query.tasks.push({ project_name, name }); + }); + const rpcResult = await window.electron.getDataForPDFExport(query); + window.electron.showFileSaveDialog(); + dispatch(setSelectedPanel({ name: 'PDFDocument' })); + dispatch(setPDFDocument({ name: rpcResult })); + } + + window.electron.on('on-pdf-export-file-selected', ({ filePath, canceled }) => { + if (canceled) { + dispatch(removePDFDocument()); + dispatch(setSelectedPanel({ name: 'PDFExport' })); + } + }) + + window.electron.on('on-pdf-export-file-saved', () => { + dispatch(removePDFDocument()); + dispatch(setSelectedPanel({ name: 'PDFExport' })); + }) + + const onTimeChange = async () => { + if (fromRef.current && toRef.current) { + const from = fromRef.current.value; + const to = toRef.current.value; + if (from && to) { + genRef.current.disabled = false; + } else { + genRef.current.disabled = true; + } + } else { + genRef.current.disabled = true; + } + } + + + const fetchAllProjects = async () => { + const p = await Datafetcher.getProjects(); + setProjects(p); + } + + const fetchAllTaskDefinitions = async () => { + const td = await Datafetcher.getAllTaskDefinitions(); + setTaskDefinitions(td); + } + + useEffect(() => { + fetchAllProjects(); + fetchAllTaskDefinitions(); + }, []) + + if (tasksDefinitions.length) { + return <> +
+

PDF Export

+

You can export your saved projects and tasks as PDF.

+
+
+
+
+ +
+ +
+ +
+
+
+
+
+ ; + } else { + return null; + } +}; + +export const PDFExport = Component; + diff --git a/src/components/Projects.tsx b/src/components/Projects.tsx index efa266b..71f66cf 100644 --- a/src/components/Projects.tsx +++ b/src/components/Projects.tsx @@ -2,7 +2,9 @@ import { FC, useEffect, useRef, useState } from 'react'; import { useAppDispatch, useAppSelector } from './Store/hooks' import { replaceProject, replaceProjects, appendProject, deleteProject } from './Store/slices/projects' import { setSelectedProject, removeSelectedProject } from './Store/slices/selectedProject' +import { removeSelectedTaskDefinition } from './Store/slices/selectedTaskDefinition' import { Datafetcher } from './../lib/Datafetcher'; +import { removeActiveClassnameProjects, removeActiveClassnameTaskDefinitions } from './../lib/Utils'; import { ModalConfirm } from './ModalConfirm'; import { EditProjectModal } from './EditProjectModal'; @@ -27,19 +29,22 @@ export const Projects: FC = () => { const onProjectSelect = async (evt: React.MouseEvent) => { const target = evt.target as HTMLDivElement - const name = target.dataset.name as string - dispatch(setSelectedProject(name)) - target.parentNode.querySelectorAll('.panel-block').forEach((n: HTMLDivElement) => n.classList.remove('is-active')) - target.classList.add('is-active'); + const root = target.closest('[data-name]') as HTMLDivElement + const name = root.dataset.name as string + removeActiveClassnameProjects(); + removeActiveClassnameTaskDefinitions(); + dispatch(removeSelectedTaskDefinition()) + dispatch(setSelectedProject({ name: name })) + root.classList.add('is-active'); } const onConfirmCallback = async (status: boolean) => { if (status) { - const project = projects.find((p) => p.name === selectedProject) + const project = projects.find((p) => p.name === selectedProject.name) if (project) { const rpcResult = await window.electron.deleteProject(project.name) if (rpcResult.success) { - dispatch(deleteProject({ name: selectedProject })); + dispatch(deleteProject({ name: selectedProject.name })); dispatch(removeSelectedProject()); } } @@ -61,7 +66,7 @@ export const Projects: FC = () => { const rpcResult = await window.electron.editProject({ oldname, name }) if (rpcResult.success) { dispatch(replaceProject({ name, oldname })); - dispatch(setSelectedProject(name)); + dispatch(setSelectedProject({ name: name })); } } setModalEdit(null) @@ -69,7 +74,7 @@ export const Projects: FC = () => { const onEditButtonClick = async (evt: React.MouseEvent) => { evt.preventDefault(); - setModalEdit() + setModalEdit() } const fetchProjects = async () => { @@ -110,7 +115,7 @@ export const Projects: FC = () => {
- {selectedProject !== null ? + {selectedProject.name !== null ?
+
+ : null } + { selectedTaskDefinition.name ? +
+ +
+ : null } + +
- - + + : null } ; }; +const mapStateToProps = (state: RootState) => { + return { + taskDefinitions: state.taskDefinitions.value, + selectedProject: state.selectedProject.value, + activeTasks: state.activeTasks.value + } +} +const connected = connect(mapStateToProps)(Component); +export const TaskDefinitions = connected + diff --git a/src/components/Tasks.tsx b/src/components/Tasks.tsx index 576192c..d4800a6 100644 --- a/src/components/Tasks.tsx +++ b/src/components/Tasks.tsx @@ -1,125 +1,332 @@ import { FC, useEffect, useRef, useState } from 'react'; +import { connect } from 'react-redux'; import { Datafetcher } from './../lib/Datafetcher'; +import { removeActiveClassnameTasks } from './../lib/Utils'; +import type { RootState } from './Store' import { useAppDispatch, useAppSelector } from './Store/hooks' import { replaceTask, replaceTasks, appendTask, deleteTask } from './Store/slices/tasks' import { replaceTaskDefinitions } from './Store/slices/taskDefinitions' import { setSelectedTask, removeSelectedTask } from './Store/slices/selectedTask' +import { appendActiveTask } from './Store/slices/activeTasks' import { ModalConfirm } from './ModalConfirm'; +import { EditTaskModal } from './EditTaskModal'; +import { TimerComponent } from './TimerComponent'; +import { TimeInputComponent } from './TimeInputComponent'; -export const Tasks: FC = () => { +type Props = { + selectedProject: { + name: string | null + }, + activeTasks: ActiveTask[] +} + +type WrappedTimerComponentProps = { + task: DBTask +} + +const Component: FC = ({ selectedProject, activeTasks }) => { const dispatch = useAppDispatch(); const tasksDefinitions = useAppSelector((state) => state.taskDefinitions.value) const tasks = useAppSelector((state) => state.tasks.value) - const selectedProject = useAppSelector((state) => state.selectedProject.value) const selectedTask = useAppSelector((state) => state.selectedTask.value) const [useModalConfirm, setModalConfirm] = useState(null) const [useModalEdit, setModalEdit] = useState(null) const useModalEditRef = useRef(null) + const WrappedTimerComponent: FC = ({ task }) => { + const activeTask = activeTasks.find((at) => at.name === task.name && at.project_name === task.project_name && at.date === task.date) + if (activeTask) { + return ( +
Task is running
+ ) + } else { + return + } + } + + const onFormSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const form = e.currentTarget as HTMLFormElement; + const formData = new FormData(form); + const task = { + name: formData.get('name') as string, + description: formData.get('description') as string, + project_name: selectedProject.name as string, + seconds: parseInt(formData.get('seconds') as string), + date: new Date().toISOString().split('T')[0] + } + const rpcResult = await window.electron.addTask({ + name: task.name, + description: task.description, + project_name: task.project_name, + seconds: task.seconds, + }) + if (rpcResult.success) { + dispatch(appendTask(task)) + } + } + + const onTaskStartClick = async (evt: React.MouseEvent) => { + evt.preventDefault(); + // TODO we have this kind of .find often, maybe we can refactor it + // and find a better way to reuse it + const task = tasks.find((t) => t.name === selectedTask.name && t.project_name === selectedTask.project_name && t.date === selectedTask.date) + if (task) { + const rpcResult = await window.electron.startActiveTask({ + name: task.name, + project_name: task.project_name, + date: task.date, + seconds: task.seconds + }) + if (rpcResult.success) { + dispatch(appendActiveTask({ + name: task.name, + project_name: task.project_name, + description: task.description, + date: task.date, + seconds: task.seconds, + isActive: true + })) + } + } + } + + const onTaskEditCallback = async (status: boolean) => { + if (status) { + const form = useModalEditRef.current.querySelector('form') as HTMLFormElement; + const formData = new FormData(form); + const task = { + name: selectedTask.name, + description: formData.get('description') as string, + project_name: selectedTask.project_name as string, + seconds: parseInt(formData.get('seconds') as string, 10), + date: selectedTask.date + } + const rpcResult = await window.electron.editTask({ + name: task.name, + description: task.description, + project_name: task.project_name, + seconds: task.seconds, + date: task.date + }) + if (rpcResult.success) { + dispatch(replaceTask({ + name: task.name, + oldname: task.name, + project_name: task.project_name, + date: task.date + })) + // TODO fix + // dirty hack to update the task in the store + window.location.reload() + } + } + setModalEdit(null) + } + + const onTaskEditClick = async (evt: React.MouseEvent) => { + evt.preventDefault(); + const task = tasks.find((t) => t.name === selectedTask.name && t.project_name === selectedTask.project_name && t.date === selectedTask.date) + if (task) { + setModalEdit() + } + } + + const onConfirmCallback = async (status: boolean) => { + if (status) { + const task = tasks.find((t) => t.name === selectedTask.name && t.project_name === selectedTask.project_name && t.date === selectedTask.date) + if (task) { + // TODO make sure task is not running on the backend + const rpcResult = await window.electron.deleteTask({ + name: task.name, + project_name: task.project_name, + date: task.date + }) + if (rpcResult.success) { + dispatch(deleteTask({ name: task.name, project_name: task.project_name, date: task.date })); + dispatch(removeSelectedTask()); + } + } + } + setModalConfirm(null) + } + + const onTaskDeleteClick = async (evt: React.MouseEvent) => { + evt.preventDefault(); + setModalConfirm() + } + const fetchTaskDefinitions = async () => { - if (!selectedProject) return; - const td = await Datafetcher.getTaskDefinitions(selectedProject); + if (!selectedProject.name) return; + const td = await Datafetcher.getTaskDefinitions(selectedProject.name); dispatch(replaceTaskDefinitions(td)) } + const fetchTasks = async () => { + if (!selectedProject.name) return; + const t = await Datafetcher.getTasksToday(selectedProject.name); + dispatch(replaceTasks(t)) + } + + const onTaskSelect = async (evt: React.MouseEvent) => { + const target = evt.target as HTMLDivElement + const root = target.closest('[data-name]') as HTMLDivElement + const name = root.dataset.name as string + removeActiveClassnameTasks(); + root.classList.add('is-active'); + dispatch(setSelectedTask({ + name: name, + seconds: parseInt(root.dataset.seconds as string, 10), + project_name: root.dataset.projectName as string, + date: root.dataset.date as string + })) + } + + const ButtonWrapperComponent: FC = ({ children }) => { + const activeTask = activeTasks.find((at) => at.name === selectedTask.name && at.project_name === selectedTask.project_name && at.date === selectedTask.date) + if (activeTask) { + return <> +
+
+

Warning

+
+
+ Task is currently active, + you need to stop it to perform a delete action. +
+
+ + + } else { + return <> + + + + + } + } + useEffect(() => { fetchTaskDefinitions(); - }, []) - - return <> - {useModalConfirm} - {useModalEdit} -
-

Tasks

-

All available Tasks for a given project

-
-
-
- -
+ + +
-
- +
+ : null} + + { selectedTask.name !== null ? +
+
-
+ + - - + : null } -
-
- -
- ; + + ; + } else { + return null; + } }; +const mapStateToProps = (state: RootState) => { + return { + selectedProject: state.selectedProject.value, + activeTasks: state.activeTasks.value + } +} +const connected = connect(mapStateToProps)(Component); +export const Tasks = connected + diff --git a/src/components/TimeInputComponent.tsx b/src/components/TimeInputComponent.tsx new file mode 100644 index 0000000..5d5b9b3 --- /dev/null +++ b/src/components/TimeInputComponent.tsx @@ -0,0 +1,58 @@ +import { FC, useRef, useEffect } from 'react'; +import { getHMSFromSeconds, getHMSToSeconds } from './../lib/Utils'; + +interface BaseLayoutProps { + task: DBTask | ActiveTask; +} + +export const TimeInputComponent: FC = ({ task }) => { + const hms = getHMSFromSeconds(task.seconds); + + const secondsInput = useRef(null); + const hInput = useRef(null); + const mInput = useRef(null); + const sInput = useRef(null); + + useEffect(() => { + if (secondsInput.current && hInput.current && mInput.current && sInput.current) { + secondsInput.current.value = String(task.seconds); + hInput.current.value = String(hms.hours); + mInput.current.value = String(hms.minutes); + sInput.current.value = String(hms.seconds); + } + }, [task.seconds, hInput, mInput, sInput]); + + const onChange = () => { + if (secondsInput.current && hInput.current && mInput.current && sInput.current) { + const secondsValue = getHMSToSeconds(Number(hInput.current.value), Number(mInput.current.value), Number(sInput.current.value)); + secondsInput.current.value = String(secondsValue); + } + } + + return <> + + +
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+ +} diff --git a/src/components/TimerComponent.tsx b/src/components/TimerComponent.tsx new file mode 100644 index 0000000..ae879fc --- /dev/null +++ b/src/components/TimerComponent.tsx @@ -0,0 +1,29 @@ +import { FC, useRef, useEffect } from 'react'; +import { getHMSStringFromSeconds } from './../lib/Utils'; + +interface BaseLayoutProps { + task: DBTask | ActiveTask; +} + +export const TimerComponent: FC = ({ task }) => { + const ref = useRef(null); + let tick: NodeJS.Timeout | null = null; + let s = task.seconds; + + useEffect(() => { + if ('isActive' in task && task.isActive) { + if (ref.current) { + tick = setInterval(() => { + s++; + ref.current.innerHTML = getHMSStringFromSeconds(s); + }, 1000); + } + } + return () => { + if (tick) clearInterval(tick); + } + }, [task]) + return ( +
{getHMSStringFromSeconds(task.seconds)}
+ ) +} diff --git a/src/countup.ts b/src/countup.ts index 24b3a2c..117de8f 100644 --- a/src/countup.ts +++ b/src/countup.ts @@ -1,16 +1,26 @@ +type CountUpOpts = { + name: string, + project_name: string, + description: string, + date: string, + seconds: number, +} + class CountUp { name: string; - projectName: string; + project_name: string; + description: string; date: string; seconds: number; tick: NodeJS.Timeout | null = null; count: number; - constructor(name: string, projectName: string, date: string, seconds: number) { - this.name = name; - this.projectName = projectName; - this.date = date; - this.seconds = seconds; + constructor(opts: CountUpOpts) { + this.name = opts.name; + this.project_name = opts.project_name; + this.description = opts.description; + this.date = opts.date; + this.seconds = opts.seconds; } start() { @@ -24,32 +34,13 @@ class CountUp { this.tick = null } - isRunning() { + isActive() { return this.tick !== null } - toggle() { - if (this.tick) { - this.stop(); - } else { - this.start(); - } - } - increment() { this.seconds++; } - - getSeconds() { - return this.count; - } - - getTime() { - const hours = Math.floor(this.seconds / 3600); - const minutes = Math.floor((this.seconds % 3600) / 60); - const seconds = this.seconds % 60; - return `${hours}:${minutes}:${seconds}`; - } } export { diff --git a/src/database.ts b/src/database.ts index 9d3c934..5e93cbb 100644 --- a/src/database.ts +++ b/src/database.ts @@ -23,14 +23,24 @@ const initDB = async (): Promise =>{ type Task = { name: string, - projectName: string, + project_name: string, date: string, seconds: number, } -const saveRunningTasks = async (db: Database, tasks: Task[]) => { +const getDataForPDFExport = async (db: Database, q: PDFQuery): Promise => { + const inTaskClause = q.tasks.map((task) => `'${task.name}'`).join(',') + const res = await db.all(`SELECT * FROM tasks WHERE date BETWEEN ? AND ? AND name IN(${inTaskClause})`, q.from, q.to) + return res +} + +const saveActiveTask = async (db: Database, task: Task) => { + await db.run('UPDATE tasks SET seconds = ? WHERE name = ? AND project_name = ? AND date = ?', task.seconds, task.name, task.project_name, task.date); +} + +const saveActiveTasks = async (db: Database, tasks: Task[]) => { tasks.forEach(async (task) => { - await db.run('UPDATE tasks SET seconds = ? WHERE name = ? AND project_name = ? AND date = ?', task.seconds, task.name, task.projectName, task.date); + await db.run('UPDATE tasks SET seconds = ? WHERE name = ? AND project_name = ? AND date = ?', task.seconds, task.name, task.project_name, task.date); }); } @@ -59,43 +69,53 @@ const deleteProject = async (db: Database, name: string) => { } const addTaskDefinition = async (db: Database, opts: DBAddTaskDefinitionOpts) => { - await db.run('INSERT INTO task_definitions (name, project_name) VALUES (?, ?)', opts.name, opts.projectName) + await db.run('INSERT INTO task_definitions (name, project_name) VALUES (?, ?)', opts.name, opts.project_name) return { success: true } } const editTaskDefinition = async (db: Database, opts: DBEditTaskDefinitionOpts) => { - await db.run('UPDATE task_definitions SET name = ? WHERE name=? AND project_name = ?', opts.name, opts.oldname, opts.projectName) + await db.run('UPDATE task_definitions SET name = ? WHERE name=? AND project_name = ?', opts.name, opts.oldname, opts.project_name) + await db.run('UPDATE tasks SET name = ? WHERE name=? AND project_name = ?', opts.name, opts.oldname, opts.project_name) return { success: true } } const deleteTaskDefinition = async (db: Database, opts: DBDeleteTaskDefinitionOpts) => { - await db.run('DELETE FROM task_definitions WHERE name = ? AND project_name = ?', opts.name, opts.projectName) + await db.run('DELETE FROM task_definitions WHERE name = ? AND project_name = ?', opts.name, opts.project_name) return { success: true } } -const getTaskDefinitions = async (db: Database, projectName: string): Promise => { - const tasks = await db.all('SELECT * FROM task_definitions WHERE project_name = ?', projectName) +const getTaskDefinitions = async (db: Database, project_name: string): Promise => { + const tasks = await db.all('SELECT * FROM task_definitions WHERE project_name = ?', project_name) return tasks } +const getAllTaskDefinitions = async (db: Database): Promise => { + const res = await db.all('SELECT * FROM task_definitions') + return res +} + const addTask = async (db: Database, opts: DBAddTaskOpts) => { - await db.run('INSERT INTO tasks (name, description, project_name) VALUES (?, ?, ?)', opts.name, opts.description, opts.projectName) + await db.run('INSERT INTO tasks (name, description, project_name, seconds) VALUES (?, ?, ?, ?)', opts.name, opts.description, opts.project_name, opts.seconds) return { success: true } } const editTask = async (db: Database, opts: DBEditTaskOpts) => { - await db.run('UPDATE tasks SET name = ?, description = ?, seconds = ? WHERE name=? AND date=? AND project_name = ?', opts.name, opts.description, opts.seconds, opts.oldname, opts.date, opts.projectName) + await db.run('UPDATE tasks SET name = ?, description = ?, seconds = ? WHERE name=? AND date=? AND project_name = ?', opts.name, opts.description, opts.seconds, opts.name, opts.date, opts.project_name) return { success: true } } const deleteTask = async (db: Database, opts: DBDeleteTaskOpts) => { - const res = await db.run('DELETE FROM tasks WHERE name = ? AND date = ? AND project_name = ?', opts.name, opts.date, opts.projectName) - console.log({res, opts}) + await db.run('DELETE FROM tasks WHERE name = ? AND date = ? AND project_name = ?', opts.name, opts.date, opts.project_name) return { success: true } } -const getTasks = async (db: Database, projectName: string): Promise => { - const tasks = await db.all('SELECT * FROM tasks WHERE project_name = ?', projectName) +const getTasks = async (db: Database, project_name: string): Promise => { + const tasks = await db.all('SELECT * FROM tasks WHERE project_name = ?', project_name) + return tasks +} + +const getTasksToday = async (db: Database, project_name: string): Promise => { + const tasks = await db.all('SELECT * FROM tasks WHERE project_name = ? AND date=DATE("now")', project_name) return tasks } @@ -109,10 +129,14 @@ export { editTaskDefinition, deleteTaskDefinition, getTaskDefinitions, + getAllTaskDefinitions, addTask, editTask, deleteTask, getTasks, - saveRunningTasks, + getTasksToday, + saveActiveTasks, + saveActiveTask, + getDataForPDFExport, } diff --git a/src/global.d.ts b/src/global.d.ts index 1d8a305..5246542 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -1,3 +1,38 @@ +type ActiveTask = { + name: string, + project_name: string, + description: string, + date: string, + seconds: number, + tick?: NodeJS.Timeout | null, + isActive?: boolean, +} + +type PDFTotalObject = { + [key: string]: { + [key: string]: number + } +} + +type PDFQueryResult = { + date: string, + description: string, + name: string, + project_name: string, + seconds: number, +} + +type PDFQueryTask = { + project_name: string, + name: string, +} + +type PDFQuery = { + from: string, + to: string, + tasks: PDFQueryTask[], +} + type DBProject = { name: string, } @@ -15,24 +50,25 @@ type DBTask = { date: string, } -type MainProcessRunningTaskMapped = { +type MainProcessActiveTaskMapped = { name: string, - projectName: string, + project_name: string, date: string, seconds: number, time: string, - isRunning: boolean, + isActive: boolean, } -type MainProcessManageRunningTasksOpts = { - projectName: string, +type MainProcessManageActiveTasksOpts = { + project_name: string, name: string, date: string, + seconds?: number, } -type MainProccessAddRunningTaskOpts = { +type MainProccessAddActiveTaskOpts = { name: string, - projectName: string, + project_name: string, date: string, seconds: number, } @@ -44,39 +80,39 @@ type DBEditProjectOpts = { type DBAddTaskDefinitionOpts = { name: string, - projectName: string, + project_name: string, } type DBEditTaskDefinitionOpts = { name: string, oldname: string, - projectName: string, + project_name: string, } type DBDeleteTaskDefinitionOpts = { name: string, - projectName: string, + project_name: string, } type DBAddTaskOpts = { name: string, description: string, - projectName: string, + project_name: string, + seconds: number, } type DBEditTaskOpts = { name: string, description: string, seconds: number, - oldname: string, date: string, - projectName: string, + project_name: string, } type DBDeleteTaskOpts = { name: string, date: string, - projectName: string, + project_name: string, } type MainProcessIPCHandle = { @@ -84,6 +120,10 @@ type MainProcessIPCHandle = { cb: any, // eslint-disable-line @typescript-eslint/no-explicit-any } +declare const MAIN_WINDOW_VITE_DEV_SERVER_URL: string | undefined; +declare const MAIN_WINDOW_VITE_NAME: string; +declare const electron: any; // eslint-disable-line @typescript-eslint/no-explicit-any + interface Window { versions: { node: string; @@ -91,23 +131,24 @@ interface Window { electron: string; } electron: { + on: (channel: string, callback: (data: any) => void) => void; // eslint-disable-line @typescript-eslint/no-explicit-any addProject: (name: string) => Promise<{success: boolean}>; editProject: (opts: { name: string, oldname: string }) => Promise<{success: boolean}>; deleteProject: (name: string) => Promise<{success: boolean}>; getProjects: () => Promise; - addTaskDefinition: (opts: { projectName: string, name: string, command: string }) => Promise<{success: boolean}>; - editTaskDefinition: (opts: { projectName: string, name: string, newName: string, command: string }) => Promise<{success: boolean}>; - deleteTaskDefinition: (opts: { projectName: string, name: string }) => Promise<{success: boolean}>; - getTaskDefinitions: (projectName: string) => Promise; - addTask: (opts: { projectName: string, name: string, definitionName: string, args: string[] }) => Promise<{success: boolean}>; - editTask: (opts: { projectName: string, name: string, newName: string, definitionName: string, args: string[] }) => Promise<{success: boolean}>; - deleteTask: (opts: { projectName: string, name: string }) => Promise<{success: boolean}>; - getTasks: (projectName: string) => Promise; - addRunningTask: (opts: { projectName: string, taskName: string }) => Promise<{success: boolean}>; - getRunningTasks: () => Promise; - getRunningTask: (opts: { projectName: string, taskName: string }) => Promise; - startRunningTask: (opts: { projectName: string, taskName: string }) => Promise<{success: boolean}>; - stopRunningTask: (opts: { projectName: string, taskName: string }) => Promise<{success: boolean}>; - toggleRunningTask: (opts: { projectName: string, taskName: string }) => Promise<{success: boolean}>; + addTaskDefinition: (opts: { project_name: string, name: string }) => Promise<{success: boolean}>; + editTaskDefinition: (opts: { project_name: string, name: string, oldname: string }) => Promise<{success: boolean}>; + deleteTaskDefinition: (opts: { project_name: string, name: string }) => Promise<{success: boolean}>; + getTaskDefinitions: (project_name: string) => Promise; + addTask: (opts: { project_name: string, name: string, description: string, seconds: number }) => Promise<{success: boolean}>; + editTask: (opts: { project_name: string, name: string, description: string, seconds: number, date: string }) => Promise<{success: boolean}>; + deleteTask: (opts: { project_name: string, name: string, date: string }) => Promise<{success: boolean}>; + getTasks: (project_name: string) => Promise; + getActiveTasks: () => Promise; + startActiveTask: (opts: { project_name: string, name: string, date: string; seconds: number }) => Promise<{success: boolean}>; + stopActiveTask: (opts: { project_name: string, name: string, date: string }) => Promise<{success: boolean}>; + getDataForPDFExport: (opts: PDFQuery) => Promise; + showFileSaveDialog: () => Promise; + getPDFExport: (filepath: string) => Promise; } } diff --git a/src/index.css b/src/index.css index b632339..ffb2c4e 100644 --- a/src/index.css +++ b/src/index.css @@ -27,3 +27,7 @@ width: 320px; max-width: 100%; } + +.timeinput { + width: 84px; +} diff --git a/src/lib/Datafetcher.ts b/src/lib/Datafetcher.ts index 744d9a1..a525f59 100644 --- a/src/lib/Datafetcher.ts +++ b/src/lib/Datafetcher.ts @@ -1,24 +1,50 @@ 'use client'; -import { convertSecondsToTime } from './Time'; - const getProjects = async () => { const projects = await window.electron.getProjects(); return projects; } -const getTasks = async (projectName: string) => { - const tasks = await electron.getTasks(projectName); +const getTasks = async (project_name: string) => { + const tasks = await electron.getTasks(project_name); + return tasks; +} + +const getTasksToday = async (project_name: string) => { + const tasks = await electron.getTasksToday(project_name); return tasks; } -const getTaskDefinitions = async (projectName: string) => { - const td = await electron.getTaskDefinitions(projectName); +const getTaskDefinitions = async (project_name: string) => { + const td = await electron.getTaskDefinitions(project_name); + return td; +} + +const getAllTaskDefinitions = async () => { + const td = await electron.getAllTaskDefinitions(); return td; } +const getDataForPDFExport = async (q: PDFQuery) => { + const r = await electron.getDataForPDFExport(q); + return r; +} + +const getPDFExport = async () => { + return await electron.getPDFExport(); +} + +const showFileSaveDialogue = async () => { + await electron.showFileSaveDialogue(); +} + export const Datafetcher = { getProjects, getTaskDefinitions, getTasks, + getTasksToday, + getAllTaskDefinitions, + getDataForPDFExport, + getPDFExport, + showFileSaveDialogue, }; diff --git a/src/lib/Utils.ts b/src/lib/Utils.ts index e69de29..586dd19 100644 --- a/src/lib/Utils.ts +++ b/src/lib/Utils.ts @@ -0,0 +1,38 @@ +export const getHMSFromSeconds = (s: number) => { + const hours = Math.floor(s / 3600); + const minutes = Math.floor((s % 3600) / 60); + const seconds = s % 60; + return { hours, minutes, seconds }; +} + +export const getHMSToSeconds = (hours: number, minutes: number, seconds: number) => { + return hours * 3600 + minutes * 60 + seconds; +} + +export const getHMSStringFromSeconds = (s: number) => { + const { hours, minutes, seconds } = getHMSFromSeconds(s); + return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; +} + + +const removeActiveClassnameGeneric = (rootQuery: string, itemsQuery: string) => { + const root = document.querySelector(rootQuery); + if (root) { + const items = root.querySelectorAll(itemsQuery); + if (items) { + items.forEach((n: HTMLDivElement) => n.classList.remove('is-active')) + } + } +} + +export const removeActiveClassnameTasks = () => { + removeActiveClassnameGeneric('[data-tasks-list]', '.panel-block'); +} + +export const removeActiveClassnameTaskDefinitions = () => { + removeActiveClassnameGeneric('[data-taskdef-list]', '.panel-block'); +} + +export const removeActiveClassnameProjects = () => { + removeActiveClassnameGeneric('[data-projects-list]', '.panel-block'); +} diff --git a/src/main.ts b/src/main.ts index d4327d3..2993c54 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,7 @@ -import { app, BrowserWindow, ipcMain } from 'electron'; +import { app, BrowserWindow, dialog, ipcMain, shell } from 'electron'; +import moment from 'moment'; +import os from 'os'; +import fs from 'fs'; import path from 'path'; import { Database } from 'sqlite'; import { CountUp } from './countup'; @@ -13,11 +16,15 @@ import { editTaskDefinition, deleteTaskDefinition, getTaskDefinitions, + getAllTaskDefinitions, addTask, editTask, deleteTask, getTasks, - saveRunningTasks, + getTasksToday, + saveActiveTasks, + saveActiveTask, + getDataForPDFExport, } from './database' if (require('electron-squirrel-startup')) app.quit() @@ -27,11 +34,21 @@ if (windowsInstallerSetupEvents()) { process.exit() } +let WINDOW = null; let DB: Database; -const runningTasks: InstanceType[] = [] +const activeTasks: InstanceType[] = [] + +const getPDFExport = async (evt, filepath) => { // eslint-disable-line @typescript-eslint/no-explicit-any + const win = BrowserWindow.fromWebContents(evt.sender); + const options = {} + const pdfWriterResult = await win.webContents.printToPDF(options) + fs.writeFileSync(filepath, pdfWriterResult); + evt.sender.send('on-pdf-export-file-saved', filepath); + shell.openExternal('file://' + filepath); +} const createWindow = async () => { - const win = new BrowserWindow({ + WINDOW = new BrowserWindow({ width: 960, height: 600, webPreferences: { @@ -40,14 +57,20 @@ const createWindow = async () => { }); if (MAIN_WINDOW_VITE_DEV_SERVER_URL) { - win.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL); - win.webContents.openDevTools(); - win.maximize(); + WINDOW.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL); + WINDOW.webContents.openDevTools(); + WINDOW.maximize(); } else { - win.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`)); + WINDOW.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`)); } - win.setMenuBarVisibility(false) + WINDOW.setMenuBarVisibility(false) + + // open external links in default browser + WINDOW.webContents.setWindowOpenHandler((details) => { + shell.openExternal(details.url); + return { action: 'deny' } + }) } // Handle creating/removing shortcuts on Windows when installing/uninstalling. @@ -65,89 +88,102 @@ app.on('activate', () => { app.commandLine.appendSwitch('disable-gpu-vsync') -const getRunningTasks = (): MainProcessRunningTaskMapped[] => { - const tasks = runningTasks.map(t => { +const getActiveTasks = (): ActiveTask[] => { + const tasks = activeTasks.map(t => { return { name: t.name, - projectName: t.projectName, + project_name: t.project_name, + description: t.description, date: t.date, seconds: t.seconds, - time: t.getTime(), - isRunning: t.isRunning(), + isActive: t.isActive(), } }) return tasks } -const periodicSaveRunningTasks = async () => { - const tasks = getRunningTasks(); - saveRunningTasks(DB, tasks); +const periodicSaveActiveTasks = async () => { + const tasks = getActiveTasks(); + saveActiveTasks(DB, tasks); } -const startRunningTask = (opts: MainProcessManageRunningTasksOpts) => { - const task = getRunningTask(opts); +const startActiveTask = (opts: ActiveTask) => { + const task = getActiveTask(opts); if (!task) { - console.error('task not found', opts); + console.warn('task not found, starting new one', opts); + const addedTask = addActiveTask({ + name: opts.name, + description: opts.description, + project_name: opts.project_name, + date: opts.date, + seconds: opts.seconds, + isActive: true, + }) + addedTask.start(); + return { + success: true, + project_name: addedTask.project_name, + name: addedTask.name, + date: addedTask.date, + seconds: addedTask.seconds, + isActive: true, + }; + } + if (task.isActive()) { + console.warn('task already active', opts); + return { + success: false, + project_name: task.project_name, + name: task.name, + date: task.date, + seconds: task.seconds, + isActive: true, + }; + } else { + task.start(); + return { + success: true, + project_name: task.project_name, + name: task.name, + date: task.date, + seconds: task.seconds, + isActive: true, + }; } - task.start(); - return { - success: true, - projectName: task.projectName, - name: task.name, - date: task.date, - seconds: task.seconds, - datestring: task.getTime(), - isRunning: task.isRunning(), - }; } -const stopRunningTask = (opts: MainProcessManageRunningTasksOpts) => { - const task = getRunningTask(opts); +const stopActiveTask = (opts: ActiveTask) => { + const task = getActiveTask(opts); if (!task) { console.error('task not found', opts); + return { success: false }; } task.stop(); - return { - success: true, - projectName: task.projectName, + saveActiveTask(DB, { name: task.name, + project_name: task.project_name, date: task.date, seconds: task.seconds, - datestring: task.getTime(), - isRunning: task.isRunning(), - }; + }) + const idx = activeTasks.findIndex(t => t.name === opts.name && t.project_name === opts.project_name && t.date === opts.date) + activeTasks.splice(idx, 1) + return { success: true }; } -const toggleRunningTask = (opts: MainProcessManageRunningTasksOpts) => { - const task = getRunningTask(opts); - if (!task) { - console.error('task not found', opts); - } - if (task.isRunning) { - task.stop(); - } else { - task.start(); - } - return { - success: true, - projectName: task.projectName, +const addActiveTask = (task: ActiveTask) => { + const countup = new CountUp({ name: task.name, + project_name: task.project_name, + description: task.description, date: task.date, - seconds: task.seconds, - datestring: task.getTime(), - isRunning: task.isRunning(), - }; -} - -const addRunningTask = (task: MainProccessAddRunningTaskOpts) => { - const countup = new CountUp(task.name, task.projectName, task.date, task.seconds) - countup.start() - runningTasks.push(countup) - return { success: true } + seconds: task.seconds + }) + activeTasks.push(countup) + return countup; } -const getRunningTask = (opts: MainProcessManageRunningTasksOpts): InstanceType => { - const task = runningTasks.find(t => t.name === opts.name && t.projectName === opts.projectName && t.date === opts.date) +const getActiveTask = (opts: MainProcessManageActiveTasksOpts): InstanceType => { + const task = activeTasks.find(t => t.name === opts.name && t.project_name === opts.project_name && t.date === opts.date) if (task) { return task } @@ -156,6 +192,21 @@ const getRunningTask = (opts: MainProcessManageRunningTasksOpts): InstanceType { const ipcHandles: MainProcessIPCHandle[] = [ + { + id: 'showFileSaveDialog', + cb: async (evt: any) => { // eslint-disable-line @typescript-eslint/no-explicit-any + const datestr = moment().format('YYYY-MM-DD'); + const dialogResult = await dialog.showSaveDialog(WINDOW, { + properties: ['showOverwriteConfirmation'], + defaultPath: `timetrack.desktop-report-${datestr}.pdf`, + }); + evt.sender.send('on-pdf-export-file-selected', dialogResult); + if (dialogResult.canceled) { + return; + } + getPDFExport(evt, dialogResult.filePath); + } + }, { id: 'getProjects', cb: async () => { @@ -166,7 +217,6 @@ const setupIPCHandles = async () => { { id: 'addProject', cb: async (_: string, name: string) => { - console.log({ name }) const json = await addProject(DB, name) return json } @@ -213,6 +263,13 @@ const setupIPCHandles = async () => { return tasks } }, + { + id: 'getAllTaskDefinitions', + cb: async (_: string, name: string) => { + const res = await getAllTaskDefinitions(DB) + return res + } + }, { id: 'addTask', cb: async (_: string, opts: DBAddTaskOpts) => { @@ -242,44 +299,37 @@ const setupIPCHandles = async () => { } }, { - id: 'addRunningTask', - cb: async (_: string, opts: MainProccessAddRunningTaskOpts) => { - const json = await addRunningTask(opts) + id: 'getTasksToday', + cb: async (_: string, name: string) => { + const json = await getTasksToday(DB, name) return json } }, { - id: 'getRunningTasks', + id: 'getActiveTasks', cb: () => { - const json = getRunningTasks() - return json - } - }, - { - id: 'getRunningTask', - cb: (_: string, opts: MainProcessManageRunningTasksOpts) => { - const json = getRunningTask(opts) + const json = getActiveTasks() return json } }, { - id: 'startRunningTask', - cb: async (_: string, opts: MainProcessManageRunningTasksOpts) => { - const json = await startRunningTask(opts) + id: 'startActiveTask', + cb: async (_: string, opts: ActiveTask) => { + const json = startActiveTask(opts) return json } }, { - id: 'stopRunningTask', - cb: async (_: string, opts: MainProcessManageRunningTasksOpts) => { - const json = await stopRunningTask(opts) + id: 'stopActiveTask', + cb: async (_: string, opts: ActiveTask) => { + const json = stopActiveTask(opts) return json } }, { - id: 'toggleRunningTask', - cb: async (_: string, opts: MainProcessManageRunningTasksOpts) => { - const json = await toggleRunningTask(opts) + id: 'getDataForPDFExport', + cb: async (_: string, opts: PDFQuery) => { + const json = getDataForPDFExport(DB, opts) return json } }, @@ -295,7 +345,7 @@ const onWhenReady = async () => { await setupIPCHandles() setInterval(async() => { - await periodicSaveRunningTasks(); + await periodicSaveActiveTasks(); }, 30000) createWindow() @@ -304,7 +354,7 @@ const onWhenReady = async () => { app.whenReady().then(onWhenReady) const onWindowAllClosed = async () => { - await periodicSaveRunningTasks() + await periodicSaveActiveTasks() DB.close() if (process.platform !== 'darwin') { app.quit() diff --git a/src/preload.ts b/src/preload.ts index 8ce6487..76d3173 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -6,7 +6,15 @@ contextBridge.exposeInMainWorld('versions', { electron: () => process.versions.electron }) +type ElectronOnCallback = { + (data: any): void // eslint-disable-line @typescript-eslint/no-explicit-any +} + contextBridge.exposeInMainWorld('electron', { + on: (channel: string, callback: ElectronOnCallback) => { + ipcRenderer.on(channel, (_, data) => callback(data)); + }, + showFileSaveDialog: () => ipcRenderer.invoke('showFileSaveDialog').then(result => result), addProject: (name: string) => ipcRenderer.invoke('addProject', name).then(result => result), editProject: (opts) => ipcRenderer.invoke('editProject', opts).then(result => result), deleteProject: (name: string) => ipcRenderer.invoke('deleteProject', name).then(result => result), @@ -14,15 +22,17 @@ contextBridge.exposeInMainWorld('electron', { addTaskDefinition: (opts) => ipcRenderer.invoke('addTaskDefinition', opts).then(result => result), editTaskDefinition: (opts) => ipcRenderer.invoke('editTaskDefinition', opts).then(result => result), deleteTaskDefinition: (opts) => ipcRenderer.invoke('deleteTaskDefinition', opts).then(result => result), - getTaskDefinitions: (projectName: string) => ipcRenderer.invoke('getTaskDefinitions', projectName).then(result => result), + getTaskDefinitions: (project_name: string) => ipcRenderer.invoke('getTaskDefinitions', project_name).then(result => result), + getAllTaskDefinitions: () => ipcRenderer.invoke('getAllTaskDefinitions').then(result => result), addTask: (opts) => ipcRenderer.invoke('addTask', opts).then(result => result), editTask: (opts) => ipcRenderer.invoke('editTask', opts).then(result => result), deleteTask: (opts) => ipcRenderer.invoke('deleteTask', opts).then(result => result), - getTasks: (projectName: string) => ipcRenderer.invoke('getTasks', projectName).then(result => result), - addRunningTask: (opts) => ipcRenderer.invoke('addRunningTask', opts).then(result => result), - getRunningTasks: () => ipcRenderer.invoke('getRunningTasks').then(result => result), - getRunningTask: (opts) => ipcRenderer.invoke('getRunningTask', opts).then(result => result), - startRunningTask: (opts) => ipcRenderer.invoke('startRunningTask', opts).then(result => result), - stopRunningTask: (opts) => ipcRenderer.invoke('stopRunningTask', opts).then(result => result), - toggleRunningTask: (opts) => ipcRenderer.invoke('toggleRunningTask', opts).then(result => result), + getTasks: (project_name: string) => ipcRenderer.invoke('getTasks', project_name).then(result => result), + getTasksToday: (project_name: string) => ipcRenderer.invoke('getTasksToday', project_name).then(result => result), + getActiveTasks: () => ipcRenderer.invoke('getActiveTasks').then(result => result), + startActiveTask: (opts: ActiveTask) => ipcRenderer.invoke('startActiveTask', opts).then(result => result), + stopActiveTask: (opts: ActiveTask) => ipcRenderer.invoke('stopActiveTask', opts).then(result => result), + getDataForPDFExport: (opts: PDFQuery) => ipcRenderer.invoke('getDataForPDFExport', opts).then(result => result), + getPDFExport: (filepath: string) => ipcRenderer.invoke('getPDFExport', filepath).then(result => result), }) + diff --git a/src/renderer.ts b/src/renderer.ts deleted file mode 100644 index c452c63..0000000 --- a/src/renderer.ts +++ /dev/null @@ -1,515 +0,0 @@ -import '@fortawesome/fontawesome-free/css/all.min.css'; -import 'bulma/css/bulma.min.css'; -import './index.css'; -import './modal.ts'; - -const onRendererReady = async () => { - const electron = window.electron; - const projectList = document.querySelector('[data-project-list]'); - const taskList = document.querySelector('[data-task-list]'); - const runningTasksList = document.querySelector('[data-running-tasks-list]'); - const runningTasksContainer = document.querySelector('[data-running-tasks-container]'); - const runningTasksEmptyInfo = document.querySelector('[data-running-tasks-empty-info]'); - const projectActionsContainer = document.querySelector('[data-project-actions-container]'); - const taskActionsContainer = document.querySelector('[data-task-actions-container]'); - const editProjectModal = document.getElementById('edit-project-modal'); - const editProjectModalButtons = editProjectModal.querySelector('[data-buttons]'); - const editTaskModal = document.getElementById('edit-task-modal'); - const editTaskDefModal = document.getElementById('edit-taskdef-modal'); - const editTaskModalButtons = editTaskModal.querySelector('[data-buttons]') - const deleteProjectModal = document.querySelector('#delete-project-modal'); - const deleteProjectModalButtons = deleteProjectModal.querySelector('[data-buttons]'); - const deleteTaskModal = document.getElementById('delete-task-modal'); - const deleteTaskDefModal = document.getElementById('delete-taskdef-modal'); - const deleteTaskDefModalButtons = deleteTaskDefModal.querySelector('[data-buttons]'); - const deleteTaskModalButtons = deleteTaskModal.querySelector('[data-buttons]'); - const projectButtons = projectActionsContainer.querySelector('[data-buttons]'); - const taskButtons = taskActionsContainer.querySelector('[data-buttons]'); - const addTaskForm = document.querySelector('[data-task-form]'); - const editProjectForm = editProjectModal.querySelector('form'); - const editTaskForm = editTaskModal.querySelector('form'); - const addProjectForm = document.querySelector('[data-project-form]'); - const taskDefSection = document.querySelector('[data-taskdef-section]'); - const addTaskDefForm = document.querySelector('[data-add-taskdef-form]'); - const taskDefList = document.querySelector('[data-taskdef-list]'); - const taskDefActionsContainer = document.querySelector('[data-taskdef-actions-container]'); - const taskDefButtons = taskDefActionsContainer.querySelector('[data-buttons]'); - const taskDefTemplate = taskDefList.querySelector('[data-taskdef-item]').cloneNode(true); - taskDefList.querySelector('[data-taskdef-item]').remove(); - - let projects = []; - let projectTemplate = null; - let taskTemplate = null; - let selectedProject = null; - let selectedTask = null; - let selectedTaskDef = null; - - runningTasksList.classList.add('is-hidden'); - runningTasksEmptyInfo.classList.remove('is-hidden'); - - const showToast = (opts) => { - opts.type = opts.type || 'info'; - opts.timeout = opts.timeout * 1000 || 3000; - const tpl = `

${opts.title}

${opts.content}
` - const container = document.createElement('div') - container.classList.add('toast') - container.innerHTML = tpl - document.body.appendChild(container) - setTimeout(() => { - container.remove() - }, opts.timeout) - } - - const loadRunningTasks = async () => { - const tasks = await electron.getRunningTasks() - if (tasks.length === 0) { - runningTasksList.classList.add('is-hidden'); - runningTasksContainer.classList.add('is-hidden'); - runningTasksEmptyInfo.classList.remove('is-hidden'); - return; - } - runningTasksContainer.classList.remove('is-hidden'); - runningTasksList.classList.remove('is-hidden'); - runningTasksEmptyInfo.classList.add('is-hidden'); - runningTasksList.innerHTML = ''; - tasks.forEach(task => { - const taskItemRoot = document.createElement('tr'); - taskItemRoot.dataset.name = task.name; - taskItemRoot.dataset.projectName = task.projectName; - taskItemRoot.dataset.date = task.date; - taskItemRoot.dataset.seconds = task.seconds; - taskItemRoot.getTime = () => { - return convertSecondsToTime(taskItemRoot.dataset.seconds) - } - taskItemRoot.tickfunc = () => { - taskItemRoot.dataset.seconds = parseInt(taskItemRoot.dataset.seconds, 10) + 1; - const t = taskItemRoot.querySelectorAll('td'); - const time = taskItemRoot.getTime() - if (task.projectName !== t[0].innerHTML) t[0].innerHTML = task.projectName - if (task.name !== t[1].innerHTML) t[1].innerHTML = task.name - if (time !== t[1].innerHTML) t[2].innerHTML = time - }; - if (task.isRunning) { - taskItemRoot.dataset.tick = setInterval(taskItemRoot.tickfunc, 1000); - taskItemRoot.innerHTML = `${task.date} - -`; - } else { - taskItemRoot.innerHTML = `${task.date} - -`; - } - taskItemRoot.tickfunc() - runningTasksList.appendChild(taskItemRoot); - }) - } - - runningTasksList.addEventListener('click', async (evt) => { - evt.preventDefault(); - const target = evt.target; - if (!target.dataset.buttonAction) return; - let taskRoot; - let btn; - let res; - switch(target.dataset.buttonAction) { - case 'toggle': - taskRoot = target.closest('tr'); - if (taskRoot.dataset.tick) { - clearInterval(taskRoot.dataset.tick); - taskRoot.removeAttribute('data-tick'); - btn = taskRoot.querySelector('button'); - btn.innerHTML = 'Resume'; - btn.classList.remove('is-warning') - btn.classList.add('is-primary') - res = await electron.stopRunningTask({ - name: taskRoot.dataset.name, - projectName: taskRoot.dataset.projectName, - date: taskRoot.dataset.date - }) - } else { - taskRoot.dataset.tick = setInterval(taskRoot.tickfunc, 1000); - btn = taskRoot.querySelector('button'); - btn.innerHTML = 'Pause'; - btn.classList.remove('is-primary') - btn.classList.add('is-warning') - res = await electron.startRunningTask({ - name: taskRoot.dataset.name, - projectName: taskRoot.dataset.projectName, - date: taskRoot.dataset.date - }) - } - break; - default: - break; - } - }); - - loadRunningTasks(); - - const toggleTask = async () => { - if (!selectedTask) { - return; - } - let maybe = await electron.getRunningTask({name: selectedTask.dataset.name, projectName: selectedProject.dataset.name, date: selectedTask.dataset.date}) - if (maybe && maybe.success) { - runningTasksList.querySelectorAll('tbody tr').forEach((taskRoot) => { - if (taskRoot.tick) { - clearInterval(taskRoot.tick); - } else { - setInterval(taskRoot.tickfunc, 1000); - } - }) - } else { - maybe = await electron.addRunningTask({name: selectedTask.dataset.name, projectName: selectedProject.dataset.name, date: selectedTask.dataset.date, seconds: selectedTask.dataset.seconds}) - if (maybe && maybe.success) { - await loadRunningTasks() - } else { - showToast({ title: 'Error', content: 'Task could not be toggled', type: 'danger' }) - } - } - } - - - const deleteProject = async (projectNode) => { - if (!projectNode) { - return; - } - const projectName = projectNode.innerText; - const res = await electron.deleteProject(projectName) - if (res.success) { - projectNode.remove() - projectActionsContainer.classList.add('is-hidden'); - } - } - - const deleteTask = async (taskNode) => { - if (!taskNode) { - return; - } - const projectName = selectedProject.dataset.name; - const name = taskNode.dataset.name; - const date = taskNode.dataset.date; - const res = await electron.deleteTask({name, date, projectName}) - if (res.success) { - taskNode.remove() - projectActionsContainer.classList.add('is-hidden'); - } - } - - const deleteTaskDefinition = async (taskDefNode) => { - if (!taskDefNode) { - return; - } - const projectName = selectedProject.dataset.name; - const name = taskDefNode.dataset.name; - const res = await electron.deleteTaskDefinition({name, projectName}) - if (res.success) { - taskDefNode.remove() - taskDefActionsContainer.classList.add('is-hidden'); - } - } - - const projectListToggleActiveItem = (item) => { - projectList.querySelectorAll('[data-project-item]').forEach(i => { - i.classList.remove('is-active'); - }); - item.classList.add('is-active'); - } - - const onTaskDefItemClick = (taskItem) => { - taskDefListToggleActiveItem(taskItem); - taskDefActionsContainer.classList.remove('is-hidden'); - selectedTaskDef = taskItem; - } - - const loadTaskDefList = async () => { - taskActionsContainer.classList.add('is-hidden'); - const taskDefs = await electron.getTaskDefinitions(selectedProject.dataset.name); - taskDefList.innerHTML = ''; - taskDefs.forEach(taskDef => { - const taskDefItem = taskDefTemplate.cloneNode(true); - taskDefItem.addEventListener('click', onTaskDefItemClick.bind(null, taskDefItem)); - taskDefItem.querySelector('[data-taskdef-name]').innerHTML = taskDef.name; - taskDefItem.dataset.name = taskDef.name; - taskDefList.appendChild(taskDefItem); - }) - } - - const onProjectItemClick = async (projectItem) => { - selectedProject = projectItem; - projectListToggleActiveItem(projectItem); - const pn = projectItem.querySelector('[data-project-item-header]').innerHTML - selectedProject.dataset.name = pn; - loadTaskList(); - loadTaskSelectbox(); - loadTaskDefList(); - projectActionsContainer.classList.remove('is-hidden'); - taskDefSection.classList.remove('is-hidden'); - } - - - const taskListToggleActiveItem = (item) => { - projectList.querySelectorAll('[data-task-item]').forEach(i => { - i.classList.remove('is-active'); - }); - item.classList.add('is-active'); - } - - const taskDefListToggleActiveItem = (item) => { - taskDefList.querySelectorAll('[data-taskdef-item]').forEach(i => { - i.classList.remove('is-active'); - }); - item.classList.add('is-active'); - } - - const loadTaskList = async () => { - taskList.classList.remove('is-hidden'); - const tasks = await electron.getTasks(selectedProject.dataset.name); - if (taskTemplate === null) { - taskTemplate = document.querySelector('[data-task-item]').cloneNode(true); - document.querySelector('[data-task-item]').remove(); - } - taskList.innerHTML = ''; - tasks.forEach(task => { - const taskItem = taskTemplate.cloneNode(true); - taskItem.addEventListener('click', async () => { - selectedTask = taskItem; - taskListToggleActiveItem(taskItem); - taskActionsContainer.classList.remove('is-hidden'); - }); - taskItem.querySelector('[data-task-name').innerHTML = task.name; - taskItem.querySelector('[data-task-time').innerHTML = convertSecondsToTime(task.seconds); - taskItem.querySelector('[data-task-date').innerHTML = task.date; - taskItem.dataset.name = task.name; - taskItem.dataset.description = task.description; - taskItem.dataset.date = task.date; - taskItem.dataset.seconds = task.seconds; - taskList.appendChild(taskItem); - }) - } - await loadProjectList(); - - addProjectForm.addEventListener('submit', async (evt) => { - evt.preventDefault(); - const form = evt.target; - const name = form.querySelector('[data-project-name]').value - const res = await electron.addProject(name) - if (res.success) { - const projectItem = projectTemplate.cloneNode(true); - projectItem.addEventListener('click', onProjectItemClick.bind(null, projectItem)); - projectItem.querySelector('[data-project-item-header]').innerHTML = name; - projectList.appendChild(projectItem); - } - }); - - editProjectForm.addEventListener('submit', async (evt) => { - evt.preventDefault(); - const form = evt.target; - const oldname = form.querySelector('[data-project-oldname]').value - const name = form.querySelector('[data-project-name]').value - const res = await electron.editProject({name, oldname}) - if (res.success) { - projectList.querySelectorAll('[data-project-item]').forEach((n) => { - if (n.querySelector('[data-project-item-header]').innerHTML === oldname) { - n.querySelector('[data-project-item-header]').innerHTML = name - } - }) - closeModal(form.closest('.modal')) - } - }); - - editTaskForm.addEventListener('submit', async (evt) => { - evt.preventDefault(); - const form = evt.target; - const activeTask = taskList.querySelector('[data-task-item].is-active') - const oldname = form.querySelector('[data-task-oldname]').value - const name = form.querySelector('[data-task-name]').value - const description = form.querySelector('[data-task-description]').value - const seconds = form.querySelector('[data-task-seconds]').value - const projectName = projectList.querySelector('[data-project-item].is-active').innerText; - const date = activeTask.dataset.date; - const res = await electron.editTask({name, oldname, description, seconds, date, projectName}) - if (res.success) { - taskList.querySelectorAll('[data-task-item]').forEach((n) => { - if (n.querySelector('[data-task-name]').innerHTML === oldname) { - n.dataset.name = name - n.dataset.description = description - n.dataset.seconds = seconds - n.dataset.date = date - n.querySelector('[data-task-name]').innerHTML = name - n.querySelector('[data-task-time]').innerHTML = convertSecondsToTime(seconds) - } - }) - closeModal(form.closest('.modal')) - } - }); - - const loadTaskSelectbox = async () => { - const taskSelectbox = addTaskForm.querySelector('select'); - const tasks = await electron.getTaskDefinitions(selectedProject.dataset.name); - taskSelectbox.innerHTML = ''; - tasks.forEach(task => { - const option = document.createElement('option'); - option.value = task.name; - option.innerHTML = task.name; - taskSelectbox.appendChild(option); - }) - } - - const onTaskItemClick = (taskItem) => { - selectedTask = taskItem; - taskListToggleActiveItem(taskItem); - taskActionsContainer.classList.remove('is-hidden'); - } - - addTaskForm.addEventListener('submit', async (evt) => { - evt.preventDefault(); - const form = evt.target; - const formData = new FormData(form); - const name = formData.get('name'); - const description = formData.get('description'); - const seconds = formData.get('seconds'); - const projectName = selectedProject.dataset.name; - const res = await electron.addTask({name, description, seconds, projectName}) - if (res.success) { - const taskItem = taskTemplate.cloneNode(true); - taskItem.dataset.name = name; - taskItem.dataset.description = description; - taskItem.dataset.seconds = seconds; - taskItem.dataset.date = new Date().toISOString().split('T')[0]; - taskItem.addEventListener('click', onTaskItemClick.bind(null, taskItem)); - taskItem.querySelector('[data-task-name]').innerHTML = name; - taskItem.querySelector('[data-task-seconds]').innerHTML = seconds; - taskItem.querySelector('[data-task-date]').innerHTML = taskItem.dataset.date; - taskList.appendChild(taskItem); - } - }); - - editTaskDefModal.querySelector('form').addEventListener('submit', async (evt) => { - evt.preventDefault(); - const form = evt.target; - const formData = new FormData(form); - const name = formData.get('name'); - const oldname = formData.get('oldname'); - const projectName = selectedProject.dataset.name; - const res = await electron.editTaskDefinition({name, oldname, projectName}) - if (res.success) { - taskDefList.querySelectorAll('[data-taskdef-item]').forEach((n) => { - if (n.querySelector('[data-taskdef-name]').innerHTML === oldname) { - n.querySelector('[data-taskdef-name]').innerHTML = name - } - }) - closeModal(form.closest('.modal')) - } - }); - - addTaskDefForm.addEventListener('submit', async (evt) => { - evt.preventDefault(); - const form = evt.target; - const formData = new FormData(form); - const name = formData.get('name'); - const projectName = selectedProject.dataset.name; - const res = await electron.addTaskDefinition({name, projectName}) - if (res.success) { - const taskDefItem = taskDefTemplate.cloneNode(true); - taskDefItem.dataset.name = name; - taskDefItem.addEventListener('click', onTaskDefItemClick.bind(null, taskDefItem)); - taskDefItem.querySelector('[data-taskdef-name]').innerHTML = name; - taskDefList.appendChild(taskDefItem); - loadTaskSelectbox(); - } - }); - - - [projectButtons, taskButtons, taskDefButtons].forEach((b) => { - b.addEventListener('click', (evt) => { - evt.preventDefault(); - let dataset - let form - let formData - const t = evt.target - if (!t.dataset.actionType) { - return; - } - switch (t.dataset.actionType) { - case 'toggle-task': - toggleTask() - break; - case 'edit-project': - editProjectModal.querySelector('[data-project-name]').value = projectList.querySelector('[data-project-item].is-active').innerText - editProjectModal.querySelector('[data-project-oldname]').value = projectList.querySelector('[data-project-item].is-active').innerText - openModal(editProjectModal) - break; - case 'edit-task': - dataset = selectedTask.dataset - editTaskModal.querySelector('[data-task-oldname]').value = dataset.name - editTaskModal.querySelector('[data-task-name]').value = dataset.name - editTaskModal.querySelector('[data-task-description]').value = dataset.description - editTaskModal.querySelector('[data-task-seconds]').value = dataset.seconds - editTaskModal.querySelector('[data-task-seconds]').parentNode.querySelector('small').innerHTML = convertSecondsToTime(dataset.seconds) - openModal(editTaskModal) - break; - case 'edit-taskdef': - form = editTaskDefModal.querySelector('form') - dataset = selectedTaskDef.dataset - form.querySelector('input[name="oldname"]').value = dataset.name - form.querySelector('input[name="name"]').value = dataset.name - openModal(editTaskDefModal) - break; - case 'delete-taskdef': - openModal(deleteTaskDefModal) - break; - case 'delete-project': - openModal(deleteProjectModal) - break; - case 'delete-task': - openModal(deleteTaskModal) - break; - default: - break; - } - }) - }); - - [ - deleteProjectModalButtons, - deleteTaskModalButtons, - deleteTaskDefModalButtons, - ].forEach((b) => { - b.addEventListener('click', async(evt) => { - evt.preventDefault(); - const t = evt.target - if (!t.dataset.actionType) { - return; - } - switch (t.dataset.actionType) { - case 'delete-project': - await deleteProject(projectList.querySelector('[data-project-item].is-active')) - taskActionsContainer.classList.add('is-hidden') - taskDefSection.classList.add('is-hidden') - closeModal(deleteProjectModal) - break; - case 'delete-task': - await deleteTask(selectedTask) - taskActionsContainer.classList.add('is-hidden') - closeModal(deleteTaskModal) - break; - case 'delete-taskdef': - await deleteTaskDefinition(selectedTaskDef) - taskDefActionsContainer.classList.add('is-hidden') - closeModal(deleteTaskDefModal) - break; - default: - break; - } - }) - }); - - document.getElementById('loader').classList.add('is-hidden') - document.getElementById('content').classList.remove('is-hidden') - document.body.classList.remove('is-loading') -} - -document.addEventListener('DOMContentLoaded', async () => { - await onRendererReady() -});