├── .gitignore ├── .travis.yml ├── COPYING ├── Makefile ├── README.md ├── appinfo ├── app.php ├── info.xml └── routes.php ├── composer.json ├── composer.lock ├── css ├── all.css ├── daterangepicker.css ├── fonts │ ├── FONT-LICENSE │ ├── ICON-LICENSE │ ├── github-link │ ├── open-iconic.eot │ ├── open-iconic.otf │ ├── open-iconic.svg │ ├── open-iconic.ttf │ └── open-iconic.woff ├── kingtable.css ├── piklor.css ├── select2.css ├── select2.min.css ├── style-compat.css ├── style.css └── tabulator.css ├── img ├── app.svg ├── play-button.svg ├── stop-button.svg ├── tiny-loader.gif └── void.png ├── js ├── package-lock.json ├── package.json ├── src │ ├── clients.js │ ├── dashboard.js │ ├── dateformat.js │ ├── goals.js │ ├── kingtable.js │ ├── piklor.js │ ├── projects.js │ ├── reports.js │ ├── tags.js │ ├── timelines-admin.js │ ├── timelines.js │ └── timer.js └── webpack.config.js ├── lib ├── AppFramework │ └── Db │ │ └── OldNextcloudMapper.php ├── AppInfo │ └── Application.php ├── Controller │ ├── AjaxController.php │ ├── ClientsController.php │ ├── DashboardController.php │ ├── GoalsController.php │ ├── PageController.php │ ├── ProjectsController.php │ ├── ReportsController.php │ ├── TagsController.php │ ├── TimelinesAdminController.php │ └── TimelinesController.php ├── Db │ ├── Client.php │ ├── ClientMapper.php │ ├── Goal.php │ ├── GoalMapper.php │ ├── Project.php │ ├── ProjectMapper.php │ ├── ReportItem.php │ ├── ReportItemMapper.php │ ├── Tag.php │ ├── TagMapper.php │ ├── Timeline.php │ ├── TimelineEntry.php │ ├── TimelineEntryMapper.php │ ├── TimelineMapper.php │ ├── UserToClient.php │ ├── UserToClientMapper.php │ ├── UserToProject.php │ ├── UserToProjectMapper.php │ ├── WorkInterval.php │ ├── WorkIntervalMapper.php │ ├── WorkIntervalToTag.php │ ├── WorkIntervalToTagMapper.php │ ├── WorkItem.php │ └── WorkItemMapper.php └── Migration │ ├── Version000000Date20210719124731.php │ ├── Version000001Date20210719192031.php │ └── Version000020Date20220528101009.php ├── phpunit.integration.xml ├── phpunit.xml ├── templates ├── content │ ├── clients.php │ ├── dashboard.php │ ├── goals.php │ ├── index.php │ ├── projects.php │ ├── reports.php │ ├── tags.php │ ├── timelines-admin.php │ └── timelines.php ├── index.php ├── navigation │ └── index.php └── settings │ └── index.php ├── tests ├── Integration │ └── AppTest.php ├── Unit │ └── Controller │ │ └── PageControllerTest.php └── bootstrap.php └── webfonts ├── fa-brands-400.eot ├── fa-brands-400.svg ├── fa-brands-400.ttf ├── fa-brands-400.woff ├── fa-brands-400.woff2 ├── fa-regular-400.eot ├── fa-regular-400.svg ├── fa-regular-400.ttf ├── fa-regular-400.woff ├── fa-regular-400.woff2 ├── fa-solid-900.eot ├── fa-solid-900.svg ├── fa-solid-900.ttf ├── fa-solid-900.woff └── fa-solid-900.woff2 /.gitignore: -------------------------------------------------------------------------------- 1 | js/dist/ 2 | js/node_modules/ 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | dist: trusty 3 | language: php 4 | php: 5 | - 5.6 6 | - 7 7 | - 7.1 8 | env: 9 | global: 10 | - CORE_BRANCH=stable14 11 | matrix: 12 | - DB=pgsql 13 | 14 | matrix: 15 | allow_failures: 16 | - env: DB=pgsql CORE_BRANCH=master 17 | include: 18 | - php: 5.6 19 | env: DB=sqlite 20 | - php: 5.6 21 | env: DB=mysql 22 | - php: 5.6 23 | env: DB=pgsql CORE_BRANCH=master 24 | fast_finish: true 25 | 26 | 27 | before_install: 28 | # enable a display for running JavaScript tests 29 | - export DISPLAY=:99.0 30 | - sh -e /etc/init.d/xvfb start 31 | - nvm install 8 32 | - npm install -g npm@latest 33 | - make 34 | - make appstore 35 | # install core 36 | - cd ../ 37 | - git clone https://github.com/nextcloud/server.git --recursive --depth 1 -b $CORE_BRANCH nextcloud 38 | - mv "$TRAVIS_BUILD_DIR" nextcloud/apps/timetracker 39 | 40 | before_script: 41 | - if [[ "$DB" == 'pgsql' ]]; then createuser -U travis -s oc_autotest; fi 42 | - if [[ "$DB" == 'mysql' ]]; then mysql -u root -e 'create database oc_autotest;'; fi 43 | - if [[ "$DB" == 'mysql' ]]; then mysql -u root -e "CREATE USER 'oc_autotest'@'localhost' IDENTIFIED BY '';"; fi 44 | - if [[ "$DB" == 'mysql' ]]; then mysql -u root -e "grant all on oc_autotest.* to 'oc_autotest'@'localhost';"; fi 45 | - cd nextcloud 46 | - mkdir data 47 | - ./occ maintenance:install --database-name oc_autotest --database-user oc_autotest --admin-user admin --admin-pass admin --database $DB --database-pass='' 48 | - ./occ app:enable timetracker 49 | - php -S localhost:8080 & 50 | - cd apps/timetracker 51 | 52 | script: 53 | - make test 54 | 55 | after_failure: 56 | - cat ../../data/nextcloud.log 57 | 58 | addons: 59 | firefox: 'latest' 60 | mariadb: '10.1' 61 | 62 | services: 63 | - postgresql 64 | - mariadb 65 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # This file is licensed under the Affero General Public License version 3 or 2 | # later. See the COPYING file. 3 | # @author Bernhard Posselt 4 | # @copyright Bernhard Posselt 2016 5 | 6 | # Generic Makefile for building and packaging a Nextcloud app which uses npm and 7 | # Composer. 8 | # 9 | # Dependencies: 10 | # * make 11 | # * which 12 | # * curl: used if phpunit and composer are not installed to fetch them from the web 13 | # * tar: for building the archive 14 | # * npm: for building and testing everything JS 15 | # 16 | # If no composer.json is in the app root directory, the Composer step 17 | # will be skipped. The same goes for the package.json which can be located in 18 | # the app root or the js/ directory. 19 | # 20 | # The npm command by launches the npm build script: 21 | # 22 | # npm run build 23 | # 24 | # The npm test command launches the npm test script: 25 | # 26 | # npm run test 27 | # 28 | # The idea behind this is to be completely testing and build tool agnostic. All 29 | # build tools and additional package managers should be installed locally in 30 | # your project, since this won't pollute people's global namespace. 31 | # 32 | # The following npm scripts in your package.json install and update the bower 33 | # and npm dependencies and use gulp as build system (notice how everything is 34 | # run from the node_modules folder): 35 | # 36 | # "scripts": { 37 | # "test": "node node_modules/gulp-cli/bin/gulp.js karma", 38 | # "prebuild": "npm install && node_modules/bower/bin/bower install && node_modules/bower/bin/bower update", 39 | # "build": "node node_modules/gulp-cli/bin/gulp.js" 40 | # }, 41 | 42 | app_name=$(notdir $(CURDIR)) 43 | build_tools_directory=$(CURDIR)/build/tools 44 | source_build_directory=$(CURDIR)/build/artifacts/source 45 | source_package_name=$(source_build_directory)/$(app_name) 46 | appstore_build_directory=$(CURDIR)/build/artifacts/appstore 47 | appstore_package_name=$(appstore_build_directory)/$(app_name) 48 | npm=$(shell which npm 2> /dev/null) 49 | composer=$(shell which composer 2> /dev/null) 50 | php=$(shell which php-8.2 2> /dev/null) -dallow_url_fopen=On 51 | 52 | COMPOSER_ARGS=--prefer-dist --no-dev 53 | 54 | all: build 55 | 56 | # Fetches the PHP and JS dependencies and compiles the JS. If no composer.json 57 | # is present, the composer step is skipped, if no package.json or js/package.json 58 | # is present, the npm step is skipped 59 | .PHONY: build 60 | build: 61 | ifneq (,$(wildcard $(CURDIR)/composer.json)) 62 | gmake composer 63 | endif 64 | ifneq (,$(wildcard $(CURDIR)/package.json)) 65 | gmake npm 66 | endif 67 | ifneq (,$(wildcard $(CURDIR)/js/package.json)) 68 | gmake npm 69 | endif 70 | 71 | # Installs and updates the composer dependencies. If composer is not installed 72 | # a copy is fetched from the web 73 | .PHONY: composer 74 | composer: 75 | ifeq (, $(composer)) 76 | @echo "No composer command available, downloading a copy from the web" 77 | mkdir -p $(build_tools_directory) 78 | curl -sS https://getcomposer.org/installer | $(php) 79 | mv composer.phar $(build_tools_directory) 80 | $(php) $(build_tools_directory)/composer.phar install $(COMPOSER_ARGS) 81 | $(php) $(build_tools_directory)/composer.phar update $(COMPOSER_ARGS) 82 | else 83 | composer install $(COMPOSER_ARGS) 84 | composer update $(COMPOSER_ARGS) 85 | endif 86 | 87 | # Installs npm dependencies 88 | .PHONY: npm 89 | npm: 90 | ifeq (,$(wildcard $(CURDIR)/package.json)) 91 | cd js && $(npm) install && $(npm) run build 92 | else 93 | npm run build 94 | endif 95 | 96 | # Removes the appstore build 97 | .PHONY: clean 98 | clean: 99 | rm -rf ./build 100 | 101 | # Same as clean but also removes dependencies installed by composer, bower and 102 | # npm 103 | .PHONY: distclean 104 | distclean: clean 105 | rm -rf vendor 106 | rm -rf node_modules 107 | rm -rf js/vendor 108 | rm -rf js/node_modules 109 | 110 | # Builds the source and appstore package 111 | .PHONY: dist 112 | dist: 113 | gmake source 114 | gmake appstore 115 | 116 | # Builds the source package 117 | .PHONY: source 118 | source: 119 | rm -rf $(source_build_directory) 120 | mkdir -p $(source_build_directory) 121 | gtar cvzf $(source_package_name).tar.gz ../$(app_name) \ 122 | --exclude-vcs \ 123 | --exclude="../$(app_name)/build" \ 124 | --exclude="../$(app_name)/js/node_modules" \ 125 | --exclude="../$(app_name)/node_modules" \ 126 | --exclude="../$(app_name)/*.log" \ 127 | --exclude="../$(app_name)/js/*.log" \ 128 | 129 | # Builds the source package for the app store, ignores php and js tests 130 | .PHONY: appstore 131 | appstore: 132 | rm -rf $(appstore_build_directory) 133 | mkdir -p $(appstore_build_directory) 134 | echo $(app_name) 135 | pwd 136 | gtar \ 137 | --owner=0 --group=0 \ 138 | --exclude-vcs \ 139 | --exclude="$(app_name)/build" \ 140 | --exclude="$(app_name)/tests" \ 141 | --exclude="$(app_name)/Makefile" \ 142 | --exclude="$(app_name)/*.log" \ 143 | --exclude="$(app_name)/phpunit*xml" \ 144 | --exclude="$(app_name)/composer.*" \ 145 | --exclude="$(app_name)/js/node_modules" \ 146 | --exclude="$(app_name)/js/tests" \ 147 | --exclude="$(app_name)/js/test" \ 148 | --exclude="$(app_name)/js/*.log" \ 149 | --exclude="$(app_name)/js/package.json" \ 150 | --exclude="$(app_name)/js/bower.json" \ 151 | --exclude="$(app_name)/js/karma.*" \ 152 | --exclude="$(app_name)/js/protractor.*" \ 153 | --exclude="$(app_name)/package.json" \ 154 | --exclude="$(app_name)/bower.json" \ 155 | --exclude="$(app_name)/karma.*" \ 156 | --exclude="$(app_name)/protractor\.*" \ 157 | --exclude="$(app_name)/.*" \ 158 | --exclude="$(app_name)/.git" \ 159 | --exclude="$(app_name)/js/.*" \ 160 | -czf $(appstore_package_name).tar.gz ../$(app_name) 161 | 162 | .PHONY: test 163 | test: composer 164 | $(CURDIR)/vendor/phpunit/phpunit/phpunit -c phpunit.xml 165 | $(CURDIR)/vendor/phpunit/phpunit/phpunit -c phpunit.integration.xml 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Time Tracker 2 | Place this app in **nextcloud/apps/** 3 | 4 | ## Building the app 5 | 6 | The app can be built by using the provided Makefile by running: 7 | 8 | make 9 | 10 | This requires the following things to be present: 11 | * make 12 | * which 13 | * tar: for building the archive 14 | * curl: used if phpunit and composer are not installed to fetch them from the web 15 | * npm: for building and testing everything JS, only required if a package.json is placed inside the **js/** folder 16 | 17 | The make command will install or update Composer dependencies if a composer.json is present and also **npm run build** if a package.json is present in the **js/** folder. The npm **build** script should use local paths for build systems and package managers, so people that simply want to build the app won't need to install npm libraries globally, e.g.: 18 | 19 | **package.json**: 20 | ```json 21 | "scripts": { 22 | "test": "node node_modules/gulp-cli/bin/gulp.js karma", 23 | "prebuild": "npm install && node_modules/bower/bin/bower install && node_modules/bower/bin/bower update", 24 | "build": "node node_modules/gulp-cli/bin/gulp.js" 25 | } 26 | ``` 27 | 28 | 29 | ## Publish to App Store 30 | 31 | First get an account for the [App Store](http://apps.nextcloud.com/) then run: 32 | 33 | make && make appstore 34 | 35 | The archive is located in build/artifacts/appstore and can then be uploaded to the App Store. 36 | 37 | ## Running tests 38 | You can use the provided Makefile to run all tests by using: 39 | 40 | make test 41 | 42 | This will run the PHP unit and integration tests and if a package.json is present in the **js/** folder will execute **npm run test** 43 | 44 | Of course you can also install [PHPUnit](http://phpunit.de/getting-started.html) and use the configurations directly: 45 | 46 | phpunit -c phpunit.xml 47 | 48 | or: 49 | 50 | phpunit -c phpunit.integration.xml 51 | 52 | for integration tests 53 | -------------------------------------------------------------------------------- /appinfo/app.php: -------------------------------------------------------------------------------- 1 | 2 | 4 | timetracker 5 | Time Tracker 6 | Track your time spent with different tasks, aggregate by project or clients! 7 | Ein Zeitmesser für tägliche Aufgaben, mit Übersichten für Projekte oder Kunden! 8 | 9 | Track your time spent with different tasks each day using this time tracker app! Features include: 10 | 11 | # Adding entries 12 | With the integrated timer, you only need to press start and stop! Times will be stored automatically, with 1-second-precision. Forgot to press start? Just use the manual editor, to add entries from the past. Or edit existing ones in case you need to change something! 13 | 14 | # Projects, Clients and Tags 15 | Assign tasks to projects and clients! This allows you to display aggregated durations for each project or client, or filter for tasks by projects or clients. You need some more categorization? That's what tags are made for! Create tags to categorize tasks independently from projects or clients. 16 | 17 | # Lost track of your work time? 18 | No problem! Just use the Dashboard view to display a nice donut / pie chart of your tasks! Or filter for tasks in the Reports or Timeline view to display total or aggregated values. 19 | 20 | This app is still under development, so stay tuned for updates and additional features! If you have any suggestions, bug reports or feature requests, feel free to head over to our [GitHub project page](https://github.com/mtierltd/timetracker) and search through the existing issues, maybe your topic is already under discussion! And if it's not, feel free to file a new issue for it. 21 | 22 | And for now, start making your time tracking easier by using this app! 23 | 24 | 25 | Behalte den Überblick über den Zeitaufwand täglicher Aufgaben mit dieser Time Tracker-App! Folgende Features erwarten dich: 26 | 27 | # Einträge anlegen 28 | Ein integrierter Timer macht es Dir leicht, neue Einträge hinzuzufügen. Mit einem Klick auf Start und Stopp werden die Zeiten automatisch aufgezeichnet! Vergessen, den Startknopf zu drücken? Kein Problem! Mit dem Manuellen Editor können Einträge auch nachträglich erstellt werden. Oder Einträge bearbeitet werden, falls die Zeiten nochmal angepasst werden müssen. 29 | 30 | # Projekte, Kunden und Tags 31 | Aufgaben können Projekten und Kunden zugeordnet werden. Dadurch lässt sich später einfach herausfinden, wie viel Zeit für ein spezifisches Projekt oder einen Kunden verwendet wurden. Und wenn das noch nicht genug ist, gibt es Tags, um Aufgaben auch Projekt- und Kundenübergreifend zu kategorisieren! 32 | 33 | # Den Überblick behalten 34 | Mit dem integrierten Dashboard lässt sich anhand eines Kuchen-/Donut-Diagramms leicht ein Gesamtbild bekommen, wie viel Zeit für ein bestimmtes Projekt verwendet wurde. Mit der Berichts- oder Timeline-Ansicht lassen sich zudem Aufgaben filtern, um Zeiten zu aggregieren! 35 | 36 | Diese App wird aktuell noch weiterentwickelt, also: Augen offen halten für neue Features! Und falls Dir irgendwelche Verbesserungsvorschläge, Probleme oder neue Features einfallen, schau mal auf unserem [GitHub Projekt](https://github.com/mtierltd/timetracker) vorbei, vielleicht wird Dein Thema bereits diskutiert! Und falls nicht, starte gerne eine neue Diskussion, wir freuen uns auf Dein Feedback! 37 | 38 | 0.0.85 39 | agpl 40 | MTier Ltd. 41 | TimeTracker 42 | 43 | https://github.com/mtierltd/timetracker 44 | 45 | tools 46 | https://github.com/mtierltd/timetracker/issues 47 | https://abload.de/img/bildschirmfotoam2021-u7j37.png 48 | https://abload.de/img/bildschirmfotoam2021-m0jq9.png 49 | https://abload.de/img/bildschirmfotoam2021-6dj9p.png 50 | https://abload.de/img/bildschirmfotoam2021-otj7x.png 51 | https://abload.de/img/bildschirmfotoam2021-kykd8.png 52 | https://abload.de/img/bildschirmfotoam2021-nbk8s.png 53 | https://abload.de/img/bildschirmfotoam2021-vuk8t.png 54 | https://abload.de/img/bildschirmfotoam2021-03kxw.png 55 | 56 | 57 | 58 | 59 | 60 | Time Tracker 61 | 10 62 | timetracker.page.index 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /appinfo/routes.php: -------------------------------------------------------------------------------- 1 | OCA\TimeTracker\Controller\PageController->index() 6 | * 7 | * The controller class has to be registered in the application.php file since 8 | * it's instantiated in there 9 | */ 10 | 11 | 12 | return [ 13 | 'routes' => [ 14 | ['name' => 'page#index', 'url' => '/', 'verb' => 'GET'], 15 | ['name' => 'page#do_echo', 'url' => '/echo', 'verb' => 'POST'], 16 | ['name' => 'clients#index', 'url' => '/clients', 'verb' => 'GET'], 17 | ['name' => 'projects#index', 'url' => '/projects', 'verb' => 'GET'], 18 | ['name' => 'dashboard#index', 'url' => '/dashboard', 'verb' => 'GET'], 19 | ['name' => 'reports#index', 'url' => '/reports', 'verb' => 'GET'], 20 | ['name' => 'timelines#index', 'url' => '/timelines', 'verb' => 'GET'], 21 | 22 | ['name' => 'timelinesAdmin#index', 'url' => '/timelines-admin', 'verb' => 'GET'], 23 | 24 | ['name' => 'tags#index', 'url' => '/tags', 'verb' => 'GET'], 25 | ['name' => 'goals#index', 'url' => '/goals', 'verb' => 'GET'], 26 | 27 | ['name' => 'ajax#start_timer', 'url' => '/ajax/start-timer/{name}', 'verb' => 'POST'], 28 | ['name' => 'ajax#stop_timer', 'url' => '/ajax/stop-timer/{name}', 'verb' => 'POST'], 29 | ['name' => 'ajax#index', 'url' => '/ajax/', 'verb' => 'GET'], 30 | ['name' => 'ajax#work_intervals', 'url' => '/ajax/work-intervals', 'verb' => 'GET'], 31 | ['name' => 'ajax#update_work_interval', 'url' => '/ajax/update-work-interval/{id}', 'verb' => 'POST'], 32 | ['name' => 'ajax#add_work_interval', 'url' => '/ajax/add-work-interval/{name}', 'verb' => 'POST'], 33 | ['name' => 'ajax#delete_work_interval', 'url' => '/ajax/delete-work-interval/{id}', 'verb' => 'POST'], 34 | ['name' => 'ajax#add_cost', 'url' => '/ajax/add-cost/{id}', 'verb' => 'POST'], 35 | 36 | 37 | ['name' => 'ajax#get_clients', 'url' => '/ajax/clients', 'verb' => 'GET'], 38 | ['name' => 'ajax#add_client', 'url' => '/ajax/add-client/{name}', 'verb' => 'POST'], 39 | ['name' => 'ajax#edit_client', 'url' => '/ajax/edit-client/{id}', 'verb' => 'POST'], 40 | ['name' => 'ajax#delete_client', 'url' => '/ajax/delete-client/{id}', 'verb' => 'POST'], 41 | 42 | ['name' => 'ajax#get_projects', 'url' => '/ajax/projects', 'verb' => 'GET'], 43 | ['name' => 'ajax#get_projects_table', 'url' => '/ajax/projects-table', 'verb' => 'GET'], 44 | ['name' => 'ajax#add_project', 'url' => '/ajax/add-project/{name}', 'verb' => 'POST'], 45 | ['name' => 'ajax#edit_project', 'url' => '/ajax/edit-project/{id}', 'verb' => 'POST'], 46 | ['name' => 'ajax#delete_project', 'url' => '/ajax/delete-project/{id}', 'verb' => 'POST'], 47 | ['name' => 'ajax#delete_project_with_data', 'url' => '/ajax/delete-project-with-data/{id}', 'verb' => 'POST'], 48 | 49 | 50 | ['name' => 'ajax#get_tags', 'url' => '/ajax/tags', 'verb' => 'GET'], 51 | ['name' => 'ajax#add_tag', 'url' => '/ajax/add-tag/{name}', 'verb' => 'POST'], 52 | ['name' => 'ajax#edit_tag', 'url' => '/ajax/edit-tag/{id}', 'verb' => 'POST'], 53 | ['name' => 'ajax#delete_tag', 'url' => '/ajax/delete-tag/{id}', 'verb' => 'POST'], 54 | 55 | ['name' => 'ajax#get_goals', 'url' => '/ajax/goals', 'verb' => 'GET'], 56 | ['name' => 'ajax#add_goal', 'url' => '/ajax/add-goal', 'verb' => 'POST'], 57 | ['name' => 'ajax#delete_goal', 'url' => '/ajax/delete-goal/{id}', 'verb' => 'POST'], 58 | 59 | 60 | ['name' => 'ajax#get_report', 'url' => '/ajax/report', 'verb' => 'GET'], 61 | ['name' => 'ajax#post_timeline', 'url' => '/ajax/timeline', 'verb' => 'POST'], 62 | ['name' => 'ajax#get_timelines', 'url' => '/ajax/timelines', 'verb' => 'GET'], 63 | ['name' => 'ajax#get_timelines_admin', 'url' => '/ajax/timelines-admin', 'verb' => 'GET'], 64 | ['name' => 'ajax#download_timeline', 'url' => '/ajax/download-timeline/{id}', 'verb' => 'GET'], 65 | ['name' => 'ajax#edit_timeline', 'url' => '/ajax/edit-timeline/{id}', 'verb' => 'POST'], 66 | ['name' => 'ajax#delete_timeline', 'url' => '/ajax/delete-timeline/{id}', 'verb' => 'POST'], 67 | ['name' => 'ajax#email_timeline', 'url' => '/ajax/email-timeline/{id}', 'verb' => 'POST'], 68 | ] 69 | ]; 70 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mtierltd/timetracker", 3 | "description": "Time Tracker App", 4 | "type": "project", 5 | "license": "AGPL", 6 | "authors": [ 7 | { 8 | "name": "MTier Ltd." 9 | } 10 | ], 11 | "require": {}, 12 | "require-dev": { 13 | "christophwurst/nextcloud": "^24.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "296f87db0439fa6556d383ab1bd9038b", 8 | "packages": [], 9 | "packages-dev": [ 10 | { 11 | "name": "christophwurst/nextcloud", 12 | "version": "v24.0.1", 13 | "source": { 14 | "type": "git", 15 | "url": "https://github.com/ChristophWurst/nextcloud_composer.git", 16 | "reference": "f032acdff1502a7323f95a6524d163290f43b446" 17 | }, 18 | "dist": { 19 | "type": "zip", 20 | "url": "https://api.github.com/repos/ChristophWurst/nextcloud_composer/zipball/f032acdff1502a7323f95a6524d163290f43b446", 21 | "reference": "f032acdff1502a7323f95a6524d163290f43b446", 22 | "shasum": "" 23 | }, 24 | "require": { 25 | "php": "^7.4 || ~8.0 || ~8.1", 26 | "psr/container": "^1.1.1", 27 | "psr/event-dispatcher": "^1.0", 28 | "psr/log": "^1.1" 29 | }, 30 | "type": "library", 31 | "extra": { 32 | "branch-alias": { 33 | "dev-master": "24.0.0-dev" 34 | } 35 | }, 36 | "notification-url": "https://packagist.org/downloads/", 37 | "license": [ 38 | "AGPL-3.0-or-later" 39 | ], 40 | "authors": [ 41 | { 42 | "name": "Christoph Wurst", 43 | "email": "christoph@winzerhof-wurst.at" 44 | } 45 | ], 46 | "description": "Composer package containing Nextcloud's public API (classes, interfaces)", 47 | "support": { 48 | "issues": "https://github.com/ChristophWurst/nextcloud_composer/issues", 49 | "source": "https://github.com/ChristophWurst/nextcloud_composer/tree/v24.0.1" 50 | }, 51 | "abandoned": "nextcloud/ocp", 52 | "time": "2022-06-02T14:16:47+00:00" 53 | }, 54 | { 55 | "name": "psr/container", 56 | "version": "1.1.2", 57 | "source": { 58 | "type": "git", 59 | "url": "https://github.com/php-fig/container.git", 60 | "reference": "513e0666f7216c7459170d56df27dfcefe1689ea" 61 | }, 62 | "dist": { 63 | "type": "zip", 64 | "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea", 65 | "reference": "513e0666f7216c7459170d56df27dfcefe1689ea", 66 | "shasum": "" 67 | }, 68 | "require": { 69 | "php": ">=7.4.0" 70 | }, 71 | "type": "library", 72 | "autoload": { 73 | "psr-4": { 74 | "Psr\\Container\\": "src/" 75 | } 76 | }, 77 | "notification-url": "https://packagist.org/downloads/", 78 | "license": [ 79 | "MIT" 80 | ], 81 | "authors": [ 82 | { 83 | "name": "PHP-FIG", 84 | "homepage": "https://www.php-fig.org/" 85 | } 86 | ], 87 | "description": "Common Container Interface (PHP FIG PSR-11)", 88 | "homepage": "https://github.com/php-fig/container", 89 | "keywords": [ 90 | "PSR-11", 91 | "container", 92 | "container-interface", 93 | "container-interop", 94 | "psr" 95 | ], 96 | "support": { 97 | "issues": "https://github.com/php-fig/container/issues", 98 | "source": "https://github.com/php-fig/container/tree/1.1.2" 99 | }, 100 | "time": "2021-11-05T16:50:12+00:00" 101 | }, 102 | { 103 | "name": "psr/event-dispatcher", 104 | "version": "1.0.0", 105 | "source": { 106 | "type": "git", 107 | "url": "https://github.com/php-fig/event-dispatcher.git", 108 | "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" 109 | }, 110 | "dist": { 111 | "type": "zip", 112 | "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", 113 | "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", 114 | "shasum": "" 115 | }, 116 | "require": { 117 | "php": ">=7.2.0" 118 | }, 119 | "type": "library", 120 | "extra": { 121 | "branch-alias": { 122 | "dev-master": "1.0.x-dev" 123 | } 124 | }, 125 | "autoload": { 126 | "psr-4": { 127 | "Psr\\EventDispatcher\\": "src/" 128 | } 129 | }, 130 | "notification-url": "https://packagist.org/downloads/", 131 | "license": [ 132 | "MIT" 133 | ], 134 | "authors": [ 135 | { 136 | "name": "PHP-FIG", 137 | "homepage": "http://www.php-fig.org/" 138 | } 139 | ], 140 | "description": "Standard interfaces for event handling.", 141 | "keywords": [ 142 | "events", 143 | "psr", 144 | "psr-14" 145 | ], 146 | "support": { 147 | "issues": "https://github.com/php-fig/event-dispatcher/issues", 148 | "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" 149 | }, 150 | "time": "2019-01-08T18:20:26+00:00" 151 | }, 152 | { 153 | "name": "psr/log", 154 | "version": "1.1.4", 155 | "source": { 156 | "type": "git", 157 | "url": "https://github.com/php-fig/log.git", 158 | "reference": "d49695b909c3b7628b6289db5479a1c204601f11" 159 | }, 160 | "dist": { 161 | "type": "zip", 162 | "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", 163 | "reference": "d49695b909c3b7628b6289db5479a1c204601f11", 164 | "shasum": "" 165 | }, 166 | "require": { 167 | "php": ">=5.3.0" 168 | }, 169 | "type": "library", 170 | "extra": { 171 | "branch-alias": { 172 | "dev-master": "1.1.x-dev" 173 | } 174 | }, 175 | "autoload": { 176 | "psr-4": { 177 | "Psr\\Log\\": "Psr/Log/" 178 | } 179 | }, 180 | "notification-url": "https://packagist.org/downloads/", 181 | "license": [ 182 | "MIT" 183 | ], 184 | "authors": [ 185 | { 186 | "name": "PHP-FIG", 187 | "homepage": "https://www.php-fig.org/" 188 | } 189 | ], 190 | "description": "Common interface for logging libraries", 191 | "homepage": "https://github.com/php-fig/log", 192 | "keywords": [ 193 | "log", 194 | "psr", 195 | "psr-3" 196 | ], 197 | "support": { 198 | "source": "https://github.com/php-fig/log/tree/1.1.4" 199 | }, 200 | "time": "2021-05-03T11:20:27+00:00" 201 | } 202 | ], 203 | "aliases": [], 204 | "minimum-stability": "stable", 205 | "stability-flags": [], 206 | "prefer-stable": false, 207 | "prefer-lowest": false, 208 | "platform": [], 209 | "platform-dev": [], 210 | "plugin-api-version": "2.3.0" 211 | } 212 | -------------------------------------------------------------------------------- /css/daterangepicker.css: -------------------------------------------------------------------------------- 1 | .daterangepicker { 2 | position: absolute; 3 | color: inherit; 4 | background-color: #fff; 5 | border-radius: 4px; 6 | border: 1px solid #ddd; 7 | width: 278px; 8 | max-width: none; 9 | padding: 0; 10 | margin-top: 7px; 11 | top: 100px; 12 | left: 20px; 13 | z-index: 3001; 14 | display: none; 15 | font-family: arial; 16 | font-size: 15px; 17 | line-height: 1em; 18 | } 19 | 20 | .daterangepicker:before, .daterangepicker:after { 21 | position: absolute; 22 | display: inline-block; 23 | border-bottom-color: rgba(0, 0, 0, 0.2); 24 | content: ''; 25 | } 26 | 27 | .daterangepicker:before { 28 | top: -7px; 29 | border-right: 7px solid transparent; 30 | border-left: 7px solid transparent; 31 | border-bottom: 7px solid #ccc; 32 | } 33 | 34 | .daterangepicker:after { 35 | top: -6px; 36 | border-right: 6px solid transparent; 37 | border-bottom: 6px solid #fff; 38 | border-left: 6px solid transparent; 39 | } 40 | 41 | .daterangepicker.opensleft:before { 42 | right: 9px; 43 | } 44 | 45 | .daterangepicker.opensleft:after { 46 | right: 10px; 47 | } 48 | 49 | .daterangepicker.openscenter:before { 50 | left: 0; 51 | right: 0; 52 | width: 0; 53 | margin-left: auto; 54 | margin-right: auto; 55 | } 56 | 57 | .daterangepicker.openscenter:after { 58 | left: 0; 59 | right: 0; 60 | width: 0; 61 | margin-left: auto; 62 | margin-right: auto; 63 | } 64 | 65 | .daterangepicker.opensright:before { 66 | left: 9px; 67 | } 68 | 69 | .daterangepicker.opensright:after { 70 | left: 10px; 71 | } 72 | 73 | .daterangepicker.drop-up { 74 | margin-top: -7px; 75 | } 76 | 77 | .daterangepicker.drop-up:before { 78 | top: initial; 79 | bottom: -7px; 80 | border-bottom: initial; 81 | border-top: 7px solid #ccc; 82 | } 83 | 84 | .daterangepicker.drop-up:after { 85 | top: initial; 86 | bottom: -6px; 87 | border-bottom: initial; 88 | border-top: 6px solid #fff; 89 | } 90 | 91 | .daterangepicker.single .daterangepicker .ranges, .daterangepicker.single .drp-calendar { 92 | float: none; 93 | } 94 | 95 | .daterangepicker.single .drp-selected { 96 | display: none; 97 | } 98 | 99 | .daterangepicker.show-calendar .drp-calendar { 100 | display: block; 101 | } 102 | 103 | .daterangepicker.show-calendar .drp-buttons { 104 | display: block; 105 | } 106 | 107 | .daterangepicker.auto-apply .drp-buttons { 108 | display: none; 109 | } 110 | 111 | .daterangepicker .drp-calendar { 112 | display: none; 113 | max-width: 270px; 114 | } 115 | 116 | .daterangepicker .drp-calendar.left { 117 | padding: 8px 0 8px 8px; 118 | } 119 | 120 | .daterangepicker .drp-calendar.right { 121 | padding: 8px; 122 | } 123 | 124 | .daterangepicker .drp-calendar.single .calendar-table { 125 | border: none; 126 | } 127 | 128 | .daterangepicker .calendar-table .next span, .daterangepicker .calendar-table .prev span { 129 | color: #fff; 130 | border: solid black; 131 | border-width: 0 2px 2px 0; 132 | border-radius: 0; 133 | display: inline-block; 134 | padding: 3px; 135 | } 136 | 137 | .daterangepicker .calendar-table .next span { 138 | transform: rotate(-45deg); 139 | -webkit-transform: rotate(-45deg); 140 | } 141 | 142 | .daterangepicker .calendar-table .prev span { 143 | transform: rotate(135deg); 144 | -webkit-transform: rotate(135deg); 145 | } 146 | 147 | .daterangepicker .calendar-table th, .daterangepicker .calendar-table td { 148 | white-space: nowrap; 149 | text-align: center; 150 | vertical-align: middle; 151 | min-width: 32px; 152 | width: 32px; 153 | height: 24px; 154 | line-height: 24px; 155 | font-size: 12px; 156 | border-radius: 4px; 157 | border: 1px solid transparent; 158 | white-space: nowrap; 159 | cursor: pointer; 160 | } 161 | 162 | .daterangepicker .calendar-table { 163 | border: 1px solid #fff; 164 | border-radius: 4px; 165 | background-color: #fff; 166 | } 167 | 168 | .daterangepicker .calendar-table table { 169 | width: 100%; 170 | margin: 0; 171 | border-spacing: 0; 172 | border-collapse: collapse; 173 | } 174 | 175 | .daterangepicker td.available:hover, .daterangepicker th.available:hover { 176 | background-color: #eee; 177 | border-color: transparent; 178 | color: inherit; 179 | } 180 | 181 | .daterangepicker td.week, .daterangepicker th.week { 182 | font-size: 80%; 183 | color: #ccc; 184 | } 185 | 186 | .daterangepicker td.off, .daterangepicker td.off.in-range, .daterangepicker td.off.start-date, .daterangepicker td.off.end-date { 187 | background-color: #fff; 188 | border-color: transparent; 189 | color: #999; 190 | } 191 | 192 | .daterangepicker td.in-range { 193 | background-color: #ebf4f8; 194 | border-color: transparent; 195 | color: #000; 196 | border-radius: 0; 197 | } 198 | 199 | .daterangepicker td.start-date { 200 | border-radius: 4px 0 0 4px; 201 | } 202 | 203 | .daterangepicker td.end-date { 204 | border-radius: 0 4px 4px 0; 205 | } 206 | 207 | .daterangepicker td.start-date.end-date { 208 | border-radius: 4px; 209 | } 210 | 211 | .daterangepicker td.active, .daterangepicker td.active:hover { 212 | background-color: #357ebd; 213 | border-color: transparent; 214 | color: #fff; 215 | } 216 | 217 | .daterangepicker th.month { 218 | width: auto; 219 | } 220 | 221 | .daterangepicker td.disabled, .daterangepicker option.disabled { 222 | color: #999; 223 | cursor: not-allowed; 224 | text-decoration: line-through; 225 | } 226 | 227 | .daterangepicker select.monthselect, .daterangepicker select.yearselect { 228 | font-size: 12px; 229 | padding: 1px; 230 | height: auto; 231 | margin: 0; 232 | cursor: default; 233 | } 234 | 235 | .daterangepicker select.monthselect { 236 | margin-right: 2%; 237 | width: 56%; 238 | } 239 | 240 | .daterangepicker select.yearselect { 241 | width: 40%; 242 | } 243 | 244 | .daterangepicker select.hourselect, .daterangepicker select.minuteselect, .daterangepicker select.secondselect, .daterangepicker select.ampmselect { 245 | width: 50px; 246 | margin: 0 auto; 247 | background: #eee; 248 | border: 1px solid #eee; 249 | padding: 2px; 250 | outline: 0; 251 | font-size: 12px; 252 | } 253 | 254 | .daterangepicker .calendar-time { 255 | text-align: center; 256 | margin: 4px auto 0 auto; 257 | line-height: 30px; 258 | position: relative; 259 | } 260 | 261 | .daterangepicker .calendar-time select.disabled { 262 | color: #ccc; 263 | cursor: not-allowed; 264 | } 265 | 266 | .daterangepicker .drp-buttons { 267 | clear: both; 268 | text-align: right; 269 | padding: 8px; 270 | border-top: 1px solid #ddd; 271 | display: none; 272 | line-height: 12px; 273 | vertical-align: middle; 274 | } 275 | 276 | .daterangepicker .drp-selected { 277 | display: inline-block; 278 | font-size: 12px; 279 | padding-right: 8px; 280 | } 281 | 282 | .daterangepicker .drp-buttons .btn { 283 | margin-left: 8px; 284 | font-size: 12px; 285 | font-weight: bold; 286 | padding: 4px 8px; 287 | } 288 | 289 | .daterangepicker.show-ranges .drp-calendar.left { 290 | border-left: 1px solid #ddd; 291 | } 292 | 293 | .daterangepicker .ranges { 294 | float: none; 295 | text-align: left; 296 | margin: 0; 297 | } 298 | 299 | .daterangepicker.show-calendar .ranges { 300 | margin-top: 8px; 301 | } 302 | 303 | .daterangepicker .ranges ul { 304 | list-style: none; 305 | margin: 0 auto; 306 | padding: 0; 307 | width: 100%; 308 | } 309 | 310 | .daterangepicker .ranges li { 311 | font-size: 12px; 312 | padding: 8px 12px; 313 | cursor: pointer; 314 | } 315 | 316 | .daterangepicker .ranges li:hover { 317 | background-color: #eee; 318 | } 319 | 320 | .daterangepicker .ranges li.active { 321 | background-color: #08c; 322 | color: #fff; 323 | } 324 | 325 | /* Larger Screen Styling */ 326 | @media (min-width: 564px) { 327 | .daterangepicker { 328 | width: auto; } 329 | .daterangepicker .ranges ul { 330 | width: 140px; } 331 | .daterangepicker.single .ranges ul { 332 | width: 100%; } 333 | .daterangepicker.single .drp-calendar.left { 334 | clear: none; } 335 | .daterangepicker.single.ltr .ranges, .daterangepicker.single.ltr .drp-calendar { 336 | float: left; } 337 | .daterangepicker.single.rtl .ranges, .daterangepicker.single.rtl .drp-calendar { 338 | float: right; } 339 | .daterangepicker.ltr { 340 | direction: ltr; 341 | text-align: left; } 342 | .daterangepicker.ltr .drp-calendar.left { 343 | clear: left; 344 | margin-right: 0; } 345 | .daterangepicker.ltr .drp-calendar.left .calendar-table { 346 | border-right: none; 347 | border-top-right-radius: 0; 348 | border-bottom-right-radius: 0; } 349 | .daterangepicker.ltr .drp-calendar.right { 350 | margin-left: 0; } 351 | .daterangepicker.ltr .drp-calendar.right .calendar-table { 352 | border-left: none; 353 | border-top-left-radius: 0; 354 | border-bottom-left-radius: 0; } 355 | .daterangepicker.ltr .drp-calendar.left .calendar-table { 356 | padding-right: 8px; } 357 | .daterangepicker.ltr .ranges, .daterangepicker.ltr .drp-calendar { 358 | float: left; } 359 | .daterangepicker.rtl { 360 | direction: rtl; 361 | text-align: right; } 362 | .daterangepicker.rtl .drp-calendar.left { 363 | clear: right; 364 | margin-left: 0; } 365 | .daterangepicker.rtl .drp-calendar.left .calendar-table { 366 | border-left: none; 367 | border-top-left-radius: 0; 368 | border-bottom-left-radius: 0; } 369 | .daterangepicker.rtl .drp-calendar.right { 370 | margin-right: 0; } 371 | .daterangepicker.rtl .drp-calendar.right .calendar-table { 372 | border-right: none; 373 | border-top-right-radius: 0; 374 | border-bottom-right-radius: 0; } 375 | .daterangepicker.rtl .drp-calendar.left .calendar-table { 376 | padding-left: 12px; } 377 | .daterangepicker.rtl .ranges, .daterangepicker.rtl .drp-calendar { 378 | text-align: right; 379 | float: right; } } 380 | @media (min-width: 730px) { 381 | .daterangepicker .ranges { 382 | width: auto; } 383 | .daterangepicker.ltr .ranges { 384 | float: left; } 385 | .daterangepicker.rtl .ranges { 386 | float: right; } 387 | .daterangepicker .drp-calendar.left { 388 | clear: none !important; } } 389 | .daterangepicker { 390 | background-color: var(--color-main-background); 391 | } 392 | .daterangepicker .calendar-table { 393 | border: 1px solid var(--color-main-background); 394 | border-radius: 4px; 395 | background-color: var(--color-main-background); 396 | } 397 | .daterangepicker select.hourselect, .daterangepicker select.minuteselect, .daterangepicker select.secondselect, .daterangepicker select.ampmselect { 398 | width: 50px; 399 | margin: 0 auto; 400 | background:var(--color-background-dark); 401 | border: 1px solid var(--color-background-dark); 402 | padding: 2px; 403 | outline: 0; 404 | font-size: 12px; 405 | } 406 | 407 | .daterangepicker td.off, .daterangepicker td.off.in-range, .daterangepicker td.off.start-date, .daterangepicker td.off.end-date { 408 | background-color:var(--color-main-background); 409 | border-color: transparent; 410 | color: var(--color-background-darker); 411 | } 412 | 413 | .daterangepicker td.available:hover, .daterangepicker th.available:hover { 414 | background-color: var(--color-background-darker); 415 | border-color: transparent; 416 | color: inherit; 417 | } 418 | .daterangepicker .ranges li:hover { 419 | background-color: var(--color-background-darker); 420 | } 421 | 422 | -------------------------------------------------------------------------------- /css/fonts/FONT-LICENSE: -------------------------------------------------------------------------------- 1 | SIL OPEN FONT LICENSE Version 1.1 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | PREAMBLE 6 | The goals of the Open Font License (OFL) are to stimulate worldwide 7 | development of collaborative font projects, to support the font creation 8 | efforts of academic and linguistic communities, and to provide a free and 9 | open framework in which fonts may be shared and improved in partnership 10 | with others. 11 | 12 | The OFL allows the licensed fonts to be used, studied, modified and 13 | redistributed freely as long as they are not sold by themselves. The 14 | fonts, including any derivative works, can be bundled, embedded, 15 | redistributed and/or sold with any software provided that any reserved 16 | names are not used by derivative works. The fonts and derivatives, 17 | however, cannot be released under any other type of license. The 18 | requirement for fonts to remain under this license does not apply 19 | to any document created using the fonts or their derivatives. 20 | 21 | DEFINITIONS 22 | "Font Software" refers to the set of files released by the Copyright 23 | Holder(s) under this license and clearly marked as such. This may 24 | include source files, build scripts and documentation. 25 | 26 | "Reserved Font Name" refers to any names specified as such after the 27 | copyright statement(s). 28 | 29 | "Original Version" refers to the collection of Font Software components as 30 | distributed by the Copyright Holder(s). 31 | 32 | "Modified Version" refers to any derivative made by adding to, deleting, 33 | or substituting -- in part or in whole -- any of the components of the 34 | Original Version, by changing formats or by porting the Font Software to a 35 | new environment. 36 | 37 | "Author" refers to any designer, engineer, programmer, technical 38 | writer or other person who contributed to the Font Software. 39 | 40 | PERMISSION & CONDITIONS 41 | Permission is hereby granted, free of charge, to any person obtaining 42 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 43 | redistribute, and sell modified and unmodified copies of the Font 44 | Software, subject to the following conditions: 45 | 46 | 1) Neither the Font Software nor any of its individual components, 47 | in Original or Modified Versions, may be sold by itself. 48 | 49 | 2) Original or Modified Versions of the Font Software may be bundled, 50 | redistributed and/or sold with any software, provided that each copy 51 | contains the above copyright notice and this license. These can be 52 | included either as stand-alone text files, human-readable headers or 53 | in the appropriate machine-readable metadata fields within text or 54 | binary files as long as those fields can be easily viewed by the user. 55 | 56 | 3) No Modified Version of the Font Software may use the Reserved Font 57 | Name(s) unless explicit written permission is granted by the corresponding 58 | Copyright Holder. This restriction only applies to the primary font name as 59 | presented to the users. 60 | 61 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 62 | Software shall not be used to promote, endorse or advertise any 63 | Modified Version, except to acknowledge the contribution(s) of the 64 | Copyright Holder(s) and the Author(s) or with their explicit written 65 | permission. 66 | 67 | 5) The Font Software, modified or unmodified, in part or in whole, 68 | must be distributed entirely under this license, and must not be 69 | distributed under any other license. The requirement for fonts to 70 | remain under this license does not apply to any document created 71 | using the Font Software. 72 | 73 | TERMINATION 74 | This license becomes null and void if any of the above conditions are 75 | not met. 76 | 77 | DISCLAIMER 78 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 79 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 80 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 81 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 82 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 83 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 84 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 85 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 86 | OTHER DEALINGS IN THE FONT SOFTWARE. 87 | -------------------------------------------------------------------------------- /css/fonts/ICON-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /css/fonts/github-link: -------------------------------------------------------------------------------- 1 | https://github.com/iconic/open-iconic -------------------------------------------------------------------------------- /css/fonts/open-iconic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtierltd/timetracker/abc60274f89a84a6be32cc2342ef7069d149d306/css/fonts/open-iconic.eot -------------------------------------------------------------------------------- /css/fonts/open-iconic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtierltd/timetracker/abc60274f89a84a6be32cc2342ef7069d149d306/css/fonts/open-iconic.otf -------------------------------------------------------------------------------- /css/fonts/open-iconic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtierltd/timetracker/abc60274f89a84a6be32cc2342ef7069d149d306/css/fonts/open-iconic.ttf -------------------------------------------------------------------------------- /css/fonts/open-iconic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtierltd/timetracker/abc60274f89a84a6be32cc2342ef7069d149d306/css/fonts/open-iconic.woff -------------------------------------------------------------------------------- /css/piklor.css: -------------------------------------------------------------------------------- 1 | /* 2 | header, footer { 3 | color: white; 4 | background: #303F9F; 5 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.51); 6 | } 7 | 8 | h1.title { 9 | font-size: 4em; 10 | font-weight: bold; 11 | } 12 | 13 | pre { 14 | background: #212121; 15 | color: white; 16 | } 17 | 18 | header p { 19 | font-size: 1.4em; 20 | } 21 | 22 | footer { 23 | padding: 50px 0; 24 | text-align: center; 25 | font-size: 1.1em; 26 | margin-top: 3em; 27 | } 28 | 29 | footer a { 30 | color: white; 31 | } 32 | 33 | footer a:hover { 34 | color: white; 35 | } 36 | */ 37 | /* picker */ 38 | .color-picker { 39 | background: rgba(255, 255, 255, 0.75); 40 | padding: 10px; 41 | border: 1px solid rgba(203, 203, 203, 0.6); 42 | border-radius: 2px; 43 | position: absolute; 44 | margin-left: -120px; 45 | width: 300px; 46 | z-index: 10; 47 | margin-top: 35px; 48 | } 49 | 50 | .color-picker > div { 51 | width: 20px; 52 | display: inline-block; 53 | height: 20px; 54 | margin: 3px; 55 | border-radius: 100%; 56 | opacity: 0.7; 57 | 58 | } 59 | 60 | .picker-wrapper { 61 | padding: 20px; 62 | display: inline; 63 | } 64 | 65 | .color-picker > div:hover { 66 | opacity: 1; 67 | } -------------------------------------------------------------------------------- /css/style-compat.css: -------------------------------------------------------------------------------- 1 | #app-navigation:not(.vue) > ul { 2 | position: relative; 3 | height: 100%; 4 | width: 100%; 5 | overflow-x: hidden; 6 | overflow-y: auto; 7 | box-sizing: border-box; 8 | display: flex; 9 | flex-direction: column; 10 | padding: calc(var(--default-grid-baseline) * 2); 11 | } 12 | 13 | #app-navigation:not(.vue) > ul > li, 14 | #app-navigation:not(.vue) > ul > li > ul > li { 15 | position: relative; 16 | box-sizing: border-box; 17 | } 18 | 19 | #app-navigation:not(.vue) > ul > li { 20 | display: inline-flex; 21 | flex-wrap: wrap; 22 | order: 1; 23 | flex-shrink: 0; 24 | margin: 0; 25 | margin-bottom: 3px; 26 | width: 100%; 27 | border-radius: var(--border-radius-pill); 28 | } 29 | 30 | #app-navigation:not(.vue) > ul > li.active, 31 | #app-navigation:not(.vue) > ul > li.active > a, 32 | #app-navigation:not(.vue) > ul > li a:active, 33 | #app-navigation:not(.vue) > ul > li a:active > a, 34 | #app-navigation:not(.vue) > ul > li a.selected, 35 | #app-navigation:not(.vue) > ul > li a.selected > a, 36 | #app-navigation:not(.vue) > ul > li a.active, 37 | #app-navigation:not(.vue) > ul > li a.active > a { 38 | background-color: var(--color-primary-light); 39 | } 40 | 41 | #app-navigation:not(.vue) > ul > li > a, 42 | #app-navigation:not(.vue) > ul > li > ul > li > a { 43 | background-size: 16px 16px; 44 | background-position: 14px center; 45 | background-repeat: no-repeat; 46 | display: block; 47 | justify-content: space-between; 48 | line-height: 44px; 49 | min-height: 44px; 50 | padding: 0 12px 0 14px; 51 | overflow: hidden; 52 | box-sizing: border-box; 53 | white-space: nowrap; 54 | text-overflow: ellipsis; 55 | border-radius: var(--border-radius-pill); 56 | color: var(--color-main-text); 57 | flex: 1 1 0px; 58 | z-index: 100; 59 | } 60 | 61 | #app-navigation:not(.vue) > ul > li a:hover, 62 | #app-navigation:not(.vue) > ul > li a:hover > a, 63 | #app-navigation:not(.vue) > ul > li a:focus, 64 | #app-navigation:not(.vue) > ul > li a:focus > a { 65 | background-color: var(--color-background-hover); 66 | } 67 | 68 | #app-navigation:not(.vue) > ul > li > a:first-child img, 69 | #app-navigation:not(.vue) > ul > li > ul > li > a:first-child img { 70 | margin-right: 11px; 71 | width: 16px; 72 | height: 16px; 73 | filter: var(--background-invert-if-dark); 74 | } 75 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | #hello { 2 | color: red; 3 | } 4 | #work-input { 5 | width: 100%; 6 | /* flex: 1 1 auto; */ 7 | height: 65px; 8 | flex: 1 1 auto; 9 | font-size: 22px; 10 | padding: 0px 5px 0px 20px; 11 | } 12 | #work-input-container { 13 | width: 100%; 14 | flex: 1 1 auto; 15 | } 16 | #top-work-bar { 17 | width: 100%; 18 | display: flex; 19 | flex-direction: row; 20 | } 21 | #top-work-bar-right { 22 | /* width: 100%; */ 23 | /* background-color: aqua; */ 24 | /* min-width:240px; */ 25 | display: flex; 26 | } 27 | 28 | #app { 29 | width: 100%; 30 | } 31 | 32 | #app-content-wrapper { 33 | width: 100%; 34 | display:block; 35 | } 36 | 37 | 38 | .play-button { 39 | 40 | background-image: url(../img/play-button.svg); 41 | background-position: 0px 0px; 42 | } 43 | .stop-button { 44 | background-image: url(../img/stop-button.svg); 45 | background-position: 0px 0px; 46 | } 47 | .clearfix:after { content: "\00A0"; display: block; clear: both; visibility: hidden; line-height: 0; height: 0;} 48 | .clearfix{ display: inline-block;} 49 | html[xmlns] .clearfix { display: block;} 50 | * html .clearfix{ height: 1%;} 51 | .clearfix {display: block} 52 | 53 | #timetracker-content { 54 | width: 100%; 55 | display:block; 56 | /*background-color: rgb(250, 251, 252);*/ 57 | } 58 | #timer { 59 | /* min-width: 200px; */ 60 | height: 40px; 61 | font-size: 22px; 62 | padding: 25px; 63 | text-align:right; 64 | } 65 | .day-work-intervals{ 66 | 67 | margin-bottom: 30px; 68 | box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1); 69 | 70 | /*background-color: white;*/ 71 | /*min-width: 100%;*/ 72 | } 73 | .day-list-item{ 74 | 75 | } 76 | .day-name { 77 | 78 | font-size: 18px; 79 | font-weight: 400; 80 | padding: 10px; 81 | } 82 | .wi-child { 83 | /*background-color: rgb(250, 251, 252);*/ 84 | background-color: var(--color-background-dark); 85 | padding-left: 25px; 86 | padding-right: 25px; 87 | } 88 | .wi-child-element { 89 | display: flex; 90 | border-bottom: #eee solid 1px; 91 | flex-direction:row; 92 | } 93 | .wi-child-duration { 94 | padding: 10px; 95 | min-width: 150px; 96 | text-align: right; 97 | } 98 | .wi-child-name { 99 | padding: 10px; 100 | width: 100%; 101 | } 102 | .wi-child-hours { 103 | /* font-size: 18px; 104 | font-weight: 400; */ 105 | padding: 10px; 106 | min-width: 150px; 107 | cursor: pointer; 108 | } 109 | 110 | .work-item { 111 | 112 | } 113 | .work-item-element { 114 | display: flex; 115 | flex-direction: row; 116 | box-shadow: inset 0 -1px 0 0 #e8e8e8; 117 | /*background-color: white;*/ 118 | line-height: 60px; 119 | padding-right: 25px; 120 | } 121 | .wi-len { 122 | min-width: 20px; 123 | padding-left: 5px; 124 | border-color: #aaa; 125 | border-width: 1px; 126 | border-style: solid; 127 | line-height: 28px; 128 | margin: 15px; 129 | border-radius: 15px; 130 | } 131 | .wi-len-empty { 132 | min-width: 20px; 133 | padding-left: 5px; 134 | margin: 15px; 135 | line-height: 28px; 136 | } 137 | .wi-name { 138 | width: 100%; 139 | font-size: 16px; 140 | } 141 | .wi-duration { 142 | font-size: 20px; 143 | min-width: 150px; 144 | text-align: right; 145 | } 146 | .nav-icon-recent { 147 | background-image: var(--icon-files-recent-000); 148 | } 149 | .controls { 150 | display: flex; 151 | align-items: center; 152 | } 153 | .client-button { 154 | display:flex; 155 | color: #595959; 156 | /*background-color: #e6e6e6;*/ 157 | font-size: 11px; 158 | padding: 7px 10px; 159 | margin: 0 12px 12px 0; 160 | } 161 | 162 | .client-edit{ 163 | padding:0px 10px; 164 | } 165 | .client-delete{ 166 | padding:0px 10px; 167 | } 168 | 169 | .clickable:hover{ 170 | color: var(--color-main-text); 171 | opacity: 1; 172 | } 173 | .clickable { 174 | cursor: pointer; 175 | color: var(--color-main-text); 176 | opacity: 0.57; 177 | } 178 | .ui-dialog { z-index: 1000 !important ;} 179 | #timetracker-clients { 180 | width: 100%; 181 | /* display:block; */ 182 | /*background-color: rgb(250, 251, 252);*/ 183 | display: inline-block; 184 | margin: 40px; 185 | padding: 20px; 186 | } 187 | .client-button{ 188 | width: min-content; 189 | } 190 | .clients-list{ 191 | display: flex; 192 | margin: 40px; 193 | flex-wrap: wrap; 194 | } 195 | #timetracker-projects{ 196 | width: 100%; 197 | /* display:block; */ 198 | /*background-color: rgb(250, 251, 252);*/ 199 | display: inline-block; 200 | margin: 40px; 201 | padding: 20px; 202 | } 203 | 204 | .project-button{ 205 | display:flex; 206 | } 207 | 208 | .project-name{ 209 | flex:0.3; 210 | } 211 | button.redButton { 212 | background-color: red; 213 | } 214 | .king-table-region .pagination-bar { 215 | height: 50px; 216 | margin: 20px 0px 20px 0px; 217 | padding: 10px; 218 | text-align: right; 219 | /*background-color: #eee;*/ 220 | } 221 | .projects{ 222 | margin-top: 50px; 223 | } 224 | .king-table-head { 225 | color: #aaa; 226 | } 227 | td.deleteBtn { 228 | width: 10px; 229 | } 230 | td.ε_row { 231 | width: 40px; 232 | } 233 | #client-select{ 234 | 235 | } 236 | .select2-container { 237 | margin: 0px 3px 3px 0; 238 | 239 | 240 | } 241 | .select2-container .select2-choice { 242 | min-height: 33px; 243 | height: 33px; 244 | margin-top:0px; 245 | } 246 | .page-title { 247 | font-size: 32px; 248 | } 249 | #timetracker-tags { 250 | width: 100%; 251 | /* display: block; */ 252 | /*background-color: rgb(250, 251, 252);*/ 253 | display: inline-block; 254 | margin: 40px; 255 | padding: 20px; 256 | } 257 | .tag-button { 258 | display:flex; 259 | color: #595959; 260 | /*background-color: #e6e6e6;*/ 261 | font-size: 11px; 262 | padding: 7px 10px; 263 | margin: 0 12px 12px 0; 264 | } 265 | 266 | .tag-edit{ 267 | padding:0px 10px; 268 | } 269 | .tag-delete{ 270 | padding:0px 10px; 271 | } 272 | .tags-list{ 273 | display: flex; 274 | margin: 40px; 275 | flex-wrap: wrap; 276 | } 277 | #timetracker-goals { 278 | width: 100%; 279 | /* display: block; */ 280 | /*background-color: rgb(250, 251, 252);*/ 281 | display: inline-block; 282 | margin: 40px; 283 | padding: 20px; 284 | } 285 | .set-project{ 286 | line-height: 32px; 287 | min-width: 200px; 288 | padding-left: 10px; 289 | } 290 | /* 291 | .set-tag{ 292 | line-height: 32px; 293 | font-size: 22px; 294 | padding-left: 10px; 295 | }*/ 296 | 297 | .select2-container.set-project .select2-default { 298 | margin: 0px; 299 | border: 0px; 300 | font-size: 22px; 301 | /*width:40px;*/ 302 | 303 | /*background-color: rgb(250, 251, 252);*/ 304 | } 305 | /* 306 | .select2-container.set-tag .select2-default { 307 | margin: 0px; 308 | border: 0px; 309 | background-color: rgb(250, 251, 252); 310 | }*/ 311 | .select2-search-choice-close { 312 | display: block; 313 | } 314 | .wi-trash { 315 | line-height: 33px; 316 | font-size: 15px; 317 | display: none; 318 | } 319 | .wi-child-element:hover .wi-trash { 320 | display: block; 321 | } 322 | .wi-play { 323 | line-height: 40px; 324 | font-size: 15px; 325 | display: none; 326 | } 327 | .wi-child-element:hover .wi-play { 328 | display: block; 329 | } 330 | .wi-play-space { 331 | min-width: 15px; 332 | } 333 | .set-tag { 334 | min-width: 200px; 335 | } 336 | #report { 337 | margin-top:30px; 338 | } 339 | #timetracker-dashboard { 340 | width: 100%; 341 | /* display: block; */ 342 | /*background-color: rgb(250, 251, 252);*/ 343 | display: inline-block; 344 | margin: 40px; 345 | padding: 20px; 346 | } 347 | #dashboard{ 348 | max-width: 600px; 349 | } 350 | #dashboard h1{ 351 | margin-top:10px; 352 | font-size: 20px; 353 | } 354 | .nav-icon-projects { 355 | 356 | background-image: var(--icon-folder-000); 357 | } 358 | .nav-icon-timer { 359 | 360 | background-image: var(--icon-play-000); 361 | } 362 | .nav-icon-clients { 363 | background-image: var(--icon-contacts-000); 364 | } 365 | .nav-icon-tags { 366 | background-image: var(--icon-tag-000); 367 | } 368 | .nav-icon-reports { 369 | background-image: var(--icon-edit-000); 370 | } 371 | .nav-icon-dashboard { 372 | background-image: var(--icon-desktop-000); 373 | } 374 | .nav-icon-goals { 375 | background-image: var(--icon-monitoring-000); 376 | } 377 | /* .ui-button.primary { 378 | background-color: red; 379 | } 380 | .ui-button.greenButton { 381 | background-color: green; 382 | } */ 383 | #projects { 384 | margin-top: 30px; 385 | } 386 | #projects .tabulator-cell{ 387 | cursor: pointer; 388 | } 389 | .my-icon { 390 | display: inline-block; 391 | vertical-align: middle; 392 | text-indent: -99999px; 393 | overflow: hidden; 394 | background-repeat: no-repeat; 395 | position: absolute; 396 | top: 50%; 397 | left: 50%; 398 | margin-top: -8px; 399 | margin-left: -8px; 400 | width: 16px; 401 | height: 16px; 402 | } 403 | #tags { 404 | margin-top: 30px; 405 | } 406 | #clients { 407 | margin-top: 30px; 408 | } 409 | #manual-entry-button { 410 | float:right; 411 | } 412 | 413 | 414 | #form-manual-entry label {display: block;} 415 | #hours-manual-entry{ 416 | width: 240px; 417 | } 418 | .daterangepicker { 419 | background-color: var(--color-main-background); 420 | } 421 | .daterangepicker .calendar-table { 422 | border: 1px solid var(--color-main-background); 423 | border-radius: 4px; 424 | background-color: var(--color-main-background); 425 | } 426 | .daterangepicker select.hourselect, .daterangepicker select.minuteselect, .daterangepicker select.secondselect, .daterangepicker select.ampmselect { 427 | width: 50px; 428 | margin: 0 auto; 429 | background:var(--color-background-dark); 430 | border: 1px solid var(--color-background-dark); 431 | padding: 2px; 432 | outline: 0; 433 | font-size: 12px; 434 | } 435 | 436 | .daterangepicker td.off, .daterangepicker td.off.in-range, .daterangepicker td.off.start-date, .daterangepicker td.off.end-date { 437 | background-color:var(--color-main-background); 438 | border-color: transparent; 439 | color: var(--color-background-darker); 440 | } 441 | 442 | .daterangepicker td.available:hover, .daterangepicker th.available:hover { 443 | background-color: var(--color-background-darker); 444 | border-color: transparent; 445 | color: inherit; 446 | } 447 | .daterangepicker .ranges li:hover { 448 | background-color: var(--color-background-darker); 449 | } 450 | 451 | .tabulator .tabulator-tableHolder .tabulator-table { 452 | background-color:var(--color-main-background); 453 | color:var(--color-main-text); 454 | } 455 | .tabulator-row { 456 | background-color:var(--color-main-background); 457 | } 458 | .tabulator-row.tabulator-row-even { 459 | background-color:var(--color-background-dark); 460 | } 461 | .tabulator-row.tabulator-selectable:hover { 462 | background-color:var(--color-background-darker); 463 | } 464 | 465 | .tabulator .tabulator-header { 466 | background-color: var(--color-background-dark); 467 | color: var(--color-text-lighter); 468 | } 469 | .tabulator .tabulator-header .tabulator-col { 470 | background-color: var(--color-background-dark); 471 | } 472 | 473 | .ui-widget-header { 474 | background: var(--color-background-dark); 475 | color: var(--color-text-lighter); 476 | } 477 | .select2-search { background-color: var(--color-main-background); color: var(--color-text-darker)} 478 | .select2-search input { background-color: var(--color-main-background); color: var(--color-text-darker)} 479 | .select2-results { background-color: var(--color-main-background); color: var(--color-text-darker)} 480 | .select2-selection { background-color: var(--color-main-background); color: var(--color-text-darker)} 481 | .select2-container--default .select2-selection--single { 482 | background-color: var(--color-main-background); 483 | color: var(--color-text-darker) 484 | } 485 | .select2-container--default .select2-selection--single .select2-selection__rendered{ 486 | line-height: 26px; 487 | background-color: var(--color-main-background); 488 | color: var(--color-text-darker) 489 | } 490 | 491 | .select2-container--default .select2-selection--single .select2-selection__placeholder{ 492 | background-color: var(--color-main-background); 493 | color: var(--color-text-lighter) 494 | } 495 | .select2-container--default .select2-selection--multiple .select2-selection__choice { 496 | background-color: var(--color-main-background); 497 | color: var(--color-text-lighter); 498 | } 499 | .select2-container--default .select2-selection--multiple { 500 | background-color: var(--color-main-background); 501 | } 502 | .select2-container--default.select2-container--focus .select2-selection--multiple { 503 | border: solid var(--color-text-lighter) 1px; 504 | } 505 | 506 | #report-range{ 507 | /*var(--color-background-dark);*/ 508 | 509 | } 510 | .select2-dropdown { 511 | background-color: var(--color-background-dark); 512 | } 513 | .ui-button{ 514 | background-color: var(--color-main-background); 515 | color: var(--color-text-normal); 516 | } 517 | .ui-widget-content { 518 | background-color: var(--color-main-background); 519 | color: var(--color-text-normal); 520 | } 521 | .select-project-color { 522 | display: inline-block; 523 | width: 9px; 524 | height: 5px; 525 | margin: 3px; 526 | border-radius: 100%; 527 | } 528 | 529 | .select-project { 530 | /*font-size: 24px;*/ 531 | } 532 | .tags-select { 533 | width:200px; 534 | } 535 | .project-select { 536 | width:200px; 537 | } 538 | @media screen and (max-width: 700px) { 539 | .wi-child-element { 540 | display: inline-flex; 541 | flex-direction:column; 542 | width: 100%; 543 | } 544 | .wi-trash { 545 | display: inline; 546 | } 547 | .wi-child-duration { 548 | text-align: left; 549 | } 550 | .wi-child{ 551 | border-bottom: 2px var(--color-background-darker) solid; 552 | } 553 | .tags-select { 554 | width:auto; 555 | } 556 | .project-select { 557 | width:auto; 558 | } 559 | .wi-play{ 560 | display:table; 561 | margin: 0 auto; 562 | } 563 | 564 | 565 | } -------------------------------------------------------------------------------- /img/app.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml 42 | 44 | 46 | 50 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /img/play-button.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/stop-button.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/tiny-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtierltd/timetracker/abc60274f89a84a6be32cc2342ef7069d149d306/img/tiny-loader.gif -------------------------------------------------------------------------------- /img/void.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtierltd/timetracker/abc60274f89a84a6be32cc2342ef7069d149d306/img/void.png -------------------------------------------------------------------------------- /js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "timetrackerjs", 3 | "version": "1.0.0", 4 | "description": "", 5 | "private": true, 6 | "scripts": { 7 | "build": "webpack --config webpack.config.js" 8 | }, 9 | "build": "webpack", 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@nextcloud/browserslist-config": "^3.0.1", 15 | "css-loader": "^5.0.1", 16 | "file-loader": "^6.2.0", 17 | "style-loader": "^2.0.0", 18 | "webpack": "^5.94.0", 19 | "webpack-cli": "^4.2.0" 20 | }, 21 | "dependencies": { 22 | "chart.js": "^2.9.4", 23 | "daterangepicker": "^3.1.0", 24 | "jquery": "^3.5.1", 25 | "jquery-migrate": "^3.3.2", 26 | "jqueryui": "^1.11.1", 27 | "moment": "^2.29.4", 28 | "select2": "^4.0.13", 29 | "tabulator-tables": "^4.9.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /js/src/clients.js: -------------------------------------------------------------------------------- 1 | 2 | var $ = require("jquery"); 3 | require("jquery-migrate"); 4 | // var moment = require("moment"); 5 | require("jqueryui"); 6 | //require("jqueryui/jquery-ui.css"); 7 | import Tabulator from 'tabulator-tables'; 8 | require('tabulator-tables/dist/css/tabulator.css'); 9 | require('../../css/style.css'); 10 | //require('tabulator-tables/dist/js/jquery_wrapper.js'); 11 | // import 'select2/dist/js/select2.full.js' 12 | // require('select2/dist/css/select2.css'); 13 | // require('daterangepicker/daterangepicker.css'); 14 | 15 | (function() { 16 | 17 | $.ajaxSetup({ 18 | headers: { 'RequestToken': OC.requestToken } 19 | }); 20 | 21 | $( function() { 22 | 23 | $(document).ready(function() { 24 | $("#dialog-confirm").dialog({ 25 | autoOpen: false, 26 | modal: true 27 | }); 28 | }); 29 | $("#new-client-submit").click(function () { 30 | if ($("#new-client-input").val().trim() == '') 31 | return false; 32 | var baseUrl = OC.generateUrl('/apps/timetracker/ajax/add-client/'+$("#new-client-input").val()); 33 | var jqxhr = $.post( baseUrl, function() { 34 | getClients(); 35 | $(dialogClientEditForm).dialog("close"); 36 | }) 37 | .done(function(data, status, jqXHR) { 38 | var response = data; 39 | if ('Error' in response){ 40 | alert(response.Error); 41 | } 42 | }) 43 | .fail(function() { 44 | alert( "error" ); 45 | }) 46 | return false; 47 | }); 48 | var dialogClientEditForm = $( "#dialog-client-edit-form" ).dialog({ 49 | autoOpen: false, 50 | height: 'auto', 51 | width: 'auto', 52 | modal: true, 53 | buttons: { 54 | "Edit client": {click:function(){ 55 | editClient(dialogClientEditForm); 56 | return false; 57 | }, 58 | text: 'Edit client', 59 | class: 'primary' 60 | }, 61 | Cancel: function() { 62 | dialogClientEditForm.dialog( "close" ); 63 | return false; 64 | } 65 | }, 66 | close: function() { 67 | form[ 0 ].reset(); 68 | } 69 | }); 70 | 71 | var form = dialogClientEditForm.find( "form" ).on( "submit", function( event ) { 72 | event.preventDefault(); 73 | editClient(dialogClientEditForm); 74 | }); 75 | 76 | getClients(); 77 | function editClient(dialogClientEditForm){ 78 | var target = dialogClientEditForm.target; 79 | var form = dialogClientEditForm.find( "form" ); 80 | var baseUrl = OC.generateUrl('/apps/timetracker/ajax/edit-client/'+target); 81 | var jqxhr = $.post( baseUrl, {name:form.find("#name").val()},function() { 82 | getClients(); 83 | $(dialogClientEditForm).dialog("close"); 84 | }) 85 | .done(function(data, status, jqXHR) { 86 | var response = data; 87 | if ('Error' in response){ 88 | alert(response.Error); 89 | } 90 | }) 91 | .fail(function() { 92 | alert( "error" ); 93 | }) 94 | .always(function() { 95 | 96 | }); 97 | 98 | } 99 | 100 | function getClients(){ 101 | var baseUrl = OC.generateUrl('/apps/timetracker/ajax/clients'); 102 | 103 | var editIcon = function(cell, formatterParams){ //plain text value 104 | return ""; 105 | }; 106 | 107 | 108 | var columns = [ 109 | {title:"#", field:"", formatter:"rownum", width: 40, align: "center"}, 110 | {title:"Name", field:"name", widthGrow:1}, //column will be allocated 1/5 of the remaining space 111 | {formatter:"buttonCross", width:40, align:"center", cellClick:function(e, cell){ 112 | $("#dialog-confirm").dialog({ 113 | buttons : { 114 | "Confirm" : {click: function() { 115 | var baseUrl = OC.generateUrl('/apps/timetracker/ajax/delete-client/'+cell.getRow().getData().id); 116 | var jqxhr = $.post( baseUrl, function() { 117 | getClients(); 118 | $("#dialog-confirm").dialog("close"); 119 | }) 120 | .done(function(data, status, jqXHR) { 121 | var response = data; 122 | if ('Error' in response){ 123 | alert(response.Error); 124 | } 125 | }) 126 | .fail(function() { 127 | alert( "error" ); 128 | }) 129 | return false; 130 | }, 131 | text: 'Confirm', 132 | class:'primary' 133 | }, 134 | "Cancel" : function() { 135 | $(this).dialog("close"); 136 | } 137 | } 138 | }); 139 | $("#dialog-confirm").dialog('open'); 140 | 141 | //cell.getRow().delete(); 142 | }}, 143 | {formatter:editIcon, width:40, align:"center", cellClick:function(e, cell){ 144 | 145 | dialogClientEditForm.target = cell.getRow().getData().id; 146 | 147 | form = dialogClientEditForm.find( "form" ) 148 | form.find("#name").val(cell.getRow().getData().name); 149 | dialogClientEditForm.dialog("open"); 150 | 151 | }}, 152 | ]; 153 | 154 | var table = new Tabulator("#clients", { 155 | ajaxURL:baseUrl, 156 | layout:"fitColumns", 157 | columns:columns, 158 | rowClick:function(e, row){ 159 | return false; 160 | }, 161 | ajaxResponse:function(url, params, response){ 162 | 163 | return response.Clients; //return the tableData property of a response json object 164 | }, 165 | }); 166 | } 167 | } ); 168 | }()); 169 | -------------------------------------------------------------------------------- /js/src/dateformat.js: -------------------------------------------------------------------------------- 1 | var moment = require("moment"); 2 | moment.locale(document.documentElement.getAttribute("data-locale") || undefined); 3 | const dformat = moment.localeData().longDateFormat('L'); 4 | const tformat = moment.localeData().longDateFormat('LTS'); 5 | 6 | exports.dformat = function () { 7 | return dformat; 8 | }; 9 | 10 | exports.tformat = function () { 11 | return tformat; 12 | }; 13 | 14 | exports.dtformat = function () { 15 | return dformat + ' ' + tformat; 16 | }; 17 | 18 | exports.mformat = function () { 19 | var sample; 20 | 21 | try { 22 | sample = window.Intl ? new Intl.DateTimeFormat((document.documentElement.getAttribute("data-locale") || undefined), { 23 | numberingSystem: 'latn', 24 | calendar: 'gregory', 25 | }).format(new Date(1970, 11, 31)) : ''; 26 | } catch { 27 | sample = window.Intl ? new Intl.DateTimeFormat(undefined, { 28 | numberingSystem: 'latn', 29 | calendar: 'gregory', 30 | }).format(new Date(1970, 11, 31)) : ''; 31 | } 32 | 33 | let mm = 0, 34 | mi = sample.indexOf(12); 35 | let dd = 1, 36 | di = sample.indexOf(31); 37 | let yy = 2, 38 | yi = sample.indexOf(1970); 39 | 40 | // IE 10 or earlier, iOS 9 or earlier, non-Latin numbering system 41 | // or non-Gregorian calendar; fall back to mm/dd/yyyy 42 | if (yi >= 0 && mi >= 0 && di >= 0) { 43 | mm = (mi > yi) + (mi > di); 44 | dd = (di > yi) + (di > mi); 45 | yy = (yi > mi) + (yi > di); 46 | } 47 | 48 | let r = []; 49 | r[yy] = 'YYYY'; 50 | r[mm] = 'MM'; 51 | 52 | return r.join(sample.match(/[-.]/) || '/').replace('//','/'); 53 | }; 54 | -------------------------------------------------------------------------------- /js/src/goals.js: -------------------------------------------------------------------------------- 1 | var $ = require("jquery"); 2 | require("jquery-migrate"); 3 | // var moment = require("moment"); 4 | require("jqueryui"); 5 | //require("jqueryui/jquery-ui.css"); 6 | import Tabulator from 'tabulator-tables'; 7 | require('tabulator-tables/dist/css/tabulator.css'); 8 | 9 | import 'select2/dist/js/select2.full.js' 10 | require('select2/dist/css/select2.css'); 11 | 12 | require('../../css/style.css'); 13 | (function() { 14 | $.ajaxSetup({ 15 | headers: { 'RequestToken': OC.requestToken } 16 | }); 17 | 18 | $( function() { 19 | var newGoalProjectId; 20 | $(document).ready(function() { 21 | $("#dialog-confirm").dialog({ 22 | autoOpen: false, 23 | modal: true 24 | }); 25 | }); 26 | 27 | 28 | $("#project-select").select2({ 29 | width: '200px', 30 | escapeMarkup : function(markup) { return markup; }, 31 | placeholder: "Select project", 32 | allowClear: true, 33 | templateResult: function formatState (project) { 34 | var color = '#ffffff'; 35 | if (project.color) { 36 | color = project.color; 37 | } 38 | var $state = $( 39 | '' + project.text + '' 40 | ); 41 | return $state; 42 | }, 43 | ajax: { 44 | tags: true, 45 | url: OC.generateUrl('/apps/timetracker/ajax/projects'), 46 | 47 | dataType: 'json', 48 | delay: 250, 49 | 50 | processResults: function (data, page) { //json parse 51 | return { 52 | results: $.map(data.Projects,function(val, i){ 53 | return { id: val.id, text:val.name, color: val.color}; 54 | }), 55 | pagination: { 56 | more: false, 57 | } 58 | }; 59 | }, 60 | cache: false, 61 | 62 | }, 63 | }); 64 | $('#project-select').on("select2:select select2:unselect", function(e) { 65 | newGoalProjectId = ($(e.target).val() != null)? $(e.target).val() : ""; 66 | }); 67 | 68 | 69 | $("#new-goal-submit").click(function () { 70 | if ($("#new-goal-hours").val().trim() == '') 71 | return false; 72 | var baseUrl = OC.generateUrl('/apps/timetracker/ajax/add-goal'); 73 | var jqxhr = $.post( baseUrl, { 74 | projectId : newGoalProjectId, 75 | hours: $("#new-goal-hours").val(), 76 | interval: $("#new-goal-interval").val(), 77 | }, function() { 78 | 79 | getGoals(); 80 | }) 81 | .done(function(data, status, jqXHR) { 82 | var response = data; 83 | if ('Error' in response){ 84 | alert(response.Error); 85 | } 86 | }) 87 | .fail(function() { 88 | alert( "error" ); 89 | }) 90 | return false; 91 | }); 92 | 93 | getGoals(); 94 | function getGoals(){ 95 | var baseUrl = OC.generateUrl('/apps/timetracker/ajax/goals'); 96 | 97 | var editIcon = function(cell, formatterParams){ //plain text value 98 | return ""; 99 | }; 100 | 101 | 102 | var columns = [ 103 | {title:"#", field:"", formatter:"rownum", width: 40, align: "center"}, 104 | {title:"Project", field:"projectName", widthGrow:1}, //column will be allocated 1/5 of the remaining space 105 | {title:"Target Hours", field:"hours", widthGrow:1}, //column will be allocated 1/5 of the remaining space 106 | {title:"Interval", field:"interval", widthGrow:1}, //column will be allocated 1/5 of the remaining space 107 | {title:"Started At", field:"createdAt", widthGrow:1, mutator: function(value, data, type, params, component){ 108 | return new Date(data.createdAt*1000).toDateString();} 109 | }, //column will be allocated 1/5 of the remaining space 110 | {title:"Hours spent current interval", field:"workedHoursCurrentPeriod", widthGrow:1}, //column will be allocated 1/5 of the remaining space 111 | {title:"Past Debt in Hours", field:"debtHours", widthGrow:1}, //column will be allocated 1/5 of the remaining space 112 | {title:"Remaining Hours", field:"remainingHours", widthGrow:1}, //column will be allocated 1/5 of the remaining space 113 | {title:"Total Remaining Hours", field:"totalRemainingHours", widthGrow:1}, //column will be allocated 1/5 of the remaining space 114 | {formatter:"buttonCross", width:40, align:"center", cellClick:function(e, cell) { 115 | $("#dialog-confirm").dialog({ 116 | buttons : { 117 | "Confirm" : {click: function() { 118 | var baseUrl = OC.generateUrl('/apps/timetracker/ajax/delete-goal/'+cell.getRow().getData().id); 119 | var jqxhr = $.post( baseUrl, function() { 120 | getGoals(); 121 | $("#dialog-confirm").dialog("close"); 122 | }) 123 | .done(function(data, status, jqXHR) { 124 | var response = data; 125 | if ('Error' in response){ 126 | alert(response.Error); 127 | } 128 | }) 129 | .fail(function() { 130 | alert( "error" ); 131 | }) 132 | return false; 133 | }, 134 | text: 'Confirm', 135 | class:'primary' 136 | }, 137 | "Cancel" : function() { 138 | $(this).dialog("close"); 139 | } 140 | } 141 | }); 142 | $("#dialog-confirm").dialog('open'); 143 | 144 | //cell.getRow().delete(); 145 | }}, 146 | 147 | ]; 148 | 149 | var table = new Tabulator("#goals", { 150 | ajaxURL:baseUrl, 151 | layout:"fitColumns", 152 | columns:columns, 153 | rowClick:function(e, row){ 154 | return false; 155 | }, 156 | ajaxResponse:function(url, params, response){ 157 | 158 | return response.Goals; //return the tableData property of a response json object 159 | }, 160 | }); 161 | } 162 | } ); 163 | }()); 164 | -------------------------------------------------------------------------------- /js/src/piklor.js: -------------------------------------------------------------------------------- 1 | (function (root) { 2 | 3 | /** 4 | * Piklor 5 | * Creates a new `Piklor` instance. 6 | * 7 | * @name Piklor 8 | * @function 9 | * @param {String|Element} sel The element where the color picker will live. 10 | * @param {Array} colors An array of strings representing colors. 11 | * @param {Object} options An object containing the following fields: 12 | * 13 | * - `open` (String|Element): The HTML element or query selector which will open the picker. 14 | * - `openEvent` (String): The open event (default: `"click"`). 15 | * - `style` (Object): Some style options: 16 | * - `display` (String): The display value when the picker is opened (default: `"block"`). 17 | * - `template` (String): The color item template. The `{color}` snippet will be replaced 18 | * with the color value (default: `"
"`). 19 | * - `autoclose` (Boolean): If `false`, the color picker will not be hided by default (default: `true`). 20 | * - `closeOnBlur` (Boolean): If `true`, the color picker will be closed when clicked outside of it (default: `false`). 21 | * 22 | * @return {Piklor} The `Piklor` instance. 23 | */ 24 | function Piklor(sel, colors, options) { 25 | var self = this; 26 | options = options || {}; 27 | options.open = self.getElm(options.open); 28 | options.openEvent = options.openEvent || "click"; 29 | options.style = Object(options.style); 30 | options.style.display = options.style.display || "inline-block"; 31 | options.closeOnBlur = options.closeOnBlur || false; 32 | options.template = options.template || "
"; 33 | self.elm = self.getElm(sel); 34 | self.cbs = []; 35 | self.isOpen = true; 36 | self.colors = colors; 37 | self.options = options; 38 | self.render(); 39 | 40 | // Handle the open element and event. 41 | if (options.open) { 42 | options.open.addEventListener(options.openEvent, function (ev) { 43 | self.isOpen ? self.close() : self.open(); 44 | ev.preventDefault(); 45 | return false; 46 | }); 47 | } 48 | 49 | // Click on colors 50 | self.elm.addEventListener("click", function (ev) { 51 | var col = ev.target.getAttribute("data-col"); 52 | if (!col) { return; } 53 | self.close(); 54 | self.set(col); 55 | }); 56 | 57 | if (options.closeOnBlur) { 58 | window.addEventListener("click", function (ev) { 59 | // check if we didn't click 'open' and 'color pallete' elements 60 | if (ev.target != options.open && ev.target != self.elm && self.isOpen) { 61 | self.close(); 62 | } 63 | }); 64 | } 65 | 66 | if (options.autoclose !== false) { 67 | self.close(); 68 | } 69 | } 70 | 71 | /** 72 | * getElm 73 | * Finds the HTML element. 74 | * 75 | * @name getElm 76 | * @function 77 | * @param {String|Element} el The HTML element or query selector. 78 | * @return {HTMLElement} The selected HTML element. 79 | */ 80 | Piklor.prototype.getElm = function (el) { 81 | if (typeof el === "string") { 82 | return document.querySelector(el); 83 | } 84 | return el; 85 | }; 86 | 87 | /** 88 | * render 89 | * Renders the colors. 90 | * 91 | * @name render 92 | * @function 93 | */ 94 | Piklor.prototype.render = function () { 95 | var self = this 96 | , html = "" 97 | ; 98 | 99 | self.colors.forEach(function (c) { 100 | html += self.options.template.replace(/\{color\}/g, c); 101 | }); 102 | 103 | self.elm.innerHTML = html; 104 | }; 105 | 106 | /** 107 | * close 108 | * Closes the color picker. 109 | * 110 | * @name close 111 | * @function 112 | */ 113 | Piklor.prototype.close = function () { 114 | this.elm.style.display = "none"; 115 | this.isOpen = false; 116 | }; 117 | 118 | /** 119 | * open 120 | * Opens the color picker. 121 | * 122 | * @name open 123 | * @function 124 | */ 125 | Piklor.prototype.open = function () { 126 | this.elm.style.display = this.options.style.display; 127 | this.isOpen = true; 128 | }; 129 | 130 | /** 131 | * colorChosen 132 | * Adds a new callback in the colorChosen callback buffer. 133 | * 134 | * @name colorChosen 135 | * @function 136 | * @param {Function} cb The callback function called with the selected color. 137 | */ 138 | Piklor.prototype.colorChosen = function (cb) { 139 | this.cbs.push(cb); 140 | }; 141 | 142 | /** 143 | * set 144 | * Sets the color picker color. 145 | * 146 | * @name set 147 | * @function 148 | * @param {String} c The color to set. 149 | * @param {Boolean} p If `false`, the `colorChosen` callbacks will not be called. 150 | */ 151 | Piklor.prototype.set = function (c, p) { 152 | var self = this; 153 | self.color = c; 154 | if (p === false) { return; } 155 | self.cbs.forEach(function (cb) { 156 | cb.call(self, c); 157 | }); 158 | }; 159 | 160 | root.Piklor = Piklor; 161 | })(this); 162 | -------------------------------------------------------------------------------- /js/src/tags.js: -------------------------------------------------------------------------------- 1 | var $ = require("jquery"); 2 | require("jquery-migrate"); 3 | // var moment = require("moment"); 4 | require("jqueryui"); 5 | //require("jqueryui/jquery-ui.css"); 6 | import Tabulator from 'tabulator-tables'; 7 | require('tabulator-tables/dist/css/tabulator.css'); 8 | require('../../css/style.css'); 9 | (function() { 10 | $.ajaxSetup({ 11 | headers: { 'RequestToken': OC.requestToken } 12 | }); 13 | 14 | $( function() { 15 | 16 | $(document).ready(function() { 17 | $("#dialog-confirm").dialog({ 18 | autoOpen: false, 19 | modal: true 20 | }); 21 | }); 22 | 23 | $("#new-tag-submit").click(function () { 24 | if ($("#new-tag-input").val().trim() == '') 25 | return false; 26 | var baseUrl = OC.generateUrl('/apps/timetracker/ajax/add-tag/'+$("#new-tag-input").val()); 27 | var jqxhr = $.post( baseUrl, function() { 28 | 29 | getTags(); 30 | $(dialogTagEditForm).dialog("close"); 31 | }) 32 | .done(function(data, status, jqXHR) { 33 | var response = data; 34 | if ('Error' in response){ 35 | alert(response.Error); 36 | } 37 | }) 38 | .fail(function() { 39 | alert( "error" ); 40 | }) 41 | return false; 42 | }); 43 | var dialogTagEditForm = $( "#dialog-tag-edit-form" ).dialog({ 44 | autoOpen: false, 45 | height: 'auto', 46 | width: 'auto', 47 | modal: true, 48 | buttons: { 49 | "Edit tag": {click: function(){ 50 | editTag(dialogTagEditForm); 51 | return false; 52 | }, 53 | text: 'Edit tag', 54 | class:'primary' 55 | }, 56 | Cancel: function() { 57 | dialogTagEditForm.dialog( "close" ); 58 | } 59 | }, 60 | close: function() { 61 | form[ 0 ].reset(); 62 | } 63 | }); 64 | 65 | var form = dialogTagEditForm.find( "form" ).on( "submit", function( event ) { 66 | event.preventDefault(); 67 | editTag(dialogTagEditForm); 68 | }); 69 | 70 | getTags(); 71 | function editTag(dialogTagEditForm){ 72 | var target = dialogTagEditForm.target; 73 | var form = dialogTagEditForm.find( "form" ); 74 | var baseUrl = OC.generateUrl('/apps/timetracker/ajax/edit-tag/'+target); 75 | var jqxhr = $.post( baseUrl, {name:form.find("#name").val()},function() { 76 | getTags(); 77 | $(dialogTagEditForm).dialog("close"); 78 | }) 79 | .done(function(data, status, jqXHR) { 80 | var response = data; 81 | if ('Error' in response){ 82 | alert(response.Error); 83 | } 84 | }) 85 | .fail(function() { 86 | alert( "error" ); 87 | }) 88 | 89 | } 90 | function getTags(){ 91 | var baseUrl = OC.generateUrl('/apps/timetracker/ajax/tags'); 92 | 93 | var editIcon = function(cell, formatterParams){ //plain text value 94 | return ""; 95 | }; 96 | 97 | 98 | var columns = [ 99 | {title:"#", field:"", formatter:"rownum", width: 40, align: "center"}, 100 | {title:"Name", field:"name", widthGrow:1}, //column will be allocated 1/5 of the remaining space 101 | {formatter:"buttonCross", width:40, align:"center", cellClick:function(e, cell){ 102 | $("#dialog-confirm").dialog({ 103 | buttons : { 104 | "Confirm" : {click: function() { 105 | var baseUrl = OC.generateUrl('/apps/timetracker/ajax/delete-tag/'+cell.getRow().getData().id); 106 | var jqxhr = $.post( baseUrl, function() { 107 | getTags(); 108 | $("#dialog-confirm").dialog("close"); 109 | }) 110 | .done(function(data, status, jqXHR) { 111 | var response = data; 112 | if ('Error' in response){ 113 | alert(response.Error); 114 | } 115 | }) 116 | .fail(function() { 117 | alert( "error" ); 118 | }) 119 | return false; 120 | }, 121 | text: 'Confirm', 122 | class:'primary' 123 | }, 124 | "Cancel" : function() { 125 | $(this).dialog("close"); 126 | } 127 | } 128 | }); 129 | $("#dialog-confirm").dialog('open'); 130 | 131 | //cell.getRow().delete(); 132 | }}, 133 | {formatter:editIcon, width:40, align:"center", cellClick:function(e, cell){ 134 | 135 | dialogTagEditForm.target = cell.getRow().getData().id; 136 | 137 | form = dialogTagEditForm.find( "form" ) 138 | form.find("#name").val(cell.getRow().getData().name); 139 | dialogTagEditForm.dialog("open"); 140 | 141 | }}, 142 | ]; 143 | 144 | var table = new Tabulator("#tags", { 145 | ajaxURL:baseUrl, 146 | layout:"fitColumns", 147 | columns:columns, 148 | rowClick:function(e, row){ 149 | return false; 150 | }, 151 | ajaxResponse:function(url, params, response){ 152 | 153 | return response.Tags; //return the tableData property of a response json object 154 | }, 155 | }); 156 | } 157 | } ); 158 | }()); 159 | -------------------------------------------------------------------------------- /js/src/timelines-admin.js: -------------------------------------------------------------------------------- 1 | var $ = require("jquery"); 2 | require("jquery-migrate"); 3 | // var moment = require("moment"); 4 | require("jqueryui"); 5 | //require("jqueryui/jquery-ui.css"); 6 | import Tabulator from 'tabulator-tables'; 7 | require('tabulator-tables/dist/css/tabulator.css'); 8 | import 'select2/dist/js/select2.full.js' 9 | require('select2/dist/css/select2.css'); 10 | require('../../css/style.css'); 11 | 12 | (function() { 13 | 14 | $.ajaxSetup({ 15 | headers: { 'RequestToken': OC.requestToken } 16 | }); 17 | function timeConverter(UNIX_timestamp){ 18 | var a = new Date(UNIX_timestamp * 1000); 19 | var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; 20 | var year = a.getFullYear(); 21 | var month = months[a.getMonth()]; 22 | var date = a.getDate(); 23 | var hour = a.getHours(); 24 | var min = a.getMinutes(); 25 | var sec = a.getSeconds(); 26 | var time = date + ' ' + month + ' ' + year + ' ' + hour + ':' + min + ':' + sec ; 27 | return time; 28 | } 29 | 30 | $( function() { 31 | 32 | var group1 = "project"; 33 | var group2 = "user"; 34 | var group3 = "day"; 35 | var filterProjectId = ""; 36 | var filterClientId = ""; 37 | 38 | 39 | $(document).ready(function() { 40 | $("#dialog-confirm").dialog({ 41 | autoOpen: false, 42 | modal: true 43 | }); 44 | 45 | 46 | 47 | function editTimeline(dialogTimelineEditForm){ 48 | var target = dialogTimelineEditForm.target; 49 | var form = dialogTimelineEditForm.find( "form" ); 50 | var baseUrl = OC.generateUrl('/apps/timetracker/ajax/edit-timeline/'+target.getData().id); 51 | var jqxhr = $.post( baseUrl, {status:form.find("#status").val()},function() { 52 | getTimelines(); 53 | $(dialogTimelineEditForm).dialog("close"); 54 | }) 55 | .done(function(data, status, jqXHR) { 56 | var response = data; 57 | if ('Error' in response){ 58 | alert(response.Error); 59 | } 60 | }) 61 | .fail(function() { 62 | alert( "error" ); 63 | }); 64 | 65 | } 66 | 67 | var dialogTimelineEditForm = $( "#dialog-timeline-edit-form" ).dialog({ 68 | autoOpen: false, 69 | height: 'auto', 70 | width: 'auto', 71 | modal: true, 72 | create: function( event, ui ) { 73 | 74 | }, 75 | buttons: { 76 | "Edit timeline": { text:'Edit timeline', 77 | click:function(){ 78 | editTimeline(dialogTimelineEditForm); 79 | }, class:'primary'}, 80 | Cancel: function() { 81 | dialogTimelineEditForm.dialog( "close" ); 82 | } 83 | }, 84 | close: function() { 85 | 86 | 87 | } 88 | }); 89 | 90 | 91 | 92 | getTimelines(); 93 | 94 | function getTimelines(){ 95 | 96 | var editIcon = function(cell, formatterParams){ //plain text value 97 | return ""; 98 | }; 99 | function pad(n, width, z) { 100 | z = z || '0'; 101 | n = n + ''; 102 | return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n; 103 | } 104 | 105 | var baseUrl = OC.generateUrl('/apps/timetracker/ajax/timelines-admin'); 106 | var table = new Tabulator("#timelines", { 107 | ajaxURL:baseUrl, 108 | layout:"fitColumns", 109 | // rowClick:function(e, row){ 110 | // e.preventDefault(); 111 | // dialogTimelineEditForm.target = row; 112 | // debugger; 113 | // dialogTimelineEditForm.find('#status').val(row.getData().status); 114 | // dialogTimelineEditForm.dialog("open"); 115 | // return false; 116 | // }, 117 | columns:[ 118 | //{title:"Id", field:"id", width:100}, //column has a fixed width of 100px; 119 | {title:"#", field:"", formatter:"rownum"}, 120 | {title:"Id", field:"id", widthGrow:1}, //column will be allocated 1/5 of the remaining space 121 | {title:"User", field:"userUid", widthGrow:1}, //column will be allocated 1/5 of the remaining space 122 | {title:"Status", field:"status", widthGrow:1}, //column will be allocated 1/5 of the remaining space 123 | 124 | //{title:"User", field:"userUid", widthGrow:1}, //column will be allocated 1/5 of the remaining space 125 | //{title:"Project", field:"projectName", widthGrow:1}, //column will be allocated 1/5 of the remaining space 126 | //{title:"Client", field:"clientName", widthGrow:1}, //column will be allocated 1/5 of the remaining space 127 | {title:"When", field:"timeInterval", widthGrow:1}, //column will be allocated 1/5 of the remaining space 128 | {title:"Total Duration", field:"totalDuration",formatter:function(cell, formatterParams, onRendered){ 129 | //cell - the cell component 130 | //formatterParams - parameters set for the column 131 | //onRendered - function to call when the formatter has been rendered 132 | var duration = cell.getValue(); 133 | var s = Math.floor( (duration) % 60 ); 134 | var m = Math.floor( (duration/60) % 60 ); 135 | var h = Math.floor( (duration/(60*60))); 136 | 137 | return pad(h,2) + ':' + pad(m,2) + ':' + pad(s,2); 138 | 139 | },}, //column will be allocated 1/5 of the remaining space 140 | {title:"created At", field:"createdAt",formatter:function(cell, formatterParams, onRendered){ 141 | //cell - the cell component 142 | //formatterParams - parameters set for the column 143 | //onRendered - function to call when the formatter has been rendered 144 | var unix = cell.getValue(); 145 | 146 | return timeConverter(unix); 147 | 148 | },}, //column will be allocated 1/5 of the remaining space 149 | {title:"Download", field:"", formatter:"rownum",formatter:function(cell, formatterParams, onRendered){ 150 | //cell - the cell component 151 | //formatterParams - parameters set for the column 152 | //onRendered - function to call when the formatter has been rendered 153 | var baseUrl = OC.generateUrl('/apps/timetracker/ajax/download-timeline/'+cell.getRow().getData()["id"]); 154 | 155 | return ''+"Download"+''; 156 | 157 | }}, 158 | 159 | {formatter:editIcon, width:40, align:"center", cellClick:function(e, cell){ 160 | 161 | dialogTimelineEditForm.target = cell.getRow(); 162 | dialogTimelineEditForm.find('#status').val(cell.getRow().getData().status); 163 | dialogTimelineEditForm.dialog("open"); 164 | return false; 165 | 166 | }}, 167 | 168 | ], 169 | ajaxResponse:function(url, params, response){ 170 | 171 | return response.Timelines; //return the tableData property of a response json object 172 | }, 173 | }); 174 | 175 | } 176 | }); 177 | 178 | 179 | } ); 180 | }()); 181 | -------------------------------------------------------------------------------- /js/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | //mode: 'development', 3 | devtool: 'cheap-module-source-map', 4 | entry: { 5 | timer: './src/timer.js', 6 | clients: './src/clients.js', 7 | dashboard: './src/dashboard.js', 8 | projects: './src/projects.js', 9 | reports: './src/reports.js', 10 | tags: './src/tags.js', 11 | goals: './src/goals.js', 12 | timelines: './src/timelines.js', 13 | timelinesadmin: './src/timelines-admin.js', 14 | }, 15 | output: { 16 | filename: '[name].js', 17 | path: __dirname + '/dist' 18 | }, 19 | resolve: { 20 | alias: { 21 | 'jquery-ui': 'jqueryui' 22 | } 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.css$/, 28 | use: ['style-loader', 'css-loader'], 29 | }, 30 | { 31 | test: /\.(jpg|png|jpeg|svg)$/, 32 | loader: 'file-loader' 33 | }, 34 | ] 35 | }, 36 | }; 37 | 38 | -------------------------------------------------------------------------------- /lib/AppFramework/Db/OldNextcloudMapper.php: -------------------------------------------------------------------------------- 1 | 6 | * @author Christoph Wurst 7 | * @author Joas Schilling 8 | * @author Lukas Reschke 9 | * @author Morris Jobke 10 | * @author Roeland Jago Douma 11 | * @author Thomas Müller 12 | * 13 | * @license AGPL-3.0 14 | * 15 | * This code is free software: you can redistribute it and/or modify 16 | * it under the terms of the GNU Affero General Public License, version 3, 17 | * as published by the Free Software Foundation. 18 | * 19 | * This program is distributed in the hope that it will be useful, 20 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 21 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 22 | * GNU Affero General Public License for more details. 23 | * 24 | * You should have received a copy of the GNU Affero General Public License, version 3, 25 | * along with this program. If not, see 26 | * 27 | */ 28 | namespace OCA\TimeTracker\AppFramework\Db; 29 | 30 | use OCP\AppFramework\Db\DoesNotExistException; 31 | use OCP\AppFramework\Db\Entity; 32 | use OCP\AppFramework\Db\MultipleObjectsReturnedException; 33 | use OCP\IDBConnection; 34 | 35 | /** 36 | * Simple parent class for inheriting your data access layer from. This class 37 | * may be subject to change in the future 38 | * @since 7.0.0 39 | * @deprecated 14.0.0 Move over to QBMapper 40 | */ 41 | abstract class OldNextcloudMapper { 42 | protected $tableName; 43 | protected $entityClass; 44 | protected $db; 45 | 46 | /** 47 | * @param IDBConnection $db Instance of the Db abstraction layer 48 | * @param string $tableName the name of the table. set this to allow entity 49 | * @param string $entityClass the name of the entity that the sql should be 50 | * mapped to queries without using sql 51 | * @since 7.0.0 52 | * @deprecated 14.0.0 Move over to QBMapper 53 | */ 54 | public function __construct(IDBConnection $db, $tableName, $entityClass = null) { 55 | $this->db = $db; 56 | $this->tableName = '*PREFIX*' . $tableName; 57 | 58 | // if not given set the entity name to the class without the mapper part 59 | // cache it here for later use since reflection is slow 60 | if ($entityClass === null) { 61 | $this->entityClass = str_replace('Mapper', '', get_class($this)); 62 | } else { 63 | $this->entityClass = $entityClass; 64 | } 65 | } 66 | 67 | 68 | /** 69 | * @return string the table name 70 | * @since 7.0.0 71 | * @deprecated 14.0.0 Move over to QBMapper 72 | */ 73 | public function getTableName() { 74 | return $this->tableName; 75 | } 76 | 77 | 78 | /** 79 | * Deletes an entity from the table 80 | * @param Entity $entity the entity that should be deleted 81 | * @return Entity the deleted entity 82 | * @since 7.0.0 - return value added in 8.1.0 83 | * @deprecated 14.0.0 Move over to QBMapper 84 | */ 85 | public function delete(Entity $entity) { 86 | $sql = 'DELETE FROM `' . $this->tableName . '` WHERE `id` = ?'; 87 | $stmt = $this->execute($sql, [$entity->getId()]); 88 | $stmt->closeCursor(); 89 | return $entity; 90 | } 91 | 92 | 93 | /** 94 | * Creates a new entry in the db from an entity 95 | * @param Entity $entity the entity that should be created 96 | * @return Entity the saved entity with the set id 97 | * @since 7.0.0 98 | * @deprecated 14.0.0 Move over to QBMapper 99 | */ 100 | public function insert(Entity $entity) { 101 | // get updated fields to save, fields have to be set using a setter to 102 | // be saved 103 | $properties = $entity->getUpdatedFields(); 104 | $values = ''; 105 | $columns = ''; 106 | $params = []; 107 | 108 | // build the fields 109 | $i = 0; 110 | foreach ($properties as $property => $updated) { 111 | $column = $entity->propertyToColumn($property); 112 | $getter = 'get' . ucfirst($property); 113 | 114 | $columns .= '`' . $column . '`'; 115 | $values .= '?'; 116 | 117 | // only append colon if there are more entries 118 | if ($i < count($properties) - 1) { 119 | $columns .= ','; 120 | $values .= ','; 121 | } 122 | 123 | $params[] = $entity->$getter(); 124 | $i++; 125 | } 126 | 127 | $sql = 'INSERT INTO `' . $this->tableName . '`(' . 128 | $columns . ') VALUES(' . $values . ')'; 129 | 130 | $stmt = $this->execute($sql, $params); 131 | 132 | $entity->setId((int) $this->db->lastInsertId($this->tableName)); 133 | 134 | $stmt->closeCursor(); 135 | 136 | return $entity; 137 | } 138 | 139 | 140 | 141 | /** 142 | * Updates an entry in the db from an entity 143 | * @throws \InvalidArgumentException if entity has no id 144 | * @param Entity $entity the entity that should be created 145 | * @return Entity the saved entity with the set id 146 | * @since 7.0.0 - return value was added in 8.0.0 147 | * @deprecated 14.0.0 Move over to QBMapper 148 | */ 149 | public function update(Entity $entity) { 150 | // if entity wasn't changed it makes no sense to run a db query 151 | $properties = $entity->getUpdatedFields(); 152 | if (count($properties) === 0) { 153 | return $entity; 154 | } 155 | 156 | // entity needs an id 157 | $id = $entity->getId(); 158 | if ($id === null) { 159 | throw new \InvalidArgumentException( 160 | 'Entity which should be updated has no id'); 161 | } 162 | 163 | // get updated fields to save, fields have to be set using a setter to 164 | // be saved 165 | // do not update the id field 166 | unset($properties['id']); 167 | 168 | $columns = ''; 169 | $params = []; 170 | 171 | // build the fields 172 | $i = 0; 173 | foreach ($properties as $property => $updated) { 174 | $column = $entity->propertyToColumn($property); 175 | $getter = 'get' . ucfirst($property); 176 | 177 | $columns .= '`' . $column . '` = ?'; 178 | 179 | // only append colon if there are more entries 180 | if ($i < count($properties) - 1) { 181 | $columns .= ','; 182 | } 183 | 184 | $params[] = $entity->$getter(); 185 | $i++; 186 | } 187 | 188 | $sql = 'UPDATE `' . $this->tableName . '` SET ' . 189 | $columns . ' WHERE `id` = ?'; 190 | $params[] = $id; 191 | 192 | $stmt = $this->execute($sql, $params); 193 | $stmt->closeCursor(); 194 | 195 | return $entity; 196 | } 197 | 198 | /** 199 | * Checks if an array is associative 200 | * @param array $array 201 | * @return bool true if associative 202 | * @since 8.1.0 203 | * @deprecated 14.0.0 Move over to QBMapper 204 | */ 205 | private function isAssocArray(array $array) { 206 | return array_values($array) !== $array; 207 | } 208 | 209 | /** 210 | * Returns the correct PDO constant based on the value type 211 | * @param $value 212 | * @return int PDO constant 213 | * @since 8.1.0 214 | * @deprecated 14.0.0 Move over to QBMapper 215 | */ 216 | private function getPDOType($value) { 217 | switch (gettype($value)) { 218 | case 'integer': 219 | return \PDO::PARAM_INT; 220 | case 'boolean': 221 | return \PDO::PARAM_BOOL; 222 | default: 223 | return \PDO::PARAM_STR; 224 | } 225 | } 226 | 227 | 228 | /** 229 | * Runs an sql query 230 | * @param string $sql the prepare string 231 | * @param array $params the params which should replace the ? in the sql query 232 | * @param int $limit the maximum number of rows 233 | * @param int $offset from which row we want to start 234 | * @return \PDOStatement the database query result 235 | * @since 7.0.0 236 | * @deprecated 14.0.0 Move over to QBMapper 237 | */ 238 | protected function execute($sql, array $params = [], $limit = null, $offset = null) { 239 | $query = $this->db->prepare($sql, $limit, $offset); 240 | 241 | if ($this->isAssocArray($params)) { 242 | foreach ($params as $key => $param) { 243 | $pdoConstant = $this->getPDOType($param); 244 | $query->bindValue($key, $param, $pdoConstant); 245 | } 246 | } else { 247 | $index = 1; // bindParam is 1 indexed 248 | foreach ($params as $param) { 249 | $pdoConstant = $this->getPDOType($param); 250 | $query->bindValue($index, $param, $pdoConstant); 251 | $index++; 252 | } 253 | } 254 | 255 | $query->execute(); 256 | 257 | return $query; 258 | } 259 | 260 | /** 261 | * Returns an db result and throws exceptions when there are more or less 262 | * results 263 | * @see findEntity 264 | * @param string $sql the sql query 265 | * @param array $params the parameters of the sql query 266 | * @param int $limit the maximum number of rows 267 | * @param int $offset from which row we want to start 268 | * @throws DoesNotExistException if the item does not exist 269 | * @throws MultipleObjectsReturnedException if more than one item exist 270 | * @return array the result as row 271 | * @since 7.0.0 272 | * @deprecated 14.0.0 Move over to QBMapper 273 | */ 274 | protected function findOneQuery($sql, array $params = [], $limit = null, $offset = null) { 275 | $stmt = $this->execute($sql, $params, $limit, $offset); 276 | $row = $stmt->fetch(); 277 | 278 | if ($row === false || $row === null) { 279 | $stmt->closeCursor(); 280 | $msg = $this->buildDebugMessage( 281 | 'Did expect one result but found none when executing', $sql, $params, $limit, $offset 282 | ); 283 | throw new DoesNotExistException($msg); 284 | } 285 | $row2 = $stmt->fetch(); 286 | $stmt->closeCursor(); 287 | //MDB2 returns null, PDO and doctrine false when no row is available 288 | if (! ($row2 === false || $row2 === null)) { 289 | $msg = $this->buildDebugMessage( 290 | 'Did not expect more than one result when executing', $sql, $params, $limit, $offset 291 | ); 292 | throw new MultipleObjectsReturnedException($msg); 293 | } else { 294 | return $row; 295 | } 296 | } 297 | 298 | /** 299 | * Builds an error message by prepending the $msg to an error message which 300 | * has the parameters 301 | * @see findEntity 302 | * @param string $sql the sql query 303 | * @param array $params the parameters of the sql query 304 | * @param int $limit the maximum number of rows 305 | * @param int $offset from which row we want to start 306 | * @return string formatted error message string 307 | * @since 9.1.0 308 | * @deprecated 14.0.0 Move over to QBMapper 309 | */ 310 | private function buildDebugMessage($msg, $sql, array $params = [], $limit = null, $offset = null) { 311 | return $msg . 312 | ': query "' . $sql . '"; ' . 313 | 'parameters ' . print_r($params, true) . '; ' . 314 | 'limit "' . $limit . '"; '. 315 | 'offset "' . $offset . '"'; 316 | } 317 | 318 | 319 | /** 320 | * Creates an entity from a row. Automatically determines the entity class 321 | * from the current mapper name (MyEntityMapper -> MyEntity) 322 | * @param array $row the row which should be converted to an entity 323 | * @return Entity the entity 324 | * @since 7.0.0 325 | * @deprecated 14.0.0 Move over to QBMapper 326 | */ 327 | protected function mapRowToEntity($row) { 328 | return call_user_func($this->entityClass .'::fromRow', $row); 329 | } 330 | 331 | 332 | /** 333 | * Runs a sql query and returns an array of entities 334 | * @param string $sql the prepare string 335 | * @param array $params the params which should replace the ? in the sql query 336 | * @param int $limit the maximum number of rows 337 | * @param int $offset from which row we want to start 338 | * @return array all fetched entities 339 | * @since 7.0.0 340 | * @deprecated 14.0.0 Move over to QBMapper 341 | */ 342 | protected function findEntities($sql, array $params = [], $limit = null, $offset = null) { 343 | $stmt = $this->execute($sql, $params, $limit, $offset); 344 | 345 | $entities = []; 346 | 347 | while ($row = $stmt->fetch()) { 348 | $entities[] = $this->mapRowToEntity($row); 349 | } 350 | 351 | $stmt->closeCursor(); 352 | 353 | return $entities; 354 | } 355 | 356 | 357 | /** 358 | * Returns an db result and throws exceptions when there are more or less 359 | * results 360 | * @param string $sql the sql query 361 | * @param array $params the parameters of the sql query 362 | * @param int $limit the maximum number of rows 363 | * @param int $offset from which row we want to start 364 | * @throws DoesNotExistException if the item does not exist 365 | * @throws MultipleObjectsReturnedException if more than one item exist 366 | * @return Entity the entity 367 | * @since 7.0.0 368 | * @deprecated 14.0.0 Move over to QBMapper 369 | */ 370 | protected function findEntity($sql, array $params = [], $limit = null, $offset = null) { 371 | return $this->mapRowToEntity($this->findOneQuery($sql, $params, $limit, $offset)); 372 | } 373 | } 374 | -------------------------------------------------------------------------------- /lib/AppInfo/Application.php: -------------------------------------------------------------------------------- 1 | getContainer(); 38 | /** 39 | * Controllers 40 | */ 41 | 42 | 43 | 44 | $container->registerService('WorkIntervalMapper', function($c){ 45 | return new WorkIntervalMapper( 46 | $c->query('ServerContainer')->getDatabaseConnection() 47 | ); 48 | }); 49 | $container->registerService('ReportItemMapper', function($c){ 50 | return new WorkIntervalMapper( 51 | $c->query('ServerContainer')->getDatabaseConnection() 52 | ); 53 | }); 54 | 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/Controller/ClientsController.php: -------------------------------------------------------------------------------- 1 | userSession = $userSession; 18 | $user = $this->userSession->getUser(); 19 | 20 | //var_dump($user); 21 | } 22 | 23 | /** 24 | * 25 | * @NoAdminRequired 26 | * @NoCSRFRequired 27 | */ 28 | public function index() { 29 | return new TemplateResponse('timetracker', 'index',['appPage' => 'content/clients', 'script' => 'dist/clients']); // templates/index.php 30 | } 31 | 32 | 33 | } 34 | -------------------------------------------------------------------------------- /lib/Controller/DashboardController.php: -------------------------------------------------------------------------------- 1 | userSession = $userSession; 18 | $user = $this->userSession->getUser(); 19 | 20 | } 21 | 22 | /** 23 | * 24 | * @NoAdminRequired 25 | * @NoCSRFRequired 26 | */ 27 | public function index() { 28 | return new TemplateResponse('timetracker', 'index',['appPage' => 'content/dashboard', 'script' => 'dist/dashboard']); // templates/index.php 29 | } 30 | 31 | 32 | } 33 | -------------------------------------------------------------------------------- /lib/Controller/GoalsController.php: -------------------------------------------------------------------------------- 1 | userSession = $userSession; 18 | $user = $this->userSession->getUser(); 19 | 20 | } 21 | 22 | /** 23 | * 24 | * @NoAdminRequired 25 | * @NoCSRFRequired 26 | */ 27 | public function index() { 28 | return new TemplateResponse('timetracker', 'index',['appPage' => 'content/goals', 'script' => 'dist/goals']); // templates/index.php 29 | } 30 | 31 | 32 | } 33 | -------------------------------------------------------------------------------- /lib/Controller/PageController.php: -------------------------------------------------------------------------------- 1 | userSession = $userSession; 18 | $user = $this->userSession->getUser(); 19 | 20 | //var_dump($user); 21 | } 22 | 23 | /** 24 | * 25 | * @NoAdminRequired 26 | * @NoCSRFRequired 27 | */ 28 | public function index() { 29 | return new TemplateResponse('timetracker', 'index',['appPage' => 'content/index', 'script' => 'dist/timer']); // templates/index.php 30 | } 31 | 32 | 33 | } 34 | -------------------------------------------------------------------------------- /lib/Controller/ProjectsController.php: -------------------------------------------------------------------------------- 1 | userSession = $userSession; 18 | $user = $this->userSession->getUser(); 19 | 20 | } 21 | 22 | /** 23 | * CAUTION: the @Stuff turns off security checks; for this page no admin is 24 | * required and no CSRF check. If you don't know what CSRF is, read 25 | * it up in the docs or you might create a security hole. This is 26 | * basically the only required method to add this exemption, don't 27 | * add it to any other method if you don't exactly know what it does 28 | * 29 | * @NoAdminRequired 30 | * @NoCSRFRequired 31 | */ 32 | public function index() { 33 | return new TemplateResponse('timetracker', 'index',['appPage' => 'content/projects', 'script' => 'dist/projects']); // templates/index.php 34 | } 35 | 36 | 37 | } 38 | -------------------------------------------------------------------------------- /lib/Controller/ReportsController.php: -------------------------------------------------------------------------------- 1 | userSession = $userSession; 18 | $user = $this->userSession->getUser(); 19 | 20 | } 21 | 22 | /** 23 | * 24 | * @NoAdminRequired 25 | * @NoCSRFRequired 26 | */ 27 | public function index() { 28 | return new TemplateResponse('timetracker', 'index',['appPage' => 'content/reports', 'script' => 'dist/reports']); // templates/index.php 29 | } 30 | 31 | 32 | } 33 | -------------------------------------------------------------------------------- /lib/Controller/TagsController.php: -------------------------------------------------------------------------------- 1 | userSession = $userSession; 18 | $user = $this->userSession->getUser(); 19 | 20 | } 21 | 22 | /** 23 | * 24 | * @NoAdminRequired 25 | * @NoCSRFRequired 26 | */ 27 | public function index() { 28 | return new TemplateResponse('timetracker', 'index',['appPage' => 'content/tags', 'script' => 'dist/tags']); // templates/index.php 29 | } 30 | 31 | 32 | } 33 | -------------------------------------------------------------------------------- /lib/Controller/TimelinesAdminController.php: -------------------------------------------------------------------------------- 1 | userSession = $userSession; 18 | $user = $this->userSession->getUser(); 19 | 20 | } 21 | 22 | /** 23 | * 24 | * @NoAdminRequired 25 | * @NoCSRFRequired 26 | */ 27 | public function index() { 28 | return new TemplateResponse('timetracker', 'index',['appPage' => 'content/timelines-admin', 'script' => 'dist/timelinesadmin']); // templates/index.php 29 | } 30 | 31 | 32 | } 33 | -------------------------------------------------------------------------------- /lib/Controller/TimelinesController.php: -------------------------------------------------------------------------------- 1 | userSession = $userSession; 18 | $user = $this->userSession->getUser(); 19 | 20 | } 21 | 22 | /** 23 | * 24 | * @NoAdminRequired 25 | * @NoCSRFRequired 26 | */ 27 | public function index() { 28 | return new TemplateResponse('timetracker', 'index',['appPage' => 'content/timelines', 'script' => 'dist/timelines']); // templates/index.php 29 | } 30 | 31 | 32 | } 33 | -------------------------------------------------------------------------------- /lib/Db/Client.php: -------------------------------------------------------------------------------- 1 | addType('id', 'integer'); 18 | $this->addType('name', 'string'); 19 | $this->addType('createdAt', 'integer'); 20 | } 21 | } -------------------------------------------------------------------------------- /lib/Db/ClientMapper.php: -------------------------------------------------------------------------------- 1 | findEntity($sql, [strtoupper($name)]); 23 | return $e; 24 | } catch (\OCP\AppFramework\Db\DoesNotExistException $e){ 25 | return null; 26 | } 27 | 28 | } 29 | 30 | /** 31 | * @throws \OCP\AppFramework\Db\DoesNotExistException if not found 32 | * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException if more than one result 33 | */ 34 | public function find($id) { 35 | $sql = 'SELECT * FROM `*PREFIX*timetracker_client` ' . 36 | 'WHERE `id` = ?'; 37 | return $this->findEntity($sql, [$id]); 38 | } 39 | 40 | public function findAll($user){ 41 | $sql = 'SELECT tc.* FROM `*PREFIX*timetracker_client` tc left join `*PREFIX*timetracker_user_to_client` uc on uc.client_id = tc.id where uc.user_uid = ? order by tc.name asc'; 42 | return $this->findEntities($sql, [$user]); 43 | } 44 | 45 | public function searchByName($user, $name){ 46 | $name = strtoupper($name); 47 | $sql = 'SELECT tc.* FROM `*PREFIX*timetracker_client` tc left join `*PREFIX*timetracker_user_to_client` uc on uc.client_id = tc.id where uc.user_uid = ? and upper(tc.name) LIKE ? order by tc.name asc'; 48 | return $this->findEntities($sql, [$user, "%" . $name . "%"]); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/Db/Goal.php: -------------------------------------------------------------------------------- 1 | addType('id', 'integer'); 22 | $this->addType('userUid', 'string'); 23 | $this->addType('projectId', 'integer'); 24 | $this->addType('projectName', 'string'); 25 | $this->addType('hours', 'integer'); 26 | $this->addType('interval', 'string'); 27 | $this->addType('createdAt', 'integer'); 28 | } 29 | } -------------------------------------------------------------------------------- /lib/Db/GoalMapper.php: -------------------------------------------------------------------------------- 1 | findEntity($sql, [$userUid, $projectId]); 23 | return $e; 24 | } catch (\OCP\AppFramework\Db\DoesNotExistException $e){ 25 | return null; 26 | } 27 | 28 | } 29 | 30 | /** 31 | * @throws \OCP\AppFramework\Db\DoesNotExistException if not found 32 | * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException if more than one result 33 | */ 34 | public function find($id) { 35 | $sql = 'SELECT * FROM `*PREFIX*timetracker_goal` ' . 36 | 'WHERE `id` = ?'; 37 | return $this->findEntity($sql, [$id]); 38 | } 39 | 40 | 41 | public function findAll($user){ 42 | $sql = 'SELECT tg.*,p.name as project_name FROM `*PREFIX*timetracker_goal` tg join `*PREFIX*timetracker_project` p on p.id = tg.project_id where tg.user_uid = ? order by tg.created_at desc'; 43 | return $this->findEntities($sql, [$user]); 44 | } 45 | 46 | 47 | } 48 | -------------------------------------------------------------------------------- /lib/Db/Project.php: -------------------------------------------------------------------------------- 1 | addType('id', 'integer'); 23 | $this->addType('name', 'string'); 24 | $this->addType('color', 'string'); 25 | $this->addType('clientId', 'integer'); 26 | $this->addType('createdByUserUid', 'string'); 27 | $this->addType('createdAt', 'integer'); 28 | $this->addType('locked', 'integer'); 29 | $this->addType('archived', 'integer'); 30 | } 31 | } -------------------------------------------------------------------------------- /lib/Db/ProjectMapper.php: -------------------------------------------------------------------------------- 1 | findEntity($sql, [strtoupper($name)]); 23 | return $e; 24 | } catch (\OCP\AppFramework\Db\DoesNotExistException $e){ 25 | return null; 26 | } 27 | 28 | } 29 | 30 | public function searchByName($user, string $name) { 31 | $name = strtoupper($name); 32 | $sql = 'SELECT tp.* FROM `*PREFIX*timetracker_project` tp LEFT JOIN `*PREFIX*timetracker_user_to_project` up ON up.project_id = tp.id WHERE up.`user_uid` = ? AND upper(tp.`name`) LIKE ? ORDER BY tp.`name`'; 33 | 34 | return $this->findEntities($sql, [$user,"%" . $name ."%"]); 35 | 36 | } 37 | 38 | /** 39 | * @throws \OCP\AppFramework\Db\DoesNotExistException if not found 40 | * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException if more than one result 41 | */ 42 | public function find($id) { 43 | $sql = 'SELECT * FROM `*PREFIX*timetracker_project` ' . 44 | 'WHERE `id` = ?'; 45 | return $this->findEntity($sql, [$id]); 46 | } 47 | 48 | 49 | public function findAll($user,$getArchived = 0){ 50 | if ($getArchived) { 51 | $sql = 'SELECT tp.* FROM `*PREFIX*timetracker_project` tp left join `*PREFIX*timetracker_user_to_project` up '. 52 | 'on up.project_id = tp.id where up.user_uid = ? order by tp.name asc'; 53 | } else { 54 | $sql = 'SELECT tp.* FROM `*PREFIX*timetracker_project` tp left join `*PREFIX*timetracker_user_to_project` up '. 55 | 'on up.project_id = tp.id where up.user_uid = ? and tp.archived != 1 order by tp.name asc'; 56 | } 57 | return $this->findEntities($sql, [$user]); 58 | } 59 | public function findAllAdmin($getArchived = 0){ 60 | if ($getArchived) { 61 | $sql = 'SELECT tp.* FROM `*PREFIX*timetracker_project` tp order by tp.name asc'; 62 | } else { 63 | $sql = 'SELECT tp.* FROM `*PREFIX*timetracker_project` tp where tp.archived != 1 order by tp.name asc'; 64 | } 65 | return $this->findEntities($sql, []); 66 | } 67 | 68 | public function delete($project_id) { 69 | $sql = 'delete FROM `*PREFIX*timetracker_project` ' . 70 | ' where id = ?'; 71 | 72 | try { 73 | $this->execute($sql, [$project_id]); 74 | return; 75 | } catch (\OCP\AppFramework\Db\DoesNotExistException $e){ 76 | return; 77 | } 78 | 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/Db/ReportItem.php: -------------------------------------------------------------------------------- 1 | addType('id', 'integer'); 28 | $this->addType('name', 'string'); 29 | $this->addType('details', 'string'); 30 | $this->addType('projectId', 'integer'); 31 | $this->addType('userUid', 'string'); 32 | $this->addType('time', 'string'); 33 | $this->addType('ftime', 'string'); 34 | $this->addType('totalDuration', 'integer'); 35 | $this->addType('project', 'string'); 36 | $this->addType('cost', 'integer'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/Db/ReportItemMapper.php: -------------------------------------------------------------------------------- 1 | dbengine = 'MYSQL'; 16 | if (strpos(get_class($db->getDatabasePlatform()),'PostgreSQL') !== FALSE){ 17 | $this->dbengine = 'POSTGRES'; 18 | } else if (strpos(get_class($db->getDatabasePlatform()),'Sqlite') !== FALSE){ 19 | $this->dbengine = 'SQLITE'; 20 | } 21 | parent::__construct($db, 'timetracker_work_interval'); 22 | } 23 | 24 | /* 25 | public $id; 26 | public $name; 27 | public $projectId; 28 | public $userUid; 29 | public $start; 30 | public $totalDuration; 31 | public $project; 32 | public $clientId; 33 | public $client; 34 | */ 35 | 36 | 37 | public function report($user, $from, $to, $filterProjectId, $filterClientId, $filterTagId, $timegroup, $groupOn1, $groupOn2, $admin, $start, $limit ){ 38 | 39 | $selectFields = ['min(wi.id) as id', 'sum(duration) as "totalDuration"']; 40 | 41 | if ($timegroup !== null) { 42 | $selectFields[]= "SUM(wi.cost) as cost"; 43 | } else { 44 | $selectFields[] = 'wi.cost as cost'; 45 | } 46 | 47 | $aggregation = true; 48 | if(empty($groupOn1) && empty($groupOn2) && empty($timegroup)) { 49 | $selectFields[] = 'min(wi.details) as "details"'; 50 | $selectFields[] = 'min(wi.name) as "name"'; 51 | $selectFields[] = '\'*\' as "projectId"'; 52 | $selectFields[] = 'min(p.name) as "project"'; 53 | 54 | $selectFields[] = '\'*\' as "clientId"'; 55 | $selectFields[] = 'min(c.name) as "client"'; 56 | $selectFields[] = 'min(user_uid) as "userUid"'; 57 | $aggregation = false; 58 | } else { 59 | $selectFields[] = '\'*\' as "details"'; 60 | } 61 | 62 | if (!$aggregation) { 63 | $selectFields[]= "start as time"; 64 | } else { 65 | $selectFields[]= "min(start) as time"; 66 | } 67 | 68 | if ($this->dbengine == 'POSTGRES') { 69 | if ($timegroup == 'week') { 70 | $selectFields[]= "concat(date_part('year', to_timestamp(start)), 'W', to_char(to_timestamp(start), 'IW')) as ftime"; 71 | }elseif ($timegroup == 'year') { 72 | $selectFields[]= "date_part('year', to_timestamp(start)) as ftime"; 73 | }elseif ($timegroup == 'month') { 74 | $selectFields[]= "to_char(to_timestamp(start), 'YYYY-MM') as ftime"; 75 | }elseif ($timegroup == 'day') { 76 | $selectFields[]= "to_char(to_timestamp(start), 'YYYY-MM-DD') as ftime"; 77 | } 78 | } else if ($this->dbengine == 'SQLITE') { 79 | if ($timegroup == 'week') { 80 | $selectFields[]= "strftime('%YW%W', datetime(start, 'unixepoch')) as ftime"; 81 | }elseif ($timegroup == 'year') { 82 | $selectFields[]= "strftime('%Y', datetime(start, 'unixepoch')) as ftime"; 83 | }elseif ($timegroup == 'month') { 84 | $selectFields[]= "strftime('%Y-%m', datetime(start, 'unixepoch')) as ftime"; 85 | }elseif ($timegroup == 'day') { 86 | $selectFields[]= "strftime('%Y-%m-%d', datetime(start, 'unixepoch')) as ftime"; 87 | } 88 | } else { 89 | if ($timegroup == 'week') { 90 | $selectFields[]= "CONCAT(YEAR(FROM_UNIXTIME(start)), 'W', WEEK(FROM_UNIXTIME(start))) as ftime"; 91 | }elseif ($timegroup == 'year') { 92 | $selectFields[]= "YEAR(FROM_UNIXTIME(start)) as ftime"; 93 | }elseif ($timegroup == 'month') { 94 | $selectFields[]= "DATE_FORMAT(FROM_UNIXTIME(start),'%Y-%m') as ftime"; 95 | }elseif ($timegroup == 'day') { 96 | $selectFields[]= "DATE(FROM_UNIXTIME(start)) as ftime"; 97 | } 98 | } 99 | 100 | if ($aggregation){ 101 | if($groupOn1 != 'name'){ 102 | if ($this->dbengine != 'MYSQL') { 103 | $selectFields[] = '\'*\' as name'; 104 | } else { 105 | $selectFields[] = 'CASE WHEN CHAR_LENGTH(group_concat(distinct wi.name)) > 40 THEN CONCAT(SUBSTRING(group_concat(distinct wi.name), 1, 40), "...") ELSE group_concat(distinct wi.name) END as name'; 106 | } 107 | //$selectFields[] = 'group_concat(distinct wi.name) as name'; 108 | } else { 109 | $selectFields[] = 'wi.name as name'; 110 | } 111 | } 112 | 113 | 114 | if ($aggregation){ 115 | if(($groupOn1 != 'project') && ($groupOn2 != 'project')){ 116 | $selectFields[] = '\'*\' as "projectId"'; 117 | if ($this->dbengine == 'POSTGRES') { 118 | $selectFields[] = 'string_agg(distinct p.name, \',\') as project'; 119 | } else { 120 | $selectFields[] = 'group_concat(distinct p.name) as project'; 121 | } 122 | } else { 123 | 124 | $selectFields[] = '\'*\' as "projectId"'; 125 | $selectFields[] = 'p.name as project'; 126 | } 127 | 128 | 129 | if(($groupOn1 != 'client') && ($groupOn2 != 'client')){ 130 | $selectFields[] = '\'*\' as "clientId"'; 131 | if ($this->dbengine == 'POSTGRES') { 132 | $selectFields[] = 'string_agg(distinct c.name, \',\') as client'; 133 | } else { 134 | $selectFields[] = 'group_concat(distinct c.name) as client'; 135 | } 136 | 137 | } else { 138 | $selectFields[] = '\'*\' as "clientId"'; 139 | $selectFields[] = 'c.name as client'; 140 | } 141 | 142 | if(($groupOn1 != 'userUid') && ($groupOn2 != 'userUid') && $aggregation){ 143 | if ($this->dbengine == 'POSTGRES') { 144 | $selectFields[] = 'string_agg(distinct user_uid, \',\') as "userUid"'; 145 | } else { 146 | $selectFields[] = 'group_concat(distinct user_uid) as "userUid"'; 147 | } 148 | 149 | } else { 150 | $selectFields[] = 'user_uid as "userUid"'; 151 | } 152 | 153 | } 154 | 155 | $selectItems = implode(", ",$selectFields). 156 | ' FROM *PREFIX*timetracker_work_interval wi 157 | left join *PREFIX*timetracker_project p on wi.project_id = p.id 158 | left join *PREFIX*timetracker_client c on p.client_id = c.id'; 159 | $filters = []; 160 | $params = []; 161 | if (!empty($from)){ 162 | $filters[] = "(start > ?)"; 163 | $params[] = $from; 164 | 165 | } 166 | if (!empty($to)){ 167 | $filters[] = "(start < ?)"; 168 | $params[] = $to; 169 | 170 | } 171 | if (!empty($filterProjectId)){ 172 | $qm = []; 173 | $append = ''; 174 | foreach($filterProjectId as $f){ 175 | $qm[] = '?'; 176 | $params[] = $f; 177 | 178 | if($f == null) { 179 | $append = ' or wi.project_id is null '; 180 | } 181 | } 182 | $filters[] = '(wi.project_id in ('.implode(",",$qm).')'.$append.')'; 183 | } 184 | if (!empty($filterClientId)){ 185 | $qm = []; 186 | $append = ''; 187 | foreach($filterClientId as $f){ 188 | $qm[] = '?'; 189 | $params[] = $f; 190 | if ($f == null) { 191 | $append = ' or c.id is null '; 192 | } 193 | } 194 | $filters[] = '(c.id in ('.implode(",",$qm).')'.$append.')'; 195 | } 196 | if ( (!empty($user)) && (!$admin) ){ 197 | $filters[] = "(user_uid = ?)"; 198 | $params[] = $user; 199 | 200 | } 201 | $group = ''; 202 | $groups = []; 203 | if (!empty($timegroup)){ 204 | // if($timegroup == 'week'){ 205 | // $groups[] = "YEARWEEK(start)"; 206 | // } elseif ($timegroup == 'day') { 207 | // $groups[] = "DATE(start)"; 208 | // } elseif ($timegroup == 'month') { 209 | // $groups[] = "EXTRACT(YEAR_MONTH FROM start)"; 210 | // } elseif ($timegroup == 'year') { 211 | // $groups[] = "YEAR(start)"; 212 | // } 213 | $groups[] = 'ftime'; 214 | } 215 | 216 | if (!empty($groupOn1)){ 217 | if ($groupOn1 == "project" || $groupOn1 == "client" || $groupOn1 == "name" || $groupOn1 == "userUid") 218 | // $groups[] = $groupOn1; 219 | if ($groupOn1 == 'name'){ 220 | $groups[] = 'wi.name'; 221 | } else { 222 | if ($this->dbengine == 'POSTGRES') { // postgres needs quotes on names 223 | $groups[] = '"'.$groupOn1.'"'; 224 | } else { 225 | $groups[] = $groupOn1; 226 | } 227 | } 228 | } 229 | if (!empty($groupOn2)){ 230 | if ($groupOn2 == "project" || $groupOn2 == "client" || $groupOn2 == "name" || $groupOn2 == "userUid"){ 231 | if ($groupOn2 == 'name'){ 232 | $groups[] = 'wi.name'; 233 | } else { 234 | if ($this->dbengine == 'POSTGRES') { // postgres needs quotes on names 235 | $groups[] = '"'.$groupOn2.'"'; 236 | } else { 237 | $groups[] = $groupOn2; 238 | } 239 | } 240 | } 241 | } 242 | 243 | if (!empty($groups)){ 244 | $group = "group by ".implode(",",$groups); 245 | } else { 246 | $group = "group by wi.id"; 247 | } 248 | if (empty($start)){ 249 | $start = 0; 250 | } 251 | if (empty($limit)){ 252 | $limit = 10000; 253 | } 254 | $sql = 'SELECT '.$selectItems.' where '.implode(" and ",$filters).' '.$group; 255 | //var_dump($sql); 256 | // var_dump($params); 257 | return $this->findEntities($sql, $params, $limit, $start); 258 | } 259 | 260 | 261 | 262 | } 263 | -------------------------------------------------------------------------------- /lib/Db/Tag.php: -------------------------------------------------------------------------------- 1 | addType('id', 'integer'); 19 | $this->addType('name', 'string'); 20 | $this->addType('createdAt', 'integer'); 21 | $this->addType('userUid', 'string'); 22 | } 23 | } -------------------------------------------------------------------------------- /lib/Db/TagMapper.php: -------------------------------------------------------------------------------- 1 | dbengine = 'MYSQL'; 14 | if (strpos(get_class($db->getDatabasePlatform()),'PostgreSQL') !== FALSE){ 15 | $this->dbengine = 'POSTGRES'; 16 | } else if (strpos(get_class($db->getDatabasePlatform()),'Sqlite') !== FALSE){ 17 | $this->dbengine = 'SQLITE'; 18 | } 19 | parent::__construct($db, 'timetracker_tag'); 20 | } 21 | 22 | 23 | public function findByNameUser($name, $userUid) { 24 | $sql = 'SELECT * FROM `*PREFIX*timetracker_tag` ' . 25 | 'WHERE upper(`name`) = ? and `user_uid` = ?'; 26 | 27 | try { 28 | $e = $this->findEntity($sql, [strtoupper($name), $userUid]); 29 | return $e; 30 | } catch (\OCP\AppFramework\Db\DoesNotExistException $e){ 31 | return null; 32 | } 33 | 34 | } 35 | 36 | /** 37 | * @throws \OCP\AppFramework\Db\DoesNotExistException if not found 38 | * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException if more than one result 39 | */ 40 | public function find($id) { 41 | $sql = 'SELECT * FROM `*PREFIX*timetracker_tag` ' . 42 | 'WHERE `id` = ?'; 43 | try { 44 | $e = $this->findEntity($sql, [$id]); 45 | return $e; 46 | } catch (\OCP\AppFramework\Db\DoesNotExistException $e){ 47 | return null; 48 | } 49 | } 50 | 51 | 52 | public function findAll($user){ 53 | $sql = 'SELECT tt.* FROM `*PREFIX*timetracker_tag` tt where tt.user_uid = ? order by tt.name asc'; 54 | return $this->findEntities($sql, [$user]); 55 | } 56 | 57 | public function findAllAlowedForProject($pid){ 58 | $sql = 'SELECT tt.* FROM `*PREFIX*timetracker_tag` tt join `*PREFIX*timetracker_lpa_tags` atg on tt.id = atg.tag_id where atg.project_id = ? order by tt.name asc'; 59 | 60 | return $this->findEntities($sql, [$pid]); 61 | } 62 | 63 | public function allowedTags($id, $tag_ids){ 64 | $sql = 'delete from `*PREFIX*timetracker_lpa_tags` where project_id = ?'; 65 | $this->execute($sql, [$id]); 66 | 67 | foreach ($tag_ids as $t){ 68 | if (empty($t)) 69 | continue; 70 | if ($this->dbengine == 'MYSQL'){ 71 | $sql = "insert into `*PREFIX*timetracker_lpa_tags` (project_id, tag_id, created_at) values(?,?,UNIX_TIMESTAMP(now()))"; 72 | } else if ($this->dbengine == 'POSTGRES'){ 73 | $sql = "insert into `*PREFIX*timetracker_lpa_tags` (project_id, tag_id, created_at) values(?,?,extract(epoch from now()))"; 74 | } else if ($this->dbengine == 'SQLITE'){ 75 | $sql = "insert into `*PREFIX*timetracker_lpa_tags` (project_id, tag_id, created_at) values(?,?,strftime('%s', 'now'))"; 76 | } 77 | $this->execute($sql, [$id, $t]); 78 | } 79 | return; 80 | } 81 | 82 | 83 | } 84 | -------------------------------------------------------------------------------- /lib/Db/Timeline.php: -------------------------------------------------------------------------------- 1 | addType('id', 'integer'); 26 | $this->addType('status', 'string'); 27 | $this->addType('userUid', 'string'); 28 | $this->addType('group1', 'string'); 29 | $this->addType('group2', 'string'); 30 | $this->addType('timeGroup', 'string'); 31 | $this->addType('filterProjects', 'string'); 32 | $this->addType('filterClients', 'string'); 33 | $this->addType('timeInterval', 'string'); 34 | $this->addType('totalDuration', 'string'); 35 | $this->addType('createdAt', 'integer'); 36 | } 37 | } -------------------------------------------------------------------------------- /lib/Db/TimelineEntry.php: -------------------------------------------------------------------------------- 1 | addType('id', 'integer'); 24 | $this->addType('timelineId', 'string'); 25 | $this->addType('userUid', 'string'); 26 | $this->addType('name', 'string'); 27 | $this->addType('projectName', 'string'); 28 | $this->addType('clientName', 'string'); 29 | $this->addType('timeInterval', 'string'); 30 | $this->addType('totalDuration', 'string'); 31 | $this->addType('createdAt', 'integer'); 32 | } 33 | } -------------------------------------------------------------------------------- /lib/Db/TimelineEntryMapper.php: -------------------------------------------------------------------------------- 1 | findEntity($sql, [$id]); 25 | } 26 | 27 | /** 28 | */ 29 | public function findTimelineEntries($tid) { 30 | $sql = 'SELECT * FROM `*PREFIX*timetracker_timeline_entry` ' . 31 | 'WHERE `timeline_id` = ?'; 32 | return $this->findEntities($sql, [$tid]); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /lib/Db/TimelineMapper.php: -------------------------------------------------------------------------------- 1 | findEntity($sql, [$id]); 25 | } 26 | 27 | 28 | 29 | public function findAll($uid) { 30 | $sql = 'SELECT * FROM `*PREFIX*timetracker_timeline` ' . 31 | 'WHERE `user_uid` = ?'; 32 | 33 | try { 34 | $e = $this->findEntities($sql, [$uid]); 35 | return $e; 36 | } catch (\OCP\AppFramework\Db\DoesNotExistException $e){ 37 | return null; 38 | } 39 | 40 | } 41 | 42 | public function findLatest() { 43 | $sql = 'SELECT * FROM `*PREFIX*timetracker_timeline` ' . 44 | 'order by created_at desc limit 100'; 45 | 46 | try { 47 | $e = $this->findEntities($sql, []); 48 | return $e; 49 | } catch (\OCP\AppFramework\Db\DoesNotExistException $e){ 50 | return null; 51 | } 52 | 53 | } 54 | 55 | public function findByStatus($status) { 56 | $sql = 'SELECT * FROM `*PREFIX*timetracker_timeline` ' . 57 | 'WHERE `status` = ?'; 58 | 59 | try { 60 | $e = $this->findEntities($sql, [$status]); 61 | return $e; 62 | } catch (\OCP\AppFramework\Db\DoesNotExistException $e){ 63 | return null; 64 | } 65 | 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/Db/UserToClient.php: -------------------------------------------------------------------------------- 1 | addType('id', 'integer'); 21 | $this->addType('userUid', 'string'); 22 | $this->addType('clientId', 'integer'); 23 | $this->addType('admin', 'integer'); 24 | $this->addType('access', 'integer'); 25 | $this->addType('createdAt', 'integer'); 26 | 27 | } 28 | } -------------------------------------------------------------------------------- /lib/Db/UserToClientMapper.php: -------------------------------------------------------------------------------- 1 | findEntity($sql, [$id]); 23 | return $e; 24 | } catch (\OCP\AppFramework\Db\DoesNotExistException $e){ 25 | return null; 26 | } 27 | 28 | } 29 | 30 | public function findAllForUser($uid) { 31 | $sql = 'SELECT * FROM `*PREFIX*timetracker_user_to_client` ' . 32 | 'WHERE `user_uid` = ?'; 33 | 34 | try { 35 | $e = $this->findEntities($sql, [$uid]); 36 | return $e; 37 | } catch (\OCP\AppFramework\Db\DoesNotExistException $e){ 38 | return null; 39 | } 40 | 41 | } 42 | 43 | public function findForUserAndClient($uid, $client) { 44 | $sql = 'SELECT * FROM `*PREFIX*timetracker_user_to_client` ' . 45 | 'WHERE `user_uid` = ? and client_id = ?'; 46 | 47 | try { 48 | $e = $this->findEntity($sql, [$uid, $client->id]); 49 | return $e; 50 | } catch (\OCP\AppFramework\Db\DoesNotExistException $e){ 51 | return null; 52 | } 53 | 54 | } 55 | 56 | 57 | 58 | } 59 | -------------------------------------------------------------------------------- /lib/Db/UserToProject.php: -------------------------------------------------------------------------------- 1 | addType('id', 'integer'); 21 | $this->addType('userUid', 'string'); 22 | $this->addType('projectId', 'integer'); 23 | $this->addType('admin', 'integer'); 24 | $this->addType('access', 'integer'); 25 | $this->addType('createdAt', 'integer'); 26 | 27 | } 28 | } -------------------------------------------------------------------------------- /lib/Db/UserToProjectMapper.php: -------------------------------------------------------------------------------- 1 | findEntity($sql, [$id]); 23 | return $e; 24 | } catch (\OCP\AppFramework\Db\DoesNotExistException $e){ 25 | return null; 26 | } 27 | 28 | } 29 | 30 | public function findAllForUser($uid) { 31 | $sql = 'SELECT * FROM `*PREFIX*timetracker_user_to_project` ' . 32 | 'WHERE `user_uid` = ?'; 33 | 34 | try { 35 | $e = $this->findEntities($sql, [$uid]); 36 | return $e; 37 | } catch (\OCP\AppFramework\Db\DoesNotExistException $e){ 38 | return null; 39 | } 40 | 41 | } 42 | public function findAllForProject($pid) { 43 | $sql = 'SELECT * FROM `*PREFIX*timetracker_user_to_project` ' . 44 | 'WHERE `project_id` = ?'; 45 | 46 | try { 47 | $e = $this->findEntities($sql, [$pid]); 48 | return $e; 49 | } catch (\OCP\AppFramework\Db\DoesNotExistException $e){ 50 | return null; 51 | } 52 | 53 | } 54 | public function findForUserAndProject($uid, $project) { 55 | $sql = 'SELECT * FROM `*PREFIX*timetracker_user_to_project` ' . 56 | 'WHERE `user_uid` = ? and project_id = ?'; 57 | 58 | try { 59 | $e = $this->findEntity($sql, [$uid, $project->id]); 60 | return $e; 61 | } catch (\OCP\AppFramework\Db\DoesNotExistException $e){ 62 | return null; 63 | } 64 | 65 | } 66 | public function deleteAllForProject($project_id) { 67 | $sql = 'delete FROM `*PREFIX*timetracker_user_to_project` ' . 68 | ' where project_id = ?'; 69 | 70 | try { 71 | $this->execute($sql, [$project_id]); 72 | return; 73 | } catch (\OCP\AppFramework\Db\DoesNotExistException $e){ 74 | return; 75 | } 76 | 77 | } 78 | 79 | 80 | 81 | } 82 | -------------------------------------------------------------------------------- /lib/Db/WorkInterval.php: -------------------------------------------------------------------------------- 1 | addType('id', 'integer'); 24 | $this->addType('name', 'string'); 25 | $this->addType('details', 'string'); 26 | $this->addType('projectId', 'integer'); 27 | $this->addType('userUid', 'string'); 28 | $this->addType('start', 'integer'); 29 | $this->addType('duration', 'integer'); 30 | $this->addType('running', 'integer'); 31 | $this->addType('cost', 'integer'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/Db/WorkIntervalMapper.php: -------------------------------------------------------------------------------- 1 | findEntity($sql, [$name]); 23 | return $e; 24 | } catch (\OCP\AppFramework\Db\DoesNotExistException $e){ 25 | return null; 26 | } 27 | 28 | } 29 | 30 | /** 31 | * @throws \OCP\AppFramework\Db\DoesNotExistException if not found 32 | * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException if more than one result 33 | */ 34 | public function find($id) { 35 | $sql = 'SELECT * FROM `*PREFIX*timetracker_work_interval` ' . 36 | 'WHERE `id` = ?'; 37 | return $this->findEntity($sql, [$id]); 38 | } 39 | 40 | 41 | public function findAllForWorkItem($workItemId, $limit=null, $offset=null) { 42 | $sql = 'SELECT * FROM `*PREFIX*timetracker_work_interval` where `work_item_id` = ?'; 43 | return $this->findEntities($sql, [$workItemId],$limit, $offset); 44 | } 45 | 46 | public function findLatest($user, $limit = 10, $offset = 0){ 47 | $sql = 'SELECT * FROM `*PREFIX*timetracker_work_interval` where user_uid = ? order by start desc'; 48 | return $this->findEntities($sql, [$user],$limit, $offset); 49 | } 50 | 51 | public function findLatestByName($user, $name){ 52 | $sql = 'SELECT * FROM `*PREFIX*timetracker_work_interval` where user_uid = ? and name = ? order by start desc'; 53 | try { 54 | return $this->findEntity($sql, [$user, $name], 1, 0); 55 | } catch (\OCP\AppFramework\Db\DoesNotExistException $e){ 56 | return null; 57 | } 58 | } 59 | 60 | public function findLatestInterval($user, $from, $to, $limit = 5000, $offset = 0){ 61 | $filters[] = "(user_uid = ?)"; 62 | $params[] = $user; 63 | 64 | if (!empty($from)){ 65 | $filters[] = "(start > ?)"; 66 | $params[] = $from; 67 | 68 | } 69 | if (!empty($to)){ 70 | $filters[] = "(start < ?)"; 71 | $params[] = $to; 72 | 73 | } 74 | 75 | $sql = 'SELECT * FROM `*PREFIX*timetracker_work_interval` where '.implode(" and ",$filters).' order by start desc'; 76 | return $this->findEntities($sql, $params, $limit, $offset); 77 | } 78 | 79 | public function findAllRunning($user, $limit = 100, $offset = 0){ 80 | $sql = 'SELECT * FROM `*PREFIX*timetracker_work_interval` where user_uid = ? and running = 1 order by start desc'; 81 | return $this->findEntities($sql, [$user],$limit, $offset); 82 | } 83 | 84 | public function stopAllRunning($user, $limit = 100, $offset = 0){ 85 | $sql = 'update wi FROM `*PREFIX*timetracker_work_interval` wi set wi.running = 0 where user_uid = ? and running = 1'; 86 | return $this->findEntities($sql, [$user],$limit, $offset); 87 | } 88 | 89 | public function findAllForProject($project_id){ 90 | $sql = 'SELECT * FROM `*PREFIX*timetracker_work_interval` where project_id = ?'; 91 | return $this->findEntities($sql, [$project_id]); 92 | } 93 | 94 | public function deleteAllForProject($project_id) { 95 | $sql = 'delete FROM `*PREFIX*timetracker_work_interval` ' . 96 | ' where project_id = ?'; 97 | 98 | try { 99 | $this->execute($sql, [$project_id]); 100 | return; 101 | } catch (\OCP\AppFramework\Db\DoesNotExistException $e){ 102 | return; 103 | } 104 | 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /lib/Db/WorkIntervalToTag.php: -------------------------------------------------------------------------------- 1 | addType('id', 'integer'); 19 | $this->addType('workIntervalId', 'integer'); 20 | $this->addType('tagId', 'integer'); 21 | $this->addType('createdAt', 'integer'); 22 | 23 | } 24 | } -------------------------------------------------------------------------------- /lib/Db/WorkIntervalToTagMapper.php: -------------------------------------------------------------------------------- 1 | findEntity($sql, [$id]); 23 | return $e; 24 | } catch (\OCP\AppFramework\Db\DoesNotExistException $e){ 25 | return null; 26 | } 27 | 28 | } 29 | 30 | public function findAllForWorkInterval($workIntervalId) { 31 | $sql = 'SELECT * FROM `*PREFIX*timetracker_workint_to_tag` ' . 32 | 'WHERE `work_interval_id` = ?'; 33 | 34 | try { 35 | $e = $this->findEntities($sql, [$workIntervalId]); 36 | return $e; 37 | } catch (\OCP\AppFramework\Db\DoesNotExistException $e){ 38 | return null; 39 | } 40 | 41 | } 42 | 43 | public function deleteAllForWorkInterval($workIntervalId) { 44 | $sql = 'DELETE FROM `*PREFIX*timetracker_workint_to_tag` ' . 45 | 'WHERE `work_interval_id` = ?'; 46 | 47 | try { 48 | $e = $this->execute($sql, [$workIntervalId]); 49 | return $e; 50 | } catch (\OCP\AppFramework\Db\DoesNotExistException $e){ 51 | return null; 52 | } 53 | 54 | } 55 | 56 | public function deleteAllForTag($tagId) { 57 | $sql = 'DELETE FROM `*PREFIX*timetracker_workint_to_tag` ' . 58 | 'WHERE `tag_id` = ?'; 59 | 60 | try { 61 | $e = $this->execute($sql, [$tagId]); 62 | return $e; 63 | } catch (\OCP\AppFramework\Db\DoesNotExistException $e){ 64 | return null; 65 | } 66 | 67 | } 68 | 69 | 70 | 71 | 72 | 73 | } 74 | -------------------------------------------------------------------------------- /lib/Db/WorkItem.php: -------------------------------------------------------------------------------- 1 | addType('id', 'integer'); 21 | $this->addType('name', 'string'); 22 | $this->addType('projectId', 'integer'); 23 | $this->addType('tagId', 'integer'); 24 | $this->addType('totalDuration', 'integer'); 25 | $this->addType('createdAt', 'integer'); 26 | $this->addType('userUid', 'string'); 27 | } 28 | } -------------------------------------------------------------------------------- /lib/Db/WorkItemMapper.php: -------------------------------------------------------------------------------- 1 | findEntity($sql, [$id]); 29 | return $e; 30 | } catch (\OCP\AppFramework\Db\DoesNotExistException $e){ 31 | return null; 32 | } 33 | } 34 | 35 | public function findByName($name) { 36 | $sql = 'SELECT * FROM `*PREFIX*timetracker_work_item` ' . 37 | 'WHERE `name` = ?'; 38 | 39 | try { 40 | $e = $this->findEntity($sql, [$name]); 41 | return $e; 42 | } catch (\OCP\AppFramework\Db\DoesNotExistException $e){ 43 | return null; 44 | } 45 | 46 | } 47 | 48 | 49 | public function findAll($limit=null, $offset=null) { 50 | $sql = 'SELECT * FROM `*PREFIX*timetracker_work_item`'; 51 | return $this->findEntities($sql, $limit, $offset); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /lib/Migration/Version000001Date20210719192031.php: -------------------------------------------------------------------------------- 1 | hasTable('timetracker_project')) { 26 | $table = $schema->getTable('timetracker_project'); 27 | if ($table->hasColumn('created_by_user_id')) { 28 | $schema->dropTable('timetracker_project'); 29 | $table = $schema->createTable('timetracker_project'); 30 | $table->addColumn('id', 'integer', [ 31 | 'autoincrement' => true, 32 | 'notnull' => true, 33 | ]); 34 | $table->addColumn('name', 'string', [ 35 | 'notnull' => true, 36 | 'length' => 64, 37 | ]); 38 | $table->addColumn('client_id', 'integer', [ 39 | 'notnull' => false, 40 | 'length' => 4, 41 | ]); 42 | $table->addColumn('created_by_user_uid', 'string', [ 43 | 'notnull' => true, 44 | 'length' => 128, 45 | ]); 46 | $table->addColumn('locked', 'integer', [ 47 | 'notnull' => false, 48 | 'default' => 0, 49 | 'length' => 4, 50 | ]); 51 | $table->addColumn('archived', 'integer', [ 52 | 'notnull' => false, 53 | 'default' => 0, 54 | 'length' => 4, 55 | ]); 56 | $table->addColumn('created_at', 'integer', [ 57 | 'notnull' => true, 58 | 'length' => 4, 59 | ]); 60 | $table->addColumn('color', 'string', [ 61 | 'notnull' => true, 62 | 'default' => '#ffffff', 63 | 'length' => 7, 64 | ]); 65 | 66 | $table->setPrimaryKey(['id']); 67 | } 68 | } 69 | 70 | if ($schema->hasTable('timetracker_timeline_entry')) { 71 | $table = $schema->getTable('timetracker_timeline_entry'); 72 | if ($table->hasColumn('user_id')) { 73 | $schema->dropTable('timetracker_timeline_entry'); 74 | $table = $schema->createTable('timetracker_timeline_entry'); 75 | $table->addColumn('id', 'integer', [ 76 | 'autoincrement' => true, 77 | 'notnull' => true, 78 | ]); 79 | $table->addColumn('timeline_id', 'integer', [ 80 | 'notnull' => false, 81 | 'length' => 4, 82 | ]); 83 | $table->addColumn('user_uid', 'text', [ 84 | 'notnull' => true, 85 | 'length' => 128, 86 | ]); 87 | $table->addColumn('name', 'text', [ 88 | 'notnull' => true, 89 | 'length' => 64, 90 | ]); 91 | $table->addColumn('project_name', 'text', [ 92 | 'notnull' => true, 93 | 'length' => 64, 94 | ]); 95 | $table->addColumn('client_name', 'text', [ 96 | 'notnull' => true, 97 | 'length' => 64, 98 | ]); 99 | $table->addColumn('time_interval', 'text', [ 100 | 'notnull' => true, 101 | 'length' => 64, 102 | ]); 103 | $table->addColumn('total_duration', 'text', [ 104 | 'notnull' => true, 105 | 'length' => 64, 106 | ]); 107 | $table->addColumn('created_at', 'integer', [ 108 | 'notnull' => true, 109 | 'length' => 4, 110 | ]); 111 | 112 | $table->setPrimaryKey(['id'], 'tt_t_e_id_idx'); 113 | } 114 | } 115 | 116 | return $schema; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /lib/Migration/Version000020Date20220528101009.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * @author Harm Akkerman 9 | * 10 | * @license GNU AGPL version 3 or any later version 11 | * 12 | * This program is free software: you can redistribute it and/or modify 13 | * it under the terms of the GNU Affero General Public License as 14 | * published by the Free Software Foundation, either version 3 of the 15 | * License, or (at your option) any later version. 16 | * 17 | * This program is distributed in the hope that it will be useful, 18 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | * GNU Affero General Public License for more details. 21 | * 22 | * You should have received a copy of the GNU Affero General Public License 23 | * along with this program. If not, see . 24 | * 25 | */ 26 | 27 | namespace OCA\TimeTracker\Migration; 28 | 29 | use Closure; 30 | use OCP\DB\ISchemaWrapper; 31 | use OCP\Migration\IOutput; 32 | use OCP\Migration\SimpleMigrationStep; 33 | 34 | /** 35 | * Auto-generated migration step: Please modify to your needs! 36 | */ 37 | class Version000020Date20220528101009 extends SimpleMigrationStep { 38 | 39 | /** 40 | * @param IOutput $output 41 | * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` 42 | * @param array $options 43 | */ 44 | public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { 45 | } 46 | 47 | /** 48 | * @param IOutput $output 49 | * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` 50 | * @param array $options 51 | * @return null|ISchemaWrapper 52 | */ 53 | public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { 54 | $schema = $schemaClosure(); 55 | if ($schema->hasTable('timetracker_work_interval')) { 56 | $table = $schema->getTable('timetracker_work_interval'); 57 | if (!$table->hasColumn('cost')) { 58 | $table->addColumn('cost', 'integer', [ 59 | 'notnull' => false, 60 | 'length' => 10, 61 | ]); 62 | } 63 | } 64 | 65 | return $schema; 66 | } 67 | 68 | /** 69 | * @param IOutput $output 70 | * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` 71 | * @param array $options 72 | */ 73 | public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /phpunit.integration.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ./tests/Integration 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ./tests/Unit 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /templates/content/clients.php: -------------------------------------------------------------------------------- 1 |
2 |

