Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64893e0a95 | ||
|
|
6fdc304313 | ||
|
|
fb265ed732 | ||
|
|
e2be5b401c | ||
|
|
8539938b8a | ||
|
|
bbf25823b8 | ||
|
|
6b066046c6 | ||
|
|
9226daa891 | ||
|
|
843cd8fb67 | ||
|
|
6d7d441d42 | ||
|
|
1c4da30b61 | ||
|
|
84e25851d3 | ||
|
|
3a52daedab | ||
|
|
b7cf9cd776 | ||
|
|
ccb184b17f | ||
|
|
162ff07bd5 | ||
|
|
7326a452c3 | ||
|
|
9453947309 | ||
|
|
d25653b6ed | ||
|
|
ea415e33f5 | ||
|
|
b6430d5622 | ||
|
|
6fcd50ecdf | ||
|
|
6857d92c4f | ||
|
|
4bb9edb2b5 | ||
|
|
dd881bb856 | ||
|
|
204e0bc179 | ||
|
|
d026460106 |
@@ -7,15 +7,6 @@ coverage:
|
||||
round: down
|
||||
range: "80...100"
|
||||
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
threshold: 0.5%
|
||||
patch:
|
||||
default:
|
||||
threshold: 50%
|
||||
changes: no
|
||||
|
||||
parsers:
|
||||
gcov:
|
||||
branch_detection:
|
||||
|
||||
24
.github/lock.yml
vendored
24
.github/lock.yml
vendored
@@ -1,24 +0,0 @@
|
||||
# Configuration for lock-threads - https://github.com/dessant/lock-threads
|
||||
|
||||
# Number of days of inactivity before a closed issue or pull request is locked
|
||||
daysUntilLock: 60
|
||||
|
||||
# Issues and pull requests with these labels will not be locked. Set to `[]` to disable
|
||||
exemptLabels: []
|
||||
|
||||
# Label to add before locking, such as `outdated`. Set to `false` to disable
|
||||
lockLabel: false
|
||||
|
||||
# Comment to post before locking. Set to `false` to disable
|
||||
lockComment: >
|
||||
This thread has been automatically locked since there has not been
|
||||
any recent activity after it was closed. Please open a new issue for
|
||||
related bugs.
|
||||
If you use Kimai on a daily basis, please [consider donating](https://www.kimai.org/donate/) to
|
||||
support further development of Kimai.
|
||||
|
||||
# Assign `resolved` as the reason for locking. Set to `false` to disable
|
||||
setLockReason: false
|
||||
|
||||
# Limit to only `issues` or `pulls`
|
||||
# only: pulls
|
||||
5
.github/workflows/coverage.yaml
vendored
5
.github/workflows/coverage.yaml
vendored
@@ -41,7 +41,8 @@ jobs:
|
||||
MAILER_URL: null://localhost
|
||||
TEST_WITH_BUNDLES: 1
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@v1
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
file: ./coverage.xml
|
||||
files: ./coverage.xml
|
||||
fail_ci_if_error: true
|
||||
|
||||
56
.github/workflows/lock.yaml
vendored
Normal file
56
.github/workflows/lock.yaml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: 'Lock Threads'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: lock
|
||||
|
||||
jobs:
|
||||
action:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v3
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-inactive-days: '180'
|
||||
exclude-issue-created-before: ''
|
||||
exclude-issue-created-after: ''
|
||||
exclude-issue-created-between: ''
|
||||
exclude-issue-closed-before: ''
|
||||
exclude-issue-closed-after: ''
|
||||
exclude-issue-closed-between: ''
|
||||
include-any-issue-labels: ''
|
||||
include-all-issue-labels: ''
|
||||
exclude-any-issue-labels: ''
|
||||
add-issue-labels: ''
|
||||
remove-issue-labels: ''
|
||||
issue-comment: >
|
||||
This thread has been automatically locked since there has not been
|
||||
any recent activity after it was closed. Please open a new issue for
|
||||
related bugs.
|
||||
If you use Kimai on a daily basis, please [consider donating](https://www.kimai.org/donate/) to
|
||||
support further development of Kimai.
|
||||
issue-lock-reason: 'resolved'
|
||||
pr-inactive-days: '180'
|
||||
exclude-pr-created-before: ''
|
||||
exclude-pr-created-after: ''
|
||||
exclude-pr-created-between: ''
|
||||
exclude-pr-closed-before: ''
|
||||
exclude-pr-closed-after: ''
|
||||
exclude-pr-closed-between: ''
|
||||
include-any-pr-labels: ''
|
||||
include-all-pr-labels: ''
|
||||
exclude-any-pr-labels: ''
|
||||
add-pr-labels: ''
|
||||
remove-pr-labels: ''
|
||||
pr-comment: ''
|
||||
pr-lock-reason: ''
|
||||
process-only: ''
|
||||
log-output: false
|
||||
@@ -1,5 +1,5 @@
|
||||
unreleased=true
|
||||
future-release=1.15
|
||||
future-release=1.15.6
|
||||
exclude-labels=duplicate,support,question,invalid,wontfix,release,waiting for feedback,documentation
|
||||
enhancement_labels=>enhancement,Enhancement,feature request,translation i18n,technical debt,documentation
|
||||
issues-wo-labels=false
|
||||
|
||||
165
CHANGELOG.md
165
CHANGELOG.md
@@ -1,166 +1,3 @@
|
||||
# Changelog
|
||||
|
||||
## [1.14](https://github.com/kevinpapst/kimai2/tree/1.14)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.13...1.14)
|
||||
|
||||
## [1.13](https://github.com/kevinpapst/kimai2/tree/1.13)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.12...1.13)
|
||||
|
||||
## [1.12](https://github.com/kevinpapst/kimai2/tree/1.12)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.11.1...1.12)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.12)
|
||||
|
||||
## [1.11.1](https://github.com/kevinpapst/kimai2/tree/1.11.1)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.11...1.11.1)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.11.1)
|
||||
|
||||
## [1.11](https://github.com/kevinpapst/kimai2/tree/1.11)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.10.2...1.11)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.11)
|
||||
|
||||
## [1.10.2](https://github.com/kevinpapst/kimai2/tree/1.10.2)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.9...1.10.2)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.10.2)
|
||||
|
||||
## [1.10.1](https://github.com/kevinpapst/kimai2/tree/1.10.1)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.9...1.10.1)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.10.1)
|
||||
|
||||
## [1.10](https://github.com/kevinpapst/kimai2/tree/1.10)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.9...1.10)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.10)
|
||||
|
||||
## [1.9](https://github.com/kevinpapst/kimai2/tree/1.9)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.8...1.9)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.9)
|
||||
|
||||
## [1.8](https://github.com/kevinpapst/kimai2/tree/1.8)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.7...1.8)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.8)
|
||||
|
||||
## [1.7](https://github.com/kevinpapst/kimai2/tree/1.7)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.6.2...1.7)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.7)
|
||||
|
||||
## [1.6.2](https://github.com/kevinpapst/kimai2/tree/1.6.2)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.6.1...1.6.2)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.6.2)
|
||||
|
||||
## [1.6.1](https://github.com/kevinpapst/kimai2/tree/1.6.1)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.6...1.6.1)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.6.1)
|
||||
|
||||
## [1.6](https://github.com/kevinpapst/kimai2/tree/1.6)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.5...1.6)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.6)
|
||||
|
||||
## [1.5](https://github.com/kevinpapst/kimai2/tree/1.5)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.4.2...1.5)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.5)
|
||||
|
||||
## [1.4.2](https://github.com/kevinpapst/kimai2/tree/1.4)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.4.1...1.4.2)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.4.2)
|
||||
|
||||
## [1.4.1](https://github.com/kevinpapst/kimai2/tree/1.4)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.4...1.4.1)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.4.1)
|
||||
|
||||
## [1.4](https://github.com/kevinpapst/kimai2/tree/1.4)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.3...1.4)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.4)
|
||||
|
||||
## [1.3](https://github.com/kevinpapst/kimai2/tree/1.3)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.2...1.3)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.3)
|
||||
|
||||
## [1.2](https://github.com/kevinpapst/kimai2/tree/1.2)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.1...1.2)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.2)
|
||||
|
||||
## [1.1](https://github.com/kevinpapst/kimai2/tree/1.1)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.0.1...1.1)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.1)
|
||||
|
||||
## [1.0.1](https://github.com/kevinpapst/kimai2/tree/1.0.1)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.0...1.0.1)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.0.1)
|
||||
|
||||
## [1.0](https://github.com/kevinpapst/kimai2/tree/1.0)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.9...1.0)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.0)
|
||||
|
||||
## [0.9](https://github.com/kevinpapst/kimai2/tree/0.9)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.8.1...0.9)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.9)
|
||||
|
||||
## [0.8.1](https://github.com/kevinpapst/kimai2/tree/0.8.1)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.8...0.8.1)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.8.1)
|
||||
|
||||
## [0.8](https://github.com/kevinpapst/kimai2/tree/0.8)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.7...0.8)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.8)
|
||||
|
||||
## [0.7](https://github.com/kevinpapst/kimai2/tree/0.7)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.6.1...0.7)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.7)
|
||||
|
||||
## [0.6.1](https://github.com/kevinpapst/kimai2/tree/0.6.1)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.6...0.6.1)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.6.1)
|
||||
|
||||
## [0.6](https://github.com/kevinpapst/kimai2/tree/0.6)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.5...0.6)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.6)
|
||||
|
||||
## [0.5](https://github.com/kevinpapst/kimai2/tree/0.5)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.4...0.5)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.5)
|
||||
|
||||
## [0.4](https://github.com/kevinpapst/kimai2/tree/0.4)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.3...0.4)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.4)
|
||||
|
||||
## [0.3](https://github.com/kevinpapst/kimai2/tree/0.3)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.2...0.3)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.3)
|
||||
|
||||
## [0.2](https://github.com/kevinpapst/kimai2/tree/0.2)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.1...0.2)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.2)
|
||||
|
||||
## [0.1](https://github.com/kevinpapst/kimai2/tree/0.1)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.1)
|
||||
See [GitHub release page](https://github.com/kevinpapst/kimai2/releases).
|
||||
|
||||
@@ -89,6 +89,15 @@ table.dataTable {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
tfoot {
|
||||
tr {
|
||||
/* overwritten if used in tfoot, to switch the border from below to top */
|
||||
&.summary td {
|
||||
border-top: 1px solid #ccc;
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
table.table-hover {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
.dataTable {
|
||||
.weekend {
|
||||
background-color: #f9f9f9;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -89,8 +89,9 @@
|
||||
"symfony/dom-crawler": "4.4.4",
|
||||
"symfony/maker-bundle": "^1.12",
|
||||
"symfony/phpunit-bridge": "^4.4",
|
||||
"symfony/profiler-pack": "^1.0",
|
||||
"symfony/var-dumper": "^4.4"
|
||||
"symfony/stopwatch": "^4.4",
|
||||
"symfony/var-dumper": "^4.4",
|
||||
"symfony/web-profiler-bundle": "^4.4"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
|
||||
1672
composer.lock
generated
1672
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,9 @@ framework:
|
||||
|
||||
session:
|
||||
handler_id: App\Security\SessionHandler
|
||||
cookie_secure: auto
|
||||
cookie_httponly: true
|
||||
cookie_samesite: lax
|
||||
|
||||
#esi: ~
|
||||
#fragments: ~
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
# DO NOT EDIT THIS FILE, INSTEAD CREATE THE FILE "local.yaml" AND ADD YOUR SETTINGS IN THERE.
|
||||
# See https://www.kimai.org/documentation/configurations.html
|
||||
# See https://www.kimai.org/documentation/local-yaml.html
|
||||
#
|
||||
# Be aware that this file is YAML format and the indentation is important.
|
||||
# Each config level needs to be indented with 4 additional spaces.
|
||||
@@ -9,19 +9,15 @@ kimai:
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# AUTHENTICATION
|
||||
# You can disable some user management functions in the authentication screens.
|
||||
# Both settings default to "true", see https://www.kimai.org/documentation/users.html
|
||||
# --------------------------------------------------------------------------------
|
||||
# user:
|
||||
# registration: false
|
||||
# password_reset: false
|
||||
# password_reset: true
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# TIME-TRACKING
|
||||
# All configs related to time-tracking, timesheets and record management
|
||||
# Most settings can be configured in the System configuration screen
|
||||
# --------------------------------------------------------------------------------
|
||||
timesheet:
|
||||
|
||||
@@ -38,7 +34,6 @@ kimai:
|
||||
# time_increment: ~
|
||||
|
||||
# The time-tracking mode that should be used.
|
||||
# See https://www.kimai.org/documentation/timesheet.html#tracking-modes
|
||||
# mode: default
|
||||
|
||||
# The default time to pre-fill the "create timesheet" form (in some cases).
|
||||
@@ -51,7 +46,6 @@ kimai:
|
||||
|
||||
# Rounding rules are used to round the begin & end dates and the duration for timesheet records.
|
||||
# The "default" rule will round "begin" down and "end" up to the full minute, the "duration" will not be rounded.
|
||||
# Find out more about rounding rules at https://www.kimai.org/documentation/timesheet.html
|
||||
# rounding:
|
||||
# default:
|
||||
# days: ['monday','tuesday','wednesday','thursday','friday','saturday','sunday']
|
||||
@@ -62,7 +56,6 @@ kimai:
|
||||
|
||||
# If you want to apply different hourly rates for specific weekdays, you can uncomment the "rates" configuration.
|
||||
# The "weekend" rule will add 50% to each timesheet entry that will be recorded on "saturdays" or "sundays".
|
||||
# See documentation at: https://www.kimai.org/documentation/timesheet.html#rate-calculation
|
||||
# rates:
|
||||
# weekend:
|
||||
# days: ['saturday','sunday']
|
||||
@@ -75,18 +68,11 @@ kimai:
|
||||
# and the user has to stop an active entry first
|
||||
# active_entries:
|
||||
# hard_limit: 3
|
||||
|
||||
# Rules that define timesheet validation and behaviour
|
||||
# allow_future_times: whether records in the future can be created
|
||||
# rules:
|
||||
# allow_future_times: true
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# PERMISSIONS
|
||||
# Find out more about the configurable permission system at:
|
||||
# https://www.kimai.org/documentation/permissions.html
|
||||
# --------------------------------------------------------------------------------
|
||||
permissions:
|
||||
# mapping complex rule sets of single permissions to named "sets" ("set name" = [array of "permissions and sets"])
|
||||
@@ -146,7 +132,6 @@ kimai:
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# CALENDAR Configuration
|
||||
# See documentation at https://www.kimai.org/documentation/calendar.html
|
||||
# --------------------------------------------------------------------------------
|
||||
# calendar:
|
||||
# week_numbers: true
|
||||
@@ -184,8 +169,6 @@ kimai:
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# DASHBOARD
|
||||
# Using the configurable widget sections.
|
||||
# See documentation at https://www.kimai.org/documentation/dashboard.html
|
||||
# --------------------------------------------------------------------------------
|
||||
dashboard:
|
||||
user_duration:
|
||||
@@ -218,12 +201,16 @@ kimai:
|
||||
order: 50
|
||||
permission: view_all_data
|
||||
widgets: [amountToday, amountWeek, amountMonth, amountYear]
|
||||
totals:
|
||||
title: ~
|
||||
order: 100
|
||||
permission: ROLE_USER
|
||||
widgets: [TotalsUser, TotalsCustomer, TotalsProject, TotalsActivity]
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# LANGUAGES
|
||||
# Locale specific settings, like date formats
|
||||
# --------------------------------------------------------------------------------
|
||||
languages:
|
||||
cs:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
|
||||
parameters:
|
||||
locale: en
|
||||
app_locales: ar|cs|da|de|de_AT|de_CH|el|en|eo|es|eu|fi|fo|fr|he|hu|it|ja|ko|nl|pl|pt|pt_BR|ro|ru|sk|sv|tr|vi|zh_CN
|
||||
app_locales: ar|cs|da|de|de_AT|de_CH|el|en|eo|es|eu|fi|fo|fr|he|hr|hu|it|ja|ko|nl|pl|pt|pt_BR|ro|ru|sk|sv|tr|vi|zh_CN
|
||||
|
||||
services:
|
||||
# default configuration for services in *this* file
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -6,7 +6,7 @@
|
||||
"build/app.e22732af.js"
|
||||
],
|
||||
"css": [
|
||||
"build/app.fdf3c5fb.css"
|
||||
"build/app.c57cdec8.css"
|
||||
]
|
||||
},
|
||||
"invoice": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"build/app.css": "build/app.fdf3c5fb.css",
|
||||
"build/app.css": "build/app.c57cdec8.css",
|
||||
"build/app.js": "build/app.e22732af.js",
|
||||
"build/invoice.css": "build/invoice.ff32661a.css",
|
||||
"build/invoice.js": "build/invoice.19f36eca.js",
|
||||
|
||||
@@ -590,7 +590,7 @@ class TimesheetController extends BaseApiController
|
||||
* required=true,
|
||||
* )
|
||||
*
|
||||
* @Rest\RequestParam(name="copy", requirements="all|tags|rates|meta|description", strict=true, nullable=true, description="Whether data should be copied to the new entry. Allowed values: all, tags, rates, description, meta (default: nothing is copied)")
|
||||
* @Rest\RequestParam(name="copy", requirements="all|tags|rates|meta|description", strict=true, nullable=true, description="Whether data should be copied to the new entry. Allowed values: all, tags (deprecated), rates (deprecated), description (deprecated), meta (deprecated) (default: nothing is copied)")
|
||||
* @Rest\RequestParam(name="begin", requirements=@Constraints\DateTime(format="Y-m-d\TH:i:s"), strict=true, nullable=true, description="Changes the restart date to the given one (default: now)")
|
||||
*
|
||||
* @ApiSecurity(name="apiUser")
|
||||
@@ -620,26 +620,24 @@ class TimesheetController extends BaseApiController
|
||||
;
|
||||
|
||||
if (null !== ($copy = $paramFetcher->get('copy'))) {
|
||||
if (\in_array($copy, ['rates', 'all'])) {
|
||||
$copyTimesheet->setHourlyRate($timesheet->getHourlyRate());
|
||||
$copyTimesheet->setFixedRate($timesheet->getFixedRate());
|
||||
if ($copy !== 'all') {
|
||||
@trigger_error('Setting the "copy" attribute in "restart timesheet" API to something else then "all" is deprecated', E_USER_DEPRECATED);
|
||||
}
|
||||
|
||||
if (\in_array($copy, ['description', 'all'])) {
|
||||
$copyTimesheet->setDescription($timesheet->getDescription());
|
||||
$copyTimesheet
|
||||
->setHourlyRate($timesheet->getHourlyRate())
|
||||
->setFixedRate($timesheet->getFixedRate())
|
||||
->setDescription($timesheet->getDescription())
|
||||
->setBillable($timesheet->isBillable())
|
||||
;
|
||||
|
||||
foreach ($timesheet->getTags() as $tag) {
|
||||
$copyTimesheet->addTag($tag);
|
||||
}
|
||||
|
||||
if (\in_array($copy, ['tags', 'all'])) {
|
||||
foreach ($timesheet->getTags() as $tag) {
|
||||
$copyTimesheet->addTag($tag);
|
||||
}
|
||||
}
|
||||
|
||||
if (\in_array($copy, ['meta', 'all'])) {
|
||||
foreach ($timesheet->getMetaFields() as $metaField) {
|
||||
$metaNew = clone $metaField;
|
||||
$copyTimesheet->setMetaField($metaNew);
|
||||
}
|
||||
foreach ($timesheet->getMetaFields() as $metaField) {
|
||||
$metaNew = clone $metaField;
|
||||
$copyTimesheet->setMetaField($metaNew);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -232,6 +232,11 @@ class SystemConfiguration implements SystemBundleConfiguration
|
||||
return (string) $this->find('timesheet.default_begin');
|
||||
}
|
||||
|
||||
public function getTimesheetDefaultBillable(): bool
|
||||
{
|
||||
return (bool) $this->find('defaults.timesheet.billable');
|
||||
}
|
||||
|
||||
public function isTimesheetAllowFutureTimes(): bool
|
||||
{
|
||||
return (bool) $this->find('timesheet.rules.allow_future_times');
|
||||
|
||||
@@ -17,11 +17,11 @@ class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '1.15.1';
|
||||
public const VERSION = '1.15.6';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 11501;
|
||||
public const VERSION_ID = 11506;
|
||||
/**
|
||||
* The current release status, either "stable" or "dev"
|
||||
*/
|
||||
@@ -43,7 +43,7 @@ class Constants
|
||||
*/
|
||||
public const HOMEPAGE = 'https://www.kimai.org';
|
||||
/**
|
||||
* Application wide default locale.
|
||||
* Application wide default locale
|
||||
*/
|
||||
public const DEFAULT_LOCALE = 'en';
|
||||
/**
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Utils\FileHelper;
|
||||
use Composer\InstalledVersions;
|
||||
use PackageVersions\Versions;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
@@ -40,24 +41,18 @@ class DoctorController extends AbstractController
|
||||
*/
|
||||
public const DIRECTORIES_WRITABLE = [
|
||||
'var/cache/',
|
||||
'var/data/',
|
||||
'var/log/',
|
||||
'var/sessions/',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $projectDirectory;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $environment;
|
||||
private $fileHelper;
|
||||
|
||||
public function __construct(string $projectDirectory, string $kernelEnvironment)
|
||||
public function __construct(string $projectDirectory, string $kernelEnvironment, FileHelper $fileHelper)
|
||||
{
|
||||
$this->projectDirectory = $projectDirectory;
|
||||
$this->environment = $kernelEnvironment;
|
||||
$this->fileHelper = $fileHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,6 +114,8 @@ class DoctorController extends AbstractController
|
||||
$versions[$package] = InstalledVersions::getPrettyVersion($package);
|
||||
}
|
||||
} else {
|
||||
@trigger_error('Please upgrade your Composer to 2.x', E_USER_DEPRECATED);
|
||||
|
||||
// @deprecated since 1.14, will be removed with 2.0
|
||||
$rootPackage = Versions::rootPackageName();
|
||||
foreach (Versions::VERSIONS as $name => $version) {
|
||||
@@ -155,12 +152,6 @@ class DoctorController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
$results['Freetype Support'] = true;
|
||||
// @see AvatarService::hasDependencies()
|
||||
if (!\function_exists('imagettfbbox')) {
|
||||
$results['Freetype Support'] = false;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
@@ -218,20 +209,31 @@ class DoctorController extends AbstractController
|
||||
|
||||
private function getFilePermissions()
|
||||
{
|
||||
$results = [];
|
||||
$testPaths = [];
|
||||
$baseDir = $this->projectDirectory . DIRECTORY_SEPARATOR;
|
||||
|
||||
foreach (self::DIRECTORIES_WRITABLE as $path) {
|
||||
$results[$path] = false;
|
||||
$fullPath = $this->projectDirectory . '/' . $path;
|
||||
$fullPath = $baseDir . $path;
|
||||
$fullUri = realpath($fullPath);
|
||||
|
||||
if ($fullUri === false && !file_exists($fullPath)) {
|
||||
@mkdir($fullPath);
|
||||
clearstatcache(true);
|
||||
$fullUri = realpath($fullPath);
|
||||
}
|
||||
|
||||
$testPaths[] = $fullUri;
|
||||
}
|
||||
|
||||
$results = [];
|
||||
$testPaths[] = $this->fileHelper->getDataDirectory();
|
||||
foreach ($testPaths as $fullUri) {
|
||||
$fullUri = rtrim($fullUri, DIRECTORY_SEPARATOR);
|
||||
$tmp = str_replace($baseDir, '', $fullUri) . DIRECTORY_SEPARATOR;
|
||||
if ($fullUri !== false && is_readable($fullUri) && is_writable($fullUri)) {
|
||||
$results[$path] = true;
|
||||
$results[$tmp] = true;
|
||||
} else {
|
||||
$results[$tmp] = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -354,10 +354,10 @@ final class SystemConfigurationController extends AbstractController
|
||||
(new Configuration())
|
||||
->setName('timesheet.time_increment')
|
||||
->setType(MinuteIncrementType::class)
|
||||
->setOptions(['deactivate' => false])
|
||||
->setOptions(['deactivate' => false, 'max_one_hour' => true])
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setConstraints([
|
||||
new GreaterThanOrEqual(['value' => 1])
|
||||
new Range(['min' => 1, 'max' => 60])
|
||||
]),
|
||||
(new Configuration())
|
||||
->setName('timesheet.duration_increment')
|
||||
@@ -382,6 +382,10 @@ final class SystemConfigurationController extends AbstractController
|
||||
->setConstraints([
|
||||
new GreaterThanOrEqual(['value' => 0])
|
||||
]),
|
||||
(new Configuration())
|
||||
->setName('defaults.timesheet.billable')
|
||||
->setType(YesNoType::class)
|
||||
->setOptions(['help' => 'default_value_new', 'label' => 'label.billable']),
|
||||
]),
|
||||
(new SystemConfigurationModel())
|
||||
->setSection(SystemConfigurationModel::SECTION_LOCKDOWN)
|
||||
@@ -449,11 +453,12 @@ final class SystemConfigurationController extends AbstractController
|
||||
(new SystemConfigurationModel())
|
||||
->setSection(SystemConfigurationModel::SECTION_FORM_INVOICE)
|
||||
->setConfiguration([
|
||||
// TODO that should be a custom type with validation
|
||||
(new Configuration())
|
||||
->setName('invoice.number_format')
|
||||
->setLabel('invoice.number_format')
|
||||
->setRequired(true)
|
||||
->setType(TextType::class) // TODO that should be a custom type with validation
|
||||
->setType(TextType::class)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
(new Configuration())
|
||||
->setName('invoice.simple_form')
|
||||
@@ -470,15 +475,18 @@ final class SystemConfigurationController extends AbstractController
|
||||
->setName('defaults.customer.timezone')
|
||||
->setLabel('timezone')
|
||||
->setType(TimezoneType::class)
|
||||
->setValue(date_default_timezone_get()),
|
||||
->setValue(date_default_timezone_get())
|
||||
->setOptions(['help' => 'default_value_new']),
|
||||
(new Configuration())
|
||||
->setName('defaults.customer.country')
|
||||
->setLabel('country')
|
||||
->setType(CountryType::class),
|
||||
->setType(CountryType::class)
|
||||
->setOptions(['help' => 'default_value_new']),
|
||||
(new Configuration())
|
||||
->setName('defaults.customer.currency')
|
||||
->setLabel('currency')
|
||||
->setType(CurrencyType::class),
|
||||
->setType(CurrencyType::class)
|
||||
->setOptions(['help' => 'default_value_new']),
|
||||
]),
|
||||
(new SystemConfigurationModel())
|
||||
->setSection(SystemConfigurationModel::SECTION_FORM_USER)
|
||||
@@ -487,19 +495,28 @@ final class SystemConfigurationController extends AbstractController
|
||||
->setName('defaults.user.timezone')
|
||||
->setLabel('timezone')
|
||||
->setType(TimezoneType::class)
|
||||
->setValue(date_default_timezone_get()),
|
||||
->setValue(date_default_timezone_get())
|
||||
->setOptions(['help' => 'default_value_new']),
|
||||
(new Configuration())
|
||||
->setName('defaults.user.language')
|
||||
->setLabel('language')
|
||||
->setType(LanguageType::class),
|
||||
->setType(LanguageType::class)
|
||||
->setOptions(['help' => 'default_value_new']),
|
||||
(new Configuration())
|
||||
->setName('defaults.user.theme')
|
||||
->setLabel('skin')
|
||||
->setType(SkinType::class),
|
||||
->setType(SkinType::class)
|
||||
->setOptions(['help' => 'default_value_new']),
|
||||
(new Configuration())
|
||||
->setName('defaults.user.currency')
|
||||
->setLabel('currency')
|
||||
->setType(CurrencyType::class),
|
||||
(new Configuration())
|
||||
->setName('theme.avatar_url')
|
||||
->setRequired(false)
|
||||
->setLabel('theme.avatar_url')
|
||||
->setType(CheckboxType::class)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
]),
|
||||
(new SystemConfigurationModel())
|
||||
->setSection(SystemConfigurationModel::SECTION_THEME)
|
||||
@@ -532,13 +549,6 @@ final class SystemConfigurationController extends AbstractController
|
||||
->setOptions(['help' => 'help.theme.color_choices'])
|
||||
->setConstraints([new ColorChoices()])
|
||||
->setTranslationDomain('system-configuration'),
|
||||
// allow avatar URLs
|
||||
(new Configuration())
|
||||
->setName('theme.avatar_url')
|
||||
->setRequired(false)
|
||||
->setLabel('theme.avatar_url')
|
||||
->setType(CheckboxType::class)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
// random colors as fallback
|
||||
(new Configuration())
|
||||
->setName('theme.random_colors')
|
||||
|
||||
@@ -215,6 +215,9 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
{
|
||||
$copyTimesheet = clone $timesheet;
|
||||
|
||||
$event = new TimesheetMetaDefinitionEvent($copyTimesheet);
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
$form = $this->getDuplicateForm($copyTimesheet, $timesheet);
|
||||
$form->handleRequest($request);
|
||||
|
||||
@@ -361,6 +364,10 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
$timesheet->setExported($dto->isExported());
|
||||
$execute = true;
|
||||
}
|
||||
if (null !== $dto->isBillable()) {
|
||||
$timesheet->setBillable($dto->isBillable());
|
||||
$execute = true;
|
||||
}
|
||||
|
||||
if ($dto->isRecalculateRates()) {
|
||||
$timesheet->setFixedRate(null);
|
||||
|
||||
@@ -635,6 +635,12 @@ class Configuration implements ConfigurationInterface
|
||||
->scalarNode('currency')->defaultValue(Customer::DEFAULT_CURRENCY)->end()
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('timesheet')
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->booleanNode('billable')->defaultTrue()->end()
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('user')
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
|
||||
@@ -47,7 +47,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
* }
|
||||
* )
|
||||
*
|
||||
* @Exporter\Order({"id", "name", "project", "budget", "timeBudget", "color", "visible", "comment"})
|
||||
* @Exporter\Order({"id", "name", "project", "budget", "timeBudget", "budgetType", "color", "visible", "comment"})
|
||||
* @Exporter\Expose("project", label="label.project", exp="object.getProject() === null ? null : object.getProject().getName()")
|
||||
*/
|
||||
class Activity implements EntityWithMetaFields, EntityWithBudget
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Export\Annotation as Exporter;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use JMS\Serializer\Annotation as Serializer;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
@@ -23,7 +24,7 @@ trait BudgetTrait
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Activity_Entity", "Project_Entity", "Customer_Entity"})
|
||||
*
|
||||
* @ Exporter\Expose(label="label.budget")
|
||||
* @Exporter\Expose(label="label.budget", type="float")
|
||||
*
|
||||
* @ORM\Column(name="budget", type="float", nullable=false)
|
||||
* @Assert\Range(min=0.00, max=900000000000.00)
|
||||
@@ -38,7 +39,7 @@ trait BudgetTrait
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Activity_Entity", "Project_Entity", "Customer_Entity"})
|
||||
*
|
||||
* @ Exporter\Expose(label="label.timeBudget", type="duration")
|
||||
* @Exporter\Expose(label="label.timeBudget", type="duration")
|
||||
*
|
||||
* @ORM\Column(name="time_budget", type="integer", nullable=false)
|
||||
* @Assert\Range(min=0, max=2145600000)
|
||||
@@ -55,7 +56,7 @@ trait BudgetTrait
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Activity_Entity", "Project_Entity", "Customer_Entity"})
|
||||
*
|
||||
* @ Exporter\Expose(label="label.timeBudget", type="duration")
|
||||
* @Exporter\Expose(label="label.budgetType")
|
||||
*
|
||||
* @ORM\Column(name="budget_type", type="string", length=10, nullable=true)
|
||||
*/
|
||||
@@ -99,6 +100,11 @@ trait BudgetTrait
|
||||
$this->budgetType = $budgetType;
|
||||
}
|
||||
|
||||
public function setIsMonthlyBudget(): void
|
||||
{
|
||||
$this->setBudgetType('month');
|
||||
}
|
||||
|
||||
public function getBudgetType(): ?string
|
||||
{
|
||||
return $this->budgetType;
|
||||
|
||||
@@ -27,7 +27,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
*
|
||||
* @Serializer\ExclusionPolicy("all")
|
||||
*
|
||||
* @Exporter\Order({"id", "name", "company", "number", "vatId", "address", "contact","email", "phone", "mobile", "fax", "homepage", "country", "currency", "timezone", "budget", "timeBudget", "color", "visible", "teams", "comment"})
|
||||
* @Exporter\Order({"id", "name", "company", "number", "vatId", "address", "contact","email", "phone", "mobile", "fax", "homepage", "country", "currency", "timezone", "budget", "timeBudget", "budgetType", "color", "visible", "teams", "comment"})
|
||||
* @ Exporter\Expose("teams", label="label.team", exp="object.getTeams().toArray()", type="array")
|
||||
*/
|
||||
class Customer implements EntityWithMetaFields, EntityWithBudget
|
||||
@@ -67,7 +67,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
|
||||
* @var string
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.number")
|
||||
*
|
||||
|
||||
@@ -48,7 +48,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
* }
|
||||
* )
|
||||
*
|
||||
* @Exporter\Order({"id", "name", "customer", "orderNumber", "orderDate", "start", "end", "budget", "timeBudget", "color", "visible", "teams", "comment"})
|
||||
* @Exporter\Order({"id", "name", "customer", "orderNumber", "orderDate", "start", "end", "budget", "timeBudget", "budgetType", "color", "visible", "teams", "comment"})
|
||||
* @Exporter\Expose("customer", label="label.customer", exp="object.getCustomer() === null ? null : object.getCustomer().getName()")
|
||||
* @ Exporter\Expose("teams", label="label.team", exp="object.getTeams().toArray()", type="array")
|
||||
*/
|
||||
@@ -474,6 +474,9 @@ class Project implements EntityWithMetaFields, EntityWithBudget
|
||||
if ($this->getCustomer() !== null && !$this->getCustomer()->isVisible()) {
|
||||
return false;
|
||||
}
|
||||
if ($this->getStart() !== null && $dateTime < $this->getStart()) {
|
||||
return false;
|
||||
}
|
||||
if ($this->getEnd() !== null && $dateTime > $this->getEnd()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
<?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\EventSubscriber;
|
||||
|
||||
use App\Event\DashboardEvent;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use App\Repository\Query\UserQuery;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Widget\Type\CompoundRow;
|
||||
use App\Widget\Type\More;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
|
||||
/**
|
||||
* Used to add Dashboard widgets for users with ROLE_ADMIN.
|
||||
*/
|
||||
class DashboardSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
/**
|
||||
* @var AuthorizationCheckerInterface
|
||||
*/
|
||||
protected $security;
|
||||
/**
|
||||
* @var UserRepository
|
||||
*/
|
||||
protected $user;
|
||||
/**
|
||||
* @var ActivityRepository
|
||||
*/
|
||||
protected $activity;
|
||||
/**
|
||||
* @var ProjectRepository
|
||||
*/
|
||||
protected $project;
|
||||
/**
|
||||
* @var CustomerRepository
|
||||
*/
|
||||
protected $customer;
|
||||
|
||||
/**
|
||||
* @param AuthorizationCheckerInterface $security
|
||||
* @param UserRepository $user
|
||||
* @param ActivityRepository $activity
|
||||
* @param ProjectRepository $project
|
||||
* @param CustomerRepository $customer
|
||||
*/
|
||||
public function __construct(
|
||||
AuthorizationCheckerInterface $security,
|
||||
UserRepository $user,
|
||||
ActivityRepository $activity,
|
||||
ProjectRepository $project,
|
||||
CustomerRepository $customer
|
||||
) {
|
||||
$this->security = $security;
|
||||
$this->user = $user;
|
||||
$this->activity = $activity;
|
||||
$this->project = $project;
|
||||
$this->customer = $customer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
DashboardEvent::class => ['onDashboardEvent', 100],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DashboardEvent $event
|
||||
*/
|
||||
public function onDashboardEvent(DashboardEvent $event)
|
||||
{
|
||||
$user = $event->getUser();
|
||||
$section = new CompoundRow();
|
||||
$section->setTitle('');
|
||||
$section->setOrder(100);
|
||||
|
||||
if ($this->security->isGranted('view_user')) {
|
||||
$query = new UserQuery();
|
||||
$query->setCurrentUser($user);
|
||||
$section->addWidget(
|
||||
(new More())
|
||||
->setId('userTotal')
|
||||
->setTitle('stats.userTotal')
|
||||
->setData($this->user->countUsersForQuery($query))
|
||||
->setOptions([
|
||||
'route' => 'admin_user',
|
||||
'icon' => 'user',
|
||||
'color' => 'primary',
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->security->isGranted('view_customer')) {
|
||||
$query = new CustomerQuery();
|
||||
$query->setCurrentUser($user);
|
||||
$section->addWidget(
|
||||
(new More())
|
||||
->setId('customerTotal')
|
||||
->setTitle('stats.customerTotal')
|
||||
->setData($this->customer->countCustomersForQuery($query))
|
||||
->setOptions([
|
||||
'route' => 'admin_customer',
|
||||
'icon' => 'customer',
|
||||
'color' => 'primary',
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->security->isGranted('view_project')) {
|
||||
$query = new ProjectQuery();
|
||||
$query->setCurrentUser($user);
|
||||
$section->addWidget(
|
||||
(new More())
|
||||
->setId('projectTotal')
|
||||
->setTitle('stats.projectTotal')
|
||||
->setData($this->project->countProjectsForQuery($query))
|
||||
->setOptions([
|
||||
'route' => 'admin_project',
|
||||
'icon' => 'project',
|
||||
'color' => 'primary',
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->security->isGranted('view_activity')) {
|
||||
$query = new ActivityQuery();
|
||||
$query->setCurrentUser($user);
|
||||
$section->addWidget(
|
||||
(new More())
|
||||
->setId('activityTotal')
|
||||
->setTitle('stats.activityTotal')
|
||||
->setData($this->activity->countActivitiesForQuery($query))
|
||||
->setOptions([
|
||||
'route' => 'admin_activity',
|
||||
'icon' => 'activity',
|
||||
'color' => 'primary',
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
if (\count($section->getWidgets()) > 0) {
|
||||
$event->addSection($section);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,8 @@ class SystemConfiguration
|
||||
public const SECTION_LOCKDOWN = 'lockdown_period';
|
||||
public const SECTION_TIMESHEET = 'timesheet';
|
||||
public const SECTION_FORM_INVOICE = 'invoice';
|
||||
public const SECTION_FORM_CUSTOMER = 'form_customer';
|
||||
public const SECTION_FORM_USER = 'form_user';
|
||||
public const SECTION_FORM_CUSTOMER = 'customer';
|
||||
public const SECTION_FORM_USER = 'user';
|
||||
public const SECTION_THEME = 'theme';
|
||||
public const SECTION_AUTHENTICATION = 'authentication';
|
||||
public const SECTION_CALENDAR = 'calendar';
|
||||
|
||||
@@ -207,6 +207,17 @@ class TimesheetMultiUpdate extends AbstractType
|
||||
]);
|
||||
}
|
||||
|
||||
if ($options['include_billable']) {
|
||||
$builder->add('billable', ChoiceType::class, [
|
||||
'label' => 'label.billable',
|
||||
'choices' => [
|
||||
'' => null,
|
||||
'yes' => true,
|
||||
'no' => false,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
if ($options['include_rate']) {
|
||||
$builder
|
||||
->add('recalculateRates', YesNoType::class, [
|
||||
@@ -279,6 +290,7 @@ class TimesheetMultiUpdate extends AbstractType
|
||||
'include_user' => false,
|
||||
'include_rate' => false,
|
||||
'include_exported' => false,
|
||||
'include_billable' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ use Doctrine\Common\Collections\Collection;
|
||||
|
||||
/**
|
||||
* @App\Validator\Constraints\TimesheetMultiUpdate
|
||||
* @internal
|
||||
*/
|
||||
class TimesheetMultiUpdateDTO extends MultiUpdateTableDTO implements EntityWithMetaFields
|
||||
{
|
||||
@@ -57,6 +58,10 @@ class TimesheetMultiUpdateDTO extends MultiUpdateTableDTO implements EntityWithM
|
||||
* @var bool|null
|
||||
*/
|
||||
private $exported = null;
|
||||
/**
|
||||
* @var bool|null
|
||||
*/
|
||||
private $billable = null;
|
||||
/**
|
||||
* @var float|null
|
||||
*/
|
||||
@@ -84,11 +89,9 @@ class TimesheetMultiUpdateDTO extends MultiUpdateTableDTO implements EntityWithM
|
||||
return $this->customer;
|
||||
}
|
||||
|
||||
public function setCustomer(Customer $customer): TimesheetMultiUpdateDTO
|
||||
public function setCustomer(Customer $customer): void
|
||||
{
|
||||
$this->customer = $customer;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getProject(): ?Project
|
||||
@@ -96,11 +99,9 @@ class TimesheetMultiUpdateDTO extends MultiUpdateTableDTO implements EntityWithM
|
||||
return $this->project;
|
||||
}
|
||||
|
||||
public function setProject(Project $project): TimesheetMultiUpdateDTO
|
||||
public function setProject(Project $project): void
|
||||
{
|
||||
$this->project = $project;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getActivity(): ?Activity
|
||||
@@ -108,11 +109,9 @@ class TimesheetMultiUpdateDTO extends MultiUpdateTableDTO implements EntityWithM
|
||||
return $this->activity;
|
||||
}
|
||||
|
||||
public function setActivity(Activity $activity): TimesheetMultiUpdateDTO
|
||||
public function setActivity(Activity $activity): void
|
||||
{
|
||||
$this->activity = $activity;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,11 +122,9 @@ class TimesheetMultiUpdateDTO extends MultiUpdateTableDTO implements EntityWithM
|
||||
return $this->tags;
|
||||
}
|
||||
|
||||
public function setTags(iterable $tags): TimesheetMultiUpdateDTO
|
||||
public function setTags(iterable $tags): void
|
||||
{
|
||||
$this->tags = $tags;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
@@ -135,11 +132,9 @@ class TimesheetMultiUpdateDTO extends MultiUpdateTableDTO implements EntityWithM
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function setUser(User $user): TimesheetMultiUpdateDTO
|
||||
public function setUser(User $user): void
|
||||
{
|
||||
$this->user = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isExported(): ?bool
|
||||
@@ -147,11 +142,19 @@ class TimesheetMultiUpdateDTO extends MultiUpdateTableDTO implements EntityWithM
|
||||
return $this->exported;
|
||||
}
|
||||
|
||||
public function setExported(bool $exported): TimesheetMultiUpdateDTO
|
||||
public function setExported(?bool $exported): void
|
||||
{
|
||||
$this->exported = $exported;
|
||||
}
|
||||
|
||||
return $this;
|
||||
public function isBillable(): ?bool
|
||||
{
|
||||
return $this->billable;
|
||||
}
|
||||
|
||||
public function setBillable(?bool $billable): void
|
||||
{
|
||||
$this->billable = $billable;
|
||||
}
|
||||
|
||||
public function isRecalculateRates(): bool
|
||||
@@ -159,11 +162,9 @@ class TimesheetMultiUpdateDTO extends MultiUpdateTableDTO implements EntityWithM
|
||||
return $this->recalculateRates;
|
||||
}
|
||||
|
||||
public function setRecalculateRates(bool $recalculateRates): TimesheetMultiUpdateDTO
|
||||
public function setRecalculateRates(bool $recalculateRates): void
|
||||
{
|
||||
$this->recalculateRates = $recalculateRates;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isReplaceTags(): bool
|
||||
@@ -171,11 +172,9 @@ class TimesheetMultiUpdateDTO extends MultiUpdateTableDTO implements EntityWithM
|
||||
return $this->replaceTags;
|
||||
}
|
||||
|
||||
public function setReplaceTags(bool $replaceTags): TimesheetMultiUpdateDTO
|
||||
public function setReplaceTags(bool $replaceTags): void
|
||||
{
|
||||
$this->replaceTags = $replaceTags;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFixedRate(): ?float
|
||||
@@ -183,11 +182,9 @@ class TimesheetMultiUpdateDTO extends MultiUpdateTableDTO implements EntityWithM
|
||||
return $this->fixedRate;
|
||||
}
|
||||
|
||||
public function setFixedRate(?float $fixedRate): TimesheetMultiUpdateDTO
|
||||
public function setFixedRate(?float $fixedRate): void
|
||||
{
|
||||
$this->fixedRate = $fixedRate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getHourlyRate(): ?float
|
||||
@@ -195,11 +192,9 @@ class TimesheetMultiUpdateDTO extends MultiUpdateTableDTO implements EntityWithM
|
||||
return $this->hourlyRate;
|
||||
}
|
||||
|
||||
public function setHourlyRate(?float $hourlyRate): TimesheetMultiUpdateDTO
|
||||
public function setHourlyRate(?float $hourlyRate): void
|
||||
{
|
||||
$this->hourlyRate = $hourlyRate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -55,13 +55,20 @@ class DateTimePickerType extends AbstractType
|
||||
|
||||
public function buildView(FormView $view, FormInterface $form, array $options)
|
||||
{
|
||||
$view->vars['attr'] = array_merge($view->vars['attr'], [
|
||||
$attr = array_merge($view->vars['attr'], [
|
||||
'data-datetimepicker' => 'on',
|
||||
'autocomplete' => 'off',
|
||||
'placeholder' => strtoupper($options['format']),
|
||||
'data-format' => $options['format_picker'],
|
||||
'data-time-picker-increment' => $options['time_increment'],
|
||||
]);
|
||||
|
||||
if ($options['time_increment'] !== null) {
|
||||
if ($options['time_increment'] >= 1) {
|
||||
$attr['data-time-picker-increment'] = $options['time_increment'];
|
||||
}
|
||||
}
|
||||
|
||||
$view->vars['attr'] = $attr;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -26,6 +26,7 @@ class MinuteIncrementType extends AbstractType
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'deactivate' => true,
|
||||
'max_one_hour' => false,
|
||||
]);
|
||||
|
||||
$resolver->setDefault('choices', function (Options $options) {
|
||||
@@ -47,8 +48,11 @@ class MinuteIncrementType extends AbstractType
|
||||
$choices['30'] = '30';
|
||||
$choices['45'] = '45';
|
||||
$choices['60'] = '60';
|
||||
$choices['90'] = '90';
|
||||
$choices['120'] = '120';
|
||||
|
||||
if (!$options['max_one_hour']) {
|
||||
$choices['90'] = '90';
|
||||
$choices['120'] = '120';
|
||||
}
|
||||
|
||||
return $choices;
|
||||
});
|
||||
|
||||
@@ -27,7 +27,6 @@ class UserRolesType extends AbstractType
|
||||
{
|
||||
$builder
|
||||
->add('roles', UserRoleType::class, [
|
||||
'label' => 'label.roles',
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
])
|
||||
|
||||
@@ -27,7 +27,6 @@ class UserTeamsType extends AbstractType
|
||||
{
|
||||
$builder
|
||||
->add('teams', TeamType::class, [
|
||||
'label' => 'label.team',
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
])
|
||||
|
||||
@@ -35,7 +35,7 @@ final class DocxRenderer extends AbstractRenderer implements RendererInterface
|
||||
|
||||
foreach ($model->toArray() as $search => $replace) {
|
||||
$replace = $xmlEscaper->escape($replace);
|
||||
$replace = str_replace(PHP_EOL, '</w:t><w:br /><w:t xml:space="preserve">', $replace);
|
||||
$replace = preg_replace('/\n|\r\n?/', '</w:t><w:br /><w:t xml:space="preserve">', $replace);
|
||||
|
||||
$template->setValue($search, $replace);
|
||||
}
|
||||
@@ -57,7 +57,7 @@ final class DocxRenderer extends AbstractRenderer implements RendererInterface
|
||||
$values = $model->itemToArray($entry);
|
||||
foreach ($values as $search => $replace) {
|
||||
$replace = $xmlEscaper->escape($replace);
|
||||
$replace = str_replace(PHP_EOL, '</w:t><w:br /><w:t xml:space="preserve">', $replace);
|
||||
$replace = preg_replace('/\n|\r\n?/', '</w:t><w:br /><w:t xml:space="preserve">', $replace);
|
||||
|
||||
$template->setValue($search . '#' . $i, $replace);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ final class Version20210717211144 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
return 'Adds the account column to the user table';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
|
||||
@@ -133,7 +133,9 @@ class ProjectStatisticService
|
||||
$qb = $this->repository->createQueryBuilder('p');
|
||||
$qb
|
||||
->select('p')
|
||||
->leftJoin('p.customer', 'c')
|
||||
->andWhere($qb->expr()->eq('p.visible', true))
|
||||
->andWhere($qb->expr()->eq('c.visible', true))
|
||||
->andWhere(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->orX(
|
||||
|
||||
@@ -211,7 +211,7 @@ class TeamRepository extends EntityRepository
|
||||
$or->add(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->eq('members.user', ':id'),
|
||||
$qb->expr()->eq('members.teamlead', true),
|
||||
$qb->expr()->eq('members.teamlead', true)
|
||||
)
|
||||
);
|
||||
$qb->setParameter('id', $user);
|
||||
|
||||
@@ -65,32 +65,43 @@ class DateTimeFactory
|
||||
public function getStartOfWeek(?DateTime $date = null): DateTime
|
||||
{
|
||||
if (null === $date) {
|
||||
$from = $this->createDateTime('now');
|
||||
$date = $this->createDateTime('now');
|
||||
} else {
|
||||
$from = clone $date;
|
||||
$date = clone $date;
|
||||
}
|
||||
|
||||
$year = $from->format('o');
|
||||
$week = $from->format('W');
|
||||
$firstDay = 1;
|
||||
|
||||
if ($this->startOnSunday) {
|
||||
$from->modify('-1 week');
|
||||
$year = $from->format('o');
|
||||
$week = $from->format('W');
|
||||
$firstDay = 7;
|
||||
|
||||
// if today = sunday => increase week by one
|
||||
if ($date->format('N') !== '7') {
|
||||
$date->modify('-1 week');
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createWeekDateTime($year, $week, $firstDay, 0, 0, 0);
|
||||
return $this->createWeekDateTime($date->format('o'), $date->format('W'), $firstDay, 0, 0, 0);
|
||||
}
|
||||
|
||||
public function getEndOfWeek(?DateTime $date = null): DateTime
|
||||
{
|
||||
if (null === $date) {
|
||||
$date = $this->createDateTime();
|
||||
} else {
|
||||
$date = clone $date;
|
||||
}
|
||||
|
||||
$lastDay = $this->startOnSunday ? 6 : 7;
|
||||
$lastDay = 7;
|
||||
|
||||
if ($this->startOnSunday) {
|
||||
$lastDay = 6;
|
||||
|
||||
// only change when today is not sunday
|
||||
if ($date->format('N') === '7') {
|
||||
$date->modify('+1 week');
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createWeekDateTime($date->format('o'), $date->format('W'), $lastDay, 23, 59, 59);
|
||||
}
|
||||
|
||||
@@ -85,7 +85,11 @@ final class RateService implements RateServiceInterface
|
||||
}
|
||||
}
|
||||
|
||||
$factor = $this->getRateFactor($record);
|
||||
$factor = 1.00;
|
||||
// do not apply once a value was calculated - see https://github.com/kevinpapst/kimai2/issues/1988
|
||||
if ($record->getFixedRate() === null && $record->getHourlyRate() === null) {
|
||||
$factor = $this->getRateFactor($record);
|
||||
}
|
||||
|
||||
$factoredHourlyRate = (float) ($hourlyRate * $factor);
|
||||
$factoredInternalRate = (float) ($internalRate * $factor);
|
||||
|
||||
@@ -110,6 +110,8 @@ final class TimesheetService
|
||||
$mode = $this->trackingModeService->getActiveMode();
|
||||
$mode->create($timesheet, $request);
|
||||
|
||||
$timesheet->setBillable($this->configuration->getTimesheetDefaultBillable());
|
||||
|
||||
return $timesheet;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ final class PaginatedWorkingTimeChart extends SimpleWidget implements UserWidget
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->systemConfiguration = $systemConfiguration;
|
||||
$this->setId('PaginatedWorkingTimeChart');
|
||||
$this->setTitle('stats.yourWorkingHours');
|
||||
}
|
||||
|
||||
|
||||
65
src/Widget/Type/TotalsActivity.php
Normal file
65
src/Widget/Type/TotalsActivity.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?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\Widget\Type;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
|
||||
final class TotalsActivity extends SimpleWidget implements UserWidget, AuthorizedWidget
|
||||
{
|
||||
use UserWidgetTrait;
|
||||
|
||||
private $activity;
|
||||
|
||||
public function __construct(ActivityRepository $activity)
|
||||
{
|
||||
$this->activity = $activity;
|
||||
$this->setTitle('stats.activityTotal');
|
||||
}
|
||||
|
||||
public function getOptions(array $options = []): array
|
||||
{
|
||||
return array_merge([
|
||||
'route' => 'admin_activity',
|
||||
'icon' => 'activity',
|
||||
'color' => 'primary',
|
||||
'dataType' => 'int',
|
||||
], parent::getOptions($options));
|
||||
}
|
||||
|
||||
public function getData(array $options = [])
|
||||
{
|
||||
$options = $this->getOptions($options);
|
||||
|
||||
$user = $options['user'];
|
||||
if (null === $user || !($user instanceof User)) {
|
||||
throw new \InvalidArgumentException('Widget option "user" must be an instance of ' . User::class);
|
||||
}
|
||||
|
||||
$query = new ActivityQuery();
|
||||
$query->setCurrentUser($user);
|
||||
|
||||
return $this->activity->countActivitiesForQuery($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getPermissions(): array
|
||||
{
|
||||
return ['view_activity', 'view_teamlead_activity', 'view_team_activity'];
|
||||
}
|
||||
|
||||
public function getTemplateName(): string
|
||||
{
|
||||
return 'widget/widget-more.html.twig';
|
||||
}
|
||||
}
|
||||
65
src/Widget/Type/TotalsCustomer.php
Normal file
65
src/Widget/Type/TotalsCustomer.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?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\Widget\Type;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
|
||||
final class TotalsCustomer extends SimpleWidget implements UserWidget, AuthorizedWidget
|
||||
{
|
||||
use UserWidgetTrait;
|
||||
|
||||
private $customer;
|
||||
|
||||
public function __construct(CustomerRepository $customer)
|
||||
{
|
||||
$this->customer = $customer;
|
||||
$this->setTitle('stats.customerTotal');
|
||||
}
|
||||
|
||||
public function getOptions(array $options = []): array
|
||||
{
|
||||
return array_merge([
|
||||
'route' => 'admin_customer',
|
||||
'icon' => 'customer',
|
||||
'color' => 'primary',
|
||||
'dataType' => 'int',
|
||||
], parent::getOptions($options));
|
||||
}
|
||||
|
||||
public function getData(array $options = [])
|
||||
{
|
||||
$options = $this->getOptions($options);
|
||||
|
||||
$user = $options['user'];
|
||||
if (null === $user || !($user instanceof User)) {
|
||||
throw new \InvalidArgumentException('Widget option "user" must be an instance of ' . User::class);
|
||||
}
|
||||
|
||||
$query = new CustomerQuery();
|
||||
$query->setCurrentUser($user);
|
||||
|
||||
return $this->customer->countCustomersForQuery($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getPermissions(): array
|
||||
{
|
||||
return ['view_customer', 'view_teamlead_customer', 'view_team_customer'];
|
||||
}
|
||||
|
||||
public function getTemplateName(): string
|
||||
{
|
||||
return 'widget/widget-more.html.twig';
|
||||
}
|
||||
}
|
||||
65
src/Widget/Type/TotalsProject.php
Normal file
65
src/Widget/Type/TotalsProject.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?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\Widget\Type;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
|
||||
final class TotalsProject extends SimpleWidget implements UserWidget, AuthorizedWidget
|
||||
{
|
||||
use UserWidgetTrait;
|
||||
|
||||
private $project;
|
||||
|
||||
public function __construct(ProjectRepository $project)
|
||||
{
|
||||
$this->project = $project;
|
||||
$this->setTitle('stats.projectTotal');
|
||||
}
|
||||
|
||||
public function getOptions(array $options = []): array
|
||||
{
|
||||
return array_merge([
|
||||
'route' => 'admin_project',
|
||||
'icon' => 'project',
|
||||
'color' => 'primary',
|
||||
'dataType' => 'int',
|
||||
], parent::getOptions($options));
|
||||
}
|
||||
|
||||
public function getData(array $options = [])
|
||||
{
|
||||
$options = $this->getOptions($options);
|
||||
|
||||
$user = $options['user'];
|
||||
if (null === $user || !($user instanceof User)) {
|
||||
throw new \InvalidArgumentException('Widget option "user" must be an instance of ' . User::class);
|
||||
}
|
||||
|
||||
$query = new ProjectQuery();
|
||||
$query->setCurrentUser($user);
|
||||
|
||||
return $this->project->countProjectsForQuery($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getPermissions(): array
|
||||
{
|
||||
return ['view_project', 'view_teamlead_project', 'view_team_project'];
|
||||
}
|
||||
|
||||
public function getTemplateName(): string
|
||||
{
|
||||
return 'widget/widget-more.html.twig';
|
||||
}
|
||||
}
|
||||
65
src/Widget/Type/TotalsUser.php
Normal file
65
src/Widget/Type/TotalsUser.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?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\Widget\Type;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\Query\UserQuery;
|
||||
use App\Repository\UserRepository;
|
||||
|
||||
final class TotalsUser extends SimpleWidget implements UserWidget, AuthorizedWidget
|
||||
{
|
||||
use UserWidgetTrait;
|
||||
|
||||
private $user;
|
||||
|
||||
public function __construct(UserRepository $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->setTitle('stats.userTotal');
|
||||
}
|
||||
|
||||
public function getOptions(array $options = []): array
|
||||
{
|
||||
return array_merge([
|
||||
'route' => 'admin_user',
|
||||
'icon' => 'user',
|
||||
'color' => 'primary',
|
||||
'dataType' => 'int',
|
||||
], parent::getOptions($options));
|
||||
}
|
||||
|
||||
public function getData(array $options = [])
|
||||
{
|
||||
$options = $this->getOptions($options);
|
||||
|
||||
$user = $options['user'];
|
||||
if (null === $user || !($user instanceof User)) {
|
||||
throw new \InvalidArgumentException('Widget option "user" must be an instance of ' . User::class);
|
||||
}
|
||||
|
||||
$query = new UserQuery();
|
||||
$query->setCurrentUser($user);
|
||||
|
||||
return $this->user->countUsersForQuery($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getPermissions(): array
|
||||
{
|
||||
return ['view_user'];
|
||||
}
|
||||
|
||||
public function getTemplateName(): string
|
||||
{
|
||||
return 'widget/widget-more.html.twig';
|
||||
}
|
||||
}
|
||||
24
src/Widget/Type/UserWidgetTrait.php
Normal file
24
src/Widget/Type/UserWidgetTrait.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?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\Widget\Type;
|
||||
|
||||
use App\Entity\User;
|
||||
|
||||
/**
|
||||
* Needs to be used on a SimpleWidget
|
||||
* @internal
|
||||
*/
|
||||
trait UserWidgetTrait
|
||||
{
|
||||
public function setUser(User $user): void
|
||||
{
|
||||
$this->setOption('user', $user);
|
||||
}
|
||||
}
|
||||
@@ -618,9 +618,6 @@
|
||||
"symfony/process": {
|
||||
"version": "v4.0.3"
|
||||
},
|
||||
"symfony/profiler-pack": {
|
||||
"version": "v1.0.3"
|
||||
},
|
||||
"symfony/property-access": {
|
||||
"version": "v4.0.3"
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
|
||||
{% block page_title %}{{ 'menu.doctor'|trans }}{% endblock %}
|
||||
{% block page_subtitle %}Environment: {{ environment }} – Version: {{ constant('App\\Constants::VERSION') }} {{ constant('App\\Constants::STATUS') }}{% endblock %}
|
||||
{% block page_subtitle %}{{ 'label.version'|trans }}: {{ constant('App\\Constants::VERSION') }} {{ constant('App\\Constants::STATUS') }} ({{ environment }}){% endblock %}
|
||||
{% block page_actions %}
|
||||
{% set event = actions(app.user, 'doctor', 'index') %}
|
||||
{{ widgets.page_actions(event.actions) }}
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
<td class="text-nowrap text-right">{{ percentReached|number_format(2) }}%</td>
|
||||
</tr>
|
||||
{% set percentReached = 0 %}
|
||||
{% if stats.rate > 0 %}
|
||||
{% if stats.rateBillable > 0 %}
|
||||
{% set percentReached = (stats.internalRate / (stats.rateBillable / 100)) %}
|
||||
{% endif %}
|
||||
<tr>
|
||||
@@ -131,7 +131,7 @@
|
||||
<tr>
|
||||
<th colspan="3" class="text-nowrap text-right">
|
||||
{% if totalPercentReached < 100 %}
|
||||
{{ 'stats.percentUsedLeft'|trans({'%percent%': totalPercentReached|number_format(2), '%left%': (entity.timeBudget - stats.rateBillable)|money(currency)}) }}
|
||||
{{ 'stats.percentUsedLeft'|trans({'%percent%': totalPercentReached|number_format(2), '%left%': (entity.budget - stats.rateBillable)|money(currency)}) }}
|
||||
{% else %}
|
||||
{{ 'stats.percentUsed'|trans({'%percent%': totalPercentReached|number_format(2)}) }}
|
||||
{% endif %}
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
{% set tableName = 'export' %}
|
||||
|
||||
{% block page_title %}{{ 'export.title'|trans }}{% endblock %}
|
||||
{% block page_subtitle %}{{ 'export.subtitle'|trans }}{% endblock %}
|
||||
{% block page_actions %}
|
||||
{% set event = actions(app.user, 'export', (preview_show ? 'preview' : 'index')) %}
|
||||
{{ widgets.page_actions(event.actions) }}
|
||||
|
||||
@@ -25,18 +25,21 @@
|
||||
{% set totals = {'totals': 0} %}
|
||||
{% set columns = 2 %}
|
||||
<table class="table table-bordered table-hover dataTable">
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th> </th>
|
||||
{% for day in days.dateTimes %}
|
||||
<th class="text-center text-nowrap{% if day is weekend %} weekend{% endif %}">
|
||||
{{ day|day_name(true) }}<br>
|
||||
{{ day|format_date('short') }}
|
||||
</th>
|
||||
{% set columns = columns + 1 %}
|
||||
{% set totals = totals|merge({(day|report_date): 0}) %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<thead>
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th> </th>
|
||||
{% for day in days.dateTimes %}
|
||||
<th class="text-center text-nowrap{% if day is weekend %} weekend{% endif %}">
|
||||
{{ day|day_name(true) }}<br>
|
||||
{{ day|format_date('short') }}
|
||||
</th>
|
||||
{% set columns = columns + 1 %}
|
||||
{% set totals = totals|merge({(day|report_date): 0}) %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% set oldCustomer = null %}
|
||||
{% for pid, project in rows|sort((a,b) => a.project.customer.id <=> b.project.customer.id) %}
|
||||
{% if oldCustomer is null or oldCustomer != project.project.customer.id %}
|
||||
@@ -76,15 +79,18 @@
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
<tr class="summary">
|
||||
<th></th>
|
||||
<th class="text-nowrap text-center total">{{ totals['totals']|duration }}</th>
|
||||
{% for day in days.dateTimes %}
|
||||
<th class="text-nowrap text-center day-total{% if day is weekend %} weekend{% endif %}">
|
||||
{{ totals[day|report_date]|duration }}
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="summary">
|
||||
<td>{{ 'stats.durationTotal'|trans }}</td>
|
||||
<td class="text-nowrap text-center total">{{ totals['totals']|duration }}</td>
|
||||
{% for day in days.dateTimes %}
|
||||
<td class="text-nowrap text-center day-total{% if day is weekend %} weekend{% endif %}">
|
||||
{{ totals[day|report_date]|duration }}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
|
||||
@@ -23,17 +23,20 @@
|
||||
{% set absoluteTotals = 0 %}
|
||||
{% set totals = {} %}
|
||||
<table class="table table-bordered table-hover dataTable">
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th> </th>
|
||||
{% for day in stats.0.getDateTimes() %}
|
||||
<th class="text-center text-nowrap{% if day is weekend %} weekend{% endif %}">
|
||||
{{ day|day_name(true) }}<br>
|
||||
{{ day|format_date('short') }}
|
||||
</th>
|
||||
{% set totals = totals|merge({(day|report_date): 0}) %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<thead>
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th> </th>
|
||||
{% for day in stats.0.getDateTimes() %}
|
||||
<th class="text-center text-nowrap{% if day is weekend %} weekend{% endif %}">
|
||||
{{ day|day_name(true) }}<br>
|
||||
{{ day|format_date('short') }}
|
||||
</th>
|
||||
{% set totals = totals|merge({(day|report_date): 0}) %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for userDay in stats %}
|
||||
{% set usersTotalDuration = 0 %}
|
||||
<tr class="user">
|
||||
@@ -59,17 +62,20 @@
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th class="text-center text-nowrap">
|
||||
{{ absoluteTotals|duration }}
|
||||
</th>
|
||||
{% for id, duration in totals %}
|
||||
<th class="text-center text-nowrap">
|
||||
{{ duration|duration }}
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="summary">
|
||||
<td>{{ 'stats.durationTotal'|trans }}</td>
|
||||
<td class="text-center text-nowrap">
|
||||
{{ absoluteTotals|duration }}
|
||||
</td>
|
||||
{% for id, duration in totals %}
|
||||
<td class="text-center text-nowrap">
|
||||
{{ duration|duration }}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -22,19 +22,22 @@
|
||||
{% else %}
|
||||
{% set totals = {} %}
|
||||
<table class="table table-bordered table-hover dataTable">
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th> </th>
|
||||
{% for month in stats.0.getDateTimes() %}
|
||||
<th class="text-center text-nowrap">
|
||||
<a href="{{ path('report_monthly_users', {'date': month|report_date}) }}">
|
||||
{{ month|month_name }}<br>
|
||||
{{ month|date_format('Y') }}
|
||||
</a>
|
||||
</th>
|
||||
{% set totals = totals|merge({(month|report_date): 0}) %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<thead>
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th> </th>
|
||||
{% for month in stats.0.getDateTimes() %}
|
||||
<th class="text-center text-nowrap">
|
||||
<a href="{{ path('report_monthly_users', {'date': month|report_date}) }}">
|
||||
{{ month|month_name }}<br>
|
||||
{{ month|date_format('Y') }}
|
||||
</a>
|
||||
</th>
|
||||
{% set totals = totals|merge({(month|report_date): 0}) %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for userYear in stats|filter(row => row.user is not null) %}
|
||||
{% set usersTotalDuration = 0 %}
|
||||
<tr class="user">
|
||||
@@ -61,15 +64,18 @@
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th> </th>
|
||||
{% for id, total in totals %}
|
||||
<th class="text-center text-nowrap">
|
||||
{{ total|duration }}
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="summary">
|
||||
<td>{{ 'stats.durationTotal'|trans }}</td>
|
||||
<td> </td>
|
||||
{% for id, total in totals %}
|
||||
<td class="text-center text-nowrap">
|
||||
{{ total|duration }}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
{% import "system-configuration/actions.html.twig" as actions %}
|
||||
|
||||
{% block page_title %}{{ 'title'|trans({}, 'system-configuration') }}{% endblock %}
|
||||
{% block page_subtitle %}{{ 'subtitle'|trans({}, 'system-configuration') }}{% endblock %}
|
||||
{% block page_actions %}{{ actions.system_configuration('index') }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
@@ -10,8 +9,12 @@
|
||||
{% set formEditTemplate = app.request.xmlHttpRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig' %}
|
||||
|
||||
{% for section in sections %}
|
||||
{% set title = section.model.section|trans({}, 'system-configuration') %}
|
||||
{% if title == section.model.section %}
|
||||
{% set title = ('label.' ~ section.model.section)|trans %}
|
||||
{% endif %}
|
||||
{{ include(formEditTemplate, {
|
||||
'title': section.model.section|trans({}, 'system-configuration'),
|
||||
'title': title,
|
||||
'form': section.form,
|
||||
}) }}
|
||||
{% endfor %}
|
||||
|
||||
@@ -36,6 +36,11 @@
|
||||
{{ form_row(form.exported) }}
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
{% if form.billable is defined %}
|
||||
<fieldset>
|
||||
{{ form_row(form.billable) }}
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
{% if form.recalculateRates is defined %}
|
||||
<fieldset>
|
||||
{{ form_row(form.recalculateRates) }}
|
||||
|
||||
@@ -368,6 +368,7 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
'name' => 'string',
|
||||
'visible' => 'bool',
|
||||
'color' => '@string',
|
||||
'number' => '@string',
|
||||
];
|
||||
|
||||
// if a list of customers is loaded
|
||||
@@ -377,6 +378,7 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
'name' => 'string',
|
||||
'visible' => 'boolean',
|
||||
'color' => '@string',
|
||||
'number' => '@string',
|
||||
'metaFields' => ['result' => 'array', 'type' => 'CustomerMeta'],
|
||||
'teams' => ['result' => 'array', 'type' => 'Team'],
|
||||
'currency' => 'string', // since 1.10
|
||||
|
||||
@@ -165,6 +165,7 @@ class CustomerControllerTest extends APIControllerBaseTest
|
||||
$team->addProject($project);
|
||||
$team->addUser($this->getUserByRole(User::ROLE_TEAMLEAD));
|
||||
$em->persist($team);
|
||||
$em->flush();
|
||||
|
||||
$this->assertAccessIsGranted($client, '/api/customers/1');
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
|
||||
@@ -12,7 +12,6 @@ namespace App\Tests\Controller;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Form\Type\InitialViewType;
|
||||
use App\Form\Type\LanguageType;
|
||||
|
||||
/**
|
||||
* @group integration
|
||||
@@ -43,16 +42,12 @@ class HomepageControllerTest extends ControllerBaseTest
|
||||
->setValue('my_profile')
|
||||
->setType(InitialViewType::class);
|
||||
|
||||
$user->addPreference($pref);
|
||||
|
||||
$pref = (new UserPreference())
|
||||
->setName('language')
|
||||
->setValue('ar')
|
||||
->setType(LanguageType::class);
|
||||
|
||||
$user->addPreference($pref);
|
||||
|
||||
$em->persist($pref);
|
||||
$user->addPreference($pref);
|
||||
|
||||
$user->setLanguage('ar');
|
||||
|
||||
$em->flush();
|
||||
|
||||
$this->request($client, '/homepage');
|
||||
$this->assertIsRedirect($client, '/ar/profile/');
|
||||
|
||||
@@ -214,6 +214,7 @@ class ProjectControllerTest extends ControllerBaseTest
|
||||
$rate->setActivity($activity);
|
||||
$rate->setRate(123.45);
|
||||
$em->persist($rate);
|
||||
$em->flush();
|
||||
|
||||
$this->request($client, '/admin/project/1/duplicate');
|
||||
$this->assertIsRedirect($client, '/details');
|
||||
|
||||
@@ -54,8 +54,6 @@ class SystemConfigurationControllerTest extends ControllerBaseTest
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/admin/system-config/edit/timesheet');
|
||||
|
||||
$expectedForms = $this->getTestDataForms();
|
||||
|
||||
$result = $client->getCrawler()->filter('section.content div.box.box-primary');
|
||||
$this->assertEquals(1, \count($result));
|
||||
|
||||
@@ -76,8 +74,8 @@ class SystemConfigurationControllerTest extends ControllerBaseTest
|
||||
['form[name=system_configuration_form_invoice]', $this->createUrl('/admin/system-config/update/invoice')],
|
||||
['form[name=system_configuration_form_authentication]', $this->createUrl('/admin/system-config/update/authentication')],
|
||||
['form[name=system_configuration_form_rounding]', $this->createUrl('/admin/system-config/update/rounding')],
|
||||
['form[name=system_configuration_form_form_customer]', $this->createUrl('/admin/system-config/update/form_customer')],
|
||||
['form[name=system_configuration_form_form_user]', $this->createUrl('/admin/system-config/update/form_user')],
|
||||
['form[name=system_configuration_form_customer]', $this->createUrl('/admin/system-config/update/customer')],
|
||||
['form[name=system_configuration_form_user]', $this->createUrl('/admin/system-config/update/user')],
|
||||
['form[name=system_configuration_form_theme]', $this->createUrl('/admin/system-config/update/theme')],
|
||||
['form[name=system_configuration_form_calendar]', $this->createUrl('/admin/system-config/update/calendar')],
|
||||
['form[name=system_configuration_form_branding]', $this->createUrl('/admin/system-config/update/branding')],
|
||||
@@ -191,9 +189,9 @@ class SystemConfigurationControllerTest extends ControllerBaseTest
|
||||
$this->assertEquals('DE', $configService->find('defaults.customer.country'));
|
||||
$this->assertEquals('EUR', $configService->find('defaults.customer.currency'));
|
||||
|
||||
$form = $client->getCrawler()->filter('form[name=system_configuration_form_form_customer]')->form();
|
||||
$form = $client->getCrawler()->filter('form[name=system_configuration_form_customer]')->form();
|
||||
$client->submit($form, [
|
||||
'system_configuration_form_form_customer' => [
|
||||
'system_configuration_form_customer' => [
|
||||
'configuration' => [
|
||||
['name' => 'defaults.customer.timezone', 'value' => 'Atlantic/Canary'],
|
||||
['name' => 'defaults.customer.country', 'value' => 'BB'],
|
||||
@@ -223,9 +221,9 @@ class SystemConfigurationControllerTest extends ControllerBaseTest
|
||||
$this->assertNull($configService->find('defaults.user.theme'));
|
||||
$this->assertEquals('en', $configService->find('defaults.user.language'));
|
||||
|
||||
$form = $client->getCrawler()->filter('form[name=system_configuration_form_form_user]')->form();
|
||||
$form = $client->getCrawler()->filter('form[name=system_configuration_form_user]')->form();
|
||||
$client->submit($form, [
|
||||
'system_configuration_form_form_user' => [
|
||||
'system_configuration_form_user' => [
|
||||
'configuration' => [
|
||||
['name' => 'defaults.user.timezone', 'value' => 'Pacific/Tahiti'],
|
||||
['name' => 'defaults.user.language', 'value' => 'ru'],
|
||||
@@ -250,9 +248,9 @@ class SystemConfigurationControllerTest extends ControllerBaseTest
|
||||
$this->assertFormHasValidationError(
|
||||
User::ROLE_SUPER_ADMIN,
|
||||
'/admin/system-config/',
|
||||
'form[name=system_configuration_form_form_customer]',
|
||||
'form[name=system_configuration_form_customer]',
|
||||
[
|
||||
'system_configuration_form_form_customer' => [
|
||||
'system_configuration_form_customer' => [
|
||||
'configuration' => [
|
||||
['name' => 'defaults.customer.timezone', 'value' => 'XX'],
|
||||
['name' => 'defaults.customer.country', 'value' => 1],
|
||||
@@ -261,9 +259,9 @@ class SystemConfigurationControllerTest extends ControllerBaseTest
|
||||
]
|
||||
],
|
||||
[
|
||||
'#system_configuration_form_form_customer_configuration_0_value',
|
||||
'#system_configuration_form_form_customer_configuration_1_value',
|
||||
'#system_configuration_form_form_customer_configuration_2_value',
|
||||
'#system_configuration_form_customer_configuration_0_value',
|
||||
'#system_configuration_form_customer_configuration_1_value',
|
||||
'#system_configuration_form_customer_configuration_2_value',
|
||||
],
|
||||
true
|
||||
);
|
||||
|
||||
@@ -427,7 +427,7 @@ final class TimesheetFixtures implements TestFixture
|
||||
}
|
||||
|
||||
if ($this->fixedRate) {
|
||||
$entry->setFixedRate(rand(10, 100));
|
||||
$entry->setFixedRate((float) rand(10, 100));
|
||||
}
|
||||
|
||||
if ($this->hourlyRate) {
|
||||
|
||||
@@ -140,6 +140,9 @@ class AppExtensionTest extends TestCase
|
||||
'templates/invoice/renderer/',
|
||||
],
|
||||
'kimai.defaults' => [
|
||||
'timesheet' => [
|
||||
'billable' => true,
|
||||
],
|
||||
'customer' => [
|
||||
'timezone' => null,
|
||||
'country' => 'DE',
|
||||
|
||||
@@ -395,6 +395,9 @@ class ConfigurationTest extends TestCase
|
||||
'dashboard' => [],
|
||||
'widgets' => [],
|
||||
'defaults' => [
|
||||
'timesheet' => [
|
||||
'billable' => true,
|
||||
],
|
||||
'customer' => [
|
||||
'timezone' => null,
|
||||
'country' => 'DE',
|
||||
|
||||
@@ -135,6 +135,9 @@ class ActivityTest extends AbstractEntityTest
|
||||
['label.id', 'integer'],
|
||||
['label.name', 'string'],
|
||||
['label.project', 'string'],
|
||||
['label.budget', 'float'],
|
||||
['label.timeBudget', 'duration'],
|
||||
['label.budgetType', 'string'],
|
||||
['label.color', 'string'],
|
||||
['label.visible', 'boolean'],
|
||||
['label.comment', 'string'],
|
||||
@@ -151,7 +154,7 @@ class ActivityTest extends AbstractEntityTest
|
||||
foreach ($expected as $item) {
|
||||
$column = $columns[$i++];
|
||||
self::assertEquals($item[0], $column->getLabel());
|
||||
self::assertEquals($item[1], $column->getType());
|
||||
self::assertEquals($item[1], $column->getType(), 'Wrong type for field: ' . $item[0]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -188,6 +188,9 @@ class CustomerTest extends AbstractEntityTest
|
||||
['label.country', 'string'],
|
||||
['label.currency', 'string'],
|
||||
['label.timezone', 'string'],
|
||||
['label.budget', 'float'],
|
||||
['label.timeBudget', 'duration'],
|
||||
['label.budgetType', 'string'],
|
||||
['label.color', 'string'],
|
||||
['label.visible', 'boolean'],
|
||||
['label.comment', 'string'],
|
||||
|
||||
@@ -163,6 +163,9 @@ class ProjectTest extends AbstractEntityTest
|
||||
['label.orderDate', 'datetime'],
|
||||
['label.project_start', 'datetime'],
|
||||
['label.project_end', 'datetime'],
|
||||
['label.budget', 'float'],
|
||||
['label.timeBudget', 'duration'],
|
||||
['label.budgetType', 'string'],
|
||||
['label.color', 'string'],
|
||||
['label.visible', 'boolean'],
|
||||
['label.comment', 'string'],
|
||||
@@ -247,5 +250,11 @@ class ProjectTest extends AbstractEntityTest
|
||||
self::assertTrue($sut->isVisibleAtDate($now));
|
||||
$sut->setEnd(new \DateTime('-1 hour'));
|
||||
self::assertFalse($sut->isVisibleAtDate($now));
|
||||
$sut->setEnd(new \DateTime('+1 hour'));
|
||||
self::assertTrue($sut->isVisibleAtDate($now));
|
||||
$sut->setStart(new \DateTime('-1 hour'));
|
||||
self::assertTrue($sut->isVisibleAtDate($now));
|
||||
$sut->setStart(new \DateTime('+1 hour'));
|
||||
self::assertFalse($sut->isVisibleAtDate($now));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
<?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\Tests\EventSubscriber;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Event\DashboardEvent;
|
||||
use App\EventSubscriber\DashboardSubscriber;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
|
||||
/**
|
||||
* @covers \App\EventSubscriber\DashboardSubscriber
|
||||
*/
|
||||
class DashboardSubscriberTest extends TestCase
|
||||
{
|
||||
public function testGetSubscribedEvents()
|
||||
{
|
||||
$events = DashboardSubscriber::getSubscribedEvents();
|
||||
$this->assertArrayHasKey(DashboardEvent::class, $events);
|
||||
$methodName = $events[DashboardEvent::class][0];
|
||||
$this->assertTrue(method_exists(DashboardSubscriber::class, $methodName));
|
||||
}
|
||||
|
||||
public function testWithNonAdminUser()
|
||||
{
|
||||
$sut = $this->getSubscriber(false, 13, 28, 37, 5);
|
||||
$event = new DashboardEvent(new User());
|
||||
|
||||
$this->assertEquals(0, \count($event->getSections()));
|
||||
$sut->onDashboardEvent($event);
|
||||
$this->assertEquals(0, \count($event->getSections()));
|
||||
}
|
||||
|
||||
public function testWithAdminUser()
|
||||
{
|
||||
$sut = $this->getSubscriber(true, 13, 28, 37, 5);
|
||||
$event = new DashboardEvent(new User());
|
||||
|
||||
$this->assertEquals(0, \count($event->getSections()));
|
||||
$sut->onDashboardEvent($event);
|
||||
|
||||
$sections = $event->getSections();
|
||||
$widgets = $sections[0]->getWidgets();
|
||||
|
||||
$this->assertEquals(1, \count($sections));
|
||||
$this->assertEquals(4, \count($widgets));
|
||||
|
||||
$this->assertEquals('stats.userTotal', $widgets[0]->getTitle());
|
||||
$this->assertEquals(13, $widgets[0]->getData());
|
||||
|
||||
$this->assertEquals('stats.customerTotal', $widgets[1]->getTitle());
|
||||
$this->assertEquals(5, $widgets[1]->getData());
|
||||
|
||||
$this->assertEquals('stats.projectTotal', $widgets[2]->getTitle());
|
||||
$this->assertEquals(37, $widgets[2]->getData());
|
||||
|
||||
$this->assertEquals('stats.activityTotal', $widgets[3]->getTitle());
|
||||
$this->assertEquals(28, $widgets[3]->getData());
|
||||
}
|
||||
|
||||
protected function getSubscriber(bool $isAdmin, int $userCount, int $activityCount, int $projectCount, int $customerCount)
|
||||
{
|
||||
$authMock = $this->getMockBuilder(AuthorizationCheckerInterface::class)->getMock();
|
||||
$authMock->method('isGranted')->willReturn($isAdmin);
|
||||
|
||||
$userMock = $this->getMockBuilder(UserRepository::class)->disableOriginalConstructor()->getMock();
|
||||
$userMock->method('countUsersForQuery')->willReturn($userCount);
|
||||
|
||||
$projectMock = $this->getMockBuilder(ProjectRepository::class)->disableOriginalConstructor()->getMock();
|
||||
$projectMock->method('countProjectsForQuery')->willReturn($projectCount);
|
||||
|
||||
$activityMock = $this->getMockBuilder(ActivityRepository::class)->disableOriginalConstructor()->getMock();
|
||||
$activityMock->method('countActivitiesForQuery')->willReturn($activityCount);
|
||||
|
||||
$customerMock = $this->getMockBuilder(CustomerRepository::class)->disableOriginalConstructor()->getMock();
|
||||
$customerMock->method('countCustomersForQuery')->willReturn($customerCount);
|
||||
|
||||
return new DashboardSubscriber($authMock, $userMock, $activityMock, $projectMock, $customerMock);
|
||||
}
|
||||
}
|
||||
@@ -10,20 +10,12 @@
|
||||
namespace App\Tests\EventSubscriber;
|
||||
|
||||
use App\Configuration\MailConfiguration;
|
||||
use App\Entity\User;
|
||||
use App\Event\DashboardEvent;
|
||||
use App\Event\EmailEvent;
|
||||
use App\EventSubscriber\DashboardSubscriber;
|
||||
use App\EventSubscriber\EmailSubscriber;
|
||||
use App\Mail\KimaiMailer;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
|
||||
/**
|
||||
* @covers \App\EventSubscriber\EmailSubscriber
|
||||
@@ -54,51 +46,4 @@ class EmailSubscriberTest extends TestCase
|
||||
|
||||
$sut->onMailEvent($event);
|
||||
}
|
||||
|
||||
public function testWithAdminUser()
|
||||
{
|
||||
$sut = $this->getSubscriber(true, 13, 28, 37, 5);
|
||||
$event = new DashboardEvent(new User());
|
||||
|
||||
$this->assertEquals(0, \count($event->getSections()));
|
||||
$sut->onDashboardEvent($event);
|
||||
|
||||
$sections = $event->getSections();
|
||||
$widgets = $sections[0]->getWidgets();
|
||||
|
||||
$this->assertEquals(1, \count($sections));
|
||||
$this->assertEquals(4, \count($widgets));
|
||||
|
||||
$this->assertEquals('stats.userTotal', $widgets[0]->getTitle());
|
||||
$this->assertEquals(13, $widgets[0]->getData());
|
||||
|
||||
$this->assertEquals('stats.customerTotal', $widgets[1]->getTitle());
|
||||
$this->assertEquals(5, $widgets[1]->getData());
|
||||
|
||||
$this->assertEquals('stats.projectTotal', $widgets[2]->getTitle());
|
||||
$this->assertEquals(37, $widgets[2]->getData());
|
||||
|
||||
$this->assertEquals('stats.activityTotal', $widgets[3]->getTitle());
|
||||
$this->assertEquals(28, $widgets[3]->getData());
|
||||
}
|
||||
|
||||
protected function getSubscriber(bool $isAdmin, int $userCount, int $activityCount, int $projectCount, int $customerCount)
|
||||
{
|
||||
$authMock = $this->getMockBuilder(AuthorizationCheckerInterface::class)->getMock();
|
||||
$authMock->method('isGranted')->willReturn($isAdmin);
|
||||
|
||||
$userMock = $this->getMockBuilder(UserRepository::class)->disableOriginalConstructor()->getMock();
|
||||
$userMock->method('countUsersForQuery')->willReturn($userCount);
|
||||
|
||||
$projectMock = $this->getMockBuilder(ProjectRepository::class)->disableOriginalConstructor()->getMock();
|
||||
$projectMock->method('countProjectsForQuery')->willReturn($projectCount);
|
||||
|
||||
$activityMock = $this->getMockBuilder(ActivityRepository::class)->disableOriginalConstructor()->getMock();
|
||||
$activityMock->method('countActivitiesForQuery')->willReturn($activityCount);
|
||||
|
||||
$customerMock = $this->getMockBuilder(CustomerRepository::class)->disableOriginalConstructor()->getMock();
|
||||
$customerMock->method('countCustomersForQuery')->willReturn($customerCount);
|
||||
|
||||
return new DashboardSubscriber($authMock, $userMock, $activityMock, $projectMock, $customerMock);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ class AnnotatedObjectExporterTest extends TestCase
|
||||
$project->setOrderNumber('1234567890');
|
||||
$project->setBudget(123456.7890);
|
||||
$project->setTimeBudget(1234567890);
|
||||
$project->setBudgetType();
|
||||
$project->setIsMonthlyBudget();
|
||||
$project->setColor('#ababab');
|
||||
$project->setVisible(false);
|
||||
|
||||
@@ -42,15 +44,19 @@ class AnnotatedObjectExporterTest extends TestCase
|
||||
$spreadsheet = $sut->export(Project::class, [$project]);
|
||||
$worksheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
self::assertNull($worksheet->getCellByColumnAndRow(1, 2)->getValue());
|
||||
self::assertEquals('test project', $worksheet->getCellByColumnAndRow(2, 2)->getValue());
|
||||
self::assertEquals('A customer', $worksheet->getCellByColumnAndRow(3, 2)->getValue());
|
||||
self::assertEquals(1234567890, $worksheet->getCellByColumnAndRow(4, 2)->getValue());
|
||||
self::assertEquals('', $worksheet->getCellByColumnAndRow(5, 2)->getValue());
|
||||
self::assertEquals('', $worksheet->getCellByColumnAndRow(6, 2)->getValue());
|
||||
self::assertEquals('', $worksheet->getCellByColumnAndRow(7, 2)->getValue());
|
||||
self::assertEquals('#ababab', $worksheet->getCellByColumnAndRow(8, 2)->getValue());
|
||||
self::assertFalse($worksheet->getCellByColumnAndRow(9, 2)->getValue());
|
||||
self::assertEquals('Lorem Ipsum', $worksheet->getCellByColumnAndRow(10, 2)->getValue());
|
||||
$i = 0;
|
||||
self::assertNull($worksheet->getCellByColumnAndRow(++$i, 2)->getValue());
|
||||
self::assertEquals('test project', $worksheet->getCellByColumnAndRow(++$i, 2)->getValue());
|
||||
self::assertEquals('A customer', $worksheet->getCellByColumnAndRow(++$i, 2)->getValue());
|
||||
self::assertEquals(1234567890, $worksheet->getCellByColumnAndRow(++$i, 2)->getValue());
|
||||
self::assertEquals('', $worksheet->getCellByColumnAndRow(++$i, 2)->getValue());
|
||||
self::assertEquals('', $worksheet->getCellByColumnAndRow(++$i, 2)->getValue());
|
||||
self::assertEquals('', $worksheet->getCellByColumnAndRow(++$i, 2)->getValue());
|
||||
self::assertEquals(123456.7890, $worksheet->getCellByColumnAndRow(++$i, 2)->getValue());
|
||||
self::assertEquals('=1234567890/86400', $worksheet->getCellByColumnAndRow(++$i, 2)->getValue());
|
||||
self::assertEquals('month', $worksheet->getCellByColumnAndRow(++$i, 2)->getValue());
|
||||
self::assertEquals('#ababab', $worksheet->getCellByColumnAndRow(++$i, 2)->getValue());
|
||||
self::assertFalse($worksheet->getCellByColumnAndRow(++$i, 2)->getValue());
|
||||
self::assertEquals('Lorem Ipsum', $worksheet->getCellByColumnAndRow(++$i, 2)->getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,17 +58,21 @@ class EntityWithMetaFieldsExporterTest extends TestCase
|
||||
$spreadsheet = $sut->export(Project::class, [$project], new ProjectMetaDisplayEvent(new ProjectQuery(), ProjectMetaDisplayEvent::EXPORT));
|
||||
$worksheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
self::assertNull($worksheet->getCellByColumnAndRow(1, 2)->getValue());
|
||||
self::assertEquals('test project', $worksheet->getCellByColumnAndRow(2, 2)->getValue());
|
||||
self::assertEquals('A customer', $worksheet->getCellByColumnAndRow(3, 2)->getValue());
|
||||
self::assertEquals(1234567890, $worksheet->getCellByColumnAndRow(4, 2)->getValue());
|
||||
self::assertEquals('', $worksheet->getCellByColumnAndRow(5, 2)->getValue());
|
||||
self::assertEquals('', $worksheet->getCellByColumnAndRow(6, 2)->getValue());
|
||||
self::assertEquals('', $worksheet->getCellByColumnAndRow(7, 2)->getValue());
|
||||
self::assertEquals('#ababab', $worksheet->getCellByColumnAndRow(8, 2)->getValue());
|
||||
self::assertFalse($worksheet->getCellByColumnAndRow(9, 2)->getValue());
|
||||
self::assertEquals('Lorem Ipsum', $worksheet->getCellByColumnAndRow(10, 2)->getValue());
|
||||
self::assertEquals('some magic', $worksheet->getCellByColumnAndRow(11, 2)->getValue());
|
||||
self::assertEquals('is happening', $worksheet->getCellByColumnAndRow(12, 2)->getValue());
|
||||
$i = 0;
|
||||
self::assertNull($worksheet->getCellByColumnAndRow(++$i, 2)->getValue());
|
||||
self::assertEquals('test project', $worksheet->getCellByColumnAndRow(++$i, 2)->getValue());
|
||||
self::assertEquals('A customer', $worksheet->getCellByColumnAndRow(++$i, 2)->getValue());
|
||||
self::assertEquals(1234567890, $worksheet->getCellByColumnAndRow(++$i, 2)->getValue());
|
||||
self::assertEquals('', $worksheet->getCellByColumnAndRow(++$i, 2)->getValue());
|
||||
self::assertEquals('', $worksheet->getCellByColumnAndRow(++$i, 2)->getValue());
|
||||
self::assertEquals('', $worksheet->getCellByColumnAndRow(++$i, 2)->getValue());
|
||||
self::assertEquals(123456.7890, $worksheet->getCellByColumnAndRow(++$i, 2)->getValue());
|
||||
self::assertEquals('=1234567890/86400', $worksheet->getCellByColumnAndRow(++$i, 2)->getValue());
|
||||
self::assertEquals('', $worksheet->getCellByColumnAndRow(++$i, 2)->getValue());
|
||||
self::assertEquals('#ababab', $worksheet->getCellByColumnAndRow(++$i, 2)->getValue());
|
||||
self::assertFalse($worksheet->getCellByColumnAndRow(++$i, 2)->getValue());
|
||||
self::assertEquals('Lorem Ipsum', $worksheet->getCellByColumnAndRow(++$i, 2)->getValue());
|
||||
self::assertEquals('some magic', $worksheet->getCellByColumnAndRow(++$i, 2)->getValue());
|
||||
self::assertEquals('is happening', $worksheet->getCellByColumnAndRow(++$i, 2)->getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ class TimesheetMultiUpdateDTOTest extends TestCase
|
||||
self::assertNull($sut->getAction());
|
||||
|
||||
self::assertNull($sut->isExported());
|
||||
self::assertNull($sut->isBillable());
|
||||
self::assertNull($sut->getProject());
|
||||
self::assertNull($sut->getAction());
|
||||
self::assertNull($sut->getCustomer());
|
||||
@@ -70,37 +71,44 @@ class TimesheetMultiUpdateDTOTest extends TestCase
|
||||
self::assertInstanceOf(TimesheetMultiUpdateDTO::class, $sut->setEntities($entities));
|
||||
self::assertEquals($entities, $sut->getEntities());
|
||||
|
||||
self::assertInstanceOf(TimesheetMultiUpdateDTO::class, $sut->setExported(true));
|
||||
self::assertNull($sut->isExported());
|
||||
$sut->setExported(true);
|
||||
self::assertTrue($sut->isExported());
|
||||
self::assertInstanceOf(TimesheetMultiUpdateDTO::class, $sut->setExported(false));
|
||||
$sut->setExported(false);
|
||||
self::assertFalse($sut->isExported());
|
||||
|
||||
self::assertInstanceOf(TimesheetMultiUpdateDTO::class, $sut->setTags(['foo', '0815']));
|
||||
self::assertNull($sut->isBillable());
|
||||
$sut->setBillable(true);
|
||||
self::assertTrue($sut->isBillable());
|
||||
$sut->setExported(false);
|
||||
self::assertFalse($sut->isExported());
|
||||
|
||||
$sut->setTags(['foo', '0815']);
|
||||
self::assertEquals(['foo', '0815'], $sut->getTags());
|
||||
|
||||
self::assertInstanceOf(TimesheetMultiUpdateDTO::class, $sut->setReplaceTags(true));
|
||||
$sut->setReplaceTags(true);
|
||||
self::assertTrue($sut->isReplaceTags());
|
||||
|
||||
$user = (new User())->setUsername('sdfsdfsd');
|
||||
self::assertInstanceOf(TimesheetMultiUpdateDTO::class, $sut->setUser($user));
|
||||
$sut->setUser($user);
|
||||
self::assertSame($user, $sut->getUser());
|
||||
|
||||
$activity = (new Activity())->setName('sdfsdfsd');
|
||||
self::assertInstanceOf(TimesheetMultiUpdateDTO::class, $sut->setActivity($activity));
|
||||
$sut->setActivity($activity);
|
||||
self::assertSame($activity, $sut->getActivity());
|
||||
|
||||
$project = (new Project())->setName('sdfsdfsd');
|
||||
self::assertInstanceOf(TimesheetMultiUpdateDTO::class, $sut->setProject($project));
|
||||
$sut->setProject($project);
|
||||
self::assertSame($project, $sut->getProject());
|
||||
|
||||
$customer = (new Customer())->setName('sdfsdfsd');
|
||||
self::assertInstanceOf(TimesheetMultiUpdateDTO::class, $sut->setCustomer($customer));
|
||||
$sut->setCustomer($customer);
|
||||
self::assertSame($customer, $sut->getCustomer());
|
||||
|
||||
self::assertInstanceOf(TimesheetMultiUpdateDTO::class, $sut->setFixedRate(12.78));
|
||||
$sut->setFixedRate(12.78);
|
||||
self::assertEquals(12.78, $sut->getFixedRate());
|
||||
|
||||
self::assertInstanceOf(TimesheetMultiUpdateDTO::class, $sut->setHourlyRate(123.45));
|
||||
$sut->setHourlyRate(123.45);
|
||||
self::assertEquals(123.45, $sut->getHourlyRate());
|
||||
|
||||
self::assertInstanceOf(TimesheetMultiUpdateDTO::class, $sut->setUpdateMeta(['foo', 'bar']));
|
||||
|
||||
48
tests/Utils/CommandStyleTest.php
Normal file
48
tests/Utils/CommandStyleTest.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?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\Tests\Utils;
|
||||
|
||||
use App\Utils\CommandStyle;
|
||||
use App\Validator\ValidationFailedException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Output\BufferedOutput;
|
||||
use Symfony\Component\Validator\ConstraintViolation;
|
||||
use Symfony\Component\Validator\ConstraintViolationList;
|
||||
|
||||
/**
|
||||
* @covers \App\Utils\CommandStyle
|
||||
*/
|
||||
class CommandStyleTest extends TestCase
|
||||
{
|
||||
public function testStyles()
|
||||
{
|
||||
$input = new ArrayInput([]);
|
||||
$output = new BufferedOutput();
|
||||
|
||||
$sut = new CommandStyle($input, $output);
|
||||
$sut->error('FooBar-Error');
|
||||
$sut->warning('Hello-Warning');
|
||||
$sut->success('World-Success');
|
||||
|
||||
$exception = new ValidationFailedException(new ConstraintViolationList([
|
||||
new ConstraintViolation('123', null, [], $this, null, 123),
|
||||
new ConstraintViolation('456', null, [], $this, null, 456),
|
||||
]));
|
||||
$sut->validationError($exception);
|
||||
|
||||
$result = $output->fetch();
|
||||
self::assertStringContainsString('[ERROR] FooBar-Error', $result);
|
||||
self::assertStringContainsString('[WARNING] Hello-Warning', $result);
|
||||
self::assertStringContainsString('[OK] World-Success', $result);
|
||||
self::assertStringContainsString('[ERROR] (123)', $result);
|
||||
self::assertStringContainsString('[ERROR] (456)', $result);
|
||||
}
|
||||
}
|
||||
@@ -44,10 +44,8 @@ class TimesheetMultiUpdateValidatorTest extends ConstraintValidatorTestCase
|
||||
$activity->setProject($project1);
|
||||
|
||||
$timesheet = new TimesheetMultiUpdateDTO();
|
||||
$timesheet
|
||||
->setActivity($activity)
|
||||
->setProject($project2)
|
||||
;
|
||||
$timesheet->setActivity($activity);
|
||||
$timesheet->setProject($project2);
|
||||
|
||||
$this->validator->validate($timesheet, new TimesheetMultiUpdateConstraint(['message' => 'myMessage']));
|
||||
|
||||
@@ -90,10 +88,8 @@ class TimesheetMultiUpdateValidatorTest extends ConstraintValidatorTestCase
|
||||
public function testHourlyRateAndFixedRateInParallelAreNotAllowed()
|
||||
{
|
||||
$timesheet = new TimesheetMultiUpdateDTO();
|
||||
$timesheet
|
||||
->setHourlyRate(10.12)
|
||||
->setFixedRate(123.45)
|
||||
;
|
||||
$timesheet->setHourlyRate(10.12);
|
||||
$timesheet->setFixedRate(123.45);
|
||||
|
||||
$this->validator->validate($timesheet, new TimesheetMultiUpdateConstraint(['message' => 'myMessage']));
|
||||
|
||||
@@ -118,10 +114,8 @@ class TimesheetMultiUpdateValidatorTest extends ConstraintValidatorTestCase
|
||||
$activity->setProject($project);
|
||||
|
||||
$timesheet = new TimesheetMultiUpdateDTO();
|
||||
$timesheet
|
||||
->setActivity($activity)
|
||||
->setProject($project)
|
||||
;
|
||||
$timesheet->setActivity($activity);
|
||||
$timesheet->setProject($project);
|
||||
|
||||
$this->validator->validate($timesheet, new TimesheetMultiUpdateConstraint(['message' => 'myMessage']));
|
||||
|
||||
|
||||
@@ -21,12 +21,17 @@ abstract class AbstractWidgetTypeTest extends TestCase
|
||||
|
||||
abstract public function getDefaultOptions(): array;
|
||||
|
||||
public function testDefaultValues()
|
||||
protected function assertDefaultData(AbstractWidgetType $sut)
|
||||
{
|
||||
self::assertNull($sut->getData());
|
||||
}
|
||||
|
||||
public function testDefaultData()
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
self::assertInstanceOf(AbstractWidgetType::class, $sut);
|
||||
self::assertEquals($this->getDefaultOptions(), $sut->getOptions());
|
||||
self::assertNull($sut->getData());
|
||||
$this->assertDefaultData($sut);
|
||||
self::assertEquals('bar', $sut->getOption('foo', 'bar'));
|
||||
}
|
||||
|
||||
|
||||
79
tests/Widget/Type/TotalsActivityTest.php
Normal file
79
tests/Widget/Type/TotalsActivityTest.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?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\Tests\Widget\Type;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Widget\Type\AbstractWidgetType;
|
||||
use App\Widget\Type\TotalsActivity;
|
||||
|
||||
/**
|
||||
* @covers \App\Widget\Type\TotalsActivity
|
||||
* @covers \App\Widget\Type\SimpleWidget
|
||||
*/
|
||||
class TotalsActivityTest extends AbstractWidgetTypeTest
|
||||
{
|
||||
/** @var User */
|
||||
private $user;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$user = new User();
|
||||
$user->setAlias('foo');
|
||||
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function createSut(): AbstractWidgetType
|
||||
{
|
||||
return $this->createWidget();
|
||||
}
|
||||
|
||||
private function createWidget(int $results = 1): TotalsActivity
|
||||
{
|
||||
$repository = $this->createMock(ActivityRepository::class);
|
||||
$repository->expects($this->any())->method('countActivitiesForQuery')->willReturn($results);
|
||||
|
||||
$widget = new TotalsActivity($repository);
|
||||
$widget->setUser($this->user);
|
||||
|
||||
return $widget;
|
||||
}
|
||||
|
||||
public function getDefaultOptions(): array
|
||||
{
|
||||
return [
|
||||
'route' => 'admin_activity',
|
||||
'icon' => 'activity',
|
||||
'color' => 'primary',
|
||||
'dataType' => 'int',
|
||||
'user' => $this->user,
|
||||
];
|
||||
}
|
||||
|
||||
protected function assertDefaultData(AbstractWidgetType $sut)
|
||||
{
|
||||
self::assertEquals(1, $sut->getData());
|
||||
}
|
||||
|
||||
public function testData()
|
||||
{
|
||||
$user = new User();
|
||||
$user->setAlias('foo');
|
||||
|
||||
$sut = $this->createWidget(99);
|
||||
self::assertEquals('widget/widget-more.html.twig', $sut->getTemplateName());
|
||||
$sut->setUser($user);
|
||||
|
||||
self::assertEquals(['view_activity', 'view_teamlead_activity', 'view_team_activity'], $sut->getPermissions());
|
||||
self::assertEquals(99, $sut->getData([]));
|
||||
}
|
||||
}
|
||||
79
tests/Widget/Type/TotalsCustomerTest.php
Normal file
79
tests/Widget/Type/TotalsCustomerTest.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?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\Tests\Widget\Type;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Widget\Type\AbstractWidgetType;
|
||||
use App\Widget\Type\TotalsCustomer;
|
||||
|
||||
/**
|
||||
* @covers \App\Widget\Type\TotalsCustomer
|
||||
* @covers \App\Widget\Type\SimpleWidget
|
||||
*/
|
||||
class TotalsCustomerTest extends AbstractWidgetTypeTest
|
||||
{
|
||||
/** @var User */
|
||||
private $user;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$user = new User();
|
||||
$user->setAlias('foo');
|
||||
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function createSut(): AbstractWidgetType
|
||||
{
|
||||
return $this->createWidget();
|
||||
}
|
||||
|
||||
private function createWidget(int $results = 1): TotalsCustomer
|
||||
{
|
||||
$repository = $this->createMock(CustomerRepository::class);
|
||||
$repository->expects($this->any())->method('countCustomersForQuery')->willReturn($results);
|
||||
|
||||
$widget = new TotalsCustomer($repository);
|
||||
$widget->setUser($this->user);
|
||||
|
||||
return $widget;
|
||||
}
|
||||
|
||||
public function getDefaultOptions(): array
|
||||
{
|
||||
return [
|
||||
'route' => 'admin_customer',
|
||||
'icon' => 'customer',
|
||||
'color' => 'primary',
|
||||
'dataType' => 'int',
|
||||
'user' => $this->user,
|
||||
];
|
||||
}
|
||||
|
||||
protected function assertDefaultData(AbstractWidgetType $sut)
|
||||
{
|
||||
self::assertEquals(1, $sut->getData());
|
||||
}
|
||||
|
||||
public function testData()
|
||||
{
|
||||
$user = new User();
|
||||
$user->setAlias('foo');
|
||||
|
||||
$sut = $this->createWidget(99);
|
||||
self::assertEquals('widget/widget-more.html.twig', $sut->getTemplateName());
|
||||
$sut->setUser($user);
|
||||
|
||||
self::assertEquals(['view_customer', 'view_teamlead_customer', 'view_team_customer'], $sut->getPermissions());
|
||||
self::assertEquals(99, $sut->getData([]));
|
||||
}
|
||||
}
|
||||
79
tests/Widget/Type/TotalsProjectTest.php
Normal file
79
tests/Widget/Type/TotalsProjectTest.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?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\Tests\Widget\Type;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Widget\Type\AbstractWidgetType;
|
||||
use App\Widget\Type\TotalsProject;
|
||||
|
||||
/**
|
||||
* @covers \App\Widget\Type\TotalsProject
|
||||
* @covers \App\Widget\Type\SimpleWidget
|
||||
*/
|
||||
class TotalsProjectTest extends AbstractWidgetTypeTest
|
||||
{
|
||||
/** @var User */
|
||||
private $user;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$user = new User();
|
||||
$user->setAlias('foo');
|
||||
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function createSut(): AbstractWidgetType
|
||||
{
|
||||
return $this->createWidget();
|
||||
}
|
||||
|
||||
private function createWidget(int $results = 1): TotalsProject
|
||||
{
|
||||
$repository = $this->createMock(ProjectRepository::class);
|
||||
$repository->expects($this->any())->method('countProjectsForQuery')->willReturn($results);
|
||||
|
||||
$widget = new TotalsProject($repository);
|
||||
$widget->setUser($this->user);
|
||||
|
||||
return $widget;
|
||||
}
|
||||
|
||||
public function getDefaultOptions(): array
|
||||
{
|
||||
return [
|
||||
'route' => 'admin_project',
|
||||
'icon' => 'project',
|
||||
'color' => 'primary',
|
||||
'dataType' => 'int',
|
||||
'user' => $this->user,
|
||||
];
|
||||
}
|
||||
|
||||
protected function assertDefaultData(AbstractWidgetType $sut)
|
||||
{
|
||||
self::assertEquals(1, $sut->getData());
|
||||
}
|
||||
|
||||
public function testData()
|
||||
{
|
||||
$user = new User();
|
||||
$user->setAlias('foo');
|
||||
|
||||
$sut = $this->createWidget(99);
|
||||
self::assertEquals('widget/widget-more.html.twig', $sut->getTemplateName());
|
||||
$sut->setUser($user);
|
||||
|
||||
self::assertEquals(['view_project', 'view_teamlead_project', 'view_team_project'], $sut->getPermissions());
|
||||
self::assertEquals(99, $sut->getData([]));
|
||||
}
|
||||
}
|
||||
79
tests/Widget/Type/TotalsUserTest.php
Normal file
79
tests/Widget/Type/TotalsUserTest.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?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\Tests\Widget\Type;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Widget\Type\AbstractWidgetType;
|
||||
use App\Widget\Type\TotalsUser;
|
||||
|
||||
/**
|
||||
* @covers \App\Widget\Type\TotalsUser
|
||||
* @covers \App\Widget\Type\SimpleWidget
|
||||
*/
|
||||
class TotalsUserTest extends AbstractWidgetTypeTest
|
||||
{
|
||||
/** @var User */
|
||||
private $user;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$user = new User();
|
||||
$user->setAlias('foo');
|
||||
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function createSut(): AbstractWidgetType
|
||||
{
|
||||
return $this->createWidget();
|
||||
}
|
||||
|
||||
private function createWidget(int $results = 1): TotalsUser
|
||||
{
|
||||
$repository = $this->createMock(UserRepository::class);
|
||||
$repository->expects($this->any())->method('countUsersForQuery')->willReturn($results);
|
||||
|
||||
$widget = new TotalsUser($repository);
|
||||
$widget->setUser($this->user);
|
||||
|
||||
return $widget;
|
||||
}
|
||||
|
||||
public function getDefaultOptions(): array
|
||||
{
|
||||
return [
|
||||
'route' => 'admin_user',
|
||||
'icon' => 'user',
|
||||
'color' => 'primary',
|
||||
'dataType' => 'int',
|
||||
'user' => $this->user,
|
||||
];
|
||||
}
|
||||
|
||||
protected function assertDefaultData(AbstractWidgetType $sut)
|
||||
{
|
||||
self::assertEquals(1, $sut->getData());
|
||||
}
|
||||
|
||||
public function testData()
|
||||
{
|
||||
$user = new User();
|
||||
$user->setAlias('foo');
|
||||
|
||||
$sut = $this->createWidget(99);
|
||||
self::assertEquals('widget/widget-more.html.twig', $sut->getTemplateName());
|
||||
$sut->setUser($user);
|
||||
|
||||
self::assertEquals(['view_user'], $sut->getPermissions());
|
||||
self::assertEquals(99, $sut->getData([]));
|
||||
}
|
||||
}
|
||||
@@ -28,11 +28,11 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="special_thanks">
|
||||
<source>special_thanks</source>
|
||||
<target>Ιδιαίτερες ευχαριστίες πηγαίνουν σε …</target>
|
||||
<target>Ιδιαίτερες ευχαριστίες πηγαίνουν …</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="library_authors">
|
||||
<source>library_authors</source>
|
||||
<target>… στους συγγραφείς των παρακάτω βιβλιοθηκών-λογισμικού, το Kimai δεν θα ήταν πραγματοποιήσιμο χωρίς εσάς 👍</target>
|
||||
<target>… στους συγγραφείς των παρακάτω βιβλιοθηκών-λογισμικού, δεν θα ήταν πραγματοποιήσιμο το Kimai χωρίς εσάς 👍</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
27
translations/about.fa.xlf
Normal file
27
translations/about.fa.xlf
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="fa" datatype="plaintext" original="about.en.xlf">
|
||||
<body>
|
||||
<trans-unit id="about.title">
|
||||
<source>about.title</source>
|
||||
<target>درباره ی کیمیایْ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="support">
|
||||
<source>support</source>
|
||||
<target>پشتیبانی</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="website">
|
||||
<source>website</source>
|
||||
<target>خانه</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="help">
|
||||
<source>help</source>
|
||||
<target>مستندات</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="special_thanks">
|
||||
<source>special_thanks</source>
|
||||
<target>سپاس ویژه از…</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
39
translations/about.hr.xlf
Normal file
39
translations/about.hr.xlf
Normal file
@@ -0,0 +1,39 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="hr" datatype="plaintext" original="about.en.xlf">
|
||||
<body>
|
||||
<trans-unit id="about.title">
|
||||
<source>about.title</source>
|
||||
<target>Kimai informacije</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="support">
|
||||
<source>support</source>
|
||||
<target>Podrška</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="website">
|
||||
<source>website</source>
|
||||
<target>Web-stranica</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="help">
|
||||
<source>help</source>
|
||||
<target>Dokumentacija</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="donate">
|
||||
<source>donate</source>
|
||||
<target>Doniraj za budućnost Kimaija</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="published_under">
|
||||
<source>published_under</source>
|
||||
<target>%kimai% je izdan pod</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="special_thanks">
|
||||
<source>special_thanks</source>
|
||||
<target>Posebno zahvaljujemo …</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="library_authors">
|
||||
<source>library_authors</source>
|
||||
<target>… autorima sljedećih softverskih biblioteka. Kimai ne bi bio moguć bez vas 👍</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
103
translations/actions.hr.xlf
Normal file
103
translations/actions.hr.xlf
Normal file
@@ -0,0 +1,103 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="hr" datatype="plaintext" original="actions.en.xlf">
|
||||
<body>
|
||||
<trans-unit id="stop">
|
||||
<source>stop</source>
|
||||
<target>Prekini</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="back">
|
||||
<source>back</source>
|
||||
<target>Natrag</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="edit">
|
||||
<source>edit</source>
|
||||
<target>Uredi</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="trash">
|
||||
<source>trash</source>
|
||||
<target>Izbriži</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="report">
|
||||
<source>report</source>
|
||||
<target>Izvještaj</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="repeat">
|
||||
<source>repeat</source>
|
||||
<target>Počni iznova</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="profile-stats">
|
||||
<source>profile-stats</source>
|
||||
<target>Profil</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="settings">
|
||||
<source>settings</source>
|
||||
<target>Postavke</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="timesheet">
|
||||
<source>timesheet</source>
|
||||
<target>Vremenske tablice</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="customer">
|
||||
<source>customer</source>
|
||||
<target>Filtriraj kupce</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="project">
|
||||
<source>project</source>
|
||||
<target>Filtriraj projekte</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="activity">
|
||||
<source>activity</source>
|
||||
<target>Filtriraj aktivnosti</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="copy">
|
||||
<source>copy</source>
|
||||
<target>Stvori kopiju</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="home">
|
||||
<source>home</source>
|
||||
<target>Web-stranica</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="create-project">
|
||||
<source>create-project</source>
|
||||
<target>Stvori projekt</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="create-activity">
|
||||
<source>create-activity</source>
|
||||
<target>Stvori aktivnost</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="create-timesheet">
|
||||
<source>create-timesheet</source>
|
||||
<target>Stvori vremensku tablicu</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="create-timesheet-multiuser">
|
||||
<source>create-timesheet-multiuser</source>
|
||||
<target>Za više korisnika</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="permissions">
|
||||
<source>permissions</source>
|
||||
<target>Timske dozvole</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="details">
|
||||
<source>details</source>
|
||||
<target>Pokaži</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="download">
|
||||
<source>download</source>
|
||||
<target>Preuzmi</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.pending">
|
||||
<source>invoice.pending</source>
|
||||
<target>Čeka na uplatu</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.paid">
|
||||
<source>invoice.paid</source>
|
||||
<target>Račun plaćen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="filter">
|
||||
<source>filter</source>
|
||||
<target>Filtriraj podatke</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -94,6 +94,10 @@
|
||||
<source>create-timesheet-multiuser</source>
|
||||
<target>Többszörös felhasználóknak</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="filter">
|
||||
<source>filter</source>
|
||||
<target>Adatok szűrése</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
43
translations/daterangepicker.hr.xlf
Normal file
43
translations/daterangepicker.hr.xlf
Normal file
@@ -0,0 +1,43 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="hr" datatype="plaintext" original="daterangepicker.en.xlf">
|
||||
<body>
|
||||
<trans-unit id="daterangepicker.today">
|
||||
<source>daterangepicker.today</source>
|
||||
<target>Danas</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="daterangepicker.yesterday">
|
||||
<source>daterangepicker.yesterday</source>
|
||||
<target>Jučer</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="daterangepicker.lastWeek">
|
||||
<source>daterangepicker.lastWeek</source>
|
||||
<target>Prošli tjedan</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="daterangepicker.thisWeek">
|
||||
<source>daterangepicker.thisWeek</source>
|
||||
<target>Ovaj tjedan</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="daterangepicker.lastMonth">
|
||||
<source>daterangepicker.lastMonth</source>
|
||||
<target>Prošli mjesec</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="daterangepicker.thisMonth">
|
||||
<source>daterangepicker.thisMonth</source>
|
||||
<target>Ovaj mjesec</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="daterangepicker.lastYear">
|
||||
<source>daterangepicker.lastYear</source>
|
||||
<target>Prošla godina</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="daterangepicker.thisYear">
|
||||
<source>daterangepicker.thisYear</source>
|
||||
<target>Ova godina</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="daterangepicker.customRange">
|
||||
<source>daterangepicker.customRange</source>
|
||||
<target>Prilagođeni raspon</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
47
translations/email.hr.xlf
Normal file
47
translations/email.hr.xlf
Normal file
@@ -0,0 +1,47 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="hr" datatype="plaintext" original="email.en.xlf">
|
||||
<body>
|
||||
<trans-unit id="button_copy_url_to_browser">
|
||||
<source>button_copy_url_to_browser</source>
|
||||
<target>Ako gumb ne radi, kopiraj i umetni sljedeći URL u preglednik: %url%</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="automated_email_dont_answer">
|
||||
<source>automated_email_dont_answer</source>
|
||||
<target>Ovo je automatski generirani e-mail. Nemoj odgovarati, adresa se neće čitati.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="registration.subject">
|
||||
<source>registration.subject</source>
|
||||
<target>Aktiviraj svoj račun</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="registration.title">
|
||||
<source>registration.title</source>
|
||||
<target>Zdravo %username%</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="registration.button">
|
||||
<source>registration.button</source>
|
||||
<target>Aktiviraj svoj račun</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="registration.intro">
|
||||
<source>registration.intro</source>
|
||||
<target>Prijavio/prijavila si se u Kimai evidenciju vremena pomoću e-mail adrese %email%. Aktiviraj svoj račun u sljedećih nekoliko sati, prije nego što poveznica istekne.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="reset.subject">
|
||||
<source>reset.subject</source>
|
||||
<target>Obnovi lozinku</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="reset.title">
|
||||
<source>reset.title</source>
|
||||
<target>To se može dogoditi …</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="reset.button">
|
||||
<source>reset.button</source>
|
||||
<target>Obnovi lozinku</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="reset.intro">
|
||||
<source>reset.intro</source>
|
||||
<target>… ne sjećaš se više kako glasi lozinka? Ne brini: Kimai će ti pomoći stvoriti novu:</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
43
translations/exceptions.hr.xlf
Normal file
43
translations/exceptions.hr.xlf
Normal file
@@ -0,0 +1,43 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="hr" datatype="plaintext" original="exceptions.en.xlf">
|
||||
<body>
|
||||
<trans-unit id="access.denied">
|
||||
<source>access.denied</source>
|
||||
<target>Pristup je odbijen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="stacktrace">
|
||||
<source>stacktrace</source>
|
||||
<target>Problem se pojavio ovdje</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="http_error.title">
|
||||
<source>http_error.title</source>
|
||||
<target>Greška</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="http_error.description">
|
||||
<source>http_error.description</source>
|
||||
<target>Nešto nije u redu</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="http_error.suggestion">
|
||||
<source>http_error.suggestion</source>
|
||||
<target>Dogodila se kritična greška, pokušaj ponovo. Možda se radi o softverskom problemu, obrati se administratoru ako se problem nastavi pojavljivati.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="http_error_404.description">
|
||||
<source>http_error_404.description</source>
|
||||
<target>Stranica nije pronađena</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="http_error_404.suggestion">
|
||||
<source>http_error_404.suggestion</source>
|
||||
<target>Nismo mogli pronaći stranicu koju tražiš. Vrati se na preglednu ploču i počni ispočetka.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="http_error_403.description">
|
||||
<source>http_error_403.description</source>
|
||||
<target>Stranica je zaštićena</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="http_error_403.suggestion">
|
||||
<source>http_error_403.suggestion</source>
|
||||
<target>Nažalost, nemaš dozvole za prikaz ove stranice. Obrati se administratoru ako misliš da je ovo greška.</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
19
translations/export.hr.xlf
Normal file
19
translations/export.hr.xlf
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="hr" datatype="plaintext" original="export.en.xlf">
|
||||
<body>
|
||||
<trans-unit id="default.pdf.twig">
|
||||
<source>default.pdf.twig</source>
|
||||
<target>Standardno</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="default-budget.pdf.twig">
|
||||
<source>default-budget.pdf.twig</source>
|
||||
<target>S preostalim budžetom</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="default-internal.pdf.twig">
|
||||
<source>default-internal.pdf.twig</source>
|
||||
<target>S internim cijenama</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
59
translations/flashmessages.hr.xlf
Normal file
59
translations/flashmessages.hr.xlf
Normal file
@@ -0,0 +1,59 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="hr" datatype="plaintext" original="flashmessages.en.xlf">
|
||||
<body>
|
||||
<trans-unit id="warning">
|
||||
<source>warning</source>
|
||||
<target>Upozorenje</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="timesheet.stop.success">
|
||||
<source>timesheet.stop.success</source>
|
||||
<target>Snimanje vremena je prekinuto</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="timesheet.stop.error">
|
||||
<source>timesheet.stop.error</source>
|
||||
<target>Snimanje vremena se nije moglo prekinuti</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="timesheet.start.success">
|
||||
<source>timesheet.start.success</source>
|
||||
<target>Snimanja vremena je pokrenuto</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="timesheet.start.error">
|
||||
<source>timesheet.start.error</source>
|
||||
<target>Snimanje vremena se nije moglo pokrenuti</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="timesheet.start.exceeded_limit">
|
||||
<source>timesheet.start.exceeded_limit</source>
|
||||
<target>Dosegnut je maksimalni broj aktivnih snimanja vremena. Najprije prekini barem jedno mjerenje vremena.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="timesheet.locked.warning">
|
||||
<source>timesheet.locked.warning</source>
|
||||
<target>Uređuješ izvezeni zapis</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="action.update.success">
|
||||
<source>action.update.success</source>
|
||||
<target>Promjene spremljene</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="action.update.error">
|
||||
<source>action.update.error</source>
|
||||
<target>Promjene se nisu mogle spremiti: %reason%</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="action.delete.success">
|
||||
<source>action.delete.success</source>
|
||||
<target>Unos je izbrisan</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="action.delete.error">
|
||||
<source>action.delete.error</source>
|
||||
<target>Unos se nisu mogao izbrisati: %reason%</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.first_template">
|
||||
<source>invoice.first_template</source>
|
||||
<target>Najprije stvori predložak računa</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="action.upload.error">
|
||||
<source>action.upload.error</source>
|
||||
<target>Datoteka se nije mogla prenijeti ili spremiti: %reason%</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -781,10 +781,6 @@
|
||||
<source>export.title</source>
|
||||
<target>Export</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="export.subtitle">
|
||||
<source>export.subtitle</source>
|
||||
<target>Exportovat odpracovanou dobu do různých formátů</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="export.period">
|
||||
<source>export.period</source>
|
||||
<target>Období</target>
|
||||
|
||||
@@ -865,10 +865,6 @@
|
||||
<source>export.title</source>
|
||||
<target>Eksporter</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="export.subtitle">
|
||||
<source>export.subtitle</source>
|
||||
<target>Eksporter timeseddeldata til diverse formater</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="export.filter">
|
||||
<source>export.filter</source>
|
||||
<target>Filtrer data til eksport</target>
|
||||
|
||||
@@ -725,7 +725,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="label.avatar">
|
||||
<source>label.avatar</source>
|
||||
<target>Profilbild</target>
|
||||
<target>Profilbild (URL)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.active">
|
||||
<source>label.active</source>
|
||||
@@ -1090,10 +1090,6 @@
|
||||
<source>export.title</source>
|
||||
<target>Export</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="export.subtitle">
|
||||
<source>export.subtitle</source>
|
||||
<target>Exportieren Sie aufgezeichnete Zeiten in verschiedene Zielformate</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="export.filter">
|
||||
<source>export.filter</source>
|
||||
<target>Daten für Export filtern</target>
|
||||
@@ -1256,6 +1252,10 @@
|
||||
<source>team.add_user.help</source>
|
||||
<target>Durch Auswahl wird dem Team ein neuer Benutzer hinzugefügt. Sie können im Anschluss festlegen, ob der Benutzer ein Teamleiter sein soll.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="default_value_new">
|
||||
<source>default_value_new</source>
|
||||
<target>Standard Wert für neue Einträge</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -145,10 +145,6 @@
|
||||
<source>export.filter</source>
|
||||
<target>Daten für Export filtern</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="export.subtitle">
|
||||
<source>export.subtitle</source>
|
||||
<target>Exportieren Sie aufgezeichnete Zeiten in verschiedene Zielformate</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="export.title">
|
||||
<source>export.title</source>
|
||||
<target>Export</target>
|
||||
@@ -1131,12 +1127,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="registration.check_email">
|
||||
<source>registration.check_email</source>
|
||||
<target>Eine E-Mail wurde an %email% gesendet. Sie enthält einen Link, den Sie anklicken müssen, um Ihr Benutzerkonto zu bestätigen.</target>
|
||||
<target>Eine E-Mail wurde an %email% gesendet. Diese enthält einen Link, den Sie anklicken müssen, um Ihr Benutzerkonto zu bestätigen.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="resetting.check_email">
|
||||
<source>resetting.check_email</source>
|
||||
<target>
|
||||
Eine E-Mail wurde verschickt. Sie beinhaltet einen Link zum Zurücksetzen des Passwortes.
|
||||
Eine E-Mail wurde verschickt. Diese beinhaltet einen Link zum Zurücksetzen des Passwortes.
|
||||
Hinweis: Ein neues Passwort kann nur alle %tokenLifetime% Stunden beantragt werden.
|
||||
|
||||
Eventuell wurde diese E-Mail als Spam markiert, wenn sie nicht angekommen ist.
|
||||
@@ -1166,6 +1162,26 @@
|
||||
<source>delete_warning.short_stats</source>
|
||||
<target>Momentan existieren insgesamt %records% Zeiteinträge, welche sich auf eine Gesamtdauer von %duration% belaufen.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.type">
|
||||
<source>label.type</source>
|
||||
<target>Typ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="export.warn_result_amount">
|
||||
<source>export.warn_result_amount</source>
|
||||
<target>Ihre Suche führt zu %count% Ergebnissen. Sollte der Export fehlschlagen, müssen Sie die Suche weiter eingrenzen.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.account_number">
|
||||
<source>label.account_number</source>
|
||||
<target>Personalnummer</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="add_user.label">
|
||||
<source>add_user.label</source>
|
||||
<target>Benutzer hinzufügen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="team.add_user.help">
|
||||
<source>team.add_user.help</source>
|
||||
<target>Durch Auswahl wird dem Team ein neuer Benutzer hinzugefügt. Sie können im Anschluss festlegen, ob der Benutzer ein Teamleiter sein soll.</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -440,7 +440,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="label.my_team_projects">
|
||||
<source>label.my_team_projects</source>
|
||||
<target>Ομαδικές Εργασίες & προϋπολογισμοί</target>
|
||||
<target>Τα έργα μου</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.progress">
|
||||
<source>label.progress</source>
|
||||
@@ -1038,10 +1038,6 @@
|
||||
<source>export.title</source>
|
||||
<target>Εξαγωγή</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="export.subtitle">
|
||||
<source>export.subtitle</source>
|
||||
<target>Εξαγωγή ωρολογίου προγράμματος σε διάφορες μορφές</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="export.filter">
|
||||
<source>export.filter</source>
|
||||
<target>Φίλτρα για τα δεδομένα προς εξαγωγή</target>
|
||||
@@ -1182,7 +1178,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="stats.durationFinancialYear">
|
||||
<source>stats.durationFinancialYear</source>
|
||||
<target>Ωράριο εργασίας κατά το τρέχον οικονομικό έτος</target>
|
||||
<target>Ώρες εργασίας κατά το τρέχον οικονομικό έτος</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="stats.revenue">
|
||||
<source>stats.revenue</source>
|
||||
@@ -1244,6 +1240,10 @@
|
||||
<source>export.warn_result_amount</source>
|
||||
<target>Η αναζήτηση σας αντιστοιχεί σε %count% αποτελέσματα. Εάν η εξαγωγή αποτυγχάνει, πρέπει να ορίσετε συγκεκριμένα κριτήρια για την αναζήτησή σας.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.type">
|
||||
<source>label.type</source>
|
||||
<target>Τύπος</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -725,7 +725,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="label.avatar">
|
||||
<source>label.avatar</source>
|
||||
<target>Profile image</target>
|
||||
<target>Profile image (URL)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.active">
|
||||
<source>label.active</source>
|
||||
@@ -1090,10 +1090,6 @@
|
||||
<source>export.title</source>
|
||||
<target>Export</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="export.subtitle">
|
||||
<source>export.subtitle</source>
|
||||
<target>Export timesheet data to various formats</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="export.filter">
|
||||
<source>export.filter</source>
|
||||
<target>Filter data for export</target>
|
||||
@@ -1256,6 +1252,10 @@
|
||||
<source>team.add_user.help</source>
|
||||
<target>Add a new user to the team by selecting it from the list. Afterwards you can decide if the user should become a teamlead.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="default_value_new">
|
||||
<source>default_value_new</source>
|
||||
<target>Default values for new entries</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -929,10 +929,6 @@
|
||||
<source>export.title</source>
|
||||
<target>Eksporti</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="export.subtitle">
|
||||
<source>export.subtitle</source>
|
||||
<target>Eksporti temposlipajn datumojn al diversaj dosierformoj</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="export.filter">
|
||||
<source>export.filter</source>
|
||||
<target>Filtri datumojn por eksporto</target>
|
||||
|
||||
@@ -344,7 +344,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="label.my_team_projects">
|
||||
<source>label.my_team_projects</source>
|
||||
<target>Proyectos de equipo y presupuestos</target>
|
||||
<target>Mis proyectos</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.progress">
|
||||
<source>label.progress</source>
|
||||
@@ -817,7 +817,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_tax_number">
|
||||
<source>label.invoice_tax_number</source>
|
||||
<target>CIF/NIF:</target>
|
||||
<target>Id. de IVA:</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_bank_account">
|
||||
<source>label.invoice_bank_account</source>
|
||||
@@ -830,10 +830,6 @@
|
||||
<source>export.title</source>
|
||||
<target>Exportar</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="export.subtitle">
|
||||
<source>export.subtitle</source>
|
||||
<target>Exportar la información de los partes de horas en varios formatos</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="export.filter">
|
||||
<source>export.filter</source>
|
||||
<target>Filtrar datos para exportar</target>
|
||||
@@ -982,7 +978,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="rates.empty">
|
||||
<source>rates.empty</source>
|
||||
<target state="translated">No se han configurado honorarios aún.</target>
|
||||
<target state="translated">Aún no se han configurado tarifas por hora.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="placeholder.type_message">
|
||||
<source>placeholder.type_message</source>
|
||||
@@ -994,7 +990,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="label.vat_id">
|
||||
<source>label.vat_id</source>
|
||||
<target>Id. de IVA</target>
|
||||
<target>CIF / NIF</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.timesheet.export_decimal">
|
||||
<source>label.timesheet.export_decimal</source>
|
||||
@@ -1224,6 +1220,30 @@
|
||||
<source>registration.check_email</source>
|
||||
<target>Se ha enviado un mensaje a %email%. Contiene un enlace de activación que debe seguir para activar su cuenta.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.type">
|
||||
<source>label.type</source>
|
||||
<target>Tipo</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.account_number">
|
||||
<source>label.account_number</source>
|
||||
<target>Número de personal</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="team.add_user.help">
|
||||
<source>team.add_user.help</source>
|
||||
<target>Agregue un nuevo usuario al equipo seleccionándolo de la lista. Después puedes decidir si el usuario debe convertirse en líder de equipo.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.budgetType">
|
||||
<source>label.budgetType</source>
|
||||
<target>Tipo de Presupuesto</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.budgetType_month">
|
||||
<source>label.budgetType_month</source>
|
||||
<target>Mensual</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="delete_warning.short_stats">
|
||||
<source>delete_warning.short_stats</source>
|
||||
<target>Actualmente %records% existen registros de tiempo, que suman una duración total de %duration%.</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -870,10 +870,6 @@
|
||||
<source>export.title</source>
|
||||
<target>Exportar</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="export.subtitle">
|
||||
<source>export.subtitle</source>
|
||||
<target>Ordu taulak hainbat formatutan exportatu</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="export.filter">
|
||||
<source>export.filter</source>
|
||||
<target>Exportatzeko datuak filtratu</target>
|
||||
|
||||
@@ -981,10 +981,6 @@
|
||||
<source>export.title</source>
|
||||
<target state="translated">Vie</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="export.subtitle">
|
||||
<source>export.subtitle</source>
|
||||
<target state="translated">Vie tuntilomake eri tiedostomuodoissa</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="export.filter">
|
||||
<source>export.filter</source>
|
||||
<target state="translated">Suodata vietävät tiedot</target>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user