Compare commits

..

27 Commits

Author SHA1 Message Date
Kevin Papst
64893e0a95 release 1.15.6 (#2863) 2021-10-18 00:30:35 +02:00
Kevin Papst
6fdc304313 improve sunday FDOW handling (#2862) 2021-10-17 19:17:47 +02:00
Kevin Papst
fb265ed732 make sure that minute_increment is not zero (#2860) 2021-10-16 13:38:28 +02:00
Kevin Papst
e2be5b401c improve summary rows in reports (#2861) 2021-10-16 13:38:08 +02:00
Kevin Papst
8539938b8a use new codecov action (#2854) 2021-10-14 17:23:45 +02:00
Kevin Papst
bbf25823b8 release 1.15.5 (#2848) 2021-10-13 18:31:28 +02:00
Kevin Papst
6b066046c6 default value for billable flag and support in batch update (#2851) 2021-10-13 15:49:20 +02:00
Weblate (bot)
9226daa891 Translations update from Weblate (#2823)
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Péter Gyetvai <gyetpet@mailbox.org>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: Frank.wu <me@wuzhiping.top>
Co-authored-by: web chigusa <chigusaweb66@gmail.com>
Co-authored-by: KasukeLp <kasukelp23@yahoo.com>
Co-authored-by: Simona Iacob <s@zp1.net>
Co-authored-by: Kevin Papst <kpapst@gmx.net>
2021-10-13 14:09:15 +02:00
Kevin Papst
843cd8fb67 refactor system configuration title and wording (#2847) 2021-10-13 13:37:44 +02:00
Kevin Papst
6d7d441d42 fix missing meta fields in duplicate timesheet action (#2845) 2021-10-12 23:41:27 +02:00
Kevin Papst
1c4da30b61 fix version number 2021-10-07 16:42:17 +02:00
Kevin Papst
84e25851d3 set security options on cookies (#2825) 2021-10-07 12:40:06 +02:00
Kevin Papst
3a52daedab fix open budget calculation (#2821) 2021-10-07 11:46:48 +02:00
Kevin Papst
b7cf9cd776 added croatian language (#2817) 2021-10-06 13:41:05 +02:00
Kevin Papst
ccb184b17f Release 1.15.3 (#2813) 2021-10-05 15:30:29 +02:00
Weblate (bot)
162ff07bd5 Translations update from Weblate (#2791)
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Simona Iacob <s@zp1.net>
Co-authored-by: Daniel' Ishmaev <dan.samara@gmail.com>
Co-authored-by: Luna Jernberg <droidbittin@gmail.com>
Co-authored-by: Kevin Papst <kpapst@gmx.net>
2021-10-05 15:22:45 +02:00
Kevin Papst
7326a452c3 re-added lock threads app 2021-10-04 17:09:59 +02:00
Kevin Papst
9453947309 export budget, timeBudget and budgetType (#2812) 2021-10-04 16:57:45 +02:00
Kevin Papst
d25653b6ed removed export and system-config subtitles (#2811) 2021-10-04 14:36:17 +02:00
Kevin Papst
ea415e33f5 do not apply rate factor multiple times (#2807) 2021-10-03 17:40:36 +02:00
Kevin Papst
b6430d5622 allow to customize dashboard permissions for latest row (#2806) 2021-10-03 17:17:01 +02:00
Kevin Papst
6fcd50ecdf hide not started projects in widget (#2805) 2021-10-03 13:53:52 +02:00
Kevin Papst
6857d92c4f respect customer visibility in project-overview report (#2804) 2021-10-03 13:41:14 +02:00
Weblate (bot)
4bb9edb2b5 Translations update from Weblate (#2781)
Co-authored-by: frooog in raain <sadsatan211@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: Mokhtar Garmehi <Mokhtar_garmeh@yahoo.com>
Co-authored-by: Pham Mina <phamthinhalinh93@gmail.com>
Co-authored-by: Ana Carla Vasconcelos <anacarlavgs@gmail.com>
Co-authored-by: SC <lalocas@protonmail.com>
Co-authored-by: dkstiler <dkstiler@gmail.com>
Co-authored-by: Kevin Papst <kevinpapst@users.noreply.github.com>
2021-09-24 15:16:36 +02:00
Input-BDF
dd881bb856 prevent whitespace on line endings in created docx invoices (#2756) 2021-09-24 15:02:01 +02:00
Kevin Papst
204e0bc179 1.15.2 (#2782)
* fix division by zero
* cleanup required directories, check configured data directory, do not check for freetype support
* bump version number
2021-09-24 14:47:40 +02:00
Guillaume-Duc-95
d026460106 copy the billable status when restarting timesheet (#2778) 2021-09-20 16:23:23 +02:00
154 changed files with 4975 additions and 2107 deletions

View File

@@ -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
View File

@@ -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

View File

@@ -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
View 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

View File

@@ -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

View File

@@ -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).

View File

@@ -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 {

View File

@@ -1,7 +1,6 @@
.dataTable {
.weekend {
background-color: #f9f9f9;
opacity: 0.6;
}
}

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,9 @@ framework:
session:
handler_id: App\Security\SessionHandler
cookie_secure: auto
cookie_httponly: true
cookie_samesite: lax
#esi: ~
#fragments: ~

View File

@@ -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:

View File

@@ -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

View File

@@ -6,7 +6,7 @@
"build/app.e22732af.js"
],
"css": [
"build/app.fdf3c5fb.css"
"build/app.c57cdec8.css"
]
},
"invoice": {

View File

@@ -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",

View File

@@ -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);
}
}

View File

@@ -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');

View File

@@ -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';
/**

View File

@@ -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;
}
}

View File

@@ -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')

View File

@@ -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);

View File

@@ -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()

View File

@@ -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

View File

@@ -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;

View File

@@ -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")
*

View File

@@ -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;
}

View File

@@ -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);
}
}
}

View File

@@ -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';

View File

@@ -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,
]);
}
}

View File

@@ -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;
}
/**

View File

@@ -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;
}
/**

View File

@@ -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;
});

View File

@@ -27,7 +27,6 @@ class UserRolesType extends AbstractType
{
$builder
->add('roles', UserRoleType::class, [
'label' => 'label.roles',
'multiple' => true,
'expanded' => true,
])

View File

@@ -27,7 +27,6 @@ class UserTeamsType extends AbstractType
{
$builder
->add('teams', TeamType::class, [
'label' => 'label.team',
'multiple' => true,
'expanded' => true,
])

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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(

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -110,6 +110,8 @@ final class TimesheetService
$mode = $this->trackingModeService->getActiveMode();
$mode->create($timesheet, $request);
$timesheet->setBillable($this->configuration->getTimesheetDefaultBillable());
return $timesheet;
}

View File

@@ -24,7 +24,6 @@ final class PaginatedWorkingTimeChart extends SimpleWidget implements UserWidget
{
$this->repository = $repository;
$this->systemConfiguration = $systemConfiguration;
$this->setId('PaginatedWorkingTimeChart');
$this->setTitle('stats.yourWorkingHours');
}

View 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';
}
}

View 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';
}
}

View 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';
}
}

View 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';
}
}

View 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);
}
}

View File

@@ -618,9 +618,6 @@
"symfony/process": {
"version": "v4.0.3"
},
"symfony/profiler-pack": {
"version": "v1.0.3"
},
"symfony/property-access": {
"version": "v4.0.3"
},

View File

@@ -2,7 +2,7 @@
{% import "macros/widgets.html.twig" as widgets %}
{% block page_title %}{{ 'menu.doctor'|trans }}{% endblock %}
{% block page_subtitle %}Environment: {{ environment }} &ndash; 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) }}

View File

@@ -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 %}

View File

@@ -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) }}

View File

@@ -25,18 +25,21 @@
{% set totals = {'totals': 0} %}
{% set columns = 2 %}
<table class="table table-bordered table-hover dataTable">
<tr>
<th>&nbsp;</th>
<th>&nbsp;</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>&nbsp;</th>
<th>&nbsp;</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 %}

View File

@@ -23,17 +23,20 @@
{% set absoluteTotals = 0 %}
{% set totals = {} %}
<table class="table table-bordered table-hover dataTable">
<tr>
<th>&nbsp;</th>
<th>&nbsp;</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>&nbsp;</th>
<th>&nbsp;</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>&nbsp;</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 %}

View File

@@ -22,19 +22,22 @@
{% else %}
{% set totals = {} %}
<table class="table table-bordered table-hover dataTable">
<tr>
<th>&nbsp;</th>
<th>&nbsp;</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>&nbsp;</th>
<th>&nbsp;</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>&nbsp;</th>
<th>&nbsp;</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>&nbsp;</td>
{% for id, total in totals %}
<td class="text-center text-nowrap">
{{ total|duration }}
</td>
{% endfor %}
</tr>
</tfoot>
</table>
{% endif %}
{% endblock %}

View File

@@ -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 %}

View File

@@ -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) }}

View File

@@ -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

View File

@@ -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);

View File

@@ -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/');

View File

@@ -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');

View File

@@ -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
);

View File

@@ -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) {

View File

@@ -140,6 +140,9 @@ class AppExtensionTest extends TestCase
'templates/invoice/renderer/',
],
'kimai.defaults' => [
'timesheet' => [
'billable' => true,
],
'customer' => [
'timezone' => null,
'country' => 'DE',

View File

@@ -395,6 +395,9 @@ class ConfigurationTest extends TestCase
'dashboard' => [],
'widgets' => [],
'defaults' => [
'timesheet' => [
'billable' => true,
],
'customer' => [
'timezone' => null,
'country' => 'DE',

View File

@@ -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]);
}
}

View File

@@ -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'],

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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']));

View 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);
}
}

View File

@@ -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']));

View File

@@ -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'));
}

View 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([]));
}
}

View 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([]));
}
}

View 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([]));
}
}

View 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([]));
}
}

View File

@@ -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
View 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
View 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
View 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>

View File

@@ -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>

View 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
View 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>

View 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>

View 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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -440,7 +440,7 @@
</trans-unit>
<trans-unit id="label.my_team_projects">
<source>label.my_team_projects</source>
<target>Ομαδικές Εργασίες &amp; προϋπολογισμοί</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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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