Clients

3 |
4 |
5 | 6 | 7 |
8 |
9 | 12 | 25 |
26 |
27 |
28 |
-------------------------------------------------------------------------------- /templates/content/dashboard.php: -------------------------------------------------------------------------------- 1 |
2 |

Dashboard

3 | 4 | 5 |
6 |
7 |
8 |

Time (in minutes) allocated to each client in the last days

9 |
10 |
11 |
12 |
13 |   14 | 15 |
16 |
17 | 18 | 19 |
20 |
21 |
22 |
23 | -------------------------------------------------------------------------------- /templates/content/goals.php: -------------------------------------------------------------------------------- 1 |
2 |

Goals

3 |
4 |
5 | 10 | 11 | 17 | 18 |
19 |
20 | 23 |
24 |
25 |
26 |
-------------------------------------------------------------------------------- /templates/content/index.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |
7 |
8 |
9 |
10 | 13 |
14 | 15 |
16 |
Manual entry
17 | 20 | 36 | 37 | 55 | 56 |
57 |
58 |
59 |
60 |   61 | 62 |
63 |
64 |
65 | 66 |
67 |
68 |
69 |
70 | -------------------------------------------------------------------------------- /templates/content/projects.php: -------------------------------------------------------------------------------- 1 |
2 |

Projects

