Compare commits
35 Commits
2.0.24
...
customer-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1cc5f2587 | ||
|
|
6b672a23b4 | ||
|
|
900254b83a | ||
|
|
114617a052 | ||
|
|
38e37f1c2e | ||
|
|
7a5b12762a | ||
|
|
de419350b2 | ||
|
|
a6b237936e | ||
|
|
6e1207578e | ||
|
|
31348da4c0 | ||
|
|
452de88e18 | ||
|
|
18d953e79b | ||
|
|
636422e342 | ||
|
|
20149ae765 | ||
|
|
a392f61d61 | ||
|
|
3e61e0dce6 | ||
|
|
9baa477bed | ||
|
|
4d182ad1a7 | ||
|
|
e77056c4b7 | ||
|
|
d7f1df2987 | ||
|
|
8c1a793b0a | ||
|
|
adc0779912 | ||
|
|
c7bc8fae73 | ||
|
|
0a0ffb4ac9 | ||
|
|
b831532323 | ||
|
|
385753fbc3 | ||
|
|
651f812da8 | ||
|
|
6a99ad41ca | ||
|
|
dd89363c72 | ||
|
|
983bf2c88b | ||
|
|
ce7fe67fcc | ||
|
|
053a1d9017 | ||
|
|
2e2bf986a4 | ||
|
|
6e781b59e2 | ||
|
|
0663995d06 |
14
.env.dist
14
.env.dist
@@ -7,19 +7,19 @@
|
||||
# Configure your database connection and set the correct server version.
|
||||
#
|
||||
# You have to replace the following values with your defaults:
|
||||
# - the version "5.7"
|
||||
# - the version "5.7" (the database server version, examples for MySQL and MariaDB below)
|
||||
# - the database username "user"
|
||||
# - the database password "password"
|
||||
# - the database schema name "database"
|
||||
# - the password "password" for "user"
|
||||
# - the database schema "database"
|
||||
# - you might have to adapt port "3306" and server IP "127.0.0.1" as well
|
||||
#
|
||||
# For MySQL that would be "serverVersion=5.7" as in:
|
||||
# DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8&serverVersion=5.7
|
||||
# DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=5.7
|
||||
#
|
||||
# For MariaDB it would be "serverVersion=mariadb-10.5.8":
|
||||
# DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8&serverVersion=mariadb-10.5.8
|
||||
# For MariaDB it would be "serverVersion=10.5.8-MariaDB":
|
||||
# DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.5.8-MariaDB
|
||||
#
|
||||
DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8&serverVersion=5.7
|
||||
DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.5.8-MariaDB
|
||||
|
||||
#================================================================================
|
||||
# The full documentation can be found at https://www.kimai.org/documentation/emails.html
|
||||
|
||||
42
.github/workflows/website.yaml
vendored
Normal file
42
.github/workflows/website.yaml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: 'Website update'
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
kimai_version:
|
||||
description: 'Kimai version for the website'
|
||||
required: true
|
||||
release:
|
||||
types: [released]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Trigger version update for website
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Determine Version"
|
||||
run: |
|
||||
input="${{ github.event.inputs.kimai_version }}"
|
||||
|
||||
# Determine between manual trigger and release event
|
||||
if [ -z "$input" ]; then
|
||||
echo "No input provided, using release tag"
|
||||
version="${{ github.event.release.tag_name }}"
|
||||
else
|
||||
echo "Using input provided: $input"
|
||||
version="$input"
|
||||
fi
|
||||
|
||||
echo "kimai_version=$version" >> $GITHUB_ENV
|
||||
|
||||
if [[ ! $version =~ ^2\.(0|[1-9]*)(0?)\.(0|[0-9]*)(0?)$ ]]; then
|
||||
echo "Invalid version number: $version"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Emit repository_dispatch
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.WEBSITE_ACCESS_TOKEN }}
|
||||
repository: kimai/www.kimai.org
|
||||
event-type: kimai_release
|
||||
client-payload: '{"kimai_version": "${{ env.kimai_version }}"}'
|
||||
38
.gitignore
vendored
38
.gitignore
vendored
@@ -1,44 +1,34 @@
|
||||
# some hosters require a htaccess to change the PHP version
|
||||
.htaccess
|
||||
.env-*
|
||||
.idea/
|
||||
.DS_Store
|
||||
rector.php
|
||||
phpstan.sh
|
||||
/public/.htaccess
|
||||
/.env-*
|
||||
/.idea/
|
||||
/.DS_Store
|
||||
/phpstan.sh
|
||||
|
||||
# custom apache rules e.g. to deactivate ioncube loader
|
||||
public/.user.ini
|
||||
/public/.user.ini
|
||||
|
||||
# for symfony local webserver
|
||||
php.ini
|
||||
.php-version
|
||||
/php.ini
|
||||
/.php-version
|
||||
|
||||
# YARN 2
|
||||
.yarnrc.yml
|
||||
.yarn
|
||||
/.yarnrc.yml
|
||||
/.yarn
|
||||
|
||||
# for keeping empty directories
|
||||
!.gitkeep
|
||||
|
||||
/bin/*
|
||||
!/bin/console
|
||||
*local.yaml
|
||||
/config/packages/local.yaml
|
||||
/config/bundles-local.php
|
||||
|
||||
/templates/invoice/renderer/.~lock*
|
||||
/translations/branding.*.xlf
|
||||
|
||||
/var/data/*
|
||||
/var/coverage/
|
||||
/var/cache/*
|
||||
|
||||
/var/invoices/*
|
||||
/var/invoices*
|
||||
/var/export/*
|
||||
/var/log/*
|
||||
/var/sessions/*
|
||||
/var/packages/*
|
||||
/var/plugins/*
|
||||
*.disabled
|
||||
/var/plugins_old/
|
||||
/var/plugins/*/*.disabled
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
/.env.local
|
||||
|
||||
@@ -33,7 +33,7 @@ $fixer
|
||||
'no_break_comment' => true,
|
||||
'no_closing_tag' => true,
|
||||
'no_spaces_after_function_name' => true,
|
||||
'no_spaces_inside_parenthesis' => true,
|
||||
'spaces_inside_parentheses' => ['space' => 'none'],
|
||||
'no_trailing_whitespace' => true,
|
||||
'no_trailing_whitespace_in_comment' => true,
|
||||
'single_blank_line_at_eof' => true,
|
||||
@@ -54,7 +54,7 @@ $fixer
|
||||
'class_attributes_separation' => ['elements' => ['method' => 'one']],
|
||||
'concat_space' => ['spacing' => 'one'],
|
||||
'declare_equal_normalize' => true,
|
||||
'function_typehint_space' => true,
|
||||
'type_declaration_spaces' => ['elements' => ['function', 'property']],
|
||||
'include' => true,
|
||||
'lowercase_cast' => true,
|
||||
'lowercase_static_reference' => true,
|
||||
@@ -123,7 +123,7 @@ $fixer
|
||||
'return_type_declaration' => true,
|
||||
'semicolon_after_instruction' => true,
|
||||
'short_scalar_cast' => true,
|
||||
'single_blank_line_before_namespace' => true,
|
||||
'blank_lines_before_namespace' => ['min_line_breaks' => 2, 'max_line_breaks' => 2],
|
||||
'single_line_comment_style' => [
|
||||
'comment_types' => ['hash'],
|
||||
],
|
||||
|
||||
12
README.md
12
README.md
@@ -3,7 +3,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/kimai/kimai/actions"><img alt="CI Status" src="https://github.com/kimai/kimai/workflows/CI/badge.svg"></a>
|
||||
<a href="https://github.com/kimai/kimai/actions"><img alt="CI Status" src="https://github.com/kimai/kimai/actions/workflows/testing.yaml/badge.svg"></a>
|
||||
<a href="https://codecov.io/gh/kimai/kimai"><img alt="Code Coverage" src="https://codecov.io/gh/kimai/kimai/branch/main/graph/badge.svg"></a>
|
||||
<a href="https://packagist.org/packages/kimai/kimai"><img alt="Latest stable version" src="https://poser.pugx.org/kimai/kimai/v/stable"></a>
|
||||
<a href="https://www.gnu.org/licenses/agpl-3.0.en.html"><img alt="License" src="https://poser.pugx.org/kimai/kimai/license"></a>
|
||||
@@ -23,12 +23,12 @@ and so much more.
|
||||
|
||||
### Versions
|
||||
|
||||
There are two versions of Kimai existing:
|
||||
There are two [versions](https://www.kimai.org/documentation/versions.html) of Kimai existing:
|
||||
|
||||
- [Version 2](https://github.com/kimai/kimai) — the current stable release (PHP 8.1 only)
|
||||
- [Version 1](https://github.com/kimai/kimai/tree/1.x) — the "old" version, in maintenance mode since Jan. 2023 (PHP 7.4, PHP 8.1)
|
||||
- [Version 2](https://github.com/kimai/kimai) — the current stable release (PHP 8.1+)
|
||||
- [Version 1](https://github.com/kimai/kimai/tree/1.x) — EOL since mid of 2023 (PHP 7.4)
|
||||
|
||||
If you start fresh, do **not** use Version 1, it won't receive any more updates.
|
||||
Do **NOT** use Version 1, it won't get any more updates!
|
||||
|
||||
### Links
|
||||
|
||||
@@ -74,8 +74,6 @@ It is open for changes and input from the community, your [ideas and questions](
|
||||
Release versions will be created on a regular basis, every couple of weeks latest.
|
||||
Every code change, whether it's a new feature or a bugfix, will be done on the `main` branch.
|
||||
|
||||
Until 2.0 is widely adopted, the [1.x branch](https://github.com/kimai/kimai/tree/1.x) will receive (critical) bug and security fixes.
|
||||
|
||||
## Contributing
|
||||
|
||||
You want to contribute to this repository? This is so great!
|
||||
|
||||
368
UPGRADING-1.md
Normal file
368
UPGRADING-1.md
Normal file
@@ -0,0 +1,368 @@
|
||||
# Upgrading Kimai - Version 1.x
|
||||
|
||||
_Make sure to create a backup before you start!_
|
||||
|
||||
Read the [updates documentation](https://www.kimai.org/documentation/updates.html) to find out how
|
||||
you can upgrade your Kimai installation to the latest stable release.
|
||||
|
||||
Check below if there are more version specific steps required, which need to be executed after the normal update process.
|
||||
Perform EACH version specific task between your version and the new one, otherwise you risk data inconsistency or a broken installation.
|
||||
|
||||
## [1.16](https://github.com/kimai/kimai/releases/tag/1.16)
|
||||
|
||||
If you are using MariaDB, it must be at least version 10.1, older versions are not supported any longer.
|
||||
|
||||
**DEVELOPER**
|
||||
|
||||
- Removed `formDateTime` field from API model `I18nConfig`
|
||||
- Upgraded to Doctrine DBAL 3, see [docs](https://github.com/doctrine/dbal/blob/3.1.x/UPGRADE.md) - you might have to update your bundle
|
||||
|
||||
## [1.15](https://github.com/kimai/kimai/releases/tag/1.15)
|
||||
|
||||
**Many database changes: don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
|
||||
|
||||
Updating the database might take quite a while, depending on the amount of timesheet entries and speed of your database server (~1 minute per 100k records).
|
||||
|
||||
**ATTENTION**
|
||||
|
||||
- This release bumps the minimum required [PHP version to 7.3](https://www.kimai.org/blog/2021/php8-support-php72-dropped/)
|
||||
- Self-registration is disabled by default
|
||||
- Self-registration now always requires email confirmation
|
||||
- All plugins that use own databases need to be updated as well
|
||||
- Removed the YearChart widget and the related configs named `userRecapThisYear`, `userRecapLastYear`, `userRecapTwoYears`, `userRecapThreeYears`
|
||||
|
||||
**LDAP & SAML**
|
||||
|
||||
Please verify your config with the [LDAP](https://www.kimai.org/documentation/ldap.html) and [SAML](https://www.kimai.org/documentation/saml.html) documentation, especially:
|
||||
|
||||
- SAML users: activate it by setting the `kimai.saml.activate: true` config key
|
||||
- LDAP users: activate it by setting the `kimai.ldap.activate: true` config key
|
||||
- LDAP and SAML users need to remove the complete `security` section from their `local.yaml`
|
||||
|
||||
**DEVELOPER**
|
||||
|
||||
PHP 8 compatibility forced to upgrade MANY libraries, including but not limited to:
|
||||
|
||||
- Removed FOSUserBundle and hslavich/oneloginsaml
|
||||
- Doctrine Migrations, whose new major version forces plugin updates
|
||||
- Gedmo v3 (which include BC breaks in definitions)
|
||||
- Doctrine DBAL and others, which required PHP 7.3 as well
|
||||
|
||||
**API BC break**: Due to team structure changes, it was impossible to keep the (writing) API structure. Please adjust your code accordingly!
|
||||
|
||||
## [1.14](https://github.com/kimai/kimai/releases/tag/1.14)
|
||||
|
||||
**CRITICAL BC break**: SQLite support was removed. If you are using SQLite, you have to [read this blog post](https://www.kimai.org/blog/2021/sqlite-and-ftp-support-removed/) and migrate to MySQL/MariaDB first!
|
||||
|
||||
**New database tables and fields: don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
|
||||
|
||||
Permission changes:
|
||||
- `history_invoice` - removed permission entirely
|
||||
|
||||
## [1.13](https://github.com/kimai/kimai/releases/tag/1.13)
|
||||
|
||||
- Deprecated `now` variable in export templates: create it yourself with `{% set now = create_date('now', app.user) %}`
|
||||
- Changed invoice filename generation (check if you use cronjob for invoices)
|
||||
- **BC break**: duration entered as plain numbers will now be treated as decimal duration in hours instead of seconds
|
||||
|
||||
## [1.12](https://github.com/kimai/kimai/releases/tag/1.12)
|
||||
|
||||
- Export templates can now include items from plugins (eg. Expenses).
|
||||
|
||||
## [1.10](https://github.com/kimai/kimai/releases/tag/1.10)
|
||||
|
||||
**New database tables and fields were created, don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
|
||||
|
||||
- Invoice renderer `CSV` was removed
|
||||
- Sessions are now stored in the database (all users have to re-login after upgrade)
|
||||
- New permissions: `lockdown_grace_timesheet`, `lockdown_override_timesheet`, `view_all_data`
|
||||
- Fixed team permissions on user queries: depending on your previous team & permission setup your users might see less data (SUPER_ADMINS see all data, but new: ADMINS only see all data if they own the `view_all_data` permission)
|
||||
- Markdown does not support headings anymore, text like `# foo` is not converted to `<h1 id="foo">foo</h1>` anymore
|
||||
|
||||
### Developer
|
||||
|
||||
- **BC break**: removed registration of `.env` with `putenv()` - do not rely on `getenv()` as it is not thread-safe
|
||||
- **BC break**: interface method signature `HtmlToPdfConverter::convertToPdf()` changed
|
||||
- **BC break**: the macros `badge` and `label` do not apply the `|trans` filter any more
|
||||
- **BC Break**: removed `getVisible()` (deprecated since 1.4) method on Customer, Project and Activity (use `isVisible()` instead, templates are still working)
|
||||
- **BC Break**: API changes
|
||||
- some representation names changed (eg. from `ActivityMetaField` to `ActivityMeta`, `TimesheetSubCollection` vs `TimesheetCollectionExpanded`), you could use `class_alias()` if you use auto-generated code from Swagger-Gen or alike
|
||||
- new result types were introduced
|
||||
- result data changed in some areas to smooth out inconsistencies (eg. TeamEntity fields changed in nested results)
|
||||
|
||||
## [1.9](https://github.com/kimai/kimai/releases/tag/1.9)
|
||||
|
||||
**New database tables and fields were created, don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
|
||||
|
||||
- The directory `var/data/invoices/` will be used to store archived invoice files (check file permissions)
|
||||
- The default invoice number format changed. If you want to use the old one: configure `{date}` as format - see [invoice documentation](https://www.kimai.org/documentation/invoices.html)
|
||||
- HTML invoice templates are now treated like other files and offered as download. If you are using relative URLs for including assets (CSS, images) you need to either inline them (see the default templates) or use absolute URLs
|
||||
- Invoice templates that use the templates variables `${activity.X}` or `${project.X}` should be checked and possibly adapted, as multi-select is now possible for filtering
|
||||
- Invoice templates have access to all meta-fields as variables, not only the ones marked as visible
|
||||
- Rates configuration/structure changed for customer, project and activity
|
||||
- **Invoice templates**: rates variables were removed
|
||||
|
||||
Permission changes:
|
||||
- `history_invoice` - NEW: grants all features for the new invoice archive (by default for all admins)
|
||||
|
||||
### Developer
|
||||
|
||||
- **BC break**: `InvoiceItemInterface` has new methods `getType()` and `getCategory()`
|
||||
- **BC break**: API fields changed - see new `/rates` endpoints
|
||||
|
||||
## [1.8](https://github.com/kimai/kimai/releases/tag/1.8)
|
||||
|
||||
**New database tables and fields were created, don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
|
||||
|
||||
- New PHP requirement: `ext-xsl` - which should be pre-installed in most environments when `ext-xml` is loaded
|
||||
- New mailer library: check if emails are still working (eg. by using the "password forgotten" function) or if you need to adjust your configuration, [see docs at symfony.com](https://symfony.com/doc/current/components/mailer.html#transport)
|
||||
- Support for line breaks in multiline invoice fields for spreadsheets (check your invoice templates after the update)
|
||||
|
||||
Permission changes:
|
||||
|
||||
- `comments_create_customer` - NEW: permission that allows to add new comments for customers
|
||||
- `comments_create_team_customer` - NEW: permission that allows to add new comments for team members of the current customer
|
||||
- `comments_create_teamlead_customer` - NEW: permission that allows to add new comments for a teamlead of the current customer
|
||||
- `comments_create_project` - NEW: permission that allows to add new comments for project
|
||||
- `comments_create_team_project` - NEW: permission that allows to add new comments for team members of the current project
|
||||
- `comments_create_teamlead_project` - NEW: permission that allows to add new comments for a teamlead of the current project
|
||||
- `edit_teamlead_project` - removed default permission from ROLE_TEAMLEAD (if you use it: change it in the Role & Permission UI)
|
||||
- `edit_teamlead_customer` - removed default permission from ROLE_TEAMLEAD (if you use it: change it in the Role & Permission UI)
|
||||
- `upload_invoice_template` - NEW: permission that allows to upload invoice documents from the UI
|
||||
|
||||
## [1.7](https://github.com/kimai/kimai/releases/tag/1.7)
|
||||
|
||||
**New database tables and fields were created, don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
|
||||
|
||||
New permissions:
|
||||
|
||||
- `comments_customer` - show comment list on customer detail page (new feature)
|
||||
- `details_customer` - show detail information for customers (customer number, vat, rates, meta-fields, assigned teams ...)
|
||||
- `comments_project` - show comment list on project detail page (new feature)
|
||||
- `details_project` - show detail information for projects (rates, meta-fields, assigned teams ...)
|
||||
|
||||
If you are using teams, please read on: The following list of permissions are now also available in the UI and they (can) replace the `X_project` and `X_customer` permissions.
|
||||
They are more strict, as they allow only access to team specific items, the older permissions without `_teamlead_`/`_team_` work on a global level instead.
|
||||
|
||||
- `view_teamlead_customer`, `edit_teamlead_customer`, `budget_teamlead_customer`, `permissions_teamlead_customer`, `comments_teamlead_customer`, `details_teamlead_customer` - allows access to customer data when user is teamlead of a team assigned to the customer (replaces more global permission like `view_customer` for teamleads)
|
||||
- `view_team_customer`, `edit_team_customer`, `budget_team_customer`, `comments_team_customer`, `details_team_customer` - allows access to customer data when user is member of a team assigned to the customer (replaces more global permission like `view_customer` for users)
|
||||
- `view_teamlead_project`, `edit_teamlead_project`, `budget_teamlead_project`, `permissions_teamlead_project`, `comments_teamlead_project`, `details_teamlead_project` - allows access to customer data when user is teamlead of a team assigned to the project (replaces more global permission like `view_project` for teamleads)
|
||||
- `view_team_project`, `edit_team_project`, `budget_team_project`, `comments_team_project`, `details_team_project` - allows access to customer data when user is member of a team assigned to the project (replaces more global permission like `view_project` for users)
|
||||
|
||||
### ExpenseBundle
|
||||
|
||||
**ATTENTION** due to incompatibilities in the underlying frameworks users of the ExpenseBundle need to do one more step:
|
||||
You need to delete the bundle before updating: `rm -r var/plugins/ExpenseBundle`, otherwise you will run into errors during the update.
|
||||
After the Kimai update was successful, you have to re-install the latest bundle version, which is compatible with Kimai 1.7 only.
|
||||
|
||||
### Developer
|
||||
|
||||
- Projects now have a start and end date and the API will only return those, which are either unconfigured or currently active, you might want to reload the list of projects once the user entered begin and end datetime OR use the new `ignoreDates` parameter.
|
||||
- Doctrine bundle was updated to v2, check your code for [the usage of RegistryInterface and ObjectManager](https://github.com/doctrine/DoctrineBundle/blob/master/UPGRADE-2.0.md)
|
||||
- Removed the webserver bundle and the command `server:run` - see [docs](https://www.kimai.org/documentation/developers.html)
|
||||
|
||||
## [1.6](https://github.com/kimai/kimai/releases/tag/1.6), [1.6.1](https://github.com/kimai/kimai/releases/tag/1.6.1), [1.6.2](https://github.com/kimai/kimai/releases/tag/1.6.2)
|
||||
|
||||
**New database tables and fields were created, don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
|
||||
|
||||
- Invoice changes:
|
||||
- Moved CSV, ODS and XSLX invoice templates to [another repository](https://github.com/Keleo/kimai2-invoice-templates). Using them? Install them manually (see [invoice documentation](https://www.kimai.org/documentation/invoices.html)).
|
||||
- Added new invoice fields (VAT, contact, payment details) and customer field (VAT). Used the twig settings before? Move them to the respective invoice template settings.
|
||||
- Permissions can be managed via Admin UI. Please move your permission settings from [local.yaml to your database](https://www.kimai.org/documentation/permissions.html).
|
||||
- Important permission change: regular users with the `view_other_timesheet` permission could see all timesheets. This was a legacy from the time before team permissions were introduced. If you rely on this behavior, you need to create a team with all users and the teamlead being the user who needs access to all timesheets.
|
||||
|
||||
### Developer
|
||||
|
||||
Please add default permissions to your [plugin](https://www.kimai.org/documentation/plugins.html).
|
||||
|
||||
## [1.5](https://github.com/kimai/kimai/releases/tag/1.5)
|
||||
|
||||
[Update as usual](https://www.kimai.org/documentation/updates.html)
|
||||
|
||||
## [1.4](https://github.com/kimai/kimai/releases/tag/1.4)
|
||||
|
||||
**There is a new directory, which needs to be writable by the webserver: `public/avatars/`.**
|
||||
|
||||
New permission (used in new dashboard widget):
|
||||
- `view_team_member` - display team assignments (names, teamleads and members) for the current user
|
||||
|
||||
Activated Javascript select component by default (check mobile devices).
|
||||
|
||||
### Developer: BC breaks
|
||||
|
||||
- Dashboard widgets and rows need to define their `type` by FQCN
|
||||
- Switched to Symfony 4.3 event types, this could fail in plugins, but only if they didn't use the official constants for event names
|
||||
|
||||
## [1.3](https://github.com/kimai/kimai/releases/tag/1.3)
|
||||
|
||||
Added `manage_tag` permission for new tag features
|
||||
|
||||
### Developer: BC breaks
|
||||
|
||||
- Refactored toolbars and search, plugins needs to be checked
|
||||
- Invoices now supports multiple repositories, some method signatures had to be changed (eg. `calculateSumIdentifier()`)
|
||||
|
||||
## [1.2](https://github.com/kimai/kimai/releases/tag/1.2)
|
||||
|
||||
**If you are still using 0.7 or below, you need to upgrade to 1.1 before upgrading to this version.**
|
||||
|
||||
- Deleted timezone conversion command.
|
||||
- Minimum password length raised from 5 to 8 character (applies only for password changes and new users)
|
||||
- Maximum customer name length lowered to 150 character
|
||||
- Maximum project name length lowered to 150 character
|
||||
- Maximum activity name length lowered to 150 character
|
||||
- Added new permission: `manage_invoice_template`
|
||||
- Removed permissions: `view_invoice_template`, `create_invoice_template`, `edit_invoice_template`, `delete_invoice_template`
|
||||
- Removed permission: `view_export` (using `create_export` only)
|
||||
|
||||
### Developer: BC breaks
|
||||
|
||||
- Custom export renderer need to check for usage of `Timesheet::getEnd()` as running entries can now be exported as well
|
||||
|
||||
## [1.1](https://github.com/kimai/kimai/releases/tag/1.1)
|
||||
|
||||
[Update as usual](https://www.kimai.org/documentation/updates.html), nothing special for this release if you upgrade from 1.0 / 1.0.1.
|
||||
|
||||
## [1.0](https://github.com/kimai/kimai/releases/tag/1.0)
|
||||
|
||||
This release contains several changes, as I still have the goal to stabilize the code base to prevent
|
||||
such "challenges" after 1.0 for a while.
|
||||
|
||||
### Changes for your local.yaml
|
||||
|
||||
New permissions are available. You have to add them to your `local.yaml` ONLY if you use a custom permission structure,
|
||||
otherwise you can't use the new features:
|
||||
- `view_tag` - view all tags
|
||||
- `delete_tag` - delete tags
|
||||
- `edit_exported_timesheet` - allows to edit records which were exported
|
||||
- `role_permissions` - view calculated permissions for user roles
|
||||
- `budget_activity` - view and edit budgets for activities
|
||||
- `budget_project` - view and edit budgets for projects
|
||||
- `budget_customer` - view and edit budgets for customers
|
||||
|
||||
Removed permission:
|
||||
- `system_actions` - removed experimental feature to flush app cache from the about screen
|
||||
|
||||
### BC BREAKS
|
||||
|
||||
- API: Format for queries including a datetime object fixed to use HTML5 format (previously `2019-03-02 14:23` - now `2019-03-02T14:23:00`)
|
||||
- **Permission config**: the `permissions` definition in your `local.yaml` needs to be verified/changed, as the internal structure was highly optimized to simplify the definition.
|
||||
Thanks to the new structure, you should be able to remove almost everything from your `local.yaml` (tip: start over from scratch!). Please read [the updated permission docu](https://www.kimai.org/documentation/permissions.html).
|
||||
- default widgets were removed from `kimai.yaml`, that shouldn't cause any issues ... but if something is odd: [look here for help](https://www.kimai.org/documentation/dashboard.html)
|
||||
|
||||
## [0.9](https://github.com/kimai/kimai/releases/tag/0.9)
|
||||
|
||||
Remember to execute the necessary timezone conversion script, if you haven't updated to 0.8 before (see below)!
|
||||
|
||||
### BC BREAKS
|
||||
|
||||
This release contains some BC breaks which were necessary before 1.0 will be released (_now or never_), to prevent those BC breaks after 1.0.
|
||||
|
||||
- **Kimai requires PHP 7.2 now => [PHP 7.1 expired 4 month ago](https://www.php.net/supported-versions.php)**
|
||||
- The `.env` variable `DATABASE_PREFIX` was removed and the table prefix is now hardcoded to `kimai2_`. If you used another prefix,
|
||||
you have to rename your tables manually before starting the update process. You can delete the row `DATABASE_PREFIX` from your `.env` file.
|
||||
- API: Format for DateTime objects changed, now including timezone identifier (previously `2019-03-02 14:23` - now `2019-03-02T14:23:00+00:00`), see [#718](https://github.com/kimai/kimai/pull/718)
|
||||
- API: changed from snake_case to camelCase (affected fields: hourlyRate vs hourly_rate / fixedRate vs fixed_rate / orderNumber vs order_number / i18n config object)
|
||||
- Plugin mechanism changed: existing Plugins have to be deleted or updated
|
||||
|
||||
### Apply necessary changes to your `local.yaml`:
|
||||
|
||||
New permissions are available:
|
||||
- `system_configuration` - for accessing the new system configuration screen
|
||||
- `system_actions` - for the experimental feature to flush your cache from the about screen
|
||||
- `plugins` - for accessing the new plugins screen
|
||||
|
||||
The setting `kimai.timesheet.mode` replaces the setting `kimai.timesheet.duration_only`. If you used the duration_only mode, you need to change your config:
|
||||
```yaml
|
||||
# Before
|
||||
kimai:
|
||||
timesheet:
|
||||
duration_only: true
|
||||
|
||||
# After
|
||||
kimai:
|
||||
timesheet:
|
||||
mode: duration_only
|
||||
```
|
||||
Or switch the mode directly in the new System configuration screen within Kimai.
|
||||
|
||||
## [0.8.1](https://github.com/kimai/kimai/releases/tag/0.8.1)
|
||||
|
||||
A bug fixing release. Remember to execute the necessary timezone conversion script, if you haven't updated to 0.8 before (see below)!
|
||||
|
||||
## [0.8](https://github.com/kimai/kimai/releases/tag/0.8)
|
||||
|
||||
After you followed the normal update and database migration process (see above), you need to execute a bash command to convert your timesheet data for timezone support:
|
||||
|
||||
- Read this [pull request](https://github.com/kimai/kimai/pull/372) BEFORE you follow the instructions to convert the
|
||||
timezones in your existing time records with `bin/console kimai:convert-timezone`. Without that, you will end up with wrong times in your database.
|
||||
|
||||
### Apply necessary changes to your `local.yaml`:
|
||||
|
||||
- A new boolean setting `kimai.timesheet.rules.allow_future_times` was introduced
|
||||
- New permissions are available:
|
||||
- `view_export` - for the new export feature
|
||||
- `create_export` - for the new export feature
|
||||
- `edit_export_own_timesheet` - for the new export feature
|
||||
- `edit_export_other_timesheet` - for the new export feature
|
||||
- `system_information` - to see the new about screen
|
||||
|
||||
## [0.7](https://github.com/kimai/kimai/releases/tag/0.7)
|
||||
|
||||
The configuration `kimai.theme.active_warning` was deprecated and should be replaced in your local.yaml,
|
||||
[read config docs for more information](https://www.kimai.org/documentation/timesheet.html#limit-active-entries).
|
||||
|
||||
## [0.6.1](https://github.com/kimai/kimai/releases/tag/0.6.1)
|
||||
|
||||
A bugfix release to address database compatibility issues with older MySQL/MariaDB versions.
|
||||
|
||||
## [0.6](https://github.com/kimai/kimai/releases/tag/0.6)
|
||||
|
||||
The API has some minor BC breaks: some fields were renamed and entities have a larger attribute set than collections.
|
||||
Be aware that the API is still is development mode and shouldn't be considered stable for now.
|
||||
|
||||
## [0.5](https://github.com/kimai/kimai/releases/tag/0.5)
|
||||
|
||||
Some configuration nodes were removed, if you have one of them in your `local.yaml` you need to delete them before you start the update:
|
||||
- `kimai.invoice.calculator`
|
||||
- `kimai.invoice.renderer`
|
||||
- `kimai.invoice.number_generator`
|
||||
|
||||
The new config `kimai.invoice.documents` was introduced, holding a list of directories ([read more](https://www.kimai.org/documentation/invoices.html)).
|
||||
|
||||
**BC break:** InvoiceTemplate name was changed from 255 characters to 60. If you used longer invoice-template names, they will be truncated when upgrading the database.
|
||||
Please make sure that they are unique in the first 60 character before you upgrade your database with `doctrine:migrations:migrate`.
|
||||
|
||||
## [0.4](https://github.com/kimai/kimai/releases/tag/0.4)
|
||||
|
||||
In the time between 0.3 and 0.4 there was a release of composer that introduced a BC break,
|
||||
which leads to problems between Composer and Symfony Flex, resulting in an error like this when running it:
|
||||
|
||||
```
|
||||
[ErrorException]
|
||||
Declaration of Symfony\Flex\ParallelDownloader::getRemoteContents($originUrl, $fileUrl, $context) should be compatible with Composer\Util\RemoteFilesystem::getRemoteContents($originUrl, $fileUrl, $context, ?array &$responseHeaders = NULL)
|
||||
```
|
||||
|
||||
This can be fixed by updating Composer and Flex before executing the Kimai update:
|
||||
```
|
||||
sudo composer self-update
|
||||
sudo -u www-data composer update symfony/flex --no-plugins --no-scripts
|
||||
```
|
||||
|
||||
## [0.3](https://github.com/kimai/kimai/releases/tag/0.3)
|
||||
|
||||
You need to adjust your `.env` file and add your `from` address for [all emails](https://www.kimai.org/documentation/emails.html) generated by Kimai 2:
|
||||
```
|
||||
MAILER_FROM=kimai@example.com
|
||||
```
|
||||
|
||||
Create a file and database backup before executing the following steps:
|
||||
|
||||
```bash
|
||||
git pull origin master
|
||||
sudo -u www-data composer install --no-dev --optimize-autoloader
|
||||
sudo -u www-data bin/console cache:clear --env=prod
|
||||
sudo -u www-data bin/console cache:warmup --env=prod
|
||||
bin/console doctrine:migrations:version --add 20180701120000
|
||||
bin/console doctrine:migrations:migrate
|
||||
```
|
||||
381
UPGRADING.md
381
UPGRADING.md
@@ -1,4 +1,4 @@
|
||||
# Upgrading Kimai
|
||||
# Upgrading Kimai - Version2.x
|
||||
|
||||
_Make sure to create a backup before you start!_
|
||||
|
||||
@@ -8,21 +8,31 @@ you can upgrade your Kimai installation to the latest stable release.
|
||||
Check below if there are more version specific steps required, which need to be executed after the normal update process.
|
||||
Perform EACH version specific task between your version and the new one, otherwise you risk data inconsistency or a broken installation.
|
||||
|
||||
## [2.0.30](https://github.com/kimai/kimai/releases/tag/2.0.30)
|
||||
|
||||
The `DATABASE_URL` in your environment settings ([.env](https://github.com/kimai/kimai/issues/4246) or in your [Docker](https://github.com/tobybatch/kimai2/issues/531) or webserver config)
|
||||
now requires the `charset` and `serverVersion` params, e.g.: `DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.5.8-MariaDB` (examples in `.env`).
|
||||
|
||||
## [2.0](https://github.com/kimai/kimai/releases/tag/2.0)
|
||||
|
||||
**!! This release requires minimum PHP version to 8.1 !!**
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- All plugins need to be updated: delete all previous version from your installation (`rm -r var/plugins/*`) before updating!
|
||||
- The `local.yaml` is not compatible with old version, remove it before the update and then re-create it after everything works
|
||||
- removed: configuring the `dashboard` is not supported any longer
|
||||
- removed: custom translation files via `theme.branding.translation`
|
||||
- removed: changing the plugin directory via `kimai.plugin_dir`
|
||||
|
||||
### Developer
|
||||
|
||||
Developer read the full documentation at [https://www.kimai.org/documentation/migration-v2.html](https://www.kimai.org/documentation/migration-v2.html).
|
||||
|
||||
- **All plugins need to be updated**: delete all previous version from your installation (`rm -r var/plugins/*`) before updating!
|
||||
- Invoice renderer and templates for XML, JSON and TEXT were moved to the [Extended invoicing plugin](https://www.kimai.org/store/invoice-bundle.html) (install if you use one of those)
|
||||
- Moved `company.docx` to [external repo](https://github.com/kimai/invoice-templates/tree/main/docx-company) (needs to be re-uploaded if you want to keep on using it!)
|
||||
- Role names are forced to be uppercase
|
||||
- Removed unused `public/avatars/` directory
|
||||
- `local.yaml` is not compatible with old version, remove it before the update and then re-create it after everything works
|
||||
- dashboard default config
|
||||
- removed: theme.branding.translation
|
||||
- removed: kimai.plugin_dir
|
||||
- Time-tracking mode `duration_only` was removed, existing installations will be switched to `duration_fixed_begin`
|
||||
- Removed Twig filters. You might have to replace them in your custom export/invoice templates:
|
||||
- `date_full` => `date_time`
|
||||
@@ -38,362 +48,3 @@ Developer read the full documentation at [https://www.kimai.org/documentation/mi
|
||||
- `composer require symfony/mailgun-mailer`
|
||||
- `composer require symfony/postmark-mailer`
|
||||
- `composer require symfony/sendgrid-mailer`
|
||||
|
||||
## [1.16](https://github.com/kimai/kimai/releases/tag/1.16)
|
||||
|
||||
If you are using MariaDB, it must be at least version 10.1, older versions are not supported any longer.
|
||||
|
||||
**DEVELOPER**
|
||||
|
||||
- Removed `formDateTime` field from API model `I18nConfig`
|
||||
- Upgraded to Doctrine DBAL 3, see [docs](https://github.com/doctrine/dbal/blob/3.1.x/UPGRADE.md) - you might have to update your bundle
|
||||
|
||||
## [1.15](https://github.com/kimai/kimai/releases/tag/1.15)
|
||||
|
||||
**Many database changes: don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
|
||||
|
||||
Updating the database might take quite a while, depending on the amount of timesheet entries and speed of your database server (~1 minute per 100k records).
|
||||
|
||||
**ATTENTION**
|
||||
|
||||
- This release bumps the minimum required [PHP version to 7.3](https://www.kimai.org/blog/2021/php8-support-php72-dropped/)
|
||||
- Self-registration is disabled by default
|
||||
- Self-registration now always requires email confirmation
|
||||
- All plugins that use own databases need to be updated as well
|
||||
- Removed the YearChart widget and the related configs named `userRecapThisYear`, `userRecapLastYear`, `userRecapTwoYears`, `userRecapThreeYears`
|
||||
|
||||
**LDAP & SAML**
|
||||
|
||||
Please verify your config with the [LDAP](https://www.kimai.org/documentation/ldap.html) and [SAML](https://www.kimai.org/documentation/saml.html) documentation, especially:
|
||||
|
||||
- SAML users: activate it by setting the `kimai.saml.activate: true` config key
|
||||
- LDAP users: activate it by setting the `kimai.ldap.activate: true` config key
|
||||
- LDAP and SAML users need to remove the complete `security` section from their `local.yaml`
|
||||
|
||||
**DEVELOPER**
|
||||
|
||||
PHP 8 compatibility forced to upgrade MANY libraries, including but not limited to:
|
||||
|
||||
- Removed FOSUserBundle and hslavich/oneloginsaml
|
||||
- Doctrine Migrations, whose new major version forces plugin updates
|
||||
- Gedmo v3 (which include BC breaks in definitions)
|
||||
- Doctrine DBAL and others, which required PHP 7.3 as well
|
||||
|
||||
**API BC break**: Due to team structure changes, it was impossible to keep the (writing) API structure. Please adjust your code accordingly!
|
||||
|
||||
## [1.14](https://github.com/kimai/kimai/releases/tag/1.14)
|
||||
|
||||
**CRITICAL BC break**: SQLite support was removed. If you are using SQLite, you have to [read this blog post](https://www.kimai.org/blog/2021/sqlite-and-ftp-support-removed/) and migrate to MySQL/MariaDB first!
|
||||
|
||||
**New database tables and fields: don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
|
||||
|
||||
Permission changes:
|
||||
- `history_invoice` - removed permission entirely
|
||||
|
||||
## [1.13](https://github.com/kimai/kimai/releases/tag/1.13)
|
||||
|
||||
- Deprecated `now` variable in export templates: create it yourself with `{% set now = create_date('now', app.user) %}`
|
||||
- Changed invoice filename generation (check if you use cronjob for invoices)
|
||||
- **BC break**: duration entered as plain numbers will now be treated as decimal duration in hours instead of seconds
|
||||
|
||||
## [1.12](https://github.com/kimai/kimai/releases/tag/1.12)
|
||||
|
||||
- Export templates can now include items from plugins (eg. Expenses).
|
||||
|
||||
## [1.10](https://github.com/kimai/kimai/releases/tag/1.10)
|
||||
|
||||
**New database tables and fields were created, don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
|
||||
|
||||
- Invoice renderer `CSV` was removed
|
||||
- Sessions are now stored in the database (all users have to re-login after upgrade)
|
||||
- New permissions: `lockdown_grace_timesheet`, `lockdown_override_timesheet`, `view_all_data`
|
||||
- Fixed team permissions on user queries: depending on your previous team & permission setup your users might see less data (SUPER_ADMINS see all data, but new: ADMINS only see all data if they own the `view_all_data` permission)
|
||||
- Markdown does not support headings anymore, text like `# foo` is not converted to `<h1 id="foo">foo</h1>` anymore
|
||||
|
||||
### Developer
|
||||
|
||||
- **BC break**: removed registration of `.env` with `putenv()` - do not rely on `getenv()` as it is not thread-safe
|
||||
- **BC break**: interface method signature `HtmlToPdfConverter::convertToPdf()` changed
|
||||
- **BC break**: the macros `badge` and `label` do not apply the `|trans` filter any more
|
||||
- **BC Break**: removed `getVisible()` (deprecated since 1.4) method on Customer, Project and Activity (use `isVisible()` instead, templates are still working)
|
||||
- **BC Break**: API changes
|
||||
- some representation names changed (eg. from `ActivityMetaField` to `ActivityMeta`, `TimesheetSubCollection` vs `TimesheetCollectionExpanded`), you could use `class_alias()` if you use auto-generated code from Swagger-Gen or alike
|
||||
- new result types were introduced
|
||||
- result data changed in some areas to smooth out inconsistencies (eg. TeamEntity fields changed in nested results)
|
||||
|
||||
## [1.9](https://github.com/kimai/kimai/releases/tag/1.9)
|
||||
|
||||
**New database tables and fields were created, don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
|
||||
|
||||
- The directory `var/data/invoices/` will be used to store archived invoice files (check file permissions)
|
||||
- The default invoice number format changed. If you want to use the old one: configure `{date}` as format - see [invoice documentation](https://www.kimai.org/documentation/invoices.html)
|
||||
- HTML invoice templates are now treated like other files and offered as download. If you are using relative URLs for including assets (CSS, images) you need to either inline them (see the default templates) or use absolute URLs
|
||||
- Invoice templates that use the templates variables `${activity.X}` or `${project.X}` should be checked and possibly adapted, as multi-select is now possible for filtering
|
||||
- Invoice templates have access to all meta-fields as variables, not only the ones marked as visible
|
||||
- Rates configuration/structure changed for customer, project and activity
|
||||
- **Invoice templates**: rates variables were removed
|
||||
|
||||
Permission changes:
|
||||
- `history_invoice` - NEW: grants all features for the new invoice archive (by default for all admins)
|
||||
|
||||
### Developer
|
||||
|
||||
- **BC break**: `InvoiceItemInterface` has new methods `getType()` and `getCategory()`
|
||||
- **BC break**: API fields changed - see new `/rates` endpoints
|
||||
|
||||
## [1.8](https://github.com/kimai/kimai/releases/tag/1.8)
|
||||
|
||||
**New database tables and fields were created, don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
|
||||
|
||||
- New PHP requirement: `ext-xsl` - which should be pre-installed in most environments when `ext-xml` is loaded
|
||||
- New mailer library: check if emails are still working (eg. by using the "password forgotten" function) or if you need to adjust your configuration, [see docs at symfony.com](https://symfony.com/doc/current/components/mailer.html#transport)
|
||||
- Support for line breaks in multiline invoice fields for spreadsheets (check your invoice templates after the update)
|
||||
|
||||
Permission changes:
|
||||
|
||||
- `comments_create_customer` - NEW: permission that allows to add new comments for customers
|
||||
- `comments_create_team_customer` - NEW: permission that allows to add new comments for team members of the current customer
|
||||
- `comments_create_teamlead_customer` - NEW: permission that allows to add new comments for a teamlead of the current customer
|
||||
- `comments_create_project` - NEW: permission that allows to add new comments for project
|
||||
- `comments_create_team_project` - NEW: permission that allows to add new comments for team members of the current project
|
||||
- `comments_create_teamlead_project` - NEW: permission that allows to add new comments for a teamlead of the current project
|
||||
- `edit_teamlead_project` - removed default permission from ROLE_TEAMLEAD (if you use it: change it in the Role & Permission UI)
|
||||
- `edit_teamlead_customer` - removed default permission from ROLE_TEAMLEAD (if you use it: change it in the Role & Permission UI)
|
||||
- `upload_invoice_template` - NEW: permission that allows to upload invoice documents from the UI
|
||||
|
||||
## [1.7](https://github.com/kimai/kimai/releases/tag/1.7)
|
||||
|
||||
**New database tables and fields were created, don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
|
||||
|
||||
New permissions:
|
||||
|
||||
- `comments_customer` - show comment list on customer detail page (new feature)
|
||||
- `details_customer` - show detail information for customers (customer number, vat, rates, meta-fields, assigned teams ...)
|
||||
- `comments_project` - show comment list on project detail page (new feature)
|
||||
- `details_project` - show detail information for projects (rates, meta-fields, assigned teams ...)
|
||||
|
||||
If you are using teams, please read on: The following list of permissions are now also available in the UI and they (can) replace the `X_project` and `X_customer` permissions.
|
||||
They are more strict, as they allow only access to team specific items, the older permissions without `_teamlead_`/`_team_` work on a global level instead.
|
||||
|
||||
- `view_teamlead_customer`, `edit_teamlead_customer`, `budget_teamlead_customer`, `permissions_teamlead_customer`, `comments_teamlead_customer`, `details_teamlead_customer` - allows access to customer data when user is teamlead of a team assigned to the customer (replaces more global permission like `view_customer` for teamleads)
|
||||
- `view_team_customer`, `edit_team_customer`, `budget_team_customer`, `comments_team_customer`, `details_team_customer` - allows access to customer data when user is member of a team assigned to the customer (replaces more global permission like `view_customer` for users)
|
||||
- `view_teamlead_project`, `edit_teamlead_project`, `budget_teamlead_project`, `permissions_teamlead_project`, `comments_teamlead_project`, `details_teamlead_project` - allows access to customer data when user is teamlead of a team assigned to the project (replaces more global permission like `view_project` for teamleads)
|
||||
- `view_team_project`, `edit_team_project`, `budget_team_project`, `comments_team_project`, `details_team_project` - allows access to customer data when user is member of a team assigned to the project (replaces more global permission like `view_project` for users)
|
||||
|
||||
### ExpenseBundle
|
||||
|
||||
**ATTENTION** due to incompatibilities in the underlying frameworks users of the ExpenseBundle need to do one more step:
|
||||
You need to delete the bundle before updating: `rm -r var/plugins/ExpenseBundle`, otherwise you will run into errors during the update.
|
||||
After the Kimai update was successful, you have to re-install the latest bundle version, which is compatible with Kimai 1.7 only.
|
||||
|
||||
### Developer
|
||||
|
||||
- Projects now have a start and end date and the API will only return those, which are either unconfigured or currently active, you might want to reload the list of projects once the user entered begin and end datetime OR use the new `ignoreDates` parameter.
|
||||
- Doctrine bundle was updated to v2, check your code for [the usage of RegistryInterface and ObjectManager](https://github.com/doctrine/DoctrineBundle/blob/master/UPGRADE-2.0.md)
|
||||
- Removed the webserver bundle and the command `server:run` - see [docs](https://www.kimai.org/documentation/developers.html)
|
||||
|
||||
## [1.6](https://github.com/kimai/kimai/releases/tag/1.6), [1.6.1](https://github.com/kimai/kimai/releases/tag/1.6.1), [1.6.2](https://github.com/kimai/kimai/releases/tag/1.6.2)
|
||||
|
||||
**New database tables and fields were created, don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
|
||||
|
||||
- Invoice changes:
|
||||
- Moved CSV, ODS and XSLX invoice templates to [another repository](https://github.com/Keleo/kimai2-invoice-templates). Using them? Install them manually (see [invoice documentation](https://www.kimai.org/documentation/invoices.html)).
|
||||
- Added new invoice fields (VAT, contact, payment details) and customer field (VAT). Used the twig settings before? Move them to the respective invoice template settings.
|
||||
- Permissions can be managed via Admin UI. Please move your permission settings from [local.yaml to your database](https://www.kimai.org/documentation/permissions.html).
|
||||
- Important permission change: regular users with the `view_other_timesheet` permission could see all timesheets. This was a legacy from the time before team permissions were introduced. If you rely on this behavior, you need to create a team with all users and the teamlead being the user who needs access to all timesheets.
|
||||
|
||||
### Developer
|
||||
|
||||
Please add default permissions to your [plugin](https://www.kimai.org/documentation/plugins.html).
|
||||
|
||||
## [1.5](https://github.com/kimai/kimai/releases/tag/1.5)
|
||||
|
||||
[Update as usual](https://www.kimai.org/documentation/updates.html)
|
||||
|
||||
## [1.4](https://github.com/kimai/kimai/releases/tag/1.4)
|
||||
|
||||
**There is a new directory, which needs to be writable by the webserver: `public/avatars/`.**
|
||||
|
||||
New permission (used in new dashboard widget):
|
||||
- `view_team_member` - display team assignments (names, teamleads and members) for the current user
|
||||
|
||||
Activated Javascript select component by default (check mobile devices).
|
||||
|
||||
### Developer: BC breaks
|
||||
|
||||
- Dashboard widgets and rows need to define their `type` by FQCN
|
||||
- Switched to Symfony 4.3 event types, this could fail in plugins, but only if they didn't use the official constants for event names
|
||||
|
||||
## [1.3](https://github.com/kimai/kimai/releases/tag/1.3)
|
||||
|
||||
Added `manage_tag` permission for new tag features
|
||||
|
||||
### Developer: BC breaks
|
||||
|
||||
- Refactored toolbars and search, plugins needs to be checked
|
||||
- Invoices now supports multiple repositories, some method signatures had to be changed (eg. `calculateSumIdentifier()`)
|
||||
|
||||
## [1.2](https://github.com/kimai/kimai/releases/tag/1.2)
|
||||
|
||||
**If you are still using 0.7 or below, you need to upgrade to 1.1 before upgrading to this version.**
|
||||
|
||||
- Deleted timezone conversion command.
|
||||
- Minimum password length raised from 5 to 8 character (applies only for password changes and new users)
|
||||
- Maximum customer name length lowered to 150 character
|
||||
- Maximum project name length lowered to 150 character
|
||||
- Maximum activity name length lowered to 150 character
|
||||
- Added new permission: `manage_invoice_template`
|
||||
- Removed permissions: `view_invoice_template`, `create_invoice_template`, `edit_invoice_template`, `delete_invoice_template`
|
||||
- Removed permission: `view_export` (using `create_export` only)
|
||||
|
||||
### Developer: BC breaks
|
||||
|
||||
- Custom export renderer need to check for usage of `Timesheet::getEnd()` as running entries can now be exported as well
|
||||
|
||||
## [1.1](https://github.com/kimai/kimai/releases/tag/1.1)
|
||||
|
||||
[Update as usual](https://www.kimai.org/documentation/updates.html), nothing special for this release if you upgrade from 1.0 / 1.0.1.
|
||||
|
||||
## [1.0](https://github.com/kimai/kimai/releases/tag/1.0)
|
||||
|
||||
This release contains several changes, as I still have the goal to stabilize the code base to prevent
|
||||
such "challenges" after 1.0 for a while.
|
||||
|
||||
### Changes for your local.yaml
|
||||
|
||||
New permissions are available. You have to add them to your `local.yaml` ONLY if you use a custom permission structure,
|
||||
otherwise you can't use the new features:
|
||||
- `view_tag` - view all tags
|
||||
- `delete_tag` - delete tags
|
||||
- `edit_exported_timesheet` - allows to edit records which were exported
|
||||
- `role_permissions` - view calculated permissions for user roles
|
||||
- `budget_activity` - view and edit budgets for activities
|
||||
- `budget_project` - view and edit budgets for projects
|
||||
- `budget_customer` - view and edit budgets for customers
|
||||
|
||||
Removed permission:
|
||||
- `system_actions` - removed experimental feature to flush app cache from the about screen
|
||||
|
||||
### BC BREAKS
|
||||
|
||||
- API: Format for queries including a datetime object fixed to use HTML5 format (previously `2019-03-02 14:23` - now `2019-03-02T14:23:00`)
|
||||
- **Permission config**: the `permissions` definition in your `local.yaml` needs to be verified/changed, as the internal structure was highly optimized to simplify the definition.
|
||||
Thanks to the new structure, you should be able to remove almost everything from your `local.yaml` (tip: start over from scratch!). Please read [the updated permission docu](https://www.kimai.org/documentation/permissions.html).
|
||||
- default widgets were removed from `kimai.yaml`, that shouldn't cause any issues ... but if something is odd: [look here for help](https://www.kimai.org/documentation/dashboard.html)
|
||||
|
||||
## [0.9](https://github.com/kimai/kimai/releases/tag/0.9)
|
||||
|
||||
Remember to execute the necessary timezone conversion script, if you haven't updated to 0.8 before (see below)!
|
||||
|
||||
### BC BREAKS
|
||||
|
||||
This release contains some BC breaks which were necessary before 1.0 will be released (_now or never_), to prevent those BC breaks after 1.0.
|
||||
|
||||
- **Kimai requires PHP 7.2 now => [PHP 7.1 expired 4 month ago](https://www.php.net/supported-versions.php)**
|
||||
- The `.env` variable `DATABASE_PREFIX` was removed and the table prefix is now hardcoded to `kimai2_`. If you used another prefix,
|
||||
you have to rename your tables manually before starting the update process. You can delete the row `DATABASE_PREFIX` from your `.env` file.
|
||||
- API: Format for DateTime objects changed, now including timezone identifier (previously `2019-03-02 14:23` - now `2019-03-02T14:23:00+00:00`), see [#718](https://github.com/kimai/kimai/pull/718)
|
||||
- API: changed from snake_case to camelCase (affected fields: hourlyRate vs hourly_rate / fixedRate vs fixed_rate / orderNumber vs order_number / i18n config object)
|
||||
- Plugin mechanism changed: existing Plugins have to be deleted or updated
|
||||
|
||||
### Apply necessary changes to your `local.yaml`:
|
||||
|
||||
New permissions are available:
|
||||
- `system_configuration` - for accessing the new system configuration screen
|
||||
- `system_actions` - for the experimental feature to flush your cache from the about screen
|
||||
- `plugins` - for accessing the new plugins screen
|
||||
|
||||
The setting `kimai.timesheet.mode` replaces the setting `kimai.timesheet.duration_only`. If you used the duration_only mode, you need to change your config:
|
||||
```yaml
|
||||
# Before
|
||||
kimai:
|
||||
timesheet:
|
||||
duration_only: true
|
||||
|
||||
# After
|
||||
kimai:
|
||||
timesheet:
|
||||
mode: duration_only
|
||||
```
|
||||
Or switch the mode directly in the new System configuration screen within Kimai.
|
||||
|
||||
## [0.8.1](https://github.com/kimai/kimai/releases/tag/0.8.1)
|
||||
|
||||
A bug fixing release. Remember to execute the necessary timezone conversion script, if you haven't updated to 0.8 before (see below)!
|
||||
|
||||
## [0.8](https://github.com/kimai/kimai/releases/tag/0.8)
|
||||
|
||||
After you followed the normal update and database migration process (see above), you need to execute a bash command to convert your timesheet data for timezone support:
|
||||
|
||||
- Read this [pull request](https://github.com/kimai/kimai/pull/372) BEFORE you follow the instructions to convert the
|
||||
timezones in your existing time records with `bin/console kimai:convert-timezone`. Without that, you will end up with wrong times in your database.
|
||||
|
||||
### Apply necessary changes to your `local.yaml`:
|
||||
|
||||
- A new boolean setting `kimai.timesheet.rules.allow_future_times` was introduced
|
||||
- New permissions are available:
|
||||
- `view_export` - for the new export feature
|
||||
- `create_export` - for the new export feature
|
||||
- `edit_export_own_timesheet` - for the new export feature
|
||||
- `edit_export_other_timesheet` - for the new export feature
|
||||
- `system_information` - to see the new about screen
|
||||
|
||||
## [0.7](https://github.com/kimai/kimai/releases/tag/0.7)
|
||||
|
||||
The configuration `kimai.theme.active_warning` was deprecated and should be replaced in your local.yaml,
|
||||
[read config docs for more information](https://www.kimai.org/documentation/timesheet.html#limit-active-entries).
|
||||
|
||||
## [0.6.1](https://github.com/kimai/kimai/releases/tag/0.6.1)
|
||||
|
||||
A bugfix release to address database compatibility issues with older MySQL/MariaDB versions.
|
||||
|
||||
## [0.6](https://github.com/kimai/kimai/releases/tag/0.6)
|
||||
|
||||
The API has some minor BC breaks: some fields were renamed and entities have a larger attribute set than collections.
|
||||
Be aware that the API is still is development mode and shouldn't be considered stable for now.
|
||||
|
||||
## [0.5](https://github.com/kimai/kimai/releases/tag/0.5)
|
||||
|
||||
Some configuration nodes were removed, if you have one of them in your `local.yaml` you need to delete them before you start the update:
|
||||
- `kimai.invoice.calculator`
|
||||
- `kimai.invoice.renderer`
|
||||
- `kimai.invoice.number_generator`
|
||||
|
||||
The new config `kimai.invoice.documents` was introduced, holding a list of directories ([read more](https://www.kimai.org/documentation/invoices.html)).
|
||||
|
||||
**BC break:** InvoiceTemplate name was changed from 255 characters to 60. If you used longer invoice-template names, they will be truncated when upgrading the database.
|
||||
Please make sure that they are unique in the first 60 character before you upgrade your database with `doctrine:migrations:migrate`.
|
||||
|
||||
## [0.4](https://github.com/kimai/kimai/releases/tag/0.4)
|
||||
|
||||
In the time between 0.3 and 0.4 there was a release of composer that introduced a BC break,
|
||||
which leads to problems between Composer and Symfony Flex, resulting in an error like this when running it:
|
||||
|
||||
```
|
||||
[ErrorException]
|
||||
Declaration of Symfony\Flex\ParallelDownloader::getRemoteContents($originUrl, $fileUrl, $context) should be compatible with Composer\Util\RemoteFilesystem::getRemoteContents($originUrl, $fileUrl, $context, ?array &$responseHeaders = NULL)
|
||||
```
|
||||
|
||||
This can be fixed by updating Composer and Flex before executing the Kimai update:
|
||||
```
|
||||
sudo composer self-update
|
||||
sudo -u www-data composer update symfony/flex --no-plugins --no-scripts
|
||||
```
|
||||
|
||||
## [0.3](https://github.com/kimai/kimai/releases/tag/0.3)
|
||||
|
||||
You need to adjust your `.env` file and add your `from` address for [all emails](https://www.kimai.org/documentation/emails.html) generated by Kimai 2:
|
||||
```
|
||||
MAILER_FROM=kimai@example.com
|
||||
```
|
||||
|
||||
Create a file and database backup before executing the following steps:
|
||||
|
||||
```bash
|
||||
git pull origin master
|
||||
sudo -u www-data composer install --no-dev --optimize-autoloader
|
||||
sudo -u www-data bin/console cache:clear --env=prod
|
||||
sudo -u www-data bin/console cache:warmup --env=prod
|
||||
bin/console doctrine:migrations:version --add 20180701120000
|
||||
bin/console doctrine:migrations:migrate
|
||||
```
|
||||
|
||||
@@ -41,6 +41,11 @@ export default class KimaiAutocomplete extends KimaiFormPlugin {
|
||||
}
|
||||
|
||||
new TomSelect(node, {
|
||||
// see https://github.com/orchidjs/tom-select/issues/543#issuecomment-1664342257
|
||||
onItemAdd: function(){
|
||||
// remove remaining characters from input after selecting an item
|
||||
this.setTextboxValue('');
|
||||
},
|
||||
// if there are more than 500, they need to be found by "typing"
|
||||
maxOptions: 500,
|
||||
// the autocomplete is ONLY used, when the user can create tags
|
||||
|
||||
@@ -79,6 +79,12 @@ export default class KimaiDatePicker extends KimaiFormPlugin {
|
||||
delete picker._wasPreselected;
|
||||
}
|
||||
});
|
||||
|
||||
// only if mobile.friendly plugin is activated
|
||||
if (picker.backdrop !== undefined) {
|
||||
// the node needs to be moved, so the flat form layout works properly (e.g. for date types)
|
||||
document.body.appendChild(picker.backdrop);
|
||||
}
|
||||
},
|
||||
}};
|
||||
|
||||
|
||||
@@ -71,6 +71,11 @@ export default class KimaiFormSelect extends KimaiFormPlugin {
|
||||
*/
|
||||
|
||||
let options = {
|
||||
// see https://github.com/orchidjs/tom-select/issues/543#issuecomment-1664342257
|
||||
onItemAdd: function(){
|
||||
// remove remaining characters from input after selecting an item
|
||||
this.setTextboxValue('');
|
||||
},
|
||||
lockOptgroupOrder: true,
|
||||
allowEmptyOption: !isRequired,
|
||||
hidePlaceholder: false,
|
||||
|
||||
@@ -318,7 +318,7 @@ export default class KimaiTimesheetForm extends KimaiFormPlugin {
|
||||
this._applyDateToField(newBegin.plus({seconds: seconds}), null, this._endTime);
|
||||
} else if (begin === null && end !== null) {
|
||||
this._applyDateToField(end.minus({seconds: seconds}), this._beginDate, this._beginTime);
|
||||
} else if (begin !== null && seconds > 0) {
|
||||
} else if (begin !== null && seconds >= 0) {
|
||||
this._applyDateToField(begin.plus({seconds: seconds}), null, this._endTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -440,8 +440,14 @@ export default class KimaiCalendar {
|
||||
calendarSource = {...calendarSource, ...{
|
||||
id: 'kimai-' + source.id,
|
||||
events: (fetchInfo, successCallback, failureCallback) => {
|
||||
let url = source.url.replace('{from}', DATES.formatForAPI(fetchInfo.start));
|
||||
url = url.replace('{to}', DATES.formatForAPI(fetchInfo.end));
|
||||
const targetFrom = DATES.formatForAPI(fetchInfo.start);
|
||||
const targetTo = DATES.formatForAPI(fetchInfo.end);
|
||||
|
||||
let url = source.url;
|
||||
url = url.replace('{from}', targetFrom);
|
||||
url = url.replace('__FROM__', targetFrom);
|
||||
url = url.replace('{to}', targetTo);
|
||||
url = url.replace('__TO__', targetTo);
|
||||
|
||||
API.get(url, {}, result => {
|
||||
let apiEvents = [];
|
||||
@@ -458,6 +464,29 @@ export default class KimaiCalendar {
|
||||
name: 'google',
|
||||
editable: false,
|
||||
}};
|
||||
} else if (source.type === 'json') {
|
||||
calendarSource = {...calendarSource, ...{
|
||||
id: 'json-' + source.id,
|
||||
editable: false,
|
||||
events: (fetchInfo, successCallback, failureCallback) => {
|
||||
const targetFrom = DATES.formatForAPI(fetchInfo.start);
|
||||
const targetTo = DATES.formatForAPI(fetchInfo.end);
|
||||
|
||||
let url = source.url;
|
||||
url = url.replace('{from}', targetFrom);
|
||||
url = url.replace('__FROM__', targetFrom);
|
||||
url = url.replace('{to}', targetTo);
|
||||
url = url.replace('__TO__', targetTo);
|
||||
|
||||
API.get(url, {}, result => {
|
||||
let apiEvents = [];
|
||||
for (const record of result) {
|
||||
apiEvents.push(record);
|
||||
}
|
||||
successCallback(apiEvents);
|
||||
}, failureCallback);
|
||||
},
|
||||
}};
|
||||
} else if (source.type === 'ical') {
|
||||
calendarSource = {...calendarSource, ...{
|
||||
id: 'ical-' + source.id,
|
||||
@@ -606,7 +635,7 @@ export default class KimaiCalendar {
|
||||
<li>` + this.options['translations']['activity'] + `: ` + escaper.escapeForHtml(eventObj.activity) + `</li>
|
||||
</ul>` +
|
||||
(eventObj.description !== null || eventObj.tags.length > 0 ? '<hr>' : '') +
|
||||
(eventObj.description ? '<p>' + escaper.escapeForHtml(eventObj.description) + '</p>' : '') + tags + `
|
||||
(eventObj.description ? '<div>' + escaper.escapeForHtml(eventObj.description) + '</div>' : '') + tags + `
|
||||
</div>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
div {
|
||||
white-space: pre-line;
|
||||
}
|
||||
}
|
||||
|
||||
.draggable {
|
||||
@@ -13,3 +16,5 @@
|
||||
--bs-gray-400: var(--tblr-border-color);
|
||||
--bs-gray-200: var(--tblr-gray-200);
|
||||
}
|
||||
|
||||
.fc-day-sat, .fc-day-sun { background-color: var(--tblr-bg-surface-secondary); }
|
||||
|
||||
@@ -35,4 +35,22 @@
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
border-radius: var(--tblr-border-radius);
|
||||
}
|
||||
|
||||
/* e.g. the work contract settings is user profile render rows with mb-3 and the last row doesn't need that */
|
||||
fieldset > .mb-3.row:last-child {
|
||||
margin-bottom: 0!important;
|
||||
}
|
||||
|
||||
.form-fieldset-light {
|
||||
border-radius: 0;
|
||||
margin-bottom: 0;
|
||||
border-top: none;
|
||||
border-right: none;
|
||||
border-left: none;
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
.form-fieldset-light:last-child {
|
||||
border:none;
|
||||
}
|
||||
@@ -15,4 +15,20 @@ fieldset.form-fieldset legend {
|
||||
}
|
||||
.dropdown-divider {
|
||||
--tblr-dropdown-divider-margin-y: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
fixes translucent scroll border, e.g. in timesheet modal duration dropdown
|
||||
https://github.com/tabler/tabler/issues/1606
|
||||
*/
|
||||
.dropdown-menu {
|
||||
background-clip: border-box;
|
||||
}
|
||||
|
||||
/*
|
||||
hide empty fieldset
|
||||
https://github.com/tabler/tabler/issues/1650
|
||||
*/
|
||||
fieldset:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -24,17 +24,18 @@
|
||||
"ext-xml": "*",
|
||||
"ext-xsl": "*",
|
||||
"ext-zip": "*",
|
||||
"composer/package-versions-deprecated": "^1.8",
|
||||
"composer-runtime-api": "^2.0",
|
||||
"azuyalabs/yasumi": "^2.6",
|
||||
"composer/semver": "^3.3",
|
||||
"doctrine/doctrine-bundle": "^2.7",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.0",
|
||||
"doctrine/orm": "^2.8",
|
||||
"endroid/qr-code": "^4.4.9",
|
||||
"endroid/qr-code": "^4.8",
|
||||
"erusev/parsedown": "^1.6",
|
||||
"friendsofsymfony/rest-bundle": "^3.0",
|
||||
"gedmo/doctrine-extensions": "^3.6",
|
||||
"jms/serializer-bundle": "^5.0",
|
||||
"kevinpapst/tabler-bundle": "^0.19",
|
||||
"kevinpapst/tabler-bundle": "^1.0",
|
||||
"league/csv": "^9.4",
|
||||
"mpdf/mpdf": "^8.0",
|
||||
"nelmio/api-doc-bundle": "^4.0",
|
||||
@@ -43,8 +44,8 @@
|
||||
"pagerfanta/pagerfanta": "^3.0",
|
||||
"phpoffice/phpspreadsheet": "^1.16",
|
||||
"phpoffice/phpword": "^1.0",
|
||||
"psr/container": "^1.0||^2.0",
|
||||
"psr/log": "^2.0||^3.0",
|
||||
"psr/container": "^2.0",
|
||||
"psr/log": "^3.0",
|
||||
"scheb/2fa-backup-code": "^6.2",
|
||||
"scheb/2fa-bundle": "^6.2",
|
||||
"scheb/2fa-totp": "^6.2",
|
||||
@@ -67,7 +68,7 @@
|
||||
"symfony/translation": "^6.0",
|
||||
"symfony/twig-bundle": "^6.0",
|
||||
"symfony/validator": "^6.0",
|
||||
"symfony/webpack-encore-bundle": "^1.16",
|
||||
"symfony/webpack-encore-bundle": "^2.0",
|
||||
"symfony/yaml": "^6.0",
|
||||
"twig/cssinliner-extra": "^3.0",
|
||||
"twig/extra-bundle": "^3.0",
|
||||
@@ -104,7 +105,6 @@
|
||||
],
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"composer/package-versions-deprecated": true,
|
||||
"symfony/flex": true,
|
||||
"symfony/runtime": true
|
||||
},
|
||||
@@ -129,6 +129,7 @@
|
||||
}
|
||||
},
|
||||
"replace": {
|
||||
"symfony/polyfill-ctype": "*",
|
||||
"symfony/polyfill-mbstring": "*",
|
||||
"symfony/polyfill-intl": "*",
|
||||
"symfony/polyfill-iconv": "*",
|
||||
@@ -179,7 +180,13 @@
|
||||
"tests-unit": "vendor/bin/phpunit --exclude-group integration tests/",
|
||||
"tests-integration": "vendor/bin/phpunit --group integration tests/",
|
||||
"phpstan": [
|
||||
"vendor/bin/phpstan analyse -c phpstan.neon",
|
||||
"@phpstan-src",
|
||||
"@phpstan-tests"
|
||||
],
|
||||
"phpstan-src": [
|
||||
"vendor/bin/phpstan analyse -c phpstan.neon"
|
||||
],
|
||||
"phpstan-tests": [
|
||||
"vendor/bin/phpstan analyse -c tests/phpstan.neon"
|
||||
],
|
||||
"codestyle": "vendor/bin/php-cs-fixer fix --dry-run --verbose --show-progress=none",
|
||||
@@ -195,7 +202,7 @@
|
||||
"symfony": {
|
||||
"id": "01C3FWRDJJEX9K6Y3A4XDFXPBR",
|
||||
"allow-contrib": true,
|
||||
"require": "6.2.*"
|
||||
"require": "6.3.*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2715
composer.lock
generated
2715
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,6 @@ parameters:
|
||||
# environment variables are not available yet.
|
||||
# You should not need to change this value.
|
||||
env(DATABASE_URL): ''
|
||||
env(DATABASE_VERSION): ~
|
||||
|
||||
doctrine:
|
||||
dbal:
|
||||
@@ -15,14 +14,11 @@ doctrine:
|
||||
#schema_filter: ~^(?!(bundle_migration_|kimai2_sessions))~
|
||||
url: '%env(DATABASE_URL)%'
|
||||
driver: 'pdo_mysql'
|
||||
# If this is wrong, automatic database detection might fail and find false-negatives when running
|
||||
# doctrine:migrations:diff on null columns with MariaDB.
|
||||
# This here is mainly a fallback for times, when users have not configured the database version in the URL
|
||||
server_version: '%env(string:DATABASE_VERSION)%'
|
||||
charset: utf8mb4
|
||||
default_table_options:
|
||||
charset: utf8mb4
|
||||
collate: utf8mb4_unicode_ci
|
||||
schema_manager_factory: doctrine.dbal.default_schema_manager_factory
|
||||
|
||||
types:
|
||||
datetime: App\Doctrine\UTCDateTimeType
|
||||
@@ -31,6 +27,7 @@ doctrine:
|
||||
default_entity_manager: default
|
||||
entity_managers:
|
||||
default:
|
||||
report_fields_where_declared: true
|
||||
connection: default
|
||||
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
||||
auto_mapping: true
|
||||
|
||||
@@ -17,8 +17,6 @@ framework:
|
||||
cookie_httponly: true
|
||||
storage_factory_id: session.storage.factory.native
|
||||
|
||||
#esi: ~
|
||||
#fragments: ~
|
||||
php_errors:
|
||||
log: true
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ kimai:
|
||||
TIMESHEET: ['view_own_timesheet','start_own_timesheet','stop_own_timesheet','create_own_timesheet','edit_own_timesheet','export_own_timesheet','delete_own_timesheet','weekly_own_timesheet']
|
||||
TIMESHEET_OTHER: ['view_other_timesheet','start_other_timesheet','stop_other_timesheet','create_other_timesheet','edit_other_timesheet','export_other_timesheet','delete_other_timesheet']
|
||||
PROFILE: ['view_own_profile','edit_own_profile','password_own_profile','preferences_own_profile','api-token_own_profile']
|
||||
PROFILE_OTHER: ['view_other_profile','edit_other_profile','password_other_profile','roles_other_profile','preferences_other_profile','api-token_other_profile','teams_other_profile']
|
||||
PROFILE_OTHER: ['view_other_profile','edit_other_profile','password_other_profile','roles_other_profile','preferences_other_profile','api-token_other_profile','teams_other_profile','supervisor_other_profile']
|
||||
TAGS: ['view_tag','manage_tag','create_tag','delete_tag']
|
||||
USER: ['view_user','create_user','delete_user','role_permissions']
|
||||
RATE: ['view_rate_own_timesheet','edit_rate_own_timesheet']
|
||||
@@ -107,7 +107,7 @@ kimai:
|
||||
REPORTING: ['view_reporting','view_other_reporting','project_reporting','customer_reporting']
|
||||
# permissions which are deactivated, as these features are hidden for now
|
||||
# brave users can try to activate them and be surprised what happens
|
||||
REGISTER_BETA: ['contract_other_profile']
|
||||
REGISTER_BETA: []
|
||||
# mapping a "role name" to an array of "set names"
|
||||
maps:
|
||||
ROLE_USER: ['TIMESHEET','PROFILE']
|
||||
@@ -118,10 +118,8 @@ kimai:
|
||||
roles:
|
||||
ROLE_USER: ['view_team_member','time_team_project','create_tag','view_reporting']
|
||||
ROLE_TEAMLEAD: ['view_rate_own_timesheet','view_rate_other_timesheet','hourly-rate_own_profile','view_team_member']
|
||||
# TODO contract_other_profile
|
||||
ROLE_ADMIN: ['hourly-rate_own_profile','edit_exported_timesheet','teams_own_profile','view_team_member','view_all_data']
|
||||
# TODO contract_other_profile
|
||||
ROLE_SUPER_ADMIN: ['hourly-rate_own_profile','hourly-rate_other_profile','roles_own_profile','system_information','system_configuration','plugins','edit_exported_timesheet','teams_own_profile','view_team_member','upload_invoice_template','view_all_data']
|
||||
ROLE_ADMIN: ['hourly-rate_own_profile','edit_exported_timesheet','teams_own_profile','view_team_member','view_all_data','contract_other_profile']
|
||||
ROLE_SUPER_ADMIN: ['hourly-rate_own_profile','hourly-rate_other_profile','roles_own_profile','supervisor_own_profile','system_information','system_configuration','plugins','edit_exported_timesheet','teams_own_profile','view_team_member','upload_invoice_template','view_all_data','contract_other_profile']
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -153,11 +151,13 @@ kimai:
|
||||
# --------------------------------------------------------------------------------
|
||||
# INVOICES
|
||||
# --------------------------------------------------------------------------------
|
||||
# invoice:
|
||||
# # all files in these directories will be used as invoice documents (if supported by a renderer)
|
||||
# documents:
|
||||
# - 'var/invoices/'
|
||||
# - 'templates/invoice/renderer/'
|
||||
# invoice:
|
||||
# # all files in these directories will be used as invoice documents (if supported by a renderer)
|
||||
# documents:
|
||||
# - 'var/invoices/'
|
||||
# - 'templates/invoice/renderer/'
|
||||
# # allow to upload twig templates as invoice documents
|
||||
# upload_twig: true
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
@@ -65,6 +65,12 @@ security:
|
||||
max_attempts: 5
|
||||
interval: '5 minutes'
|
||||
|
||||
login_link:
|
||||
check_route: link_login_check
|
||||
signature_properties: ['id']
|
||||
lifetime: 300
|
||||
max_uses: 1
|
||||
|
||||
access_decision_manager:
|
||||
strategy: unanimous
|
||||
allow_if_all_abstain: false
|
||||
|
||||
@@ -78,6 +78,8 @@ tabler:
|
||||
filter: fas fa-filter
|
||||
help: far fa-question-circle
|
||||
home: fas fa-home
|
||||
info: fas fa-info-circle
|
||||
import: fas fa-file-import
|
||||
invoice: fas fa-file-contract
|
||||
invoice-template: fas fa-file-signature
|
||||
left: fas fa-chevron-left
|
||||
@@ -98,6 +100,7 @@ tabler:
|
||||
password: fas fa-key
|
||||
pause: fas fa-pause
|
||||
pause-small: far fa-pause-circle
|
||||
pending: fas fa-hourglass-half
|
||||
pin: fas fa-thumbtack
|
||||
pdf: fas fa-file-pdf
|
||||
permissions: fas fa-user-lock
|
||||
@@ -111,6 +114,7 @@ tabler:
|
||||
reporting: far fa-chart-bar
|
||||
report: far fa-chart-bar
|
||||
review: fas fa-user-check
|
||||
rejected: fas fa-ban
|
||||
right: fas fa-chevron-right
|
||||
right2: fas fa-angle-double-right
|
||||
roles: fas fa-user-shield
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
controllers:
|
||||
resource: ../src/Controller/
|
||||
type: annotation
|
||||
type: attribute
|
||||
prefix: /{_locale}
|
||||
requirements:
|
||||
_locale: '%app_locales%'
|
||||
@@ -19,17 +19,17 @@ api.swagger:
|
||||
|
||||
api:
|
||||
resource: ../src/API/
|
||||
type: annotation
|
||||
type: attribute
|
||||
prefix: /api
|
||||
|
||||
auth:
|
||||
resource: ../src/Controller/Auth/
|
||||
type: annotation
|
||||
type: attribute
|
||||
prefix: /auth
|
||||
|
||||
security:
|
||||
resource: ../src/Controller/Security/
|
||||
type: annotation
|
||||
type: attribute
|
||||
prefix: /{_locale}
|
||||
requirements:
|
||||
_locale: '%app_locales%'
|
||||
@@ -38,7 +38,7 @@ security:
|
||||
|
||||
kernel:
|
||||
resource: ../src/Kernel.php
|
||||
type: annotation
|
||||
type: attribute
|
||||
|
||||
home:
|
||||
path: /
|
||||
|
||||
@@ -91,16 +91,10 @@ services:
|
||||
App\Doctrine\TimesheetSubscriber:
|
||||
class: App\Doctrine\TimesheetSubscriber
|
||||
arguments: [!tagged timesheet.calculator]
|
||||
tags:
|
||||
- { name: doctrine.event_subscriber, priority: 50 }
|
||||
|
||||
# updates timestampable columns (higher priority, so the TimesheetSubscriber will be executed later)
|
||||
Gedmo\Timestampable\TimestampableListener:
|
||||
class: Gedmo\Timestampable\TimestampableListener
|
||||
tags:
|
||||
- { name: doctrine.event_subscriber, priority: 60 }
|
||||
calls:
|
||||
- [ setAnnotationReader, [ "@annotation_reader" ] ]
|
||||
App\Doctrine\ModifiedSubscriber:
|
||||
class: App\Doctrine\ModifiedSubscriber
|
||||
|
||||
# ================================================================================
|
||||
# TIMESHEET RECORD CALCULATOR
|
||||
|
||||
@@ -41,6 +41,6 @@ final class Version20210316224358 extends AbstractMigration
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP TABLE kimai2_bookmarks');
|
||||
$schema->dropTable('kimai2_bookmarks');
|
||||
}
|
||||
}
|
||||
|
||||
36
migrations/Version20230606125948.php
Normal file
36
migrations/Version20230606125948.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use App\Doctrine\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
/**
|
||||
* @version 2.0.26
|
||||
*/
|
||||
final class Version20230606125948 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Adds the visible column for tags';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$tags = $schema->getTable('kimai2_tags');
|
||||
$tags->addColumn('visible', 'boolean', ['notnull' => false, 'default' => true]);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$tags = $schema->getTable('kimai2_tags');
|
||||
$tags->dropColumn('visible');
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
41
migrations/Version20230819090536.php
Normal file
41
migrations/Version20230819090536.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* @version 2.0.31
|
||||
*/
|
||||
final class Version20230819090536 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Adds the supervisor columns to the user-table';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$table = $schema->getTable('kimai2_users');
|
||||
$table->addColumn('supervisor_id', 'integer', ['length' => 11, 'notnull' => false, 'default' => null]);
|
||||
$table->addForeignKeyConstraint('kimai2_users', ['supervisor_id'], ['id'], ['onDelete' => 'SET NULL'], 'FK_B9AC5BCE19E9AC5F');
|
||||
$table->addIndex(['supervisor_id'], 'IDX_B9AC5BCE19E9AC5F');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$table = $schema->getTable('kimai2_users');
|
||||
|
||||
$table->removeForeignKey('FK_B9AC5BCE19E9AC5F');
|
||||
$table->dropIndex('IDX_B9AC5BCE19E9AC5F');
|
||||
$table->dropColumn('supervisor_id');
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
951
phpstan.neon
951
phpstan.neon
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,7 @@
|
||||
<env name="APP_ENV" value="test" force="true"/>
|
||||
<env name="APP_DEBUG" value="0" force="true"/>
|
||||
<env name="APP_SECRET" value="5a79a1c866efef9ca1800f971d689f3e" force="true"/>
|
||||
<env name="DATABASE_URL" value="mysql://kimai2_test:kimai2_test@127.0.0.1:3306/kimai2_test"/>
|
||||
<env name="DATABASE_URL" value="mysql://kimai2_test:kimai2_test@127.0.0.1:3306/kimai2_test?serverVersion=10.5.8-MariaDB"/>
|
||||
<env name="CORS_ALLOW_ORIGIN" value="^https?://localhost(:[0-9]+)?$"/>
|
||||
<env name="MAILER_URL" value="null://null"/>
|
||||
<env name="MAILER_FROM" value="kimai@example.com"/>
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
public/build/app.9acfcfc8.js
Normal file
2
public/build/app.9acfcfc8.js
Normal file
File diff suppressed because one or more lines are too long
1
public/build/app.fc41168c.css
Normal file
1
public/build/app.fc41168c.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -3,10 +3,10 @@
|
||||
"app": {
|
||||
"js": [
|
||||
"/build/runtime.f0079159.js",
|
||||
"/build/app.79ed1fb4.js"
|
||||
"/build/app.9acfcfc8.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/app.5710a463.css"
|
||||
"/build/app.fc41168c.css"
|
||||
]
|
||||
},
|
||||
"export-pdf": {
|
||||
@@ -45,7 +45,7 @@
|
||||
"calendar": {
|
||||
"js": [
|
||||
"/build/runtime.f0079159.js",
|
||||
"/build/calendar.25b368b9.js"
|
||||
"/build/calendar.3f4f78fe.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/calendar.bb473428.css"
|
||||
@@ -63,8 +63,8 @@
|
||||
},
|
||||
"integrity": {
|
||||
"/build/runtime.f0079159.js": "sha384-H22sAW1aTvyIPqvHOvGXWSWTxf0y6mptp+MsVmyXCfjx/WJjBbhX9gbUZ+qIuihV",
|
||||
"/build/app.79ed1fb4.js": "sha384-QQ83F5EDtEEzO9+/Sg8jAkxsM/GVnZmTdC/a7p83t4zxVU1VYPD2AKp21pcM1Lj1",
|
||||
"/build/app.5710a463.css": "sha384-kqam1K3HVJSgDgp3YsKvzA5VGQQB2USKq6UbuIHQvn9tshMribY5Axgg2ar7vTIR",
|
||||
"/build/app.9acfcfc8.js": "sha384-tzbF0uEmHghIo+D5IuKarLYkwB3AyrLMTEFqR5mqaXCP5VSOWx3t+ZfD81kd80T+",
|
||||
"/build/app.fc41168c.css": "sha384-3Td9W7KogJoBiywzWOgpkPPNHEIT8/a/SkMZM5NgIRmuChj/3PeOKkOHVJQlhP2E",
|
||||
"/build/export-pdf.d367a32e.js": "sha384-Z5baqnzjI636nYFs4g63ViIKBZKRW4Jhv/7PQmTEQlqhfA7eK0vUMUtiyy0R5A9u",
|
||||
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
|
||||
"/build/invoice.2604495e.js": "sha384-D6JvhGSqlx7z72b/qD3nF3QDXPy+XsCSRGtWfs1icjDKOcd2UzuXwuSa/E1Fg2TJ",
|
||||
@@ -72,7 +72,7 @@
|
||||
"/build/invoice-pdf.ce9aace0.js": "sha384-mtZkXrpsXFMX2YvmmTlCm91QgsTlZLzLJnDAFcTuBDtDZ7TNkqLYkfCeLzFLVfwb",
|
||||
"/build/invoice-pdf.2b749265.css": "sha384-DXXgkz2WWnrWnfBnXX5fmfPQSPb98upMnWxYKwTGYS04EhrPIWfDCutB2unIrWh7",
|
||||
"/build/chart.f5becfac.js": "sha384-GSqETm8wULiVXyizvwRompfwu63r/C0Qd/AvrHDE4cqAKiIGCssb3QyBtGu1WN+W",
|
||||
"/build/calendar.25b368b9.js": "sha384-jKqj+OqQVIm8TXCEmnRveqG3348yVwvlb0k88kRn90DZGzbthjToMHUb5pwL/TtI",
|
||||
"/build/calendar.3f4f78fe.js": "sha384-RD8enOf0Foe7IhSOGnv2DCes4QUCvLjlONH0HFq3RK9l/ozrFZakTX6SWwKOoEda",
|
||||
"/build/calendar.bb473428.css": "sha384-900W9o8666Qpw21rLP7hn5Ql8tWd40k0IcE3QpFm7E7V6bKva7xQgGlyzVuuh4GK",
|
||||
"/build/dashboard.6774a712.js": "sha384-lBwkNqUPv+IBbznagiFakY2BOIueq/Bg89wHO2XeM9YAZ51rPkrvdMd+XmFupHsM",
|
||||
"/build/dashboard.18f5a8b7.css": "sha384-PBD8ftb2yBoSjRe2sN5Xb4dEz045kOEUfcIufdSis+tWoWdAx5j4Yic+F/6CYbrP"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"build/app.css": "/build/app.5710a463.css",
|
||||
"build/app.js": "/build/app.79ed1fb4.js",
|
||||
"build/app.css": "/build/app.fc41168c.css",
|
||||
"build/app.js": "/build/app.9acfcfc8.js",
|
||||
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
|
||||
"build/export-pdf.js": "/build/export-pdf.d367a32e.js",
|
||||
"build/invoice.css": "/build/invoice.5bea118e.css",
|
||||
@@ -9,7 +9,7 @@
|
||||
"build/invoice-pdf.js": "/build/invoice-pdf.ce9aace0.js",
|
||||
"build/chart.js": "/build/chart.f5becfac.js",
|
||||
"build/calendar.css": "/build/calendar.bb473428.css",
|
||||
"build/calendar.js": "/build/calendar.25b368b9.js",
|
||||
"build/calendar.js": "/build/calendar.3f4f78fe.js",
|
||||
"build/dashboard.css": "/build/dashboard.18f5a8b7.css",
|
||||
"build/dashboard.js": "/build/dashboard.6774a712.js",
|
||||
"build/runtime.js": "/build/runtime.f0079159.js"
|
||||
|
||||
@@ -21,10 +21,10 @@ use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Nelmio\ApiDocBundle\Annotation\Model;
|
||||
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
#[Route(path: '/actions')]
|
||||
|
||||
@@ -26,8 +26,8 @@ use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
@@ -112,6 +112,7 @@ final class ActivityController extends BaseApiController
|
||||
$query->setSearchTerm(new SearchTerm($term));
|
||||
}
|
||||
|
||||
$query->setIsApiCall(true);
|
||||
$data = $this->repository->getActivitiesForQuery($query);
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(self::GROUPS_COLLECTION);
|
||||
|
||||
@@ -10,12 +10,12 @@
|
||||
namespace App\API;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\Query\BaseQuery;
|
||||
use App\Timesheet\DateTimeFactory;
|
||||
use App\Utils\Pagination;
|
||||
use FOS\RestBundle\View\View;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormTypeInterface;
|
||||
|
||||
@@ -30,13 +30,13 @@ abstract class BaseApiController extends AbstractController
|
||||
|
||||
/**
|
||||
* @template TFormType of FormTypeInterface<TData>
|
||||
* @template TData of mixed
|
||||
* @template TData of BaseQuery
|
||||
* @param class-string<TFormType> $type
|
||||
* @param TData|null $data
|
||||
* @param TData $data
|
||||
* @param array<mixed> $options
|
||||
* @return FormInterface<TData|null>
|
||||
* @return FormInterface<BaseQuery>
|
||||
*/
|
||||
protected function createSearchForm(string $type = FormType::class, mixed $data = null, array $options = []): FormInterface
|
||||
protected function createSearchForm(string $type, BaseQuery $data, array $options = []): FormInterface
|
||||
{
|
||||
return $this->container
|
||||
->get('form.factory')
|
||||
|
||||
@@ -26,8 +26,8 @@ use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
@@ -90,6 +90,7 @@ final class CustomerController extends BaseApiController
|
||||
$query->setSearchTerm(new SearchTerm($term));
|
||||
}
|
||||
|
||||
$query->setIsApiCall(true);
|
||||
$data = $this->repository->getCustomersForQuery($query);
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(self::GROUPS_COLLECTION);
|
||||
|
||||
93
src/API/Model/CalendarEvent.php
Normal file
93
src/API/Model/CalendarEvent.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\API\Model;
|
||||
|
||||
use App\Utils\Color;
|
||||
use JMS\Serializer\Annotation as Serializer;
|
||||
|
||||
#[Serializer\ExclusionPolicy('all')]
|
||||
final class CalendarEvent
|
||||
{
|
||||
/**
|
||||
* Calendar entry title
|
||||
*/
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Serializer\Type(name: 'string')]
|
||||
private string $title; // @phpstan-ignore-line
|
||||
/**
|
||||
* Calendar background color
|
||||
*/
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Serializer\Type(name: 'string')]
|
||||
private ?string $color = null; // @phpstan-ignore-line
|
||||
/**
|
||||
* Calendar text color
|
||||
*/
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Serializer\Type(name: 'string')]
|
||||
private ?string $textColor = null;
|
||||
/**
|
||||
* If this entry is all-day long
|
||||
*/
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Serializer\Type(name: 'boolean')]
|
||||
private bool $allDay = false; // @phpstan-ignore-line
|
||||
/**
|
||||
* Calendar entry start date
|
||||
*/
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Serializer\Type(name: 'DateTime')]
|
||||
private \DateTimeInterface $start; // @phpstan-ignore-line
|
||||
/**
|
||||
* Calendar entry end date
|
||||
*/
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Serializer\Type(name: 'DateTime')]
|
||||
private \DateTimeInterface $end; // @phpstan-ignore-line
|
||||
|
||||
public function setTitle(string $title): void
|
||||
{
|
||||
$this->title = $title;
|
||||
}
|
||||
|
||||
public function setStart(\DateTimeInterface $start): void
|
||||
{
|
||||
$this->start = $start;
|
||||
}
|
||||
|
||||
public function setEnd(\DateTimeInterface $end): void
|
||||
{
|
||||
$this->end = $end;
|
||||
}
|
||||
|
||||
public function setColor(?string $color): void
|
||||
{
|
||||
$this->color = $color;
|
||||
if ($color !== null && $this->textColor === null) {
|
||||
$this->textColor = (new Color())->getFontContrastColor($color);
|
||||
}
|
||||
}
|
||||
|
||||
public function setAllDay(bool $allDay): void
|
||||
{
|
||||
$this->allDay = $allDay;
|
||||
}
|
||||
|
||||
public function setTextColor(?string $textColor): void
|
||||
{
|
||||
$this->textColor = $textColor;
|
||||
}
|
||||
}
|
||||
@@ -27,8 +27,8 @@ use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
@@ -144,6 +144,7 @@ final class ProjectController extends BaseApiController
|
||||
$query->setSearchTerm(new SearchTerm($term));
|
||||
}
|
||||
|
||||
$query->setIsApiCall(true);
|
||||
$data = $this->repository->getProjectsForQuery($query);
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(self::GROUPS_COLLECTION);
|
||||
|
||||
@@ -33,6 +33,7 @@ use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\Form\FormError;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -40,7 +41,6 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Component\Validator\Constraints;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
#[Route(path: '/timesheets')]
|
||||
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
|
||||
@@ -93,7 +93,7 @@ final class TimesheetController extends BaseApiController
|
||||
#[Rest\QueryParam(name: 'exported', requirements: '0|1', strict: true, nullable: true, description: 'Use this flag if you want to filter for export state. Allowed values: 0=not exported, 1=exported (default: all)')]
|
||||
#[Rest\QueryParam(name: 'active', requirements: '0|1', strict: true, nullable: true, description: 'Filter for running/active records. Allowed values: 0=stopped, 1=active (default: all)')]
|
||||
#[Rest\QueryParam(name: 'billable', requirements: '0|1', strict: true, nullable: true, description: 'Filter for non-/billable records. Allowed values: 0=non-billable, 1=billable (default: all)')]
|
||||
#[Rest\QueryParam(name: 'full', strict: true, nullable: true, description: 'Allows to fetch fully serialized objects including subresources. Allowed values: true (default: false)')]
|
||||
#[Rest\QueryParam(name: 'full', requirements: '0|1|true|false', strict: true, nullable: true, description: 'Allows to fetch full objects including subresources. Allowed values: 0|1|false|true (default: false)')]
|
||||
#[Rest\QueryParam(name: 'term', description: 'Free search term')]
|
||||
#[Rest\QueryParam(name: 'modified_after', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records changed after this date will be included (format: HTML5). Available since Kimai 1.10 and works only for records that were created/updated since then.')]
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher, CustomerRepository $customerRepository, ProjectRepository $projectRepository, ActivityRepository $activityRepository, UserRepository $userRepository): Response
|
||||
@@ -180,9 +180,10 @@ final class TimesheetController extends BaseApiController
|
||||
$query->setPageSize((int) $size);
|
||||
}
|
||||
|
||||
/** @var array<string> $tags */
|
||||
$tags = $paramFetcher->get('tags');
|
||||
if (\is_array($tags) && \count($tags) > 0) {
|
||||
$tags = $this->tagRepository->findTagsByName($tags);
|
||||
$tags = $this->tagRepository->findTagsByName($tags, true);
|
||||
foreach ($tags as $tag) {
|
||||
$query->addTag($tag);
|
||||
}
|
||||
@@ -249,13 +250,15 @@ final class TimesheetController extends BaseApiController
|
||||
$query->setModifiedAfter($factory->createDateTime($modifiedAfter));
|
||||
}
|
||||
|
||||
$query->setIsApiCall(true);
|
||||
$data = $this->repository->getPagerfantaForQuery($query);
|
||||
$results = (array) $data->getCurrentPageResults();
|
||||
|
||||
$view = new View($results, 200);
|
||||
$this->addPagination($view, $data);
|
||||
|
||||
if (null !== $paramFetcher->get('full')) {
|
||||
$full = $paramFetcher->get('full');
|
||||
if ($full === '1' || $full === 'true') {
|
||||
$view->getContext()->setGroups(self::GROUPS_COLLECTION_FULL);
|
||||
} else {
|
||||
$view->getContext()->setGroups(self::GROUPS_COLLECTION);
|
||||
|
||||
@@ -23,7 +23,7 @@ use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
@@ -39,6 +39,7 @@ final class UserController extends BaseApiController
|
||||
public const GROUPS_ENTITY = ['Default', 'Entity', 'User', 'User_Entity'];
|
||||
public const GROUPS_FORM = ['Default', 'Entity', 'User', 'User_Entity'];
|
||||
public const GROUPS_COLLECTION = ['Default', 'Collection', 'User'];
|
||||
public const GROUPS_COLLECTION_FULL = ['Default', 'Collection', 'User', 'User_Entity'];
|
||||
|
||||
public function __construct(
|
||||
private ViewHandlerInterface $viewHandler,
|
||||
@@ -60,6 +61,7 @@ final class UserController extends BaseApiController
|
||||
#[Rest\QueryParam(name: 'orderBy', requirements: 'id|username|alias|email', strict: true, nullable: true, description: 'The field by which results will be ordered. Allowed values: id, username, alias, email (default: username)')]
|
||||
#[Rest\QueryParam(name: 'order', requirements: 'ASC|DESC', strict: true, nullable: true, description: 'The result order. Allowed values: ASC, DESC (default: ASC)')]
|
||||
#[Rest\QueryParam(name: 'term', description: 'Free search term')]
|
||||
#[Rest\QueryParam(name: 'full', requirements: '0|1|true|false', strict: true, nullable: true, description: 'Allows to fetch full objects including subresources. Allowed values: 0|1|false|true (default: false)')]
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher): Response
|
||||
{
|
||||
$query = new UserQuery();
|
||||
@@ -85,9 +87,16 @@ final class UserController extends BaseApiController
|
||||
$query->setSearchTerm(new SearchTerm($term));
|
||||
}
|
||||
|
||||
$query->setIsApiCall(true);
|
||||
$data = $this->repository->getUsersForQuery($query);
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(self::GROUPS_COLLECTION);
|
||||
|
||||
$full = $paramFetcher->get('full');
|
||||
if ($full === '1' || $full === 'true') {
|
||||
$view->getContext()->setGroups(self::GROUPS_COLLECTION_FULL);
|
||||
} else {
|
||||
$view->getContext()->setGroups(self::GROUPS_COLLECTION);
|
||||
}
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
@@ -197,6 +206,7 @@ final class UserController extends BaseApiController
|
||||
'include_roles' => $this->isGranted('roles', $profile),
|
||||
'include_active_flag' => ($profile->getId() !== $this->getUser()->getId()),
|
||||
'include_preferences' => $this->isGranted('preferences', $profile),
|
||||
'include_supervisor' => $this->isGranted('supervisor', $profile),
|
||||
]);
|
||||
|
||||
$form->setData($profile);
|
||||
|
||||
@@ -20,7 +20,7 @@ use App\Event\ActivityUpdatePreEvent;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Validator\ValidationFailedException;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,10 +14,11 @@ use App\Entity\User;
|
||||
use App\Event\CalendarConfigurationEvent;
|
||||
use App\Event\CalendarDragAndDropSourceEvent;
|
||||
use App\Event\CalendarGoogleSourceEvent;
|
||||
use App\Event\CalendarSourceEvent;
|
||||
use App\Event\RecentActivityEvent;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Utils\Color;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
final class CalendarService
|
||||
{
|
||||
@@ -81,6 +82,23 @@ final class CalendarService
|
||||
return new Google($apiKey, $sources);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<CalendarSource>
|
||||
*/
|
||||
public function getSources(User $user): array
|
||||
{
|
||||
$sources = [];
|
||||
|
||||
$event = new CalendarSourceEvent($user);
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
foreach ($event->getSources() as $source) {
|
||||
$sources[] = $source;
|
||||
}
|
||||
|
||||
return $sources;
|
||||
}
|
||||
|
||||
public function getConfiguration(): array
|
||||
{
|
||||
$config = [
|
||||
|
||||
58
src/Calendar/CalendarSource.php
Normal file
58
src/Calendar/CalendarSource.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Calendar;
|
||||
|
||||
class CalendarSource
|
||||
{
|
||||
/** @var array<string, string|bool|int> */
|
||||
private array $options = [];
|
||||
|
||||
public function __construct(private CalendarSourceType $type, private string $id, private string $uri, private ?string $color = null)
|
||||
{
|
||||
}
|
||||
|
||||
public function getType(): CalendarSourceType
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function getTypeName(): string
|
||||
{
|
||||
return $this->type->value;
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getUri(): string
|
||||
{
|
||||
return $this->uri;
|
||||
}
|
||||
|
||||
public function getColor(): ?string
|
||||
{
|
||||
return $this->color;
|
||||
}
|
||||
|
||||
public function addOption(string $name, int|bool|string $value): void
|
||||
{
|
||||
$this->options[$name] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, bool|int|string>
|
||||
*/
|
||||
public function getOptions(): array
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,12 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Widget\Type;
|
||||
namespace App\Calendar;
|
||||
|
||||
/**
|
||||
* @covers \App\Widget\Type\AbstractSimpleStatisticChart
|
||||
*/
|
||||
abstract class AbstractSimpleStatisticsWidgetTypeTest extends AbstractWidgetTypeTest
|
||||
enum CalendarSourceType: string
|
||||
{
|
||||
case GOOGLE = 'google';
|
||||
case ICAL = 'ical';
|
||||
case JSON = 'json';
|
||||
case TIMESHEET = 'timesheet';
|
||||
}
|
||||
@@ -9,24 +9,10 @@
|
||||
|
||||
namespace App\Calendar;
|
||||
|
||||
final class GoogleSource
|
||||
final class GoogleSource extends CalendarSource
|
||||
{
|
||||
public function __construct(private string $id, private string $uri, private ?string $color = null)
|
||||
public function __construct(string $id, string $uri, ?string $color = null)
|
||||
{
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getUri(): string
|
||||
{
|
||||
return $this->uri;
|
||||
}
|
||||
|
||||
public function getColor(): ?string
|
||||
{
|
||||
return $this->color;
|
||||
parent::__construct(CalendarSourceType::GOOGLE, $id, $uri, $color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ abstract class AbstractBundleInstallerCommand extends Command
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
protected function installAssets(SymfonyStyle $io, OutputInterface $output)
|
||||
protected function installAssets(SymfonyStyle $io, OutputInterface $output): void
|
||||
{
|
||||
$command = $this->getApplication()->find('assets:install');
|
||||
$cmdInput = new ArrayInput([]);
|
||||
@@ -160,12 +160,12 @@ abstract class AbstractBundleInstallerCommand extends Command
|
||||
$io->writeln('');
|
||||
}
|
||||
|
||||
protected function importMigrations(SymfonyStyle $io, OutputInterface $output)
|
||||
protected function importMigrations(SymfonyStyle $io, OutputInterface $output): void
|
||||
{
|
||||
$config = $this->getMigrationConfigFilename();
|
||||
|
||||
if (null === $config) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file_exists($config)) {
|
||||
|
||||
@@ -56,5 +56,5 @@ abstract class AbstractRoleCommand extends Command
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
abstract protected function executeRoleCommand(UserService $manipulator, SymfonyStyle $output, User $user, bool $super, $role);
|
||||
abstract protected function executeRoleCommand(UserService $manipulator, SymfonyStyle $output, User $user, bool $super, $role): void;
|
||||
}
|
||||
|
||||
@@ -33,10 +33,7 @@ final class DemoteUserCommand extends AbstractRoleCommand
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function executeRoleCommand(UserService $manipulator, SymfonyStyle $output, User $user, bool $super, $role)
|
||||
protected function executeRoleCommand(UserService $manipulator, SymfonyStyle $output, User $user, bool $super, $role): void
|
||||
{
|
||||
$username = $user->getUserIdentifier();
|
||||
if ($super) {
|
||||
|
||||
@@ -46,10 +46,7 @@ final class ExportCreateCommand extends Command
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure()
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setDescription('Create exports')
|
||||
|
||||
@@ -22,6 +22,7 @@ use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Timesheet\DateTimeFactory;
|
||||
use App\Utils\SearchTerm;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
@@ -32,7 +33,6 @@ use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
#[AsCommand(name: 'kimai:invoice:create')]
|
||||
final class InvoiceCreateCommand extends Command
|
||||
|
||||
@@ -33,7 +33,7 @@ final class PromoteUserCommand extends AbstractRoleCommand
|
||||
);
|
||||
}
|
||||
|
||||
protected function executeRoleCommand(UserService $manipulator, SymfonyStyle $output, User $user, bool $super, $role)
|
||||
protected function executeRoleCommand(UserService $manipulator, SymfonyStyle $output, User $user, bool $super, $role): void
|
||||
{
|
||||
$username = $user->getUserIdentifier();
|
||||
if ($super) {
|
||||
|
||||
@@ -52,7 +52,7 @@ final class ReloadCommand extends Command
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$io->title('Reloading configurations ...');
|
||||
$io->text('Validating config file syntax ...');
|
||||
|
||||
// many users execute the bin/console command from arbitrary locations
|
||||
$path = getcwd();
|
||||
@@ -106,7 +106,7 @@ final class ReloadCommand extends Command
|
||||
]
|
||||
);
|
||||
|
||||
return (int) $cacheResult;
|
||||
return $cacheResult;
|
||||
}
|
||||
|
||||
$io->success(
|
||||
@@ -116,7 +116,7 @@ final class ReloadCommand extends Command
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function rebuildCaches(string $environment, SymfonyStyle $io, InputInterface $input, OutputInterface $output)
|
||||
private function rebuildCaches(string $environment, SymfonyStyle $io, InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io->text('Rebuilding your cache, please be patient ...');
|
||||
|
||||
|
||||
89
src/Command/UserLoginLinkCommand.php
Normal file
89
src/Command/UserLoginLinkCommand.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Repository\UserRepository;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Security\Http\LoginLink\LoginLinkHandlerInterface;
|
||||
|
||||
#[AsCommand(name: 'kimai:user:login-link', description: 'Create a URL that can be used to login as that user', hidden: true)]
|
||||
/**
|
||||
* @CloudRequired
|
||||
*/
|
||||
final class UserLoginLinkCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private LoginLinkHandlerInterface $loginLink,
|
||||
private UserRepository $userRepository,
|
||||
private RequestStack $requestStack
|
||||
)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->addArgument('email', InputArgument::REQUIRED, 'The email of the user');
|
||||
$this->addOption('password-reset', null, InputOption::VALUE_NONE, 'Whether the user needs to reset the password afterwards');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$email = $input->getArgument('email');
|
||||
if ($email === null || $email === '') {
|
||||
$io->error('Need email to create login URL');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$user = $this->userRepository->findOneBy(['email' => $email]);
|
||||
|
||||
if ($user === null) {
|
||||
$io->error('Need username to create login URL');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
if (!$user->isEnabled()) {
|
||||
$io->error('User is not enabled');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
if (!$user->isInternalUser()) {
|
||||
$io->error('User does not use internal login');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$request = new Request();
|
||||
$request->setLocale($user->getLocale());
|
||||
$this->requestStack->push($request);
|
||||
|
||||
$loginLinkDetails = $this->loginLink->createLoginLink($user, $request);
|
||||
$loginLink = $loginLinkDetails->getUrl();
|
||||
|
||||
if ($input->getOption('password-reset') === true) {
|
||||
$user->setPasswordRequestedAt(new \DateTime());
|
||||
$user->setRequiresPasswordReset(true);
|
||||
$this->userRepository->saveUser($user);
|
||||
}
|
||||
|
||||
$output->writeln($loginLink);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,6 @@
|
||||
|
||||
namespace App\Configuration;
|
||||
|
||||
use App\Constants;
|
||||
|
||||
final class LocaleService
|
||||
{
|
||||
public function __construct(private array $languageSettings)
|
||||
@@ -32,11 +30,6 @@ final class LocaleService
|
||||
return \in_array($language, $this->getAllLocales());
|
||||
}
|
||||
|
||||
public function getDefaultLocale(): string
|
||||
{
|
||||
return Constants::DEFAULT_LOCALE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the locale specific date format, which should be used in combination with the twig filter "|date".
|
||||
*
|
||||
|
||||
@@ -32,14 +32,11 @@ final class SystemConfiguration
|
||||
|
||||
/**
|
||||
* Set an array item to a given value using "dot" notation.
|
||||
*
|
||||
* If no key is given to the method, the entire array will be replaced.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @return void
|
||||
* @internal
|
||||
*/
|
||||
private function set(string $key, $value): void
|
||||
public function set(string $key, mixed $value): void
|
||||
{
|
||||
if (\array_key_exists($key, $this->settings)) {
|
||||
if (\is_bool($this->settings[$key])) {
|
||||
@@ -125,31 +122,35 @@ final class SystemConfiguration
|
||||
|
||||
// ========== Array access methods ==========
|
||||
|
||||
/**
|
||||
* @deprecated since 2.0.35
|
||||
*/
|
||||
public function offsetExists($offset): bool
|
||||
{
|
||||
return $this->has($offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 2.0.35
|
||||
*/
|
||||
public function offsetGet($offset): mixed
|
||||
{
|
||||
return $this->find($offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $offset
|
||||
* @param mixed $value
|
||||
* @throws \BadMethodCallException
|
||||
* @deprecated since 2.0.35
|
||||
*/
|
||||
public function offsetSet($offset, $value)
|
||||
public function offsetSet(mixed $offset, mixed $value): void
|
||||
{
|
||||
$this->set($offset, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $offset
|
||||
* @deprecated since 2.0.35
|
||||
* @throws \BadMethodCallException
|
||||
*/
|
||||
public function offsetUnset($offset)
|
||||
public function offsetUnset(mixed $offset): void
|
||||
{
|
||||
throw new \BadMethodCallException('SystemBundleConfiguration does not support offsetUnset()');
|
||||
}
|
||||
|
||||
@@ -17,11 +17,11 @@ class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '2.0.24';
|
||||
public const VERSION = '2.1.0';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 20024;
|
||||
public const VERSION_ID = 20100;
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
@@ -31,17 +31,13 @@ class Constants
|
||||
*/
|
||||
public const GITHUB = 'https://github.com/kimai/kimai/';
|
||||
/**
|
||||
* The Github repository name
|
||||
* The GitHub repository name
|
||||
*/
|
||||
public const GITHUB_REPO = 'kimai/kimai';
|
||||
/**
|
||||
* Homepage, used in multiple views
|
||||
*/
|
||||
public const HOMEPAGE = 'https://www.kimai.org';
|
||||
/**
|
||||
* Application wide default locale
|
||||
*/
|
||||
public const DEFAULT_LOCALE = 'en';
|
||||
/**
|
||||
* Default color for Customer, Project and Activity entities
|
||||
*/
|
||||
|
||||
@@ -17,7 +17,6 @@ use App\Timesheet\DateTimeFactory;
|
||||
use App\Validator\ValidationFailedException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as BaseAbstractController;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\FormError;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormTypeInterface;
|
||||
@@ -76,13 +75,13 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
||||
|
||||
/**
|
||||
* @template TFormType of FormTypeInterface<TData>
|
||||
* @template TData of mixed
|
||||
* @template TData of array|object
|
||||
* @param class-string<TFormType> $type
|
||||
* @param TData|null $data
|
||||
* @param TData $data
|
||||
* @param array<mixed> $options
|
||||
* @return FormInterface<TData|null>
|
||||
* @return FormInterface<TData>
|
||||
*/
|
||||
protected function createFormWithName(string $name, string $type = FormType::class, mixed $data = null, array $options = []): FormInterface
|
||||
protected function createFormWithName(string $name, string $type, mixed $data, array $options = []): FormInterface
|
||||
{
|
||||
return $this->container->get('form.factory')->createNamed($name, $type, $data, $options);
|
||||
}
|
||||
@@ -122,17 +121,12 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
||||
* Adds an "error" flash message to the stack.
|
||||
*
|
||||
* @param string $translationKey
|
||||
* @param array<string, string>|string $reason passing an array is deprecated
|
||||
* @param string $reason
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function flashError(string $translationKey, array|string $reason = ''): void
|
||||
protected function flashError(string $translationKey, string $reason = ''): void
|
||||
{
|
||||
if (\is_array($reason)) {
|
||||
@trigger_error('Calling "flashError" with an array $reason is deprecated and will be removed soon. Refactor and pass a string instead.', E_USER_DEPRECATED);
|
||||
$reason = \array_key_exists('%reason%', $reason) ? $reason['%reason%'] : '';
|
||||
}
|
||||
|
||||
$this->addFlashTranslated('error', $translationKey, ['%reason%' => $reason]);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ use App\Repository\TeamRepository;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
use Exception;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -47,7 +47,6 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
* Controller used to manage activities.
|
||||
*/
|
||||
#[Route(path: '/admin/activity')]
|
||||
#[IsGranted(new Expression("is_granted('view_activity') or is_granted('view_teamlead_activity') or is_granted('view_team_activity')"))]
|
||||
final class ActivityController extends AbstractController
|
||||
{
|
||||
public function __construct(private ActivityRepository $repository, private SystemConfiguration $configuration, private EventDispatcherInterface $dispatcher, private ActivityService $activityService)
|
||||
@@ -56,6 +55,7 @@ final class ActivityController extends AbstractController
|
||||
|
||||
#[Route(path: '/', defaults: ['page' => 1], name: 'admin_activity', methods: ['GET'])]
|
||||
#[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'admin_activity_paginated', methods: ['GET'])]
|
||||
#[IsGranted(new Expression("is_granted('listing', 'activity')"))]
|
||||
public function indexAction(int $page, Request $request): Response
|
||||
{
|
||||
$query = new ActivityQuery();
|
||||
@@ -329,7 +329,11 @@ final class ActivityController extends AbstractController
|
||||
$this->activityService->updateActivity($activity);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('activity_details', ['id' => $activity->getId()]);
|
||||
if ($this->isGranted('view', $activity)) {
|
||||
return $this->redirectToRoute('activity_details', ['id' => $activity->getId()]);
|
||||
} else {
|
||||
return new Response();
|
||||
}
|
||||
} catch (Exception $ex) {
|
||||
$this->flashUpdateException($ex);
|
||||
}
|
||||
@@ -389,6 +393,7 @@ final class ActivityController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/export', name: 'activity_export', methods: ['GET'])]
|
||||
#[IsGranted(new Expression("is_granted('listing', 'activity')"))]
|
||||
public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter): Response
|
||||
{
|
||||
$query = new ActivityQuery();
|
||||
|
||||
@@ -25,7 +25,7 @@ final class SamlController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/login', name: 'saml_login')]
|
||||
public function loginAction(Request $request)
|
||||
public function loginAction(Request $request): Response
|
||||
{
|
||||
if (!$this->samlConfiguration->isActivated()) {
|
||||
throw $this->createNotFoundException('SAML deactivated');
|
||||
@@ -50,11 +50,19 @@ final class SamlController extends AbstractController
|
||||
throw new \RuntimeException($error);
|
||||
}
|
||||
|
||||
$this->authFactory->create()->login($session->get('_security.main.target_path'));
|
||||
// this does set headers and exit as $stay is not set to true
|
||||
$url = $this->authFactory->create()->login($session->get('_security.main.target_path'));
|
||||
|
||||
if ($url === null) {
|
||||
throw new \RuntimeException('SAML login failed');
|
||||
}
|
||||
|
||||
// this line is not (yet) reached, as the previous call will exit
|
||||
return $this->redirect($url);
|
||||
}
|
||||
|
||||
#[Route(path: '/metadata', name: 'saml_metadata')]
|
||||
public function metadataAction()
|
||||
public function metadataAction(): Response
|
||||
{
|
||||
if (!$this->samlConfiguration->isActivated()) {
|
||||
throw $this->createNotFoundException('SAML deactivated');
|
||||
@@ -69,7 +77,7 @@ final class SamlController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/acs', name: 'saml_acs')]
|
||||
public function assertionConsumerServiceAction()
|
||||
public function assertionConsumerServiceAction(): Response
|
||||
{
|
||||
if (!$this->samlConfiguration->isActivated()) {
|
||||
throw $this->createNotFoundException('SAML deactivated');
|
||||
@@ -79,7 +87,7 @@ final class SamlController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/logout', name: 'saml_logout')]
|
||||
public function logoutAction()
|
||||
public function logoutAction(): Response
|
||||
{
|
||||
if (!$this->samlConfiguration->isActivated()) {
|
||||
throw $this->createNotFoundException('SAML deactivated');
|
||||
|
||||
@@ -94,6 +94,7 @@ final class CalendarController extends AbstractController
|
||||
'config' => $config,
|
||||
'dragAndDrop' => $dragAndDrop,
|
||||
'google' => $this->calendarService->getGoogleSources($profile),
|
||||
'sources' => $this->calendarService->getSources($profile),
|
||||
'now' => $factory->createDateTime(),
|
||||
'defaultStartTime' => $defaultStart,
|
||||
'is_punch_mode' => $isPunchMode,
|
||||
|
||||
@@ -16,10 +16,10 @@ use App\Reporting\YearByUser\YearByUser;
|
||||
use App\Utils\PageSetup;
|
||||
use App\WorkingTime\Model\BoxConfiguration;
|
||||
use App\WorkingTime\WorkingTimeService;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* Users can control their working time statistics
|
||||
@@ -33,6 +33,7 @@ final class ContractController extends AbstractController
|
||||
$dateTimeFactory = $this->getDateTimeFactory($currentUser);
|
||||
$canChangeUser = $this->isGranted('contract_other_profile');
|
||||
$defaultDate = $dateTimeFactory->createStartOfYear();
|
||||
$now = $dateTimeFactory->createDateTime();
|
||||
|
||||
$values = new YearByUser();
|
||||
$values->setUser($currentUser);
|
||||
@@ -62,16 +63,18 @@ final class ContractController extends AbstractController
|
||||
|
||||
/** @var \DateTime $yearDate */
|
||||
$yearDate = $values->getDate();
|
||||
$year = $workingTimeService->getYear($profile, $yearDate);
|
||||
$year = $workingTimeService->getYear($profile, $yearDate, $now);
|
||||
|
||||
$page = new PageSetup('status');
|
||||
$page = new PageSetup('work_times');
|
||||
$page->setHelp('contract.html');
|
||||
$page->setActionName('contract');
|
||||
$page->setActionPayload(['profile' => $profile, 'year' => $yearDate]);
|
||||
$page->setPaginationForm($form);
|
||||
|
||||
// additional boxes by plugins
|
||||
$controllerEvent = new WorkContractDetailControllerEvent($year);
|
||||
$eventDispatcher->dispatch($controllerEvent);
|
||||
|
||||
$now = $dateTimeFactory->createDateTime();
|
||||
$summary = $workingTimeService->getYearSummary($year, $now);
|
||||
|
||||
$boxConfiguration = new BoxConfiguration();
|
||||
|
||||
@@ -36,7 +36,7 @@ use App\Repository\Query\ProjectQuery;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -47,10 +47,9 @@ use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
* Controller used to manage customer in the admin part of the site.
|
||||
* Controller used to manage customers.
|
||||
*/
|
||||
#[Route(path: '/admin/customer')]
|
||||
#[IsGranted(new Expression("is_granted('view_customer') or is_granted('view_teamlead_customer') or is_granted('view_team_customer')"))]
|
||||
final class CustomerController extends AbstractController
|
||||
{
|
||||
public function __construct(private CustomerRepository $repository, private EventDispatcherInterface $dispatcher)
|
||||
@@ -59,6 +58,7 @@ final class CustomerController extends AbstractController
|
||||
|
||||
#[Route(path: '/', defaults: ['page' => 1], name: 'admin_customer', methods: ['GET'])]
|
||||
#[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'admin_customer_paginated', methods: ['GET'])]
|
||||
#[IsGranted(new Expression("is_granted('listing', 'customer')"))]
|
||||
public function indexAction(int $page, Request $request): Response
|
||||
{
|
||||
$query = new CustomerQuery();
|
||||
@@ -452,6 +452,7 @@ final class CustomerController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/export', name: 'customer_export', methods: ['GET'])]
|
||||
#[IsGranted(new Expression("is_granted('listing', 'customer')"))]
|
||||
public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter): Response
|
||||
{
|
||||
$query = new CustomerQuery();
|
||||
@@ -492,7 +493,11 @@ final class CustomerController extends AbstractController
|
||||
return $this->redirectToRouteAfterCreate('customer_details', ['id' => $customer->getId()]);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('customer_details', ['id' => $customer->getId()]);
|
||||
if ($this->isGranted('view', $customer)) {
|
||||
return $this->redirectToRoute('customer_details', ['id' => $customer->getId()]);
|
||||
} else {
|
||||
return new Response();
|
||||
}
|
||||
} catch (\Exception $ex) {
|
||||
$this->handleFormUpdateException($ex, $editForm);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ use App\Repository\BookmarkRepository;
|
||||
use App\Utils\PageSetup;
|
||||
use App\Widget\WidgetInterface;
|
||||
use App\Widget\WidgetService;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
@@ -120,27 +120,25 @@ final class DoctorController extends AbstractController
|
||||
/** @var array<string, string> $versions */
|
||||
$versions = [];
|
||||
|
||||
if (class_exists(InstalledVersions::class)) {
|
||||
$rootPackage = InstalledVersions::getRootPackage()['name'];
|
||||
foreach (InstalledVersions::getInstalledPackages() as $package) {
|
||||
$versions[$package] = InstalledVersions::getPrettyVersion($package);
|
||||
$rootPackage = InstalledVersions::getRootPackage()['name'];
|
||||
foreach (InstalledVersions::getInstalledPackages() as $package) {
|
||||
$versions[$package] = InstalledVersions::getPrettyVersion($package);
|
||||
}
|
||||
|
||||
// remove kimai from the package list
|
||||
$versions = array_filter($versions, function ($version, $name) use ($rootPackage): bool {
|
||||
if ($name === $rootPackage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// remove kimai from the package list
|
||||
$versions = array_filter($versions, function ($version, $name) use ($rootPackage): bool {
|
||||
if ($name === $rootPackage) {
|
||||
return false;
|
||||
}
|
||||
if ($version === null || $version === '*') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($version === null || $version === '*') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, ARRAY_FILTER_USE_BOTH);
|
||||
|
||||
return true;
|
||||
}, ARRAY_FILTER_USE_BOTH);
|
||||
|
||||
ksort($versions);
|
||||
}
|
||||
ksort($versions);
|
||||
|
||||
return $versions;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ namespace App\Controller;
|
||||
use App\Configuration\LocaleService;
|
||||
use App\Entity\User;
|
||||
use App\Event\ConfigureMainMenuEvent;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use App\Repository\UserRepository;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
@@ -28,7 +29,7 @@ final class HomepageController extends AbstractController
|
||||
public const DEFAULT_ROUTE = 'timesheet';
|
||||
|
||||
#[Route(path: '', defaults: [], name: 'homepage', methods: ['GET'])]
|
||||
public function indexAction(Request $request, LocaleService $service, EventDispatcherInterface $eventDispatcher): Response
|
||||
public function homepage(Request $request, LocaleService $service, EventDispatcherInterface $eventDispatcher, UserRepository $userRepository): Response
|
||||
{
|
||||
$user = $this->getUser();
|
||||
$userLanguage = $user->getLanguage();
|
||||
@@ -45,7 +46,7 @@ final class HomepageController extends AbstractController
|
||||
// if a user somehow managed to get a wrong locale into hos account (eg. an imported user from Kimai 1)
|
||||
// make sure that he will still see a beautiful page and not a 404
|
||||
if (!$service->isKnownLocale($userLanguage)) {
|
||||
$userLanguage = $service->getDefaultLocale();
|
||||
$userLanguage = 'en';
|
||||
}
|
||||
|
||||
$routes = [];
|
||||
@@ -72,8 +73,13 @@ final class HomepageController extends AbstractController
|
||||
try {
|
||||
return $this->redirectToRoute($route, ['_locale' => $language]);
|
||||
} catch (\Exception $ex) {
|
||||
$this->logException($ex);
|
||||
// something is wrong with the url parameters ...
|
||||
if ($route === $userRoute) {
|
||||
// fix invalid routes from old plugins / versions
|
||||
$user->setPreferenceValue('login_initial_view', 'dashboard');
|
||||
$userRepository->saveUser($user);
|
||||
} else {
|
||||
$this->logException($ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ use App\Repository\Query\InvoiceQuery;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
use Exception;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
@@ -48,7 +49,6 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Csrf\CsrfToken;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
use Twig\Environment;
|
||||
|
||||
/**
|
||||
@@ -387,7 +387,7 @@ final class InvoiceController extends AbstractController
|
||||
|
||||
#[Route(path: '/export', name: 'invoice_export', methods: ['GET'])]
|
||||
#[IsGranted('view_invoice')]
|
||||
public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter)
|
||||
public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter): Response
|
||||
{
|
||||
$query = new InvoiceArchiveQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
@@ -433,7 +433,7 @@ final class InvoiceController extends AbstractController
|
||||
$table->addColumn('calculator', ['class' => 'd-none', 'orderBy' => false, 'title' => 'invoice_calculator', 'translation_domain' => 'invoice-calculator']);
|
||||
$table->addColumn('renderer', ['class' => 'd-none', 'orderBy' => false, 'title' => 'invoice_renderer', 'translation_domain' => 'invoice-renderer']);
|
||||
$table->addColumn('language', ['class' => 'd-none text-nowrap', 'orderBy' => false]);
|
||||
$table->addColumn('actions', ['class' => 'actions', 'orderBy' => false]);
|
||||
$table->addColumn('actions', ['class' => 'actions']);
|
||||
|
||||
$page = $this->createPageSetup('admin_invoice_template.title');
|
||||
$page->setDataTable($table);
|
||||
@@ -468,36 +468,6 @@ final class InvoiceController extends AbstractController
|
||||
throw $this->createNotFoundException('Unknown document: ' . $document);
|
||||
}
|
||||
|
||||
#[Route(path: '/document_reload/{document}', name: 'admin_invoice_document_reload', methods: ['GET', 'POST'])]
|
||||
#[IsGranted('upload_invoice_template')]
|
||||
public function reloadDocument(string $document, Environment $twig): Response
|
||||
{
|
||||
$event = new InvoiceDocumentsEvent($this->service->getDocuments(true));
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
$reloaded = false;
|
||||
|
||||
foreach ($event->getInvoiceDocuments() as $doc) {
|
||||
if ($document === $doc->getId() && $doc->isTwig()) {
|
||||
$reloaded = true;
|
||||
try {
|
||||
$twig->enableAutoReload();
|
||||
$twig->load('@invoice/' . basename($doc->getFilename()));
|
||||
$twig->disableAutoReload();
|
||||
$this->flashSuccess('Reloaded template');
|
||||
} catch (Exception $ex) {
|
||||
$this->flashException($ex, 'Failed to reload template: ' . $ex->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$reloaded) {
|
||||
throw $this->createNotFoundException('Unknown document: ' . $document);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_invoice_document_upload');
|
||||
}
|
||||
|
||||
#[Route(path: '/document_upload', name: 'admin_invoice_document_upload', methods: ['GET', 'POST'])]
|
||||
#[IsGranted('upload_invoice_template')]
|
||||
public function uploadDocumentAction(Request $request, string $projectDirectory, InvoiceDocumentRepository $documentRepository, Environment $twig, SystemConfiguration $systemConfiguration): Response
|
||||
|
||||
@@ -22,7 +22,7 @@ use App\Security\RolePermissionManager;
|
||||
use App\Security\RoleService;
|
||||
use App\User\PermissionService;
|
||||
use App\Utils\PageSetup;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
@@ -46,7 +46,7 @@ final class PermissionController extends AbstractController
|
||||
|
||||
#[Route(path: '', name: 'admin_user_permissions', methods: ['GET', 'POST'])]
|
||||
#[IsGranted('role_permissions')]
|
||||
public function permissions(EventDispatcherInterface $dispatcher, CsrfTokenManagerInterface $csrfTokenManager, RoleService $roleService)
|
||||
public function permissions(EventDispatcherInterface $dispatcher, CsrfTokenManagerInterface $csrfTokenManager, RoleService $roleService): Response
|
||||
{
|
||||
$all = $this->roleRepository->findAll();
|
||||
$existing = [];
|
||||
|
||||
@@ -32,8 +32,8 @@ use Endroid\QrCode\Encoding\Encoding;
|
||||
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh;
|
||||
use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeMargin;
|
||||
use Endroid\QrCode\Writer\PngWriter;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Totp\TotpAuthenticatorInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -60,9 +60,12 @@ final class ProfileController extends AbstractController
|
||||
{
|
||||
$dateFactory = $this->getDateTimeFactory();
|
||||
$userStats = $repository->getUserStatistics($profile);
|
||||
$firstEntry = $statisticService->findFirstRecordDate($profile);
|
||||
$workStartingDay = $profile->getWorkStartingDay();
|
||||
if ($workStartingDay === null) {
|
||||
$workStartingDay = $statisticService->findFirstRecordDate($profile);
|
||||
}
|
||||
|
||||
$begin = $firstEntry ?? $dateFactory->getStartOfMonth();
|
||||
$begin = $workStartingDay ?? $dateFactory->getStartOfMonth();
|
||||
$end = $dateFactory->getEndOfMonth();
|
||||
|
||||
// statistic service does not fill up the complete year by default!
|
||||
@@ -73,7 +76,7 @@ final class ProfileController extends AbstractController
|
||||
'tab' => 'charts',
|
||||
'user' => $profile,
|
||||
'stats' => $userStats,
|
||||
'firstTimesheet' => $firstEntry,
|
||||
'workingSince' => $workStartingDay,
|
||||
'workMonths' => $statisticService->getMonthlyStats($begin, $end, [$profile])[0]
|
||||
];
|
||||
|
||||
@@ -271,9 +274,9 @@ final class ProfileController extends AbstractController
|
||||
// prepare ordered preferences
|
||||
$sections = [];
|
||||
|
||||
/** @var \ArrayIterator $iterator */
|
||||
/** @var \ArrayIterator<int, UserPreference> $iterator */
|
||||
$iterator = $profile->getPreferences()->getIterator();
|
||||
$iterator->uasort(function (UserPreference $a, UserPreference $b) {
|
||||
$iterator->uasort(function ($a, $b) {
|
||||
return ($a->getOrder() < $b->getOrder()) ? -1 : 1;
|
||||
});
|
||||
|
||||
@@ -313,7 +316,8 @@ final class ProfileController extends AbstractController
|
||||
'action' => $this->generateUrl('user_profile_edit', ['username' => $user->getUserIdentifier()]),
|
||||
'method' => 'POST',
|
||||
'include_active_flag' => ($user->getId() !== $this->getUser()->getId()),
|
||||
'include_preferences' => false,
|
||||
'include_preferences' => true,
|
||||
'include_supervisor' => $this->isGranted('supervisor', $user),
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -413,6 +417,7 @@ final class ProfileController extends AbstractController
|
||||
'form' => $form->createView(),
|
||||
'deactivate' => $this->getTwoFactorDeactivationForm($profile)->createView(),
|
||||
'qr_code' => $result,
|
||||
'secret' => $profile->getTotpSecret(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ use App\Repository\TeamRepository;
|
||||
use App\Utils\Context;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -54,7 +54,6 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
* Controller used to manage projects.
|
||||
*/
|
||||
#[Route(path: '/admin/project')]
|
||||
#[IsGranted(new Expression("is_granted('view_project') or is_granted('view_teamlead_project') or is_granted('view_team_project')"))]
|
||||
final class ProjectController extends AbstractController
|
||||
{
|
||||
public function __construct(private ProjectRepository $repository, private SystemConfiguration $configuration, private EventDispatcherInterface $dispatcher, private ProjectService $projectService)
|
||||
@@ -63,6 +62,7 @@ final class ProjectController extends AbstractController
|
||||
|
||||
#[Route(path: '/', defaults: ['page' => 1], name: 'admin_project', methods: ['GET'])]
|
||||
#[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'admin_project_paginated', methods: ['GET'])]
|
||||
#[IsGranted(new Expression("is_granted('listing', 'project')"))]
|
||||
public function indexAction(int $page, Request $request): Response
|
||||
{
|
||||
$query = new ProjectQuery();
|
||||
@@ -435,7 +435,11 @@ final class ProjectController extends AbstractController
|
||||
$this->projectService->updateProject($project);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('project_details', ['id' => $project->getId()]);
|
||||
if ($this->isGranted('view', $project)) {
|
||||
return $this->redirectToRoute('project_details', ['id' => $project->getId()]);
|
||||
} else {
|
||||
return new Response();
|
||||
}
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashUpdateException($ex);
|
||||
}
|
||||
@@ -512,6 +516,7 @@ final class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/export', name: 'project_export', methods: ['GET'])]
|
||||
#[IsGranted(new Expression("is_granted('listing', 'project')"))]
|
||||
public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter): Response
|
||||
{
|
||||
$query = new ProjectQuery();
|
||||
|
||||
@@ -18,6 +18,7 @@ use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\TimesheetService;
|
||||
use App\Utils\PageSetup;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
@@ -33,7 +34,7 @@ final class QuickEntryController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{begin}', name: 'quick_entry', methods: ['GET', 'POST'])]
|
||||
public function quickEntry(Request $request, ?string $begin = null)
|
||||
public function quickEntry(Request $request, ?string $begin = null): Response
|
||||
{
|
||||
$factory = $this->getDateTimeFactory();
|
||||
|
||||
|
||||
53
src/Controller/Reporting/CustomerViewController.php
Normal file
53
src/Controller/Reporting/CustomerViewController.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Controller\Reporting;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use App\Customer\CustomerStatisticService;
|
||||
use App\Reporting\CustomerView\CustomerViewForm;
|
||||
use App\Reporting\CustomerView\CustomerViewQuery;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
final class CustomerViewController extends AbstractController
|
||||
{
|
||||
#[Route(path: '/reporting/customer_view', name: 'report_customer_view', methods: ['GET', 'POST'])]
|
||||
#[IsGranted('report:customer')]
|
||||
#[IsGranted(new Expression("is_granted('budget_any', 'customer')"))]
|
||||
public function __invoke(Request $request, CustomerStatisticService $service): Response
|
||||
{
|
||||
$dateFactory = $this->getDateTimeFactory();
|
||||
$user = $this->getUser();
|
||||
|
||||
$query = new CustomerViewQuery($dateFactory->createDateTime(), $user);
|
||||
$form = $this->createFormForGetRequest(CustomerViewForm::class, $query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
$customers = $service->findCustomersForView($query);
|
||||
$entries = $service->getCustomerView($user, $customers, $query->getToday());
|
||||
|
||||
$byCustomer = [];
|
||||
foreach ($entries as $entry) {
|
||||
$customer = $entry->getCustomer();
|
||||
$byCustomer[$customer->getId()] = $entry;
|
||||
}
|
||||
|
||||
return $this->render('reporting/customer_view.html.twig', [
|
||||
'entries' => $byCustomer,
|
||||
'form' => $form->createView(),
|
||||
'report_title' => 'report_customer_view',
|
||||
'tableName' => 'customer_view_reporting',
|
||||
'now' => $dateFactory->createDateTime(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ use App\Reporting\ProjectDateRange\ProjectDateRangeForm;
|
||||
use App\Reporting\ProjectDateRange\ProjectDateRangeQuery;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
@@ -24,7 +25,7 @@ final class ProjectDateRangeController extends AbstractController
|
||||
#[Route(path: '/reporting/project_daterange', name: 'report_project_daterange', methods: ['GET', 'POST'])]
|
||||
#[IsGranted('report:project')]
|
||||
#[IsGranted(new Expression("is_granted('budget_any', 'project')"))]
|
||||
public function __invoke(Request $request, ProjectStatisticService $service)
|
||||
public function __invoke(Request $request, ProjectStatisticService $service): Response
|
||||
{
|
||||
$dateFactory = $this->getDateTimeFactory();
|
||||
$user = $this->getUser();
|
||||
|
||||
@@ -16,6 +16,7 @@ use App\Reporting\ProjectDetails\ProjectDetailsQuery;
|
||||
use App\Utils\PageSetup;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
@@ -24,7 +25,7 @@ final class ProjectDetailsController extends AbstractController
|
||||
#[Route(path: '/reporting/project_details', name: 'report_project_details', methods: ['GET'])]
|
||||
#[IsGranted('report:project')]
|
||||
#[IsGranted(new Expression("is_granted('details', 'project')"))]
|
||||
public function __invoke(Request $request, ProjectStatisticService $service)
|
||||
public function __invoke(Request $request, ProjectStatisticService $service): Response
|
||||
{
|
||||
$dateFactory = $this->getDateTimeFactory();
|
||||
$user = $this->getUser();
|
||||
|
||||
@@ -15,6 +15,7 @@ use App\Reporting\ProjectInactive\ProjectInactiveForm;
|
||||
use App\Reporting\ProjectInactive\ProjectInactiveQuery;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
@@ -23,7 +24,7 @@ final class ProjectInactiveController extends AbstractController
|
||||
#[Route(path: '/reporting/project_inactive', name: 'report_project_inactive', methods: ['GET', 'POST'])]
|
||||
#[IsGranted('report:project')]
|
||||
#[IsGranted(new Expression("is_granted('budget_any', 'project')"))]
|
||||
public function __invoke(Request $request, ProjectStatisticService $service)
|
||||
public function __invoke(Request $request, ProjectStatisticService $service): Response
|
||||
{
|
||||
$dateFactory = $this->getDateTimeFactory();
|
||||
$user = $this->getUser();
|
||||
|
||||
@@ -15,6 +15,7 @@ use App\Reporting\ProjectView\ProjectViewForm;
|
||||
use App\Reporting\ProjectView\ProjectViewQuery;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
@@ -23,7 +24,7 @@ final class ProjectViewController extends AbstractController
|
||||
#[Route(path: '/reporting/project_view', name: 'report_project_view', methods: ['GET', 'POST'])]
|
||||
#[IsGranted('report:project')]
|
||||
#[IsGranted(new Expression("is_granted('budget_any', 'project')"))]
|
||||
public function __invoke(Request $request, ProjectStatisticService $service)
|
||||
public function __invoke(Request $request, ProjectStatisticService $service): Response
|
||||
{
|
||||
$dateFactory = $this->getDateTimeFactory();
|
||||
$user = $this->getUser();
|
||||
|
||||
@@ -42,7 +42,7 @@ final class ReportUsersMonthController extends AbstractController
|
||||
{
|
||||
$data = $this->getData($request, $statisticService, $userRepository);
|
||||
|
||||
$content = $this->container->get('twig')->render('reporting/report_user_list_export.html.twig', $data);
|
||||
$content = $this->renderView('reporting/report_user_list_export.html.twig', $data);
|
||||
|
||||
$reader = new Html();
|
||||
$spreadsheet = $reader->loadFromString($content);
|
||||
|
||||
@@ -42,7 +42,7 @@ final class ReportUsersWeekController extends AbstractController
|
||||
{
|
||||
$data = $this->getData($request, $statisticService, $userRepository);
|
||||
|
||||
$content = $this->container->get('twig')->render('reporting/report_user_list_export.html.twig', $data);
|
||||
$content = $this->renderView('reporting/report_user_list_export.html.twig', $data);
|
||||
|
||||
$reader = new Html();
|
||||
$spreadsheet = $reader->loadFromString($content);
|
||||
|
||||
@@ -19,7 +19,6 @@ use App\Reporting\YearlyUserList\YearlyUserListForm;
|
||||
use App\Repository\Query\UserQuery;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Timesheet\TimesheetStatisticService;
|
||||
use Exception;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Html;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -30,11 +29,6 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
#[IsGranted('report:other')]
|
||||
final class ReportUsersYearController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws Exception
|
||||
*/
|
||||
#[Route(path: '/year', name: 'report_yearly_users', methods: ['GET', 'POST'])]
|
||||
public function report(Request $request, SystemConfiguration $systemConfiguration, TimesheetStatisticService $statisticService, UserRepository $userRepository): Response
|
||||
{
|
||||
@@ -44,17 +38,12 @@ final class ReportUsersYearController extends AbstractController
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws Exception
|
||||
*/
|
||||
#[Route(path: '/year_export', name: 'report_yearly_users_export', methods: ['GET', 'POST'])]
|
||||
public function export(Request $request, SystemConfiguration $systemConfiguration, TimesheetStatisticService $statisticService, UserRepository $userRepository): Response
|
||||
{
|
||||
$data = $this->getData($request, $systemConfiguration, $statisticService, $userRepository);
|
||||
|
||||
$content = $this->container->get('twig')->render('reporting/report_user_list_monthly_export.html.twig', $data);
|
||||
$content = $this->renderView('reporting/report_user_list_monthly_export.html.twig', $data);
|
||||
|
||||
$reader = new Html();
|
||||
$spreadsheet = $reader->loadFromString($content);
|
||||
|
||||
@@ -10,10 +10,13 @@
|
||||
namespace App\Controller\Reporting;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter;
|
||||
use App\Export\Spreadsheet\Writer\XlsxWriter;
|
||||
use App\Model\DailyStatistic;
|
||||
use App\Reporting\MonthByUser\MonthByUser;
|
||||
use App\Reporting\MonthByUser\MonthByUserForm;
|
||||
use Exception;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Html;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
@@ -35,6 +38,21 @@ final class UserMonthController extends AbstractUserReportController
|
||||
return $this->render('reporting/report_by_user.html.twig', $this->getData($request));
|
||||
}
|
||||
|
||||
#[Route(path: '/month_export', name: 'report_user_month_export', methods: ['GET', 'POST'])]
|
||||
public function export(Request $request): Response
|
||||
{
|
||||
$data = $this->getData($request);
|
||||
|
||||
$content = $this->renderView('reporting/report_by_user_data.html.twig', $data);
|
||||
|
||||
$reader = new Html();
|
||||
$spreadsheet = $reader->loadFromString($content);
|
||||
|
||||
$writer = new BinaryFileResponseWriter(new XlsxWriter(), 'kimai-export-user-monthly');
|
||||
|
||||
return $writer->getFileResponse($spreadsheet);
|
||||
}
|
||||
|
||||
private function getData(Request $request): array
|
||||
{
|
||||
$currentUser = $this->getUser();
|
||||
@@ -95,6 +113,7 @@ final class UserMonthController extends AbstractUserReportController
|
||||
'current' => $start,
|
||||
'next' => $nextMonth,
|
||||
'previous' => $previousMonth,
|
||||
'export_route' => 'report_user_month_export',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,13 @@
|
||||
|
||||
namespace App\Controller\Reporting;
|
||||
|
||||
use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter;
|
||||
use App\Export\Spreadsheet\Writer\XlsxWriter;
|
||||
use App\Model\DailyStatistic;
|
||||
use App\Reporting\WeekByUser\WeekByUser;
|
||||
use App\Reporting\WeekByUser\WeekByUserForm;
|
||||
use Exception;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Html;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
@@ -34,6 +37,21 @@ final class UserWeekController extends AbstractUserReportController
|
||||
return $this->render('reporting/report_by_user.html.twig', $this->getData($request));
|
||||
}
|
||||
|
||||
#[Route(path: '/week_export', name: 'report_user_week_export', methods: ['GET', 'POST'])]
|
||||
public function export(Request $request): Response
|
||||
{
|
||||
$data = $this->getData($request);
|
||||
|
||||
$content = $this->renderView('reporting/report_by_user_data.html.twig', $data);
|
||||
|
||||
$reader = new Html();
|
||||
$spreadsheet = $reader->loadFromString($content);
|
||||
|
||||
$writer = new BinaryFileResponseWriter(new XlsxWriter(), 'kimai-export-user-weekly');
|
||||
|
||||
return $writer->getFileResponse($spreadsheet);
|
||||
}
|
||||
|
||||
private function getData(Request $request): array
|
||||
{
|
||||
$currentUser = $this->getUser();
|
||||
@@ -88,6 +106,7 @@ final class UserWeekController extends AbstractUserReportController
|
||||
'current' => $start,
|
||||
'next' => $next,
|
||||
'previous' => $previous,
|
||||
'export_route' => 'report_user_week_export',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ namespace App\Controller\Reporting;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\User;
|
||||
use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter;
|
||||
use App\Export\Spreadsheet\Writer\XlsxWriter;
|
||||
use App\Model\DateStatisticInterface;
|
||||
use App\Model\MonthlyStatistic;
|
||||
use App\Reporting\YearByUser\YearByUser;
|
||||
@@ -18,6 +20,7 @@ use App\Reporting\YearByUser\YearByUserForm;
|
||||
use DateTime;
|
||||
use DateTimeInterface;
|
||||
use Exception;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Html;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
@@ -39,6 +42,21 @@ final class UserYearController extends AbstractUserReportController
|
||||
return $this->render('reporting/report_by_user_year.html.twig', $this->getData($request, $systemConfiguration));
|
||||
}
|
||||
|
||||
#[Route(path: '/year_export', name: 'report_user_year_export', methods: ['GET', 'POST'])]
|
||||
public function export(Request $request, SystemConfiguration $systemConfiguration): Response
|
||||
{
|
||||
$data = $this->getData($request, $systemConfiguration);
|
||||
|
||||
$content = $this->renderView('reporting/report_by_user_year_export.html.twig', $data);
|
||||
|
||||
$reader = new Html();
|
||||
$spreadsheet = $reader->loadFromString($content);
|
||||
|
||||
$writer = new BinaryFileResponseWriter(new XlsxWriter(), 'kimai-export-user-yearly');
|
||||
|
||||
return $writer->getFileResponse($spreadsheet);
|
||||
}
|
||||
|
||||
private function getData(Request $request, SystemConfiguration $systemConfiguration): array
|
||||
{
|
||||
$currentUser = $this->getUser();
|
||||
@@ -104,6 +122,7 @@ final class UserYearController extends AbstractUserReportController
|
||||
'current' => $start,
|
||||
'next' => $next,
|
||||
'previous' => $previous,
|
||||
'export_route' => 'report_user_year_export',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
27
src/Controller/Security/LoginLinkController.php
Normal file
27
src/Controller/Security/LoginLinkController.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Controller\Security;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
#[Route(path: '/auth/link')]
|
||||
/**
|
||||
* @CloudRequired
|
||||
*/
|
||||
final class LoginLinkController extends AbstractController
|
||||
{
|
||||
#[Route(path: '/check', name: 'link_login_check', methods: ['GET'])]
|
||||
public function check(): Response
|
||||
{
|
||||
return new Response();
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ use App\Form\PasswordResetForm;
|
||||
use App\User\LoginManager;
|
||||
use App\User\UserService;
|
||||
use DateTime;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -26,7 +27,6 @@ use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
#[Route(path: '/resetting')]
|
||||
@@ -108,7 +108,7 @@ final class PasswordResetController extends AbstractController
|
||||
}
|
||||
|
||||
return $this->render('security/password-reset/check_email.html.twig', [
|
||||
'tokenLifetime' => ceil($this->configuration->getPasswordResetRetryLifetime() / 3600),
|
||||
'tokenLifetime' => $this->configuration->getPasswordResetRetryLifetime(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -49,13 +49,13 @@ final class SecurityController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/login_check', name: 'security_check', methods: ['POST'])]
|
||||
public function checkAction()
|
||||
public function checkAction(): Response
|
||||
{
|
||||
throw new \RuntimeException('You must configure the check path to be handled by the firewall using form_login in your security firewall configuration.');
|
||||
}
|
||||
|
||||
#[Route(path: '/logout', name: 'logout', methods: ['GET', 'POST'])]
|
||||
public function logoutAction()
|
||||
public function logoutAction(): Response
|
||||
{
|
||||
throw new \RuntimeException('You must activate the logout in your security firewall configuration.');
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ use App\Event\EmailSelfRegistrationEvent;
|
||||
use App\Form\SelfRegistrationForm;
|
||||
use App\User\LoginManager;
|
||||
use App\User\UserService;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -27,7 +28,6 @@ use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
#[Route(path: '/register')]
|
||||
|
||||
@@ -36,7 +36,7 @@ use App\Utils\PageSetup;
|
||||
use App\Validator\Constraints\ColorChoices;
|
||||
use App\Validator\Constraints\DateTimeFormat;
|
||||
use App\Validator\Constraints\TimeFormat;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CountryType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CurrencyType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
||||
@@ -422,6 +422,7 @@ final class SystemConfigurationController extends AbstractController
|
||||
]),
|
||||
(new Configuration('timesheet.rounding.default.days'))
|
||||
->setType(WeekDaysType::class)
|
||||
->setRequired(false)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
]),
|
||||
(new SystemConfigurationModel('invoice'))
|
||||
|
||||
@@ -62,6 +62,7 @@ final class TagController extends AbstractController
|
||||
|
||||
$table->addColumn('name', ['class' => 'alwaysVisible']);
|
||||
$table->addColumn('amount', ['class' => 'text-center w-min']);
|
||||
$table->addColumn('visible', ['class' => 'd-none text-center w-min']);
|
||||
$table->addColumn('actions', ['class' => 'actions']);
|
||||
|
||||
$page = new PageSetup('tags');
|
||||
@@ -146,16 +147,59 @@ final class TagController extends AbstractController
|
||||
public function multiDelete(TagRepository $repository, Request $request): Response
|
||||
{
|
||||
$form = $this->getMultiUpdateForm($repository);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
try {
|
||||
/** @var MultiUpdateTableDTO $dto */
|
||||
$dto = $form->getData();
|
||||
$repository->multiDelete($dto->getEntities());
|
||||
$this->flashSuccess('action.delete.success');
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashDeleteException($ex);
|
||||
if ($form !== null) {
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
try {
|
||||
/** @var MultiUpdateTableDTO $dto */
|
||||
$dto = $form->getData();
|
||||
$repository->multiDelete($dto->getEntities());
|
||||
$this->flashSuccess('action.delete.success');
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashDeleteException($ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('tags');
|
||||
}
|
||||
|
||||
#[Route(path: '/multi-invisible', name: 'tags_multi_invisible', methods: ['POST'])]
|
||||
#[IsGranted('manage_tag')]
|
||||
public function multiInvisible(TagRepository $repository, Request $request): Response
|
||||
{
|
||||
return $this->multiUpdateVisible($repository, $request, false);
|
||||
}
|
||||
|
||||
#[Route(path: '/multi-visible', name: 'tags_multi_visible', methods: ['POST'])]
|
||||
#[IsGranted('manage_tag')]
|
||||
public function multiVisible(TagRepository $repository, Request $request): Response
|
||||
{
|
||||
return $this->multiUpdateVisible($repository, $request, true);
|
||||
}
|
||||
|
||||
private function multiUpdateVisible(TagRepository $repository, Request $request, bool $visible): Response
|
||||
{
|
||||
$form = $this->getMultiUpdateForm($repository);
|
||||
|
||||
if ($form !== null) {
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
try {
|
||||
/** @var MultiUpdateTableDTO $dto */
|
||||
$dto = $form->getData();
|
||||
/** @var Tag $tag */
|
||||
foreach ($dto->getEntities() as $tag) {
|
||||
$tag->setVisible($visible);
|
||||
}
|
||||
$repository->multiUpdate($dto->getEntities());
|
||||
$this->flashSuccess('action.delete.success');
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashDeleteException($ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,6 +209,12 @@ final class TagController extends AbstractController
|
||||
private function getMultiUpdateForm(TagRepository $repository): ?FormInterface
|
||||
{
|
||||
$dto = new MultiUpdateTableDTO();
|
||||
|
||||
if ($this->isGranted('manage_tag')) {
|
||||
$dto->addAction('visible', $this->generateUrl('tags_multi_visible'));
|
||||
$dto->addAction('invisible', $this->generateUrl('tags_multi_invisible'));
|
||||
}
|
||||
|
||||
if ($this->isGranted('delete_tag')) {
|
||||
$dto->addDelete($this->generateUrl('tags_multi_delete'));
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ use App\Timesheet\TimesheetService;
|
||||
use App\Timesheet\TrackingMode\TrackingModeInterface;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormError;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -334,6 +334,13 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
$execute = false;
|
||||
/** @var Timesheet $timesheet */
|
||||
foreach ($timesheets as $timesheet) {
|
||||
if ($dto->isReplaceDescription()) {
|
||||
$timesheet->setDescription($dto->getDescription());
|
||||
$execute = true;
|
||||
} elseif($dto->getDescription() !== null && $dto->getDescription() !== '') {
|
||||
$timesheet->setDescription($timesheet->getDescription() . PHP_EOL . $dto->getDescription());
|
||||
$execute = true;
|
||||
}
|
||||
if ($dto->isReplaceTags()) {
|
||||
foreach ($timesheet->getTags() as $tag) {
|
||||
$timesheet->removeTag($tag);
|
||||
@@ -418,7 +425,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
protected function multiDelete(Request $request)
|
||||
protected function multiDelete(Request $request): Response
|
||||
{
|
||||
$form = $this->getMultiUpdateActionForm();
|
||||
$form->handleRequest($request);
|
||||
@@ -446,7 +453,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
return $this->redirectToRoute($this->getTimesheetRoute());
|
||||
}
|
||||
|
||||
protected function prepareQuery(TimesheetQuery $query)
|
||||
protected function prepareQuery(TimesheetQuery $query): void
|
||||
{
|
||||
$query->setUser($this->getUser());
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ final class TimesheetTeamController extends TimesheetAbstractController
|
||||
return $this->multiDelete($request);
|
||||
}
|
||||
|
||||
protected function prepareQuery(TimesheetQuery $query)
|
||||
protected function prepareQuery(TimesheetQuery $query): void
|
||||
{
|
||||
$query->setCurrentUser($this->getUser());
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ use App\Repository\UserRepository;
|
||||
use App\User\UserService;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -193,7 +193,7 @@ final class UserController extends AbstractController
|
||||
|
||||
#[Route(path: '/export', name: 'user_export', methods: ['GET'])]
|
||||
#[IsGranted('view_user')]
|
||||
public function exportAction(Request $request, UserExporter $exporter)
|
||||
public function exportAction(Request $request, UserExporter $exporter): Response
|
||||
{
|
||||
$query = new UserQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
@@ -233,6 +233,7 @@ final class UserController extends AbstractController
|
||||
'method' => 'POST',
|
||||
'include_active_flag' => true,
|
||||
'include_preferences' => true,
|
||||
'include_supervisor' => $this->isGranted('supervisor_other_profile'),
|
||||
'include_teams' => $this->isGranted('teams_other_profile'),
|
||||
'include_roles' => $this->isGranted('roles_other_profile'),
|
||||
]);
|
||||
|
||||
@@ -14,6 +14,7 @@ use App\Entity\UserPreference;
|
||||
use App\Form\Type\LanguageType;
|
||||
use App\Form\Type\SkinType;
|
||||
use App\Form\Type\TimezoneType;
|
||||
use App\Form\UserPasswordType;
|
||||
use App\User\UserService;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -58,6 +59,11 @@ final class WizardController extends AbstractController
|
||||
->setMethod('POST')
|
||||
->getForm();
|
||||
|
||||
$next = 'done';
|
||||
if ($user->requiresPasswordReset()) {
|
||||
$next = 'password';
|
||||
}
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
@@ -72,13 +78,44 @@ final class WizardController extends AbstractController
|
||||
if ($data['reload'] === '1') {
|
||||
return $this->redirectToRoute('wizard', ['wizard' => 'profile', '_locale' => $data['language']]);
|
||||
} else {
|
||||
return $this->redirectToRoute('wizard', ['wizard' => 'done', '_locale' => $data['language']]);
|
||||
return $this->redirectToRoute('wizard', ['wizard' => $next, '_locale' => $data['language']]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('wizard/profile.html.twig', [
|
||||
'percent' => \intval(100 / \count(User::WIZARDS) * 1),
|
||||
'previous' => 'intro',
|
||||
'next' => $next,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
if ($wizard === 'password' || $user->requiresPasswordReset()) {
|
||||
$form = $this->createForm(UserPasswordType::class, $user, [
|
||||
'action' => $this->generateUrl('wizard', ['wizard' => 'password']),
|
||||
'method' => 'POST',
|
||||
]);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$user->setRequiresPasswordReset(false);
|
||||
$userService->updateUser($user);
|
||||
|
||||
return $this->redirectToRoute('wizard', ['wizard' => 'done']);
|
||||
}
|
||||
|
||||
$previous = 'profile';
|
||||
$percent = \intval(100 / \count(User::WIZARDS) * 1);
|
||||
|
||||
if ($user->requiresPasswordReset()) {
|
||||
$previous = null;
|
||||
$percent = null;
|
||||
}
|
||||
|
||||
return $this->render('wizard/password.html.twig', [
|
||||
'percent' => $percent,
|
||||
'previous' => $previous,
|
||||
'next' => 'done',
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
|
||||
@@ -18,10 +18,11 @@ use App\Event\CustomerMetaDefinitionEvent;
|
||||
use App\Event\CustomerUpdatePostEvent;
|
||||
use App\Event\CustomerUpdatePreEvent;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use App\Utils\NumberGenerator;
|
||||
use App\Validator\ValidationFailedException;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
final class CustomerService
|
||||
@@ -107,6 +108,19 @@ final class CustomerService
|
||||
return $this->repository->findOneBy(['number' => $number]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<Customer>
|
||||
*/
|
||||
public function findCustomer(CustomerQuery $query): iterable
|
||||
{
|
||||
return $this->repository->getCustomersForQuery($query);
|
||||
}
|
||||
|
||||
public function countCustomer(bool $visible = true): int
|
||||
{
|
||||
return $this->repository->countCustomer($visible);
|
||||
}
|
||||
|
||||
public function calculateNextCustomerNumber(): string
|
||||
{
|
||||
$format = $this->configuration->find('customer.number_format');
|
||||
|
||||
@@ -11,23 +11,33 @@ namespace App\Customer;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\User;
|
||||
use App\Event\CustomerBudgetStatisticEvent;
|
||||
use App\Event\CustomerStatisticEvent;
|
||||
use App\Model\CustomerBudgetStatisticModel;
|
||||
use App\Model\CustomerStatistic;
|
||||
use App\Reporting\CustomerView\CustomerViewModel;
|
||||
use App\Reporting\CustomerView\CustomerViewQuery;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\Loader\CustomerLoader;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\DateTimeFactory;
|
||||
use DateTime;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* @final
|
||||
*/
|
||||
class CustomerStatisticService
|
||||
{
|
||||
public function __construct(private TimesheetRepository $timesheetRepository, private EventDispatcherInterface $dispatcher)
|
||||
public function __construct(
|
||||
private CustomerRepository $customerRepository,
|
||||
private TimesheetRepository $timesheetRepository,
|
||||
private EventDispatcherInterface $dispatcher
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -85,6 +95,7 @@ class CustomerStatisticService
|
||||
$result = $qb->getQuery()->getResult();
|
||||
|
||||
if (null !== $result) {
|
||||
/** @var array{'id': string, 'duration': int, 'internalRate': float, 'counter': int, 'rate': float, 'billable': int, 'exported': int} $resultRow */
|
||||
foreach ($result as $resultRow) {
|
||||
$statistic = $statistics[$resultRow['id']];
|
||||
$statistic->setDuration($statistic->getDuration() + $resultRow['duration']);
|
||||
@@ -113,12 +124,16 @@ class CustomerStatisticService
|
||||
return $statistics;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Customer> $customers
|
||||
*/
|
||||
private function createStatisticQueryBuilder(array $customers, DateTime $begin = null, ?DateTime $end = null): QueryBuilder
|
||||
{
|
||||
$qb = $this->timesheetRepository->createQueryBuilder('t');
|
||||
$qb
|
||||
->select('IDENTITY(p.customer) AS id')
|
||||
->join(Project::class, 'p', Query\Expr\Join::WITH, 't.project = p.id')
|
||||
->join(Customer::class, 'c', Query\Expr\Join::WITH, 'p.customer = c.id')
|
||||
->addSelect('COALESCE(SUM(t.duration), 0) as duration')
|
||||
->addSelect('COALESCE(SUM(t.rate), 0) as rate')
|
||||
->addSelect('COALESCE(SUM(t.internalRate), 0) as internalRate')
|
||||
@@ -149,4 +164,177 @@ class CustomerStatisticService
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CustomerViewQuery $query
|
||||
* @return Customer[]
|
||||
*/
|
||||
public function findCustomersForView(CustomerViewQuery $query): array
|
||||
{
|
||||
$user = $query->getUser();
|
||||
|
||||
$qb = $this->customerRepository->createQueryBuilder('c');
|
||||
$qb
|
||||
->select('c')
|
||||
->andWhere($qb->expr()->eq('c.visible', true))
|
||||
->addGroupBy('c')
|
||||
;
|
||||
|
||||
if ($query->isIncludeWithBudget()) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->gt('c.timeBudget', 0),
|
||||
$qb->expr()->gt('c.budget', 0)
|
||||
)
|
||||
);
|
||||
} elseif ($query->isIncludeWithoutBudget()) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->eq('c.timeBudget', 0),
|
||||
$qb->expr()->eq('c.budget', 0)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->customerRepository->addPermissionCriteria($qb, $user);
|
||||
|
||||
/** @var Customer[] $customers */
|
||||
$customers = $qb->getQuery()->getResult();
|
||||
|
||||
// pre-cache customer objects instead of joining them
|
||||
$loader = new CustomerLoader($this->customerRepository->createQueryBuilder('c')->getEntityManager(), false);
|
||||
$loader->loadResults($customers);
|
||||
|
||||
return $customers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param Customer[] $customers
|
||||
* @param DateTime $today
|
||||
* @return CustomerViewModel[]
|
||||
*/
|
||||
public function getCustomerView(User $user, array $customers, DateTime $today): array
|
||||
{
|
||||
$today = clone $today;
|
||||
|
||||
/** @var array<int, CustomerViewModel> $customerViews */
|
||||
$customerViews = [];
|
||||
foreach ($customers as $customer) {
|
||||
$customerViews[$customer->getId()] = new CustomerViewModel($customer);
|
||||
}
|
||||
|
||||
$budgetStats = $this->getBudgetStatisticModelForCustomers($customers, $today);
|
||||
foreach ($budgetStats as $model) {
|
||||
$customerViews[$model->getCustomer()->getId()]->setBudgetStatisticModel($model);
|
||||
}
|
||||
|
||||
$customerIds = array_keys($customerViews);
|
||||
|
||||
$tplQb = $this->timesheetRepository->createQueryBuilder('t');
|
||||
$tplQb
|
||||
->select('c.id AS id')
|
||||
->join(Project::class, 'p', Query\Expr\Join::WITH, 't.project = p.id')
|
||||
->join(Customer::class, 'c', Query\Expr\Join::WITH, 'p.customer = c.id')
|
||||
->addSelect('COUNT(t.id) as amount')
|
||||
->addSelect('COALESCE(SUM(t.duration), 0) AS duration')
|
||||
->addSelect('COALESCE(SUM(t.rate), 0) AS rate')
|
||||
->andWhere($tplQb->expr()->in('c.id', ':customer'))
|
||||
->groupBy('id')
|
||||
->setParameter('customer', array_values($customerIds))
|
||||
;
|
||||
|
||||
$qb = clone $tplQb;
|
||||
|
||||
$result = $qb->getQuery()->getScalarResult();
|
||||
/** @var array{'duration': int, 'amount': int, 'rate': float, 'id': string, 'exported': int} $row */
|
||||
foreach ($result as $row) {
|
||||
$customerViews[$row['id']]->setDurationTotal($row['duration']);
|
||||
$customerViews[$row['id']]->setRateTotal($row['rate']);
|
||||
$customerViews[$row['id']]->setTimesheetCounter($row['amount']);
|
||||
}
|
||||
|
||||
$qb = clone $tplQb;
|
||||
$qb
|
||||
->addSelect('t.exported')
|
||||
->addSelect('t.billable')
|
||||
->addGroupBy('t.exported')
|
||||
->addGroupBy('t.billable')
|
||||
;
|
||||
$result = $qb->getQuery()->getScalarResult();
|
||||
/** @var array{'duration': int, 'billable': int, 'rate': float, 'exported': int, 'id': string} $row */
|
||||
foreach ($result as $row) {
|
||||
$view = $customerViews[$row['id']];
|
||||
if ($row['billable'] === 1 && $row['exported'] === 1) {
|
||||
$view->setBillableDuration($view->getBillableDuration() + $row['duration']);
|
||||
$view->setBillableRate($view->getBillableRate() + $row['rate']);
|
||||
} elseif ($row['billable'] === 1 && $row['exported'] === 0) {
|
||||
$view->setBillableDuration($view->getBillableDuration() + $row['duration']);
|
||||
$view->setBillableRate($view->getBillableRate() + $row['rate']);
|
||||
$view->setNotExportedDuration($view->getNotExportedDuration() + $row['duration']);
|
||||
$view->setNotExportedRate($view->getNotExportedRate() + $row['rate']);
|
||||
$view->setNotBilledDuration($view->getNotBilledDuration() + $row['duration']);
|
||||
$view->setNotBilledRate($view->getNotBilledRate() + $row['rate']);
|
||||
} elseif ($row['billable'] === 0 && $row['exported'] === 0) {
|
||||
$view->setNotExportedDuration($view->getNotExportedDuration() + $row['duration']);
|
||||
$view->setNotExportedRate($view->getNotExportedRate() + $row['rate']);
|
||||
}
|
||||
// the last possible case $row['billable'] === 0 && $row['exported'] === 1 is extremely unlikely and not used
|
||||
}
|
||||
|
||||
return array_values($customerViews);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Customer[] $customers
|
||||
* @param DateTime $today
|
||||
* @return CustomerBudgetStatisticModel[]
|
||||
*/
|
||||
public function getBudgetStatisticModelForCustomers(array $customers, DateTime $today): array
|
||||
{
|
||||
$models = [];
|
||||
$monthly = [];
|
||||
$allTime = [];
|
||||
|
||||
foreach ($customers as $customer) {
|
||||
$models[$customer->getId()] = new CustomerBudgetStatisticModel($customer);
|
||||
if ($customer->isMonthlyBudget()) {
|
||||
$monthly[] = $customer;
|
||||
} else {
|
||||
$allTime[] = $customer;
|
||||
}
|
||||
}
|
||||
|
||||
$statisticsTotal = $this->getBudgetStatistic($customers);
|
||||
foreach ($statisticsTotal as $id => $statistic) {
|
||||
$models[$id]->setStatisticTotal($statistic);
|
||||
}
|
||||
|
||||
$dateFactory = new DateTimeFactory($today->getTimezone());
|
||||
|
||||
$begin = null;
|
||||
$end = $today;
|
||||
|
||||
if (\count($monthly) > 0) {
|
||||
$begin = $dateFactory->getStartOfMonth($today);
|
||||
$end = $dateFactory->getEndOfMonth($today);
|
||||
$statistics = $this->getBudgetStatistic($monthly, $begin, $end);
|
||||
foreach ($statistics as $id => $statistic) {
|
||||
$models[$id]->setStatistic($statistic);
|
||||
}
|
||||
}
|
||||
|
||||
if (\count($allTime) > 0) {
|
||||
// display the budget at the end of the selected period and not the total sum of all times (do not include times in the future)
|
||||
$statistics = $this->getBudgetStatistic($allTime, null, $today);
|
||||
foreach ($statistics as $id => $statistic) {
|
||||
$models[$id]->setStatistic($statistic);
|
||||
}
|
||||
}
|
||||
|
||||
$event = new CustomerBudgetStatisticEvent($models, $begin, $end);
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
return $models;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ final class Configuration implements ConfigurationInterface
|
||||
return $node;
|
||||
}
|
||||
|
||||
private function getProjectNode()
|
||||
private function getProjectNode(): ArrayNodeDefinition
|
||||
{
|
||||
$builder = new TreeBuilder('project');
|
||||
/** @var ArrayNodeDefinition $node */
|
||||
@@ -305,7 +305,7 @@ final class Configuration implements ConfigurationInterface
|
||||
->defaultValue(0)
|
||||
->end()
|
||||
->integerNode('long_running_duration')
|
||||
->defaultValue(600)
|
||||
->defaultValue(0)
|
||||
->end()
|
||||
->booleanNode('require_activity')
|
||||
->defaultTrue()
|
||||
@@ -343,7 +343,7 @@ final class Configuration implements ConfigurationInterface
|
||||
->defaultValue('{Y}/{cy,3}')
|
||||
->end()
|
||||
->booleanNode('upload_twig')
|
||||
->defaultTrue()
|
||||
->defaultFalse()
|
||||
->end()
|
||||
->end()
|
||||
;
|
||||
@@ -789,7 +789,9 @@ final class Configuration implements ConfigurationInterface
|
||||
->defaultValue('Login with SAML')
|
||||
->end()
|
||||
->scalarNode('provider')
|
||||
->defaultNull()
|
||||
// the "default" was only added, to prevent support requests by people who did not
|
||||
// adjust their config between 1.x and 2.0
|
||||
->defaultValue('default')
|
||||
->end()
|
||||
->arrayNode('roles')
|
||||
->addDefaultsIfNotSet()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user