├── .eslintrc.js ├── .gitignore ├── .l10nignore ├── .php_cs.dist ├── CHANGELOG.md ├── COPYING ├── LICENSE ├── Makefile ├── README.md ├── appinfo ├── info.xml └── routes.php ├── babel.config.js ├── bin └── tools │ └── file_from_env.php ├── composer.json ├── composer.lock ├── css ├── app-sidebar.scss ├── custom.scss ├── settings.scss ├── shifts.css └── shifts.scss ├── docs ├── INSTALL-DE.md ├── INSTALL-EN.md ├── README.md └── frontpage.png ├── img ├── app-dark.svg ├── app.svg └── mdi-swap-horizontal.svg ├── l10n ├── .gitkeep ├── ca.js ├── ca.json ├── cs.js ├── cs.json ├── de.js ├── de.json ├── es.js └── es.json ├── lib ├── AppInfo │ └── Application.php ├── Controller │ ├── Errors.php │ ├── PageController.php │ ├── SettingsController.php │ ├── ShiftController.php │ ├── ShiftsCalendarChangeController.php │ ├── ShiftsCalendarController.php │ ├── ShiftsChangeController.php │ └── ShiftsTypeController.php ├── Db │ ├── Shift.php │ ├── ShiftMapper.php │ ├── ShiftsCalendarChange.php │ ├── ShiftsCalendarChangeMapper.php │ ├── ShiftsChange.php │ ├── ShiftsChangeMapper.php │ ├── ShiftsType.php │ └── ShiftsTypeMapper.php ├── Migration │ ├── Version100001Date20181013124731.php │ ├── Version110000Date20210308091041.php │ ├── Version120000Date20210406073005.php │ ├── Version140000Date20210420123817.php │ ├── Version150000Date20210506123426.php │ ├── Version160000Date20210824102626.php │ ├── Version183000Date20211117121300.php │ └── Version184000Date20211215101000.php ├── Service │ ├── InvalidArgumentException.php │ ├── NotFoundException.php │ ├── PermissionException.php │ ├── PermissionService.php │ ├── ServiceException.php │ ├── ShiftService.php │ ├── ShiftsCalendarChangeService.php │ ├── ShiftsCalendarService.php │ ├── ShiftsChangeService.php │ └── ShiftsTypeService.php └── Settings │ ├── AdminSection.php │ ├── AdminSettings.php │ └── Settings.php ├── package-lock.json ├── package.json ├── phpunit.integration.xml ├── phpunit.xml ├── settings ├── Settings.vue ├── settings.js └── store │ ├── index.js │ └── settings.js ├── src ├── App.vue ├── components │ ├── Archive │ │ ├── ArchiveContent.vue │ │ └── ArchiveTopBar.vue │ ├── Calendar │ │ ├── Calendar.vue │ │ └── CalendarTopBar.vue │ ├── ChangeRequests │ │ └── ChangeRequestModal.vue │ ├── Modal │ │ └── ShiftsTypeModal.vue │ └── Navigation │ │ └── AppNavigation.vue ├── main.js ├── models │ └── consts.js ├── router.js ├── store │ ├── archive.js │ ├── database.js │ ├── index.js │ ├── newShiftInstance.js │ ├── settings.js │ └── shiftsTypeInstance.js ├── utils │ ├── date.js │ ├── logger.js │ ├── router.js │ └── timezone.js └── views │ ├── Archive.vue │ ├── NewShift.vue │ ├── Requests.vue │ ├── Shifts.vue │ └── ShiftsTypes.vue ├── stylelint.config.js ├── templates ├── main.php └── settings.php ├── tests ├── Integration │ ├── ShiftsIntegrationTest.php │ └── ShiftsTypeIntegrationTest.php ├── Unit │ └── Service │ │ └── ShiftsServiceTest.php └── bootstrap.php ├── timezones ├── README.md ├── update-zones.py └── zones.json ├── translationfiles ├── ca │ └── shifts.po ├── de │ └── shifts.po ├── es │ └── shifts.po └── templates │ └── shifts.pot └── webpack.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | 'no-console': 'off', 4 | }, 5 | extends: [ 6 | '@nextcloud', 7 | ], 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /vendor/ 3 | .idea/ 4 | js/ 5 | build/ 6 | -------------------------------------------------------------------------------- /.l10nignore: -------------------------------------------------------------------------------- 1 | shifts-main.js 2 | -------------------------------------------------------------------------------- /.php_cs.dist: -------------------------------------------------------------------------------- 1 | getFinder() 12 | ->notPath('build') 13 | ->notPath('l10n') 14 | ->notPath('src') 15 | ->notPath('vendor') 16 | ->in(__DIR__); 17 | return $config; 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | 8 | ## [0.0.2] - 2017-07-31 9 | 10 | ### Added 11 | 12 | - First release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 csoc-de 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # This file is licensed under the Affero General Public License version 3 or 2 | # later. See the COPYING file. 3 | app_name=shifts 4 | build_tools_directory=$(CURDIR)/build/tools 5 | composer=$(shell which composer 2> /dev/null) 6 | 7 | project_dir=$(CURDIR)/../$(notdir $(CURDIR)) 8 | build_dir=$(CURDIR)/build/artifacts 9 | appstore_dir=$(build_dir)/appstore 10 | source_dir=$(build_dir)/source 11 | sign_dir=$(build_dir)/sign 12 | package_name=$(app_name) 13 | cert_dir=$(HOME)/.nextcloud/ 14 | version = 1.9.10 15 | 16 | all: dev-setup lint build-js-production test 17 | 18 | # Dev env management 19 | dev-setup: clean clean-dev composer npm-init 20 | 21 | 22 | # Installs and updates the composer dependencies. If composer is not installed 23 | # a copy is fetched from the web 24 | composer: 25 | ifeq (, $(composer)) 26 | @echo "No composer command available, downloading a copy from the web" 27 | mkdir -p $(build_tools_directory) 28 | curl -sS https://getcomposer.org/installer | php 29 | mv composer.phar $(build_tools_directory) 30 | php $(build_tools_directory)/composer.phar install --prefer-dist 31 | php $(build_tools_directory)/composer.phar update --prefer-dist 32 | else 33 | composer install --prefer-dist 34 | composer update --prefer-dist 35 | endif 36 | 37 | all: dev-setup build-js-production 38 | 39 | dev-setup: clean-dev npm-init 40 | 41 | dependabot: dev-setup npm-update build-js-production 42 | 43 | release: appstore 44 | 45 | release-tag: appstore create-tag 46 | 47 | build-js: 48 | npm --openssl-legacy-provider run dev 49 | 50 | build-js-production: 51 | npm run build 52 | 53 | watch-js: 54 | npm run watch 55 | 56 | test: 57 | npm run test:unit 58 | 59 | lint: 60 | npm run lint 61 | 62 | lint-fix: 63 | npm run lint:fix 64 | 65 | npm-init: 66 | npm ci 67 | 68 | npm-update: 69 | npm update 70 | 71 | clean: 72 | rm -rf js/* 73 | rm -rf $(build_dir) 74 | 75 | clean-dev: clean 76 | rm -rf node_modules 77 | 78 | create-tag: 79 | git tag -a v$(version) -m "Tagging the $(version) release." 80 | git push origin v$(version) 81 | 82 | # Tests 83 | test: 84 | ./vendor/phpunit/phpunit/phpunit -c phpunit.xml 85 | ./vendor/phpunit/phpunit/phpunit -c phpunit.integration.xml 86 | 87 | appstore: 88 | rm -rf $(build_dir) 89 | mkdir -p $(sign_dir) 90 | mkdir -p $(cert_dir) 91 | rsync -a \ 92 | --exclude=babel.config.js \ 93 | --exclude=/build \ 94 | --exclude=composer.json \ 95 | --exclude=composer.lock \ 96 | --exclude=docs \ 97 | --exclude=.idea \ 98 | --exclude=.drone.yml \ 99 | --exclude=.eslintignore \ 100 | --exclude=.eslintrc.js \ 101 | --exclude=.git \ 102 | --exclude=.gitattributes \ 103 | --exclude=.github \ 104 | --exclude=.gitignore \ 105 | --exclude=jest.config.js \ 106 | --exclude=.l10nignore \ 107 | --exclude=mkdocs.yml \ 108 | --exclude=Makefile \ 109 | --exclude=node_modules \ 110 | --exclude=package.json \ 111 | --exclude=package-lock.json \ 112 | --exclude=.php_cs.dist \ 113 | --exclude=.php_cs.cache \ 114 | --exclude=README.md \ 115 | --exclude=src \ 116 | --exclude=.stylelintignore \ 117 | --exclude=stylelint.config.js \ 118 | --exclude=.tx \ 119 | --exclude=tests \ 120 | --exclude=vendor \ 121 | --exclude=webpack.*.js \ 122 | $(project_dir)/ $(sign_dir)/$(app_name) 123 | @if [ -f $(cert_dir)/$(app_name).key ]; then \ 124 | echo "Signing app files…"; \ 125 | docker exec master-nextcloud-1 adduser --uid 1001 --disabled-password --gecos "" builder; \ 126 | docker exec -u builder master-nextcloud-1 php /var/www/html/occ integrity:sign-app \ 127 | --privateKey=/$(app_name).key\ 128 | --certificate=/$(app_name).crt\ 129 | --path=/var/www/html/apps-extra/$(app_name); \ 130 | fi 131 | tar -czf $(build_dir)/$(app_name)-$(version).tar.gz \ 132 | -C $(sign_dir) $(app_name) 133 | @if [ -f $(cert_dir)/$(app_name).key ]; then \ 134 | echo "Signing package…"; \ 135 | openssl dgst -sha512 -sign $(cert_dir)/$(app_name).key $(build_dir)/$(app_name)-$(version).tar.gz | openssl base64; \ 136 | fi 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nextcloud Shifts 2 | 3 | A shiftsplaner app for [Nextcloud](https://nextcloud.com). 4 | 5 | 6 |  7 | 8 | 9 | ## Why is this so awesome? 10 | 11 | * **Interactions with the Nextcloud Calender app!** Easy integration into the existing Calender of Nextcloud. 12 | * **Manage and organize your Shifts System!** Customize your Shifts Model with different Shifttypes 13 | * **Separation of Users by different Nextcloud user Groups!** Divide your Workforce with different Skill-Levels 14 | 15 | 16 | 17 | More to come: 18 | * Further maturing of the app 19 | * Individualisation 20 | 21 | 22 | If you experience any issues or have any suggestions for improvement, use the issue tracker. 23 | 24 | ## Get on board 25 | For new contributors, please check out [ContributingToNextcloudIntroductoryWorkshop](https://github.com/sleepypioneer/ContributingToNextcloudIntroductoryWorkshop) 26 | 27 | 28 | ## Development 29 | ### Setup 30 | 31 | Just clone this repo into your apps directory ([Nextcloud server](https://github.com/nextcloud/server#running-master-checkouts) installation needed). Additionally, [npm](https://www.npmjs.com/) to fetch [Node.js](https://nodejs.org/en/download/package-manager/) is needed for installing JavaScript dependencies. 32 | 33 | Once npm and Node.js are installed, PHP and JavaScript dependencies can be installed by running: 34 | ```bash 35 | make dev-setup 36 | ``` 37 | 38 | ### Translation 39 | Documentation for Nextcloud translation: https://docs.nextcloud.com/server/22/developer_manual/basics/front-end/l10n.html 40 | 41 | Nextcloud translation tool: https://github.com/nextcloud/docker-ci/tree/master/translations/translationtool 42 | 43 | 1. Generate .pot file: ```translationtool.phar create-pot-files``` 44 | 2. Copy the template file into the language directory: ```cp shifts/translationfiles/templates/shifts.pot shifts/translationfiles/de/shifts.po``` 45 | 3. Edit po file 46 | 4. Convert po file: ```translationtool.phar convert-po-files``` 47 | 48 | ## Documentation 49 | 50 | * [Admin documentation](docs/README.md) (installation, configuration, troubleshooting) 51 | -------------------------------------------------------------------------------- /appinfo/info.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 10 | shifts 11 | Shiftplan 12 | App for organising Shifts 13 | 24 | 1.9.10 25 | agpl 26 | CSOC 27 | Shifts 28 | office 29 | organization 30 | https://github.com/csoc-de/Shifts 31 | https://github.com/csoc-de/Shifts/-/issues 32 | https://github.com/csoc-de/Shifts/-/issues 33 | https://github.com/csoc-de/shifts.git 34 | https://raw.githubusercontent.com/csoc-de/shifts/master/docs/frontpage.png 35 | 36 | 37 | 38 | 39 | OCA\Shifts\Settings\AdminSettings 40 | OCA\Shifts\Settings\AdminSection 41 | 42 | 43 | 44 | Shift plan 45 | shifts.page.index 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /appinfo/routes.php: -------------------------------------------------------------------------------- 1 | 4 | * @copyright Copyright (c) 2023. Kevin Küchler 5 | * 6 | * @author Fabian Kirchesch 7 | * @author Kevin Küchler 8 | */ 9 | 10 | return [ 11 | 'resources' => [ 12 | 'shift' => ['url' => '/shifts'], 13 | 'shiftsType' => ['url' => '/shiftsType'], 14 | 'shiftsChange' => ['url' => '/shiftsChange'], 15 | 'shiftsCalendar' => ['url' => '/shiftsCalendar'], 16 | 'shiftsCalendarChange' => ['url' => '/shiftsCalendarChange'], 17 | ], 18 | 'routes' => [ 19 | ['name' => 'page#index', 'url' => '/', 'verb' => 'GET'], 20 | ['name' => 'page#index', 'url' => '/timeline', 'verb' => 'GET', 'postfix' => 'direct.timeline'], 21 | ['name' => 'page#index', 'url' => '/requests', 'verb' => 'GET', 'postfix' => 'direct.requests'], 22 | ['name' => 'page#index', 'url' => '/shiftsTypes', 'verb' => 'GET', 'postfix' => 'direct.shiftsTypes'], 23 | ['name' => 'page#index', 'url' => '/archive', 'verb' => 'GET', 'postfix' => 'direct.archive'], 24 | ['name' => 'shift#getGroupStatus', 'url' => '/checkAdmin', 'verb' => 'GET'], 25 | ['name' => 'shift#getAllAnalysts', 'url' => '/getAllAnalysts', 'verb' => 'GET'], 26 | ['name' => 'shift#getAnalystsExcludingCurrent', 'url' => '/getAnalysts', 'verb' => 'GET'], 27 | ['name' => 'shift#getShiftsByUserId', 'url' => '/shifts/getAllByUserId', 'verb' => 'GET'], 28 | ['name' => 'shift#getCurrentUserId', 'url' => '/getCurrentUserId', 'verb' => 'GET'], 29 | ['name' => 'shift#triggerUnassignedShifts', 'url' => '/triggerUnassignedShifts', 'verb' => 'GET'], 30 | ['name' => 'shift#getShiftsDataByTimeRange', 'url' => '/getShiftsDataByTimeRange/{start}/{end}', 'verb' => 'GET'], 31 | ['name' => 'settings#getSettings', 'url' => '/settings', 'verb' => 'GET'], 32 | ['name' => 'settings#saveSettings', 'url' => '/settings', 'verb' => 'PUT'], 33 | ['name' => 'shiftsCalendar#index', 'url' => '/shiftsCalendar', 'verb' => 'GET'], 34 | ['name' => 'shiftsCalendar#create', 'url' => '/shiftsCalendar', 'verb' => 'POST'], 35 | ['name' => 'shiftsCalendar#update', 'url' => '/shiftsCalendar', 'verb' => 'PUT'], 36 | ['name' => 'shiftsCalendar#delete', 'url' => '/shiftsCalendar', 'verb' => 'DELETE'], 37 | ['name' => 'shiftsCalendar#synchronize', 'url' => '/shiftsCalendar', 'verb' => 'PATCH'], 38 | ] 39 | ]; 40 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | '@babel/plugin-syntax-dynamic-import', 4 | ], 5 | presets: ['@babel/preset-env'], 6 | } 7 | -------------------------------------------------------------------------------- /bin/tools/file_from_env.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 10 | * @copyright Benjamin Brahmer 2020 11 | */ 12 | 13 | if ($argc < 2) { 14 | echo "This script expects two parameters:\n"; 15 | echo "./file_from_env.php ENV_VAR PATH_TO_FILE\n"; 16 | exit(1); 17 | } 18 | 19 | # Read environment variable 20 | $content = getenv($argv[1]); 21 | 22 | if (!$content){ 23 | echo "Variable was empty\n"; 24 | exit(1); 25 | } 26 | 27 | file_put_contents($argv[2], $content); 28 | 29 | echo "Done...\n"; 30 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "csoc/shifts", 3 | "description": "Shifts Organisation", 4 | "type": "project", 5 | "license": "AGPL", 6 | "authors": [ 7 | { 8 | "name": "Fabian Kirchesch", 9 | "email": "fabian.kirchesch@csoc.de" 10 | } 11 | ], 12 | "require": { 13 | "ext-json": "*" 14 | }, 15 | "require-dev": { 16 | "phpunit/phpunit": "^7.5", 17 | "nextcloud/coding-standard": "^0.5.0" 18 | }, 19 | "config": { 20 | "optimize-autoloader": true, 21 | "classmap-authoritative": true, 22 | "platform": { 23 | "php": "7.4" 24 | } 25 | }, 26 | "scripts": { 27 | "lint": "find . -name \\*.php -not -path './vendor/*' -not -path './build/*' -print0 | xargs -0 -n1 php -l", 28 | "cs:check": "php-cs-fixer fix --dry-run --diff", 29 | "cs:fix": "php-cs-fixer fix" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /css/settings.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * @copyright Copyright (c) 2021. Fabian Kirchesch 3 | * 4 | * @author Fabian Kirchesch 5 | */ 6 | 7 | #admin_settings { 8 | padding: 30px; 9 | } 10 | 11 | .settings_container { 12 | display: flex; 13 | } 14 | 15 | .settings_container label { 16 | width: 300px; 17 | } 18 | 19 | .settings_container input { 20 | width: auto; 21 | } 22 | 23 | #skillGroupsContainer p { 24 | width: 300px; 25 | } 26 | 27 | #addNewSkillGroup { 28 | height: fit-content; 29 | margin-left: 20px; 30 | } 31 | -------------------------------------------------------------------------------- /css/shifts.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * @copyright Copyright (c) 2021. Fabian Kirchesch 3 | * @copyright Copyright (c) 2023. Kevin Küchler 4 | * 5 | * @author Fabian Kirchesch 6 | * @author Kevin Küchler 7 | */ 8 | 9 | .v-row { 10 | display: flex; 11 | flex-wrap: wrap; 12 | flex: 1 1 auto; 13 | margin: -12px; 14 | } 15 | 16 | .v-col { 17 | flex-basis: 0; 18 | flex-grow: 1; 19 | max-width: 100%; 20 | } 21 | -------------------------------------------------------------------------------- /css/shifts.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * @copyright Copyright (c) 2021. Fabian Kirchesch 3 | * 4 | * @author Fabian Kirchesch 5 | */ 6 | 7 | @import 'app-sidebar'; 8 | @import 'custom'; 9 | -------------------------------------------------------------------------------- /docs/INSTALL-DE.md: -------------------------------------------------------------------------------- 1 | # Installation Nextcloud-Shifts 2 | 3 | ## Voraussetzung 4 | 5 | * Benötigte Nextcloud Konfigurationen 6 | * Analysten-Gruppe mit dem Namen "analyst" (Falls andere Namen besser passen ist eine kleine Änderung im Code notwendig) 7 | * Schichten-Administratoren-Gruppe mit dem Namen "ShiftsAdmin" (gleiches gilt hier) 8 | * Schichtplan-Admin der Organisator für die Schichteneinträge im Kalender(name: 'shiftsorganizer', Email: 'shifts@csoc.de', kann natürlich geändert werden, muss nur im Code aktualiesiert werden) 9 | * Leitstellen Schichtplan Kalender mit Bearbeitungsrechten für "ShiftsAdmin" 10 | * Name übernommen von momentanem Plan, nachträgliche Änderung des Namen benötigt zunächst noch Codeänderung (in Zukunft wird dies nicht mehr benötigt) 11 | * Schichtplan Admin muss der Ersteller des Kalenders sein (Ich habe leider noch keinen Weg gefunden dies zu umgehen) 12 | * Benötigte Server Konfiguration 13 | * git installiert 14 | * npm und Node installiert 15 | 16 | 17 | ## Installation 18 | 19 | ``` 20 | cd /var/www/ 21 | sudo mkdir .cache 22 | sudo mkdir .config 23 | chown -R www-data .cache 24 | chown -R www-data .config 25 | ``` 26 | 27 | Öffne Nextcloud Ordner mit Kommdanozeile 28 | 29 | normalerweise `/var/www/nextcloud` (Apache2 Webserver) 30 | 31 | Navigiere in den Apps Ordner 32 | 33 | ``` 34 | cd apps/ 35 | ``` 36 | 37 | Klonen des Git Repositories 38 | 39 | ``` 40 | git clone https://github.com/csoc-de/shifts (Nur erreichbar mit Allgemeinem VPN) 41 | ``` 42 | 43 | Navigiere in Shifts Ordner 44 | 45 | ``` 46 | cd shifts/ 47 | ``` 48 | 49 | Installation der benötigten PHP und Node Pakete 50 | 51 | ``` 52 | sudo -u www-data make composer 53 | sudo -u www-data make dev-setup 54 | ``` 55 | 56 | Kompilieren des Frontend-Codes 57 | 58 | ``` 59 | sudo -u www-data make build-js 60 | ``` 61 | 62 | Anschließende Aktivierung der App "Shifts"in den Nextcloud Apps-Einstellungen 63 | 64 | 65 | ## Allgemeine Hinweise 66 | - Die App/Nextcloud sind case-sensitiv. Die Gruppe 'Analyst' ist nicht das Selbe wie 'analyst'. Bitte prüfe, ob die Gruppenname **exakt** die Selben sind. 67 | - Wenn eine Nextcloud-Gruppe umbenannt wird, kann es sein, dass sich die ID im Hintergrund nicht ändert ([#65](https://github.com/csoc-de/Shifts/issues/65)) 68 | -------------------------------------------------------------------------------- /docs/INSTALL-EN.md: -------------------------------------------------------------------------------- 1 | # Installation Nextcloud Shifts 2 | 3 | ## Prerequisite 4 | 5 | * Required Nextcloud configurations 6 | * Analysts group with the name "analyst" (If other names fit better a small change in the code is necessary) 7 | * Shifts admin group with the name "ShiftsAdmin" (same here). 8 | * Shift schedule admin the organizer for the shift entries in the calendar(name: 'shiftsorganizer', email: 'shifts@csoc.de', can be changed of course, just needs to be updated in the code). 9 | * Control center shift plan calendar with editing rights for "ShiftsAdmin". 10 | * Name taken from current plan, subsequent change of name still needs code change at first (in future this will not be needed) 11 | * Shift plan admin must be the creator of the calendar (I haven't found a way to work around this yet) 12 | * Required server configuration 13 | * git installed 14 | * npm and node installed 15 | 16 | ## Installation 17 | 18 | ``` 19 | cd /var/www/ 20 | sudo mkdir .cache 21 | sudo mkdir .config 22 | chown -R www-data .cache 23 | chown -R www-data .config 24 | ``` 25 | 26 | Open Nextcloud folder with command line 27 | 28 | usually `/var/www/nextcloud` (Apache2 web server) 29 | 30 | Navigate to the Apps folder 31 | 32 | ``` 33 | cd apps/ 34 | ``` 35 | 36 | Cloning the Git repository 37 | 38 | ``` 39 | git clone https://github.com/csoc-de/shifts 40 | ``` 41 | 42 | Navigate to Shift's folder 43 | 44 | ``` 45 | cd shifts/ 46 | ``` 47 | 48 | Install the required PHP and Node packages 49 | 50 | ``` 51 | sudo -u www-data make composer 52 | sudo -u www-data make dev-setup 53 | ``` 54 | 55 | Compiling the frontend code 56 | 57 | ``` 58 | sudo -u www-data make build-js 59 | ``` 60 | 61 | Then activate the app "Shifts" in the Nextcloud Apps settings. 62 | 63 | ## General hints 64 | - The app/nextcloud groups are case sensitiv. It will not recognise 'Analyst' as 'analyst'. Please double check if the names you enter are the **exact** same. 65 | - If you rename a group in nextcloud the corresponting ID migh not change as well (see [#65](https://github.com/csoc-de/Shifts/issues/65)) 66 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Shifts Admin Documentation 2 | 3 | ## Installation 4 | 5 | In your Nextcloud, simply navigate to »Apps«, choose the category »Organization«, find the shifts app and enable it. After installing the app onto your Nextcloud you need to setup different groups and settings explained below. 6 | ## Configuration 7 | ### Groups 8 | 9 | | Group | Default | Purpose | 10 | |---|---|---| 11 | | Shiftworkers | Analyst | Group to identify all Shiftworkers that can take shifts | 12 | | Shiftadmin | Shiftsadmin | Administrators to organize and plan the Shifts | 13 | | Skillgroups | Level 1 | Skill groups which can be assigned to new Shifttypes which can then only be taken over by Shiftworkers with an equal or higher Skillgroup | 14 | 15 | ### Additional Strings 16 | 17 | | String | Default | Purpose | 18 | |---|---|---| 19 | | CalendarOrganizer | admin | Account which acts as an organizer for the Nextcloud-Calendar events | 20 | | CalendarOrganizerEmail | admin@test.com | Email of the afformentionend Organizer | 21 | | CalendarName | ShiftsCalendar | Name of the Calendar where the shifts are saved | 22 | 23 | ### Explaining some Funtionality 24 | 25 | #### The Rules Tab for new Shift-types 26 | 27 | These Options allow the Admin to define how many shifts are needed for a given weekday. When updating these values, open shifts will either be added or removed upon saving the changes. 28 | 29 | When 0 is given, there will not be any open shifts added, but the admins can still add shifts of this type to the Plan. 30 | 31 | When X is given, the app will add X amount of open shifts to the databse for a given weekday. 32 | -------------------------------------------------------------------------------- /docs/frontpage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csoc-de/Shifts/9473a98e0595b2bc3179ea28960ec4feb64bd9be/docs/frontpage.png -------------------------------------------------------------------------------- /img/app-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ic_fluent_shifts_team_24_regular 5 | Created with Sketch. 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /img/app.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ic_fluent_shifts_team_24_regular 5 | Created with Sketch. 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /img/mdi-swap-horizontal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /l10n/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csoc-de/Shifts/9473a98e0595b2bc3179ea28960ec4feb64bd9be/l10n/.gitkeep -------------------------------------------------------------------------------- /l10n/ca.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "shifts", 3 | { 4 | "Month" : "Mes", 5 | "Week" : "Setmana", 6 | "Open Shifts" : "Crear Torns", 7 | "Search for Emails or Users" : "Cerca Usuàries o Correus", 8 | "No match found" : "No es van trobar coincidències", 9 | "Swap" : "Intercanviar", 10 | "Offer" : "Oferir", 11 | "Could not fetch shifts" : "No vaig poder obtenir torns", 12 | "Analyst" : "Persona", 13 | "Select Shiftstype" : "Selecciona el tipus de torn", 14 | "Could not save shifts Changes" : "No vaig poder guardar els canvis en els torns", 15 | "Could not fetch data" : "No vaig poder obtenir les dades", 16 | "Could not fetch shifts-types" : "No vaig poder obtenir els tipus de torns", 17 | "Could not fetch shifts-changes" : "No vaig poder obtenir els canvis en els torns", 18 | "Could not delete shift" : "No vaig poder esborrar el torn", 19 | "Could not delete shiftsType" : "No vaig poder esborrar el tipus de torn", 20 | "Could not update Calendar" : "No pude actualizar el calendario", 21 | "Could not create the shift" : "No vaig poder actualitzar el calendari", 22 | "No Analysts or Dates for Shift given" : "No hi ha persones ni dates en el Torn indicat", 23 | "Could not fetch Settings" : "No vaig poder obtenir la configuració", 24 | "Please set a Calendarname in the App-Settings" : "Si us plau escriu el nom del calendari en la configuració de l'aplicació", 25 | "Please set an Organizername in the App-Settings" : "Si us plau escriu el nom del qui organitza els torns en la configuració de l'aplicació", 26 | "Please set an Organizeremail in the App-Settings" : "Si us plau escriu el correu electrònic del qui organitza els torns en la configuració de l'aplicació", 27 | "Please set at least one Skillgroup in the App-Settings" : "Si us plau escriu almenys un nom d'una categoria de grup en la configuració de l'aplicació", 28 | "Could not save the shiftType" : "No vaig poder guardar el tipus de torn", 29 | "Today" : "Avui", 30 | "Remove analyst" : "Llevar persona", 31 | "No analysts yet" : "No hi ha cap persona encara", 32 | "New Request" : "Nova petició", 33 | "Select old analyst" : "Selecciona la persona original", 34 | "Select new analyst" : "Selecciona la nova persona", 35 | "Select shifts of old analyst" : "Selecciona els torns de la persona original", 36 | "Select shifts of new analyst" : "Selecciona els torns de la nova persona", 37 | "Description" : "Descripció", 38 | "Description or Purpose" : "Descripció o propòsit", 39 | "Select shifts of analyst" : "Selecciona els torns de la persona", 40 | "Cancel" : "Cancel·lar", 41 | "Save" : "Desar", 42 | "New Shiftstype" : "Nou tipus de torn", 43 | "Weekly" : "Setmanal", 44 | "weekly Shifts" : "Torns setmanals", 45 | "Rules" : "Regles", 46 | "Monday" : "Dilluns", 47 | "Tuesday" : "Dimarts", 48 | "Wednesday" : "Dimecres", 49 | "Thursday" : "Dijous", 50 | "Friday" : "Divendres", 51 | "Saturday" : "Dissabte", 52 | "Sunday" : "Diumenge", 53 | "Shift plan" : "Pla de Torns", 54 | "Shiftchange" : "Canvi de Torns", 55 | "Shifttypes" : "Tipus de Torns", 56 | "Shiftsdata" : "Arxiu de dades de Torns", 57 | "Timespan" : "Durada", 58 | "Select Date" : "Selecciona data", 59 | "In Progress" : "En Actiu", 60 | "Analyst Approval: " : "Aprovació de la persona afectada: ", 61 | " at " : " en ", 62 | "Decline" : "Rebutjar", 63 | "Approve" : "Aprovar", 64 | "Processed" : "Processada", 65 | "Admin Approval: " : "Aprovació de la persona responsable:", 66 | "Add Shift" : "Afegir un torn", 67 | "Synchronize Calendar" : "Sincronitzar calendari", 68 | "Add new Shiftstype" : "Afegir nou tipus de torn", 69 | "Shiftstype" : "Tipus de torn", 70 | "Delete" : "Esborrar", 71 | "Are you sure that you want to delete the Shiftstype and all its Shifts" : "De veritat vols esborrar aquest tipus de torn i tots els seus torns assignats?", 72 | "Edit" : "Editar", 73 | "App for organising Shifts" : "Aplicació per a organitzar Torns", 74 | "Name of the Shift-Type" : "Nom de la mena de torn", 75 | "Shifts" : "Torns", 76 | "Documentation" : "Documentació", 77 | "Name of the Shiftsorganizer" : "Nom de la persona responsable", 78 | "Email fo the Shiftsorganizer" : "Correu-e de la persona responsable", 79 | "Name of the Shiftscalendar" : "Nom del calendari dels torns", 80 | "Name of the Shiftsadmin Group" : "Nom del grup responsable dels torns", 81 | "Name of the Analyst Group" : "Nom del grup de les persones assignades a torns", 82 | "Name of the Analyst-Skill Group" : "Nom del grup segons responsabilitats de les persones assignades a torns", 83 | "Add" : "Afegir", 84 | "Successfully saved changes" : "Canvis guardats satisfactòriament", 85 | "Could not save changes" : "No vaig poder guardar els canvis", 86 | "Calendar Color" : "Color del calendari", 87 | "Start Time" : "Horari d'inici", 88 | "Stop Time" : "Horari de fi" 89 | }, 90 | "nplurals=2; plural=(n != 1);"); 91 | -------------------------------------------------------------------------------- /l10n/ca.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Month" : "Mes", 3 | "Week" : "Setmana", 4 | "Open Shifts" : "Crear Torns", 5 | "Search for Emails or Users" : "Cerca Usuàries o Correus", 6 | "No match found" : "No es van trobar coincidències", 7 | "Swap" : "Intercanviar", 8 | "Offer" : "Oferir", 9 | "Could not fetch shifts" : "No vaig poder obtenir torns", 10 | "Analyst" : "Persona", 11 | "Select Shiftstype" : "Selecciona el tipus de torn", 12 | "Could not save shifts Changes" : "No vaig poder guardar els canvis en els torns", 13 | "Could not fetch data" : "No vaig poder obtenir les dades", 14 | "Could not fetch shifts-types" : "No vaig poder obtenir els tipus de torns", 15 | "Could not fetch shifts-changes" : "No vaig poder obtenir els canvis en els torns", 16 | "Could not delete shift" : "No vaig poder esborrar el torn", 17 | "Could not delete shiftsType" : "No vaig poder esborrar el tipus de torn", 18 | "Could not update Calendar" : "No pude actualizar el calendario", 19 | "Could not create the shift" : "No vaig poder actualitzar el calendari", 20 | "No Analysts or Dates for Shift given" : "No hi ha persones ni dates en el Torn indicat", 21 | "Could not fetch Settings" : "No vaig poder obtenir la configuració", 22 | "Please set a Calendarname in the App-Settings" : "Si us plau escriu el nom del calendari en la configuració de l'aplicació", 23 | "Please set an Organizername in the App-Settings" : "Si us plau escriu el nom del qui organitza els torns en la configuració de l'aplicació", 24 | "Please set an Organizeremail in the App-Settings" : "Si us plau escriu el correu electrònic del qui organitza els torns en la configuració de l'aplicació", 25 | "Please set at least one Skillgroup in the App-Settings" : "Si us plau escriu almenys un nom d'una categoria de grup en la configuració de l'aplicació", 26 | "Could not save the shiftType" : "No vaig poder guardar el tipus de torn", 27 | "Today" : "Avui", 28 | "Remove analyst" : "Llevar persona", 29 | "No analysts yet" : "No hi ha cap persona encara", 30 | "New Request" : "Nova petició", 31 | "Select old analyst" : "Selecciona la persona original", 32 | "Select new analyst" : "Selecciona la nova persona", 33 | "Select shifts of old analyst" : "Selecciona els torns de la persona original", 34 | "Select shifts of new analyst" : "Selecciona els torns de la nova persona", 35 | "Description" : "Descripció", 36 | "Description or Purpose" : "Descripció o propòsit", 37 | "Select shifts of analyst" : "Selecciona els torns de la persona", 38 | "Cancel" : "Cancel·lar", 39 | "Save" : "Desar", 40 | "New Shiftstype" : "Nou tipus de torn", 41 | "Weekly" : "Setmanal", 42 | "weekly Shifts" : "Torns setmanals", 43 | "Rules" : "Regles", 44 | "Monday" : "Dilluns", 45 | "Tuesday" : "Dimarts", 46 | "Wednesday" : "Dimecres", 47 | "Thursday" : "Dijous", 48 | "Friday" : "Divendres", 49 | "Saturday" : "Dissabte", 50 | "Sunday" : "Diumenge", 51 | "Shift plan" : "Pla de Torns", 52 | "Shiftchange" : "Canvi de Torns", 53 | "Shifttypes" : "Tipus de Torns", 54 | "Shiftsdata" : "Arxiu de dades de Torns", 55 | "Timespan" : "Durada", 56 | "Select Date" : "Selecciona data", 57 | "In Progress" : "En Actiu", 58 | "Analyst Approval: " : "Aprovació de la persona afectada: ", 59 | " at " : " en ", 60 | "Decline" : "Rebutjar", 61 | "Approve" : "Aprovar", 62 | "Processed" : "Processada", 63 | "Admin Approval: " : "Aprovació de la persona responsable: ", 64 | "Add Shift" : "Afegir un torn", 65 | "Synchronize Calendar" : "Sincronitzar calendari", 66 | "Add new Shiftstype" : "Afegir nou tipus de torn", 67 | "Shiftstype" : "Tipus de torn", 68 | "Delete" : "Esborrar", 69 | "Are you sure that you want to delete the Shiftstype and all its Shifts" : "De veritat vols esborrar aquest tipus de torn i tots els seus torns assignats?", 70 | "Edit" : "Editar", 71 | "App for organising Shifts" : "Aplicació per a organitzar Torns", 72 | "Name of the Shift-Type" : "Nom de la mena de torn", 73 | "Shifts" : "Torns", 74 | "Documentation" : "Documentació", 75 | "Name of the Shiftsorganizer" : "Nom de la persona responsable", 76 | "Email fo the Shiftsorganizer" : "Correu-e de la persona responsable", 77 | "Name of the Shiftscalendar" : "Nom del calendari dels torns", 78 | "Name of the Shiftsadmin Group" : "Nom del grup responsable dels torns", 79 | "Name of the Analyst Group" : "Nom del grup de les persones assignades a torns", 80 | "Name of the Analyst-Skill Group" : "Nom del grup segons responsabilitats de les persones assignades a torns", 81 | "Add" : "Afegir", 82 | "Successfully saved changes" : "Canvis guardats satisfactòriament", 83 | "Could not save changes" : "No vaig poder guardar els canvis", 84 | "Calendar Color" : "Color del calendari", 85 | "Start Time" : "Horari d'inici", 86 | "Stop Time" : "Horari de fi" 87 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 88 | } 89 | -------------------------------------------------------------------------------- /l10n/cs.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "shifts", 3 | { 4 | "Month" : "měsíc", 5 | "Week" : "týden", 6 | "Open Shifts" : "neobsazené směny", 7 | "Search for Emails or Users" : "Hledej e-mail nebo uživatele", 8 | "No match found" : "Nic nenalezeno", 9 | "Swap" : "Prohodit", 10 | "Offer" : "Nabídka", 11 | "Could not fetch shifts" : "Směny nelze stáhnout", 12 | "Analyst" : "Pracovník", 13 | "Select Shiftstype" : "Vyber druh směny", 14 | "Could not save shifts Changes" : "Změny nelze uložit", 15 | "Could not fetch data" : "Data nelze stáhnout", 16 | "Could not fetch shifts-types" : "Druhy směn nelze stáhnout", 17 | "Could not fetch shifts-changes" : "Změny směn nelze stáhnout", 18 | "Could not delete shift" : "Směnu nelze smazat", 19 | "Could not delete shiftsType" : "Druh směny nelze smazat", 20 | " Could not update Calendar" : "Kalendář se nepodařilo aktualizovat", 21 | "Could not create the shift" : "Směnu se nepodařilo vytvořit", 22 | "No Analysts or Dates for Shift given" : "Chybí pracovník nebo datum směny", 23 | "Could not fetch Settings" : "Nastavení nelze stáhnout", 24 | "Please set a Calendarname in the App-Settings" : "Nastavte prosím jméno kalendáře v nastavení aplikací", 25 | "Please set an Organizername in the App-Settings" : "Nastavte prosím organizátora směn v nastavení aplikací", 26 | "Please set an Organizeremail in the App-Settings" : "Nastavte prosím email organizátora v nastavení aplikací", 27 | "Please set at least one Skillgroup in the App-Settings" : "Nastavte prosím uživatelskou skupinu v nastavení aplikací", 28 | "Could not save the shiftType" : "Druh směny nelze uložit", 29 | "Today" : "Dnes", 30 | "Remove analyst" : "Odeber pracovníka", 31 | "No analysts yet" : "Pracovník nenalezen", 32 | "New Request" : "Nový požadavek", 33 | "Select old analyst" : "Vyber původního pracovníka", 34 | "Select new analyst" : "Vyber nového pracovníka", 35 | "Select shifts of old analyst" : "Vyber směny původního pracovníka", 36 | "Select shifts of new analyst" : "Vyber směny nového pracovníka", 37 | "Description" : "Popis", 38 | "Description or Purpose" : "Popis nebo důvod", 39 | "Select shifts of analyst" : "Vyber směny pracovníka", 40 | "Cancel" : "Zrušit", 41 | "Save" : "Uložit", 42 | "New Shiftstype" : "Nový druh směny", 43 | "Weekly" : "Týdně", 44 | "weekly Shifts" : "týdenní směny", 45 | "Rules" : "Pravidla", 46 | "Monday" : "Pondělí", 47 | "Tuesday" : "Úterý", 48 | "Wednesday" : "Středa", 49 | "Thursday" : "Čtvrtek", 50 | "Friday" : "Pátek", 51 | "Saturday" : "Sobota", 52 | "Sunday" : "Neděle", 53 | "Shift plan" : "Plán směn", 54 | "Shiftchange" : "Výměny směn", 55 | "Shifttypes" : "Druhy směn", 56 | "Shiftsdata" : "Archiv směn", 57 | "Timespan" : "Časové rozpětí", 58 | "Select Date" : "Vyber datum", 59 | "In Progress" : "Probíhá", 60 | "Analyst Approval: " : "Pracovníkův souhlas: ", 61 | " at " : " v ", 62 | "Decline" : "Odmítnout", 63 | "Approve" : "Schválit", 64 | "Processed" : "Zpracováno", 65 | "Admin Approval: " : "Souhlas administrátora: ", 66 | "Add Shift" : "Nová směna", 67 | "Synchronize Calendar" : "Synchronizuj kalendář", 68 | "Add new Shiftstype" : "Nový druh směny", 69 | "Shiftstype" : "Druh směny", 70 | "Delete" : "Smazat", 71 | "Are you sure that you want to delete the Shiftstype and all its Shifts" : "Určitě chcete smazat druh směny a všechny související směny?", 72 | "Edit" : "Upravit", 73 | "App for organising Shifts" : "Aplikace pro organizaci směn", 74 | "Name of the Shift-Type" : "Druh směny", 75 | "Shifts" : "Směny", 76 | "Documentation" : "Dokumentace", 77 | "Name of the Shiftscalendar" : "Kalendář pro směny", 78 | "Name of the Shiftsorganizer" : "Organizátor směn", 79 | "Email fo the Shiftsorganizer" : "Email organizátora směn", 80 | "Name of the Shiftsadmin Group" : "Skupina administrátorů směn", 81 | "Name of the Analyst Group" : "Skupina pracovníků", 82 | "Name of the Analyst-Skill Group" : "Podskupina pracovníků", 83 | "Add" : "Přidat", 84 | "Successfully saved changes" : "Změny úspěšně uloženy", 85 | "Could not save changes" : "Změny nelze uložit", 86 | "Calendar Color" : "Barva", 87 | "Start Time" : "Začátek směny", 88 | "Stop Time" : "Konec směny" 89 | }, 90 | "nplurals=2; plural=(n != 1);"); 91 | -------------------------------------------------------------------------------- /l10n/cs.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Month" : "měsíc", 3 | "Week" : "týden", 4 | "Open Shifts" : "neobsazené směny", 5 | "Search for Emails or Users" : "Hledej e-mail nebo uživatele", 6 | "No match found" : "Nic nenalezeno", 7 | "Swap" : "Prohodit", 8 | "Offer" : "Nabídka", 9 | "Could not fetch shifts" : "Směny nelze stáhnout", 10 | "Analyst" : "Pracovník", 11 | "Select Shiftstype" : "Vyber druh směny", 12 | "Could not save shifts Changes" : "Změny nelze uložit", 13 | "Could not fetch data" : "Data nelze stáhnout", 14 | "Could not fetch shifts-types" : "Druhy směn nelze stáhnout", 15 | "Could not fetch shifts-changes" : "Změny směn nelze stáhnout", 16 | "Could not delete shift" : "Směnu nelze smazat", 17 | "Could not delete shiftsType" : "Druh směny nelze smazat", 18 | " Could not update Calendar" : "Kalendář se nepodařilo aktualizovat", 19 | "Could not create the shift" : "Směnu se nepodařilo vytvořit", 20 | "No Analysts or Dates for Shift given" : "Chybí pracovník nebo datum směny", 21 | "Could not fetch Settings" : "Nastavení nelze stáhnout", 22 | "Please set a Calendarname in the App-Settings" : "Nastavte prosím jméno kalendáře v nastavení aplikací", 23 | "Please set an Organizername in the App-Settings" : "Nastavte prosím organizátora směn v nastavení aplikací", 24 | "Please set an Organizeremail in the App-Settings" : "Nastavte prosím email organizátora v nastavení aplikací", 25 | "Please set at least one Skillgroup in the App-Settings" : "Nastavte prosím uživatelskou skupinu v nastavení aplikací", 26 | "Could not save the shiftType" : "Druh směny nelze uložit", 27 | "Today" : "Dnes", 28 | "Remove analyst" : "Odeber pracovníka", 29 | "No analysts yet" : "Pracovník nenalezen", 30 | "New Request" : "Nový požadavek", 31 | "Select old analyst" : "Vyber původního pracovníka", 32 | "Select new analyst" : "Vyber nového pracovníka", 33 | "Select shifts of old analyst" : "Vyber směny původního pracovníka", 34 | "Select shifts of new analyst" : "Vyber směny nového pracovníka", 35 | "Description" : "Popis", 36 | "Description or Purpose" : "Popis nebo důvod", 37 | "Select shifts of analyst" : "Vyber směny pracovníka", 38 | "Cancel" : "Zrušit", 39 | "Save" : "Uložit", 40 | "New Shiftstype" : "Nový druh směny", 41 | "Weekly" : "Týdně", 42 | "weekly Shifts" : "týdenní směny", 43 | "Rules" : "Pravidla", 44 | "Monday" : "Pondělí", 45 | "Tuesday" : "Úterý", 46 | "Wednesday" : "Středa", 47 | "Thursday" : "Čtvrtek", 48 | "Friday" : "Pátek", 49 | "Saturday" : "Sobota", 50 | "Sunday" : "Neděle", 51 | "Shift plan" : "Plán směn", 52 | "Shiftchange" : "Výměny směn", 53 | "Shifttypes" : "Druhy směn", 54 | "Shiftsdata" : "Archiv směn", 55 | "Timespan" : "Časové rozpětí", 56 | "Select Date" : "Vyber datum", 57 | "In Progress" : "Probíhá", 58 | "Analyst Approval: " : "Pracovníkův souhlas: ", 59 | " at " : " v ", 60 | "Decline" : "Odmítnout", 61 | "Approve" : "Schválit", 62 | "Processed" : "Zpracováno", 63 | "Admin Approval: " : "Souhlas administrátora: ", 64 | "Add Shift" : "Nová směna", 65 | "Synchronize Calendar" : "Synchronizuj kalendář", 66 | "Add new Shiftstype" : "Nový druh směny", 67 | "Shiftstype" : "Druh směny", 68 | "Delete" : "Smazat", 69 | "Are you sure that you want to delete the Shiftstype and all its Shifts" : "Určitě chcete smazat druh směny a všechny související směny?", 70 | "Edit" : "Upravit", 71 | "App for organising Shifts" : "Aplikace pro organizaci směn", 72 | "Name of the Shift-Type" : "Druh směny", 73 | "Shifts" : "Směny", 74 | "Documentation" : "Dokumentace", 75 | "Name of the Shiftscalendar" : "Kalendář pro směny", 76 | "Name of the Shiftsorganizer" : "Organizátor směn", 77 | "Email fo the Shiftsorganizer" : "Email organizátora směn", 78 | "Name of the Shiftsadmin Group" : "Skupina administrátorů směn", 79 | "Name of the Analyst Group" : "Skupina pracovníků", 80 | "Name of the Analyst-Skill Group" : "Podskupina pracovníků", 81 | "Add" : "Přidat", 82 | "Successfully saved changes" : "Změny úspěšně uloženy", 83 | "Could not save changes" : "Změny nelze uložit", 84 | "Calendar Color" : "Barva", 85 | "Start Time" : "Začátek směny", 86 | "Stop Time" : "Konec směny" 87 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 88 | } 89 | -------------------------------------------------------------------------------- /l10n/es.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "shifts", 3 | { 4 | "Month" : "Mes", 5 | "Week" : "Semana", 6 | "Open Shifts" : "Crear Turnos", 7 | "Search for Emails or Users" : "Busca Usuarias o Correos", 8 | "No match found" : "No se encontraron coincidencias", 9 | "Swap" : "Intercambiar", 10 | "Offer" : "Ofrecer", 11 | "Could not fetch shifts" : "No pude obtener turnos", 12 | "Analyst" : "Persona", 13 | "Select Shiftstype" : "Selecciona el tipo de turno", 14 | "Could not save shifts Changes" : "No pude guardar los cambios en los turnos", 15 | "Could not fetch data" : "No pude obtener los datos", 16 | "Could not fetch shifts-types" : "No pude obtener los tipos de turnos", 17 | "Could not fetch shifts-changes" : "No pude obtener los cambios en los turnos", 18 | "Could not delete shift" : "No pude borrar el turno", 19 | "Could not delete shiftsType" : "No pude borrar el tipo de turno", 20 | "Could not update Calendar" : "No pude actualizar el calendario", 21 | "Could not create the shift" : "No pude crear el turno", 22 | "No Analysts or Dates for Shift given" : "No hay personas ni fechas en el Turno indicado", 23 | "Could not fetch Settings" : "No pude obtener la configuración", 24 | "Please set a Calendarname in the App-Settings" : "Por favor escribe el nombre del calendario en la configuración de la aplicación", 25 | "Please set an Organizername in the App-Settings" : "Por favor escribe el nombre del quien organiza los turnos en la configuración de la aplicación", 26 | "Please set an Organizeremail in the App-Settings" : "Por favor escribe el correo electrónico del quien organiza los turnos en la configuración de la aplicación", 27 | "Please set at least one Skillgroup in the App-Settings" : "Por favor escribe almenos un nombre de una categoría de grupo en la configuración de la aplicación", 28 | "Could not save the shiftType" : "No pude guardar el tipo de turno", 29 | "Today" : "Hoy", 30 | "Remove analyst" : "Quitar persona", 31 | "No analysts yet" : "No hay ninguna persona todavia", 32 | "New Request" : "Nueva petición", 33 | "Select old analyst" : "Selecciona la persona original", 34 | "Select new analyst" : "Selecciona la nueva persona", 35 | "Select shifts of old analyst" : "Selecciona los turnos de la persona original", 36 | "Select shifts of new analyst" : "Selecciona los turnos de la nueva persona", 37 | "Description" : "Descripción", 38 | "Description or Purpose" : "Descripción o propósito", 39 | "Select shifts of analyst" : "Selecciona los turnos de la persona", 40 | "Cancel" : "Cancelar", 41 | "Save" : "Guardar", 42 | "New Shiftstype" : "Nuevo tipo de turno", 43 | "Weekly" : "Semanal", 44 | "weekly Shifts" : "Turnos semanales", 45 | "Rules" : "Reglas", 46 | "Monday" : "Lunes", 47 | "Tuesday" : "Martes", 48 | "Wednesday" : "Miércoles", 49 | "Thursday" : "Jueves", 50 | "Friday" : "Viernes", 51 | "Saturday" : "Sábado", 52 | "Sunday" : "Domingo", 53 | "Shift plan" : "Plan de Turnos", 54 | "Shiftchange" : "Cambio de Turnos", 55 | "Shifttypes" : "Tipos de Turnos", 56 | "Shiftsdata" : "Archivo de datos de Turnos", 57 | "Timespan" : "Duración", 58 | "Select Date" : "Selecciona fecha", 59 | "In Progress" : "En Activo", 60 | "Analyst Approval: " : "Aprobación de la persona afectada: ", 61 | " at " : " en ", 62 | "Decline" : "Rechazar", 63 | "Approve" : "Aprobar", 64 | "Processed" : "Procesada", 65 | "Admin Approval: " : "Aprobación de la persona responsable: ", 66 | "Add Shift" : "Añadir un turno", 67 | "Synchronize Calendar" : "Sincronizar calendario", 68 | "Add new Shiftstype" : "Añadir nuevo tipo de turno", 69 | "Shiftstype" : "Tipo de turno", 70 | "Delete" : "Borrar", 71 | "Are you sure that you want to delete the Shiftstype and all its Shifts" : "¿De verdad quieres borrar este tipo de turno y todos sus turnos asignados?", 72 | "Edit" : "Editar", 73 | "App for organising Shifts" : "Aplicación para organizar Turnos", 74 | "Name of the Shift-Type" : "Nombre del tipo de turno", 75 | "Shifts" : "Turnos", 76 | "Documentation" : "Documentación", 77 | "Name of the Shiftscalendar" : "Nombre del calendario de turnos", 78 | "Name of the Shiftsorganizer" : "Nombre de la persona responsable", 79 | "Email fo the Shiftsorganizer" : "Correo-e de la persona responsable", 80 | "Name of the Shiftsadmin Group" : "Nombre del grupo responsable de los turnos", 81 | "Name of the Analyst Group" : "Nombre del grupo de las personas asiganas a turnos", 82 | "Name of the Analyst-Skill Group" : "Nombre del grupo según responsabilidades de las personas asignadas a turnos", 83 | "Add" : "Añadir", 84 | "Successfully saved changes" : "Cambios guardados satisfactoriamente", 85 | "Could not save changes" : "No pude guardar los cambios", 86 | "Calendar Color" : "Color del calendario", 87 | "Start Time" : "Horario de inicio", 88 | "Stop Time" : "Horario de fin" 89 | }, 90 | "nplurals=2; plural=(n != 1);"); 91 | -------------------------------------------------------------------------------- /l10n/es.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Month" : "Mes", 3 | "Week" : "Semana", 4 | "Open Shifts" : "Crear Turnos", 5 | "Search for Emails or Users" : "Busca Usuarias o Correos", 6 | "No match found" : "No se encontraron coincidencias", 7 | "Swap" : "Intercambiar", 8 | "Offer" : "Ofrecer", 9 | "Could not fetch shifts" : "No pude obtener turnos", 10 | "Analyst" : "Persona", 11 | "Select Shiftstype" : "Selecciona el tipo de turno", 12 | "Could not save shifts Changes" : "No pude guardar los cambios en los turnos", 13 | "Could not fetch data" : "No pude obtener los datos", 14 | "Could not fetch shifts-types" : "No pude obtener los tipos de turnos", 15 | "Could not fetch shifts-changes" : "No pude obtener los cambios en los turnos", 16 | "Could not delete shift" : "No pude borrar el turno", 17 | "Could not delete shiftsType" : "No pude borrar el tipo de turno", 18 | "Could not update Calendar" : "No pude actualizar el calendario", 19 | "Could not create the shift" : "No pude crear el turno", 20 | "No Analysts or Dates for Shift given" : "No hay personas ni fechas en el Turno indicado", 21 | "Could not fetch Settings" : "No pude obtener la configuración", 22 | "Please set a Calendarname in the App-Settings" : "Por favor escribe el nombre del calendario en la configuración de la aplicación", 23 | "Please set an Organizername in the App-Settings" : "Por favor escribe el nombre del quien organiza los turnos en la configuración de la aplicación", 24 | "Please set an Organizeremail in the App-Settings" : "Por favor escribe el correo electrónico del quien organiza los turnos en la configuración de la aplicación", 25 | "Please set at least one Skillgroup in the App-Settings" : "Por favor escribe almenos un nombre de una categoría de grupo en la configuración de la aplicación", 26 | "Could not save the shiftType" : "No pude guardar el tipo de turno", 27 | "Today" : "Hoy", 28 | "Remove analyst" : "Quitar persona", 29 | "No analysts yet" : "No hay ninguna persona todavia", 30 | "New Request" : "Nueva petición", 31 | "Select old analyst" : "Selecciona la persona original", 32 | "Select new analyst" : "Selecciona la nueva persona", 33 | "Select shifts of old analyst" : "Selecciona los turnos de la persona original", 34 | "Select shifts of new analyst" : "Selecciona los turnos de la nueva persona", 35 | "Description" : "Descripción", 36 | "Description or Purpose" : "Descripción o propósito", 37 | "Select shifts of analyst" : "Selecciona los turnos de la persona", 38 | "Cancel" : "Cancelar", 39 | "Save" : "Guardar", 40 | "New Shiftstype" : "Nuevo tipo de turno", 41 | "Weekly" : "Semanal", 42 | "weekly Shifts" : "Turnos semanales", 43 | "Rules" : "Reglas", 44 | "Monday" : "Lunes", 45 | "Tuesday" : "Martes", 46 | "Wednesday" : "Miércoles", 47 | "Thursday" : "Jueves", 48 | "Friday" : "Viernes", 49 | "Saturday" : "Sábado", 50 | "Sunday" : "Domingo", 51 | "Shift plan" : "Plan de Turnos", 52 | "Shiftchange" : "Cambio de Turnos", 53 | "Shifttypes" : "Tipos de Turnos", 54 | "Shiftsdata" : "Archivo de datos de Turnos", 55 | "Timespan" : "Duración", 56 | "Select Date" : "Selecciona fecha", 57 | "In Progress" : "En Activo", 58 | "Analyst Approval: " : "Aprobación de la persona afectada: ", 59 | " at " : " en ", 60 | "Decline" : "Rechazar", 61 | "Approve" : "Aprobar", 62 | "Processed" : "Procesada", 63 | "Admin Approval: " : "Aprobación de la persona responsable: ", 64 | "Add Shift" : "Añadir un turno", 65 | "Synchronize Calendar" : "Sincronizar calendario", 66 | "Add new Shiftstype" : "Añadir nuevo tipo de turno", 67 | "Shiftstype" : "Tipo de turno", 68 | "Delete" : "Borrar", 69 | "Are you sure that you want to delete the Shiftstype and all its Shifts" : "¿De verdad quieres borrar este tipo de turno y todos sus turnos asignados?", 70 | "Edit" : "Editar", 71 | "App for organising Shifts" : "Aplicación para organizar Turnos", 72 | "Name of the Shift-Type" : "Nombre del tipo de turno", 73 | "Shifts" : "Turnos", 74 | "Documentation" : "Documentación", 75 | "Name of the Shiftsorganizer" : "Nombre de la persona responsable", 76 | "Email fo the Shiftsorganizer" : "Correo-e de la persona responsable", 77 | "Name of the Shiftscalendar" : "Nombre del calendario de turnos", 78 | "Name of the Shiftsadmin Group" : "Nombre del grupo responsable de los turnos", 79 | "Name of the Analyst Group" : "Nombre del grupo de las personas asiganas a turnos", 80 | "Name of the Analyst-Skill Group" : "Nombre del grupo según responsabilidades de las personas asignadas a turnos", 81 | "Add" : "Añadir", 82 | "Successfully saved changes" : "Cambios guardados satisfactoriamente", 83 | "Could not save changes" : "No pude guardar los cambios", 84 | "Calendar Color" : "Color del calendario", 85 | "Start Time" : "Horario de inicio", 86 | "Stop Time" : "Horario de fin" 87 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 88 | } 89 | -------------------------------------------------------------------------------- /lib/AppInfo/Application.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * @author Fabian Kirchesch 6 | */ 7 | 8 | namespace OCA\Shifts\AppInfo; 9 | 10 | use OCP\AppFramework\App; 11 | use OCP\EventDispatcher\IEventDispatcher; 12 | use OCP\IConfig; 13 | use OCP\Security\CSP\AddContentSecurityPolicyEvent; 14 | use OCP\AppFramework\Http\ContentSecurityPolicy; 15 | 16 | class Application extends App{ 17 | public const APP_ID = 'shifts'; 18 | 19 | public function __construct() { 20 | parent::__construct(self::APP_ID); 21 | 22 | $container = $this->getContainer(); 23 | 24 | $container->registerService('URLGenerator', function($c) { 25 | return $c->query('ServerContainer')->getURLGenerator(); 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/Controller/Errors.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * @author Fabian Kirchesch 6 | */ 7 | 8 | namespace OCA\Shifts\Controller; 9 | 10 | use Closure; 11 | 12 | use OCP\AppFramework\Http; 13 | use OCP\AppFramework\Http\DataResponse; 14 | 15 | use OCA\Shifts\Service\NotFoundException; 16 | 17 | // Allows for handling of NotFoundExceptions 18 | trait Errors { 19 | protected function handleNotFound(Closure $callback): DataResponse { 20 | try { 21 | return new DataResponse($callback()); 22 | } catch (NotFoundException $e) { 23 | $message = ['message' => $e->getMessage()]; 24 | return new DataResponse($message, Http::STATUS_NOT_FOUND); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/Controller/PageController.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * @author Fabian Kirchesch 6 | */ 7 | 8 | namespace OCA\Shifts\Controller; 9 | 10 | use OCA\Shifts\AppInfo\Application; 11 | use OCP\AppFramework\Controller; 12 | use OCP\AppFramework\Http\TemplateResponse; 13 | use OCP\IRequest; 14 | use OCP\Util; 15 | 16 | class PageController extends Controller { 17 | public function __construct(IRequest $request) { 18 | parent::__construct(Application::APP_ID, $request); 19 | } 20 | 21 | /** 22 | * @NoAdminRequired 23 | * @NoCSRFRequired 24 | * 25 | * Render default template 26 | */ 27 | public function index() { 28 | Util::addScript(Application::APP_ID, 'shifts-main'); 29 | 30 | return new TemplateResponse(Application::APP_ID, 'main'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/Controller/SettingsController.php: -------------------------------------------------------------------------------- 1 | 4 | * @copyright Copyright (c) 2023. Kevin Küchler 5 | * 6 | * @author Fabian Kirchesch 7 | * @author Kevin Küchler 8 | */ 9 | 10 | namespace OCA\Shifts\Controller; 11 | 12 | use \OCP\ILogger; 13 | use OCA\Shifts\AppInfo\Application; 14 | use OCA\Shifts\Db\ShiftsTypeMapper; 15 | use OCA\Shifts\Settings\Settings; 16 | use OCP\AppFramework\Controller; 17 | use OCP\AppFramework\Http\TemplateResponse; 18 | use OCP\AppFramework\Http\DataResponse; 19 | use OCP\IRequest; 20 | use OCP\IURLGenerator; 21 | use OCP\AppFramework\Http; 22 | use Psr\Log\LoggerInterface; 23 | 24 | class SettingsController extends Controller { 25 | /** @var LoggerInterface */ 26 | private LoggerInterface $logger; 27 | 28 | /** 29 | * Settings 30 | * 31 | * @var Settings 32 | */ 33 | private $settings; 34 | 35 | /** @var ShiftsTypeMapper */ 36 | private $mapper; 37 | 38 | /** 39 | * Url generator service 40 | * 41 | * @var IURLGenerator 42 | */ 43 | private $urlGenerator; 44 | 45 | public function __construct(LoggerInterface $logger, IRequest $request, IURLGenerator $urlGenerator, ShiftsTypeMapper $mapper, Settings $settings) { 46 | parent::__construct(Application::APP_ID, $request); 47 | $this->logger = $logger; 48 | 49 | $this->urlGenerator = $urlGenerator; 50 | $this->settings = $settings; 51 | $this->mapper = $mapper; 52 | } 53 | /** 54 | * Print Settings section 55 | * 56 | * @return TemplateResponse 57 | */ 58 | public function index(): TemplateResponse { 59 | $data = $this->getSettings(); 60 | return new TemplateResponse(Application::APP_ID, 'settings', $data, 'blank'); 61 | } 62 | 63 | /** 64 | * Saves Settings 65 | * 66 | * @AdminRequired 67 | * 68 | * @param string $calendarName 69 | * @param boolean $addUserCalendarEvent 70 | * @param string $organizerName 71 | * @param string $organizerEmail 72 | * @param string $adminGroup 73 | * @param string $shiftWorkerGroup 74 | * @param string $shiftChangeSameShiftType 75 | * @param array $skillGroups 76 | * @return DataResponse 77 | */ 78 | public function saveSettings(string $calendarName, bool $addUserCalendarEvent, string $organizerName, string $organizerEmail, string $timezone, string $adminGroup, string $shiftWorkerGroup, string $shiftChangeSameShiftType, array $skillGroups): DataResponse { 79 | $skillGroupIds = $this->mapper->findAllSkillGroupIds(); 80 | for($i = 0; $i < count($skillGroupIds); $i++) { 81 | $skillGroupPresent = false; 82 | for($j = 0; $j < count($skillGroups); $j++) { 83 | if($skillGroupIds[$i]->getSkillGroupId() == $skillGroups[$j]['id']) { 84 | $skillGroupPresent = true; 85 | } 86 | } 87 | 88 | if(!$skillGroupPresent) { 89 | return new DataResponse("Skill group is still in use and cannot be deleted!", Http::STATUS_BAD_REQUEST); 90 | } 91 | } 92 | 93 | $this->settings->setCalendarName($calendarName); 94 | $this->settings->setAddUserCalendarEvent($addUserCalendarEvent); 95 | $this->settings->setOrganizerName($organizerName); 96 | $this->settings->setOrganizerEmail($organizerEmail); 97 | $this->settings->setAdminGroup($adminGroup); 98 | $this->settings->setShiftsTimezone($timezone); 99 | $this->settings->setShiftWorkerGroup($shiftWorkerGroup); 100 | $this->settings->setSkillGroups($skillGroups); 101 | $this->settings->setShiftChangeSameShiftType($shiftChangeSameShiftType); 102 | return new DataResponse($this->getSettings()); 103 | } 104 | 105 | /** 106 | * Get app settings 107 | * @NoAdminRequired 108 | * 109 | * @return array 110 | */ 111 | public function getSettings(): array { 112 | return [ 113 | 'calendarName' => $this->settings->getCalendarName(), 114 | 'addUserCalendarEvent' => $this->settings->getAddUserCalendarEvent(), 115 | 'organizerName' => $this->settings->getOrganizerName(), 116 | 'organizerEmail' => $this->settings->getOrganizerEmail(), 117 | 'timezone' => $this->settings->getShiftsTimezone(), 118 | 'adminGroup' => $this->settings->getAdminGroup(), 119 | 'shiftWorkerGroup' => $this->settings->getShiftWorkerGroup(), 120 | 'skillGroups' => $this->settings->getSkillGroups(), 121 | 'shiftChangeSameShiftType' => $this->settings->getShiftChangeSameShiftType(), 122 | ]; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /lib/Controller/ShiftsCalendarChangeController.php: -------------------------------------------------------------------------------- 1 | 4 | * @copyright Copyright (c) 2023. Kevin Küchler 5 | * 6 | * @author Fabian Kirchesch 7 | * @author Kevin Küchler 8 | */ 9 | 10 | 11 | namespace OCA\Shifts\Controller; 12 | 13 | use OCP\AppFramework\Http; 14 | use OCA\Shifts\AppInfo\Application; 15 | use OCA\Shifts\Settings\Settings; 16 | use OCA\Shifts\Service\PermissionService; 17 | use OCA\Shifts\Service\ShiftsCalendarChangeService; 18 | use OCP\AppFramework\Controller; 19 | use OCP\AppFramework\Http\DataResponse; 20 | use OCP\IRequest; 21 | 22 | class ShiftsCalendarChangeController extends Controller 23 | { 24 | /** @var ShiftsCalendarChangeService */ 25 | private $service; 26 | 27 | /** @var Settings */ 28 | private $settings; 29 | 30 | /** @var PermissionService */ 31 | private $permService; 32 | 33 | use Errors; 34 | 35 | 36 | public function __construct(IRequest $request, ShiftsCalendarChangeService $service, Settings $settings, PermissionService $permService){ 37 | parent::__construct(Application::APP_ID, $request); 38 | $this->service = $service; 39 | $this->settings = $settings; 40 | $this->permService = $permService; 41 | } 42 | 43 | /** 44 | * @NoAdminRequired 45 | * @NoCSRFRequired 46 | */ 47 | public function index(): DataResponse { 48 | if(!$this->permService->isRequestingUserAdmin()) { 49 | return new DataResponse(NULL, Http::STATUS_UNAUTHORIZED); 50 | } 51 | 52 | return new DataResponse($this->service->findAll()); 53 | } 54 | 55 | /** 56 | * @NoAdminRequired 57 | * 58 | * @param int $id 59 | * @return DataResponse 60 | */ 61 | public function show(int $id): DataResponse { 62 | if(!$this->permService->isRequestingUserAdmin()) { 63 | return new DataResponse(NULL, Http::STATUS_UNAUTHORIZED); 64 | } 65 | 66 | return $this->handleNotFound(function () use($id){ 67 | return $this->service->find($id); 68 | }); 69 | } 70 | 71 | /** 72 | * @NoAdminRequired 73 | * 74 | * @param int $shiftId 75 | * @param int $shiftTypeId 76 | * @param string $shiftDate 77 | * @param string $oldUserId 78 | * @param string $newUserId 79 | * @param string $action 80 | * @param string $dateChanged 81 | * @param string $adminId 82 | * @param bool $isDone 83 | * @return DataResponse 84 | */ 85 | public function create(int $shiftId, int $shiftTypeId, string $shiftDate, string $oldUserId, string $newUserId, string $action, string $dateChanged, string $adminId, bool $isDone): DataResponse { 86 | if(!$this->permService->isRequestingUserAdmin()) { 87 | return new DataResponse(NULL, Http::STATUS_UNAUTHORIZED); 88 | } 89 | 90 | return new DataResponse($this->service->create($shiftId, $shiftTypeId, $shiftDate, $oldUserId, $newUserId, $action, $dateChanged, $adminId, $isDone)); 91 | } 92 | 93 | /** 94 | * @NoAdminRequired 95 | * 96 | * @param int $id 97 | * @param int $shiftId 98 | * @param int $shiftTypeId 99 | * @param string $shiftDate 100 | * @param string $oldUserId 101 | * @param string $newUserId 102 | * @param string $action 103 | * @param string $dateChanged 104 | * @param string $adminId 105 | * @param bool $isDone 106 | * @return DataResponse 107 | */ 108 | public function update(int $id,int $shiftId, int $shiftTypeId, string $shiftDate, string $oldUserId, string $newUserId, string $action, string $dateChanged, string $adminId, bool $isDone):DataResponse 109 | { 110 | if(!$this->permService->isRequestingUserAdmin()) { 111 | return new DataResponse(NULL, Http::STATUS_UNAUTHORIZED); 112 | } 113 | 114 | return $this->handleNotFound(function() use ($id, $shiftId, $shiftTypeId, $shiftDate, $oldUserId, $newUserId, $action, $dateChanged, $adminId, $isDone){ 115 | return $this->service->update($id, $shiftId, $shiftTypeId, $shiftDate, $oldUserId, $newUserId, $action, $dateChanged, $adminId, $isDone); 116 | }); 117 | } 118 | 119 | /** 120 | * @NoAdminRequired 121 | * 122 | * @param int $id 123 | * @return DataResponse 124 | */ 125 | public function destroy(int $id): DataResponse 126 | { 127 | if(!$this->permService->isRequestingUserAdmin()) { 128 | return new DataResponse(NULL, Http::STATUS_UNAUTHORIZED); 129 | } 130 | 131 | return $this->handleNotFound(function() use($id) { 132 | return $this->service->delete($id); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /lib/Controller/ShiftsCalendarController.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * @author Kevin Küchler 6 | */ 7 | 8 | 9 | namespace OCA\Shifts\Controller; 10 | 11 | use Exception; 12 | use OC\Security\CSP\ContentSecurityPolicy; 13 | use OCA\Shifts\Service\NotFoundException; 14 | use OCA\Shifts\Service\PermissionException; 15 | use OCA\Shifts\Service\ShiftsCalendarChangeService; 16 | use OCP\AppFramework\Http\DataResponse; 17 | use OCP\AppFramework\Http\TemplateResponse; 18 | use OCP\IRequest; 19 | use OCP\AppFramework\Http; 20 | use OCP\AppFramework\Controller; 21 | use OCP\AppFramework\Http\Response; 22 | use OCP\AppFramework\Http\NotFoundResponse; 23 | 24 | use OCA\Shifts\AppInfo\Application; 25 | use OCA\Shifts\Service\PermissionService; 26 | use OCA\Shifts\Service\ShiftsCalendarService; 27 | use Psr\Log\LoggerInterface; 28 | 29 | 30 | class ShiftsCalendarController extends Controller { 31 | /** @var LoggerInterface */ 32 | private LoggerInterface $logger; 33 | 34 | /** @var ShiftsCalendarService */ 35 | private ShiftsCalendarService $service; 36 | 37 | /** @var ShiftsCalendarChangeService */ 38 | private ShiftsCalendarChangeService $changeService; 39 | 40 | /** @var PermissionService */ 41 | private PermissionService $permService; 42 | 43 | use Errors; 44 | 45 | 46 | public function __construct(LoggerInterface $logger, IRequest $request, ShiftsCalendarService $service, ShiftsCalendarChangeService $changeService, PermissionService $permService) { 47 | parent::__construct(Application::APP_ID, $request); 48 | $this->logger = $logger; 49 | 50 | $this->service = $service; 51 | $this->permService = $permService; 52 | $this->changeService = $changeService; 53 | } 54 | 55 | private function handleException(Exception $e): Response { 56 | if($e instanceof NotFoundException) { 57 | return new NotFoundResponse(); 58 | } else if($e instanceof PermissionException) { 59 | $resp = new TemplateResponse('shifts', '401', [], 'guest'); 60 | $resp->setContentSecurityPolicy(new ContentSecurityPolicy()); 61 | $resp->setStatus(Http::STATUS_UNAUTHORIZED); 62 | return $resp; 63 | } else { 64 | throw $e; 65 | } 66 | } 67 | 68 | /** 69 | * @NoAdminRequired 70 | * @NoCSRFRequired 71 | * 72 | * @throws Exception 73 | */ 74 | public function index(): Response { 75 | if(!$this->permService->isRequestingUserAdmin()) { 76 | $this->handleException(new PermissionException()); 77 | } 78 | 79 | return new Response(); 80 | } 81 | /** 82 | * @NoAdminRequired 83 | * 84 | * @param int $shiftId 85 | * @return Response 86 | * @throws Exception 87 | */ 88 | public function create(int $shiftId): Response { 89 | if(!$this->permService->isRequestingUserAdmin()) { 90 | $this->handleException(new PermissionException()); 91 | } 92 | 93 | try { 94 | $this->service->create($shiftId); 95 | } catch (Exception $e) { 96 | return $this->handleException($e); 97 | } 98 | 99 | return new Response(); 100 | } 101 | 102 | /** 103 | * @NoAdminRequired 104 | * 105 | * @param int $shiftId 106 | * @return Response 107 | * @throws Exception 108 | */ 109 | public function update(int $shiftId): Response { 110 | if(!$this->permService->isRequestingUserAdmin()) { 111 | $this->handleException(new PermissionException()); 112 | } 113 | 114 | try { 115 | $this->service->update($shiftId); 116 | } catch (Exception $e) { 117 | return $this->handleException($e); 118 | } 119 | 120 | return new Response(); 121 | } 122 | 123 | /** 124 | * @NoAdminRequired 125 | * 126 | * @param int $shiftId 127 | * @return Response 128 | * @throws Exception 129 | */ 130 | public function destroy(int $shiftId): Response { 131 | if(!$this->permService->isRequestingUserAdmin()) { 132 | $this->handleException(new PermissionException()); 133 | } 134 | 135 | try { 136 | $this->service->delete($shiftId); 137 | } catch (Exception $e) { 138 | return $this->handleException($e); 139 | } 140 | 141 | return new Response(); 142 | } 143 | 144 | /** 145 | * @NoAdminRequired 146 | * 147 | * @return Response 148 | * @throws Exception 149 | */ 150 | public function synchronize(): Response { 151 | if(!$this->permService->isRequestingUserAdmin()) { 152 | $this->handleException(new PermissionException()); 153 | } 154 | 155 | $errors = array(); 156 | $openChanges = $this->changeService->findAllOpen(); 157 | foreach($openChanges as $change) { 158 | $action = $change->getAction(); 159 | $this->logger->debug("ShiftsCalendarController::synchronize()", ['openChange' => $change]); 160 | 161 | try { 162 | if($action == "assign") { 163 | $this->logger->debug("ShiftsCalendarController::synchronize()", ['action' => 'assign']); 164 | $this->service->create($change->getShiftId()); 165 | } else if($action == "update") { 166 | $this->logger->debug("ShiftsCalendarController::synchronize()", ['action' => 'update']); 167 | $this->service->updateByShiftChange($change); 168 | } else if($action == "unassign") { 169 | $this->logger->debug("ShiftsCalendarController::synchronize()", ['action' => 'unassign']); 170 | $this->service->delete($change->getShiftId()); 171 | } else { 172 | $this->logger->warning("Unknown shift change action type '" . $action . "'"); 173 | continue; 174 | } 175 | 176 | $this->changeService->updateDone($change->getId(), true); 177 | } catch(NotFoundException $e) { 178 | $this->logger->debug("ShiftsCalendarController::synchronize()", ['exception' => $e]); 179 | array_push($errors, $e->getMessage()); 180 | } catch (Exception $e) { 181 | $this->logger->error("ShiftsCalendarController::synchronize()", ['exception' => $e]); 182 | return $this->handleException($e); 183 | } 184 | } 185 | 186 | if(sizeof($errors) != 0) { 187 | $resp = new DataResponse(); 188 | $resp->setContentSecurityPolicy(new ContentSecurityPolicy()); 189 | $resp->setStatus(Http::STATUS_INTERNAL_SERVER_ERROR); 190 | $resp->setData($errors); 191 | return $resp; 192 | } else { 193 | return new Response(); 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /lib/Controller/ShiftsChangeController.php: -------------------------------------------------------------------------------- 1 | 4 | * @copyright Copyright (c) 2023. Kevin Küchler 5 | * 6 | * @author Fabian Kirchesch 7 | * @author Kevin Küchler 8 | */ 9 | 10 | namespace OCA\Shifts\Controller; 11 | 12 | use Exception; 13 | use OCA\Shifts\Service\PermissionException; 14 | use OCA\Shifts\Service\InvalidArgumentException; 15 | 16 | use OCP\AppFramework\Http; 17 | use OCA\Shifts\AppInfo\Application; 18 | use OCA\Shifts\Service\ShiftsChangeService; 19 | use OCA\Shifts\Service\PermissionService; 20 | use OCP\AppFramework\Controller; 21 | use OCP\AppFramework\Http\DataResponse; 22 | use OCP\IRequest; 23 | 24 | class ShiftsChangeController extends Controller{ 25 | /** @var ShiftsChangeService */ 26 | private $service; 27 | 28 | private $permService; 29 | 30 | use Errors; 31 | 32 | 33 | public function __construct(IRequest $request, ShiftsChangeService $service, PermissionService $permService){ 34 | parent::__construct(Application::APP_ID, $request); 35 | $this->service = $service; 36 | $this->permService = $permService; 37 | } 38 | 39 | private function handleException(Exception $e): DataResponse { 40 | if($e instanceof PermissionException) { 41 | return new DataResponse(NULL, Http::STATUS_UNAUTHORIZED); 42 | } elseif($e instanceof InvalidArgumentException) { 43 | return new DataResponse(NULL, Http::STATUS_BAD_REQUEST); 44 | } elseif($e instanceof NotFoundException) { 45 | return new DataResponse(NULL, Http::STATUS_NOT_FOUND); 46 | } else { 47 | return new DataResponse(NULL, Http::STATUS_INTERNAL_SERVER_ERROR); 48 | } 49 | } 50 | 51 | /** 52 | * @NoAdminRequired 53 | * @NoCSRFRequired 54 | */ 55 | public function index(): DataResponse { 56 | return new DataResponse($this->service->findAll()); 57 | } 58 | 59 | /** 60 | * @NoAdminRequired 61 | * 62 | * @param int $id 63 | * @return DataResponse 64 | */ 65 | public function show(int $id): DataResponse { 66 | try { 67 | return $this->service->find($id); 68 | } catch(Exception $e) { 69 | return $this->handleException($e); 70 | } 71 | } 72 | 73 | /** 74 | * @NoAdminRequired 75 | * 76 | * @param string oldAnalystId 77 | * @param string newAnalystId 78 | * @param string $adminApproval 79 | * @param string $adminApprovalDate 80 | * @param string $analystApproval 81 | * @param string $analystApprovalDate 82 | * @param int $oldShiftsId 83 | * @param int $newShiftsId 84 | * @param string desc 85 | * @param int type 86 | * @return DataResponse 87 | */ 88 | public function create(string $oldAnalystId, string $newAnalystId, string $adminApproval, string $adminApprovalDate, string $analystApproval, string $analystApprovalDate, int $oldShiftsId, int $newShiftsId, string $desc, int $type): DataResponse { 89 | if($this->permService->isRequestingUser($oldAnalystId) || $this->permService->isRequestingUserAdmin()) { 90 | try { 91 | return new DataResponse($this->service->create($oldAnalystId, $newAnalystId, $adminApproval, $adminApprovalDate, $analystApproval, $analystApprovalDate, $oldShiftsId, $newShiftsId, $desc, $type)); 92 | } catch (Exception $e) { 93 | return $this->handleException($e); 94 | } 95 | } else { 96 | return new DataResponse(NULL, Http::STATUS_UNAUTHORIZED); 97 | } 98 | } 99 | 100 | /** 101 | * @NoAdminRequired 102 | * 103 | * @param int $id 104 | * @param string oldAnalystId 105 | * @param string newAnalystId 106 | * @param string $adminApproval 107 | * @param string $adminApprovalDate 108 | * @param string $analystApproval 109 | * @param string $analystApprovalDate 110 | * @param int $oldShiftsId 111 | * @param int $newShiftsId 112 | * @param string desc 113 | * @param string type 114 | * @return DataResponse 115 | */ 116 | public function update(int $id, string $oldAnalystId, string $newAnalystId, string $adminApproval, string $adminApprovalDate, string $analystApproval, string $analystApprovalDate, int $oldShiftsId, int $newShiftsId, string $desc, int $type): DataResponse 117 | { 118 | if($this->permService->isRequestingUser($oldAnalystId) || $this->permService->isRequestingUser($newAnalystId) || $this->permService->isRequestingUserAdmin()) { 119 | try { 120 | $result = $this->service->update($id, $oldAnalystId, $newAnalystId, $adminApproval, $adminApprovalDate, $analystApproval, $analystApprovalDate, $oldShiftsId, $newShiftsId, $desc, $type); 121 | return new DataResponse($result, Http::STATUS_OK); 122 | } catch (Exception $e) { 123 | return $this->handleException($e); 124 | } 125 | } else { 126 | return new DataResponse(NULL, Http::STATUS_UNAUTHORIZED); 127 | } 128 | } 129 | 130 | /** 131 | * @NoAdminRequired 132 | * 133 | * @param int $id 134 | * @return DataResponse 135 | */ 136 | public function destroy(int $id): DataResponse 137 | { 138 | if($this->permService->isRequestingUserAdmin()) { 139 | try { 140 | $result = $this->service->delete($id); 141 | return new DataResponse($result, Http::STATUS_OK); 142 | } catch(Exception $e) { 143 | return $this->handleException($e); 144 | } 145 | } else { 146 | return new DataResponse(NULL, Http::STATUS_UNAUTHORIZED); 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /lib/Controller/ShiftsTypeController.php: -------------------------------------------------------------------------------- 1 | 4 | * @copyright Copyright (c) 2023. Kevin Küchler 5 | * 6 | * @author Fabian Kirchesch 7 | * @author Kevin Küchler 8 | */ 9 | 10 | namespace OCA\Shifts\Controller; 11 | 12 | use OCP\AppFramework\Http; 13 | use OCA\Shifts\Service\PermissionService; 14 | 15 | use OCA\Shifts\AppInfo\Application; 16 | use OCA\Shifts\Service\ShiftsTypeService; 17 | use OCP\AppFramework\Controller; 18 | use OCP\AppFramework\Http\DataResponse; 19 | use OCP\IRequest; 20 | 21 | class ShiftsTypeController extends Controller{ 22 | /** @var ShiftsTypeService */ 23 | private $service; 24 | 25 | /** @var PermissionService */ 26 | private $permService; 27 | 28 | use Errors; 29 | 30 | 31 | public function __construct(IRequest $request, ShiftsTypeService $service, PermissionService $permService){ 32 | parent::__construct(Application::APP_ID, $request); 33 | $this->service = $service; 34 | $this->permService = $permService; 35 | } 36 | 37 | /** 38 | * @NoAdminRequired 39 | * @NoCSRFRequired 40 | */ 41 | public function index(): DataResponse { 42 | return new DataResponse($this->service->findAll()); 43 | } 44 | 45 | /** 46 | * @NoAdminRequired 47 | * 48 | * @param int $id 49 | * @return DataResponse 50 | */ 51 | public function show(int $id): DataResponse { 52 | return $this->handleNotFound(function () use($id){ 53 | return $this->service->find($id); 54 | }); 55 | } 56 | 57 | /** 58 | * @NoAdminRequired 59 | * 60 | * @param string $name 61 | * @param string $description 62 | * @param string $startTimestamp 63 | * @param string $stopTimestamp 64 | * @param string $color 65 | * @param int $moRule 66 | * @param int $tuRule 67 | * @param int $weRule 68 | * @param int $thRule 69 | * @param int $frRule 70 | * @param int $saRule 71 | * @param int $soRule 72 | * @param int $skillGroupId 73 | * @param boolean $isWeekly 74 | * @param boolean $deleted 75 | * @return DataResponse 76 | */ 77 | public function create(string $name, string $description, string $startTimestamp, string $stopTimestamp, string $color, 78 | int $moRule, int $tuRule, int $weRule, int $thRule, int $frRule, int $saRule, int $soRule, int $skillGroupId, bool $isWeekly): DataResponse { 79 | if(!$this->permService->isRequestingUserAdmin()) { 80 | return new DataResponse(NULL, Http::STATUS_UNAUTHORIZED); 81 | } 82 | 83 | return new DataResponse($this->service->create($name, $description, $startTimestamp, $stopTimestamp, $color, 84 | $moRule, $tuRule, $weRule, $thRule, $frRule, $saRule, $soRule, $skillGroupId, $isWeekly)); 85 | } 86 | 87 | /** 88 | * @NoAdminRequired 89 | * 90 | * @param int $id 91 | * @param string $name 92 | * @param string $desc 93 | * @param string $startTimestamp 94 | * @param string $stopTimestamp 95 | * @param string $color 96 | * @param int $moRule 97 | * @param int $tuRule 98 | * @param int $weRule 99 | * @param int $thRule 100 | * @param int $frRule 101 | * @param int $saRule 102 | * @param int $soRule 103 | * @param int skillGroupId 104 | * @param boolean isWeekly 105 | * @param boolean deleted 106 | * @return DataResponse 107 | */ 108 | public function update(int $id, string $name, string $desc, string $startTimestamp, string $stopTimestamp, string $color, 109 | int $moRule, int $tuRule, int $weRule, int $thRule, int $frRule, int $saRule, int $soRule, int $skillGroupId, bool $isWeekly, bool $deleted): DataResponse 110 | { 111 | if(!$this->permService->isRequestingUserAdmin()) { 112 | return new DataResponse(NULL, Http::STATUS_UNAUTHORIZED); 113 | } 114 | 115 | return $this->handleNotFound(function() use ($id, $name, $desc, $startTimestamp, $stopTimestamp, $color, 116 | $moRule, $tuRule, $weRule, $thRule, $frRule, $saRule, $soRule, $skillGroupId, $isWeekly, $deleted){ 117 | return $this->service->update($id, $name, $desc, $startTimestamp, $stopTimestamp, $color, 118 | $moRule, $tuRule, $weRule, $thRule, $frRule, $saRule, $soRule, $skillGroupId, $isWeekly, $deleted); 119 | }); 120 | } 121 | 122 | /** 123 | * @NoAdminRequired 124 | * 125 | * @param int $id 126 | * @return DataResponse 127 | */ 128 | public function destroy(int $id): DataResponse 129 | { 130 | if(!$this->permService->isRequestingUserAdmin()) { 131 | return new DataResponse(NULL, Http::STATUS_UNAUTHORIZED); 132 | } 133 | 134 | return $this->handleNotFound(function() use($id) { 135 | return $this->service->delete($id); 136 | }); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /lib/Db/Shift.php: -------------------------------------------------------------------------------- 1 | 4 | * @copyright Copyright (c) 2023. Kevin Küchler 5 | * 6 | * @author Fabian Kirchesch 7 | * @author Kevin Küchler 8 | */ 9 | 10 | namespace OCA\Shifts\Db; 11 | 12 | use JsonSerializable; 13 | 14 | use OCP\AppFramework\Db\Entity; 15 | 16 | class Shift extends Entity implements JsonSerializable { 17 | 18 | protected $userId; 19 | protected $shiftTypeId; 20 | protected $date; 21 | 22 | public function __construct(){ 23 | $this->addType('id','integer'); 24 | } 25 | 26 | /** 27 | * @return string 28 | */ 29 | public function getUserId(): string { 30 | return $this->userId; 31 | } 32 | 33 | /** 34 | * @return int 35 | */ 36 | public function getShiftTypeId(): int { 37 | return $this->shiftTypeId; 38 | } 39 | 40 | /** 41 | * @return string 42 | */ 43 | public function getDate(): string { 44 | return $this->date; 45 | } 46 | 47 | public function jsonSerialize(){ 48 | return [ 49 | 'id' => $this->id, 50 | 'userId' => $this->userId, 51 | 'shiftTypeId' => $this->shiftTypeId, 52 | 'date' => $this->date, 53 | ]; 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /lib/Db/ShiftMapper.php: -------------------------------------------------------------------------------- 1 | 4 | * @copyright Copyright (c) 2023. Kevin Küchler 5 | * 6 | * @author Fabian Kirchesch 7 | * @author Kevin Küchler 8 | */ 9 | 10 | namespace OCA\Shifts\Db; 11 | 12 | use OCP\AppFramework\Db\DoesNotExistException; 13 | use OCP\AppFramework\Db\Entity; 14 | use OCP\AppFramework\Db\MultipleObjectsReturnedException; 15 | use OCP\AppFramework\Db\QBMapper; 16 | use OCP\DB\QueryBuilder\IQueryBuilder; 17 | use OCP\IDBConnection; 18 | use Throwable; 19 | 20 | class ShiftMapper extends QBMapper { 21 | public function __construct(IDBConnection $db) { 22 | parent::__construct($db, 'shifts', Shift::class); 23 | } 24 | 25 | /** 26 | * Finds Shift by Shiftid 27 | * @param int $id 28 | * @return Shift 29 | */ 30 | public function find(int $id): Shift { 31 | /* @var $qb IQueryBuilder */ 32 | $qb = $this->db->getQueryBuilder(); 33 | $qb->select('*') 34 | ->from('shifts') 35 | ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); 36 | return $this->findEntity($qb); 37 | } 38 | 39 | /** 40 | * Finds all Shifts by given userId 41 | * @param string $userId 42 | * @return array 43 | */ 44 | public function findById(string $userId): array { 45 | /* @var $qb IQueryBuilder */ 46 | $qb = $this->db->getQueryBuilder(); 47 | $qb->select('*') 48 | ->from('shifts') 49 | ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId))); 50 | return $this->findEntities($qb); 51 | } 52 | 53 | /** 54 | * Fetches all Shifts by ShiftsType 55 | */ 56 | public function findByShiftsTypeId(int $id): array { 57 | /* @var $qb IQueryBuilder */ 58 | $qb = $this->db->getQueryBuilder(); 59 | $qb->select('*') 60 | ->from('shifts') 61 | ->where($qb->expr()->eq('shift_type_id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); 62 | return $this->findEntities($qb); 63 | } 64 | 65 | /** 66 | * Fetches all Shifts 67 | * @return array 68 | */ 69 | public function findAll(): array { 70 | /* @var $qb IQueryBuilder */ 71 | $qb = $this->db->getQueryBuilder(); 72 | $qb->select('*') 73 | ->from('shifts'); 74 | return $this->findEntities($qb); 75 | } 76 | 77 | /** 78 | * Fetches shifts by Datestring 79 | * @param $currentDate 80 | * @param $type 81 | * @return array 82 | */ 83 | public function findByDateAndType($currentDate, $type): array { 84 | /* @var $qb IQueryBuilder */ 85 | $qb = $this->db->getQueryBuilder(); 86 | $qb->select('*') 87 | ->from('shifts') 88 | ->where($qb->expr()->eq('date', $qb->createNamedParameter($currentDate))) 89 | ->andWhere($qb->expr()->eq('shift_type_id', $qb->createNamedParameter($type))); 90 | return $this->findEntities($qb); 91 | } 92 | 93 | /** 94 | * Fetches shifts by date, type and assignmentstatus 95 | * 96 | */ 97 | public function findByDateTypeandAssignment($currentDate, $type): array { 98 | /* @var $qb IQueryBuilder */ 99 | $qb = $this->db->getQueryBuilder(); 100 | $qb->select('*') 101 | ->from('shifts') 102 | ->where($qb->expr()->eq('date', $qb->createNamedParameter($currentDate))) 103 | ->andWhere($qb->expr()->eq('shift_type_id', $qb->createNamedParameter($type))) 104 | ->andWhere($qb->expr()->eq('user_id', $qb->createNamedParameter('-1'))); 105 | return $this->findEntities($qb); 106 | } 107 | 108 | /** 109 | * Fetches all shifts in timerange for archival display 110 | * @param string $start 111 | * @param string $end 112 | * @return array 113 | */ 114 | public function findByTimeRange(string $start, string $end) : array { 115 | $startDate = date('Y-m-d', strtotime($start)); 116 | $endDate = date('Y-m-d', strtotime($end)); 117 | /* @var $qb IQueryBuilder */ 118 | $qb = $this->db->getQueryBuilder(); 119 | $qb->select('user_id', 'shift_type_id', $qb->func()->count('*','count')) 120 | ->from('shifts') 121 | ->where($qb->expr()->neq('user_id', $qb->createNamedParameter('-1'))) 122 | ->andWhere($qb->expr()->gte('date', $qb->createNamedParameter($startDate))) 123 | ->andWhere($qb->expr()->lt('date', $qb->createNamedParameter($endDate))) 124 | ->groupBy('user_id', 'shift_type_id') 125 | ->orderBy('user_id'); 126 | $result = $qb->execute(); 127 | return $result->fetchAll(); 128 | } 129 | 130 | /** 131 | * Swaps dates of two shifts with the same shiftTypeId 132 | * @param int oldShiftId 133 | * @param int newShiftId 134 | * @return void 135 | */ 136 | public function swapShifts(int $oldShiftId, string $oldUserId, string $oldDate, int $newShiftId, string $newUserId, string $newDate): void { 137 | $this->db->beginTransaction(); 138 | try { 139 | /* @var $qb IQueryBuilder */ 140 | $qb = $this->db->getQueryBuilder(); 141 | $qb->update('shifts','s') 142 | ->set('s.user_id', $qb->createNamedParameter($newUserId)) 143 | ->where($qb->expr()->eq('s.user_id', $qb->createNamedParameter($oldUserId))) 144 | ->andWhere($qb->expr()->eq('s.date', $qb->createNamedParameter($oldDate))) 145 | ->andWhere($qb->expr()->eq('s.id', $qb->createNamedParameter($oldShiftId))); 146 | $qb->executeStatement(); 147 | 148 | $qb = $this->db->getQueryBuilder(); 149 | $qb->update('shifts','s') 150 | ->set('s.user_id', $qb->createNamedParameter($oldUserId)) 151 | ->where($qb->expr()->eq('s.user_id', $qb->createNamedParameter($newUserId))) 152 | ->andWhere($qb->expr()->eq('s.date', $qb->createNamedParameter($newDate))) 153 | ->andWhere($qb->expr()->eq('s.id', $qb->createNamedParameter($newShiftId))); 154 | $qb->executeStatement(); 155 | 156 | $this->db->commit(); 157 | } catch(Throwable $e) { 158 | $this->db->rollBack(); 159 | throw $e; 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /lib/Db/ShiftsCalendarChange.php: -------------------------------------------------------------------------------- 1 | 4 | * @copyright Copyright (c) 2023. Kevin Küchler 5 | * 6 | * @author Fabian Kirchesch 7 | * @author Kevin Küchler 8 | */ 9 | 10 | namespace OCA\Shifts\Db; 11 | 12 | use JsonSerializable; 13 | 14 | use OCP\AppFramework\Db\Entity; 15 | 16 | class ShiftsCalendarChange extends Entity implements JsonSerializable { 17 | protected $shiftId; 18 | protected $shiftTypeId; 19 | protected $shiftDate; 20 | protected $oldUserId; 21 | protected $newUserId; 22 | protected $action; 23 | protected $dateChanged; 24 | protected $adminId; 25 | protected $isDone; 26 | 27 | public function __construct(){ 28 | $this->addType('id','integer'); 29 | } 30 | 31 | public function getShiftId(): int { 32 | return $this->shiftId; 33 | } 34 | 35 | public function getShiftTypeId(): int { 36 | return $this->shiftTypeId; 37 | } 38 | 39 | public function setIsDone(bool $isDone): void { 40 | $this->setter('isDone', array($isDone ? '1' : '0')); 41 | } 42 | 43 | public function getAction(): string { 44 | return $this->action; 45 | } 46 | 47 | public function getOldUserId(): string { 48 | return $this->oldUserId; 49 | } 50 | 51 | public function getNewUserId(): string { 52 | return $this->newUserId; 53 | } 54 | 55 | public function jsonSerialize(){ 56 | return [ 57 | 'id' => $this->id, 58 | 'shiftId' => $this->shiftId, 59 | 'shiftTypeId' => $this->shiftTypeId, 60 | 'shiftDate' => $this->shiftDate, 61 | 'oldUserId' => $this->oldUserId, 62 | 'newUserId' => $this->newUserId, 63 | 'action' => $this->action, 64 | 'dateChanged' => $this->dateChanged, 65 | 'adminId' => $this->adminId, 66 | 'isDone' => $this->isDone, 67 | ]; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/Db/ShiftsCalendarChangeMapper.php: -------------------------------------------------------------------------------- 1 | 4 | * @copyright Copyright (c) 2023. Kevin Küchler 5 | * 6 | * @author Fabian Kirchesch 7 | * @author Kevin Küchler 8 | */ 9 | 10 | namespace OCA\Shifts\Db; 11 | 12 | use OCP\AppFramework\Db\DoesNotExistException; 13 | use OCP\AppFramework\Db\Entity; 14 | use OCP\AppFramework\Db\QBMapper; 15 | use OCP\DB\Exception; 16 | use OCP\DB\QueryBuilder\IQueryBuilder; 17 | use OCP\IDBConnection; 18 | 19 | class ShiftsCalendarChangeMapper extends QBMapper { 20 | public function __construct(IDBConnection $db) { 21 | parent::__construct($db, 'shifts_cal_changes', ShiftsCalendarChange::class); 22 | } 23 | 24 | public function find(int $id): ShiftsCalendarChange { 25 | /* @var $qb IQueryBuilder */ 26 | $qb = $this->db->getQueryBuilder(); 27 | $qb->select('*') 28 | ->from('shifts_cal_changes') 29 | ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); 30 | 31 | return $this->findEntity($qb); 32 | } 33 | 34 | /** 35 | * Fetches all ShiftsChanges 36 | * @return ShiftsCalendarChange[] 37 | */ 38 | public function findAll(): array { 39 | /* @var $qb IQueryBuilder */ 40 | $qb = $this->db->getQueryBuilder(); 41 | $qb->select('*') 42 | ->from('shifts_cal_changes') 43 | ->orderBy('shift_type_id'); 44 | return $this->findEntities($qb); 45 | } 46 | 47 | /** 48 | * Fetches all open ShiftsChanges 49 | * 50 | * @return ShiftsCalendarChange[] 51 | * @throws Exception 52 | */ 53 | public function findAllOpen(): array { 54 | $qb = $this->db->getQueryBuilder(); 55 | $qb->select('*') 56 | ->from('shifts_cal_changes') 57 | ->where($qb->expr()->eq('is_done', $qb->createNamedParameter('0'))) 58 | ->orderBy('shift_type_id', 'DESC'); 59 | return $this->findEntities($qb); 60 | } 61 | 62 | /** 63 | * Fetches all open ShiftsChanges for shiftId 64 | * 65 | * @return ShiftsCalendarChange[] 66 | * @throws Exception 67 | */ 68 | public function findAllOpenByShiftId(int $shiftId): array { 69 | $qb = $this->db->getQueryBuilder(); 70 | $qb->select('*') 71 | ->from('shifts_cal_changes') 72 | ->where($qb->expr()->eq('is_done', $qb->createNamedParameter('0'))) 73 | ->andWhere($qb->expr()->eq('shift_id', $qb->createNamedParameter($shiftId, IQueryBuilder::PARAM_INT))); 74 | return $this->findEntities($qb); 75 | } 76 | 77 | /** 78 | * Fetches Shiftchange by shiftId and action 79 | * @param int $shiftId 80 | * @param string $shiftChangeAction 81 | * @return ShiftsCalendarChange 82 | */ 83 | public function findByShiftIdAndType(int $shiftId, string $shiftChangeAction): ShiftsCalendarChange { 84 | /* @var $qb IQueryBuilder */ 85 | $qb = $this->db->getQueryBuilder(); 86 | $qb->select('*') 87 | ->from('shifts_cal_changes') 88 | ->where($qb->expr()->eq('is_done', $qb->createNamedParameter('0'))) 89 | ->andWhere($qb->expr()->eq('shift_id', $qb->createNamedParameter($shiftId, IQueryBuilder::PARAM_INT))) 90 | ->orderBy('date_changed') 91 | ->setMaxResults(1); 92 | return $this->findEntity($qb); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/Db/ShiftsChange.php: -------------------------------------------------------------------------------- 1 | 4 | * @copyright Copyright (c) 2023. Kevin Küchler 5 | * 6 | * @author Fabian Kirchesch 7 | * @author Kevin Küchler 8 | */ 9 | 10 | namespace OCA\Shifts\Db; 11 | 12 | use JsonSerializable; 13 | 14 | use OCP\AppFramework\Db\Entity; 15 | 16 | class ShiftsChange extends Entity implements JsonSerializable { 17 | 18 | protected $oldAnalystId; 19 | protected $newAnalystId; 20 | protected $adminApproval; 21 | protected $adminApprovalDate; 22 | protected $analystApproval; 23 | protected $analystApprovalDate; 24 | protected $oldShiftsId; 25 | protected $newShiftsId; 26 | protected $desc; 27 | protected $type; 28 | 29 | public function __construct(){ 30 | $this->addType('id','integer'); 31 | } 32 | 33 | public function getOldShiftsId(): int { 34 | return $this->oldShiftsId; 35 | } 36 | 37 | public function getNewShiftsId(): int { 38 | return $this->newShiftsId; 39 | } 40 | 41 | public function getOldAnalystId(): string { 42 | return $this->oldAnalystId; 43 | } 44 | 45 | public function getNewAnalystId(): string { 46 | return $this->newAnalystId; 47 | } 48 | 49 | public function jsonSerialize(){ 50 | return [ 51 | 'id' => $this->id, 52 | 'oldAnalystId' => $this->oldAnalystId, 53 | 'newAnalystId' => $this->newAnalystId, 54 | 'adminApproval' => $this->adminApproval, 55 | 'adminApprovalDate' => $this->adminApprovalDate, 56 | 'analystApproval' => $this->analystApproval, 57 | 'analystApprovalDate' => $this->analystApprovalDate, 58 | 'oldShiftsId' => $this->oldShiftsId, 59 | 'newShiftsId' => $this->newShiftsId, 60 | 'desc' => $this->desc, 61 | 'type' => $this->type, 62 | ]; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /lib/Db/ShiftsChangeMapper.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * @author Fabian Kirchesch 6 | */ 7 | 8 | namespace OCA\Shifts\Db; 9 | 10 | use OCP\AppFramework\Db\DoesNotExistException; 11 | use OCP\AppFramework\Db\Entity; 12 | use OCP\AppFramework\Db\QBMapper; 13 | use OCP\DB\QueryBuilder\IQueryBuilder; 14 | use OCP\IDBConnection; 15 | 16 | class ShiftsChangeMapper extends QBMapper { 17 | public function __construct(IDBConnection $db) { 18 | parent::__construct($db, 'shifts_change', ShiftsChange::class); 19 | } 20 | 21 | /** 22 | * Finds ShiftsChange by id 23 | * @param int $id 24 | * @return Entity|ShiftsChange 25 | * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException 26 | * @throws DoesNotExistException 27 | */ 28 | public function find(int $id): ShiftsChange { 29 | /* @var $qb IQueryBuilder */ 30 | $qb = $this->db->getQueryBuilder(); 31 | $qb->select('*') 32 | ->from('shifts_change') 33 | ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); 34 | return $this->findEntity($qb); 35 | } 36 | 37 | /** 38 | * Fetches all ShiftsChanges 39 | * @return array 40 | */ 41 | public function findAll(): array { 42 | /* @var $qb IQueryBuilder */ 43 | $qb = $this->db->getQueryBuilder(); 44 | $qb->select('*') 45 | ->from('shifts_change'); 46 | return $this->findEntities($qb); 47 | } 48 | 49 | /** 50 | * Finds ShiftsChanges by given userId 51 | * @param string $userId 52 | * @return array 53 | */ 54 | public function findByUserId(string $userId): array { 55 | /* @var $qb IQueryBuilder */ 56 | $qb = $this->db->getQueryBuilder(); 57 | $qb->select('*') 58 | ->from('shifts_change') 59 | ->where($qb->expr()->eq('old_analyst_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR ))) 60 | ->orWhere($qb->expr()->eq('new_analyst_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR ))); 61 | 62 | return $this->findEntities($qb); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /lib/Db/ShiftsType.php: -------------------------------------------------------------------------------- 1 | 4 | * @copyright Copyright (c) 2023. Kevin Küchler 5 | * 6 | * @author Fabian Kirchesch 7 | * @author Kevin Küchler 8 | */ 9 | 10 | namespace OCA\Shifts\Db; 11 | 12 | use JsonSerializable; 13 | 14 | use OCP\AppFramework\Db\Entity; 15 | 16 | class ShiftsType extends Entity implements JsonSerializable { 17 | 18 | protected $name; 19 | protected $desc; 20 | protected $startTimeStamp; 21 | protected $stopTimeStamp; 22 | protected $calendarColor; 23 | protected $moRule; 24 | protected $tuRule; 25 | protected $weRule; 26 | protected $thRule; 27 | protected $frRule; 28 | protected $saRule; 29 | protected $soRule; 30 | protected $skillGroupId; 31 | protected $isWeekly; 32 | protected $deleted; 33 | 34 | public function __construct(){ 35 | $this->addType('id','integer'); 36 | } 37 | 38 | public function getName(): string { 39 | return $this->name; 40 | } 41 | 42 | public function getDescription(): string { 43 | return $this->desc; 44 | } 45 | 46 | public function getStartTimestamp(): string { 47 | return $this->startTimeStamp; 48 | } 49 | 50 | public function getStopTimestamp(): string { 51 | return $this->stopTimeStamp; 52 | } 53 | 54 | public function isWeekly(): bool { 55 | return $this->isWeekly; 56 | } 57 | 58 | public function getSkillGroupId() { 59 | return $this->skillGroupId; 60 | } 61 | 62 | public function jsonSerialize(){ 63 | return [ 64 | 'id' => $this->id, 65 | 'name' => $this->name, 66 | 'desc' => $this->desc, 67 | 'startTimestamp' => $this->startTimeStamp, 68 | 'stopTimestamp' => $this->stopTimeStamp, 69 | 'color' => $this->calendarColor, 70 | 'moRule' => $this->moRule, 71 | 'tuRule' => $this->tuRule, 72 | 'weRule' => $this->weRule, 73 | 'thRule' => $this->thRule, 74 | 'frRule' => $this->frRule, 75 | 'saRule' => $this->saRule, 76 | 'soRule' => $this->soRule, 77 | 'skillGroupId' => $this->skillGroupId, 78 | 'isWeekly' => $this->isWeekly, 79 | 'deleted' => $this->deleted, 80 | ]; 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /lib/Db/ShiftsTypeMapper.php: -------------------------------------------------------------------------------- 1 | 4 | * @copyright Copyright (c) 2023. Kevin Küchler 5 | * 6 | * @author Fabian Kirchesch 7 | * @author Kevin Küchler 8 | */ 9 | 10 | namespace OCA\Shifts\Db; 11 | 12 | use OCP\AppFramework\Db\DoesNotExistException; 13 | use OCP\AppFramework\Db\Entity; 14 | use OCP\AppFramework\Db\QBMapper; 15 | use OCP\DB\QueryBuilder\IQueryBuilder; 16 | use OCP\IDBConnection; 17 | 18 | class ShiftsTypeMapper extends QBMapper { 19 | public function __construct(IDBConnection $db) { 20 | parent::__construct($db, 'shifts_type', ShiftsType::class); 21 | } 22 | 23 | /** 24 | * Finds ShiftsType by id 25 | * @param int $id 26 | * @return Entity|ShiftsType 27 | * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException 28 | * @throws DoesNotExistException 29 | */ 30 | public function find(int $id): ShiftsType { 31 | /* @var $qb IQueryBuilder */ 32 | $qb = $this->db->getQueryBuilder(); 33 | $qb->select('*') 34 | ->from('shifts_type') 35 | ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); 36 | return $this->findEntity($qb); 37 | } 38 | 39 | /** 40 | * Fetches all ShiftsTypes 41 | * @return array 42 | */ 43 | public function findAll(): array { 44 | /* @var $qb IQueryBuilder */ 45 | $qb = $this->db->getQueryBuilder(); 46 | $qb->select('*') 47 | ->from('shifts_type') 48 | ->where($qb->expr()->eq('deleted', $qb->createNamedParameter('0'))); 49 | return $this->findEntities($qb); 50 | } 51 | 52 | /** 53 | * Fetches Shift Rules 54 | * @return array 55 | */ 56 | public function findAllRules(): array { 57 | /* @var $qb IQueryBuilder */ 58 | $qb = $this->db->getQueryBuilder(); 59 | $qb->select('id', 'mo_rule', 'tu_rule', 'we_rule', 'th_rule', 'fr_rule', 'sa_rule', 'so_rule', 'is_weekly') 60 | ->from('shifts_type'); 61 | return $this->findEntities($qb); 62 | } 63 | 64 | /** 65 | * Find all skill group ids 66 | * @return array 67 | */ 68 | public function findAllSkillGroupIds(): array { 69 | /* @var $qb IQueryBuilder */ 70 | $qb = $this->db->getQueryBuilder(); 71 | $qb->select('skill_group_id') 72 | ->from('shifts_type') 73 | ->groupBy('skill_group_id'); 74 | return $this->findEntities($qb); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/Migration/Version100001Date20181013124731.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * @author Fabian Kirchesch 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace OCA\Shifts\Migration; 11 | 12 | use Closure; 13 | use OCP\DB\ISchemaWrapper; 14 | use OCP\Migration\SimpleMigrationStep; 15 | use OCP\Migration\IOutput; 16 | 17 | class Version100001Date20181013124731 extends SimpleMigrationStep { 18 | 19 | /** 20 | * Creates Tables for Version 1.0.0 (deprecated) 21 | * @param IOutput $output 22 | * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` 23 | * @param array $options 24 | * @return null|ISchemaWrapper 25 | */ 26 | public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) { 27 | /** @var ISchemaWrapper $schema */ 28 | $schema = $schemaClosure(); 29 | 30 | if (!$schema->hasTable('shifts')) { 31 | $table = $schema->createTable('shifts'); 32 | $table->addColumn('id', 'integer', [ 33 | 'autoincrement' => true, 34 | 'notnull' => true, 35 | ]); 36 | $table->addColumn('user_id', 'string', [ 37 | 'notnull' => true, 38 | 'length' => 200, 39 | ]); 40 | $table->addColumn('shift_type_id', 'integer', [ 41 | 'notnull' => true, 42 | 'length' => 200, 43 | ]); 44 | $table->addColumn('date', 'string', [ 45 | 'notnull' => true, 46 | 'default' => '' 47 | ]); 48 | 49 | $table->setPrimaryKey(['id']); 50 | } 51 | if (!$schema->hasTable('shifts_type')) { 52 | $table = $schema->createTable('shifts_type'); 53 | $table->addColumn('id', 'integer', [ 54 | 'autoincrement' => true, 55 | 'notnull' => true, 56 | ]); 57 | $table->addColumn('name', 'string', [ 58 | 'notnull' => true, 59 | 'length' => 200, 60 | ]); 61 | $table->addColumn('desc', 'string', [ 62 | 'notnull' => true, 63 | 'length' => 200, 64 | ]); 65 | $table->addColumn('start_time_stamp', 'string', [ 66 | 'notnull' => true, 67 | 'length' => 200, 68 | ]); 69 | $table->addColumn('stop_time_stamp', 'string', [ 70 | 'notnull' => true, 71 | 'length' => 200, 72 | ]); 73 | 74 | $table->setPrimaryKey(['id']); 75 | } 76 | return $schema; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/Migration/Version110000Date20210308091041.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * @author Fabian Kirchesch 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace OCA\Shifts\Migration; 11 | 12 | use Closure; 13 | use OCP\DB\ISchemaWrapper; 14 | use OCP\Migration\IOutput; 15 | use OCP\Migration\SimpleMigrationStep; 16 | 17 | /** 18 | * Auto-generated migration step: Please modify to your needs! 19 | */ 20 | class Version110000Date20210308091041 extends SimpleMigrationStep { 21 | 22 | /** 23 | * @param IOutput $output 24 | * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` 25 | * @param array $options 26 | */ 27 | public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { 28 | } 29 | 30 | /** 31 | * Adds Tables for Version 1.1.0 32 | * @param IOutput $output 33 | * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` 34 | * @param array $options 35 | * @return null|ISchemaWrapper 36 | */ 37 | public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { 38 | /** @var ISchemaWrapper $schema */ 39 | $schema = $schemaClosure(); 40 | 41 | if (!$schema->hasTable('shifts_change')) { 42 | $table = $schema->createTable('shifts_change'); 43 | $table->addColumn('id', 'integer', [ 44 | 'autoincrement' => true, 45 | 'notnull' => true, 46 | ]); 47 | $table->addColumn('old_analyst_id', 'string', [ 48 | 'notnull' => true, 49 | 'length' => 200, 50 | ]); 51 | $table->addColumn('new_analyst_id', 'string', [ 52 | 'notnull' => true, 53 | 'length' => 200, 54 | ]); 55 | $table->addColumn('admin_approval', 'string', [ 56 | 'length' => 200, 57 | ]); 58 | $table->addColumn('admin_approval_date', 'string', [ 59 | 'notnull' => true, 60 | 'length' => 200, 61 | ]); 62 | $table->addColumn('analyst_approval', 'string', [ 63 | 'length' => 200, 64 | ]); 65 | $table->addColumn('analyst_approval_date', 'string', [ 66 | 'notnull' => true, 67 | 'length' => 200, 68 | ]); 69 | $table->addColumn('old_shifts_id', 'integer', [ 70 | 'notnull' => true, 71 | ]); 72 | $table->addColumn('new_shifts_id', 'integer'); 73 | $table->addColumn('desc', 'string', [ 74 | 'notnull' => true, 75 | 'length' => 200, 76 | ]); 77 | $table->addColumn('type', 'string', [ 78 | 'notnull' => true, 79 | 'length' => 200, 80 | ]); 81 | 82 | $table->setPrimaryKey(['id']); 83 | } 84 | return $schema; 85 | } 86 | 87 | /** 88 | * @param IOutput $output 89 | * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` 90 | * @param array $options 91 | */ 92 | public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/Migration/Version120000Date20210406073005.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * @author Fabian Kirchesch 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace OCA\Shifts\Migration; 11 | 12 | use Closure; 13 | use OCP\DB\ISchemaWrapper; 14 | use OCP\Migration\IOutput; 15 | use OCP\Migration\SimpleMigrationStep; 16 | 17 | /** 18 | * Auto-generated migration step: Please modify to your needs! 19 | */ 20 | class Version120000Date20210406073005 extends SimpleMigrationStep { 21 | 22 | /** 23 | * @param IOutput $output 24 | * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` 25 | * @param array $options 26 | */ 27 | public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { 28 | } 29 | 30 | /** 31 | * @param IOutput $output 32 | * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` 33 | * @param array $options 34 | * @return null|ISchemaWrapper 35 | */ 36 | public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { 37 | $schema = $schemaClosure(); 38 | 39 | $table = $schema->getTable('shifts_type'); 40 | if (!$table->hasColumn('calendar_color')) { 41 | $table->addColumn('calendar_color', 'string', [ 42 | 'notnull' => true, 43 | 'length' => 64, 44 | ]); 45 | } 46 | return $schema; 47 | } 48 | 49 | /** 50 | * @param IOutput $output 51 | * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` 52 | * @param array $options 53 | */ 54 | public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/Migration/Version140000Date20210420123817.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * @author Fabian Kirchesch 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace OCA\Shifts\Migration; 11 | 12 | use Closure; 13 | use OCP\DB\ISchemaWrapper; 14 | use OCP\Migration\IOutput; 15 | use OCP\Migration\SimpleMigrationStep; 16 | 17 | /** 18 | * Auto-generated migration step: Please modify to your needs! 19 | */ 20 | class Version140000Date20210420123817 extends SimpleMigrationStep { 21 | 22 | /** 23 | * @param IOutput $output 24 | * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` 25 | * @param array $options 26 | */ 27 | public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { 28 | } 29 | 30 | /** 31 | * @param IOutput $output 32 | * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` 33 | * @param array $options 34 | * @return null|ISchemaWrapper 35 | */ 36 | public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { 37 | $schema = $schemaClosure(); 38 | 39 | $shiftsTypeTable = $schema->getTable('shifts_type'); 40 | 41 | if (!$shiftsTypeTable->hasColumn('mo_rule')) { 42 | $shiftsTypeTable->addColumn('mo_rule', 'integer', [ 43 | 'notnull' => true, 44 | ]); 45 | $shiftsTypeTable->addColumn('tu_rule', 'integer', [ 46 | 'notnull' => true, 47 | ]); 48 | $shiftsTypeTable->addColumn('we_rule', 'integer', [ 49 | 'notnull' => true, 50 | ]); 51 | $shiftsTypeTable->addColumn('th_rule', 'integer', [ 52 | 'notnull' => true, 53 | ]); 54 | $shiftsTypeTable->addColumn('fr_rule', 'integer', [ 55 | 'notnull' => true, 56 | ]); 57 | $shiftsTypeTable->addColumn('sa_rule', 'integer', [ 58 | 'notnull' => true, 59 | ]); 60 | $shiftsTypeTable->addColumn('so_rule', 'integer', [ 61 | 'notnull' => true, 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 | -------------------------------------------------------------------------------- /lib/Migration/Version150000Date20210506123426.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * @author Fabian Kirchesch 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace OCA\Shifts\Migration; 11 | 12 | use Closure; 13 | use OCP\DB\ISchemaWrapper; 14 | use OCP\Migration\IOutput; 15 | use OCP\Migration\SimpleMigrationStep; 16 | 17 | /** 18 | * Auto-generated migration step: Please modify to your needs! 19 | */ 20 | class Version150000Date20210506123426 extends SimpleMigrationStep { 21 | 22 | /** 23 | * @param IOutput $output 24 | * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` 25 | * @param array $options 26 | */ 27 | public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { 28 | } 29 | 30 | /** 31 | * @param IOutput $output 32 | * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` 33 | * @param array $options 34 | * @return null|ISchemaWrapper 35 | */ 36 | public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { 37 | $schema = $schemaClosure(); 38 | $shiftsTypeTable = $schema->getTable('shifts_type'); 39 | 40 | if (!$shiftsTypeTable->hasColumn('skill_group_id')) { 41 | $shiftsTypeTable->addColumn('skill_group_id', 'integer', [ 42 | 'notnull' => true, 43 | ]); 44 | } 45 | return $schema; 46 | } 47 | 48 | /** 49 | * @param IOutput $output 50 | * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` 51 | * @param array $options 52 | */ 53 | public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/Migration/Version160000Date20210824102626.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * @author Fabian Kirchesch 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace OCA\Shifts\Migration; 11 | 12 | use Closure; 13 | use OCP\DB\ISchemaWrapper; 14 | use OCP\Migration\IOutput; 15 | use OCP\Migration\SimpleMigrationStep; 16 | 17 | /** 18 | * Auto-generated migration step: Please modify to your needs! 19 | */ 20 | class Version160000Date20210824102626 extends SimpleMigrationStep { 21 | 22 | /** 23 | * @param IOutput $output 24 | * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` 25 | * @param array $options 26 | */ 27 | public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { 28 | } 29 | 30 | /** 31 | * @param IOutput $output 32 | * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` 33 | * @param array $options 34 | * @return null|ISchemaWrapper 35 | */ 36 | public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { 37 | $schema = $schemaClosure(); 38 | $shiftsTypeTable = $schema->getTable('shifts_type'); 39 | 40 | if (!$shiftsTypeTable->hasColumn('is_weekly')) { 41 | $shiftsTypeTable->addColumn('is_weekly', 'boolean', [ 42 | 'notnull' => false, 43 | 'default' => false, 44 | ]); 45 | } 46 | return $schema; 47 | } 48 | 49 | /** 50 | * @param IOutput $output 51 | * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` 52 | * @param array $options 53 | */ 54 | public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/Migration/Version183000Date20211117121300.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * @author Fabian Kirchesch 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace OCA\Shifts\Migration; 11 | 12 | use Closure; 13 | use OCP\DB\ISchemaWrapper; 14 | use OCP\Migration\IOutput; 15 | use OCP\Migration\SimpleMigrationStep; 16 | 17 | /** 18 | * Auto-generated migration step: Please modify to your needs! 19 | */ 20 | class Version183000Date20211117121300 extends SimpleMigrationStep { 21 | 22 | /** 23 | * @param IOutput $output 24 | * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` 25 | * @param array $options 26 | */ 27 | public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { 28 | } 29 | 30 | /** 31 | * @param IOutput $output 32 | * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` 33 | * @param array $options 34 | * @return null|ISchemaWrapper 35 | */ 36 | public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { 37 | $schema = $schemaClosure(); 38 | 39 | if (!$schema->hasTable('shifts_cal_changes')) { 40 | $table = $schema->createTable('shifts_cal_changes'); 41 | $table->addColumn('id', 'integer', [ 42 | 'autoincrement' => true, 43 | 'notnull' => true, 44 | ]); 45 | $table->addColumn('shift_id', 'integer', [ 46 | 'notnull' => true, 47 | ]); 48 | $table->addColumn('shift_type_id', 'integer', [ 49 | 'notnull' => true, 50 | ]); 51 | $table->addColumn('shift_date', 'string', [ 52 | 'notnull' => true, 53 | 'length' => 200 54 | ]); 55 | $table->addColumn('old_user_id', 'string', [ 56 | 'notnull' => true, 57 | 'length' => 200 58 | ]); 59 | $table->addColumn('new_user_id', 'string', [ 60 | 'notnull' => true, 61 | 'length' => 200 62 | ]); 63 | $table->addColumn('action', 'string', [ 64 | 'notnull' => true, 65 | 'length' => 200 66 | ]); 67 | $table->addColumn('date_changed', 'string', [ 68 | 'notnull' => true, 69 | 'length' => 200 70 | ]); 71 | $table->addColumn('admin_id', 'string', [ 72 | 'notnull' => true, 73 | 'length' => 200 74 | ]); 75 | $table->addColumn('is_done', 'boolean', [ 76 | 'notnull' => false, 77 | 'default' => false, 78 | ]); 79 | 80 | $table->setPrimaryKey(['id']); 81 | } 82 | return $schema; 83 | } 84 | 85 | /** 86 | * @param IOutput $output 87 | * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` 88 | * @param array $options 89 | */ 90 | public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/Migration/Version184000Date20211215101000.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * @author Fabian Kirchesch 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace OCA\Shifts\Migration; 11 | 12 | use Closure; 13 | use OCP\DB\ISchemaWrapper; 14 | use OCP\Migration\IOutput; 15 | use OCP\Migration\SimpleMigrationStep; 16 | 17 | /** 18 | * Auto-generated migration step: Please modify to your needs! 19 | */ 20 | class Version184000Date20211215101000 extends SimpleMigrationStep { 21 | 22 | /** 23 | * @param IOutput $output 24 | * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` 25 | * @param array $options 26 | */ 27 | public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { 28 | } 29 | 30 | /** 31 | * @param IOutput $output 32 | * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` 33 | * @param array $options 34 | * @return null|ISchemaWrapper 35 | */ 36 | public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { 37 | $schema = $schemaClosure(); 38 | $shiftsTypeTable = $schema->getTable('shifts_type'); 39 | 40 | if (!$shiftsTypeTable->hasColumn('deleted')) { 41 | $shiftsTypeTable->addColumn('deleted', 'boolean', [ 42 | 'notnull' => false, 43 | 'default' => false, 44 | ]); 45 | 46 | } 47 | return $schema; 48 | } 49 | 50 | /** 51 | * @param IOutput $output 52 | * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` 53 | * @param array $options 54 | */ 55 | public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/Service/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * @author Kevin Küchler 6 | */ 7 | 8 | namespace OCA\Shifts\Service; 9 | 10 | use Exception; 11 | 12 | class InvalidArgumentException extends Exception{} 13 | -------------------------------------------------------------------------------- /lib/Service/NotFoundException.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * @author Fabian Kirchesch 6 | */ 7 | 8 | namespace OCA\Shifts\Service; 9 | 10 | 11 | class NotFoundException extends ServiceException {} 12 | -------------------------------------------------------------------------------- /lib/Service/PermissionException.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * @author Kevin Küchler 6 | */ 7 | 8 | namespace OCA\Shifts\Service; 9 | 10 | use Exception; 11 | 12 | class PermissionException extends Exception{} 13 | -------------------------------------------------------------------------------- /lib/Service/PermissionService.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * @author Kevin Küchler 6 | */ 7 | 8 | namespace OCA\Shifts\Service; 9 | 10 | use OCP\IGroupManager; 11 | use OCA\Shifts\Settings\Settings; 12 | 13 | class PermissionService { 14 | 15 | /** @var IGroupManager */ 16 | private $groupManager; 17 | 18 | /** @var Settings */ 19 | private $settings; 20 | 21 | /** @var UserID */ 22 | private $userId; 23 | 24 | public function __construct(IGroupManager $groupManager, Settings $settings, $userId){ 25 | $this->groupManager = $groupManager; 26 | $this->settings = $settings; 27 | $this->userId = $userId; 28 | } 29 | 30 | public function getUserId(): string { 31 | return $this->userId; 32 | } 33 | 34 | public function isAdmin(string $userId): bool { 35 | return $this->groupManager->isInGroup($userId, $this->settings->getAdminGroup()); 36 | } 37 | 38 | public function isRequestingUserAdmin(): bool { 39 | return $this->groupManager->isInGroup($this->userId, $this->settings->getAdminGroup()); 40 | } 41 | 42 | public function isRequestingUser(string $userId): bool { 43 | return $this->userId == $userId; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/Service/ServiceException.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * @author Fabian Kirchesch 6 | */ 7 | 8 | namespace OCA\Shifts\Service; 9 | 10 | use Exception; 11 | 12 | class ServiceException extends Exception{} 13 | -------------------------------------------------------------------------------- /lib/Service/ShiftService.php: -------------------------------------------------------------------------------- 1 | 4 | * @copyright Copyright (c) 2023. Kevin Küchler 5 | * 6 | * @author Fabian Kirchesch 7 | * @author Kevin Küchler 8 | */ 9 | 10 | namespace OCA\Shifts\Service; 11 | 12 | use DateTime; 13 | use DateInterval; 14 | use DatePeriod; 15 | use Exception; 16 | 17 | use OCA\Shifts\Db\ShiftsCalendarChangeMapper; 18 | use OCP\AppFramework\Db\DoesNotExistException; 19 | use OCP\AppFramework\Db\MultipleObjectsReturnedException; 20 | 21 | use OCA\Shifts\Db\Shift; 22 | use OCA\Shifts\Db\ShiftMapper; 23 | use OCA\Shifts\Db\ShiftsTypeMapper; 24 | use phpDocumentor\Reflection\Types\Boolean; 25 | 26 | class ShiftService { 27 | 28 | /** @var ShiftMapper */ 29 | private $mapper; 30 | 31 | /** @var ShiftsTypeMapper */ 32 | private $typeMapper; 33 | 34 | public function __construct(ShiftMapper $mapper, ShiftsTypeMapper $typeMapper){ 35 | $this->mapper = $mapper; 36 | $this->typeMapper = $typeMapper; 37 | } 38 | 39 | public function findAll(): array 40 | { 41 | return $this->mapper->findAll(); 42 | } 43 | 44 | public function findById(string $userId): array 45 | { 46 | try{ 47 | return $this->mapper->findById($userId); 48 | } catch(Exception $e){ 49 | $this->handleException($e); 50 | } 51 | } 52 | 53 | public function findByTimeRange(string $start, string $end) : array 54 | { 55 | try { 56 | return $this->mapper->findByTimeRange($start, $end); 57 | } catch(Exception $e) { 58 | $this->handleException($e); 59 | } 60 | } 61 | 62 | private function handleException($e){ 63 | if($e instanceof DoesNotExistException || 64 | $e instanceof MultipleObjectsReturnedException){ 65 | throw new NotFoundException($e->getMessage()); 66 | }else { 67 | throw $e; 68 | } 69 | } 70 | 71 | public function find(int $id): Shift { 72 | try{ 73 | return $this->mapper->find($id); 74 | } catch(Exception $e){ 75 | $this->handleException($e); 76 | } 77 | } 78 | 79 | public function create(string $userId, int $shiftTypeId, string $date){ 80 | $shift = new Shift(); 81 | $shift->setUserId($userId); 82 | $shift->setShiftTypeId($shiftTypeId); 83 | $shift->setDate($date); 84 | return $this->mapper->insert($shift); 85 | } 86 | 87 | public function update(int $id, string $userId, string $shiftTypeId, string $date){ 88 | try{ 89 | $shift = $this->mapper->find($id); 90 | $shift->setUserId($userId); 91 | $shift->setShiftTypeId($shiftTypeId); 92 | $shift->setDate($date); 93 | return $this->mapper->update($shift); 94 | } catch(Exception $e){ 95 | $this->handleException($e); 96 | } 97 | return null; 98 | } 99 | 100 | public function delete(int $id){ 101 | try{ 102 | $shift = $this->mapper->find($id); 103 | $this->mapper->delete($shift); 104 | return $shift; 105 | } catch(Exception $e){ 106 | $this->handleException($e); 107 | } 108 | return null; 109 | } 110 | 111 | public function triggerUnassignedShifts(): bool { 112 | try{ 113 | $start = new DateTime(); 114 | $start->modify('this monday'); 115 | $interval = DateInterval::createFromDateString('1day'); 116 | $rules = $this->typeMapper->findAllRules(); 117 | 118 | $end = date_create(date('Y-m-d')); 119 | $end = date_add($end, date_interval_create_from_date_string('365 days')); 120 | $period = new DatePeriod($start, $interval, $end); 121 | 122 | foreach ($period as $dt) { 123 | foreach ($rules as $rule) { 124 | $shiftsType = $rule->jsonSerialize(); 125 | $shifts = $this->mapper->findByDateAndType($dt->format('Y-m-d'), $shiftsType['id']); 126 | $dayOfWeek = date('w', $dt->getTimestamp()); 127 | 128 | switch ($dayOfWeek) { 129 | case '0': 130 | $ruleString = $shiftsType['soRule']; 131 | break; 132 | case '1': 133 | $ruleString = $shiftsType['moRule']; 134 | break; 135 | case '2': 136 | $ruleString = $shiftsType['tuRule']; 137 | break; 138 | case '3': 139 | $ruleString = $shiftsType['weRule']; 140 | break; 141 | case '4': 142 | $ruleString = $shiftsType['thRule']; 143 | break; 144 | case '5': 145 | $ruleString = $shiftsType['frRule']; 146 | break; 147 | case '6': 148 | $ruleString = $shiftsType['saRule']; 149 | break; 150 | default: 151 | $ruleString = 1; 152 | }; 153 | if (intval($ruleString) > intval(count($shifts))) { 154 | for ($x = 0; $x < (intval($ruleString) - intval(count($shifts))); $x++) { 155 | $shift = new Shift(); 156 | $shift->setUserId('-1'); 157 | $shift->setShiftTypeId($shiftsType['id']); 158 | $shift->setDate($dt->format('Y-m-d')); 159 | $this->mapper->insert($shift); 160 | } 161 | } 162 | } 163 | } 164 | return true; 165 | 166 | } catch(Exception $e){ 167 | $this->handleException($e); 168 | } 169 | return false; 170 | } 171 | 172 | public function swap(int $oldId, int $newId) { 173 | try { 174 | $oldShift = $this->mapper->find($oldId); 175 | $newShift = $this->mapper->find($newId); 176 | $this->mapper->swapShifts($oldId, $oldShift->getUserId(), $oldShift->getDate(), $newId, $newShift->getUserId(), $newShift->getDate()); 177 | } catch(Exception $e) { 178 | $this->handleException($e); 179 | } 180 | } 181 | 182 | public function cede(int $shiftId, string $newAnalystId) { 183 | try { 184 | $shift = $this->mapper->find($shiftId); 185 | $shift->setUserId($newAnalystId); 186 | return $this->mapper->update($shift); 187 | } catch(Exception $e) { 188 | $this->handleException($e); 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /lib/Service/ShiftsCalendarChangeService.php: -------------------------------------------------------------------------------- 1 | 4 | * @copyright Copyright (c) 2023. Kevin Küchler 5 | * 6 | * @author Fabian Kirchesch 7 | * @author Kevin Küchler 8 | */ 9 | 10 | namespace OCA\Shifts\Service; 11 | 12 | 13 | use OCA\Shifts\Db\ShiftsCalendarChangeMapper; 14 | use OCA\Shifts\Db\ShiftsCalendarChange; 15 | 16 | use OCP\AppFramework\Db\DoesNotExistException; 17 | use OCP\AppFramework\Db\MultipleObjectsReturnedException; 18 | use OCP\DB\Exception; 19 | use Psr\Log\LoggerInterface; 20 | use Sabre\DAV\Exception\Forbidden; 21 | 22 | class ShiftsCalendarChangeService { 23 | /** @var LoggerInterface */ 24 | private LoggerInterface $logger; 25 | 26 | /** @var ShiftsCalendarChangeMapper */ 27 | private $mapper; 28 | 29 | public function __construct(LoggerInterface $logger, ShiftsCalendarChangeMapper $mapper) { 30 | $this->logger = $logger; 31 | 32 | $this->mapper = $mapper; 33 | } 34 | 35 | public function find(int $id) { 36 | try{ 37 | return $this->mapper->find($id); 38 | } catch(Exception $e){ 39 | $this->handleException($e); 40 | } 41 | } 42 | 43 | /** 44 | * Find all ShiftsChanges 45 | * @return ShiftsCalendarChange[] 46 | */ 47 | public function findAll(): array { 48 | return $this->mapper->findAll(); 49 | } 50 | 51 | /** 52 | * Find all ShiftsChanges 53 | * 54 | * @return ShiftsCalendarChange[] 55 | * @throws Exception 56 | */ 57 | public function findAllOpen(): array { 58 | return $this->mapper->findAllOpen(); 59 | } 60 | 61 | /** 62 | * @throws PermissionException 63 | * @throws NotFoundException 64 | */ 65 | private function handleException($e) { 66 | if($e instanceof DoesNotExistException || $e instanceof MultipleObjectsReturnedException) { 67 | throw new NotFoundException($e->getMessage()); 68 | } else if($e instanceof Forbidden) { 69 | throw new PermissionException($e->getMessage()); 70 | } else { 71 | throw $e; 72 | } 73 | } 74 | 75 | /** 76 | * @throws Exception 77 | * @throws NotFoundException 78 | * @throws PermissionException 79 | */ 80 | public function create(int $shiftId, int $shiftTypeId, string $shiftDate, string $oldUserId, string $newUserId, string $action, string $dateChanged, string $adminId, bool $isDone){ 81 | try { 82 | $shiftsCalendarChange = $this->mapper->findByShiftIdAndType($shiftId, $action); 83 | return $this->update($shiftsCalendarChange->getId(), $shiftId, $shiftTypeId, $shiftDate, $shiftsCalendarChange->getOldUserId(), $newUserId, $shiftsCalendarChange->getAction(), $dateChanged, $adminId, $isDone); 84 | } catch (DoesNotExistException $exception) { 85 | $shiftsCalendarChange = new ShiftsCalendarChange(); 86 | $shiftsCalendarChange->setShiftId($shiftId); 87 | $shiftsCalendarChange->setShiftTypeId($shiftTypeId); 88 | $shiftsCalendarChange->setShiftDate($shiftDate); 89 | $shiftsCalendarChange->setOldUserId($oldUserId); 90 | $shiftsCalendarChange->setNewUserId($newUserId); 91 | $shiftsCalendarChange->setAction($action); 92 | $shiftsCalendarChange->setDateChanged($dateChanged); 93 | $shiftsCalendarChange->setAdminId($adminId); 94 | $shiftsCalendarChange->setIsDone($isDone); 95 | return $this->mapper->insert($shiftsCalendarChange); 96 | } catch (Exception $e) { 97 | $this->handleException($e); 98 | } 99 | } 100 | 101 | public function update(int $id, int $shiftId, int $shiftTypeId, string $shiftDate, string $oldUserId, string $newUserId, string $action, string $dateChanged, string $adminId, bool $isDone) { 102 | try { 103 | $shiftsCalendarChange = $this->mapper->find($id); 104 | 105 | if($oldUserId == $newUserId) { 106 | return $this->mapper->delete($shiftsCalendarChange); 107 | } else { 108 | $shiftsCalendarChange->setShiftId($shiftId); 109 | $shiftsCalendarChange->setShiftTypeId($shiftTypeId); 110 | $shiftsCalendarChange->setShiftDate($shiftDate); 111 | $shiftsCalendarChange->setOldUserId($oldUserId); 112 | $shiftsCalendarChange->setNewUserId($newUserId); 113 | $shiftsCalendarChange->setAction($action); 114 | $shiftsCalendarChange->setDateChanged($dateChanged); 115 | $shiftsCalendarChange->setAdminId($adminId); 116 | $shiftsCalendarChange->setIsDone($isDone); 117 | return $this->mapper->update($shiftsCalendarChange); 118 | } 119 | } catch(Exception $e) { 120 | $this->handleException($e); 121 | } 122 | return null; 123 | } 124 | 125 | /** 126 | * @throws NotFoundException 127 | */ 128 | public function updateDone(int $id, bool $isDone) { 129 | try { 130 | $shiftsCalendarChange = $this->mapper->find($id); 131 | $shiftsCalendarChange->setIsDone($isDone); 132 | return $this->mapper->update($shiftsCalendarChange); 133 | } catch(Exception $e) { 134 | $this->handleException($e); 135 | } 136 | } 137 | 138 | public function delete(int $id) { 139 | try { 140 | $shift = $this->mapper->find($id); 141 | $this->mapper->delete($shift); 142 | return $shift; 143 | } catch(Exception $e) { 144 | $this->handleException($e); 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /lib/Settings/AdminSection.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * @author Fabian Kirchesch 6 | */ 7 | 8 | namespace OCA\Shifts\Settings; 9 | 10 | use OCP\IURLGenerator; 11 | use OCP\Settings\IIconSection; 12 | 13 | 14 | class AdminSection implements IIconSection { 15 | 16 | /** @var IURLGenerator */ 17 | private $urlGenerator; 18 | 19 | /** 20 | * @param IURLGenerator $urlGenerator 21 | */ 22 | public function __construct(IURLGenerator $urlGenerator) { 23 | $this->urlGenerator = $urlGenerator; 24 | } 25 | /** 26 | * ID 27 | * 28 | * @return string 29 | */ 30 | public function getID(): string { 31 | return 'shifts'; 32 | } 33 | 34 | /** 35 | * Icon 36 | * 37 | * @return string 38 | */ 39 | public function getIcon(): string { 40 | return $this->urlGenerator->imagePath("shifts", "app-dark.svg"); 41 | } 42 | 43 | /** 44 | * Get Name 45 | * 46 | * @return string 47 | */ 48 | public function getName(): string { 49 | return 'Shifts'; 50 | } 51 | 52 | /** 53 | * Get section ID 54 | * 55 | * @return string 56 | */ 57 | public function getSection(): string { 58 | return "shifts"; 59 | } 60 | 61 | /** 62 | * Get Priority for Settings Order 63 | * 64 | * @return int 65 | */ 66 | public function getPriority(): int { 67 | return 50; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/Settings/AdminSettings.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * @author Fabian Kirchesch 6 | */ 7 | 8 | namespace OCA\Shifts\Settings; 9 | 10 | use OCA\Shifts\Controller\SettingsController; 11 | use OCA\Shifts\AppInfo\Application; 12 | use OCP\AppFramework\Http\TemplateResponse; 13 | use OCP\BackgroundJob\IJobList; 14 | use OCP\IConfig; 15 | use OCP\IDateTimeFormatter; 16 | use OCP\IL10N; 17 | use OCP\Settings\ISettings; 18 | 19 | 20 | class AdminSettings implements ISettings { 21 | 22 | /** 23 | * Admin constructor 24 | */ 25 | public function __construct() { 26 | 27 | } 28 | 29 | /** 30 | * @return TemplateResponse 31 | */ 32 | public function getForm() : TemplateResponse{ 33 | $app = \OC::$server->query(Application::class); 34 | $container = $app->getContainer(); 35 | return $container->query(SettingsController::class)->index(); 36 | } 37 | 38 | /** 39 | * Get section ID 40 | * 41 | * @return string 42 | */ 43 | public function getSection(): string { 44 | return "shifts"; 45 | } 46 | 47 | /** 48 | * Get Priority for Settings Order 49 | * 50 | * @return int 51 | */ 52 | public function getPriority(): int { 53 | return 50; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shifts", 3 | "description": "Shifts Organisation", 4 | "version": "1.9.10", 5 | "author": "CSOC ", 6 | "license": "agpl", 7 | "private": true, 8 | "scripts": { 9 | "build": "NODE_ENV=production webpack --progress --config webpack.js", 10 | "dev": "NODE_ENV=development webpack --progress --watch --config webpack.js", 11 | "watch": "NODE_ENV=development webpack --progress --watch --config webpack.js", 12 | "lint": "eslint --ext .js,.vue --ignore-path .gitignoresrc", 13 | "lint:fix": "eslint --ext .js,.vue --ignore-path .gitignore src --fix", 14 | "stylelint": "stylelint css/*.css css/*.scss src/**/*.scss src/**/*.vue", 15 | "stylelint:fix": "stylelint css/*.css css/*.scss src/**/*.scss src/**/*.vue --fix" 16 | }, 17 | "dependencies": { 18 | "@mdi/font": "^7.4.47", 19 | "@mdi/js": "^7.4.47", 20 | "@nextcloud/auth": "^2.4.0", 21 | "@nextcloud/axios": "^2.5.0", 22 | "@nextcloud/calendar-js": "^8.0.0", 23 | "@nextcloud/cdav-library": "^1.5.1", 24 | "@nextcloud/dialogs": "^4.2.7", 25 | "@nextcloud/initial-state": "^2.2.0", 26 | "@nextcloud/l10n": "^3.1.0", 27 | "@nextcloud/logger": "^3.0.2", 28 | "@nextcloud/router": "^3.0.1", 29 | "@nextcloud/vue": "^7.12.8", 30 | "autosize": "^6.0.1", 31 | "core-js": "^3.38.0", 32 | "dayjs": "^1.11.12", 33 | "material-design-icons-iconfont": "^6.7.0", 34 | "moment": "^2.30.1", 35 | "npm-ls": "^1.0.0", 36 | "v-tooltip": "^2.1.3", 37 | "vue": "^2.7.16", 38 | "vue-click-outside": "^1.1.0", 39 | "vue-clipboard2": "^0.3.3", 40 | "vue-material-design-icons": "^5.3.0", 41 | "vue-router": "^3.6.5", 42 | "vue-shortkey": "^3.1.7", 43 | "vue2-timepicker": "^1.1.6", 44 | "vuejs-weekpicker": "^0.3.2", 45 | "vuex": "^3.6.2", 46 | "vuex-router-sync": "^5.0.0", 47 | "webpack-merge": "^6.0.1" 48 | }, 49 | "devDependencies": { 50 | "@babel/core": "^7.25.2", 51 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 52 | "@babel/preset-env": "^7.25.3", 53 | "@nextcloud/browserslist-config": "^3.0.1", 54 | "@nextcloud/eslint-config": "^8.4.1", 55 | "@nextcloud/eslint-plugin": "^2.2.1", 56 | "@nextcloud/webpack-vue-config": "^6.0.1", 57 | "babel-loader": "^9.1.3", 58 | "deepmerge": "^4.3.1", 59 | "eslint-import-resolver-webpack": "^0.13.8", 60 | "eslint-plugin-import": "^2.29.1", 61 | "eslint-plugin-node": "^11.1.0", 62 | "eslint-plugin-standard": "^5.0.0", 63 | "eslint-webpack-plugin": "^4.2.0", 64 | "file-loader": "^6.2.0", 65 | "sass": "^1.77.8", 66 | "stylelint-config-recommended-scss": "^14.1.0", 67 | "stylelint-scss": "^6.5.0", 68 | "url-loader": "^4.1.1", 69 | "vue-template-compiler": "^2.7.16", 70 | "webpack-cli": "^5.1.4" 71 | }, 72 | "browserslist": [ 73 | "extends @nextcloud/browserslist-config" 74 | ], 75 | "engines": { 76 | "node": "^16.0.0", 77 | "npm": "^7.0.0 || ^8.0.0" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /settings/settings.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright Copyright (c) 2021. Fabian Kirchesch 3 | * @copyright Copyright (c) 2023. Kevin Küchler 4 | * 5 | * @author Fabian Kirchesch 6 | * @author Kevin Küchler 7 | */ 8 | 9 | import 'core-js/stable' 10 | 11 | import Vue from 'vue' 12 | import store from './store' 13 | import Settings from './Settings' 14 | import ClickOutside from 'vue-click-outside' 15 | import VueClipboard from 'vue-clipboard2' 16 | import { VTooltip, VPopover } from 'v-tooltip' 17 | import VueShortKey from 'vue-shortkey' 18 | import { translate, translatePlural } from '@nextcloud/l10n' 19 | 20 | // to allow clicking autoside of popover 21 | Vue.directive('ClickOutside', ClickOutside) 22 | 23 | // adds v-popover Component to allow usage in App 24 | Vue.component('VPopover', VPopover) 25 | 26 | // changes appearence of VTooltip 27 | Vue.use(VTooltip, { 28 | popover: { 29 | defaultWrapperClass: 'popover__wrapper', 30 | defaultBaseClass: 'event-popover popover', 31 | defaultInnerClass: 'popover__inner', 32 | defaultArrowClass: 'popover__arrow', 33 | }, 34 | }) 35 | Vue.use(VueClipboard) 36 | Vue.use(VueShortKey, { prevent: ['input', 'textarea'] }) 37 | 38 | Vue.mixin({ methods: { t, n } }) 39 | 40 | // sync(store, router) 41 | 42 | // Translation Compatibility 43 | Vue.prototype.$t = translate 44 | Vue.prototype.$n = translatePlural 45 | 46 | // Translation 47 | Vue.prototype.t = translate 48 | Vue.prototype.n = translatePlural 49 | 50 | export default new Vue({ 51 | el: '#admin_settings', 52 | store, 53 | render: h => h(Settings), 54 | }) 55 | -------------------------------------------------------------------------------- /settings/store/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright Copyright (c) 2021. Fabian Kirchesch 3 | * @copyright Copyright (c) 2023. Kevin Küchler 4 | * 5 | * @author Fabian Kirchesch 6 | * @author Kevin Küchler 7 | */ 8 | 9 | import Vue from 'vue' 10 | import Vuex from 'vuex' 11 | 12 | import settings from './settings' 13 | 14 | Vue.use(Vuex) 15 | 16 | const store = new Vuex.Store({ 17 | modules: { 18 | settings, 19 | }, 20 | }) 21 | 22 | export default store 23 | -------------------------------------------------------------------------------- /settings/store/settings.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright Copyright (c) 2021. Fabian Kirchesch 3 | * 4 | * @author Fabian Kirchesch 5 | */ 6 | 7 | import axios from '@nextcloud/axios' 8 | import { generateUrl } from '@nextcloud/router' 9 | import Vue from 'vue' 10 | 11 | const state = { 12 | calendarName: '', 13 | addUserCalendarEvent: true, 14 | analystGroup: '', 15 | shiftAdminGroup: '', 16 | 17 | organizerName: '', 18 | organizerEmail: '', 19 | timezone: '', 20 | 21 | skillGroups: [], 22 | 23 | shiftChangeSameShiftType: false, 24 | 25 | settingsFetched: false, 26 | } 27 | 28 | const getters = { 29 | getCalendarName(state) { 30 | return state.calendarName 31 | }, 32 | getAddUserCalendarEvent(state) { 33 | return state.addUserCalendarEvent 34 | }, 35 | getAnalystGroup(state) { 36 | return state.analystGroup 37 | }, 38 | getShiftAdminGroup(state) { 39 | return state.shiftAdminGroup 40 | }, 41 | 42 | getOrganizerName(state) { 43 | return state.organizerName 44 | }, 45 | getOrganizerEmail(state) { 46 | return state.organizerEmail 47 | }, 48 | getShiftTimezone(state) { 49 | return state.timezone 50 | }, 51 | 52 | getSkillGroups(state) { 53 | return state.skillGroups 54 | }, 55 | 56 | getShiftChangeSameType(state) { 57 | return state.shiftChangeSameShiftType 58 | }, 59 | 60 | getSettingsFetched(state) { 61 | return state.settingsFetched 62 | }, 63 | } 64 | 65 | const actions = { 66 | fetchSettings(store) { 67 | return new Promise((resolve, reject) => { 68 | axios.get(generateUrl('/apps/shifts/settings')).then((result) => { 69 | if (result.status === 200 && result.data) { 70 | const settings = result.data 71 | console.debug('SETTINGS:', settings) 72 | 73 | store.commit('updateCalendarName', settings.calendarName) 74 | store.commit('updateAddUserCalendarEvent', settings.addUserCalendarEvent) 75 | store.commit('updateAnalystGroup', settings.shiftWorkerGroup) 76 | store.commit('updateShiftAdminGroup', settings.adminGroup) 77 | store.commit('updateOrganizerName', settings.organizerName) 78 | store.commit('updateOrganizerEmail', settings.organizerEmail) 79 | store.commit('updateShiftTimezone', settings.timezone) 80 | store.commit('updateSkillGroups', settings.skillGroups) 81 | 82 | if (typeof settings.shiftChangeSameShiftType === 'string') { 83 | settings.shiftChangeSameShiftType = settings.shiftChangeSameShiftType === '1' 84 | } 85 | store.commit('updateShiftChangeSameType', settings.shiftChangeSameShiftType) 86 | 87 | store.commit('updateSettingsFetched', true) 88 | 89 | resolve() 90 | } else { 91 | reject(new Error('Failed to load settings: ' + result.statusText)) 92 | } 93 | }).catch((e) => { 94 | reject(e) 95 | }) 96 | }) 97 | }, 98 | 99 | saveSettings(store) { 100 | return new Promise((resolve, reject) => { 101 | console.debug('SAMESHIFT:', store.state.shiftChangeSameShiftType) 102 | axios.put(generateUrl('/apps/shifts/settings'), { 103 | calendarName: store.state.calendarName, 104 | addUserCalendarEvent: store.state.addUserCalendarEvent, 105 | shiftWorkerGroup: store.state.analystGroup, 106 | adminGroup: store.state.shiftAdminGroup, 107 | organizerName: store.state.organizerName, 108 | organizerEmail: store.state.organizerEmail, 109 | timezone: store.state.timezone, 110 | skillGroups: store.state.skillGroups, 111 | shiftChangeSameShiftType: store.state.shiftChangeSameShiftType ? '1' : '0', 112 | }).then(() => { 113 | resolve() 114 | }).catch((e) => { 115 | console.error('Could not update settings:', e) 116 | reject(e) 117 | }) 118 | }) 119 | }, 120 | 121 | updateCalendarName(store, calendarName) { 122 | store.commit('updateCalendarName', calendarName) 123 | }, 124 | updateAddUserCalendarEvent(store, addEvent) { 125 | store.commit('updateAddUserCalendarEvent', addEvent) 126 | }, 127 | updateAnalystGroup(store, analystGroup) { 128 | store.commit('updateAnalystGroup', analystGroup) 129 | }, 130 | updateShiftAdminGroup(store, shiftAdminGroup) { 131 | store.commit('updateShiftAdminGroup', shiftAdminGroup) 132 | }, 133 | updateShiftTimezone(store, timezone) { 134 | store.commit('updateShiftTimezone', timezone) 135 | }, 136 | 137 | updateOrganizerName(store, organizerName) { 138 | store.commit('updateOrganizerName', organizerName) 139 | }, 140 | updateOrganizerEmail(store, organizerEmail) { 141 | store.commit('updateOrganizerEmail', organizerEmail) 142 | }, 143 | 144 | updateShiftChangeSameType(store, shiftChangeSameType) { 145 | store.commit('updateShiftChangeSameType', shiftChangeSameType) 146 | }, 147 | 148 | addEmptyRowToSkillGroups(store) { 149 | const len = store.state.skillGroups.length 150 | store.commit('addEmptyRowToSkillGroups', parseInt(store.state.skillGroups[len - 1].id) + 1) 151 | }, 152 | removeSkillGroup(store, group) { 153 | let index = -1 154 | for (const i in state.skillGroups) { 155 | if (state.skillGroups[i].id === group.id) { 156 | index = i 157 | } 158 | } 159 | 160 | if (index > -1) { 161 | store.commit('removeSkillGroup', index) 162 | } 163 | } 164 | } 165 | 166 | const mutations = { 167 | updateCalendarName(state, calendarName) { 168 | Vue.set(state, 'calendarName', calendarName) 169 | }, 170 | updateAddUserCalendarEvent(state, addEvent) { 171 | Vue.set(state, 'addUserCalendarEvent', addEvent) 172 | }, 173 | updateAnalystGroup(state, analystGroup) { 174 | Vue.set(state, 'analystGroup', analystGroup) 175 | }, 176 | updateShiftAdminGroup(state, shiftAdminGroup) { 177 | Vue.set(state, 'shiftAdminGroup', shiftAdminGroup) 178 | }, 179 | 180 | updateOrganizerName(state, organizerName) { 181 | Vue.set(state, 'organizerName', organizerName) 182 | }, 183 | updateOrganizerEmail(state, organizerEmail) { 184 | Vue.set(state, 'organizerEmail', organizerEmail) 185 | }, 186 | updateShiftTimezone(state, timezone) { 187 | Vue.set(state, 'timezone', timezone) 188 | }, 189 | 190 | updateSkillGroups(state, skillGroups) { 191 | Vue.set(state, 'skillGroups', skillGroups) 192 | }, 193 | 194 | updateShiftChangeSameType(store, shiftChangeSameType) { 195 | Vue.set(state, 'shiftChangeSameShiftType', shiftChangeSameType) 196 | }, 197 | 198 | updateSettingsFetched(state, fetched) { 199 | Vue.set(state, 'settingsFetched', fetched) 200 | }, 201 | 202 | addEmptyRowToSkillGroups(state, newId) { 203 | state.skillGroups.push({ 204 | id: newId, 205 | name: '', 206 | }) 207 | }, 208 | removeSkillGroup(state, index) { 209 | state.skillGroups.splice(index, 1) 210 | } 211 | } 212 | 213 | export default { state, mutations, getters, actions } 214 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Loading... 22 | 23 | 24 | 25 | 26 | 27 | 28 | 45 | 46 | 49 | -------------------------------------------------------------------------------- /src/components/Archive/ArchiveContent.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {{ t('shifts','Analyst') }} 15 | 16 | 17 | 20 | {{ header.name }} 21 | 22 | 23 | 24 | 25 | 26 | 29 | {{ analyst.name }} 30 | 33 | {{ getShiftsForAnalystByShiftType(analyst.uid, header.id) }} 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 67 | 68 | 122 | -------------------------------------------------------------------------------- /src/components/Archive/ArchiveTopBar.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 12 {{ t('shifts','Months') }} 20 | 21 | 22 | 26 | 9 {{ t('shifts','Months') }} 27 | 28 | 29 | 33 | 6 {{ t('shifts','Months') }} 34 | 35 | 36 | 40 | 3 {{ t('shifts','Months') }} 41 | 42 | 43 | 47 | {{ t('shifts','Last Month') }} 48 | 49 | 50 | 51 | 52 | 56 | 57 | 58 | 59 | 63 | 64 | 65 | 66 | 67 | 68 | 115 | -------------------------------------------------------------------------------- /src/components/Calendar/CalendarTopBar.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | Heute 19 | 20 | 22 | 23 | 24 | 25 | 26 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 46 | 47 | 48 | 49 | 105 | -------------------------------------------------------------------------------- /src/components/Navigation/AppNavigation.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | 15 | 19 | 20 | 24 | 25 | 30 | 31 | 36 | 37 | 38 | Version: 1.9.10 39 | 40 | 41 | 42 | 43 | 60 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright Copyright (c) 2021. Fabian Kirchesch 3 | * @copyright Copyright (c) 2023. Kevin Küchler 4 | * 5 | * @author Fabian Kirchesch 6 | * @author Kevin Küchler 7 | */ 8 | 9 | import 'core-js/stable' 10 | 11 | import Vue from 'vue' 12 | import App from './App' 13 | import store from './store' 14 | import router from './router' 15 | import { sync } from 'vuex-router-sync' 16 | import ClickOutside from 'vue-click-outside' 17 | import VueClipboard from 'vue-clipboard2' 18 | import { VTooltip, VPopover } from 'v-tooltip' 19 | import VueShortKey from 'vue-shortkey' 20 | import { translate, translatePlural } from '@nextcloud/l10n' 21 | import dayjs from 'dayjs' 22 | import dayOfYear from 'dayjs/plugin/dayOfYear' 23 | import weekOfYear from 'dayjs/plugin/weekOfYear' 24 | import isSameOrAfter from 'dayjs/plugin/isSameOrAfter' 25 | 26 | // to allow clicking autoside of popover 27 | Vue.directive('ClickOutside', ClickOutside) 28 | 29 | // adds v-popover Component to allow usage in App 30 | Vue.component('VPopover', VPopover) 31 | 32 | // changes appearence of VTooltip 33 | Vue.use(VTooltip, { 34 | popover: { 35 | defaultWrapperClass: 'popover__wrapper', 36 | defaultBaseClass: 'event-popover popover', 37 | defaultInnerClass: 'popover__inner', 38 | defaultArrowClass: 'popover__arrow', 39 | }, 40 | }) 41 | Vue.use(VueClipboard) 42 | Vue.use(VueShortKey, { prevent: ['input', 'textarea'] }) 43 | 44 | Vue.mixin({ methods: { t, n } }) 45 | 46 | sync(store, router) 47 | 48 | // Translation Compatibility 49 | Vue.prototype.$t = translate 50 | Vue.prototype.$n = translatePlural 51 | 52 | // Translation 53 | Vue.prototype.t = translate 54 | Vue.prototype.n = translatePlural 55 | 56 | dayjs.locale('de') 57 | dayjs.extend(dayOfYear) 58 | dayjs.extend(weekOfYear) 59 | dayjs.extend(isSameOrAfter) 60 | 61 | export default new Vue({ 62 | el: '#content', 63 | router, 64 | store, 65 | created() { 66 | this.$store.dispatch('setup') 67 | this.$store.dispatch('fetchSettings') 68 | }, 69 | render: h => h(App), 70 | }) 71 | -------------------------------------------------------------------------------- /src/models/consts.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright Copyright (c) 2019 Georg Ehrke 3 | * 4 | * @author Georg Ehrke 5 | * 6 | * @license GNU AGPL version 3 or any later version 7 | * 8 | * This program is free software: you can redistribute it and/or modify 9 | * it under the terms of the GNU Affero General Public License as 10 | * published by the Free Software Foundation, either version 3 of the 11 | * License, or (at your option) any later version. 12 | * 13 | * This program is distributed in the hope that it will be useful, 14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | * GNU Affero General Public License for more details. 17 | * 18 | * You should have received a copy of the GNU Affero General Public License 19 | * along with this program. If not, see . 20 | */ 21 | const COMPONENT_NAME_EVENT = 'VEVENT' 22 | const COMPONENT_NAME_JOURNAL = 'VJOURNAL' 23 | const COMPONENT_NAME_VTODO = 'VTODO' 24 | 25 | const ITIP_MESSAGE_ADD = 'ADD' 26 | const ITIP_MESSAGE_CANCEL = 'CANCEL' 27 | const ITIP_MESSAGE_COUNTER = 'COUNTER' 28 | const ITIP_MESSAGE_DECLINECOUNTER = 'DECLINECOUNTER' 29 | const ITIP_MESSAGE_PUBLISH = 'PUBLISH' 30 | const ITIP_MESSAGE_REFRESH = 'REFRESH' 31 | const ITIP_MESSAGE_REPLY = 'REPLY' 32 | const ITIP_MESSAGE_REQUEST = 'REQUEST' 33 | 34 | const PRINCIPAL_PREFIX_USER = 'principal:principals/users/' 35 | const PRINCIPAL_PREFIX_GROUP = 'principal:principals/groups/' 36 | const PRINCIPAL_PREFIX_CIRCLE = 'principal:principals/circles/' 37 | const PRINCIPAL_PREFIX_CALENDAR_RESOURCE = 'principal:principals/calendar-resources/' 38 | const PRINCIPAL_PREFIX_CALENDAR_ROOM = 'principal:principals/calendar-rooms/' 39 | 40 | const CALDAV_BIRTHDAY_CALENDAR = 'contact_birthdays' 41 | 42 | const IMPORT_STAGE_DEFAULT = 'default' 43 | const IMPORT_STAGE_IMPORTING = 'importing' 44 | const IMPORT_STAGE_AWAITING_USER_SELECT = 'awaitingUserSelect' 45 | const IMPORT_STAGE_PROCESSING = 'processing' 46 | 47 | export { 48 | COMPONENT_NAME_EVENT, 49 | COMPONENT_NAME_JOURNAL, 50 | COMPONENT_NAME_VTODO, 51 | ITIP_MESSAGE_ADD, 52 | ITIP_MESSAGE_CANCEL, 53 | ITIP_MESSAGE_COUNTER, 54 | ITIP_MESSAGE_DECLINECOUNTER, 55 | ITIP_MESSAGE_PUBLISH, 56 | ITIP_MESSAGE_REFRESH, 57 | ITIP_MESSAGE_REPLY, 58 | ITIP_MESSAGE_REQUEST, 59 | PRINCIPAL_PREFIX_USER, 60 | PRINCIPAL_PREFIX_GROUP, 61 | PRINCIPAL_PREFIX_CIRCLE, 62 | PRINCIPAL_PREFIX_CALENDAR_RESOURCE, 63 | PRINCIPAL_PREFIX_CALENDAR_ROOM, 64 | CALDAV_BIRTHDAY_CALENDAR, 65 | IMPORT_STAGE_DEFAULT, 66 | IMPORT_STAGE_IMPORTING, 67 | IMPORT_STAGE_AWAITING_USER_SELECT, 68 | IMPORT_STAGE_PROCESSING, 69 | } 70 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright Copyright (c) 2021. Fabian Kirchesch 3 | * 4 | * @author Fabian Kirchesch 5 | */ 6 | 7 | import Vue from 'vue' 8 | import Router from 'vue-router' 9 | import { getRootUrl, generateUrl } from '@nextcloud/router' 10 | 11 | import Shifts from './views/Shifts' 12 | import Requests from './views/Requests' 13 | import ShiftsTypes from './views/ShiftsTypes' 14 | import Archive from './views/Archive' 15 | 16 | import store from './store' 17 | 18 | Vue.use(Router) 19 | 20 | const webRootWithIndexPHP = getRootUrl() + '/index.php' 21 | const doesURLContainIndexPHP = window.location.pathname.startsWith(webRootWithIndexPHP) 22 | const base = generateUrl('apps/shifts', {}, { 23 | noRewrite: doesURLContainIndexPHP, 24 | }) 25 | 26 | // Vue Router 27 | const router = new Router({ 28 | mode: 'history', 29 | base, 30 | routes: [ 31 | { 32 | path: '/', 33 | redirect: '/timeline', 34 | }, 35 | { 36 | path: '/timeline', 37 | component: Shifts, 38 | name: 'MainView', 39 | }, 40 | { 41 | path: '/requests', 42 | component: Requests, 43 | name: 'RequestsView', 44 | }, 45 | { 46 | path: '/shiftsTypes', 47 | component: ShiftsTypes, 48 | name: 'ShiftsTypes', 49 | beforeEnter: async (to, from, next) => { 50 | const val = await store.dispatch('requestAdminStatus') 51 | if (val) { 52 | next() 53 | } else { 54 | next('/timeline') 55 | } 56 | } 57 | }, 58 | { 59 | path: '/archive', 60 | component: Archive, 61 | name: 'Archive', 62 | beforeEnter: async (to, from, next) => { 63 | const val = await store.dispatch('requestAdminStatus') 64 | if (val) { 65 | next() 66 | } else { 67 | next('/timeline') 68 | } 69 | } 70 | }, 71 | ], 72 | }) 73 | 74 | export default router 75 | -------------------------------------------------------------------------------- /src/store/archive.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright Copyright (c) 2021. Fabian Kirchesch 3 | * @copyright Copyright (c) 2023. Kevin Küchler 4 | * 5 | * @author Fabian Kirchesch 6 | * @author Kevin Küchler 7 | */ 8 | 9 | import { showError } from '@nextcloud/dialogs' 10 | import axios from '@nextcloud/axios' 11 | import { generateUrl } from '@nextcloud/router' 12 | import Vue from 'vue' 13 | import dayjs from 'dayjs' 14 | 15 | const state = { 16 | timeRange: 6, 17 | startDate: 0, 18 | endDate: 0, 19 | archiveShifts: {}, 20 | } 21 | 22 | const getters = { 23 | getArchiveTimeRange(state) { 24 | return state.timeRange 25 | }, 26 | getShiftsForAnalystByShiftType: (state) => (analystId, shiftTypeId) => { 27 | if (analystId in state.archiveShifts) { 28 | if (shiftTypeId in state.archiveShifts[analystId]) { 29 | return state.archiveShifts[analystId][shiftTypeId] 30 | } else { 31 | return 0 32 | } 33 | } else { 34 | return 0 35 | } 36 | }, 37 | } 38 | 39 | const actions = { 40 | setArchiveTimeRange(context, { timeRange, startDate, endDate }) { 41 | if (!startDate) { 42 | startDate = dayjs() 43 | } 44 | 45 | if (!endDate) { 46 | endDate = dayjs() 47 | } 48 | 49 | if (timeRange) { 50 | startDate = startDate.subtract(timeRange, 'month') 51 | context.commit('setArchiveTimeRange', timeRange) 52 | } else { 53 | context.commit('setArchiveTimeRange', 0) 54 | } 55 | 56 | context.commit('setArchiveStartAndEndTime', { startDate, endDate }) 57 | context.dispatch('fetchCurrentArchiveData') 58 | }, 59 | 60 | fetchCurrentArchiveData(context) { 61 | try { 62 | axios.get(generateUrl('/apps/shifts/getShiftsDataByTimeRange/' + context.state.startDate.format('YYYY-MM-DD') + '/' + context.state.endDate.format('YYYY-MM-DD'))).then(response => { 63 | context.commit('clearArchiveShifts') 64 | 65 | response.data.forEach(shift => { 66 | 67 | if (typeof shift.shift_type_id === 'string') { 68 | shift.shift_type_id = parseInt(shift.shift_type_id) 69 | } 70 | if (typeof shift.count === 'string') { 71 | shift.count = parseInt(shift.count) 72 | } 73 | 74 | context.commit('setArchiveShiftsForAnalyst', { 75 | analystId: shift.user_id, 76 | shiftTypeId: shift.shift_type_id, 77 | count: shift.count, 78 | }) 79 | }) 80 | }).catch(e => { 81 | console.error('Failed to fetch archive data:', e) 82 | }) 83 | } catch (e) { 84 | console.error(e) 85 | showError(t('shifts', 'Could not fetch archive data')) 86 | } 87 | } 88 | } 89 | 90 | const mutations = { 91 | setArchiveTimeRange(state, timeRange) { 92 | state.timeRange = timeRange 93 | }, 94 | setArchiveStartAndEndTime(state, { startDate, endDate }) { 95 | state.startDate = startDate 96 | state.endDate = endDate 97 | }, 98 | 99 | clearArchiveShifts(state) { 100 | Vue.set(state, 'archiveShifts', {}) 101 | }, 102 | setArchiveShiftsForAnalyst(state, { analystId, shiftTypeId, count }) { 103 | let obj = {} 104 | if (analystId in state.archiveShifts) { 105 | obj = state.archiveShifts[analystId] 106 | } 107 | obj[shiftTypeId] = count 108 | Vue.set(state.archiveShifts, analystId, obj) 109 | } 110 | } 111 | 112 | export default { state, mutations, getters, actions } 113 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright Copyright (c) 2021. Fabian Kirchesch 3 | * 4 | * @author Fabian Kirchesch 5 | */ 6 | 7 | import Vue from 'vue' 8 | import Vuex from 'vuex' 9 | 10 | import newShiftInstance from './newShiftInstance' 11 | import shiftsTypeInstance from './shiftsTypeInstance' 12 | import database from './database' 13 | import settings from './settings' 14 | import archive from './archive' 15 | 16 | Vue.use(Vuex) 17 | 18 | const store = new Vuex.Store({ 19 | modules: { 20 | newShiftInstance, 21 | shiftsTypeInstance, 22 | database, 23 | settings, 24 | archive, 25 | }, 26 | }) 27 | 28 | export default store 29 | -------------------------------------------------------------------------------- /src/store/newShiftInstance.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright Copyright (c) 2021. Fabian Kirchesch 3 | * @copyright Copyright (c) 2023. Kevin Küchler 4 | * 5 | * @author Fabian Kirchesch 6 | * @author Kevin Küchler 7 | */ 8 | 9 | import { showError, showWarning } from '@nextcloud/dialogs' 10 | import axios from '@nextcloud/axios' 11 | import { generateUrl } from '@nextcloud/router' 12 | 13 | const state = { 14 | newShiftInstance: { 15 | analysts: [], 16 | shiftsType: '', 17 | dates: [], 18 | }, 19 | } 20 | 21 | const mutations = { 22 | resetNewShiftInstance(state) { 23 | state.newShiftInstance.analysts = [] 24 | state.dates = [] 25 | }, 26 | addAnalyst(state, analyst) { 27 | state.newShiftInstance.analysts.push(analyst) 28 | }, 29 | removeAnalyst(state, userId) { 30 | const index = state.newShiftInstance.analysts.findIndex((analyst) => { 31 | return analyst.userId === userId 32 | }) 33 | if (index !== -1) { 34 | state.newShiftInstance.analysts.splice(index, 1) 35 | } 36 | }, 37 | changeShiftsType(state, shiftsType) { 38 | state.newShiftInstance.analysts = [] 39 | state.newShiftInstance.shiftsType = shiftsType 40 | }, 41 | } 42 | 43 | const getters = { 44 | getCurrentShiftsType(state) { 45 | return state.newShiftInstance.shiftsType 46 | }, 47 | } 48 | 49 | const actions = { 50 | async saveNewShift({ state, dispatch, commit, rootState }) { 51 | const allShifts = rootState.database.allShifts 52 | const newShiftInstance = state.newShiftInstance 53 | if (newShiftInstance.analysts && newShiftInstance.dates && newShiftInstance.shiftsType) { 54 | try { 55 | await Promise.all(newShiftInstance.analysts.map(async (analyst) => { 56 | const analystId = analyst.userId 57 | const shiftTypeId = newShiftInstance.shiftsType.id 58 | const newShifts = newShiftInstance.dates.map((date) => { 59 | return { 60 | analystId, 61 | shiftTypeId, 62 | date, 63 | oldAnalystId: analystId, 64 | saveChanges: true 65 | } 66 | }) 67 | await Promise.all(newShifts.map(async (newShift) => { 68 | const exists = allShifts.find((shift) => { 69 | return shift.userId === '-1' 70 | && newShift.date === shift.date 71 | && newShift.shiftTypeId.toString() === shift.shiftTypeId 72 | }) 73 | if (exists === undefined) { 74 | const response = await axios.post(generateUrl('/apps/shifts/shifts'), newShift) 75 | if (response.data && response.data.date !== newShift.date) { 76 | await axios.put(generateUrl(`/apps/shifts/shifts/${response.data.id}`), newShift) 77 | } 78 | } else { 79 | newShift.id = exists.id 80 | await axios.put(generateUrl(`/apps/shifts/shifts/${newShift.id}`), newShift) 81 | } 82 | })) 83 | })) 84 | commit('resetNewShiftInstance') 85 | dispatch('updateShifts') 86 | } catch (e) { 87 | console.error(e) 88 | showError(t('shifts', 'Could not create the shift')) 89 | } 90 | } else { 91 | console.log('No Name for ShiftType') 92 | showWarning(t('shifts', 'No Analysts or Dates for Shift given')) 93 | } 94 | 95 | }, 96 | 97 | createNewShift(context, shift) { 98 | return new Promise((resolve, reject) => { 99 | axios.post(generateUrl('/apps/shifts/shifts'), shift).then(() => { 100 | resolve() 101 | }).catch((e) => { 102 | if (e.response) { 103 | reject(e.response) 104 | } else { 105 | reject(e) 106 | } 107 | }) 108 | }) 109 | } 110 | } 111 | 112 | export default { state, mutations, getters, actions } 113 | -------------------------------------------------------------------------------- /src/store/settings.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright Copyright (c) 2021. Fabian Kirchesch 3 | * @copyright Copyright (c) 2023. Kevin Küchler 4 | * 5 | * @author Fabian Kirchesch 6 | * @author Kevin Küchler 7 | */ 8 | 9 | import axios from '@nextcloud/axios' 10 | import { generateUrl } from '@nextcloud/router' 11 | import { showError, showWarning } from '@nextcloud/dialogs' 12 | 13 | const state = { 14 | calendarName: null, 15 | addUserCalendarEvent: true, 16 | organizerName: null, 17 | organizerEmail: null, 18 | timezone: 'UTC', 19 | skillGroups: null, 20 | shiftChangeSameShiftType: false, 21 | settingsFetched: false, 22 | } 23 | 24 | const getters = { 25 | getCalendarName(state) { 26 | return state.calendarName 27 | }, 28 | getAddUserCalendarEvent(state) { 29 | return state.addUserCalendarEvent 30 | }, 31 | getOrganizerName(state) { 32 | return state.organizerName 33 | }, 34 | getOrganizerEmail(state) { 35 | return state.organizerEmail 36 | }, 37 | getShiftsTimezone(state) { 38 | return state.timezone 39 | }, 40 | getSkillGroups(state) { 41 | return state.skillGroups 42 | }, 43 | getShiftChangeSameType(state) { 44 | return state.shiftChangeSameShiftType 45 | }, 46 | getSettingsFetched(state) { 47 | return state.settingsFetched 48 | }, 49 | } 50 | 51 | const actions = { 52 | async fetchSettings({ state, dispatch, commit }) { 53 | try { 54 | const settingsResponse = await axios.get(generateUrl('/apps/shifts/settings')) 55 | const settings = settingsResponse.data 56 | 57 | commit('updateCalendarName', settings.calendarName) 58 | commit('updateAddUserCalendarEvent', settings.addUserCalendarEvent) 59 | commit('updateOrganizerName', settings.organizerName) 60 | commit('updateOrganizerEmail', settings.organizerEmail) 61 | commit('updateShiftsTimezone', settings.timezone) 62 | commit('updateSkillGroups', settings.skillGroups) 63 | commit('updateShiftChangeSameType', settings.shiftChangeSameShiftType === '1') 64 | commit('updateSettingsFetched', true) 65 | 66 | } catch (e) { 67 | console.error(e) 68 | showError(t('shifts', 'Could not fetch Settings')) 69 | } 70 | if (state.calendarName === '') { 71 | showWarning(t('shifts', 'Please set a Calendarname in the App-Settings')) 72 | commit('updateSettingsFetched', false) 73 | } 74 | if (state.organizerName === '') { 75 | showWarning(t('shifts', 'Please set an Organizername in the App-Settings')) 76 | commit('updateSettingsFetched', false) 77 | } 78 | if (state.organizerEmail === '') { 79 | showWarning(t('shifts', 'Please set an Organizeremail in the App-Settings')) 80 | commit('updateSettingsFetched', false) 81 | } 82 | if (state.skillGroups !== null && state.skillGroups.length === 0) { 83 | showWarning(t('shifts', 'Please set at least one Skillgroup in the App-Settings')) 84 | commit('updateSettingsFetched', false) 85 | } 86 | }, 87 | } 88 | 89 | const mutations = { 90 | updateCalendarName(state, calendarName) { 91 | state.calendarName = calendarName 92 | }, 93 | updateAddUserCalendarEvent(state, addEvent) { 94 | state.addUserCalendarEvent = addEvent 95 | }, 96 | updateOrganizerName(state, organizerName) { 97 | state.organizerName = organizerName 98 | }, 99 | updateOrganizerEmail(state, organizerEmail) { 100 | state.organizerEmail = organizerEmail 101 | }, 102 | updateShiftChangeSameType(store, shiftChangeSameType) { 103 | state.shiftChangeSameShiftType = shiftChangeSameType 104 | }, 105 | updateSkillGroups(state, skillGroups) { 106 | state.skillGroups = skillGroups 107 | }, 108 | updateShiftsTimezone(state, timezone) { 109 | state.timezone = timezone 110 | }, 111 | updateSettingsFetched(state, fetched) { 112 | state.settingsFetched = fetched 113 | }, 114 | } 115 | 116 | export default { state, mutations, getters, actions } 117 | -------------------------------------------------------------------------------- /src/utils/date.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright Copyright (c) 2019 Georg Ehrke 3 | * 4 | * @author Georg Ehrke 5 | * 6 | * @author Fabian Kirchesch 7 | * 8 | * @license AGPL-3.0-or-later 9 | * 10 | * This program is free software: you can redistribute it and/or modify 11 | * it under the terms of the GNU Affero General Public License as 12 | * published by the Free Software Foundation, either version 3 of the 13 | * License, or (at your option) any later version. 14 | * 15 | * This program is distributed in the hope that it will be useful, 16 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | * GNU Affero General Public License for more details. 19 | * 20 | * You should have received a copy of the GNU Affero General Public License 21 | * along with this program. If not, see . 22 | * 23 | */ 24 | 25 | import logger from './logger.js' 26 | 27 | /** 28 | * returns a new Date object 29 | * 30 | * @return {Date} 31 | */ 32 | export function dateFactory() { 33 | return new Date() 34 | } 35 | 36 | /** 37 | * formats a Date object as YYYYMMDD 38 | * 39 | * @param {Date} date Date to format 40 | * @return {string} 41 | */ 42 | export function getYYYYMMDDFromDate(date) { 43 | return new Date(date.getTime() - (date.getTimezoneOffset() * 60000)) 44 | .toISOString() 45 | .split('T')[0] 46 | } 47 | 48 | /** 49 | * get unix time from date object 50 | * 51 | * @param {Date} date Date to format 52 | * @return {number} 53 | */ 54 | export function getUnixTimestampFromDate(date) { 55 | return Math.floor(date.getTime() / 1000) 56 | } 57 | 58 | /** 59 | * Gets a Date-object based on the firstday param used in routes 60 | * 61 | * @param {string} firstDayParam The firstday param from the router 62 | * @return {Date} 63 | */ 64 | export function getDateFromFirstdayParam(firstDayParam) { 65 | if (firstDayParam === 'now') { 66 | return dateFactory() 67 | } 68 | 69 | const [year, month, date] = firstDayParam.split('-') 70 | .map((str) => parseInt(str, 10)) 71 | 72 | if (Number.isNaN(year) || Number.isNaN(month) || Number.isNaN(date)) { 73 | logger.error('First day parameter contains non-numerical components, falling back to today') 74 | return dateFactory() 75 | } 76 | 77 | const dateObject = dateFactory() 78 | dateObject.setFullYear(year, month - 1, date) 79 | dateObject.setHours(0, 0, 0, 0) 80 | 81 | return dateObject 82 | } 83 | 84 | /** 85 | * formats firstday param as YYYYMMDD 86 | * 87 | * @param {string} firstDayParam The firstday param from the router 88 | * @return {string} 89 | */ 90 | export function getYYYYMMDDFromFirstdayParam(firstDayParam) { 91 | if (firstDayParam === 'now') { 92 | return getYYYYMMDDFromDate(dateFactory()) 93 | } 94 | 95 | return firstDayParam 96 | } 97 | 98 | /** 99 | * Gets a date object based on the given DateTimeValue 100 | * Ignores given timezone-information 101 | * 102 | * @param {DateTimeValue} dateTimeValue Value to get date from 103 | * @return {Date} 104 | */ 105 | export function getDateFromDateTimeValue(dateTimeValue) { 106 | return new Date( 107 | dateTimeValue.year, 108 | dateTimeValue.month - 1, 109 | dateTimeValue.day, 110 | dateTimeValue.hour, 111 | dateTimeValue.minute, 112 | 0, 113 | 0 114 | ) 115 | } 116 | 117 | /** 118 | * modifies a date 119 | * 120 | * @param {Date} date Date object to modify 121 | * @param {object} data The destructuring object 122 | * @param {number} data.day Number of days to add 123 | * @param {number} data.week Number of weeks to add 124 | * @param {number} data.month Number of months to add 125 | * @return {Date} 126 | */ 127 | export function modifyDate(date, { day = 0, week = 0, month = 0 }) { 128 | date = new Date(date.getTime()) 129 | date.setDate(date.getDate() + day) 130 | date.setDate(date.getDate() + week * 7) 131 | date.setMonth(date.getMonth() + month) 132 | 133 | return date 134 | } 135 | 136 | /** 137 | * combines day of Shift with Timestamp 138 | * 139 | * @param {string} date Date object of Day 140 | * @param {string} timestamp Timestamp to add 141 | * @return {Date} 142 | */ 143 | export function calcShiftDate(date, timestamp) { 144 | if (timestamp.includes('T')) { 145 | timestamp = timestamp.split('T')[1].substring(0, 5) 146 | } 147 | return new Date(date + 'T' + timestamp) 148 | } 149 | -------------------------------------------------------------------------------- /src/utils/logger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright Copyright (c) 2019 Georg Ehrke 3 | * 4 | * @author Georg Ehrke 5 | * 6 | * @author Fabian Kirchesch 7 | * 8 | * @license AGPL-3.0-or-later 9 | * 10 | * This program is free software: you can redistribute it and/or modify 11 | * it under the terms of the GNU Affero General Public License as 12 | * published by the Free Software Foundation, either version 3 of the 13 | * License, or (at your option) any later version. 14 | * 15 | * This program is distributed in the hope that it will be useful, 16 | * but WITHOUT ANY WARRANTY without even the implied warranty of 17 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | * GNU Affero General Public License for more details. 19 | * 20 | * You should have received a copy of the GNU Affero General Public License 21 | * along with this program. If not, see . 22 | * 23 | */ 24 | import { getLoggerBuilder } from '@nextcloud/logger' 25 | 26 | const logger = getLoggerBuilder() 27 | .setApp('calendar') 28 | .detectUser() 29 | .build() 30 | 31 | /** 32 | * Logs a debug message 33 | * 34 | * @param {string} message The message to log 35 | * @param {object=} context Additional context if needed 36 | */ 37 | const logDebug = (message, context = {}) => { 38 | logger.debug(message, context) 39 | } 40 | 41 | /** 42 | * Logs an error message 43 | * 44 | * @param {string} message The message to log 45 | * @param {object=} context Additional context if needed 46 | */ 47 | const logError = (message, context = {}) => { 48 | logger.error(message, context) 49 | } 50 | 51 | /** 52 | * Logs a fatal message 53 | * 54 | * @param {string} message The message to log 55 | * @param {object=} context Additional context if needed 56 | */ 57 | const logFatal = (message, context = {}) => { 58 | logger.fatal(message, context) 59 | } 60 | 61 | /** 62 | * Logs an info message 63 | * 64 | * @param {string} message The message to log 65 | * @param {object=} context Additional context if needed 66 | */ 67 | const logInfo = (message, context = {}) => { 68 | logger.info(message, context) 69 | } 70 | 71 | /** 72 | * Logs a warn message 73 | * 74 | * @param {string} message The message to log 75 | * @param {object=} context Additional context if needed 76 | */ 77 | const logWarn = (message, context = {}) => { 78 | logger.warn(message, context) 79 | } 80 | 81 | export default logger 82 | export { 83 | logDebug, 84 | logError, 85 | logFatal, 86 | logInfo, 87 | logWarn, 88 | } 89 | -------------------------------------------------------------------------------- /src/utils/router.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright Copyright (c) 2020 Georg Ehrke 3 | * @author Georg Ehrke 4 | * @modified Fabian Kirchesch 5 | * 6 | * @license GNU AGPL version 3 or any later version 7 | * 8 | * This program is free software: you can redistribute it and/or modify 9 | * it under the terms of the GNU Affero General Public License as 10 | * published by the Free Software Foundation, either version 3 of the 11 | * License, or (at your option) any later version. 12 | * 13 | * This program is distributed in the hope that it will be useful, 14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | * GNU Affero General Public License for more details. 17 | * 18 | * You should have received a copy of the GNU Affero General Public License 19 | * along with this program. If not, see . 20 | * 21 | */ 22 | import { loadState } from '@nextcloud/initial-state' 23 | import { 24 | dateFactory, 25 | getUnixTimestampFromDate, 26 | } from './date.js' 27 | 28 | /** 29 | * Gets the initial view 30 | * 31 | * @returns {String} 32 | */ 33 | export function getInitialView() { 34 | try { 35 | return loadState('shifts', 'initial_view') 36 | } catch (error) { 37 | return 'dayGridMonth' 38 | } 39 | } 40 | 41 | /** 42 | * Gets the preferred editor view 43 | * 44 | * @returns {string} Either popover or sidebar 45 | */ 46 | export function getPreferredEditorRoute() { 47 | let skipPopover 48 | try { 49 | skipPopover = loadState('shifts', 'skip_popover') 50 | } catch (error) { 51 | skipPopover = false 52 | } 53 | 54 | if (window.innerWidth <= 768) { 55 | skipPopover = true 56 | } 57 | 58 | return skipPopover 59 | ? 'sidebar' 60 | : 'popover' 61 | } 62 | 63 | /** 64 | * Gets the default start-date for a new event 65 | * 66 | * @returns {string} 67 | */ 68 | export function getDefaultStartDateForNewEvent() { 69 | const start = dateFactory() 70 | start.setHours(start.getHours() + Math.ceil(start.getMinutes() / 60)) 71 | start.setMinutes(0) 72 | 73 | return String(getUnixTimestampFromDate(start)) 74 | } 75 | 76 | /** 77 | * Gets the default end-date for a new event 78 | * 79 | * @returns {string} 80 | */ 81 | export function getDefaultEndDateForNewEvent() { 82 | // When we have a setting for default event duration, 83 | // this needs to be taken into consideration here 84 | const start = getDefaultStartDateForNewEvent() 85 | const end = new Date(Number(start) * 1000) 86 | end.setHours(end.getHours() + 1) 87 | 88 | return String(getUnixTimestampFromDate(end)) 89 | } 90 | 91 | /** 92 | * Prefixes a desired route name based on the current route 93 | * 94 | * @param {String} currentRouteName The name of the current route 95 | * @param {String} toRouteName The name of the desired route 96 | * @returns {String} 97 | */ 98 | export function getPrefixedRoute(currentRouteName, toRouteName) { 99 | if (currentRouteName.startsWith('Embed')) { 100 | return 'Embed' + toRouteName 101 | } 102 | 103 | if (currentRouteName.startsWith('Public')) { 104 | return 'Public' + toRouteName 105 | } 106 | 107 | return toRouteName 108 | } 109 | 110 | /** 111 | * Checks whether a routeName represents a public / embedded route 112 | * 113 | * @param {String} routeName Name of the route 114 | * @returns {Boolean} 115 | */ 116 | export function isPublicOrEmbeddedRoute(routeName) { 117 | return routeName.startsWith('Embed') || routeName.startsWith('Public') 118 | } 119 | -------------------------------------------------------------------------------- /src/utils/timezone.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright Copyright (c) 2020 Georg Ehrke 3 | * @author Georg Ehrke 4 | * @modified Fabian Kirchesch 5 | * 6 | * @license GNU AGPL version 3 or any later version 7 | * 8 | * This program is free software: you can redistribute it and/or modify 9 | * it under the terms of the GNU Affero General Public License as 10 | * published by the Free Software Foundation, either version 3 of the 11 | * License, or (at your option) any later version. 12 | * 13 | * This program is distributed in the hope that it will be useful, 14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | * GNU Affero General Public License for more details. 17 | * 18 | * You should have received a copy of the GNU Affero General Public License 19 | * along with this program. If not, see . 20 | * 21 | */ 22 | import { translate as t } from '@nextcloud/l10n' 23 | 24 | /** 25 | * 26 | * @param {String[]} timezoneList List of Olsen timezones 27 | * @param {Array} additionalTimezones List of additional timezones 28 | * @returns {[]} 29 | */ 30 | export function getSortedTimezoneList(timezoneList = [], additionalTimezones = []) { 31 | const sortedByContinent = {} 32 | const sortedList = [] 33 | 34 | for (const timezoneId of timezoneList) { 35 | const components = timezoneId.split('/') 36 | let [continent, name] = [components.shift(), components.join('/')] 37 | if (!name) { 38 | name = continent 39 | // TRANSLATORS This refers to global timezones in the timezone picker 40 | continent = t('calendar', 'Global') 41 | } 42 | 43 | sortedByContinent[continent] = sortedByContinent[continent] || { 44 | continent, 45 | regions: [], 46 | } 47 | 48 | sortedByContinent[continent].regions.push({ 49 | label: getReadableTimezoneName(name), 50 | cities: [], 51 | timezoneId, 52 | }) 53 | } 54 | 55 | for (const additionalTimezone of additionalTimezones) { 56 | const { continent, label, timezoneId } = additionalTimezone 57 | 58 | sortedByContinent[continent] = sortedByContinent[continent] || { 59 | continent, 60 | regions: [], 61 | } 62 | 63 | sortedByContinent[continent].regions.push({ 64 | label, 65 | cities: [], 66 | timezoneId, 67 | }) 68 | } 69 | 70 | for (const continent in sortedByContinent) { 71 | if (!Object.prototype.hasOwnProperty.call(sortedByContinent, continent)) { 72 | continue 73 | } 74 | 75 | sortedByContinent[continent].regions.sort((a, b) => { 76 | if (a.label < b.label) { 77 | return -1 78 | } 79 | 80 | return 1 81 | }) 82 | sortedList.push(sortedByContinent[continent]) 83 | } 84 | 85 | // Sort continents by name 86 | sortedList.sort((a, b) => { 87 | if (a.continent < b.continent) { 88 | return -1 89 | } 90 | 91 | return 1 92 | }) 93 | 94 | return sortedList 95 | } 96 | 97 | /** 98 | * Get human-readable name for timezoneId 99 | * 100 | * @param {String} timezoneId TimezoneId to turn human-readable 101 | * @returns {String} 102 | */ 103 | export function getReadableTimezoneName(timezoneId) { 104 | return timezoneId 105 | .split('_') 106 | .join(' ') 107 | .replace('St ', 'St. ') 108 | .split('/') 109 | .join(' - ') 110 | } 111 | -------------------------------------------------------------------------------- /src/views/Archive.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 37 | -------------------------------------------------------------------------------- /src/views/NewShift.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 17 | 18 | 19 | 20 | {{ t('shifts','Add Shift') }} 21 | 22 | 23 | 24 | 25 | 32 | 33 | 34 | 41 | 42 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 68 | {{ t('shifts','Cancel') }} 69 | 70 | 73 | {{ t('shifts','Save') }} 74 | 75 | 76 | 77 | 78 | 79 | 80 | 198 | 199 | 204 | -------------------------------------------------------------------------------- /src/views/Shifts.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | 17 | 18 | 19 | 23 | {{ t('shifts','Add Shift') }} 24 | 25 | 26 | 30 | {{ t('shifts','Synchronize Calendar') }} 31 | 32 | 33 | 34 | 35 | 38 | 39 | 40 | 41 | 42 | 43 | 91 | 115 | -------------------------------------------------------------------------------- /src/views/ShiftsTypes.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | {{ t('shifts','Add new Shiftstype') }} 18 | 19 | 20 | 21 | 22 | 23 | 26 | 27 | 28 | 29 | 30 | {{ t('shifts', 'Shiftstype') }} 31 | 32 | 33 | 34 | 35 | 40 | 41 | {{item.name}} 42 | 43 | 44 | 45 | 46 | 49 | {{ t('shifts', 'Edit') }} 50 | 51 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 116 | 117 | 149 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'stylelint-config-recommended-scss', 3 | rules: { 4 | indentation: 'tab', 5 | 'selector-type-no-unknown': null, 6 | 'number-leading-zero': null, 7 | 'rule-empty-line-before': [ 8 | 'always', 9 | { 10 | ignore: ['after-comment', 'inside-block'], 11 | }, 12 | ], 13 | 'declaration-empty-line-before': [ 14 | 'never', 15 | { 16 | ignore: ['after-declaration'], 17 | }, 18 | ], 19 | 'comment-empty-line-before': null, 20 | 'selector-type-case': null, 21 | 'selector-list-comma-newline-after': null, 22 | 'no-descending-specificity': null, 23 | 'string-quotes': 'single', 24 | 'selector-pseudo-element-no-unknown': [ 25 | true, 26 | { 27 | ignorePseudoElements: ['v-deep'], 28 | }, 29 | ], 30 | }, 31 | plugins: ['stylelint-scss'], 32 | } 33 | -------------------------------------------------------------------------------- /templates/main.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * @author Fabian Kirchesch 6 | */ 7 | 8 | script('shifts','shifts-main'); 9 | style('shifts','shifts'); 10 | -------------------------------------------------------------------------------- /templates/settings.php: -------------------------------------------------------------------------------- 1 | 4 | * @copyright Copyright (c) 2023. Kevin Küchler 5 | * 6 | * @author Fabian Kirchesch 7 | * @author Kevin Küchler 8 | */ 9 | 10 | /** @var $l \OCP\IL10N */ 11 | /** @var $_ array */ 12 | style('shifts', 'shifts'); 13 | script('shifts', 'shifts-settings'); 14 | ?> 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/Integration/ShiftsIntegrationTest.php: -------------------------------------------------------------------------------- 1 | getContainer(); 24 | 25 | $container->registerService('UserId', function($c) { 26 | return $this->userId; 27 | }); 28 | 29 | $this->service = $container->query( 30 | 'OCA\Shifts\Service\ShiftService' 31 | ); 32 | 33 | $this->typeMapper = $container->query( 34 | 'OCA\Shifts\Db\ShiftsTypeMapper' 35 | ); 36 | 37 | $this->mapper = $container->query( 38 | 'OCA\Shifts\Db\ShiftMapper' 39 | ); 40 | } 41 | 42 | public function testGetShiftsByUserId() { 43 | $shift = new Shift(); 44 | $shift->setUserId('fabian'); 45 | $shift->setShiftTypeId('1'); 46 | $shift->setDate('1970-01-01'); 47 | 48 | $resultShift = $this->mapper->insert($shift); 49 | 50 | $shiftList = $this->service->findById($this->userId); 51 | 52 | $shiftsIdList = array_column($shiftList, 'id'); 53 | 54 | $this->assertContains($resultShift->getId(), $shiftsIdList); 55 | 56 | $this->mapper->delete($resultShift); 57 | } 58 | 59 | public function testFindInTimeRange() { 60 | $shiftIn = new Shift(); 61 | $shiftIn->setUserId('fabian'); 62 | $shiftIn->setShiftTypeId('1'); 63 | $shiftIn->setDate('1971-01-01'); 64 | 65 | $shiftOut = new Shift(); 66 | $shiftOut->setUserId('-1'); 67 | $shiftOut->setShiftTypeId('1'); 68 | $shiftOut->setDate('1970-01-01'); 69 | 70 | $resultIn = $this->mapper->insert($shiftIn); 71 | $resultOut = $this->mapper->insert($shiftOut); 72 | 73 | $result = $this->service->findByTimeRange('1970-12-31', '1971-01-02'); 74 | 75 | $this->assertCount(1, $result); 76 | 77 | $this->mapper->delete($resultIn); 78 | $this->mapper->delete($resultOut); 79 | } 80 | 81 | public function testTriggerUnassignedShifts() { 82 | $shiftsType = new ShiftsType(); 83 | $shiftsType->setName('testing'); 84 | $shiftsType->setDesc(''); 85 | $shiftsType->setStartTimeStamp('00:00'); 86 | $shiftsType->setStopTimeStamp('00:01'); 87 | $shiftsType->setCalendarColor('#ffffffff'); 88 | $shiftsType->setMoRule('1'); 89 | $shiftsType->setTuRule('0'); 90 | $shiftsType->setWeRule('0'); 91 | $shiftsType->setThRule('0'); 92 | $shiftsType->setFrRule('0'); 93 | $shiftsType->setSaRule('0'); 94 | $shiftsType->setSoRule('0'); 95 | $shiftsType->setSkillGroupId('0'); 96 | $shiftsType->setIsWeekly('1'); 97 | 98 | $resultShiftsType = $this->typeMapper->insert($shiftsType); 99 | 100 | $this->service->triggerUnassignedShifts(); 101 | 102 | $resultShifts = $this->mapper->findByShiftsTypeId($resultShiftsType->getId()); 103 | 104 | $this->assertTrue(count($resultShifts) > 0); 105 | foreach ($resultShifts as $values) { 106 | $this->assertTrue(date('D', strtotime($values->getDate())) === 'Mon'); 107 | $this->assertEquals($values->getShiftTypeId(), $resultShiftsType->getId()); 108 | $this->assertEquals($values->getUserId(), '-1'); 109 | $this->mapper->delete($values); 110 | } 111 | 112 | $this->typeMapper->delete($resultShiftsType); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /tests/Integration/ShiftsTypeIntegrationTest.php: -------------------------------------------------------------------------------- 1 | getContainer(); 24 | 25 | $container->registerService('UserId', function($c) { 26 | return $this->userId; 27 | }); 28 | 29 | $this->service = $container->query( 30 | 'OCA\Shifts\Service\ShiftsTypeService' 31 | ); 32 | 33 | $this->shiftsMapper = $container->query( 34 | 'OCA\Shifts\Db\ShiftMapper' 35 | ); 36 | 37 | $this->mapper = $container->query( 38 | 'OCA\Shifts\Db\ShiftsTypeMapper' 39 | ); 40 | } 41 | 42 | public function testDeleteShiftsType() { 43 | $shiftsType = new ShiftsType(); 44 | $shiftsType->setName('testing'); 45 | $shiftsType->setDesc(''); 46 | $shiftsType->setStartTimeStamp('00:00'); 47 | $shiftsType->setStopTimeStamp('00:01'); 48 | $shiftsType->setCalendarColor('#ffffffff'); 49 | $shiftsType->setMoRule('1'); 50 | $shiftsType->setTuRule('0'); 51 | $shiftsType->setWeRule('0'); 52 | $shiftsType->setThRule('0'); 53 | $shiftsType->setFrRule('0'); 54 | $shiftsType->setSaRule('0'); 55 | $shiftsType->setSoRule('0'); 56 | $shiftsType->setSkillGroupId('0'); 57 | $shiftsType->setIsWeekly('1'); 58 | 59 | $resultShiftsType = $this->mapper->insert($shiftsType); 60 | 61 | 62 | $shiftOut = new Shift(); 63 | $shiftOut->setUserId('-1'); 64 | $shiftOut->setShiftTypeId($resultShiftsType->getId()); 65 | $shiftOut->setDate('2022-01-03'); 66 | $shiftIn = new Shift(); 67 | $shiftIn->setUserId('fabian'); 68 | $shiftIn->setShiftTypeId($resultShiftsType->getId() + 1); 69 | $shiftIn->setDate('2022-01-10'); 70 | 71 | $resultShiftOut = $this->shiftsMapper->insert($shiftOut); 72 | $resultShiftIn = $this->shiftsMapper->insert($shiftIn); 73 | 74 | $this->service->delete($resultShiftsType->getId()); 75 | 76 | $shiftList = $this->shiftsMapper->findAll(); 77 | 78 | $shiftsIdList = array_column($shiftList, 'id'); 79 | 80 | $this->assertNotContains($resultShiftOut->getId(), $shiftsIdList); 81 | $this->assertContains($resultShiftIn->getId(), $shiftsIdList); 82 | 83 | $this->shiftsMapper->delete($resultShiftIn); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tests/Unit/Service/ShiftsServiceTest.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * @author Fabian Kirchesch 6 | */ 7 | 8 | 9 | namespace OCA\Shifts\Tests\Unit\Service; 10 | 11 | use OCA\Shifts\Service\NotFoundException; 12 | use PHPUnit\Framework\TestCase; 13 | 14 | use OCP\AppFramework\Db\DoesNotExistException; 15 | 16 | use OCA\Shifts\Db\Shift; 17 | use OCA\Shifts\Service\ShiftService; 18 | use OCA\Shifts\Db\ShiftMapper; 19 | use OCA\Shifts\Db\ShiftsTypeMapper; 20 | 21 | class ShiftServiceTest extends TestCase 22 | { 23 | private $service; 24 | private $mapper; 25 | private $typeMapper; 26 | private $userId = 'fabian'; 27 | 28 | public function setUp(): void 29 | { 30 | $this->mapper = $this->getMockBuilder(ShiftMapper::class) 31 | ->disableOriginalConstructor() 32 | ->getMock(); 33 | $this->typeMapper = $this->getMockBuilder(ShiftsTypeMapper::class) 34 | ->disableOriginalConstructor() 35 | ->getMock(); 36 | $this->service = new ShiftService($this->mapper, $this->typeMapper); 37 | } 38 | 39 | public function testUpdate() 40 | { 41 | $shift = new Shift(); 42 | $shift->setUserId('fabian'); 43 | $shift->setShiftTypeId('1'); 44 | $shift->setDate('2021-10-19'); 45 | $this->mapper->expects($this->once()) 46 | ->method('find') 47 | ->with($this->equalTo(0)) 48 | ->will($this->returnValue($shift)); 49 | 50 | // the note when updated 51 | $updatedShift = new Shift(); 52 | $updatedShift->setUserId('test'); 53 | $updatedShift->setShiftTypeId('0'); 54 | $updatedShift->setDate('2021-10-20'); 55 | $this->mapper->expects($this->once()) 56 | ->method('update') 57 | ->with($this->equalTo($updatedShift)) 58 | ->will($this->returnValue($updatedShift)); 59 | 60 | $result = $this->service->update(0, 'test', '0', '2021-10-20'); 61 | 62 | $this->assertEquals($updatedShift, $result); 63 | 64 | } 65 | 66 | public function testUpdateNotFound() 67 | { 68 | $this->expectException(NotFoundException::class); 69 | // test the correct status code if no note is found 70 | $this->mapper->expects($this->once()) 71 | ->method('find') 72 | ->with($this->equalTo(0)) 73 | ->will($this->throwException(new DoesNotExistException(''))); 74 | 75 | $this->service->update(0, 'test', '0', '2021-10-20'); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | a.findIndex(t => (t.test.toString() === v.test.toString())) === i) 30 | module.exports = mergedConfigs 31 | 32 | module.exports.watchOptions = { 33 | aggregateTimeout: 200, 34 | poll: 750, 35 | } 36 | --------------------------------------------------------------------------------