3 |
4 |
5 | 6 | 7 | 8 |
9 | 10 |
11 |
12 |
13 | 14 | 15 |
16 |
17 |
18 | 19 |
20 | 24 |
25 | 26 |
27 | 30 | 77 |
78 | 79 |
80 |
81 |
-------------------------------------------------------------------------------- /templates/content/reports.php: -------------------------------------------------------------------------------- 1 |
2 |

Reports

3 | 4 | 7 | 20 |
21 |
22 |
23 |
24 |   25 | 26 |
27 |
28 |
29 |
30 | 39 | 48 | 55 | 57 |
58 |
59 | 64 | 69 |
70 |
71 | 72 | 73 | 75 |
76 | 77 |
78 |
79 |
-------------------------------------------------------------------------------- /templates/content/tags.php: -------------------------------------------------------------------------------- 1 |
2 |

Tags

3 |
4 |
5 | 6 | 7 |
8 |
9 | 12 | 25 |
26 |
27 |
28 |
-------------------------------------------------------------------------------- /templates/content/timelines-admin.php: -------------------------------------------------------------------------------- 1 |
2 |

Timelines Admin

3 | 4 | 7 | 20 |
21 |
22 | 40 | 41 |
42 |
43 |
-------------------------------------------------------------------------------- /templates/content/timelines.php: -------------------------------------------------------------------------------- 1 |
2 |

