Compare commits

...

57 Commits

Author SHA1 Message Date
643f546142
#173 - button behaviour in creating request (#174)
#173 - improved button behaviour
2022-06-20 09:33:30 +02:00
995c0b6696
- changed limits for item list (#171)
* - changed limits for displayed items

* - transferred to models

* - updated tests
2022-06-15 10:14:58 +02:00
9147b859d3
- fix docker volumes (#170)
- improved location for volumes
2022-06-14 11:48:23 +02:00
Krzysztof Rewak
2f9ef0ff12
- phrasing (#169)
* - removing "vacations" word

* - styling childcare vacations label
2022-06-14 10:29:55 +02:00
Adrian Hopek
68e32ad930
#166 - Slack and Google Calendar feature flags (#167)
* slack and google calendar feature flags

* cs fix

* update .env.example
2022-06-13 13:50:23 +02:00
Adrian Hopek
31a6d287c8
- fix automatically build on heroku (#165)
fix automatically build on heroku
2022-06-13 09:35:29 +02:00
77d3b4df0d
#153 - button behaviour in form editing (#164)
* #153 - behaviour of buttons has been changed

* #153 - changed the gray background to opacity

Co-authored-by: EwelinaLasowy <ewelina.lasowy@blumilk.pl>

* #153 - added for created forms

* #153 - default value for downloaded types is set

* #153 - removed hover for disabled buttons

Co-authored-by: EwelinaLasowy <ewelina.lasowy@blumilk.pl>
2022-06-10 12:44:24 +02:00
2fb2415203
#157 - more interactive calendar (#162)
* #157 - added support for the date parameter

* #157 - prepared clikable days

* removed line with consol log

* lint

* #157 - prepared clikable days for calendar of holidays

* #157 - added a privilege check

* #157 - added support for the user id parameter

* improved loading of selected date

* updated calendar

* #157 - added the request class and improved the request parameters

* csf

* Apply suggestions from code review

Co-authored-by: Krzysztof Rewak <krzysztof.rewak@gmail.com>

* Apply suggestion

Co-authored-by: Krzysztof Rewak <krzysztof.rewak@gmail.com>

* icon has been renamed

Co-authored-by: EwelinaLasowy <ewelina.lasowy@blumilk.pl>

* selection of previous days restored

Co-authored-by: EwelinaLasowy <ewelina.lasowy@blumilk.pl>
Co-authored-by: Adrian Hopek <adrian.hopek@blumilk.pl>

* changes for the controller have been withdrawn

Co-authored-by: Adrian Hopek <adrian.hopek@blumilk.pl>

* wip

* Update resources/js/Composables/vacationTypeInfo.js

Co-authored-by: Ewelina Lasowy <56546832+EwelinaLasowy@users.noreply.github.com>

* #157 - updated cursor type for weekened

Co-authored-by: EwelinaLasowy <ewelina.lasowy@blumilk.pl>

Co-authored-by: Krzysztof Rewak <krzysztof.rewak@gmail.com>
Co-authored-by: EwelinaLasowy <ewelina.lasowy@blumilk.pl>
Co-authored-by: Adrian Hopek <adrian.hopek@blumilk.pl>
Co-authored-by: Ewelina Lasowy <56546832+EwelinaLasowy@users.noreply.github.com>
2022-06-08 11:02:37 +02:00
fe639f264d
- fix to empty states (#163)
* #150 - changed the default colour for empty states

* #150 - added empty states for user requests

* #31 - amended text for empty states - employee
2022-06-07 12:43:38 +02:00
dependabot[bot]
4b06e6c02b
#5 - (js) Bump vue from 3.2.21 to 3.2.36 (#159)
* #5 - (js) Bump vue from 3.2.21 to 3.2.36

Bumps [vue](https://github.com/vuejs/core) from 3.2.21 to 3.2.36.
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/compare/v3.2.21...v3.2.36)

---
updated-dependencies:
- dependency-name: vue
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* #5 - June 2022 composer and npm packages update

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: dawidrudnik <dawid.rudnik@blumilk.pl>
2022-06-03 13:39:10 +02:00
4309e8104b
#150 - empty states (#160)
* #150 - added base for empty states

* #150 - added empty states to subpages

* #150 - added empty states for the widget: vacation requests

disabled "see more" button when no results are available

* Update resources/js/Pages/Holidays/Index.vue

Removed emoji from text

Co-authored-by: Krzysztof Rewak <krzysztof.rewak@gmail.com>

* #150 - added empty state for key page

* #150 - added empty state for user vacation request widget

* #31 - title corrected

Co-authored-by: Krzysztof Rewak <krzysztof.rewak@gmail.com>
2022-06-02 10:37:41 +02:00
Krzysztof Rewak
3af92b2085
- restricting PR approvals for code owners only (#161) 2022-06-02 10:12:47 +02:00
Blumilkbot
0bebe2ecf1
- MIT licence added (#149) 2022-05-26 08:15:43 +02:00
Adrian Hopek
a3b8b18384
#145 - update locale path (#148) 2022-05-18 09:05:17 +02:00
Adrian Hopek
431262dfb7
#134 - fill users data for resume (#144)
* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* lint fixes

* missing empty lines

* translations

* fix vue version

* #134 - fixes

* fix

* fix

* #134 - fix

* fix

* fix

* #134 - added tests

* #134 - fix to translations

* #134 - tests

* #134 - fix

* Update database/factories/ResumeFactory.php

Co-authored-by: Krzysztof Rewak <krzysztof.rewak@gmail.com>

* #134 - fix

* #134 - fix

Co-authored-by: EwelinaLasowy <ewelina.lasowy@blumilk.pl>
Co-authored-by: Krzysztof Rewak <krzysztof.rewak@gmail.com>
2022-05-18 08:50:41 +02:00
Ewelina Lasowy
7154caa340
#132 - clean up notifications (#147)
* #132 - added translations

* wip

* #132 - added translations

* #132 - cs fix

* #132 - cs fix

Co-authored-by: Adrian Hopek <adrian.hopek@blumilk.pl>
2022-05-17 15:17:09 +02:00
Adrian Hopek
74f33d6c0b
- vacation type filter (#146)
* vacation type filter

* fix

* fix
2022-05-11 14:58:30 +02:00
Krzysztof Rewak
88b1543071
- adding day of week to upcoming holidays (#143) 2022-05-10 09:41:30 +02:00
Ewelina Lasowy
f4d928c6ae
- UX fixes (#142)
* - changed homeoffice icon color

* - improved list of request on dashboard

* - hide holidays if no data

* - fix to holidays

* - fix

* - made forms looks better

* - hide chart when user has no vacation limit

* - linter fix

* - fix to pdf attachment for vacation request
2022-05-06 12:29:06 +02:00
dependabot[bot]
497f47068c
#5 - (php) Bump laravel/framework from 9.9.0 to 9.10.1 (#140)
* #5 - (php) Bump laravel/framework from 9.9.0 to 9.10.1

Bumps [laravel/framework](https://github.com/laravel/framework) from 9.9.0 to 9.10.1.
- [Release notes](https://github.com/laravel/framework/releases)
- [Changelog](https://github.com/laravel/framework/blob/9.x/CHANGELOG.md)
- [Commits](https://github.com/laravel/framework/compare/v9.9.0...v9.10.1)

---
updated-dependencies:
- dependency-name: laravel/framework
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* #5 - May 2022 composer update

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: EwelinaLasowy <ewelina.lasowy@blumilk.pl>
2022-05-06 08:34:13 +02:00
dependabot[bot]
e1f449fb52
#5 - (js) Bump @vue/compiler-sfc from 3.2.31 to 3.2.33 (#141)
* #5 - (js) Bump @vue/compiler-sfc from 3.2.31 to 3.2.33

Bumps [@vue/compiler-sfc](https://github.com/vuejs/core/tree/HEAD/packages/compiler-sfc) from 3.2.31 to 3.2.33.
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/commits/v3.2.33/packages/compiler-sfc)

---
updated-dependencies:
- dependency-name: "@vue/compiler-sfc"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* #5 - May 2022 npm packages update

* #5 - disabled multi-word-component-names

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: EwelinaLasowy <ewelina.lasowy@blumilk.pl>
2022-05-06 08:34:03 +02:00
Krzysztof Rewak
3404bf1da8
#115 - Heroku deploy in GitHub Actions (#136) 2022-04-29 09:35:47 +02:00
dependabot[bot]
94da727fe4
- bump async from 2.6.3 to 2.6.4 (#137)
Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4.
- [Release notes](https://github.com/caolan/async/releases)
- [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md)
- [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4)

---
updated-dependencies:
- dependency-name: async
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-29 09:22:41 +02:00
Adrian Hopek
06863b854a
#138 - fix tables on safari (#139) 2022-04-29 09:22:02 +02:00
Adrian Hopek
6b2556c1da
#126 - vacation request reminders (#130)
* #126 - vacation request reminders

* #126 - fix workdays

* #126 - changes

* #126 - cs fix

* #5 - bump codestyle

* #126 - fix

* #126 - fix

* #126 - fix

* #126 - fix

* #126 - tests

* #126 - fix

* #126 - fix

* #126 - fix seeders

* #126 - fix

* #126 - tests

Co-authored-by: EwelinaLasowy <ewelina.lasowy@blumilk.pl>
2022-04-27 15:26:55 +02:00
Adrian Hopek
c69866bb52
#116 - integration with slack (#129)
* wip

* wip

* wip

* wip

* fix

* wip

* wip

* fix

* fix

* cs fix

* #116 - fix

* #116 - changed home-office icon

* Apply suggestions from code review

Co-authored-by: Krzysztof Rewak <krzysztof.rewak@gmail.com>

* #116 - cr fix

* #116 - cs fix

* #116 - cs fix

* Apply suggestions from code review

Co-authored-by: Ewelina Lasowy <56546832+EwelinaLasowy@users.noreply.github.com>

* #5 - bump codestyle

Co-authored-by: EwelinaLasowy <ewelina.lasowy@blumilk.pl>
Co-authored-by: Krzysztof Rewak <krzysztof.rewak@gmail.com>
Co-authored-by: Ewelina Lasowy <56546832+EwelinaLasowy@users.noreply.github.com>
2022-04-27 09:57:13 +02:00
Adrian Hopek
d60dc75f99
#118 - keys (#128)
* #118 - wip

* #118 - keys

* #118 - fix

* #118 - fix menu

* #118 - fix to policies and added translations

* #118 - wip

* #118 - tests

* #118 - fix

* #118 - fix

* #118 - fix

* Update resources/lang/pl.json

Co-authored-by: Ewelina Lasowy <56546832+EwelinaLasowy@users.noreply.github.com>

* #118 - cr fix

Co-authored-by: EwelinaLasowy <ewelina.lasowy@blumilk.pl>
Co-authored-by: Ewelina Lasowy <56546832+EwelinaLasowy@users.noreply.github.com>
2022-04-21 08:16:31 +02:00
Adrian Hopek
c95d08c861
#120 - remote work (#127)
* #120 - wip

* #120 - add icon to home office

* #120 - wip

* #120 - wip

* #120 - wip

* #120 - wip

* #120 - wip

* #120 - ui fixes

* #120 - fix

* #120 - fix

* #120 - fix

* #120 - fix

* #120 - translation fix

* #120 - fix

Co-authored-by: EwelinaLasowy <ewelina.lasowy@blumilk.pl>
2022-04-21 07:44:22 +02:00
Adrian Hopek
cc981b02b4
#90 - user profile (#125)
* fix css focuses

* #90 - wip

* #90 - fix to generate PDF

* #90 - wip

* #90 - wip

* #90 - wip

* #90 - wip

* #90 - fix to calendar

* #90 - wip

* #90 - fix

* #90 - fix lint

* #90 - fix

* Apply suggestions from code review

Co-authored-by: Krzysztof Rewak <krzysztof.rewak@gmail.com>
Co-authored-by: Ewelina Lasowy <56546832+EwelinaLasowy@users.noreply.github.com>

* #90 - cr fixes

* #90 - fix

Co-authored-by: EwelinaLasowy <ewelina.lasowy@blumilk.pl>
Co-authored-by: Krzysztof Rewak <krzysztof.rewak@gmail.com>
Co-authored-by: Ewelina Lasowy <56546832+EwelinaLasowy@users.noreply.github.com>
2022-04-14 11:58:45 +02:00
Adrian Hopek
459b62500e
#122 - fix missing people in vacation request view (#123) 2022-04-13 11:58:30 +02:00
Adrian Hopek
4af0482a93
#112 - change content of emails (#114)
* #112 - change content of emails

* #112 - wip
2022-04-08 08:12:41 +02:00
Adrian Hopek
ff8d6aade6
#71 - annual summary (#113)
* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* fix

* lint fix

* cr fix

* fix
2022-04-07 14:33:15 +02:00
Adrian Hopek
84403a762a
#107 - bump laravel (#111) 2022-04-07 08:45:04 +02:00
Adrian Hopek
6af4380fe6
#108 - types of vacation request (#110)
* #108 - wip

* #108 - add icon to absence

* #108 - wip

* #108 - fix

* #108 - fix title

Co-authored-by: EwelinaLasowy <ewelina.lasowy@blumilk.pl>
2022-04-06 15:13:54 +02:00
Ewelina Lasowy
fa244b96cd
- Change logo to svg (#109)
* - sidebar logo fix

* - change icon

* - change logo to svg

* #108 - change icon color
2022-04-06 10:59:26 +02:00
Adrian Hopek
3ab02f1df4
- change document numbering system (#106) 2022-04-04 15:02:22 +02:00
Adrian Hopek
1ae23bd7cb
#48 - fix heroku deploy (#105)
* wip

* wip

* wip

* wip

* wip

* wip
2022-04-04 12:25:19 +02:00
Adrian Hopek
b52d206554
change order of release phase commands (#104) 2022-04-04 08:39:25 +02:00
dependabot[bot]
172eab162d
#5 - (php) Bump laravel/tinker from 2.7.1 to 2.7.2 (#102)
* #5 - (php) Bump laravel/tinker from 2.7.1 to 2.7.2

Bumps [laravel/tinker](https://github.com/laravel/tinker) from 2.7.1 to 2.7.2.
- [Release notes](https://github.com/laravel/tinker/releases)
- [Changelog](https://github.com/laravel/tinker/blob/2.x/CHANGELOG.md)
- [Commits](https://github.com/laravel/tinker/compare/v2.7.1...v2.7.2)

---
updated-dependencies:
- dependency-name: laravel/tinker
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* #5 - bump dependencies

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Adrian Hopek <adrian.hopek@blumilk.pl>
2022-04-01 08:35:14 +02:00
Krzysztof Rewak
794e8df3ea
#5 - update codestyle to 1.x (#101)
* #5 - update codestyle to 1.x

* #5 - update codestyle to 1.x
2022-03-31 14:16:14 +02:00
Adrian Hopek
b49fcadbba
#99 - ui changes (#100)
* #99 - ui changes

* #99 - logo fix

* #99 - tailwind plugin for eslint

* #99 - fix

* #99 - fix

* #99 - fix pagination

* #99 - fix logo

Co-authored-by: EwelinaLasowy <ewelina.lasowy@blumilk.pl>
2022-03-31 10:12:33 +02:00
Adrian Hopek
08421b8a69
- small changes (#98)
* - added some test

* - cr fix

* wip

* wip

* Update resources/js/Shared/MainMenu.vue

Co-authored-by: Ewelina Lasowy <56546832+EwelinaLasowy@users.noreply.github.com>

* fix

Co-authored-by: EwelinaLasowy <ewelina.lasowy@blumilk.pl>
Co-authored-by: Ewelina Lasowy <56546832+EwelinaLasowy@users.noreply.github.com>
2022-03-30 10:33:18 +02:00
Ewelina Lasowy
ab16af1ca9
- Permissions tests (#97)
* - added some test

* - cr fix

* - cr fix

* - cr fix

* - cr fix
2022-03-30 09:54:29 +02:00
Ewelina Lasowy
fdbc374d7e
#93 - custom error pages (#94)
* #93 - wip

* #93 - wip

* #93 - fix if statement

* #93 - wip

* #93 - added default error page

* #93 - fix to error page

* #93 - fix linter

* #93 - delete unnecessary file

* #93 - added EOL

* #93 - fix

* #93 - cr fix

Co-authored-by: Adrian Hopek <adrian.hopek@blumilk.pl>
2022-03-30 09:06:25 +02:00
Adrian Hopek
720d2c4e7b
#95 - unavailable days (#96)
* #95 - unavailable days

* #95 - fix

* #95 - fix
2022-03-28 14:05:59 +02:00
Adrian Hopek
957b07b3eb
#44 - vacation summary of all employees (#92)
* #44 - ui for summary

* #44 - vacation monthly usage

* #44 - fix

* #44 - fix

Co-authored-by: EwelinaLasowy <ewelina.lasowy@blumilk.pl>
2022-03-24 10:33:34 +01:00
Adrian Hopek
dcda8c6255
- vue composition api (#91)
* wip

* fix

Co-authored-by: EwelinaLasowy <ewelina.lasowy@blumilk.pl>
2022-03-22 15:03:42 +01:00
Adrian Hopek
95f5ed44d6
- actions and notifications refactor (#88)
* wip

* fix

* fix

* fix

* add test

* fix

* wip

* fix

* fix translations

Co-authored-by: EwelinaLasowy <ewelina.lasowy@blumilk.pl>
2022-03-21 15:29:20 +01:00
Adrian Hopek
d8ac2bd61f
#84 - fix remembering user (#89)
* #84 - fix remembering user

* #84 - ecs fix

* #84 - fix

* #84 - fix test

* #84 - wip

* #84 - add comment to observer

* #84 - cr fix

* Apply suggestions from code review

Co-authored-by: Krzysztof Rewak <krzysztof.rewak@blumilk.pl>

* #84 - cr fix

Co-authored-by: Krzysztof Rewak <krzysztof.rewak@blumilk.pl>
2022-03-21 14:36:18 +01:00
Adrian Hopek
a0e60a3160
#74 - vacation calendar (#87)
* - polishing calendar

* wip

* wip

* #74 - wip

* #74 - wip

* #74 - wip

* #74 - fix icons

Co-authored-by: EwelinaLasowy <ewelina.lasowy@blumilk.pl>
2022-03-21 11:57:32 +01:00
Adrian Hopek
8c1819aa01
#85 - google calendar improvements (#86)
* google calendar improvements

* fix

* change vacation request name

* #85 - google calendar improvements

* #85 - fix

* #85 - fix

* #85 - fix
2022-03-18 08:11:34 +01:00
Adrian Hopek
afb1a5e9ff
#73 - cancel vacation request (#83)
* #73 - cancel vacation request

* #73 - fix

* #73 - changed text for cancelling vacation request

Co-authored-by: EwelinaLasowy <ewelina.lasowy@blumilk.pl>
2022-03-17 09:33:29 +01:00
Ewelina Lasowy
43870fa060
#72 - UX improvement (#81)
* #72 - UX improvement

* #72 - wip

* #72 - UX improvement fix

Co-authored-by: Adrian Hopek <adrian.hopek@blumilk.pl>
2022-03-16 14:07:27 +01:00
Adrian Hopek
0076c04e88
#76 - fix menu active status (#80) 2022-03-16 11:53:16 +01:00
Adrian Hopek
287c6c19ab
#75 - fix data leak to ui-avatars (#79)
* #75 - fix data leak to ui-avatars

* #75 - ecs fix

* #75 - new colors

* #75 - change font color

* #75 - ecs fix

* #75 - change colors order

* #75 - fix

* #75 - fix

* #75 - ecs fix

Co-authored-by: EwelinaLasowy <ewelina.lasowy@blumilk.pl>
2022-03-16 11:53:05 +01:00
Adrian Hopek
8a54403318
#77 - center Google "G" icon (#78) 2022-03-16 09:10:24 +01:00
Adrian Hopek
6d62c8b776
#48 - heroku deployment (#70)
* test

* #48 - deployment

* #48 - fixes

* #48 - prod assets

* #48 - readme for heroku deployment

* #48 - fix

* #48 - ecs fix

* #48 - fix

* #48 - fix

* #48 - cr fix

* #48 - remove predis dependency
2022-03-15 14:46:42 +01:00
298 changed files with 14876 additions and 6693 deletions

View File

@ -57,6 +57,13 @@ DOCKER_INSTALL_XDEBUG=false
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_CALENDAR_ENABLED=true
GOOGLE_REDIRECT=http://localhost/login/google/end
GOOGLE_CALENDAR_ID=
LOCAL_EMAIL_FOR_LOGIN_VIA_GOOGLE=
SLACK_URL=https://slack.com/api
SLACK_ENABLED=true
SLACK_CLIENT_TOKEN=
SLACK_SIGNING_SECRET=
SLACK_DEFAULT_CHANNEL="#general"

View File

@ -13,5 +13,8 @@ module.exports = {
indent: ['error', 2],
'vue/html-indent': ['error', 2],
'comma-dangle': ['error', 'always-multiline'],
'object-curly-spacing': ['error', 'always'],
'vue/require-default-prop': 0,
'vue/multi-word-component-names': 0,
},
}

1
.github/CODEOWNERS vendored Normal file
View File

@ -0,0 +1 @@
* @blumilksoftware/toby

17
.github/workflows/deploy.yml vendored Normal file
View File

@ -0,0 +1,17 @@
name: Deploy
on:
push:
tags:
- v*
jobs:
deploy:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: akhileshns/heroku-deploy@v3.12.12
with:
heroku_api_key: ${{secrets.HEROKU_API_KEY}}
heroku_app_name: ${{secrets.HEROKU_APP_NAME}}
heroku_email: ${{secrets.HEROKU_EMAIL}}

View File

@ -45,7 +45,7 @@ jobs:
run: composer install --prefer-dist --no-interaction --no-suggest
- name: Run PHP linter
run: composer ecs
run: composer cs
- name: Execute tests
run: php artisan test --env=ci

1
.gitignore vendored
View File

@ -15,5 +15,6 @@ Homestead.json
Homestead.yaml
npm-debug.log
yarn-error.log
google-credentials.json
.idea/
.composer

3
Procfile Normal file
View File

@ -0,0 +1,3 @@
web: vendor/bin/heroku-php-nginx -C environment/prod/nginx.conf public/
release: php artisan config:cache && php artisan route:cache && php artisan migrate --force
worker: php artisan queue:work

View File

@ -5,6 +5,9 @@ declare(strict_types=1);
namespace Toby\Architecture;
use Illuminate\Foundation\Exceptions\Handler;
use Inertia\Inertia;
use Symfony\Component\HttpFoundation\Response;
use Throwable;
class ExceptionHandler extends Handler
{
@ -13,4 +16,36 @@ class ExceptionHandler extends Handler
"password",
"password_confirmation",
];
protected array $handleByInertia = [
Response::HTTP_INTERNAL_SERVER_ERROR,
Response::HTTP_SERVICE_UNAVAILABLE,
Response::HTTP_TOO_MANY_REQUESTS,
419, // CSRF
Response::HTTP_NOT_FOUND,
Response::HTTP_FORBIDDEN,
Response::HTTP_UNAUTHORIZED,
];
public function render($request, Throwable $e): Response
{
$response = parent::render($request, $e);
if (!app()->environment("production")) {
return $response;
}
if ($response->status() === Response::HTTP_METHOD_NOT_ALLOWED) {
$response->setStatusCode(Response::HTTP_NOT_FOUND);
}
if (in_array($response->status(), $this->handleByInertia, true)) {
return Inertia::render("Error", [
"status" => $response->status(),
])
->toResponse($request)
->setStatusCode($response->status());
}
return $response;
}
}

View File

@ -4,13 +4,24 @@ declare(strict_types=1);
namespace Toby\Architecture\Providers;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Notifications\ChannelManager;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\ServiceProvider;
use Toby\Infrastructure\Slack\Channels\SlackApiChannel;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
Notification::resolved(function (ChannelManager $service): void {
$service->extend("slack", fn(Application $app): SlackApiChannel => $app->make(SlackApiChannel::class));
});
}
public function boot(): void
{
Carbon::macro("toDisplayString", fn() => $this->translatedFormat("d.m.Y"));
Carbon::macro("toDisplayString", fn(): string => $this->translatedFormat("d.m.Y"));
}
}

View File

@ -7,7 +7,9 @@ namespace Toby\Architecture\Providers;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
use Toby\Domain\Enums\Role;
use Toby\Domain\Policies\KeyPolicy;
use Toby\Domain\Policies\VacationRequestPolicy;
use Toby\Eloquent\Models\Key;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\VacationRequest;
@ -15,6 +17,7 @@ class AuthServiceProvider extends ServiceProvider
{
protected $policies = [
VacationRequest::class => VacationRequestPolicy::class,
Key::class => KeyPolicy::class,
];
public function boot(): void
@ -27,9 +30,11 @@ class AuthServiceProvider extends ServiceProvider
}
});
Gate::define("manageUsers", fn(User $user) => $user->role === Role::AdministrativeApprover);
Gate::define("manageHolidays", fn(User $user) => $user->role === Role::AdministrativeApprover);
Gate::define("manageVacationLimits", fn(User $user) => $user->role === Role::AdministrativeApprover);
Gate::define("generateTimesheet", fn(User $user) => $user->role === Role::AdministrativeApprover);
Gate::define("manageUsers", fn(User $user): bool => $user->role === Role::AdministrativeApprover);
Gate::define("manageHolidays", fn(User $user): bool => $user->role === Role::AdministrativeApprover);
Gate::define("manageVacationLimits", fn(User $user): bool => $user->role === Role::AdministrativeApprover);
Gate::define("generateTimesheet", fn(User $user): bool => $user->role === Role::AdministrativeApprover);
Gate::define("listMonthlyUsage", fn(User $user): bool => $user->role === Role::AdministrativeApprover);
Gate::define("manageResumes", fn(User $user): bool => $user->role === Role::TechnicalApprover);
}
}

View File

@ -5,39 +5,8 @@ declare(strict_types=1);
namespace Toby\Architecture\Providers;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Toby\Domain\Events\VacationRequestAcceptedByAdministrative;
use Toby\Domain\Events\VacationRequestAcceptedByTechnical;
use Toby\Domain\Events\VacationRequestApproved;
use Toby\Domain\Events\VacationRequestCancelled;
use Toby\Domain\Events\VacationRequestCreated;
use Toby\Domain\Events\VacationRequestRejected;
use Toby\Domain\Events\VacationRequestStateChanged;
use Toby\Domain\Events\VacationRequestWaitsForAdminApproval;
use Toby\Domain\Events\VacationRequestWaitsForTechApproval;
use Toby\Domain\Listeners\CreateVacationRequestActivity;
use Toby\Domain\Listeners\HandleAcceptedByAdministrativeVacationRequest;
use Toby\Domain\Listeners\HandleAcceptedByTechnicalVacationRequest;
use Toby\Domain\Listeners\HandleApprovedVacationRequest;
use Toby\Domain\Listeners\HandleCancelledVacationRequest;
use Toby\Domain\Listeners\HandleCreatedVacationRequest;
use Toby\Domain\Listeners\SendApprovedVacationRequestNotification;
use Toby\Domain\Listeners\SendCancelledVacationRequestNotification;
use Toby\Domain\Listeners\SendCreatedVacationRequestNotification;
use Toby\Domain\Listeners\SendRejectedVacationRequestNotification;
use Toby\Domain\Listeners\SendWaitedForAdministrativeVacationRequestNotification;
use Toby\Domain\Listeners\SendWaitedForTechnicalVacationRequestNotification;
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
VacationRequestStateChanged::class => [CreateVacationRequestActivity::class],
VacationRequestCreated::class => [HandleCreatedVacationRequest::class, SendCreatedVacationRequestNotification::class],
VacationRequestAcceptedByTechnical::class => [HandleAcceptedByTechnicalVacationRequest::class],
VacationRequestAcceptedByAdministrative::class => [HandleAcceptedByAdministrativeVacationRequest::class],
VacationRequestApproved::class => [HandleApprovedVacationRequest::class, SendApprovedVacationRequestNotification::class],
VacationRequestRejected::class => [SendRejectedVacationRequestNotification::class],
VacationRequestCancelled::class => [HandleCancelledVacationRequest::class, SendCancelledVacationRequestNotification::class],
VacationRequestWaitsForTechApproval::class => [SendWaitedForTechnicalVacationRequestNotification::class],
VacationRequestWaitsForAdminApproval::class => [SendWaitedForAdministrativeVacationRequestNotification::class],
];
protected $listen = [];
}

View File

@ -7,17 +7,14 @@ namespace Toby\Architecture\Providers;
use Illuminate\Support\ServiceProvider;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\VacationRequest;
use Toby\Eloquent\Models\YearPeriod;
use Toby\Eloquent\Observers\UserObserver;
use Toby\Eloquent\Observers\VacationRequestObserver;
use Toby\Eloquent\Observers\YearPeriodObserver;
class ObserverServiceProvider extends ServiceProvider
{
public function boot(): void
{
User::observe(UserObserver::class);
YearPeriod::observe(YearPeriodObserver::class);
VacationRequest::observe(VacationRequestObserver::class);
}
}

View File

@ -28,6 +28,6 @@ class RouteServiceProvider extends ServiceProvider
protected function configureRateLimiting(): void
{
RateLimiter::for("api", fn(Request $request) => Limit::perMinute(60)->by(optional($request->user())->id ?: $request->ip()));
RateLimiter::for("api", fn(Request $request): Limit => Limit::perMinute(60)->by(optional($request->user())->id ?: $request->ip()));
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Actions;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\YearPeriod;
class CreateUserAction
{
public function execute(array $userData, array $profileData): User
{
$user = new User($userData);
$user->save();
$user->profile()->create($profileData);
$this->createVacationLimitsFor($user);
return $user;
}
protected function createVacationLimitsFor(User $user): void
{
$yearPeriods = YearPeriod::all();
foreach ($yearPeriods as $yearPeriod) {
$user->vacationLimits()->create([
"year_period_id" => $yearPeriod->id,
]);
}
}
}

View File

@ -2,22 +2,30 @@
declare(strict_types=1);
namespace Toby\Eloquent\Observers;
namespace Toby\Domain\Actions;
use Toby\Domain\PolishHolidaysRetriever;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\YearPeriod;
class YearPeriodObserver
class CreateYearPeriodAction
{
public function __construct(
protected PolishHolidaysRetriever $polishHolidaysRetriever,
) {}
public function created(YearPeriod $yearPeriod): void
public function execute(int $year): YearPeriod
{
$yearPeriod = new YearPeriod([
"year" => $year,
]);
$yearPeriod->save();
$this->createVacationLimitsFor($yearPeriod);
$this->createHolidaysFor($yearPeriod);
return $yearPeriod;
}
protected function createVacationLimitsFor(YearPeriod $yearPeriod): void

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Actions;
use Toby\Eloquent\Models\User;
class UpdateUserAction
{
public function execute(User $user, array $userData, array $profileData): User
{
$user->update($userData);
$user->profile->update($profileData);
return $user;
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Actions\VacationRequest;
use Toby\Domain\VacationRequestStateManager;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\VacationRequest;
class AcceptAsAdministrativeAction
{
public function __construct(
protected VacationRequestStateManager $stateManager,
protected ApproveAction $approveAction,
) {}
public function execute(VacationRequest $vacationRequest, User $user): void
{
$this->stateManager->acceptAsAdministrative($vacationRequest, $user);
$this->approveAction->execute($vacationRequest);
}
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Actions\VacationRequest;
use Toby\Domain\VacationRequestStateManager;
use Toby\Domain\VacationTypeConfigRetriever;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\VacationRequest;
class AcceptAsTechnicalAction
{
public function __construct(
protected VacationRequestStateManager $stateManager,
protected VacationTypeConfigRetriever $configRetriever,
protected WaitForAdminApprovalAction $waitForAdminApprovalAction,
protected ApproveAction $approveAction,
) {}
public function execute(VacationRequest $vacationRequest, User $user): void
{
$this->stateManager->acceptAsTechnical($vacationRequest, $user);
if ($this->configRetriever->needsAdministrativeApproval($vacationRequest->type)) {
$this->waitForAdminApprovalAction->execute($vacationRequest);
return;
}
$this->approveAction->execute($vacationRequest);
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Actions\VacationRequest;
use Toby\Domain\Enums\Role;
use Toby\Domain\Notifications\VacationRequestStatusChangedNotification;
use Toby\Domain\VacationRequestStateManager;
use Toby\Domain\VacationTypeConfigRetriever;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\VacationRequest;
use Toby\Infrastructure\Jobs\SendVacationRequestDaysToGoogleCalendar;
class ApproveAction
{
public function __construct(
protected VacationRequestStateManager $stateManager,
protected VacationTypeConfigRetriever $configRetriever,
) {}
public function execute(VacationRequest $vacationRequest, ?User $user = null): void
{
$this->stateManager->approve($vacationRequest, $user);
if ($this->configRetriever->isVacation($vacationRequest->type)) {
SendVacationRequestDaysToGoogleCalendar::dispatch($vacationRequest);
$this->notify($vacationRequest);
}
}
protected function notify(VacationRequest $vacationRequest): void
{
$users = User::query()
->where("id", "!=", $vacationRequest->user->id)
->whereIn("role", [Role::TechnicalApprover, Role::AdministrativeApprover, Role::Administrator])
->get();
foreach ($users as $user) {
$user->notify(new VacationRequestStatusChangedNotification($vacationRequest, $user));
}
$vacationRequest->user->notify(new VacationRequestStatusChangedNotification($vacationRequest, $vacationRequest->user));
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Actions\VacationRequest;
use Toby\Domain\Enums\Role;
use Toby\Domain\Notifications\VacationRequestStatusChangedNotification;
use Toby\Domain\VacationRequestStateManager;
use Toby\Domain\VacationTypeConfigRetriever;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\VacationRequest;
use Toby\Infrastructure\Jobs\ClearVacationRequestDaysInGoogleCalendar;
class CancelAction
{
public function __construct(
protected VacationRequestStateManager $stateManager,
protected VacationTypeConfigRetriever $configRetriever,
) {}
public function execute(VacationRequest $vacationRequest, User $user): void
{
$this->stateManager->cancel($vacationRequest, $user);
ClearVacationRequestDaysInGoogleCalendar::dispatch($vacationRequest);
if ($this->configRetriever->isVacation($vacationRequest->type)) {
$this->notify($vacationRequest);
}
}
protected function notify(VacationRequest $vacationRequest): void
{
$users = User::query()
->where("id", "!=", $vacationRequest->user->id)
->whereIn("role", [Role::TechnicalApprover, Role::AdministrativeApprover, Role::Administrator])
->get();
foreach ($users as $user) {
$user->notify(new VacationRequestStatusChangedNotification($vacationRequest, $user));
}
$vacationRequest->user->notify(new VacationRequestStatusChangedNotification($vacationRequest, $vacationRequest->user));
}
}

View File

@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Actions\VacationRequest;
use Illuminate\Validation\ValidationException;
use Toby\Domain\Notifications\VacationRequestCreatedNotification;
use Toby\Domain\VacationRequestStateManager;
use Toby\Domain\VacationTypeConfigRetriever;
use Toby\Domain\Validation\VacationRequestValidator;
use Toby\Domain\WorkDaysCalculator;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\VacationRequest;
class CreateAction
{
public function __construct(
protected VacationRequestStateManager $stateManager,
protected VacationRequestValidator $vacationRequestValidator,
protected VacationTypeConfigRetriever $configRetriever,
protected WorkDaysCalculator $workDaysCalculator,
protected WaitForTechApprovalAction $waitForTechApprovalAction,
protected WaitForAdminApprovalAction $waitForAdminApprovalAction,
protected ApproveAction $approveAction,
) {}
/**
* @throws ValidationException
*/
public function execute(array $data, User $creator): VacationRequest
{
$vacationRequest = $this->createVacationRequest($data, $creator);
$this->handleCreatedVacationRequest($vacationRequest);
if ($this->configRetriever->isVacation($vacationRequest->type)) {
$this->notify($vacationRequest);
}
return $vacationRequest;
}
/**
* @throws ValidationException
*/
protected function createVacationRequest(array $data, User $creator): VacationRequest
{
/** @var VacationRequest $vacationRequest */
$vacationRequest = $creator->createdVacationRequests()->make($data);
$this->vacationRequestValidator->validate($vacationRequest);
$vacationRequest->save();
$days = $this->workDaysCalculator->calculateDays($vacationRequest->from, $vacationRequest->to);
foreach ($days as $day) {
$vacationRequest->vacations()->create([
"date" => $day,
"user_id" => $vacationRequest->user->id,
"year_period_id" => $vacationRequest->yearPeriod->id,
]);
}
$this->stateManager->markAsCreated($vacationRequest);
return $vacationRequest;
}
protected function handleCreatedVacationRequest(VacationRequest $vacationRequest): void
{
if ($vacationRequest->hasFlowSkipped()) {
$this->approveAction->execute($vacationRequest);
return;
}
if ($this->configRetriever->needsTechnicalApproval($vacationRequest->type)) {
$this->waitForTechApprovalAction->execute($vacationRequest);
return;
}
if ($this->configRetriever->needsAdministrativeApproval($vacationRequest->type)) {
$this->waitForAdminApprovalAction->execute($vacationRequest);
return;
}
$this->stateManager->approve($vacationRequest);
}
protected function notify(VacationRequest $vacationRequest): void
{
$vacationRequest->user->notify(new VacationRequestCreatedNotification($vacationRequest));
}
}

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Actions\VacationRequest;
use Toby\Domain\Enums\Role;
use Toby\Domain\Notifications\VacationRequestStatusChangedNotification;
use Toby\Domain\VacationRequestStateManager;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\VacationRequest;
class RejectAction
{
public function __construct(
protected VacationRequestStateManager $stateManager,
) {}
public function execute(VacationRequest $vacationRequest, User $user): void
{
$this->stateManager->reject($vacationRequest, $user);
$this->notify($vacationRequest);
}
protected function notify(VacationRequest $vacationRequest): void
{
$users = User::query()
->where("id", "!=", $vacationRequest->user->id)
->whereIn("role", [Role::TechnicalApprover, Role::AdministrativeApprover, Role::Administrator])
->get();
foreach ($users as $user) {
$user->notify(new VacationRequestStatusChangedNotification($vacationRequest, $user));
}
$vacationRequest->user->notify(new VacationRequestStatusChangedNotification($vacationRequest, $vacationRequest->user));
}
}

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Actions\VacationRequest;
use Toby\Domain\Enums\Role;
use Toby\Domain\Notifications\VacationRequestWaitsForApprovalNotification;
use Toby\Domain\VacationRequestStateManager;
use Toby\Domain\VacationTypeConfigRetriever;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\VacationRequest;
class WaitForAdminApprovalAction
{
public function __construct(
protected VacationRequestStateManager $stateManager,
protected VacationTypeConfigRetriever $configRetriever,
protected ApproveAction $approveAction,
) {}
public function execute(VacationRequest $vacationRequest): void
{
$this->stateManager->waitForAdministrative($vacationRequest);
if ($this->configRetriever->isVacation($vacationRequest->type)) {
$this->notifyAdminApprovers($vacationRequest);
}
}
protected function notifyAdminApprovers(VacationRequest $vacationRequest): void
{
$users = User::query()
->whereIn("role", [Role::AdministrativeApprover, Role::Administrator])
->get();
foreach ($users as $user) {
$user->notify(new VacationRequestWaitsForApprovalNotification($vacationRequest, $user));
}
}
}

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Actions\VacationRequest;
use Toby\Domain\Enums\Role;
use Toby\Domain\Notifications\VacationRequestWaitsForApprovalNotification;
use Toby\Domain\VacationRequestStateManager;
use Toby\Domain\VacationTypeConfigRetriever;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\VacationRequest;
class WaitForTechApprovalAction
{
public function __construct(
protected VacationRequestStateManager $stateManager,
protected VacationTypeConfigRetriever $configRetriever,
protected ApproveAction $approveAction,
) {}
public function execute(VacationRequest $vacationRequest): void
{
$this->stateManager->waitForTechnical($vacationRequest);
if ($this->configRetriever->isVacation($vacationRequest->type)) {
$this->notifyTechApprovers($vacationRequest);
}
}
protected function notifyTechApprovers(VacationRequest $vacationRequest): void
{
$users = User::query()
->whereIn("role", [Role::TechnicalApprover, Role::Administrator])
->get();
foreach ($users as $user) {
$user->notify(new VacationRequestWaitsForApprovalNotification($vacationRequest, $user));
}
}
}

View File

@ -5,7 +5,6 @@ declare(strict_types=1);
namespace Toby\Domain;
use Carbon\CarbonPeriod;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Toby\Eloquent\Helpers\YearPeriodRetriever;
@ -44,6 +43,7 @@ class CalendarGenerator
"isWeekend" => $day->isWeekend(),
"isHoliday" => $holidays->contains($day),
"vacations" => $vacationsForDay->pluck("user_id"),
"vacationTypes" => $vacationsForDay->pluck("vacationRequest.type", "user_id"),
];
}
@ -54,8 +54,9 @@ class CalendarGenerator
{
return Vacation::query()
->whereBetween("date", [$period->start, $period->end])
->whereRelation("vacationRequest", fn(Builder $query) => $query->states(VacationRequestStatesRetriever::successStates()))
->approved()
->with("vacationRequest")
->get()
->groupBy(fn(Vacation $vacation) => $vacation->date->toDateString());
->groupBy(fn(Vacation $vacation): string => $vacation->date->toDateString());
}
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Toby\Domain;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Toby\Domain\Enums\VacationType;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\Vacation;
class DailySummaryRetriever
{
public function __construct(
protected VacationTypeConfigRetriever $configRetriever,
) {}
public function getAbsences(Carbon $date): Collection
{
return Vacation::query()
->with(["user", "vacationRequest"])
->whereDate("date", $date)
->approved()
->whereTypes(
VacationType::all()->filter(fn(VacationType $type): bool => $this->configRetriever->isVacation($type)),
)
->get();
}
public function getRemoteDays(Carbon $date): Collection
{
return Vacation::query()
->with(["user", "vacationRequest"])
->whereDate("date", $date)
->approved()
->whereTypes(
VacationType::all()->filter(fn(VacationType $type): bool => !$this->configRetriever->isVacation($type)),
)
->get();
}
public function getBirthdays(Carbon $date): Collection
{
return User::query()
->whereRelation("profile", "birthday", $date)
->get();
}
}

View File

@ -21,7 +21,7 @@ enum EmploymentForm: string
$cases = collect(EmploymentForm::cases());
return $cases->map(
fn(EmploymentForm $enum) => [
fn(EmploymentForm $enum): array => [
"label" => $enum->label(),
"value" => $enum->value,
],

View File

@ -21,7 +21,7 @@ enum Role: string
$cases = collect(Role::cases());
return $cases->map(
fn(Role $enum) => [
fn(Role $enum): array => [
"label" => $enum->label(),
"value" => $enum->value,
],

View File

@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Toby\Domain\Enums;
use Illuminate\Support\Collection;
enum VacationType: string
{
case Vacation = "vacation";
@ -15,6 +17,8 @@ enum VacationType: string
case Volunteering = "volunteering_vacation";
case TimeInLieu = "time_in_lieu";
case Sick = "sick_vacation";
case Absence = "absence";
case HomeOffice = "home_office";
public function label(): string
{
@ -23,13 +27,18 @@ enum VacationType: string
public static function casesToSelect(): array
{
$cases = collect(VacationType::cases());
$cases = VacationType::all();
return $cases->map(
fn(VacationType $enum) => [
fn(VacationType $enum): array => [
"label" => $enum->label(),
"value" => $enum->value,
],
)->toArray();
}
public static function all(): Collection
{
return new Collection(VacationType::cases());
}
}

View File

@ -1,19 +0,0 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Toby\Eloquent\Models\VacationRequest;
class VacationRequestAcceptedByAdministrative
{
use Dispatchable;
use SerializesModels;
public function __construct(
public VacationRequest $vacationRequest,
) {}
}

View File

@ -1,19 +0,0 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Toby\Eloquent\Models\VacationRequest;
class VacationRequestAcceptedByTechnical
{
use Dispatchable;
use SerializesModels;
public function __construct(
public VacationRequest $vacationRequest,
) {}
}

View File

@ -1,19 +0,0 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Toby\Eloquent\Models\VacationRequest;
class VacationRequestApproved
{
use Dispatchable;
use SerializesModels;
public function __construct(
public VacationRequest $vacationRequest,
) {}
}

View File

@ -1,19 +0,0 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Toby\Eloquent\Models\VacationRequest;
class VacationRequestCancelled
{
use Dispatchable;
use SerializesModels;
public function __construct(
public VacationRequest $vacationRequest,
) {}
}

View File

@ -1,19 +0,0 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Toby\Eloquent\Models\VacationRequest;
class VacationRequestCreated
{
use Dispatchable;
use SerializesModels;
public function __construct(
public VacationRequest $vacationRequest,
) {}
}

View File

@ -1,19 +0,0 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Toby\Eloquent\Models\VacationRequest;
class VacationRequestRejected
{
use Dispatchable;
use SerializesModels;
public function __construct(
public VacationRequest $vacationRequest,
) {}
}

View File

@ -1,24 +0,0 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Toby\Domain\States\VacationRequest\VacationRequestState;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\VacationRequest;
class VacationRequestStateChanged
{
use Dispatchable;
use SerializesModels;
public function __construct(
public VacationRequest $vacationRequest,
public ?VacationRequestState $from,
public VacationRequestState $to,
public ?User $user = null,
) {}
}

View File

@ -1,19 +0,0 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Toby\Eloquent\Models\VacationRequest;
class VacationRequestWaitsForAdminApproval
{
use Dispatchable;
use SerializesModels;
public function __construct(
public VacationRequest $vacationRequest,
) {}
}

View File

@ -1,19 +0,0 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Toby\Eloquent\Models\VacationRequest;
class VacationRequestWaitsForTechApproval
{
use Dispatchable;
use SerializesModels;
public function __construct(
public VacationRequest $vacationRequest,
) {}
}

View File

@ -1,19 +0,0 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Listeners;
use Toby\Domain\Events\VacationRequestStateChanged;
class CreateVacationRequestActivity
{
public function handle(VacationRequestStateChanged $event): void
{
$event->vacationRequest->activities()->create([
"from" => $event->from,
"to" => $event->to,
"user_id" => $event->user?->id,
]);
}
}

View File

@ -1,20 +0,0 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Listeners;
use Toby\Domain\Events\VacationRequestAcceptedByAdministrative;
use Toby\Domain\VacationRequestStateManager;
class HandleAcceptedByAdministrativeVacationRequest
{
public function __construct(
protected VacationRequestStateManager $stateManager,
) {}
public function handle(VacationRequestAcceptedByAdministrative $event): void
{
$this->stateManager->approve($event->vacationRequest);
}
}

View File

@ -1,30 +0,0 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Listeners;
use Toby\Domain\Events\VacationRequestAcceptedByTechnical;
use Toby\Domain\VacationRequestStateManager;
use Toby\Domain\VacationTypeConfigRetriever;
class HandleAcceptedByTechnicalVacationRequest
{
public function __construct(
protected VacationTypeConfigRetriever $configRetriever,
protected VacationRequestStateManager $stateManager,
) {}
public function handle(VacationRequestAcceptedByTechnical $event): void
{
$vacationRequest = $event->vacationRequest;
if ($this->configRetriever->needsAdministrativeApproval($vacationRequest->type)) {
$this->stateManager->waitForAdministrative($vacationRequest);
return;
}
$this->stateManager->approve($vacationRequest);
}
}

View File

@ -1,16 +0,0 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Listeners;
use Toby\Domain\Events\VacationRequestApproved;
use Toby\Infrastructure\Jobs\SendVacationRequestDaysToGoogleCalendar;
class HandleApprovedVacationRequest
{
public function handle(VacationRequestApproved $event): void
{
SendVacationRequestDaysToGoogleCalendar::dispatch($event->vacationRequest);
}
}

View File

@ -1,16 +0,0 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Listeners;
use Toby\Domain\Events\VacationRequestCancelled;
use Toby\Infrastructure\Jobs\ClearVacationRequestDaysInGoogleCalendar;
class HandleCancelledVacationRequest
{
public function handle(VacationRequestCancelled $event): void
{
ClearVacationRequestDaysInGoogleCalendar::dispatch($event->vacationRequest);
}
}

View File

@ -1,42 +0,0 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Listeners;
use Toby\Domain\Events\VacationRequestCreated;
use Toby\Domain\VacationRequestStateManager;
use Toby\Domain\VacationTypeConfigRetriever;
class HandleCreatedVacationRequest
{
public function __construct(
protected VacationTypeConfigRetriever $configRetriever,
protected VacationRequestStateManager $stateManager,
) {}
public function handle(VacationRequestCreated $event): void
{
$vacationRequest = $event->vacationRequest;
if ($vacationRequest->hasFlowSkipped()) {
$this->stateManager->approve($vacationRequest);
return;
}
if ($this->configRetriever->needsTechnicalApproval($vacationRequest->type)) {
$this->stateManager->waitForTechnical($vacationRequest);
return;
}
if ($this->configRetriever->needsAdministrativeApproval($vacationRequest->type)) {
$this->stateManager->waitForAdministrative($vacationRequest);
return;
}
$this->stateManager->approve($vacationRequest);
}
}

View File

@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Listeners;
use Illuminate\Support\Collection;
use Toby\Domain\Enums\Role;
use Toby\Domain\Events\VacationRequestApproved;
use Toby\Domain\Notifications\VacationRequestApprovedNotification;
use Toby\Eloquent\Models\User;
class SendApprovedVacationRequestNotification
{
public function __construct(
) {}
public function handle(VacationRequestApproved $event): void
{
foreach ($this->getUsersForNotifications() as $user) {
$user->notify(new VacationRequestApprovedNotification($event->vacationRequest, $user));
}
$event->vacationRequest->user->notify(new VacationRequestApprovedNotification($event->vacationRequest, $event->vacationRequest->user));
}
protected function getUsersForNotifications(): Collection
{
return User::query()
->whereIn("role", [Role::TechnicalApprover, Role::AdministrativeApprover])
->get();
}
}

View File

@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Listeners;
use Illuminate\Support\Collection;
use Toby\Domain\Enums\Role;
use Toby\Domain\Events\VacationRequestCancelled;
use Toby\Domain\Notifications\VacationRequestCancelledNotification;
use Toby\Eloquent\Models\User;
class SendCancelledVacationRequestNotification
{
public function __construct(
) {}
public function handle(VacationRequestCancelled $event): void
{
foreach ($this->getUsersForNotifications() as $user) {
$user->notify(new VacationRequestCancelledNotification($event->vacationRequest, $user));
}
$event->vacationRequest->user->notify(new VacationRequestCancelledNotification($event->vacationRequest, $event->vacationRequest->user));
}
protected function getUsersForNotifications(): Collection
{
return User::query()
->whereIn("role", [Role::TechnicalApprover, Role::AdministrativeApprover])
->get();
}
}

View File

@ -1,26 +0,0 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Listeners;
use Toby\Domain\Events\VacationRequestCreated;
use Toby\Domain\Notifications\VacationRequestCreatedNotification;
use Toby\Domain\Notifications\VacationRequestCreatedOnEmployeeBehalf;
class SendCreatedVacationRequestNotification
{
public function __construct(
) {}
public function handle(VacationRequestCreated $event): void
{
$vacationRequest = $event->vacationRequest;
if ($vacationRequest->creator->is($vacationRequest->user)) {
$vacationRequest->user->notify(new VacationRequestCreatedNotification($vacationRequest));
} else {
$vacationRequest->user->notify(new VacationRequestCreatedOnEmployeeBehalf($vacationRequest));
}
}
}

View File

@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Listeners;
use Illuminate\Support\Collection;
use Toby\Domain\Enums\Role;
use Toby\Domain\Events\VacationRequestRejected;
use Toby\Domain\Notifications\VacationRequestRejectedNotification;
use Toby\Eloquent\Models\User;
class SendRejectedVacationRequestNotification
{
public function __construct(
) {}
public function handle(VacationRequestRejected $event): void
{
foreach ($this->getUsersForNotifications() as $user) {
$user->notify(new VacationRequestRejectedNotification($event->vacationRequest, $user));
}
$event->vacationRequest->user->notify(new VacationRequestRejectedNotification($event->vacationRequest, $event->vacationRequest->user));
}
protected function getUsersForNotifications(): Collection
{
return User::query()
->whereIn("role", [Role::TechnicalApprover, Role::AdministrativeApprover])
->get();
}
}

View File

@ -1,31 +0,0 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Listeners;
use Illuminate\Support\Collection;
use Toby\Domain\Enums\Role;
use Toby\Domain\Events\VacationRequestWaitsForAdminApproval;
use Toby\Domain\Notifications\VacationRequestWaitsForAdminApprovalNotification;
use Toby\Eloquent\Models\User;
class SendWaitedForAdministrativeVacationRequestNotification
{
public function __construct(
) {}
public function handle(VacationRequestWaitsForAdminApproval $event): void
{
foreach ($this->getUsersForNotifications() as $user) {
$user->notify(new VacationRequestWaitsForAdminApprovalNotification($event->vacationRequest, $user));
}
}
protected function getUsersForNotifications(): Collection
{
return User::query()
->where("role", [Role::AdministrativeApprover])
->get();
}
}

View File

@ -1,31 +0,0 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Listeners;
use Illuminate\Support\Collection;
use Toby\Domain\Enums\Role;
use Toby\Domain\Events\VacationRequestWaitsForTechApproval;
use Toby\Domain\Notifications\VacationRequestWaitsForTechApprovalNotification;
use Toby\Eloquent\Models\User;
class SendWaitedForTechnicalVacationRequestNotification
{
public function __construct(
) {}
public function handle(VacationRequestWaitsForTechApproval $event): void
{
foreach ($this->getUsersForNotifications() as $user) {
$user->notify(new VacationRequestWaitsForTechApprovalNotification($event->vacationRequest, $user));
}
}
protected function getUsersForNotifications(): Collection
{
return User::query()
->where("role", [Role::TechnicalApprover])
->get();
}
}

View File

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Notifications;
class Channels
{
public const MAIL = "mail";
public const SLACK = "slack";
}

View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Toby\Eloquent\Models\User;
use Toby\Infrastructure\Slack\Elements\SlackMessage;
class KeyHasBeenGivenNotification extends Notification
{
use Queueable;
public function __construct(
protected User $sender,
protected User $recipient,
) {}
public function via(): array
{
return [Channels::SLACK];
}
public function toSlack(Notifiable $notifiable): SlackMessage
{
return (new SlackMessage())
->text(__(":sender gives key no :key to :recipient", [
"sender" => $this->getName($this->sender),
"recipient" => $this->getName($this->recipient),
"key" => $notifiable->id,
]));
}
protected function getName(User $user): string
{
if ($user->profile->slack_id !== null) {
return "<@{$user->profile->slack_id}>";
}
return $user->profile->full_name;
}
}

View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Toby\Eloquent\Models\User;
use Toby\Infrastructure\Slack\Elements\SlackMessage;
class KeyHasBeenTakenNotification extends Notification
{
use Queueable;
public function __construct(
protected User $recipient,
protected User $sender,
) {}
public function via(): array
{
return [Channels::SLACK];
}
public function toSlack(Notifiable $notifiable): SlackMessage
{
return (new SlackMessage())
->text(__(":recipient takes key no :key from :sender", [
"recipient" => $this->getName($this->recipient),
"sender" => $this->getName($this->sender),
"key" => $notifiable->id,
]));
}
protected function getName(User $user): string
{
if ($user->profile->slack_id !== null) {
return "<@{$user->profile->slack_id}>";
}
return $user->profile->full_name;
}
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Notifications;
interface Notifiable
{
public function notify($instance);
}

View File

@ -9,6 +9,7 @@ use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use InvalidArgumentException;
use Toby\Eloquent\Models\VacationRequest;
use Toby\Infrastructure\Slack\Elements\SlackMessage;
class VacationRequestCreatedNotification extends Notification
{
@ -20,7 +21,16 @@ class VacationRequestCreatedNotification extends Notification
public function via(): array
{
return ["mail"];
return [Channels::MAIL, Channels::SLACK];
}
public function toSlack(): SlackMessage
{
$url = route("vacation.requests.show", ["vacationRequest" => $this->vacationRequest->id]);
$seeDetails = __("See details");
return (new SlackMessage())
->text("{$this->buildDescription()}\n <${url}|${seeDetails}>");
}
/**
@ -39,33 +49,64 @@ class VacationRequestCreatedNotification extends Notification
protected function buildMailMessage(string $url): MailMessage
{
$user = $this->vacationRequest->user->first_name;
$title = $this->vacationRequest->name;
$user = $this->vacationRequest->user->profile->first_name;
$type = $this->vacationRequest->type->label();
$from = $this->vacationRequest->from->toDisplayString();
$to = $this->vacationRequest->to->toDisplayString();
$days = $this->vacationRequest->vacations()->count();
$appName = config("app.name");
return (new MailMessage())
->greeting(__("Hi :user!", [
"user" => $user,
]))
->subject(__("Vacation request :title has been created", [
"title" => $title,
]))
->line(__("The vacation request :title has been created correctly in the :appName.", [
"title" => $title,
"appName" => $appName,
]))
->line(__("Vacation type: :type", [
"type" => $type,
]))
->line(__("From :from to :to (number of days: :days)", [
"from" => $from,
"to" => $to,
"days" => $days,
]))
->greeting(
__("Hi :user!", [
"user" => $user,
]),
)
->subject($this->buildSubject())
->line($this->buildDescription())
->line(
__("Vacation type: :type", [
"type" => $type,
]),
)
->line(
__("From :from to :to (number of days: :days)", [
"from" => $from,
"to" => $to,
"days" => $days,
]),
)
->action(__("Click here for details"), $url);
}
protected function buildSubject(): string
{
$name = $this->vacationRequest->name;
if ($this->vacationRequest->creator()->is($this->vacationRequest->user)) {
return __("Vacation request :title has been created", [
"title" => $name,
]);
}
return __("Vacation request :title has been created on your behalf", [
"title" => $name,
]);
}
protected function buildDescription(): string
{
$name = $this->vacationRequest->name;
if ($this->vacationRequest->creator()->is($this->vacationRequest->user)) {
return __("The vacation request :title has been created successfully.", [
"requester" => $this->vacationRequest->user->profile->full_name,
"title" => $name,
]);
}
return __("The vacation request :title has been created successfully by user :creator on your behalf.", [
"title" => $this->vacationRequest->name,
"creator" => $this->vacationRequest->creator->profile->full_name,
]);
}
}

View File

@ -1,73 +0,0 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use InvalidArgumentException;
use Toby\Eloquent\Models\VacationRequest;
class VacationRequestCreatedOnEmployeeBehalf extends Notification
{
use Queueable;
public function __construct(
protected VacationRequest $vacationRequest,
) {}
public function via(): array
{
return ["mail"];
}
/**
* @throws InvalidArgumentException
*/
public function toMail(): MailMessage
{
$url = route(
"vacation.requests.show",
[
"vacationRequest" => $this->vacationRequest,
],
);
return $this->buildMailMessage($url);
}
protected function buildMailMessage(string $url): MailMessage
{
$creator = $this->vacationRequest->creator->fullName;
$user = $this->vacationRequest->user->first_name;
$title = $this->vacationRequest->name;
$type = $this->vacationRequest->type->label();
$from = $this->vacationRequest->from->toDisplayString();
$to = $this->vacationRequest->to->toDisplayString();
$days = $this->vacationRequest->vacations()->count();
$appName = config("app.name");
return (new MailMessage())
->greeting(__("Hi :user!", [
"user" => $user,
]))
->subject(__("Vacation request :title has been created on your behalf", [
"title" => $title,
]))
->line(__("The vacation request :title has been created correctly by user :creator on your behalf in the :appName.", [
"title" => $title,
"appName" => $appName,
"creator" => $creator,
]))
->line(__("Vacation type: :type", [
"type" => $type,
]))
->line(__("From :from to :to (number of days: :days)", [
"from" => $from,
"to" => $to,
"days" => $days,
]))
->action(__("Click here for details"), $url);
}
}

View File

@ -1,74 +0,0 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use InvalidArgumentException;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\VacationRequest;
class VacationRequestRejectedNotification extends Notification
{
use Queueable;
public function __construct(
protected VacationRequest $vacationRequest,
protected User $user,
) {}
public function via(): array
{
return ["mail"];
}
/**
* @throws InvalidArgumentException
*/
public function toMail(): MailMessage
{
$url = route(
"vacation.requests.show",
[
"vacationRequest" => $this->vacationRequest,
],
);
return $this->buildMailMessage($url);
}
protected function buildMailMessage(string $url): MailMessage
{
$user = $this->user->first_name;
$title = $this->vacationRequest->name;
$type = $this->vacationRequest->type->label();
$from = $this->vacationRequest->from->toDisplayString();
$to = $this->vacationRequest->to->toDisplayString();
$days = $this->vacationRequest->vacations()->count();
$requester = $this->vacationRequest->user->fullName;
return (new MailMessage())
->greeting(__("Hi :user!", [
"user" => $user,
]))
->subject(__("Vacation request :title has been rejected", [
"title" => $title,
]))
->line(__("The vacation request :title for user :requester has been rejected.", [
"title" => $title,
"requester" => $requester,
]))
->line(__("Vacation type: :type", [
"type" => $type,
]))
->line(__("From :from to :to (number of days: :days)", [
"from" => $from,
"to" => $to,
"days" => $days,
]))
->action(__("Click here for details"), $url);
}
}

View File

@ -10,8 +10,9 @@ use Illuminate\Notifications\Notification;
use InvalidArgumentException;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\VacationRequest;
use Toby\Infrastructure\Slack\Elements\SlackMessage;
class VacationRequestApprovedNotification extends Notification
class VacationRequestStatusChangedNotification extends Notification
{
use Queueable;
@ -22,7 +23,16 @@ class VacationRequestApprovedNotification extends Notification
public function via(): array
{
return ["mail"];
return [Channels::MAIL, Channels::SLACK];
}
public function toSlack(): SlackMessage
{
$url = route("vacation.requests.show", ["vacationRequest" => $this->vacationRequest->id]);
$seeDetails = __("See details");
return (new SlackMessage())
->text("{$this->buildDescription()}\n <${url}|${seeDetails}>");
}
/**
@ -42,25 +52,18 @@ class VacationRequestApprovedNotification extends Notification
protected function buildMailMessage(string $url): MailMessage
{
$user = $this->user->first_name;
$title = $this->vacationRequest->name;
$user = $this->user->profile->first_name;
$type = $this->vacationRequest->type->label();
$from = $this->vacationRequest->from->toDisplayString();
$to = $this->vacationRequest->to->toDisplayString();
$days = $this->vacationRequest->vacations()->count();
$requester = $this->vacationRequest->user->fullName;
return (new MailMessage())
->greeting(__("Hi :user!", [
"user" => $user,
]))
->subject(__("Vacation request :title has been approved", [
"title" => $title,
]))
->line(__("The vacation request :title for user :requester has been approved.", [
"title" => $title,
"requester" => $requester,
]))
->subject($this->buildSubject())
->line($this->buildDescription())
->line(__("Vacation type: :type", [
"type" => $type,
]))
@ -71,4 +74,21 @@ class VacationRequestApprovedNotification extends Notification
]))
->action(__("Click here for details"), $url);
}
protected function buildSubject(): string
{
return __("Vacation request :title has been :status", [
"title" => $this->vacationRequest->name,
"status" => $this->vacationRequest->state->label(),
]);
}
protected function buildDescription(): string
{
return __("The vacation request :title from user :requester has been :status.", [
"title" => $this->vacationRequest->name,
"requester" => $this->vacationRequest->user->profile->full_name,
"status" => $this->vacationRequest->state->label(),
]);
}
}

View File

@ -1,74 +0,0 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use InvalidArgumentException;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\VacationRequest;
class VacationRequestWaitsForAdminApprovalNotification extends Notification
{
use Queueable;
public function __construct(
protected VacationRequest $vacationRequest,
protected User $user,
) {}
public function via(): array
{
return ["mail"];
}
/**
* @throws InvalidArgumentException
*/
public function toMail(): MailMessage
{
$url = route(
"vacation.requests.show",
[
"vacationRequest" => $this->vacationRequest,
],
);
return $this->buildMailMessage($url);
}
protected function buildMailMessage(string $url): MailMessage
{
$user = $this->user->first_name;
$requester = $this->vacationRequest->user->fullName;
$title = $this->vacationRequest->name;
$type = $this->vacationRequest->type->label();
$from = $this->vacationRequest->from->toDisplayString();
$to = $this->vacationRequest->to->toDisplayString();
$days = $this->vacationRequest->vacations()->count();
return (new MailMessage())
->greeting(__("Hi :user!", [
"user" => $user,
]))
->subject(__("Vacation request :title is waiting for your approval", [
"title" => $title,
]))
->line(__("The vacation request :title from user: :requester is waiting for your approval.", [
"title" => $title,
"requester" => $requester,
]))
->line(__("Vacation type: :type", [
"type" => $type,
]))
->line(__("From :from to :to (number of days: :days)", [
"from" => $from,
"to" => $to,
"days" => $days,
]))
->action(__("Click here for details"), $url);
}
}

View File

@ -8,10 +8,12 @@ use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use InvalidArgumentException;
use Toby\Domain\States\VacationRequest\WaitingForTechnical;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\VacationRequest;
use Toby\Infrastructure\Slack\Elements\SlackMessage;
class VacationRequestCancelledNotification extends Notification
class VacationRequestWaitsForApprovalNotification extends Notification
{
use Queueable;
@ -22,7 +24,16 @@ class VacationRequestCancelledNotification extends Notification
public function via(): array
{
return ["mail"];
return [Channels::MAIL, Channels::SLACK];
}
public function toSlack(): SlackMessage
{
$url = route("vacation.requests.show", ["vacationRequest" => $this->vacationRequest->id]);
$seeDetails = __("See details");
return (new SlackMessage())
->text("{$this->buildDescription()}\n <${url}|${seeDetails}>");
}
/**
@ -42,25 +53,18 @@ class VacationRequestCancelledNotification extends Notification
protected function buildMailMessage(string $url): MailMessage
{
$user = $this->user->first_name;
$title = $this->vacationRequest->name;
$user = $this->user->profile->first_name;
$type = $this->vacationRequest->type->label();
$from = $this->vacationRequest->from->toDisplayString();
$to = $this->vacationRequest->to->toDisplayString();
$days = $this->vacationRequest->vacations()->count();
$requester = $this->vacationRequest->user->fullName;
return (new MailMessage())
->greeting(__("Hi :user!", [
"user" => $user,
]))
->subject(__("Vacation request :title has been cancelled", [
"title" => $title,
]))
->line(__("The vacation request :title for user :requester has been cancelled.", [
"title" => $title,
"requester" => $requester,
]))
->subject($this->buildSubject())
->line($this->buildDescription())
->line(__("Vacation type: :type", [
"type" => $type,
]))
@ -71,4 +75,37 @@ class VacationRequestCancelledNotification extends Notification
]))
->action(__("Click here for details"), $url);
}
protected function buildSubject(): string
{
$title = $this->vacationRequest->name;
if ($this->vacationRequest->state->equals(WaitingForTechnical::class)) {
return __("Vacation request :title is waiting for your technical approval", [
"title" => $title,
]);
}
return __("Vacation request :title is waiting for your administrative approval", [
"title" => $title,
]);
}
protected function buildDescription(): string
{
$title = $this->vacationRequest->name;
$requester = $this->vacationRequest->user->profile->full_name;
if ($this->vacationRequest->state->equals(WaitingForTechnical::class)) {
return __("The vacation request :title from user :requester is waiting for your technical approval.", [
"title" => $title,
"requester" => $requester,
]);
}
return __("The vacation request :title from user :requester is waiting for your administrative approval.", [
"title" => $title,
"requester" => $requester,
]);
}
}

View File

@ -1,74 +0,0 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use InvalidArgumentException;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\VacationRequest;
class VacationRequestWaitsForTechApprovalNotification extends Notification
{
use Queueable;
public function __construct(
protected VacationRequest $vacationRequest,
protected User $user,
) {}
public function via(): array
{
return ["mail"];
}
/**
* @throws InvalidArgumentException
*/
public function toMail(): MailMessage
{
$url = route(
"vacation.requests.show",
[
"vacationRequest" => $this->vacationRequest,
],
);
return $this->buildMailMessage($url);
}
protected function buildMailMessage(string $url): MailMessage
{
$user = $this->user->first_name;
$requester = $this->vacationRequest->user->fullName;
$title = $this->vacationRequest->name;
$type = $this->vacationRequest->type->label();
$from = $this->vacationRequest->from->toDisplayString();
$to = $this->vacationRequest->to->toDisplayString();
$days = $this->vacationRequest->vacations()->count();
return (new MailMessage())
->greeting(__("Hi :user!", [
"user" => $user,
]))
->subject(__("Vacation request :title is waiting for your approval", [
"title" => $title,
]))
->line(__("The vacation request :title from user: :requester is waiting for your approval.", [
"title" => $title,
"requester" => $requester,
]))
->line(__("Vacation type: :type", [
"type" => $type,
]))
->line(__("From :from to :to (number of days: :days)", [
"from" => $from,
"to" => $to,
"days" => $days,
]))
->action(__("Click here for details"), $url);
}
}

View File

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Toby\Infrastructure\Slack\Elements\SlackMessage;
use Toby\Infrastructure\Slack\Elements\VacationRequestsAttachment;
class VacationRequestsSummaryNotification extends Notification
{
use Queueable;
public function __construct(
protected Carbon $day,
protected Collection $vacationRequests,
) {}
public function via(): array
{
return [Channels::MAIL, Channels::SLACK];
}
public function toSlack(): SlackMessage
{
return (new SlackMessage())
->text(__("Requests wait for your approval - status for day :date:", ["date" => $this->day->toDisplayString()]))
->withAttachment(new VacationRequestsAttachment($this->vacationRequests));
}
public function toMail(Notifiable $notifiable): MailMessage
{
$url = route(
"vacation.requests.indexForApprovers",
[
"status" => "waiting_for_action",
],
);
return $this->buildMailMessage($notifiable, $url);
}
protected function buildMailMessage(Notifiable $notifiable, string $url): MailMessage
{
$user = $notifiable->profile->first_name;
$message = (new MailMessage())
->greeting(
__("Hi :user!", [
"user" => $user,
]),
)
->line (__("Requests list waits for your approval - status for day :date:", ["date" => $this->day->toDisplayString()]))
->subject(__("Requests wait for your approval - status for day :date:", ["date" => $this->day->toDisplayString()]));
foreach ($this->vacationRequests as $request) {
$url = route("vacation.requests.show", ["vacationRequest" => $request->id]);
$message->line(
__("- [request no. :request](:url) of user :user (:startDate - :endDate)", ["request" => $request->name, "url" => $url, "user" => $request->user->profile->full_name, "startDate" => $request->from->toDisplayString(), "endDate" => $request->to->toDisplayString()]),
);
}
return $message
->action(__("Go to requests"), $url);
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Policies;
use Toby\Domain\Enums\Role;
use Toby\Eloquent\Models\Key;
use Toby\Eloquent\Models\User;
class KeyPolicy
{
public function manage(User $user): bool
{
return $user->role === Role::AdministrativeApprover;
}
public function give(User $user, Key $key): bool
{
if ($key->user()->is($user)) {
return true;
}
return $user->role === Role::AdministrativeApprover;
}
}

View File

@ -5,6 +5,9 @@ declare(strict_types=1);
namespace Toby\Domain\Policies;
use Toby\Domain\Enums\Role;
use Toby\Domain\States\VacationRequest\Created;
use Toby\Domain\States\VacationRequest\WaitingForAdministrative;
use Toby\Domain\States\VacationRequest\WaitingForTechnical;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\VacationRequest;
@ -40,8 +43,16 @@ class VacationRequestPolicy
return in_array($user->role, [Role::AdministrativeApprover, Role::TechnicalApprover], true);
}
public function cancel(User $user): bool
public function cancel(User $user, VacationRequest $vacationRequest): bool
{
if ($vacationRequest->user->is($user) && $vacationRequest->state->equals(
Created::class,
WaitingForAdministrative::class,
WaitingForTechnical::class,
)) {
return true;
}
return $user->role === Role::AdministrativeApprover;
}

View File

@ -26,7 +26,7 @@ class PolishHolidaysRetriever
protected function prepareHolidays(array $holidays): Collection
{
return collect($holidays)->map(fn(Holiday $holiday) => [
return collect($holidays)->map(fn(Holiday $holiday): array => [
"name" => $holiday->getName([static::LANG_KEY]),
"date" => Carbon::createFromTimestamp($holiday->getTimestamp()),
])->values();

View File

@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace Toby\Domain;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use PhpOffice\PhpWord\TemplateProcessor;
use Toby\Eloquent\Models\Resume;
class ResumeGenerator
{
public function generate(Resume $resume): string
{
$processor = new TemplateProcessor($this->getTemplate());
$processor->setValue("id", $resume->id);
$processor->setValue("name", $resume->user ? $resume->user->profile->full_name : $resume->name);
$this->fillTechnologies($processor, $resume);
$this->fillLanguages($processor, $resume);
$this->fillEducation($processor, $resume);
$this->fillProjects($processor, $resume);
return $processor->save();
}
public function getTemplate(): string
{
return resource_path("views/docx/resume_eng.docx");
}
protected function fillTechnologies(TemplateProcessor $processor, Resume $resume): void
{
$processor->cloneBlock("technologies", 0, true, false, $this->getTechnologies($resume));
}
protected function fillLanguages(TemplateProcessor $processor, Resume $resume): void
{
$processor->cloneBlock("languages", 0, true, false, $this->getLanguages($resume));
}
protected function fillEducation(TemplateProcessor $processor, Resume $resume): void
{
$processor->cloneBlock("education", 0, true, false, $this->getEducation($resume));
}
protected function fillProjects(TemplateProcessor $processor, Resume $resume): void
{
$processor->cloneBlock("projects", $resume->projects->count(), true, true);
foreach ($resume->projects as $index => $project) {
++$index;
$processor->setValues($this->getProject($project, $index));
$processor->cloneBlock("project_technologies#{$index}", 0, true, false, $this->getProjectTechnologies($project, $index));
}
}
protected function getProject(array $project, int $index): array
{
return [
"index#{$index}" => $index,
"start_date#{$index}" => Carbon::createFromFormat("m/Y", $project["startDate"])->format("n.Y"),
"end_date#{$index}" => $project["current"] ? "present" : Carbon::createFromFormat("m/Y", $project["endDate"])->format("n.Y"),
"description#{$index}" => $project["description"],
"tasks#{$index}" => $this->withNewLines($project["tasks"]),
];
}
protected function withNewLines(string $text): string
{
return Str::replace("\n", "</w:t><w:br/><w:t>", $text);
}
protected function getProjectTechnologies(array $project, int $index): array
{
$technologies = new Collection($project["technologies"] ?? []);
return $technologies->map(fn(string $name) => [
"technology#{$index}" => $name,
])->all();
}
protected function getTechnologies(Resume $resume): array
{
return $resume->technologies->map(fn(array $technology): array => [
"technology_name" => $technology["name"],
"technology_level" => __("resume.technology_levels.{$technology["level"]}"),
])->all();
}
protected function getLanguages(Resume $resume): array
{
return $resume->languages->map(fn(array $language): array => [
"language_name" => $language["name"],
"language_level" => __("resume.language_levels.{$language["level"]}"),
])->all();
}
protected function getEducation(Resume $resume): array
{
return $resume->education->map(fn(array $project, int $index): array => [
"start_date" => Carbon::createFromFormat("m/Y", $project["startDate"])->format("n.Y"),
"end_date" => $project["current"] ? "present" : Carbon::createFromFormat("m/Y", $project["endDate"])->format("n.Y"),
"school" => $project["school"],
"field_of_study" => $project["fieldOfStudy"],
"degree" => $project["degree"],
])->all();
}
}

View File

@ -36,4 +36,9 @@ abstract class VacationRequestState extends State
Approved::class,
], Cancelled::class);
}
public function label(): string
{
return __(static::$name);
}
}

View File

@ -4,20 +4,21 @@ declare(strict_types=1);
namespace Toby\Domain;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
use Toby\Eloquent\Models\User;
class TimesheetExport implements WithMultipleSheets
{
protected Collection $users;
protected Collection $types;
protected Carbon $month;
public function sheets(): array
{
return $this->users
->map(fn(User $user) => new TimesheetPerUserSheet($user, $this->month))
->map(fn(User $user): TimesheetPerUserSheet => new TimesheetPerUserSheet($user, $this->month, $this->types))
->toArray();
}
@ -34,4 +35,11 @@ class TimesheetExport implements WithMultipleSheets
return $this;
}
public function forVacationTypes(Collection $types): static
{
$this->types = $types;
return $this;
}
}

View File

@ -7,6 +7,7 @@ namespace Toby\Domain;
use Carbon\CarbonInterface;
use Carbon\CarbonPeriod;
use Generator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\FromGenerator;
@ -25,7 +26,6 @@ use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Toby\Domain\Enums\VacationType;
use Toby\Domain\States\VacationRequest\Approved;
use Toby\Eloquent\Models\Holiday;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\Vacation;
@ -41,17 +41,16 @@ class TimesheetPerUserSheet implements WithTitle, WithHeadings, WithEvents, With
public function __construct(
protected User $user,
protected Carbon $month,
protected Collection $types,
) {}
public function title(): string
{
return $this->user->fullName;
return $this->user->profile->full_name;
}
public function headings(): array
{
$types = VacationType::cases();
$headings = [
__("Date"),
__("Day of week"),
@ -60,7 +59,7 @@ class TimesheetPerUserSheet implements WithTitle, WithHeadings, WithEvents, With
__("Worked hours"),
];
foreach ($types as $type) {
foreach ($this->types as $type) {
$headings[] = $type->label();
}
@ -188,13 +187,14 @@ class TimesheetPerUserSheet implements WithTitle, WithHeadings, WithEvents, With
{
return $user->vacations()
->with("vacationRequest")
->whereRelation("vacationRequest", fn(Builder $query): Builder => $query->whereIn("type", $this->types))
->whereBetween("date", [$period->start, $period->end])
->whereRelation("vacationRequest", "state", Approved::$name)
->approved()
->get()
->groupBy(
[
fn(Vacation $vacation) => $vacation->date->toDateString(),
fn(Vacation $vacation) => $vacation->vacationRequest->type->value,
fn(Vacation $vacation): string => $vacation->date->toDateString(),
fn(Vacation $vacation): string => $vacation->vacationRequest->type->value,
],
);
}

View File

@ -5,9 +5,10 @@ declare(strict_types=1);
namespace Toby\Domain;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Collection;
use Toby\Domain\Enums\VacationType;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\Vacation;
use Toby\Eloquent\Models\YearPeriod;
class UserVacationStatsRetriever
@ -20,24 +21,39 @@ class UserVacationStatsRetriever
{
return $user
->vacations()
->where("year_period_id", $yearPeriod->id)
->whereBelongsTo($yearPeriod)
->whereRelation(
"vacationRequest",
fn(Builder $query) => $query
fn(Builder $query): Builder => $query
->whereIn("type", $this->getLimitableVacationTypes())
->states(VacationRequestStatesRetriever::successStates()),
)
->count();
}
public function getUsedVacationDaysByMonth(User $user, YearPeriod $yearPeriod): Collection
{
return $user->vacations()
->whereBelongsTo($yearPeriod)
->whereRelation(
"vacationRequest",
fn(Builder $query): Builder => $query
->whereIn("type", $this->getLimitableVacationTypes())
->states(VacationRequestStatesRetriever::successStates()),
)
->get()
->groupBy(fn(Vacation $vacation): string => strtolower($vacation->date->englishMonth))
->map(fn(Collection $items): int => $items->count());
}
public function getPendingVacationDays(User $user, YearPeriod $yearPeriod): int
{
return $user
->vacations()
->where("year_period_id", $yearPeriod->id)
->whereBelongsTo($yearPeriod)
->whereRelation(
"vacationRequest",
fn(Builder $query) => $query
fn(Builder $query): Builder => $query
->whereIn("type", $this->getLimitableVacationTypes())
->states(VacationRequestStatesRetriever::pendingStates()),
)
@ -48,16 +64,26 @@ class UserVacationStatsRetriever
{
return $user
->vacations()
->where("year_period_id", $yearPeriod->id)
->whereBelongsTo($yearPeriod)
->whereRelation(
"vacationRequest",
fn(Builder $query) => $query
fn(Builder $query): Builder => $query
->whereIn("type", $this->getNotLimitableVacationTypes())
->whereNot("type", VacationType::HomeOffice)
->states(VacationRequestStatesRetriever::successStates()),
)
->count();
}
public function getHomeOfficeDays(User $user, YearPeriod $yearPeriod): int
{
return $user
->vacations()
->whereBelongsTo($yearPeriod)
->whereRelation("vacationRequest", "type", VacationType::HomeOffice)
->count();
}
public function getRemainingVacationDays(User $user, YearPeriod $yearPeriod): int
{
$limit = $this->getVacationDaysLimit($user, $yearPeriod);
@ -70,24 +96,24 @@ class UserVacationStatsRetriever
public function getVacationDaysLimit(User $user, YearPeriod $yearPeriod): int
{
$limit = $user->vacationLimits()
->where("year_period_id", $yearPeriod->id)
->whereBelongsTo($yearPeriod)
->first()
->days;
?->days;
return $limit ?? 0;
}
protected function getLimitableVacationTypes(): Collection
{
$types = new Collection(VacationType::cases());
$types = VacationType::all();
return $types->filter(fn(VacationType $type) => $this->configRetriever->hasLimit($type));
return $types->filter(fn(VacationType $type): bool => $this->configRetriever->hasLimit($type));
}
protected function getNotLimitableVacationTypes(): Collection
{
$types = new Collection(VacationType::cases());
$types = VacationType::all();
return $types->filter(fn(VacationType $type) => !$this->configRetriever->hasLimit($type));
return $types->filter(fn(VacationType $type): bool => !$this->configRetriever->hasLimit($type));
}
}

View File

@ -4,17 +4,7 @@ declare(strict_types=1);
namespace Toby\Domain;
use Illuminate\Contracts\Auth\Factory as Auth;
use Illuminate\Contracts\Events\Dispatcher;
use Toby\Domain\Events\VacationRequestAcceptedByAdministrative;
use Toby\Domain\Events\VacationRequestAcceptedByTechnical;
use Toby\Domain\Events\VacationRequestApproved;
use Toby\Domain\Events\VacationRequestCancelled;
use Toby\Domain\Events\VacationRequestCreated;
use Toby\Domain\Events\VacationRequestRejected;
use Toby\Domain\Events\VacationRequestStateChanged;
use Toby\Domain\Events\VacationRequestWaitsForAdminApproval;
use Toby\Domain\Events\VacationRequestWaitsForTechApproval;
use Toby\Domain\States\VacationRequest\AcceptedByAdministrative;
use Toby\Domain\States\VacationRequest\AcceptedByTechnical;
use Toby\Domain\States\VacationRequest\Approved;
@ -29,63 +19,47 @@ use Toby\Eloquent\Models\VacationRequest;
class VacationRequestStateManager
{
public function __construct(
protected Auth $auth,
protected Dispatcher $dispatcher,
) {}
public function markAsCreated(VacationRequest $vacationRequest, ?User $user = null): void
public function markAsCreated(VacationRequest $vacationRequest): void
{
$this->fireStateChangedEvent($vacationRequest, null, $vacationRequest->state, $user);
$this->dispatcher->dispatch(new VacationRequestCreated($vacationRequest));
$this->createActivity($vacationRequest, null, $vacationRequest->state, $vacationRequest->creator);
}
public function approve(VacationRequest $vacationRequest, ?User $user = null): void
{
$this->changeState($vacationRequest, Approved::class, $user);
$this->dispatcher->dispatch(new VacationRequestApproved($vacationRequest));
}
public function reject(VacationRequest $vacationRequest, ?User $user = null): void
public function reject(VacationRequest $vacationRequest, User $user): void
{
$this->changeState($vacationRequest, Rejected::class, $user);
$this->dispatcher->dispatch(new VacationRequestRejected($vacationRequest));
}
public function cancel(VacationRequest $vacationRequest, ?User $user = null): void
public function cancel(VacationRequest $vacationRequest, User $user): void
{
$this->changeState($vacationRequest, Cancelled::class, $user);
$this->dispatcher->dispatch(new VacationRequestCancelled($vacationRequest));
}
public function acceptAsTechnical(VacationRequest $vacationRequest, ?User $user = null): void
public function acceptAsTechnical(VacationRequest $vacationRequest, User $user): void
{
$this->changeState($vacationRequest, AcceptedByTechnical::class, $user);
$this->dispatcher->dispatch(new VacationRequestAcceptedByTechnical($vacationRequest));
}
public function acceptAsAdministrative(VacationRequest $vacationRequest, ?User $user = null): void
public function acceptAsAdministrative(VacationRequest $vacationRequest, User $user): void
{
$this->changeState($vacationRequest, AcceptedByAdministrative::class, $user);
$this->dispatcher->dispatch(new VacationRequestAcceptedByAdministrative($vacationRequest));
}
public function waitForTechnical(VacationRequest $vacationRequest, ?User $user = null): void
public function waitForTechnical(VacationRequest $vacationRequest): void
{
$this->changeState($vacationRequest, WaitingForTechnical::class, $user);
$this->dispatcher->dispatch(new VacationRequestWaitsForTechApproval($vacationRequest));
$this->changeState($vacationRequest, WaitingForTechnical::class);
}
public function waitForAdministrative(VacationRequest $vacationRequest, ?User $user = null): void
public function waitForAdministrative(VacationRequest $vacationRequest): void
{
$this->changeState($vacationRequest, WaitingForAdministrative::class, $user);
$this->dispatcher->dispatch(new VacationRequestWaitsForAdminApproval($vacationRequest));
$this->changeState($vacationRequest, WaitingForAdministrative::class);
}
protected function changeState(VacationRequest $vacationRequest, string $state, ?User $user = null): void
@ -94,16 +68,19 @@ class VacationRequestStateManager
$vacationRequest->state->transitionTo($state);
$vacationRequest->save();
$this->fireStateChangedEvent($vacationRequest, $previousState, $vacationRequest->state, $user);
$this->createActivity($vacationRequest, $previousState, $vacationRequest->state, $user);
}
protected function fireStateChangedEvent(
protected function createActivity(
VacationRequest $vacationRequest,
?VacationRequestState $from,
VacationRequestState $to,
?User $user = null,
): void {
$event = new VacationRequestStateChanged($vacationRequest, $from, $to, $user);
$this->dispatcher->dispatch($event);
$vacationRequest->activities()->create([
"from" => $from,
"to" => $to,
"user_id" => $user?->id,
]);
}
}

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Toby\Domain;
use Illuminate\Contracts\Config\Repository;
use Toby\Domain\Enums\EmploymentForm;
use Toby\Domain\Enums\VacationType;
class VacationTypeConfigRetriever
@ -13,6 +14,8 @@ class VacationTypeConfigRetriever
public const KEY_ADMINISTRATIVE_APPROVAL = "administrative_approval";
public const KEY_BILLABLE = "billable";
public const KEY_HAS_LIMIT = "has_limit";
public const KEY_AVAILABLE_FOR = "available_for";
public const KEY_IS_VACATION = "is_vacation";
public function __construct(
protected Repository $config,
@ -38,6 +41,16 @@ class VacationTypeConfigRetriever
return $this->getConfigFor($type)[static::KEY_HAS_LIMIT];
}
public function isVacation(VacationType $type): bool
{
return $this->getConfigFor($type)[static::KEY_IS_VACATION];
}
public function isAvailableFor(VacationType $type, EmploymentForm $employmentForm): bool
{
return in_array($employmentForm, $this->getConfigFor($type)[static::KEY_AVAILABLE_FOR], true);
}
protected function getConfigFor(VacationType $type): array
{
return $this->config->get("vacation_types.{$type->value}");

View File

@ -5,11 +5,11 @@ declare(strict_types=1);
namespace Toby\Domain\Validation\Rules;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Collection;
use Toby\Domain\Enums\VacationType;
use Toby\Domain\VacationDaysCalculator;
use Toby\Domain\VacationRequestStatesRetriever;
use Toby\Domain\VacationTypeConfigRetriever;
use Toby\Domain\WorkDaysCalculator;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\VacationRequest;
use Toby\Eloquent\Models\YearPeriod;
@ -18,7 +18,7 @@ class DoesNotExceedLimitRule implements VacationRequestRule
{
public function __construct(
protected VacationTypeConfigRetriever $configRetriever,
protected VacationDaysCalculator $vacationDaysCalculator,
protected WorkDaysCalculator $workDaysCalculator,
) {}
public function check(VacationRequest $vacationRequest): bool
@ -29,7 +29,9 @@ class DoesNotExceedLimitRule implements VacationRequestRule
$limit = $this->getUserVacationLimit($vacationRequest->user, $vacationRequest->yearPeriod);
$vacationDays = $this->getVacationDaysWithLimit($vacationRequest->user, $vacationRequest->yearPeriod);
$estimatedDays = $this->vacationDaysCalculator->calculateDays($vacationRequest->yearPeriod, $vacationRequest->from, $vacationRequest->to)->count();
$estimatedDays = $this->workDaysCalculator
->calculateDays($vacationRequest->from, $vacationRequest->to)
->count();
return $limit >= ($vacationDays + $estimatedDays);
}
@ -41,16 +43,19 @@ class DoesNotExceedLimitRule implements VacationRequestRule
protected function getUserVacationLimit(User $user, YearPeriod $yearPeriod): int
{
return $user->vacationLimits()->where("year_period_id", $yearPeriod->id)->first()->days ?? 0;
return $user->vacationLimits()
->whereBelongsTo($yearPeriod)
->first()
?->days ?? 0;
}
protected function getVacationDaysWithLimit(User $user, YearPeriod $yearPeriod): int
{
return $user->vacations()
->where("year_period_id", $yearPeriod->id)
->whereBelongsTo($yearPeriod)
->whereRelation(
"vacationRequest",
fn(Builder $query) => $query
fn(Builder $query): Builder => $query
->whereIn("type", $this->getLimitableVacationTypes())
->noStates(VacationRequestStatesRetriever::failedStates()),
)
@ -59,8 +64,8 @@ class DoesNotExceedLimitRule implements VacationRequestRule
protected function getLimitableVacationTypes(): Collection
{
$types = new Collection(VacationType::cases());
$types = VacationType::all();
return $types->filter(fn(VacationType $type) => $this->configRetriever->hasLimit($type));
return $types->filter(fn(VacationType $type): bool => $this->configRetriever->hasLimit($type));
}
}

View File

@ -4,19 +4,19 @@ declare(strict_types=1);
namespace Toby\Domain\Validation\Rules;
use Toby\Domain\VacationDaysCalculator;
use Toby\Domain\WorkDaysCalculator;
use Toby\Eloquent\Models\VacationRequest;
class MinimumOneVacationDayRule implements VacationRequestRule
{
public function __construct(
protected VacationDaysCalculator $vacationDaysCalculator,
protected WorkDaysCalculator $workDaysCalculator,
) {}
public function check(VacationRequest $vacationRequest): bool
{
return $this->vacationDaysCalculator
->calculateDays($vacationRequest->yearPeriod, $vacationRequest->from, $vacationRequest->to)
return $this->workDaysCalculator
->calculateDays($vacationRequest->from, $vacationRequest->to)
->isNotEmpty();
}

View File

@ -9,5 +9,6 @@ use Toby\Eloquent\Models\VacationRequest;
interface VacationRequestRule
{
public function check(VacationRequest $vacationRequest): bool;
public function errorMessage(): string;
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Validation\Rules;
use Toby\Domain\Enums\VacationType;
use Toby\Domain\VacationTypeConfigRetriever;
use Toby\Eloquent\Models\VacationRequest;
class VacationTypeCanBeSelected implements VacationRequestRule
{
public function __construct(
protected VacationTypeConfigRetriever $configRetriever,
) {}
public function check(VacationRequest $vacationRequest): bool
{
$employmentForm = $vacationRequest->user->profile->employment_form;
$availableTypes = VacationType::all()
->filter(fn(VacationType $type): bool => $this->configRetriever->isAvailableFor($type, $employmentForm));
return $availableTypes->contains($vacationRequest->type);
}
public function errorMessage(): string
{
return __("You cannot create vacation request of this type.");
}
}

View File

@ -12,6 +12,7 @@ use Toby\Domain\Validation\Rules\NoApprovedVacationRequestsInRange;
use Toby\Domain\Validation\Rules\NoPendingVacationRequestInRange;
use Toby\Domain\Validation\Rules\VacationRangeIsInTheSameYearRule;
use Toby\Domain\Validation\Rules\VacationRequestRule;
use Toby\Domain\Validation\Rules\VacationTypeCanBeSelected;
use Toby\Eloquent\Models\VacationRequest;
class VacationRequestValidator
@ -19,6 +20,7 @@ class VacationRequestValidator
protected array $rules = [
VacationRangeIsInTheSameYearRule::class,
MinimumOneVacationDayRule::class,
VacationTypeCanBeSelected::class,
DoesNotExceedLimitRule::class,
NoPendingVacationRequestInRange::class,
NoApprovedVacationRequestsInRange::class,

View File

@ -9,11 +9,12 @@ use Carbon\CarbonPeriod;
use Illuminate\Support\Collection;
use Toby\Eloquent\Models\YearPeriod;
class VacationDaysCalculator
class WorkDaysCalculator
{
public function calculateDays(YearPeriod $yearPeriod, CarbonInterface $from, CarbonInterface $to): Collection
public function calculateDays(CarbonInterface $from, CarbonInterface $to): Collection
{
$period = CarbonPeriod::create($from, $to);
$yearPeriod = YearPeriod::findByYear($from->year);
$holidays = $yearPeriod->holidays()->pluck("date");
$validDays = new Collection();

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Toby\Eloquent\Helpers;
class ColorGenerator
{
public static function generate(string $text): string
{
$colors = config("colors");
$hash = static::calculateHash($text);
$index = $hash - count($colors) * floor($hash / count($colors));
return $colors[$index];
}
protected static function calculateHash(string $text): int
{
$hash = 0;
if (empty($text)) {
return $hash;
}
for ($i = 0; $i < mb_strlen($text); $i++) {
$hash = abs((int)(($hash << 2) - $hash) + mb_ord($text[$i]));
}
return $hash;
}
}

View File

@ -30,22 +30,20 @@ class YearPeriodRetriever
public function links(): array
{
$current = $this->selected();
$selected = $this->selected();
$current = $this->current();
$years = YearPeriod::query()->whereIn("year", $this->offset($current->year))->get();
$navigation = $years->map(fn(YearPeriod $yearPeriod) => $this->toNavigation($yearPeriod));
$years = YearPeriod::all();
$navigation = $years->map(fn(YearPeriod $yearPeriod): array => $this->toNavigation($yearPeriod));
return [
"current" => $current->year,
"current" => $this->toNavigation($current),
"selected" => $this->toNavigation($selected),
"navigation" => $navigation->toArray(),
];
}
protected function offset(int $year): array
{
return range($year - 2, $year + 2);
}
protected function toNavigation(YearPeriod $yearPeriod): array
{
return [

View File

@ -21,7 +21,6 @@ class Holiday extends Model
use HasFactory;
protected $guarded = [];
protected $casts = [
"date" => "date",
];

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Toby\Eloquent\Models;
use Database\Factories\KeyFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Notifications\Notifiable;
use Toby\Domain\Notifications\Notifiable as NotifiableInterface;
/**
* @property int $id
* @property User $user
*/
class Key extends Model implements NotifiableInterface
{
use HasFactory;
use Notifiable;
protected $guarded = [];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function routeNotificationForSlack(): string
{
return config("services.slack.default_channel");
}
protected static function newFactory(): KeyFactory
{
return KeyFactory::new();
}
}

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Toby\Eloquent\Models;
use Database\Factories\ProfileFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;
use Rackbeat\UIAvatars\HasAvatar;
use Toby\Domain\Enums\EmploymentForm;
use Toby\Eloquent\Helpers\ColorGenerator;
/**
* @property string $first_name
* @property string $last_name
* @property string $position
* @property EmploymentForm $employment_form
* @property Carbon $employment_date
* @property Carbon $birthday
*/
class Profile extends Model
{
use HasFactory;
use HasAvatar;
protected $primaryKey = "user_id";
protected $guarded = [];
protected $casts = [
"employment_form" => EmploymentForm::class,
"employment_date" => "date",
"birthday" => "date",
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function getAvatar(): string
{
return $this->getAvatarGenerator()
->backgroundColor(ColorGenerator::generate($this->full_name))
->image();
}
public function getfullNameAttribute(): string
{
return "{$this->first_name} {$this->last_name}";
}
protected function getAvatarName(): string
{
return mb_substr($this->first_name, 0, 1) . mb_substr($this->last_name, 0, 1);
}
protected static function newFactory(): ProfileFactory
{
return ProfileFactory::new();
}
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Toby\Eloquent\Models;
use Database\Factories\ResumeFactory;
use Illuminate\Database\Eloquent\Casts\AsCollection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Collection;
/**
* @property int $id
* @property ?User $user
* @property string $name
* @property Collection $education
* @property Collection $languages
* @property Collection $technologies
* @property Collection $projects
*/
class Resume extends Model
{
use HasFactory;
protected $guarded = [];
protected $casts = [
"education" => AsCollection::class,
"languages" => AsCollection::class,
"technologies" => AsCollection::class,
"projects" => AsCollection::class,
];
protected $perPage = 50;
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
protected static function newFactory(): ResumeFactory
{
return ResumeFactory::new();
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Toby\Eloquent\Models;
use Database\Factories\TechnologyFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property string $name
*/
class Technology extends Model
{
use HasFactory;
protected $guarded = [];
protected static function newFactory(): TechnologyFactory
{
return TechnologyFactory::new();
}
}

View File

@ -8,47 +8,50 @@ use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Rackbeat\UIAvatars\HasAvatar;
use Toby\Domain\Enums\EmploymentForm;
use Toby\Domain\Enums\Role;
use Toby\Domain\Notifications\Notifiable as NotifiableInterface;
/**
* @property int $id
* @property string $first_name
* @property string $last_name
* @property string $email
* @property string $avatar
* @property string $position
* @property string $password
* @property Role $role
* @property EmploymentForm $employment_form
* @property Carbon $employment_date
* @property Profile $profile
* @property Collection $vacationLimits
* @property Collection $vacationRequests
* @property Collection $vacations
*/
class User extends Authenticatable
class User extends Authenticatable implements NotifiableInterface
{
use HasFactory;
use Notifiable;
use SoftDeletes;
use HasAvatar;
protected $guarded = [];
protected $casts = [
"role" => Role::class,
"last_active_at" => "datetime",
"employment_form" => EmploymentForm::class,
"employment_date" => "date",
];
protected $hidden = [
"remember_token",
];
protected $with = [
"profile",
];
protected $perPage = 50;
public function profile(): HasOne
{
return $this->hasOne(Profile::class);
}
public function vacationLimits(): HasMany
{
@ -70,30 +73,9 @@ class User extends Authenticatable
return $this->hasMany(Vacation::class);
}
public function scopeSearch(Builder $query, ?string $text): Builder
public function keys(): HasMany
{
if ($text === null) {
return $query;
}
return $query
->where("first_name", "ILIKE", $text)
->orWhere("last_name", "ILIKE", $text)
->orWhere("email", "ILIKE", $text);
}
public function getAvatar(): string
{
$colors = config("colors");
return $this->getAvatarGenerator()
->backgroundColor($colors[strlen($this->fullname) % count($colors)])
->image();
}
public function getFullNameAttribute(): string
{
return "{$this->first_name} {$this->last_name}";
return $this->hasMany(Key::class);
}
public function hasRole(Role $role): bool
@ -101,9 +83,50 @@ class User extends Authenticatable
return $this->role === $role;
}
protected function getAvatarNameKey(): string
public function hasVacationLimit(YearPeriod $yearPeriod): bool
{
return "fullName";
return $this->vacationLimits()
->whereBelongsTo($yearPeriod)
->whereNotNull("days")
->exists();
}
public function scopeSearch(Builder $query, ?string $text): Builder
{
if ($text === null) {
return $query;
}
return $query
->where("email", "ILIKE", "%{$text}%")
->orWhereRelation(
"profile",
fn(Builder $query): Builder => $query
->where("first_name", "ILIKE", "%{$text}%")
->orWhere("last_name", "ILIKE", "%{$text}%"),
);
}
public function scopeOrderByProfileField(Builder $query, string $field): Builder
{
$profileQuery = Profile::query()->select($field)->whereColumn("users.id", "profiles.user_id");
return $query->orderBy($profileQuery);
}
public function scopeWithVacationLimitIn(Builder $query, YearPeriod $yearPeriod): Builder
{
return $query->whereRelation(
"vacationlimits",
fn(Builder $query): Builder => $query
->whereBelongsTo($yearPeriod)
->whereNotNull("days"),
);
}
public function routeNotificationForSlack()
{
return $this->profile->slack_id;
}
protected static function newFactory(): UserFactory

View File

@ -4,15 +4,17 @@ declare(strict_types=1);
namespace Toby\Eloquent\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Toby\Domain\VacationRequestStatesRetriever;
/**
* @property int $id
* @property Carbon $date
* @property string $event_id
* @property User $user
* @property VacationRequest $vacationRequest
* @property YearPeriod $yearPeriod
@ -41,4 +43,28 @@ class Vacation extends Model
{
return $this->belongsTo(YearPeriod::class);
}
public function scopeApproved(Builder $query): Builder
{
return $query->whereRelation(
"vacationRequest",
fn(Builder $query): Builder => $query->states(VacationRequestStatesRetriever::successStates()),
);
}
public function scopePending(Builder $query): Builder
{
return $query->whereRelation(
"vacationRequest",
fn(Builder $query): Builder => $query->states(VacationRequestStatesRetriever::pendingStates()),
);
}
public function scopeWhereTypes(Builder $query, Collection $types): Builder
{
return $query->whereRelation(
"vacationRequest",
fn(Builder $query): Builder => $query->whereIn("type", $types),
);
}
}

View File

@ -5,7 +5,6 @@ declare(strict_types=1);
namespace Toby\Eloquent\Models;
use Database\Factories\VacationLimitFactory;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -37,13 +36,6 @@ class VacationLimit extends Model
return $this->belongsTo(YearPeriod::class);
}
public function scopeOrderByUserField(Builder $query, string $field): Builder
{
$userQuery = User::query()->select($field)->whereColumn("vacation_limits.user_id", "users.id");
return $query->orderBy($userQuery);
}
protected static function newFactory(): VacationLimitFactory
{
return VacationLimitFactory::new();

View File

@ -6,10 +6,12 @@ namespace Toby\Eloquent\Models;
use Database\Factories\VacationRequestFactory;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\AsCollection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Spatie\ModelStates\HasStates;
@ -18,6 +20,7 @@ use Toby\Domain\States\VacationRequest\VacationRequestState;
/**
* @property int $id
* @property string $name
* @property VacationType $type
* @property VacationRequestState $state
* @property Carbon $from
@ -29,6 +32,7 @@ use Toby\Domain\States\VacationRequest\VacationRequestState;
* @property YearPeriod $yearPeriod
* @property Collection $activities
* @property Collection $vacations
* @property Collection $event_ids
* @property Carbon $created_at
* @property Carbon $updated_at
*/
@ -38,13 +42,14 @@ class VacationRequest extends Model
use HasStates;
protected $guarded = [];
protected $casts = [
"type" => VacationType::class,
"state" => VacationRequestState::class,
"from" => "date",
"to" => "date",
"event_ids" => AsCollection::class,
];
protected $perPage = 50;
public function user(): BelongsTo
{
@ -81,6 +86,13 @@ class VacationRequest extends Model
return $query->whereNotState("state", $states);
}
public function scopeType(Builder $query, VacationType|array $types): Builder
{
$types = Arr::wrap($types);
return $query->whereIn("type", $types);
}
public function scopeOverlapsWith(Builder $query, self $vacationRequest): Builder
{
return $query->where("from", "<=", $vacationRequest->to)

View File

@ -22,7 +22,6 @@ class VacationRequestActivity extends Model
use HasFactory;
protected $guarded = [];
protected $casts = [
"from" => VacationRequestState::class,
"to" => VacationRequestState::class,

View File

@ -15,6 +15,7 @@ use Illuminate\Support\Collection;
* @property int $id
* @property int $year
* @property Collection $vacationLimits
* @property Collection $vacationRequests
* @property Collection $holidays
*/
class YearPeriod extends Model
@ -41,6 +42,11 @@ class YearPeriod extends Model
return $this->hasMany(VacationLimit::class);
}
public function vacationRequests(): HasMany
{
return $this->hasMany(VacationRequest::class);
}
public function holidays(): HasMany
{
return $this->hasMany(Holiday::class);

View File

@ -4,19 +4,22 @@ declare(strict_types=1);
namespace Toby\Eloquent\Observers;
use Toby\Eloquent\Helpers\YearPeriodRetriever;
use Illuminate\Contracts\Hashing\Hasher;
use Illuminate\Support\Str;
use Toby\Eloquent\Models\User;
class UserObserver
{
public function __construct(
protected YearPeriodRetriever $yearPeriodRetriever,
protected Hasher $hash,
) {}
public function created(User $user): void
public function creating(User $user): void
{
$user->vacationLimits()->create([
"year_period_id" => $this->yearPeriodRetriever->current()->id,
]);
/**
* A random password for user is generated because AuthenticateSession middleware needs a user's password
* for some checks. Users use Google to login, so they don't need to know the password (GitHub issue #84)
*/
$user->password = $this->hash->make(Str::random(40));
}
}

View File

@ -4,25 +4,15 @@ declare(strict_types=1);
namespace Toby\Eloquent\Observers;
use Illuminate\Contracts\Auth\Factory as Auth;
use Illuminate\Contracts\Events\Dispatcher;
use Toby\Eloquent\Models\VacationRequest;
class VacationRequestObserver
{
public function __construct(
protected Auth $auth,
protected Dispatcher $dispatcher,
) {}
public function creating(VacationRequest $vacationRequest): void
{
$year = $vacationRequest->from->year;
$count = $vacationRequest->yearPeriod->vacationRequests()->count();
$number = $count + 1;
$vacationRequestNumber = $vacationRequest->user->vacationRequests()
->whereYear("from", $year)
->count() + 1;
$vacationRequest->name = "{$vacationRequestNumber}/${year}";
$vacationRequest->name = "{$number}/{$vacationRequest->yearPeriod->year}";
}
}

View File

@ -1,22 +0,0 @@
<?php
declare(strict_types=1);
namespace Toby\Eloquent\Scopes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
use Toby\Eloquent\Helpers\YearPeriodRetriever;
class SelectedYearPeriodScope implements Scope
{
public function __construct(
protected YearPeriodRetriever $yearPeriodRetriever,
) {}
public function apply(Builder $builder, Model $model): Builder
{
return $builder->where("year_period_id", $this->yearPeriodRetriever->selected()->id);
}
}

View File

@ -1,25 +0,0 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\Console\Commands;
use Illuminate\Console\Command;
use Toby\Eloquent\Models\User;
class CreateUserCommand extends Command
{
protected $signature = "user:create {email : an email for the user}";
protected $description = "Creates a user";
public function handle(): void
{
$email = $this->argument("email");
User::factory([
"email" => $email,
])->create();
$this->info("The user has been created");
}
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\Console\Commands;
use Illuminate\Console\Command;
use Toby\Eloquent\Models\User;
class MoveUserDataToProfile extends Command
{
protected $signature = "toby:move-user-data-to-profile";
protected $description = "Move user data to their profiles";
public function handle(): void
{
$users = User::all();
foreach ($users as $user) {
$user->profile()->updateOrCreate(["user_id" => $user->id], [
"first_name" => $user->first_name,
"last_name" => $user->last_name,
"position" => $user->position,
"employment_form" => $user->employment_form,
"employment_date" => $user->employment_date,
]);
}
}
}

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\Console\Commands;
use Illuminate\Console\Command;
use Toby\Eloquent\Models\YearPeriod;
class RebuildDocumentNumberingSystem extends Command
{
protected $signature = "toby:rebuild-document-numbering-system";
protected $description = "Rebuilds the document numbering system to {number}/{year}";
public function handle(): void
{
$yearPeriods = YearPeriod::all();
foreach ($yearPeriods as $yearPeriod) {
$number = 1;
$vacationRequests = $yearPeriod
->vacationRequests()
->oldest()
->get();
foreach ($vacationRequests as $vacationRequest) {
$vacationRequest->update(["name" => "{$number}/{$yearPeriod->year}"]);
$number++;
}
}
}
}

View File

@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\Console\Commands;
use Carbon\CarbonInterface;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http;
use Toby\Domain\DailySummaryRetriever;
use Toby\Eloquent\Models\Holiday;
use Toby\Infrastructure\Slack\Elements\AbsencesAttachment;
use Toby\Infrastructure\Slack\Elements\BirthdaysAttachment;
use Toby\Infrastructure\Slack\Elements\RemotesAttachment;
class SendDailySummaryToSlack extends Command
{
protected $signature = "toby:slack:daily-summary {--f|force}";
protected $description = "Sent daily summary to Slack";
public function handle(DailySummaryRetriever $dailySummaryRetriever): void
{
$now = Carbon::today();
if (!$this->option("force") && !$this->shouldHandle($now)) {
return;
}
$attachments = new Collection([
new AbsencesAttachment($dailySummaryRetriever->getAbsences($now)),
new RemotesAttachment($dailySummaryRetriever->getRemoteDays($now)),
new BirthdaysAttachment($dailySummaryRetriever->getBirthdays($now)),
]);
Http::withToken($this->getSlackClientToken())
->post($this->getUrl(), [
"channel" => $this->getSlackChannel(),
"text" => __("Daily summary for day :day", ["day" => $now->toDisplayString()]),
"attachments" => $attachments,
]);
}
protected function shouldHandle(CarbonInterface $day): bool
{
$holidays = Holiday::query()->whereDate("date", $day)->pluck("date");
if ($day->isWeekend()) {
return false;
}
if ($holidays->contains($day)) {
return false;
}
return true;
}
protected function getUrl(): string
{
return "{$this->getSlackBaseUrl()}/chat.postMessage";
}
protected function getSlackBaseUrl(): ?string
{
return config("services.slack.url");
}
protected function getSlackClientToken(): ?string
{
return config("services.slack.client_token");
}
protected function getSlackChannel(): ?string
{
return config("services.slack.default_channel");
}
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Toby\Domain\Enums\Role;
use Toby\Domain\Notifications\VacationRequestsSummaryNotification;
use Toby\Domain\VacationRequestStatesRetriever;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\VacationRequest;
class SendVacationRequestSummariesToApprovers extends Command
{
protected $signature = "toby:send-vacation-request-reminders";
protected $description = "Sends vacation request reminders to approvers if they didn't approve";
public function handle(): void
{
$users = User::query()
->whereIn("role", [Role::AdministrativeApprover, Role::TechnicalApprover, Role::Administrator])
->get();
foreach ($users as $user) {
$vacationRequests = VacationRequest::query()
->states(VacationRequestStatesRetriever::waitingForUserActionStates($user))
->get();
if ($vacationRequests->isNotEmpty()) {
$user->notify(new VacationRequestsSummaryNotification(Carbon::today(), $vacationRequests));
}
}
}
}

View File

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\Http\Controllers;
use Illuminate\Http\Request;
use Inertia\Response;
use Toby\Eloquent\Helpers\YearPeriodRetriever;
use Toby\Eloquent\Models\Holiday;
use Toby\Eloquent\Models\Vacation;
use Toby\Infrastructure\Http\Resources\SimpleVacationRequestResource;
class AnnualSummaryController extends Controller
{
public function __invoke(Request $request, YearPeriodRetriever $yearPeriodRetriever): Response
{
$yearPeriod = $yearPeriodRetriever->selected();
$holidays = $yearPeriod->holidays()
->get();
$vacations = $request->user()
->vacations()
->with("vacationRequest.vacations")
->whereBelongsTo($yearPeriod)
->approved()
->get();
$pendingVacations = $request->user()
->vacations()
->with("vacationRequest.vacations")
->whereBelongsTo($yearPeriod)
->pending()
->get();
return inertia("AnnualSummary", [
"holidays" => $holidays->mapWithKeys(
fn(Holiday $holiday): array => [$holiday->date->toDateString() => $holiday->name],
),
"vacations" => $vacations->mapWithKeys(
fn(Vacation $vacation): array => [
$vacation->date->toDateString() => new SimpleVacationRequestResource($vacation->vacationRequest),
],
),
"pendingVacations" => $pendingVacations->mapWithKeys(
fn(Vacation $vacation): array => [
$vacation->date->toDateString() => new SimpleVacationRequestResource($vacation->vacationRequest),
],
),
]);
}
}

Some files were not shown because too many files have changed in this diff Show More