Timelines

3 | 4 | 7 | 20 | 43 |
44 |
45 |
46 |
47 |   48 | 49 |
50 |
51 |
52 |
53 | 60 | 67 | 73 | 75 |
76 |
77 | 82 | 87 |
88 |
89 | 90 | 91 | 92 | 93 |
94 | 95 |
96 |
97 |
98 |

Exported Timelines Statuses

99 |
100 |
101 |
102 |
-------------------------------------------------------------------------------- /templates/index.php: -------------------------------------------------------------------------------- 1 | = 28) { 9 | style('timetracker', 'style-compat'); 10 | } 11 | 12 | script('timetracker', $script); 13 | 14 | ?> 15 | 16 |
17 | inc('navigation/index')); ?> 18 |
19 | 20 |
21 |
22 | inc($appPage)); ?> 23 |
24 |
25 | -------------------------------------------------------------------------------- /templates/navigation/index.php: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /templates/settings/index.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | 6 |
7 |
8 | 9 |
10 |
11 | -------------------------------------------------------------------------------- /tests/Integration/AppTest.php: -------------------------------------------------------------------------------- 1 | container = $app->getContainer(); 22 | } 23 | 24 | public function testAppInstalled() { 25 | $appManager = $this->container->query('OCP\App\IAppManager'); 26 | $this->assertTrue($appManager->isInstalled('timetracker')); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /tests/Unit/Controller/PageControllerTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder('OCP\IRequest')->getMock(); 18 | 19 | $this->controller = new PageController( 20 | 'timetracker', $request, $this->userId 21 | ); 22 | } 23 | 24 | public function testIndex() { 25 | $result = $this->controller->index(); 26 | 27 | $this->assertEquals('index', $result->getTemplateName()); 28 | $this->assertTrue($result instanceof TemplateResponse); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | addValidRoot(OC::$SERVERROOT . '/tests'); 11 | 12 | // Fix for "Autoload path not allowed: .../timetracker/tests/testcase.php" 13 | \OC_App::loadApp('timetracker'); 14 | 15 | if(!class_exists('PHPUnit_Framework_TestCase')) { 16 | require_once('PHPUnit/Autoload.php'); 17 | } 18 | 19 | OC_Hook::clear(); 20 | -------------------------------------------------------------------------------- /webfonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtierltd/timetracker/abc60274f89a84a6be32cc2342ef7069d149d306/webfonts/fa-brands-400.eot -------------------------------------------------------------------------------- /webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtierltd/timetracker/abc60274f89a84a6be32cc2342ef7069d149d306/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /webfonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtierltd/timetracker/abc60274f89a84a6be32cc2342ef7069d149d306/webfonts/fa-brands-400.woff -------------------------------------------------------------------------------- /webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtierltd/timetracker/abc60274f89a84a6be32cc2342ef7069d149d306/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /webfonts/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtierltd/timetracker/abc60274f89a84a6be32cc2342ef7069d149d306/webfonts/fa-regular-400.eot -------------------------------------------------------------------------------- /webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtierltd/timetracker/abc60274f89a84a6be32cc2342ef7069d149d306/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /webfonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtierltd/timetracker/abc60274f89a84a6be32cc2342ef7069d149d306/webfonts/fa-regular-400.woff -------------------------------------------------------------------------------- /webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtierltd/timetracker/abc60274f89a84a6be32cc2342ef7069d149d306/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtierltd/timetracker/abc60274f89a84a6be32cc2342ef7069d149d306/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtierltd/timetracker/abc60274f89a84a6be32cc2342ef7069d149d306/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtierltd/timetracker/abc60274f89a84a6be32cc2342ef7069d149d306/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtierltd/timetracker/abc60274f89a84a6be32cc2342ef7069d149d306/webfonts/fa-solid-900.woff2 --------------------------------------------------------------------------------