0
0
Fork 0
mirror of https://github.com/kevinpapst/kimai2.git synced 2025-04-08 23:10:18 +00:00

Merge branch 'master' into api-user-prefs

This commit is contained in:
Kevin Papst 2021-11-15 19:28:23 +01:00
commit 5f6fdee86a
514 changed files with 21095 additions and 19703 deletions
.env.distSECURITY.mdUPGRADING.md
assets
composer.json
config
package.json
public/build
src
templates
tests
translations

View file

@ -1,30 +1,16 @@
### DATABASE CONFIGURATION # Configure your database connection and set the correct server version:
# Replace "user", "password" and "database" with your database connection. # for MySQL "serverVersion=5.7" and for MariaDB "serverVersion=mariadb-10.5.8"
# Configure the server version, MariaDB requires the "mariadb-" prefix, eg:
# for MySQL "serverVersion=5.7" and for MariaDB "serverVersion=mariadb-10.5.8"
DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8&serverVersion=5.7 DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8&serverVersion=5.7
# Email will be sent with this address as sender
### EMAIL CONFIGURATION
# Emails will be sent "from":
MAILER_FROM=kimai@example.com MAILER_FROM=kimai@example.com
# Email connection (disabled by default) more info at https://www.kimai.org/documentation/emails.html
# Email connection (disabled by default with MAILER_URL=null://null)
# SMTP: smtp://localhost:25?encryption=&auth_mode=
# Google: gmail://username:password@default
# Amazon: ses://ACCESS_KEY:SECRET_KEY@default?region=eu-west-1
# Mailchimp: mandrill://KEY@default
# Mailgun: mailgun://KEY:DOMAIN@default
# Postmark: postmark://ID@default
# Sendgrid: sendgrid://KEY@default
# Disable emails: null://null
MAILER_URL=null://null MAILER_URL=null://null
# do not change, unless you are developing for Kimai
### APPLICATION CONFIGURATION
APP_ENV=prod APP_ENV=prod
# should be changed to a unique character sequence
APP_SECRET=change_this_to_something_unique APP_SECRET=change_this_to_something_unique
# unlikely, that you need to change this one
# Running in a "special" environment, eg. behind reverse proxies?
# Check those:
# TRUSTED_PROXIES=127.0.0.1,127.0.0.2
# TRUSTED_HOSTS=localhost,example.com
CORS_ALLOW_ORIGIN=^https?://localhost(:[0-9]+)?$ CORS_ALLOW_ORIGIN=^https?://localhost(:[0-9]+)?$
# Running behind reverse proxies? Use those:
# TRUSTED_PROXIES=127.0.0.1,127.0.0.2
# TRUSTED_HOSTS=localhost,example.com

View file

@ -12,12 +12,13 @@ As announced in the [README](README.md) I only support the latest available rele
## Reporting a Vulnerability ## Reporting a Vulnerability
Please report any security related vulnerability in the [advisories section at GitHub](https://github.com/kevinpapst/kimai2/security/advisories) or via email to info@keleo.de or kpapst@gmx.net. Please read the [Bughunter](https://www.kimai.org/documentation/bughunter.html) documentation before posting,
and then you can report any security related vulnerability in the [advisories section at GitHub](https://github.com/kevinpapst/kimai2/security/advisories) or via email to kpapst@gmx.net.
I will work as fast as I can to fix the problem and publish a bugfix release / security update. I will work as fast as I can to fix the problem and publish a bugfix release / security update.
Depending on the size of the required fixes, this might take a couple of hours or a couple of days. Depending on the size of the required fixes, this might take a couple of hours or a couple of days.
You can expect that your message will be answered ASAP, but please take into account that I am living in the timezone CET. You can expect that your message will be answered ASAP.
If your issue is valid and after I verified and fixed it, you will be mentioned in the release notes. If your issue is valid and after I verified and fixed it, you will be mentioned in the release notes.
I am grateful for any (discrete) disclosure of vulnerabilities! I am grateful for any (discrete) disclosure of vulnerabilities!

View file

@ -8,6 +8,13 @@ you can upgrade your Kimai installation to the latest stable release.
Check below if there are more version specific steps required, which need to be executed after the normal update process. Check below if there are more version specific steps required, which need to be executed after the normal update process.
Perform EACH version specific task between your version and the new one, otherwise you risk data inconsistency or a broken installation. Perform EACH version specific task between your version and the new one, otherwise you risk data inconsistency or a broken installation.
## [1.16](https://github.com/kevinpapst/kimai2/releases/tag/1.16)
**DEVELOPER**
- Removed `formDateTime` field from API model `I18nConfig`
## [1.15](https://github.com/kevinpapst/kimai2/releases/tag/1.15) ## [1.15](https://github.com/kevinpapst/kimai2/releases/tag/1.15)
**Many database changes: don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).** **Many database changes: don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**

View file

@ -34,6 +34,7 @@ export default class KimaiDatePicker extends KimaiPlugin {
singleDatePicker: true, singleDatePicker: true,
showDropdowns: true, showDropdowns: true,
autoUpdateInput: false, autoUpdateInput: false,
drops: 'down',
locale: { locale: {
format: localeFormat, format: localeFormat,
firstDay: firstDow, firstDay: firstDow,
@ -47,7 +48,8 @@ export default class KimaiDatePicker extends KimaiPlugin {
jQuery(this).on('show.daterangepicker', function (ev, picker) { jQuery(this).on('show.daterangepicker', function (ev, picker) {
if (picker.element.offset().top - jQuery(window).scrollTop() + picker.container.outerHeight() + 30 > jQuery(window).height()) { if (picker.element.offset().top - jQuery(window).scrollTop() + picker.container.outerHeight() + 30 > jQuery(window).height()) {
picker.drops = 'up'; // "up" is not possible here, because the code is triggered on many mobile phones and the picker then appears out of window
picker.drops = 'auto';
picker.move(); picker.move();
} }
}); });

View file

@ -52,6 +52,7 @@ export default class KimaiDateRangePicker extends KimaiPlugin {
autoUpdateInput: false, autoUpdateInput: false,
autoApply: false, autoApply: false,
linkedCalendars: true, linkedCalendars: true,
drops: 'down',
locale: { locale: {
separator: separator, separator: separator,
format: localeFormat, format: localeFormat,
@ -68,7 +69,8 @@ export default class KimaiDateRangePicker extends KimaiPlugin {
jQuery(this).on('show.daterangepicker', function (ev, picker) { jQuery(this).on('show.daterangepicker', function (ev, picker) {
if (picker.element.offset().top - jQuery(window).scrollTop() + picker.container.outerHeight() + 30 > jQuery(window).height()) { if (picker.element.offset().top - jQuery(window).scrollTop() + picker.container.outerHeight() + 30 > jQuery(window).height()) {
picker.drops = 'up'; // "up" is not possible here, because the code is triggered on many mobile phones and the picker then appears out of window
picker.drops = 'auto';
picker.move(); picker.move();
} }
}); });

View file

@ -37,6 +37,7 @@ export default class KimaiDateTimePicker extends KimaiPlugin {
timePicker24Hour: is24hours, timePicker24Hour: is24hours,
showDropdowns: true, showDropdowns: true,
autoUpdateInput: false, autoUpdateInput: false,
drops: 'down',
locale: { locale: {
format: localeFormat, format: localeFormat,
firstDay: firstDow, firstDay: firstDow,
@ -50,7 +51,8 @@ export default class KimaiDateTimePicker extends KimaiPlugin {
jQuery(this).on('show.daterangepicker', function (ev, picker) { jQuery(this).on('show.daterangepicker', function (ev, picker) {
if (picker.element.offset().top - jQuery(window).scrollTop() + picker.container.outerHeight() + 30 > jQuery(window).height()) { if (picker.element.offset().top - jQuery(window).scrollTop() + picker.container.outerHeight() + 30 > jQuery(window).height()) {
picker.drops = 'up'; // "up" is not possible here, because the code is triggered on many mobile phones and the picker then appears out of window
picker.drops = 'auto';
picker.move(); picker.move();
} }
}); });

View file

@ -12,4 +12,44 @@ form.form-narrow {
.form-group { .form-group {
margin: 0 0 5px 0; margin: 0 0 5px 0;
} }
}
/* bootstrap3 hack because the filter look plain ugly without it (see report: project month) */
.checkbox-menu li label {
display: block;
padding: 5px 15px 5px 10px !important;
clear: both;
font-weight: normal;
line-height: 1.42857143;
color: #333;
white-space: nowrap;
margin:0;
transition: background-color .4s ease;
}
.checkbox-menu li div.checkbox {
display: block;
width: 100%;
}
.checkbox-menu li div.radio {
display: block;
width: 100%;
}
.checkbox-menu li input {
margin: 0 5px !important;
position: relative;
}
.checkbox-menu li.active label {
background-color: #cbcbff;
font-weight:bold;
}
.checkbox-menu li label:hover,
.checkbox-menu li label:focus {
background-color: #f5f5f5;
}
.checkbox-menu li.active label:hover,
.checkbox-menu li.active label:focus {
background-color: #b8b8ff;
} }

View file

@ -165,7 +165,8 @@
"bin/console lint:xliff translations", "bin/console lint:xliff translations",
"bin/console doctrine:schema:validate --skip-sync -vvv --no-interaction" "bin/console doctrine:schema:validate --skip-sync -vvv --no-interaction"
], ],
"kimai:tests": "vendor/bin/phpunit tests/", "tests": "vendor/bin/phpunit tests/",
"kimai:tests": "@tests",
"kimai:tests-unit": "vendor/bin/phpunit --exclude-group integration tests/", "kimai:tests-unit": "vendor/bin/phpunit --exclude-group integration tests/",
"kimai:tests-integration": "vendor/bin/phpunit --group integration tests/", "kimai:tests-integration": "vendor/bin/phpunit --group integration tests/",
"kimai:phpstan": "@phpstan", "kimai:phpstan": "@phpstan",

View file

@ -91,7 +91,8 @@ kimai:
CUSTOMERS_TEAMLEAD: ['view_teamlead_customer','budget_teamlead_customer','comments_teamlead_customer','comments_create_teamlead_customer','details_teamlead_customer'] CUSTOMERS_TEAMLEAD: ['view_teamlead_customer','budget_teamlead_customer','comments_teamlead_customer','comments_create_teamlead_customer','details_teamlead_customer']
INVOICE: ['view_invoice','create_invoice'] INVOICE: ['view_invoice','create_invoice']
INVOICE_ADMIN: ['manage_invoice_template'] INVOICE_ADMIN: ['manage_invoice_template']
TIMESHEET: ['view_own_timesheet','start_own_timesheet','stop_own_timesheet','create_own_timesheet','edit_own_timesheet','export_own_timesheet','delete_own_timesheet'] INVOICE_ALL: ['delete_invoice']
TIMESHEET: ['view_own_timesheet','start_own_timesheet','stop_own_timesheet','create_own_timesheet','edit_own_timesheet','export_own_timesheet','delete_own_timesheet','weekly_own_timesheet']
TIMESHEET_OTHER: ['view_other_timesheet','start_other_timesheet','stop_other_timesheet','create_other_timesheet','edit_other_timesheet','export_other_timesheet','delete_other_timesheet'] TIMESHEET_OTHER: ['view_other_timesheet','start_other_timesheet','stop_other_timesheet','create_other_timesheet','edit_other_timesheet','export_other_timesheet','delete_other_timesheet']
PROFILE: ['view_own_profile','edit_own_profile','password_own_profile','preferences_own_profile','api-token_own_profile'] PROFILE: ['view_own_profile','edit_own_profile','password_own_profile','preferences_own_profile','api-token_own_profile']
PROFILE_OTHER: ['view_other_profile','edit_other_profile','password_other_profile','roles_other_profile','preferences_other_profile','api-token_other_profile','teams_other_profile'] PROFILE_OTHER: ['view_other_profile','edit_other_profile','password_other_profile','roles_other_profile','preferences_other_profile','api-token_other_profile','teams_other_profile']
@ -120,7 +121,7 @@ kimai:
ROLE_ADMIN: ['ROLE_ADMIN'] ROLE_ADMIN: ['ROLE_ADMIN']
ROLE_SUPER_ADMIN: ['ROLE_SUPER_ADMIN'] ROLE_SUPER_ADMIN: ['ROLE_SUPER_ADMIN']
# only here to register the (partially) unused permissions in the UI # only here to register the (partially) unused permissions in the UI
ROLE_FAKE: ['CUSTOMERS_ALL_TEAMLEAD','CUSTOMERS_ALL_TEAM','PROJECTS_ALL_TEAMLEAD','PROJECTS_ALL_TEAM','ACTIVITIES_ALL_TEAMLEAD','ACTIVITIES_ALL_TEAM'] ROLE_FAKE: ['CUSTOMERS_ALL_TEAMLEAD','CUSTOMERS_ALL_TEAM','PROJECTS_ALL_TEAMLEAD','PROJECTS_ALL_TEAM','ACTIVITIES_ALL_TEAMLEAD','ACTIVITIES_ALL_TEAM','INVOICE_ALL']
# add or remove single permissions # add or remove single permissions
roles: roles:
ROLE_USER: [] ROLE_USER: []
@ -214,91 +215,74 @@ kimai:
# -------------------------------------------------------------------------------- # --------------------------------------------------------------------------------
languages: languages:
cs: cs:
date_time_type: 'dd.MM.yyyy HH:mm'
date_type: 'dd.MM.yyyy' date_type: 'dd.MM.yyyy'
date: 'd.m.Y' date: 'd.m.Y'
date_time: 'd.m H:i' date_time: 'd.m H:i'
da: da:
date_time_type: 'dd.MM.yyyy HH:mm'
date_type: 'dd.MM.yyyy' date_type: 'dd.MM.yyyy'
date: 'd.m.Y' date: 'd.m.Y'
date_time: 'd.m. H:i' date_time: 'd.m. H:i'
de: de:
date_time_type: 'dd.MM.yyyy HH:mm'
date_type: 'dd.MM.yyyy' date_type: 'dd.MM.yyyy'
date: 'd.m.Y' date: 'd.m.Y'
date_time: 'd.m. H:i' date_time: 'd.m. H:i'
de_AT: de_AT:
date_time_type: 'dd.MM.yyyy HH:mm'
date_type: 'dd.MM.yyyy' date_type: 'dd.MM.yyyy'
date: 'd.m.Y' date: 'd.m.Y'
date_time: 'd.m. H:i' date_time: 'd.m. H:i'
de_CH: de_CH:
date_time_type: 'dd.MM.yyyy HH:mm'
date_type: 'dd.MM.yyyy' date_type: 'dd.MM.yyyy'
date: 'd.m.Y' date: 'd.m.Y'
date_time: 'd.m. H:i' date_time: 'd.m. H:i'
el: el:
date_time_type: 'dd.MM.yyyy HH:mm'
date_type: 'dd.MM.yyyy' date_type: 'dd.MM.yyyy'
date: 'd.m.Y' date: 'd.m.Y'
date_time: 'd.m. H:i' date_time: 'd.m. H:i'
en: en:
date_time_type: 'yyyy-MM-dd HH:mm'
date_type: 'yyyy-MM-dd' date_type: 'yyyy-MM-dd'
date: 'Y-m-d' date: 'Y-m-d'
date_time: 'm-d H:i' date_time: 'm-d H:i'
duration: '%%h:%%m h' duration: '%%h:%%m h'
es: es:
date_time_type: 'dd.MM.yyyy HH:mm'
date_type: 'dd.MM.yyyy' date_type: 'dd.MM.yyyy'
date: 'd.m.Y' date: 'd.m.Y'
date_time: 'd.m. H:i' date_time: 'd.m. H:i'
fi: fi:
date_time_type: 'dd.MM.yyyy HH:mm'
date_type: 'dd.MM.yyyy' date_type: 'dd.MM.yyyy'
date: 'd.m.Y' date: 'd.m.Y'
date_time: 'd.m. H:i' date_time: 'd.m. H:i'
fr: fr:
date_time_type: 'dd/MM/yyyy HH:mm'
date_type: 'dd/MM/yyyy' date_type: 'dd/MM/yyyy'
date: 'd/m/Y' date: 'd/m/Y'
date_time: 'd/m H:i' date_time: 'd/m H:i'
duration: '%%h h %%m' duration: '%%h h %%m'
he: he:
date_time_type: 'dd/MM/yyyy HH:mm'
date_type: 'dd/MM/yyyy' date_type: 'dd/MM/yyyy'
date: 'd/m/Y' date: 'd/m/Y'
date_time: 'd/m H:i' date_time: 'd/m H:i'
duration: '%%h:%%m' duration: '%%h:%%m'
hu: hu:
date_time_type: 'yyyy.MM.dd. HH:mm'
date_type: 'yyyy.MM.dd.' date_type: 'yyyy.MM.dd.'
date: 'Y.m.d.' date: 'Y.m.d.'
date_time: 'm.d. H:i' date_time: 'm.d. H:i'
it: it:
date_time_type: 'dd.MM.yyyy HH:mm'
date_type: 'dd.MM.yyyy' date_type: 'dd.MM.yyyy'
date: 'd.m.Y' date: 'd.m.Y'
date_time: 'd.m. H:i' date_time: 'd.m. H:i'
nl: nl:
date_time_type: 'dd.MM.yyyy HH:mm'
date_type: 'dd.MM.yyyy' date_type: 'dd.MM.yyyy'
date: 'd.m.Y' date: 'd.m.Y'
date_time: 'd.m. H:i' date_time: 'd.m. H:i'
duration: '%%hu%%m' duration: '%%hu%%m'
pt_BR: pt_BR:
date_time_type: 'dd-MM-yyyy HH:mm'
date_type: 'dd-MM-yyyy' date_type: 'dd-MM-yyyy'
date: 'd-m-Y' date: 'd-m-Y'
date_time: 'd-m H:i' date_time: 'd-m H:i'
ru: ru:
date_time_type: 'dd.MM.yyyy HH:mm'
date_type: 'dd.MM.yyyy' date_type: 'dd.MM.yyyy'
date: 'd.m.Y' date: 'd.m.Y'
date_time: 'd.m. H:i' date_time: 'd.m. H:i'
sk: sk:
date_time_type: 'dd. MM. yyyy HH:mm'
date_type: 'dd. MM. yyyy' date_type: 'dd. MM. yyyy'
date: 'd. m. Y' date: 'd. m. Y'
date_time: 'd. m. H:i' date_time: 'd. m. H:i'
@ -306,7 +290,6 @@ kimai:
duration: '%%h:%%m tim' duration: '%%h:%%m tim'
date_time: 'd/m H:i' date_time: 'd/m H:i'
pl: pl:
date_time_type: 'dd. MM. yyyy HH:mm'
date_type: 'dd. MM. yyyy' date_type: 'dd. MM. yyyy'
date: 'd. m. Y' date: 'd. m. Y'
date_time: 'd. m. H:i' date_time: 'd. m. H:i'

View file

@ -65,6 +65,9 @@ services:
App\Validator\Constraints\TimesheetValidator: App\Validator\Constraints\TimesheetValidator:
arguments: [!tagged timesheet.validator] arguments: [!tagged timesheet.validator]
App\Validator\Constraints\ProjectValidator:
arguments: [!tagged project.validator]
App\Validator\Constraints\QuickEntryTimesheetValidator: App\Validator\Constraints\QuickEntryTimesheetValidator:
arguments: [!tagged timesheet.validator] arguments: [!tagged timesheet.validator]

View file

@ -48,5 +48,6 @@
}, },
"browserslist": [ "browserslist": [
"defaults" "defaults"
] ],
"dependencies": {}
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -3,10 +3,10 @@
"app": { "app": {
"js": [ "js": [
"build/runtime.b8e7bb04.js", "build/runtime.b8e7bb04.js",
"build/app.da44b7f8.js" "build/app.3947eaef.js"
], ],
"css": [ "css": [
"build/app.d2b280dd.css" "build/app.3bc2b4d9.css"
] ]
}, },
"invoice": { "invoice": {

View file

@ -1,6 +1,6 @@
{ {
"build/app.css": "build/app.d2b280dd.css", "build/app.css": "build/app.3bc2b4d9.css",
"build/app.js": "build/app.da44b7f8.js", "build/app.js": "build/app.3947eaef.js",
"build/invoice.css": "build/invoice.ff32661a.css", "build/invoice.css": "build/invoice.ff32661a.css",
"build/invoice.js": "build/invoice.19f36eca.js", "build/invoice.js": "build/invoice.19f36eca.js",
"build/invoice-pdf.css": "build/invoice-pdf.9a7468ef.css", "build/invoice-pdf.css": "build/invoice-pdf.9a7468ef.css",

View file

@ -64,13 +64,12 @@ final class ConfigurationController extends BaseApiController
$model = new I18nConfig(); $model = new I18nConfig();
$model $model
->setFormDateTime($formats->getDateTimeTypeFormat($locale))
->setFormDate($formats->getDateTypeFormat($locale)) ->setFormDate($formats->getDateTypeFormat($locale))
->setDateTime($formats->getDateTimeFormat($locale)) ->setDateTime($formats->getDateTimeFormat($locale))
->setDate($formats->getDateFormat($locale)) ->setDate($formats->getDateFormat($locale))
->setDuration($formats->getDurationFormat($locale)) ->setDuration($formats->getDurationFormat($locale))
->setTime($formats->getTimeFormat($locale)) ->setTime($formats->getTimeFormat($locale))
->setIs24hours($formats->isTwentyFourHours($locale)) ->setIs24hours($user->is24Hour())
->setNow($this->getDateTimeFactory()->createDateTime()) ->setNow($this->getDateTimeFactory()->createDateTime())
; ;

View file

@ -18,17 +18,6 @@ use JMS\Serializer\Annotation as Serializer;
*/ */
final class I18nConfig final class I18nConfig
{ {
/**
* Format used for 'begin' and 'end'
*
* @var string
*
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
* @Serializer\Type(name="string")
* @phpstan-ignore-next-line
*/
private $formDateTime = '';
/** /**
* Format used for toolbar queries * Format used for toolbar queries
* *
@ -114,13 +103,6 @@ final class I18nConfig
return $this; return $this;
} }
public function setFormDateTime(string $formDateTime): I18nConfig
{
$this->formDateTime = $formDateTime;
return $this;
}
public function setFormDate(string $formDate): I18nConfig public function setFormDate(string $formDate): I18nConfig
{ {
$this->formDate = $formDate; $this->formDate = $formDate;

View file

@ -63,6 +63,7 @@ class InvoiceCreateCommand extends Command
* @var string|null * @var string|null
*/ */
private $previewDirectory; private $previewDirectory;
private $previewUniqueFile = false;
public function __construct( public function __construct(
ServiceInvoice $serviceInvoice, ServiceInvoice $serviceInvoice,
@ -104,6 +105,7 @@ class InvoiceCreateCommand extends Command
->addOption('search', null, InputOption::VALUE_OPTIONAL, 'Search term to filter invoice entries', null) ->addOption('search', null, InputOption::VALUE_OPTIONAL, 'Search term to filter invoice entries', null)
->addOption('exported', null, InputOption::VALUE_OPTIONAL, 'Exported filter for invoice entries (possible values: exported, all), by default only "not exported" items are fetched', null) ->addOption('exported', null, InputOption::VALUE_OPTIONAL, 'Exported filter for invoice entries (possible values: exported, all), by default only "not exported" items are fetched', null)
->addOption('preview', null, InputOption::VALUE_OPTIONAL, 'Absolute path for a rendered preview of the invoice, which will neither be saved nor the items be marked as exported.', null) ->addOption('preview', null, InputOption::VALUE_OPTIONAL, 'Absolute path for a rendered preview of the invoice, which will neither be saved nor the items be marked as exported.', null)
->addOption('preview-unique', null, InputOption::VALUE_NONE, 'Adds a unique part to the filename of the generated invoice preview file, so there is no chance that they get overwritten on same project name.')
; ;
} }
@ -225,6 +227,7 @@ class InvoiceCreateCommand extends Command
$markAsExported = false; $markAsExported = false;
if ($input->getOption('preview') !== null) { if ($input->getOption('preview') !== null) {
$this->previewUniqueFile = $input->getOption('preview-unique');
$this->previewDirectory = rtrim($input->getOption('preview'), '/') . '/'; $this->previewDirectory = rtrim($input->getOption('preview'), '/') . '/';
if (!is_dir($this->previewDirectory) || !is_writable($this->previewDirectory)) { if (!is_dir($this->previewDirectory) || !is_writable($this->previewDirectory)) {
$io->error('Invalid preview directory given'); $io->error('Invalid preview directory given');
@ -350,8 +353,9 @@ class InvoiceCreateCommand extends Command
$filename = $filename[1]; $filename = $filename[1];
} }
} }
// depending on your setup, this might be a good idea if ($this->previewUniqueFile) {
// $filename = uniqid() . $filename; $filename = uniqid('invoice_') . $filename;
}
} }
if ($response instanceof BinaryFileResponse) { if ($response instanceof BinaryFileResponse) {

View file

@ -60,28 +60,6 @@ final class LanguageFormattings
return $this->momentFormatter->convert($this->getDateTypeFormat($locale)); return $this->momentFormatter->convert($this->getDateTypeFormat($locale));
} }
/**
* Returns the format which is used by the form component to handle datetime values.
*
* @param string $locale
* @return string
*/
public function getDateTimeTypeFormat(string $locale): string
{
return $this->getConfig('date_time_type', $locale);
}
/**
* Returns the format which is used by the Javascript component to handle datetime values.
*
* @param string $locale
* @return string
*/
public function getDateTimePickerFormat(string $locale): string
{
return $this->momentFormatter->convert($this->getDateTimeTypeFormat($locale));
}
/** /**
* Returns the locale specific date format, which should be used in combination with the twig filter "|date". * Returns the locale specific date format, which should be used in combination with the twig filter "|date".
* *
@ -126,17 +104,6 @@ final class LanguageFormattings
return $this->getConfig('duration', $locale); return $this->getConfig('duration', $locale);
} }
/**
* Returns whether this locale uses the 24 hour format.
*
* @param string $locale
* @return bool
*/
public function isTwentyFourHours(string $locale): bool
{
return (bool) $this->getConfig('24_hours', $locale);
}
/** /**
* @param string $key * @param string $key
* @param string $locale * @param string $locale

View file

@ -15,6 +15,8 @@ use PackageVersions\Versions;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
/** /**
* @Route(path="/doctor") * @Route(path="/doctor")
@ -56,11 +58,19 @@ class DoctorController extends AbstractController
} }
/** /**
* @Route(path="/flush-log", name="doctor_flush_log", methods={"GET"}) * @Route(path="/flush-log/{token}", name="doctor_flush_log", methods={"GET"})
* @Security("is_granted('system_configuration')") * @Security("is_granted('system_configuration')")
*/ */
public function deleteLogfileAction(): Response public function deleteLogfileAction(string $token, CsrfTokenManagerInterface $csrfTokenManager): Response
{ {
if (!$csrfTokenManager->isTokenValid(new CsrfToken('doctor.flush_log', $token))) {
$this->flashError('action.delete.error');
return $this->redirectToRoute('doctor');
}
$csrfTokenManager->refreshToken($token);
$logfile = $this->getLogFilename(); $logfile = $this->getLogFilename();
if (file_exists($logfile)) { if (file_exists($logfile)) {

View file

@ -144,6 +144,7 @@ class ExportController extends AbstractController
/** /**
* @param ExportQuery $query * @param ExportQuery $query
* @return ExportItemInterface[] * @return ExportItemInterface[]
* @throws TooManyItemsExportException
*/ */
protected function getEntries(ExportQuery $query): array protected function getEntries(ExportQuery $query): array
{ {

View file

@ -25,7 +25,7 @@ use Symfony\Component\Routing\Annotation\Route;
* Controller used to enter times in weekly form. * Controller used to enter times in weekly form.
* *
* @Route(path="/quick_entry") * @Route(path="/quick_entry")
* @Security("is_granted('edit_own_timesheet')") * @Security("is_granted('weekly_own_timesheet') and is_granted('edit_own_timesheet')")
*/ */
class QuickEntryController extends AbstractController class QuickEntryController extends AbstractController
{ {

View file

@ -33,7 +33,7 @@ final class ProjectDateRangeController extends AbstractController
$form = $this->createForm(ProjectDateRangeForm::class, $query, [ $form = $this->createForm(ProjectDateRangeForm::class, $query, [
'timezone' => $user->getTimezone() 'timezone' => $user->getTimezone()
]); ]);
$form->submit($request->query->all(), false); $form->handleRequest($request);
$dateRange = new DateRange(true); $dateRange = new DateRange(true);
$dateRange->setBegin($query->getMonth()); $dateRange->setBegin($query->getMonth());

View file

@ -329,13 +329,19 @@ class Configuration implements ConfigurationInterface
->useAttributeAsKey('name', false) // see https://github.com/symfony/symfony/issues/18988 ->useAttributeAsKey('name', false) // see https://github.com/symfony/symfony/issues/18988
->arrayPrototype() ->arrayPrototype()
->children() ->children()
->scalarNode('date_time_type')->defaultValue('yyyy-MM-dd HH:mm')->end() // for DateTimeType ->scalarNode('date_time_type') // for DateTimeType
->defaultValue('yyyy-MM-dd HH:mm')
->setDeprecated('date_time_type is deprecated since 1.16 and was replaced by the 24 user configuration')
->end()
->scalarNode('date_type')->defaultValue('yyyy-MM-dd')->end() // for DateType ->scalarNode('date_type')->defaultValue('yyyy-MM-dd')->end() // for DateType
->scalarNode('date')->defaultValue('Y-m-d')->end() // for display via twig ->scalarNode('date')->defaultValue('Y-m-d')->end() // for display via twig
->scalarNode('date_time')->defaultValue('m-d H:i')->end() // for display via twig ->scalarNode('date_time')->defaultValue('m-d H:i')->end() // for display via twig
->scalarNode('duration')->defaultValue('%%h:%%m h')->end() // for display via twig ->scalarNode('duration')->defaultValue('%%h:%%m h')->end() // for display via twig
->scalarNode('time')->defaultValue('H:i')->end() // for display via twig ->scalarNode('time')->defaultValue('H:i')->end() // for display via twig
->booleanNode('24_hours')->defaultTrue()->end() // for DateTimeType JS component ->booleanNode('24_hours') // for DateTimeType JS component
->defaultTrue()
->setDeprecated('24_hours is deprecated since 1.16 and a user configuration now')
->end()
->end() ->end()
->end() ->end()
; ;

View file

@ -37,6 +37,7 @@ class Invoice
{ {
public const STATUS_PENDING = 'pending'; public const STATUS_PENDING = 'pending';
public const STATUS_PAID = 'paid'; public const STATUS_PAID = 'paid';
public const STATUS_CANCELED = 'canceled';
public const STATUS_NEW = 'new'; public const STATUS_NEW = 'new';
/** /**
@ -306,6 +307,16 @@ class Invoice
return $this; return $this;
} }
public function isCanceled(): bool
{
return $this->status === self::STATUS_CANCELED;
}
public function setIsCanceled(): void
{
$this->status = self::STATUS_CANCELED;
}
public function getDueDays(): int public function getDueDays(): int
{ {
return $this->dueDays; return $this->dueDays;

View file

@ -455,6 +455,20 @@ class User implements UserInterface, EquatableInterface, \Serializable
return null; return null;
} }
public function getTimeFormat(): string
{
if ($this->is24Hour()) {
return 'H:i';
}
return 'h:i A';
}
public function is24Hour(): bool
{
return (bool) $this->getPreferenceValue(UserPreference::HOUR_24, true);
}
public function getLocale(): string public function getLocale(): string
{ {
return $this->getPreferenceValue(UserPreference::LOCALE, User::DEFAULT_LANGUAGE); return $this->getPreferenceValue(UserPreference::LOCALE, User::DEFAULT_LANGUAGE);

View file

@ -33,6 +33,7 @@ class UserPreference
public const INTERNAL_RATE = 'internal_rate'; public const INTERNAL_RATE = 'internal_rate';
public const SKIN = 'skin'; public const SKIN = 'skin';
public const LOCALE = 'language'; public const LOCALE = 'language';
public const HOUR_24 = 'hours_24';
public const TIMEZONE = 'timezone'; public const TIMEZONE = 'timezone';
public const FIRST_WEEKDAY = 'first_weekday'; public const FIRST_WEEKDAY = 'first_weekday';

View file

@ -30,13 +30,24 @@ class InvoiceSubscriber extends AbstractActionsSubscriber
return; return;
} }
if ($invoice->isNew() || $invoice->isPaid()) { if (!$invoice->isPending()) {
$event->addAction('invoice.pending', ['url' => $this->path('admin_invoice_status', ['id' => $invoice->getId(), 'status' => 'pending'])]); $event->addAction('invoice.pending', ['url' => $this->path('admin_invoice_status', ['id' => $invoice->getId(), 'status' => 'pending'])]);
} elseif ($invoice->isPending()) { } else {
$event->addAction('invoice.paid', ['url' => $this->path('admin_invoice_status', ['id' => $invoice->getId(), 'status' => 'paid']), 'class' => 'modal-ajax-form']); $event->addAction('invoice.paid', ['url' => $this->path('admin_invoice_status', ['id' => $invoice->getId(), 'status' => 'paid']), 'class' => 'modal-ajax-form']);
} }
$allowDelete = $this->isGranted('delete_invoice');
if (!$invoice->isCanceled()) {
$id = $allowDelete ? 'invoice.cancel' : 'trash';
$event->addAction($id, ['url' => $this->path('admin_invoice_status', ['id' => $invoice->getId(), 'status' => 'canceled']), 'title' => 'invoice.cancel', 'translation_domain' => 'actions']);
}
$event->addDivider();
$event->addAction('download', ['url' => $this->path('admin_invoice_download', ['id' => $invoice->getId()]), 'target' => '_blank']); $event->addAction('download', ['url' => $this->path('admin_invoice_download', ['id' => $invoice->getId()]), 'target' => '_blank']);
$event->addDelete($this->path('admin_invoice_delete', ['id' => $invoice->getId(), 'token' => $payload['token']]), false);
if ($this->isGranted('delete_invoice')) {
$event->addDelete($this->path('admin_invoice_delete', ['id' => $invoice->getId(), 'token' => $payload['token']]), false);
}
} }
} }

View file

@ -55,7 +55,7 @@ final class MenuSubscriber implements EventSubscriberInterface
$timesheets->setChildRoutes(['timesheet_export', 'timesheet_edit', 'timesheet_create', 'timesheet_multi_update']); $timesheets->setChildRoutes(['timesheet_export', 'timesheet_edit', 'timesheet_create', 'timesheet_multi_update']);
$menu->addItem($timesheets); $menu->addItem($timesheets);
if ($auth->isGranted('edit_own_timesheet')) { if ($auth->isGranted('weekly_own_timesheet') && $auth->isGranted('edit_own_timesheet')) {
$mode = $this->trackingModeService->getActiveMode(); $mode = $this->trackingModeService->getActiveMode();
if ($mode->canEditDuration() || $mode->canEditEnd()) { if ($mode->canEditDuration() || $mode->canEditEnd()) {
$menu->addItem( $menu->addItem(

View file

@ -11,6 +11,7 @@ namespace App\EventSubscriber;
use App\Entity\User; use App\Entity\User;
use App\Entity\UserPreference; use App\Entity\UserPreference;
use App\Form\Type\SkinType;
use KevinPapst\AdminLTEBundle\Helper\ContextHelper; use KevinPapst\AdminLTEBundle\Helper\ContextHelper;
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\KernelEvent; use Symfony\Component\HttpKernel\Event\KernelEvent;
@ -70,7 +71,7 @@ final class ThemeOptionsSubscriber implements EventSubscriberInterface
$name = $ref->getName(); $name = $ref->getName();
switch ($name) { switch ($name) {
case UserPreference::SKIN: case UserPreference::SKIN:
if (!empty($ref->getValue())) { if (!empty($ref->getValue()) && \in_array($ref->getValue(), SkinType::THEMES)) {
$this->helper->setOption('skin', 'skin-' . $ref->getValue()); $this->helper->setOption('skin', 'skin-' . $ref->getValue());
} }
break; break;
@ -79,7 +80,7 @@ final class ThemeOptionsSubscriber implements EventSubscriberInterface
if ($ref->getValue() === 'boxed') { if ($ref->getValue() === 'boxed') {
$this->helper->setOption('boxed_layout', true); $this->helper->setOption('boxed_layout', true);
$this->helper->setOption('fixed_layout', false); $this->helper->setOption('fixed_layout', false);
} elseif ($ref->getValue() === 'fixed') { } else {
$this->helper->setOption('boxed_layout', false); $this->helper->setOption('boxed_layout', false);
$this->helper->setOption('fixed_layout', true); $this->helper->setOption('fixed_layout', true);
} }

View file

@ -112,6 +112,13 @@ final class UserPreferenceSubscriber implements EventSubscriberInterface
->setSection('locale') ->setSection('locale')
->setType(FirstWeekDayType::class), ->setType(FirstWeekDayType::class),
(new UserPreference())
->setName(UserPreference::HOUR_24)
->setValue(true)
->setOrder(305)
->setSection('locale')
->setType(CheckboxType::class),
(new UserPreference()) (new UserPreference())
->setName(UserPreference::SKIN) ->setName(UserPreference::SKIN)
->setValue($this->configuration->getUserDefaultTheme()) ->setValue($this->configuration->getUserDefaultTheme())

View file

@ -18,6 +18,8 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
*/ */
class BudgetType extends AbstractType class BudgetType extends AbstractType
{ {
public const TYPE_MONTH = 'month';
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
@ -25,9 +27,12 @@ class BudgetType extends AbstractType
{ {
$resolver->setDefaults([ $resolver->setDefaults([
'label' => 'label.budgetType', 'label' => 'label.budgetType',
// not yet translated in enough languages
//'placeholder' => 'label.budgetType_full',
'required' => false, 'required' => false,
'search' => false,
'choices' => [ 'choices' => [
'label.budgetType_month' => 'month', 'label.budgetType_month' => self::TYPE_MONTH,
], ],
]); ]);
} }

View file

@ -10,11 +10,15 @@
namespace App\Form\Type; namespace App\Form\Type;
use App\API\BaseApiController; use App\API\BaseApiController;
use App\Entity\User;
use App\Utils\DateFormatConverter;
use App\Utils\LocaleSettings; use App\Utils\LocaleSettings;
use App\Utils\MomentFormatConverter;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType; use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView; use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
/** /**
@ -34,9 +38,6 @@ class DateTimePickerType extends AbstractType
*/ */
public function configureOptions(OptionsResolver $resolver) public function configureOptions(OptionsResolver $resolver)
{ {
$dateTimePicker = $this->localeSettings->getDateTimePickerFormat();
$dateTimeFormat = $this->localeSettings->getDateTimeTypeFormat();
$resolver->setDefaults([ $resolver->setDefaults([
'documentation' => [ 'documentation' => [
'type' => 'string', 'type' => 'string',
@ -46,8 +47,18 @@ class DateTimePickerType extends AbstractType
'label' => 'label.begin', 'label' => 'label.begin',
'widget' => 'single_text', 'widget' => 'single_text',
'html5' => false, 'html5' => false,
'format' => $dateTimeFormat, 'format' => function (Options $options) {
'format_picker' => $dateTimePicker, /** @var User $user */
$user = $options['user'];
$converter = new DateFormatConverter();
return $this->localeSettings->getDateTypeFormat() . ' ' . $converter->convert($user->getTimeFormat()); // PHP
},
'format_picker' => function (Options $options) {
$converter = new MomentFormatConverter();
return $converter->convert($options['format']); // JS
},
'with_seconds' => false, 'with_seconds' => false,
'time_increment' => 1, 'time_increment' => 1,
]); ]);

View file

@ -18,6 +18,21 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
*/ */
class SkinType extends AbstractType class SkinType extends AbstractType
{ {
public const THEMES = [
'blue' => 'blue',
'black' => 'black',
'green' => 'green',
'purple' => 'purple',
'red' => 'red',
'yellow' => 'yellow',
'blue-light' => 'blue-light',
'black-light' => 'black-light',
'green-light' => 'green-light',
'purple-light' => 'purple-light',
'red-light' => 'red-light',
'yellow-light' => 'yellow-light',
];
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
@ -25,20 +40,7 @@ class SkinType extends AbstractType
{ {
$resolver->setDefaults([ $resolver->setDefaults([
'required' => true, 'required' => true,
'choices' => [ 'choices' => self::THEMES,
'blue' => 'blue',
'black' => 'black',
'green' => 'green',
'purple' => 'purple',
'red' => 'red',
'yellow' => 'yellow',
'blue-light' => 'blue-light',
'black-light' => 'black-light',
'green-light' => 'green-light',
'purple-light' => 'purple-light',
'red-light' => 'red-light',
'yellow-light' => 'yellow-light',
]
]); ]);
} }

View file

@ -0,0 +1,18 @@
<?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\Invoice;
final class DuplicateInvoiceNumberException extends \Exception
{
public function __construct(string $invoiceNumber)
{
parent::__construct('Invoice number "' . $invoiceNumber . '" already existing');
}
}

View file

@ -58,21 +58,29 @@ final class ConfigurableNumberGenerator implements NumberGeneratorInterface
{ {
$format = $this->configuration->find('invoice.number_format'); $format = $this->configuration->find('invoice.number_format');
$invoiceDate = $this->model->getInvoiceDate(); $invoiceDate = $this->model->getInvoiceDate();
$result = $format;
preg_match_all('/{[^}]*?}/', $format, $matches); $loops = 0;
foreach ($matches[0] as $part) { $increaseBy = 0;
$partialResult = $this->parseReplacer($invoiceDate, $part);
$result = str_replace($part, $partialResult, $result); do {
} $result = $format;
preg_match_all('/{[^}]*?}/', $format, $matches);
foreach ($matches[0] as $part) {
$partialResult = $this->parseReplacer($invoiceDate, $part, $increaseBy);
$result = str_replace($part, $partialResult, $result);
}
$increaseBy++;
} while ($this->repository->hasInvoice($result) && $loops++ < 99);
return (string) $result; return (string) $result;
} }
private function parseReplacer(\DateTime $invoiceDate, string $originalFormat): string private function parseReplacer(\DateTime $invoiceDate, string $originalFormat, int $increaseBy): string
{ {
$formatterLength = null; $formatterLength = null;
$increaseBy = 0;
$formatPattern = str_replace(['{', '}'], '', $originalFormat); $formatPattern = str_replace(['{', '}'], '', $originalFormat);
$parts = preg_split('/([+\-,])+/', $formatPattern, -1, PREG_SPLIT_DELIM_CAPTURE); $parts = preg_split('/([+\-,])+/', $formatPattern, -1, PREG_SPLIT_DELIM_CAPTURE);

View file

@ -239,10 +239,6 @@ final class ServiceInvoice
public function changeInvoiceStatus(Invoice $invoice, string $status) public function changeInvoiceStatus(Invoice $invoice, string $status)
{ {
if (!\in_array($status, [Invoice::STATUS_NEW, Invoice::STATUS_PENDING, Invoice::STATUS_PAID])) {
throw new \InvalidArgumentException('Unknown invoice status');
}
switch ($status) { switch ($status) {
case Invoice::STATUS_NEW: case Invoice::STATUS_NEW:
$invoice->setIsNew(); $invoice->setIsNew();
@ -255,6 +251,13 @@ final class ServiceInvoice
case Invoice::STATUS_PAID: case Invoice::STATUS_PAID:
$invoice->setIsPaid(); $invoice->setIsPaid();
break; break;
case Invoice::STATUS_CANCELED:
$invoice->setIsCanceled();
break;
default:
throw new \InvalidArgumentException('Unknown invoice status');
} }
$this->invoiceRepository->saveInvoice($invoice); $this->invoiceRepository->saveInvoice($invoice);
@ -363,12 +366,12 @@ final class ServiceInvoice
if ($renderer->supports($document)) { if ($renderer->supports($document)) {
$dispatcher->dispatch(new InvoicePreRenderEvent($model, $document, $renderer)); $dispatcher->dispatch(new InvoicePreRenderEvent($model, $document, $renderer));
$response = $renderer->render($document, $model); if ($this->invoiceRepository->hasInvoice($model->getInvoiceNumber())) {
throw new DuplicateInvoiceNumberException($model->getInvoiceNumber());
if ($model->getQuery()->isMarkAsExported()) {
$this->markEntriesAsExported($model->getEntries());
} }
$response = $renderer->render($document, $model);
$event = new InvoicePostRenderEvent($model, $document, $renderer, $response); $event = new InvoicePostRenderEvent($model, $document, $renderer, $response);
$dispatcher->dispatch($event); $dispatcher->dispatch($event);
@ -379,6 +382,10 @@ final class ServiceInvoice
$invoice->setFilename($invoiceFilename); $invoice->setFilename($invoiceFilename);
$this->invoiceRepository->saveInvoice($invoice); $this->invoiceRepository->saveInvoice($invoice);
if ($model->getQuery()->isMarkAsExported()) {
$this->markEntriesAsExported($model->getEntries());
}
$dispatcher->dispatch(new InvoiceCreatedEvent($invoice)); $dispatcher->dispatch(new InvoiceCreatedEvent($invoice));
return $invoice; return $invoice;

View file

@ -27,6 +27,7 @@ use App\Saml\Security\SamlFactory;
use App\Timesheet\CalculatorInterface as TimesheetCalculator; use App\Timesheet\CalculatorInterface as TimesheetCalculator;
use App\Timesheet\Rounding\RoundingInterface; use App\Timesheet\Rounding\RoundingInterface;
use App\Timesheet\TrackingMode\TrackingModeInterface; use App\Timesheet\TrackingMode\TrackingModeInterface;
use App\Validator\Constraints\ProjectConstraint;
use App\Validator\Constraints\TimesheetConstraint; use App\Validator\Constraints\TimesheetConstraint;
use App\Widget\WidgetInterface; use App\Widget\WidgetInterface;
use App\Widget\WidgetRendererInterface; use App\Widget\WidgetRendererInterface;
@ -60,6 +61,7 @@ class Kernel extends BaseKernel
public const TAG_TIMESHEET_EXPORTER = 'timesheet.exporter'; public const TAG_TIMESHEET_EXPORTER = 'timesheet.exporter';
public const TAG_TIMESHEET_TRACKING_MODE = 'timesheet.tracking_mode'; public const TAG_TIMESHEET_TRACKING_MODE = 'timesheet.tracking_mode';
public const TAG_TIMESHEET_ROUNDING_MODE = 'timesheet.rounding_mode'; public const TAG_TIMESHEET_ROUNDING_MODE = 'timesheet.rounding_mode';
public const TAG_PROJECT_VALIDATOR = 'project.validator';
public function getCacheDir() public function getCacheDir()
{ {
@ -87,6 +89,7 @@ class Kernel extends BaseKernel
$container->registerForAutoconfiguration(TrackingModeInterface::class)->addTag(self::TAG_TIMESHEET_TRACKING_MODE); $container->registerForAutoconfiguration(TrackingModeInterface::class)->addTag(self::TAG_TIMESHEET_TRACKING_MODE);
$container->registerForAutoconfiguration(RoundingInterface::class)->addTag(self::TAG_TIMESHEET_ROUNDING_MODE); $container->registerForAutoconfiguration(RoundingInterface::class)->addTag(self::TAG_TIMESHEET_ROUNDING_MODE);
$container->registerForAutoconfiguration(TimesheetConstraint::class)->addTag(self::TAG_TIMESHEET_VALIDATOR); $container->registerForAutoconfiguration(TimesheetConstraint::class)->addTag(self::TAG_TIMESHEET_VALIDATOR);
$container->registerForAutoconfiguration(ProjectConstraint::class)->addTag(self::TAG_PROJECT_VALIDATOR);
/** @var SecurityExtension $extension */ /** @var SecurityExtension $extension */
$extension = $container->getExtension('security'); $extension = $container->getExtension('security');

View file

@ -75,9 +75,11 @@ class LdapUserHydrator
} }
// fill them after hydrating account, so they can't be overwritten // fill them after hydrating account, so they can't be overwritten
$user->setPassword(''); // by the mapping attributes
if ($user->getId() === null) {
$user->setPassword('');
}
$user->setAuth(User::AUTH_LDAP); $user->setAuth(User::AUTH_LDAP);
$user->setPreferenceValue('ldap.dn', $ldapEntry['dn']); $user->setPreferenceValue('ldap.dn', $ldapEntry['dn']);
} }

View file

@ -152,7 +152,7 @@ class ProjectStatisticService
->setParameter('end', $end, Types::DATETIME_MUTABLE) ->setParameter('end', $end, Types::DATETIME_MUTABLE)
; ;
if ($query->isOnlyWithRecords()) { if (!$query->isIncludeNoWork()) {
$qb2 = $this->repository->createQueryBuilder('t1'); $qb2 = $this->repository->createQueryBuilder('t1');
$qb2 $qb2
->select('1') ->select('1')
@ -163,15 +163,28 @@ class ProjectStatisticService
$qb->andWhere($qb->expr()->exists($qb2)); $qb->andWhere($qb->expr()->exists($qb2));
} }
if (!$query->isIncludeNoBudget()) { if ($query->isIncludeNoBudget()) {
$qb $qb->andWhere(
->andWhere( $qb->expr()->eq('p.budget', 0.0),
$qb->expr()->orX( $qb->expr()->eq('p.timeBudget', 0)
$qb->expr()->gt('p.budget', 0.0), );
$qb->expr()->gt('p.timeBudget', 0) } else {
) $qb->andWhere(
$qb->expr()->orX(
$qb->expr()->gt('p.budget', 0.0),
$qb->expr()->gt('p.timeBudget', 0)
) )
; );
if ($query->isBudgetTypeMonthly()) {
$qb->andWhere(
$qb->expr()->eq('p.budgetType', ':typeMonth')
);
$qb->setParameter('typeMonth', 'month');
} else {
$qb->andWhere(
$qb->expr()->isNull('p.budgetType')
);
}
} }
if ($query->getCustomer() !== null) { if ($query->getCustomer() !== null) {

View file

@ -13,6 +13,7 @@ use App\Form\Type\CustomerType;
use App\Form\Type\MonthPickerType; use App\Form\Type\MonthPickerType;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
@ -45,9 +46,20 @@ class ProjectDateRangeForm extends AbstractType
'model_timezone' => $options['timezone'], 'model_timezone' => $options['timezone'],
]); ]);
$builder->add('includeNoBudget', CheckboxType::class, [ $builder->add('includeNoWork', CheckboxType::class, [
'required' => false, 'required' => false,
'label' => 'label.includeNoBudget', 'label' => 'label.includeNoWork',
]);
$builder->add('budgetType', ChoiceType::class, [
'required' => true,
'multiple' => false,
'expanded' => true,
'choices' => [
'label.includeNoBudget' => 'none',
'label.includeBudgetType_full' => 'full',
'label.includeBudgetType_month' => 'month',
],
]); ]);
} }

View file

@ -27,8 +27,8 @@ final class ProjectDateRangeQuery
*/ */
private $customer; private $customer;
private $includeNoBudget = false; private $includeNoWork = true;
private $onlyWithRecords = false; private $budgetType = 'month';
public function __construct(\DateTime $month, User $user) public function __construct(\DateTime $month, User $user)
{ {
@ -38,22 +38,17 @@ final class ProjectDateRangeQuery
public function isIncludeNoBudget(): bool public function isIncludeNoBudget(): bool
{ {
return $this->includeNoBudget; return $this->budgetType === 'none';
} }
public function setIncludeNoBudget(bool $includeNoBudget): void public function isIncludeNoWork(): bool
{ {
$this->includeNoBudget = $includeNoBudget; return $this->includeNoWork;
} }
public function isOnlyWithRecords(): bool public function setIncludeNoWork(bool $includeNoWork): void
{ {
return $this->onlyWithRecords; $this->includeNoWork = $includeNoWork;
}
public function setOnlyWithRecords(bool $onlyWithRecords): void
{
$this->onlyWithRecords = $onlyWithRecords;
} }
public function getUser(): ?User public function getUser(): ?User
@ -61,12 +56,12 @@ final class ProjectDateRangeQuery
return $this->user; return $this->user;
} }
public function getMonth(): \DateTime public function getMonth(): ?\DateTime
{ {
return $this->month; return $this->month;
} }
public function setMonth(\DateTime $month): void public function setMonth(?\DateTime $month): void
{ {
$this->month = $month; $this->month = $month;
} }
@ -80,4 +75,19 @@ final class ProjectDateRangeQuery
{ {
$this->customer = $customer; $this->customer = $customer;
} }
public function isBudgetTypeMonthly(): bool
{
return $this->budgetType === 'month';
}
public function getBudgetType(): ?string
{
return $this->budgetType;
}
public function setBudgetType(?string $budgetType): void
{
$this->budgetType = $budgetType;
}
} }

View file

@ -28,6 +28,7 @@ final class SamlUserFactory
$user = new User(); $user = new User();
$user->setEnabled(true); $user->setEnabled(true);
$user->setUsername($token->getUsername()); $user->setUsername($token->getUsername());
$user->setPassword('');
$this->hydrateUser($user, $token); $this->hydrateUser($user, $token);
@ -73,8 +74,11 @@ final class SamlUserFactory
} }
// fill them after hydrating account, so they can't be overwritten // fill them after hydrating account, so they can't be overwritten
// by the mapping attributes
if ($user->getId() === null) {
$user->setPassword('');
}
$user->setUsername($token->getUsername()); $user->setUsername($token->getUsername());
$user->setPassword('');
$user->setAuth(User::AUTH_SAML); $user->setAuth(User::AUTH_SAML);
} }

View file

@ -19,9 +19,8 @@ use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator; use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface;
class TokenAuthenticator extends AbstractGuardAuthenticator implements PasswordAuthenticatedInterface class TokenAuthenticator extends AbstractGuardAuthenticator
{ {
public const HEADER_USERNAME = 'X-AUTH-USER'; public const HEADER_USERNAME = 'X-AUTH-USER';
public const HEADER_TOKEN = 'X-AUTH-TOKEN'; public const HEADER_TOKEN = 'X-AUTH-TOKEN';
@ -158,13 +157,4 @@ class TokenAuthenticator extends AbstractGuardAuthenticator implements PasswordA
{ {
return false; return false;
} }
public function getPassword($credentials): ?string
{
if (!\is_array($credentials) || !\array_key_exists('token', $credentials) || empty($credentials['token'])) {
return null;
}
return $credentials['token'];
}
} }

View file

@ -15,6 +15,7 @@ use App\Entity\User;
use App\Utils\LocaleFormats; use App\Utils\LocaleFormats;
use App\Utils\LocaleFormatter; use App\Utils\LocaleFormatter;
use DateTime; use DateTime;
use Symfony\Component\Security\Core\Security;
use Twig\Extension\AbstractExtension; use Twig\Extension\AbstractExtension;
use Twig\TwigFilter; use Twig\TwigFilter;
use Twig\TwigFunction; use Twig\TwigFunction;
@ -22,10 +23,9 @@ use Twig\TwigTest;
final class LocaleFormatExtensions extends AbstractExtension final class LocaleFormatExtensions extends AbstractExtension
{ {
/**
* @var LanguageFormattings|null
*/
private $formats; private $formats;
private $security;
/** /**
* @var LocaleFormats|null * @var LocaleFormats|null
*/ */
@ -38,10 +38,12 @@ final class LocaleFormatExtensions extends AbstractExtension
* @var string * @var string
*/ */
private $locale; private $locale;
private $userFormat;
public function __construct(LanguageFormattings $formats) public function __construct(LanguageFormattings $formats, Security $security)
{ {
$this->formats = $formats; $this->formats = $formats;
$this->security = $security;
} }
/** /**
@ -164,11 +166,23 @@ final class LocaleFormatExtensions extends AbstractExtension
/** /**
* @param DateTime|string $date * @param DateTime|string $date
* @param bool $stripMidnight
* @return bool|false|string * @return bool|false|string
*/ */
public function dateTimeFull($date) public function dateTimeFull($date, bool $stripMidnight = false)
{ {
return $this->getFormatter()->dateTimeFull($date); return $this->getFormatter()->dateTimeFull($date, $this->getUserTimeFormat(), $stripMidnight);
}
private function getUserTimeFormat(): string
{
if ($this->userFormat === null) {
/** @var User|null $user */
$user = $this->security->getUser();
$this->userFormat = $user !== null ? $user->getTimeFormat() : 'H:i';
}
return $this->userFormat;
} }
public function createDate(string $date, ?User $user = null): \DateTime public function createDate(string $date, ?User $user = null): \DateTime
@ -200,7 +214,7 @@ final class LocaleFormatExtensions extends AbstractExtension
*/ */
public function time($date) public function time($date)
{ {
return $this->getFormatter()->time($date); return $this->getFormatter()->time($date, $this->getUserTimeFormat());
} }
/** /**
@ -239,7 +253,16 @@ final class LocaleFormatExtensions extends AbstractExtension
*/ */
public function hour24($twentyFour, $twelveHour) public function hour24($twentyFour, $twelveHour)
{ {
return $this->getFormatter()->hour24($twentyFour, $twelveHour); @trigger_error('Twig filter "hour24" is deprecated, use app.user.is24Hour() instead', E_USER_DEPRECATED);
/** @var User|null $user */
$user = $this->security->getUser();
if (null === $user) {
return true;
}
return $user->is24Hour();
} }
public function getDurationFormat(): string public function getDurationFormat(): string

View file

@ -0,0 +1,33 @@
<?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\Utils;
class DateFormatConverter
{
/**
* This defines the mapping between PHP date format (key) and ICU date format (value).
* https://unicode-org.github.io/icu/userguide/format_parse/datetime/#datetime-format-syntax
*
* @var array
*/
private static $formatConvertRules = [
// hours
'h' => 'hh', 'H' => 'HH',
// minutes
'i' => 'mm',
// am/pm to AM/PM
'A' => 'a'
];
public function convert(string $format): string
{
return strtr($format, self::$formatConvertRules);
}
}

View file

@ -75,21 +75,12 @@ class LocaleFormats
/** /**
* Returns the format which is used by the form component to handle datetime values. * Returns the format which is used by the form component to handle datetime values.
* *
* @deprecated since 1.16
* @return string * @return string
*/ */
public function getDateTimeTypeFormat(): string public function getDateTimeTypeFormat(): string
{ {
return $this->formats->getDateTimeTypeFormat($this->getLocale()); return $this->formats->getDateTypeFormat($this->getLocale()) . ' HH:mm';
}
/**
* Returns the format which is used by the Javascript component to handle datetime values.
*
* @return string
*/
public function getDateTimePickerFormat(): string
{
return $this->formats->getDateTimePickerFormat($this->getLocale());
} }
/** /**
@ -131,14 +122,4 @@ class LocaleFormats
{ {
return $this->formats->getDurationFormat($this->getLocale()); return $this->formats->getDurationFormat($this->getLocale());
} }
/**
* Returns whether this locale uses the 24 hour format.
*
* @return bool
*/
public function isTwentyFourHours(): bool
{
return $this->formats->isTwentyFourHours($this->getLocale());
}
} }

View file

@ -46,6 +46,10 @@ final class LocaleFormatter
* @var string * @var string
*/ */
private $dateTimeFormat = null; private $dateTimeFormat = null;
/**
* @var string
*/
private $dateTypeFormat = null;
/** /**
* @var string * @var string
*/ */
@ -54,10 +58,6 @@ final class LocaleFormatter
* @var string * @var string
*/ */
private $timeFormat = null; private $timeFormat = null;
/**
* @var bool
*/
private $isTwentyFourHour = null;
public function __construct(LanguageFormattings $formats, string $locale) public function __construct(LanguageFormattings $formats, string $locale)
{ {
@ -216,6 +216,15 @@ final class LocaleFormatter
return $date->format($this->dateFormat); return $date->format($this->dateFormat);
} }
private function getDateTypeFormat(): string
{
if (null === $this->dateTypeFormat) {
$this->dateTypeFormat = $this->localeFormats->getDateTypeFormat();
}
return $this->dateTypeFormat;
}
/** /**
* @param DateTime|string $date * @param DateTime|string $date
* @return string * @return string
@ -239,12 +248,15 @@ final class LocaleFormatter
/** /**
* @param DateTime|string $date * @param DateTime|string $date
* @param string $timeFormat
* @param bool $stripMidnight
* @return bool|false|string * @return bool|false|string
*/ */
public function dateTimeFull($date) public function dateTimeFull($date, string $timeFormat, bool $stripMidnight = false)
{ {
if (null === $this->dateTimeTypeFormat) { if (null === $this->dateTimeTypeFormat) {
$this->dateTimeTypeFormat = $this->localeFormats->getDateTimeTypeFormat(); $converter = new DateFormatConverter();
$this->dateTimeTypeFormat = $this->getDateTypeFormat() . ' ' . $converter->convert($timeFormat);
} }
if (!$date instanceof DateTime) { if (!$date instanceof DateTime) {
@ -255,13 +267,19 @@ final class LocaleFormatter
} }
} }
$format = $this->dateTimeTypeFormat;
if ($stripMidnight && $date->format('H') == '00' && $date->format('i') == '00') {
$format = $this->localeFormats->getDateTypeFormat();
}
$formatter = new IntlDateFormatter( $formatter = new IntlDateFormatter(
$this->locale, $this->locale,
IntlDateFormatter::MEDIUM, IntlDateFormatter::MEDIUM,
IntlDateFormatter::MEDIUM, IntlDateFormatter::MEDIUM,
date_default_timezone_get(), date_default_timezone_get(),
IntlDateFormatter::GREGORIAN, IntlDateFormatter::GREGORIAN,
$this->dateTimeTypeFormat $format
); );
return $formatter->format($date); return $formatter->format($date);
@ -291,7 +309,7 @@ final class LocaleFormatter
* @return string * @return string
* @throws Exception * @throws Exception
*/ */
public function time($date) public function time($date, string $format = null)
{ {
if (null === $this->timeFormat) { if (null === $this->timeFormat) {
$this->timeFormat = $this->localeFormats->getTimeFormat(); $this->timeFormat = $this->localeFormats->getTimeFormat();
@ -301,7 +319,7 @@ final class LocaleFormatter
$date = new DateTime($date); $date = new DateTime($date);
} }
return $date->format($this->timeFormat); return $date->format($format ?? $this->timeFormat);
} }
/** /**
@ -335,22 +353,4 @@ final class LocaleFormatter
{ {
return $this->formatIntl($dateTime, ($short ? 'EE' : 'EEEE')); return $this->formatIntl($dateTime, ($short ? 'EE' : 'EEEE'));
} }
/**
* @param mixed $twentyFour
* @param mixed $twelveHour
* @return mixed
*/
public function hour24($twentyFour, $twelveHour)
{
if (null === $this->isTwentyFourHour) {
$this->isTwentyFourHour = $this->localeFormats->isTwentyFourHours();
}
if (true === $this->isTwentyFourHour) {
return $twentyFour;
}
return $twelveHour;
}
} }

View file

@ -74,11 +74,23 @@ class MPdfConverter implements HtmlToPdfConverter
@ini_set('pcre.backtrack_limit', '1000000'); @ini_set('pcre.backtrack_limit', '1000000');
} }
// large amount of data take time
@ini_set('max_execution_time', '120');
// reduce the size of content parts that are passed to MPDF, to prevent // reduce the size of content parts that are passed to MPDF, to prevent
// https://mpdf.github.io/troubleshooting/known-issues.html#blank-pages-or-some-sections-missing // https://mpdf.github.io/troubleshooting/known-issues.html#blank-pages-or-some-sections-missing
$parts = explode('<pagebreak>', $html); $parts = explode('<pagebreak>', $html);
for ($i = 0; $i < \count($parts); $i++) { for ($i = 0; $i < \count($parts); $i++) {
$mpdf->WriteHTML($parts[$i]); if (stripos($parts[$i], '<!-- CONTENT_PART -->') !== false) {
$subParts = explode('<!-- CONTENT_PART -->', $parts[$i]);
$run = 0;
foreach ($subParts as $subPart) {
$mpdf->WriteHTML($subPart);
}
} else {
$mpdf->WriteHTML($parts[$i]);
}
if ($i < \count($parts) - 1) { if ($i < \count($parts) - 1) {
$mpdf->WriteHTML('<pagebreak>'); $mpdf->WriteHTML('<pagebreak>');
} }

View file

@ -18,8 +18,8 @@ class MomentFormatConverter
{ {
/** /**
* This defines the mapping between PHP ICU date format (key) and moment.js date format (value) * This defines the mapping between PHP ICU date format (key) and moment.js date format (value)
* For ICU formats see http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Time-Format-Syntax * For ICU formats see https://unicode-org.github.io/icu/userguide/format_parse/datetime/#datetime-format-syntax
* For Moment formats see http://momentjs.com/docs/#/displaying/format/. * For Moment formats see http://momentjs.com/docs/#/displaying/format/
* *
* @var array * @var array
*/ */
@ -34,6 +34,8 @@ class MomentFormatConverter
'ZZZZZ' => 'Z', 'ZZZ' => 'ZZ', 'ZZZZZ' => 'Z', 'ZZZ' => 'ZZ',
// letter 'T' // letter 'T'
'\'T\'' => 'T', '\'T\'' => 'T',
// am/pm to AM/PM
'a' => 'A',
]; ];
/** /**

View file

@ -0,0 +1,19 @@
<?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\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* Extend this class if you want to add dynamic project validation (eg. via a bundle).
*/
abstract class ProjectConstraint extends Constraint
{
}

View file

@ -18,6 +18,19 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class ProjectValidator extends ConstraintValidator class ProjectValidator extends ConstraintValidator
{ {
/**
* @var Constraint[]
*/
private $constraints;
/**
* @param Constraint[] $constraints
*/
public function __construct(iterable $constraints = [])
{
$this->constraints = $constraints;
}
/** /**
* @param Project|mixed $value * @param Project|mixed $value
* @param Constraint $constraint * @param Constraint $constraint
@ -33,6 +46,13 @@ class ProjectValidator extends ConstraintValidator
} }
$this->validateProject($value, $this->context); $this->validateProject($value, $this->context);
foreach ($this->constraints as $constraint) {
$this->context
->getValidator()
->inContext($this->context)
->validate($value, $constraint, [Constraint::DEFAULT_GROUP]);
}
} }
protected function validateProject(Project $project, ExecutionContextInterface $context) protected function validateProject(Project $project, ExecutionContextInterface $context)

View file

@ -296,7 +296,7 @@
login: '{{ path('fos_user_security_login') }}', login: '{{ path('fos_user_security_login') }}',
locale: '{{ app.request.locale }}', locale: '{{ app.request.locale }}',
first_dow_iso: {{ iso_day_by_name(app.user.firstDayOfWeek) }}, first_dow_iso: {{ iso_day_by_name(app.user.firstDayOfWeek) }},
twentyFourHours: {{ 'true'|hour24('false') }}, twentyFourHours: {{ app.user.is24Hour() ? 'true' : 'false' }},
autoComplete: {{ kimai_config.themeAutocompleteCharacters }}, autoComplete: {{ kimai_config.themeAutocompleteCharacters }},
defaultColor: '{{ constant('App\\Constants::DEFAULT_COLOR') }}', defaultColor: '{{ constant('App\\Constants::DEFAULT_COLOR') }}',
updateBrowserTitle: {% if app.user.preferenceValue('theme.update_browser_title') %}true{% else %}false{% endif %} updateBrowserTitle: {% if app.user.preferenceValue('theme.update_browser_title') %}true{% else %}false{% endif %}

View file

@ -80,7 +80,7 @@
{% block box_title %}Logfile (max. {{ logLines }} last lines){% endblock %} {% block box_title %}Logfile (max. {{ logLines }} last lines){% endblock %}
{% block box_tools %} {% block box_tools %}
{% if log_delete %} {% if log_delete %}
<a class="btn-box-tool confirmation-link" href="{{ path('doctor_flush_log') }}" data-question="confirm.delete"><i class="{{ 'delete'|icon }}"></i></a> <a class="btn-box-tool confirmation-link" href="{{ path('doctor_flush_log', {'token': csrf_token('doctor.flush_log')}) }}" data-question="confirm.delete"><i class="{{ 'delete'|icon }}"></i></a>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block box_body %} {% block box_body %}

View file

@ -140,6 +140,7 @@ mpdf-->
{% set totalInternalRate = 0 %} {% set totalInternalRate = 0 %}
{% set totalRate = 0 %} {% set totalRate = 0 %}
{% for id, summary in summaries %} {% for id, summary in summaries %}
<!-- CONTENT_PART -->
{% set totalDuration = totalDuration + summary.duration %} {% set totalDuration = totalDuration + summary.duration %}
{% set totalInternalRate = totalInternalRate + summary.rate_internal %} {% set totalInternalRate = totalInternalRate + summary.rate_internal %}
{% set totalRate = totalRate + summary.rate %} {% set totalRate = totalRate + summary.rate %}
@ -274,6 +275,7 @@ mpdf-->
</thead> </thead>
<tbody> <tbody>
{% for entry in entries %} {% for entry in entries %}
<!-- CONTENT_PART -->
{% set duration = duration + entry.duration %} {% set duration = duration + entry.duration %}
{% if currency is same as(false) %} {% if currency is same as(false) %}
{% set currency = entry.project.customer.currency %} {% set currency = entry.project.customer.currency %}

View file

@ -309,7 +309,7 @@
| |
<label class="control-label" for="begin-format"> <label class="control-label" for="begin-format">
{{ 'label.begin'|trans }}: {{ 'label.begin'|trans }}:
{% set demo_date = create_date('2020-01-01 13:00:00') %} {% set demo_date = create_date('2020-01-01 11:00:00') %}
<select id="begin-format" name="begin-format"> <select id="begin-format" name="begin-format">
<option value="plain">{{ demo_date|date_format('H:i') }}</option> <option value="plain">{{ demo_date|date_format('H:i') }}</option>
<option value="time">{{ demo_date|date_time }}</option> <option value="time">{{ demo_date|date_time }}</option>

View file

@ -235,6 +235,12 @@
document.addEventListener('kimai.initialized', function() { document.addEventListener('kimai.initialized', function() {
KimaiReloadPageWidget.create('kimai.systemConfigUpdate', true); KimaiReloadPageWidget.create('kimai.systemConfigUpdate', true);
{% if models|length > 0 %}
jQuery('body').on('change', '#{{ form.template.vars.id }}', function(event) {
document.getElementById('{{ form.vars.attr.id }}').submit();
});
{% endif %}
}); });
</script> </script>

View file

@ -36,7 +36,7 @@
{% else %} {% else %}
{{ tables.datatable_header(tableName, columns, query, {}) }} {{ tables.datatable_header(tableName, columns, query, {}) }}
{% for entry in entries %} {% for entry in entries %}
<tr class="alternative-link open-edit" data-href="{{ path('admin_invoice_download', {'id': entry.id}) }}"> <tr class="alternative-link open-edit{% if entry.canceled %} warning text-muted{% endif %}" data-href="{{ path('admin_invoice_download', {'id': entry.id}) }}">
<td class="{{ tables.data_table_column_class(tableName, columns, 'date') }}">{{ entry.createdAt|date_short }}</td> <td class="{{ tables.data_table_column_class(tableName, columns, 'date') }}">{{ entry.createdAt|date_short }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'user') }}">{{ widgets.user_avatar(entry.user) }} {{ widgets.username(entry.user) }}</td> <td class="{{ tables.data_table_column_class(tableName, columns, 'user') }}">{{ widgets.user_avatar(entry.user) }} {{ widgets.username(entry.user) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'customer') }}">{{ widgets.label_customer(entry.customer) }}</td> <td class="{{ tables.data_table_column_class(tableName, columns, 'customer') }}">{{ widgets.label_customer(entry.customer) }}</td>

View file

@ -12,12 +12,16 @@
{{ widgets.label('status.pending'|trans, 'warning') }} {{ widgets.label('status.pending'|trans, 'warning') }}
{% elseif invoice.paid %} {% elseif invoice.paid %}
{{ widgets.label('status.paid'|trans, 'success') }} {{ widgets.label('status.paid'|trans, 'success') }}
{% elseif invoice.canceled %}
{{ widgets.label('status.canceled'|trans, 'gray') }}
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}
{% macro invoice_due_date(invoice) %} {% macro invoice_due_date(invoice) %}
{% import "macros/widgets.html.twig" as widgets %} {% import "macros/widgets.html.twig" as widgets %}
{% if invoice.overdue and not invoice.paid %} {% if invoice.canceled %}
{{ widgets.label('status.canceled'|trans, 'gray') }}
{% elseif invoice.overdue and not invoice.paid %}
{{ widgets.label(invoice.dueDate|date_short, 'danger') }} {{ widgets.label(invoice.dueDate|date_short, 'danger') }}
{% else %} {% else %}
{{ widgets.label(invoice.dueDate|date_short, 'primary') }} {{ widgets.label(invoice.dueDate|date_short, 'primary') }}

View file

@ -102,6 +102,7 @@ mpdf-->
</thead> </thead>
<tbody> <tbody>
{% for id, entry in model.calculator.entries %} {% for id, entry in model.calculator.entries %}
<!-- CONTENT_PART -->
{% set duration = entry.duration|duration(isDecimal) %} {% set duration = entry.duration|duration(isDecimal) %}
{% if entry.fixedRate %} {% if entry.fixedRate %}
{% set rate = entry.fixedRate %} {% set rate = entry.fixedRate %}

View file

@ -109,6 +109,7 @@ mpdf-->
</thead> </thead>
<tbody> <tbody>
{% for entry in model.calculator.entries %} {% for entry in model.calculator.entries %}
<!-- CONTENT_PART -->
{% set duration = entry.duration|duration(isDecimal) %} {% set duration = entry.duration|duration(isDecimal) %}
{% if entry.fixedRate is not null %} {% if entry.fixedRate is not null %}
{% set rate = entry.fixedRate %} {% set rate = entry.fixedRate %}

View file

@ -478,15 +478,16 @@
{% elseif '\\LanguageType' in type %} {% elseif '\\LanguageType' in type %}
{{ value|language }} {{ value|language }}
{% elseif '\\MoneyType' in type %} {% elseif '\\MoneyType' in type %}
{% set classname = class_name(entity) %}
{% if entity is null %} {% if entity is null %}
{{ value }} {{ value }}
{% elseif class_name(entity) == 'App\\Entity\\Timesheet' %} {% elseif classname == 'App\\Entity\\Timesheet' %}
{{ value|money(entity.project.customer.currency) }} {{ value|money(entity.project.customer.currency) }}
{% elseif class_name(entity) == 'App\\Entity\\Customer' %} {% elseif classname == 'App\\Entity\\Customer' %}
{{ value|money(entity.currency) }} {{ value|money(entity.currency) }}
{% elseif class_name(entity) == 'App\\Entity\\Project' %} {% elseif classname == 'App\\Entity\\Project' %}
{{ value|money(entity.customer.currency) }} {{ value|money(entity.customer.currency) }}
{% elseif class_name(entity) == 'App\\Entity\\Activity' and entity.project is not null %} {% elseif classname == 'App\\Entity\\Activity' and entity.project is not null %}
{{ value|money(entity.project.customer.currency) }} {{ value|money(entity.project.customer.currency) }}
{% else %} {% else %}
{{ value }} {{ value }}

View file

@ -61,7 +61,7 @@
<tr> <tr>
<th>{{ 'label.orderDate'|trans }}</th> <th>{{ 'label.orderDate'|trans }}</th>
<td colspan="3"> <td colspan="3">
{{ project.orderDate|date_full }} {{ project.orderDate|date_full(true) }}
</td> </td>
</tr> </tr>
{% endif %} {% endif %}
@ -69,7 +69,7 @@
<tr> <tr>
<th>{{ 'label.project_start'|trans }}</th> <th>{{ 'label.project_start'|trans }}</th>
<td colspan="3"> <td colspan="3">
{{ project.start|date_full }} {{ project.start|date_full(true) }}
</td> </td>
</tr> </tr>
{% endif %} {% endif %}
@ -77,7 +77,7 @@
<tr> <tr>
<th>{{ 'label.project_end'|trans }}</th> <th>{{ 'label.project_end'|trans }}</th>
<td colspan="3"> <td colspan="3">
{{ project.end|date_full }} {{ project.end|date_full(true) }}
</td> </td>
</tr> </tr>
{% endif %} {% endif %}

View file

@ -51,9 +51,9 @@
<td class="{{ tables.data_table_column_class(tableName, columns, 'customer') }}">{{ widgets.label_customer(entry.customer) }}</td> <td class="{{ tables.data_table_column_class(tableName, columns, 'customer') }}">{{ widgets.label_customer(entry.customer) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment|comment1line }}</td> <td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment|comment1line }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'orderNumber') }}">{{ entry.orderNumber }}</td> <td class="{{ tables.data_table_column_class(tableName, columns, 'orderNumber') }}">{{ entry.orderNumber }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'orderDate') }}">{% if entry.orderDate is not null %}{{ entry.orderDate|date_full }}{% endif %}</td> <td class="{{ tables.data_table_column_class(tableName, columns, 'orderDate') }}">{% if entry.orderDate is not null %}{{ entry.orderDate|date_full(true) }}{% endif %}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'project_start') }}">{% if entry.start is not null %}{{ entry.start|date_full }}{% endif %}</td> <td class="{{ tables.data_table_column_class(tableName, columns, 'project_start') }}">{% if entry.start is not null %}{{ entry.start|date_full(true) }}{% endif %}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'project_end') }}">{% if entry.end is not null %}{{ entry.end|date_full }}{% endif %}</td> <td class="{{ tables.data_table_column_class(tableName, columns, 'project_end') }}">{% if entry.end is not null %}{{ entry.end|date_full(true) }}{% endif %}</td>
{% for field in metaColumns %} {% for field in metaColumns %}
<td class="{{ tables.data_table_column_class(tableName, columns, 'mf_' ~ field.name) }}"> <td class="{{ tables.data_table_column_class(tableName, columns, 'mf_' ~ field.name) }}">
{{ tables.datatable_meta_column(entry, field) }} {{ tables.datatable_meta_column(entry, field) }}

View file

@ -38,7 +38,26 @@
{{ widgets.action_button('visibility', {'modal': ('#modal_' ~ tableName), 'class': 'btn-sm'}) }} {{ widgets.action_button('visibility', {'modal': ('#modal_' ~ tableName), 'class': 'btn-sm'}) }}
{% endblock %} {% endblock %}
{% block box_title %} {% block box_title %}
{{ form_widget(form) }} {% if form.customer is defined %}
{{ form_widget(form.customer) }}
{% endif %}
{% if form.month is defined %}
{{ form_widget(form.month) }}
{% endif %}
<div class="btn-group">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="{{ 'filter'|icon }}"></i> <span class="caret"></span>
</button>
<ul class="dropdown-menu checkbox-menu">
<li>
{{ form_widget(form.includeNoWork) }}
</li>
<li>
{{ form_widget(form.budgetType) }}
</li>
</ul>
</div>
{{ form_rest(form) }}
{% endblock %} {% endblock %}
{% block box_body %} {% block box_body %}
{% if not hasData %} {% if not hasData %}

View file

@ -52,7 +52,32 @@
{{ widgets.action_button('visibility', {'modal': ('#modal_' ~ tableName), 'class': 'btn-sm'}) }} {{ widgets.action_button('visibility', {'modal': ('#modal_' ~ tableName), 'class': 'btn-sm'}) }}
{% endblock %} {% endblock %}
{% block box_title %} {% block box_title %}
{{ form_widget(form) }} {% if form.customer is defined %}
{{ form_widget(form.customer) }}
{% endif %}
{% if form.month is defined %}
{{ form_widget(form.month) }}
{% endif %}
{% if form.includeNoBudget is defined or form.includeNoWork is defined %}
<div class="btn-group">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="{{ 'filter'|icon }}"></i> <span class="caret"></span>
</button>
<ul class="dropdown-menu checkbox-menu">
{% if form.includeNoBudget is defined %}
<li>
{{ form_widget(form.includeNoBudget) }}
</li>
{% endif %}
{% if form.includeNoWork is defined %}
<li>
{{ form_widget(form.includeNoWork) }}
</li>
{% endif %}
</ul>
</div>
{% endif %}
{{ form_rest(form) }}
{% endblock %} {% endblock %}
{% block box_body %} {% block box_body %}
{% if not hasData %} {% if not hasData %}

View file

@ -29,13 +29,13 @@ class ConfigurationControllerTest extends APIControllerBaseTest
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals(8, \count($result)); $this->assertEquals(7, \count($result));
$this->assertI18nStructure($result); $this->assertI18nStructure($result);
} }
protected function assertI18nStructure(array $result) protected function assertI18nStructure(array $result)
{ {
$expectedKeys = ['date', 'dateTime', 'duration', 'formDate', 'formDateTime', 'is24hours', 'time', 'now']; $expectedKeys = ['date', 'dateTime', 'duration', 'formDate', 'is24hours', 'time', 'now'];
$actual = array_keys($result); $actual = array_keys($result);
sort($actual); sort($actual);
sort($expectedKeys); sort($expectedKeys);

View file

@ -26,7 +26,6 @@ class I18nConfigTest extends TestCase
$this->assertInstanceOf(I18nConfig::class, $sut->setDate('bar')); $this->assertInstanceOf(I18nConfig::class, $sut->setDate('bar'));
$this->assertInstanceOf(I18nConfig::class, $sut->setDateTime('hello')); $this->assertInstanceOf(I18nConfig::class, $sut->setDateTime('hello'));
$this->assertInstanceOf(I18nConfig::class, $sut->setFormDate('world')); $this->assertInstanceOf(I18nConfig::class, $sut->setFormDate('world'));
$this->assertInstanceOf(I18nConfig::class, $sut->setFormDateTime('testing'));
$this->assertInstanceOf(I18nConfig::class, $sut->setTime('fun')); $this->assertInstanceOf(I18nConfig::class, $sut->setTime('fun'));
} }
} }

View file

@ -26,61 +26,50 @@ class LanguageFormattingsTest extends TestCase
{ {
return [ return [
'de' => [ 'de' => [
'date_time_type' => 'dd.MM.yyyy HH:mm',
'date_type' => 'dd.MM.yyyy', 'date_type' => 'dd.MM.yyyy',
'date' => 'd.m.Y', 'date' => 'd.m.Y',
'date_time' => 'd.m. H:i', 'date_time' => 'd.m. H:i',
'duration' => '%h:%m h', 'duration' => '%h:%m h',
'time' => 'H:i', 'time' => 'H:i',
'24_hours' => true,
], ],
'en' => [ 'en' => [
'date_time_type' => 'yyyy-MM-dd HH:mm',
'date_type' => 'yyyy-MM-dd', 'date_type' => 'yyyy-MM-dd',
'date' => 'Y-m-d', 'date' => 'Y-m-d',
'date_time' => 'm-d H:i', 'date_time' => 'm-d H:i',
'duration' => '%h:%m h', 'duration' => '%h:%m h',
'time' => 'H:i:s', 'time' => 'H:i:s',
'24_hours' => false,
], ],
'pt_BR' => [ 'pt_BR' => [
'date_time_type' => 'dd-MM-yyyy HH:mm',
'date_type' => 'dd-MM-yyyy', 'date_type' => 'dd-MM-yyyy',
'date' => 'd-m-Y', 'date' => 'd-m-Y',
'duration' => '%h:%m h', 'duration' => '%h:%m h',
], ],
'it' => [ 'it' => [
'date_time_type' => 'dd.MM.yyyy HH:mm',
'date_type' => 'dd.MM.yyyy', 'date_type' => 'dd.MM.yyyy',
'date' => 'd.m.Y', 'date' => 'd.m.Y',
'duration' => '%h:%m h', 'duration' => '%h:%m h',
], ],
'fr' => [ 'fr' => [
'date_time_type' => 'dd/MM/yyyy HH:mm',
'date_type' => 'dd/MM/yyyy', 'date_type' => 'dd/MM/yyyy',
'date' => 'd/m/Y', 'date' => 'd/m/Y',
'duration' => '%h h %m', 'duration' => '%h h %m',
], ],
'es' => [ 'es' => [
'date_time_type' => 'dd.MM.yyyy HH:mm',
'date_type' => 'dd.MM.yyyy', 'date_type' => 'dd.MM.yyyy',
'date' => 'd.m.Y', 'date' => 'd.m.Y',
'duration' => '%h:%m h', 'duration' => '%h:%m h',
], ],
'ru' => [ 'ru' => [
'date_time_type' => 'dd.MM.yyyy HH:mm',
'date_type' => 'dd.MM.yyyy', 'date_type' => 'dd.MM.yyyy',
'date' => 'd.m.Y', 'date' => 'd.m.Y',
'duration' => '%h:%m h', 'duration' => '%h:%m h',
], ],
'ar' => [ 'ar' => [
'date_time_type' => 'yyyy-MM-dd HH:mm',
'date_type' => 'yyyy-MM-dd', 'date_type' => 'yyyy-MM-dd',
'date' => 'Y-m-d', 'date' => 'Y-m-d',
'duration' => '%h:%m h', 'duration' => '%h:%m h',
], ],
'hu' => [ 'hu' => [
'date_time_type' => 'yyyy.MM.dd HH:mm',
'date_type' => 'yyyy.MM.dd', 'date_type' => 'yyyy.MM.dd',
'date' => 'Y.m.d.', 'date' => 'Y.m.d.',
'duration' => '%h:%m h', 'duration' => '%h:%m h',
@ -136,40 +125,10 @@ class LanguageFormattingsTest extends TestCase
$this->assertEquals('DD.MM.YYYY', $sut->getDatePickerFormat('de')); $this->assertEquals('DD.MM.YYYY', $sut->getDatePickerFormat('de'));
} }
public function testGetDateTimeTypeFormat()
{
$sut = $this->getSut($this->getDefaultSettings());
$this->assertEquals('dd.MM.yyyy HH:mm', $sut->getDateTimeTypeFormat('de'));
}
public function testGetDateTimePickerFormat()
{
$sut = $this->getSut($this->getDefaultSettings());
$this->assertEquals('DD.MM.YYYY HH:mm', $sut->getDateTimePickerFormat('de'));
}
public function testIs24Hours()
{
$sut = $this->getSut($this->getDefaultSettings());
$this->assertTrue($sut->isTwentyFourHours('de'));
$this->assertFalse($sut->isTwentyFourHours('en'));
}
public function testGetTimeFormat() public function testGetTimeFormat()
{ {
$sut = $this->getSut($this->getDefaultSettings()); $sut = $this->getSut($this->getDefaultSettings());
$this->assertEquals('H:i', $sut->getTimeFormat('de')); $this->assertEquals('H:i', $sut->getTimeFormat('de'));
$this->assertEquals('H:i:s', $sut->getTimeFormat('en')); $this->assertEquals('H:i:s', $sut->getTimeFormat('en'));
} }
public function testUnknownSetting()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Unknown setting for locale en: date_time_type');
$sut = $this->getSut(['en' => [
'xxx' => 'dd.MM.yyyy HH:mm',
]]);
$sut->getDateTimePickerFormat('en');
}
} }

View file

@ -35,7 +35,7 @@ class PermissionControllerTest extends ControllerBaseTest
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN); $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
$this->assertAccessIsGranted($client, '/admin/permissions'); $this->assertAccessIsGranted($client, '/admin/permissions');
$this->assertHasDataTable($client); $this->assertHasDataTable($client);
$this->assertDataTableRowCount($client, 'datatable_user_admin_permissions', 119); $this->assertDataTableRowCount($client, 'datatable_user_admin_permissions', 121);
$this->assertPageActions($client, [ $this->assertPageActions($client, [
//'back' => $this->createUrl('/admin/user/'), //'back' => $this->createUrl('/admin/user/'),
'create modal-ajax-form' => $this->createUrl('/admin/permissions/roles/create'), 'create modal-ajax-form' => $this->createUrl('/admin/permissions/roles/create'),

View file

@ -430,7 +430,7 @@ class ProfileControllerTest extends ControllerBaseTest
2 => ['name' => UserPreference::TIMEZONE, 'value' => 'America/Creston'], 2 => ['name' => UserPreference::TIMEZONE, 'value' => 'America/Creston'],
3 => ['name' => UserPreference::LOCALE, 'value' => 'ar'], 3 => ['name' => UserPreference::LOCALE, 'value' => 'ar'],
4 => ['name' => UserPreference::FIRST_WEEKDAY, 'value' => 'sunday'], 4 => ['name' => UserPreference::FIRST_WEEKDAY, 'value' => 'sunday'],
5 => ['name' => UserPreference::SKIN, 'value' => 'blue'], 6 => ['name' => UserPreference::SKIN, 'value' => 'blue'],
] ]
] ]
]); ]);

View file

@ -9,6 +9,7 @@
namespace App\Tests\Controller\Reporting; namespace App\Tests\Controller\Reporting;
use App\Entity\Project;
use App\Entity\User; use App\Entity\User;
use App\Tests\Controller\ControllerBaseTest; use App\Tests\Controller\ControllerBaseTest;
use App\Tests\DataFixtures\ActivityFixtures; use App\Tests\DataFixtures\ActivityFixtures;
@ -39,6 +40,9 @@ class ProjectDateRangeControllerTest extends ControllerBaseTest
$projects->setCustomers($customers); $projects->setCustomers($customers);
$projects->setAmount(2); $projects->setAmount(2);
$projects->setIsVisible(true); $projects->setIsVisible(true);
$projects->setCallback(function (Project $project) {
$project->setIsMonthlyBudget();
});
$this->importFixture($projects); $this->importFixture($projects);
$activities = new ActivityFixtures(); $activities = new ActivityFixtures();

View file

@ -49,6 +49,7 @@ class UserTest extends TestCase
self::assertFalse($user->canSeeAllData()); self::assertFalse($user->canSeeAllData());
self::assertFalse($user->isSmallLayout()); self::assertFalse($user->isSmallLayout());
self::assertFalse($user->isExportDecimal()); self::assertFalse($user->isExportDecimal());
self::assertTrue($user->is24Hour());
$user->setAvatar('https://www.gravatar.com/avatar/00000000000000000000000000000000?d=retro&f=y'); $user->setAvatar('https://www.gravatar.com/avatar/00000000000000000000000000000000?d=retro&f=y');
self::assertEquals('https://www.gravatar.com/avatar/00000000000000000000000000000000?d=retro&f=y', $user->getAvatar()); self::assertEquals('https://www.gravatar.com/avatar/00000000000000000000000000000000?d=retro&f=y', $user->getAvatar());
@ -481,4 +482,15 @@ class UserTest extends TestCase
$member->setUser(new User()); $member->setUser(new User());
$sut->addMembership($member); $sut->addMembership($member);
} }
public function test24Hour()
{
$user = new User();
self::assertTrue($user->is24Hour());
self::assertEquals('H:i', $user->getTimeFormat());
$user->setPreferenceValue(UserPreference::HOUR_24, false);
self::assertFalse($user->is24Hour());
self::assertEquals('h:i A', $user->getTimeFormat());
}
} }

View file

@ -30,6 +30,7 @@ class UserPreferenceSubscriberTest extends TestCase
'language', 'language',
'first_weekday', 'first_weekday',
'skin', 'skin',
'hours_24',
'theme.layout', 'theme.layout',
'theme.collapsed_sidebar', 'theme.collapsed_sidebar',
'theme.update_browser_title', 'theme.update_browser_title',

View file

@ -34,6 +34,7 @@ use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
abstract class AbstractRendererTest extends KernelTestCase abstract class AbstractRendererTest extends KernelTestCase
@ -52,8 +53,11 @@ abstract class AbstractRendererTest extends KernelTestCase
] ]
]; ];
$security = $this->createMock(Security::class);
$security->expects($this->any())->method('getUser')->willReturn(new User());
$translator = $this->createMock(TranslatorInterface::class); $translator = $this->createMock(TranslatorInterface::class);
$dateExtension = new LocaleFormatExtensions(new LanguageFormattings($languages)); $dateExtension = new LocaleFormatExtensions(new LanguageFormattings($languages), $security);
$dispatcher = new EventDispatcher(); $dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new MetaFieldColumnSubscriber()); $dispatcher->addSubscriber(new MetaFieldColumnSubscriber());

View file

@ -10,10 +10,14 @@
namespace App\Tests\Export\Renderer; namespace App\Tests\Export\Renderer;
use App\Activity\ActivityStatisticService; use App\Activity\ActivityStatisticService;
use App\Entity\User;
use App\Export\Renderer\HtmlRenderer; use App\Export\Renderer\HtmlRenderer;
use App\Project\ProjectStatisticService; use App\Project\ProjectStatisticService;
use Symfony\Bridge\Twig\AppVariable;
use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Twig\Environment; use Twig\Environment;
/** /**
@ -43,6 +47,16 @@ class HtmlRendererTest extends AbstractRendererTest
$kernel = self::bootKernel(); $kernel = self::bootKernel();
/** @var Environment $twig */ /** @var Environment $twig */
$twig = $kernel->getContainer()->get('twig'); $twig = $kernel->getContainer()->get('twig');
$token = $this->createMock(TokenInterface::class);
$token->expects($this->any())->method('getUser')->willReturn(new User());
$tokenStorage = new TokenStorage();
$tokenStorage->setToken($token);
/** @var AppVariable $app */
$app = $twig->getGlobals()['app'];
$twig->addGlobal('app', $app);
$app->setTokenStorage($tokenStorage);
$stack = $kernel->getContainer()->get('request_stack'); $stack = $kernel->getContainer()->get('request_stack');
$request = new Request(); $request = new Request();
$request->setLocale('en'); $request->setLocale('en');

View file

@ -33,6 +33,7 @@ use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
abstract class AbstractRendererTest extends KernelTestCase abstract class AbstractRendererTest extends KernelTestCase
@ -51,8 +52,11 @@ abstract class AbstractRendererTest extends KernelTestCase
] ]
]; ];
$security = $this->createMock(Security::class);
$security->expects($this->any())->method('getUser')->willReturn(new User());
$translator = $this->getMockBuilder(TranslatorInterface::class)->getMock(); $translator = $this->getMockBuilder(TranslatorInterface::class)->getMock();
$dateExtension = new LocaleFormatExtensions(new LanguageFormattings($languages)); $dateExtension = new LocaleFormatExtensions(new LanguageFormattings($languages), $security);
$dispatcher = new EventDispatcher(); $dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new MetaFieldColumnSubscriber()); $dispatcher->addSubscriber(new MetaFieldColumnSubscriber());

View file

@ -28,8 +28,11 @@ class ProjectDateRangeQueryTest extends TestCase
self::assertEquals($date->getTimestamp(), $sut->getMonth()->getTimestamp()); self::assertEquals($date->getTimestamp(), $sut->getMonth()->getTimestamp());
self::assertSame($user, $sut->getUser()); self::assertSame($user, $sut->getUser());
self::assertNull($sut->getCustomer()); self::assertNull($sut->getCustomer());
self::assertFalse($sut->isOnlyWithRecords()); self::assertTrue($sut->isIncludeNoWork());
self::assertEquals('month', $sut->getBudgetType());
self::assertFalse($sut->isIncludeNoBudget()); self::assertFalse($sut->isIncludeNoBudget());
self::assertTrue($sut->isBudgetTypeMonthly());
} }
public function testSetterGetter() public function testSetterGetter()
@ -41,12 +44,20 @@ class ProjectDateRangeQueryTest extends TestCase
$sut->setMonth($date); $sut->setMonth($date);
$sut->setCustomer($customer); $sut->setCustomer($customer);
$sut->setIncludeNoBudget(true); $sut->setIncludeNoWork(false);
$sut->setOnlyWithRecords(true);
self::assertEquals($date->getTimestamp(), $sut->getMonth()->getTimestamp()); self::assertEquals($date->getTimestamp(), $sut->getMonth()->getTimestamp());
self::assertSame($customer, $sut->getCustomer()); self::assertSame($customer, $sut->getCustomer());
self::assertTrue($sut->isOnlyWithRecords()); self::assertFalse($sut->isIncludeNoWork());
$sut->setBudgetType('none');
self::assertEquals('none', $sut->getBudgetType());
self::assertTrue($sut->isIncludeNoBudget()); self::assertTrue($sut->isIncludeNoBudget());
self::assertFalse($sut->isBudgetTypeMonthly());
$sut->setBudgetType('full');
self::assertEquals('full', $sut->getBudgetType());
self::assertFalse($sut->isBudgetTypeMonthly());
self::assertFalse($sut->isIncludeNoBudget());
} }
} }

View file

@ -42,22 +42,6 @@ class TokenAuthenticatorTest extends TestCase
self::assertFalse($sut->supports($request)); self::assertFalse($sut->supports($request));
} }
public function testGetPassword()
{
$factory = $this->createMock(EncoderFactoryInterface::class);
$sut = new TokenAuthenticator($factory);
self::assertNull($sut->getPassword('asdfgh'));
self::assertNull($sut->getPassword(null));
self::assertNull($sut->getPassword([]));
self::assertNull($sut->getPassword(['password' => '1234567890']));
self::assertNull($sut->getPassword(['token' => null]));
self::assertNull($sut->getPassword(['token' => 0]));
self::assertNull($sut->getPassword(['token' => '']));
self::assertNull($sut->getPassword(['token' => false]));
self::assertEquals('foo-bar', $sut->getPassword(['token' => 'foo-bar']));
}
public function testGetCredentials() public function testGetCredentials()
{ {
$factory = $this->createMock(EncoderFactoryInterface::class); $factory = $this->createMock(EncoderFactoryInterface::class);

View file

@ -15,6 +15,7 @@ use App\Entity\User;
use App\Twig\LocaleFormatExtensions; use App\Twig\LocaleFormatExtensions;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\Intl\Util\IntlTestHelper; use Symfony\Component\Intl\Util\IntlTestHelper;
use Symfony\Component\Security\Core\Security;
use Twig\TwigFilter; use Twig\TwigFilter;
use Twig\TwigFunction; use Twig\TwigFunction;
use Twig\TwigTest; use Twig\TwigTest;
@ -43,7 +44,11 @@ class LocaleFormatExtensionsTest extends TestCase
$language = $dateSettings; $language = $dateSettings;
$dateSettings = $locale; $dateSettings = $locale;
} }
$sut = new LocaleFormatExtensions(new LanguageFormattings($dateSettings));
$security = $this->createMock(Security::class);
$security->expects($this->any())->method('getUser')->willReturn(new User());
$sut = new LocaleFormatExtensions(new LanguageFormattings($dateSettings), $security);
$sut->setLocale($language); $sut->setLocale($language);
return $sut; return $sut;
@ -220,31 +225,21 @@ class LocaleFormatExtensionsTest extends TestCase
$this->assertEquals('17:53', $sut->time('2016-06-23 17:53')); $this->assertEquals('17:53', $sut->time('2016-06-23 17:53'));
} }
public function testHour24()
{
$sut = $this->getSut('en', [
'en' => ['24_hours' => false],
]);
$this->assertEquals('bar', $sut->hour24('foo', 'bar'));
$sut = $this->getSut('de', [
'de' => ['24_hours' => true],
]);
$this->assertEquals('foo', $sut->hour24('foo', 'bar'));
}
public function testDateTimeFull() public function testDateTimeFull()
{ {
$sut = $this->getSut('en', [ $sut = $this->getSut('en', [
'en' => ['date_time_type' => 'yyyy-MM-dd HH:mm:ss'], 'en' => ['date_type' => 'dd-yyyy-MM-'],
]); ]);
$dateTime = new \DateTime('2019-08-17 12:29:47', new \DateTimeZone(date_default_timezone_get())); $dateTime = new \DateTime('2019-08-17 12:29:47', new \DateTimeZone(date_default_timezone_get()));
$dateTime->setDate(2019, 8, 17); $dateTime->setDate(2019, 8, 17);
$dateTime->setTime(12, 29, 47); $dateTime->setTime(12, 29, 47);
$this->assertEquals('2019-08-17 12:29:47', $sut->dateTimeFull($dateTime)); $this->assertEquals('17-2019-08- 12:29', $sut->dateTimeFull($dateTime));
$this->assertEquals('2019-08-17 12:29:47', $sut->dateTimeFull('2019-08-17 12:29:47')); $this->assertEquals('17-2019-08- 12:29', $sut->dateTimeFull('2019-08-17 12:29:47'));
$dateTime = new \DateTime('2019-08-17 00:00:00');
$this->assertEquals('17-2019-08-', $sut->dateTimeFull($dateTime, true));
// next test checks the fallback for errors while converting the date // next test checks the fallback for errors while converting the date
/* @phpstan-ignore-next-line */ /* @phpstan-ignore-next-line */

View file

@ -28,61 +28,50 @@ class LocaleFormatsTest extends TestCase
{ {
return [ return [
'de' => [ 'de' => [
'date_time_type' => 'dd.MM.yyyy HH:mm',
'date_type' => 'dd.MM.yyyy', 'date_type' => 'dd.MM.yyyy',
'date' => 'd.m.Y', 'date' => 'd.m.Y',
'date_time' => 'd.m. H:i', 'date_time' => 'd.m. H:i',
'duration' => '%h:%m h', 'duration' => '%h:%m h',
'time' => 'H:i', 'time' => 'H:i',
'24_hours' => true,
], ],
'en' => [ 'en' => [
'date_time_type' => 'yyyy-MM-dd HH:mm',
'date_type' => 'yyyy-MM-dd', 'date_type' => 'yyyy-MM-dd',
'date' => 'Y-m-d', 'date' => 'Y-m-d',
'date_time' => 'm-d H:i', 'date_time' => 'm-d H:i',
'duration' => '%h:%m h', 'duration' => '%h:%m h',
'time' => 'H:i:s', 'time' => 'H:i:s',
'24_hours' => false,
], ],
'pt_BR' => [ 'pt_BR' => [
'date_time_type' => 'dd-MM-yyyy HH:mm',
'date_type' => 'dd-MM-yyyy', 'date_type' => 'dd-MM-yyyy',
'date' => 'd-m-Y', 'date' => 'd-m-Y',
'duration' => '%h:%m h', 'duration' => '%h:%m h',
], ],
'it' => [ 'it' => [
'date_time_type' => 'dd.MM.yyyy HH:mm',
'date_type' => 'dd.MM.yyyy', 'date_type' => 'dd.MM.yyyy',
'date' => 'd.m.Y', 'date' => 'd.m.Y',
'duration' => '%h:%m h', 'duration' => '%h:%m h',
], ],
'fr' => [ 'fr' => [
'date_time_type' => 'dd/MM/yyyy HH:mm',
'date_type' => 'dd/MM/yyyy', 'date_type' => 'dd/MM/yyyy',
'date' => 'd/m/Y', 'date' => 'd/m/Y',
'duration' => '%h h %m', 'duration' => '%h h %m',
], ],
'es' => [ 'es' => [
'date_time_type' => 'dd.MM.yyyy HH:mm',
'date_type' => 'dd.MM.yyyy', 'date_type' => 'dd.MM.yyyy',
'date' => 'd.m.Y', 'date' => 'd.m.Y',
'duration' => '%h:%m h', 'duration' => '%h:%m h',
], ],
'ru' => [ 'ru' => [
'date_time_type' => 'dd.MM.yyyy HH:mm',
'date_type' => 'dd.MM.yyyy', 'date_type' => 'dd.MM.yyyy',
'date' => 'd.m.Y', 'date' => 'd.m.Y',
'duration' => '%h:%m h', 'duration' => '%h:%m h',
], ],
'ar' => [ 'ar' => [
'date_time_type' => 'yyyy-MM-dd HH:mm',
'date_type' => 'yyyy-MM-dd', 'date_type' => 'yyyy-MM-dd',
'date' => 'Y-m-d', 'date' => 'Y-m-d',
'duration' => '%h:%m h', 'duration' => '%h:%m h',
], ],
'hu' => [ 'hu' => [
'date_time_type' => 'yyyy.MM.dd HH:mm',
'date_type' => 'yyyy.MM.dd', 'date_type' => 'yyyy.MM.dd',
'date' => 'Y.m.d.', 'date' => 'Y.m.d.',
'duration' => '%h:%m h', 'duration' => '%h:%m h',
@ -160,32 +149,9 @@ class LocaleFormatsTest extends TestCase
$this->assertEquals('yyyy-MM-dd HH:mm', $sut->getDateTimeTypeFormat()); $this->assertEquals('yyyy-MM-dd HH:mm', $sut->getDateTimeTypeFormat());
} }
public function testGetDateTimePickerFormat()
{
$sut = $this->getSut('en', $this->getDefaultSettings());
$this->assertEquals('YYYY-MM-DD HH:mm', $sut->getDateTimePickerFormat());
}
public function testIs24Hours()
{
$sut = $this->getSut('en', $this->getDefaultSettings());
$this->assertFalse($sut->isTwentyFourHours());
}
public function testGetTimeFormat() public function testGetTimeFormat()
{ {
$sut = $this->getSut('en', $this->getDefaultSettings()); $sut = $this->getSut('en', $this->getDefaultSettings());
$this->assertEquals('H:i:s', $sut->getTimeFormat()); $this->assertEquals('H:i:s', $sut->getTimeFormat());
} }
public function testUnknownSetting()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Unknown setting for locale en: date_time_type');
$sut = $this->getSut('en', ['en' => [
'xxx' => 'dd.MM.yyyy HH:mm',
]]);
$sut->getDateTimePickerFormat();
}
} }

View file

@ -1,36 +1,36 @@
<?xml version='1.0' encoding='UTF-8'?> <?xml version="1.0" encoding="UTF-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2"> <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file source-language="en" target-language="ar" datatype="plaintext" original="about.en.xlf"> <file source-language="en" target-language="ar" datatype="plaintext" original="about.en.xlf">
<body> <body>
<trans-unit id="help"> <trans-unit id="EGpYQvx" resname="help">
<source>help</source> <source>help</source>
<target>مساعدة</target> <target>مساعدة</target>
</trans-unit> </trans-unit>
<trans-unit id="special_thanks"> <trans-unit id="A0nWoXZ" resname="special_thanks">
<source>special_thanks</source> <source>special_thanks</source>
<target>شكر خاص يذهب الى…</target> <target>شكر خاص يذهب الى…</target>
</trans-unit> </trans-unit>
<trans-unit id="donate"> <trans-unit id="mz4ieCN" resname="donate">
<source>donate</source> <source>donate</source>
<target>تبرع لمستقبل KIMAI</target> <target>تبرع لمستقبل KIMAI</target>
</trans-unit> </trans-unit>
<trans-unit id="website"> <trans-unit id="dHqPOYO" resname="website">
<source>website</source> <source>website</source>
<target>الصفحة الرئيسية</target> <target>الصفحة الرئيسية</target>
</trans-unit> </trans-unit>
<trans-unit id="support"> <trans-unit id="oYYDCG5" resname="support">
<source>support</source> <source>support</source>
<target>الدعم</target> <target>الدعم</target>
</trans-unit> </trans-unit>
<trans-unit id="about.title"> <trans-unit id="3Clo55j" resname="about.title">
<source>about.title</source> <source>about.title</source>
<target>حول Kimai</target> <target>حول Kimai</target>
</trans-unit> </trans-unit>
<trans-unit id="published_under"> <trans-unit id="DwbowBh" resname="published_under">
<source>published_under</source> <source>published_under</source>
<target>تم نشره حسب %kimai%</target> <target>تم نشره حسب %kimai%</target>
</trans-unit> </trans-unit>
<trans-unit id="library_authors"> <trans-unit id="L7cff3Q" resname="library_authors">
<source>library_authors</source> <source>library_authors</source>
<target>… مؤلفو مكتبات البرامج الحالية ،، لم يكون kimai قادراً على إكمالها بدونك 👍</target> <target>… مؤلفو مكتبات البرامج الحالية ،، لم يكون kimai قادراً على إكمالها بدونك 👍</target>
</trans-unit> </trans-unit>

View file

@ -1,39 +1,39 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file source-language="en" target-language="cs" datatype="plaintext" original="about.en.xlf"> <file source-language="en" target-language="cs" datatype="plaintext" original="about.en.xlf">
<body> <body>
<trans-unit id="about.title"> <trans-unit id="3Clo55j" resname="about.title">
<source>about.title</source> <source>about.title</source>
<target>O Kimai</target> <target>O Kimai</target>
</trans-unit> </trans-unit>
<trans-unit id="support"> <trans-unit id="oYYDCG5" resname="support">
<source>support</source> <source>support</source>
<target>Podpora</target> <target>Podpora</target>
</trans-unit> </trans-unit>
<trans-unit id="website"> <trans-unit id="dHqPOYO" resname="website">
<source>website</source> <source>website</source>
<target>Úvodní stránka</target> <target>Úvodní stránka</target>
</trans-unit> </trans-unit>
<trans-unit id="help"> <trans-unit id="EGpYQvx" resname="help">
<source>help</source> <source>help</source>
<target>Dokumentace</target> <target>Dokumentace</target>
</trans-unit> </trans-unit>
<trans-unit id="donate"> <trans-unit id="mz4ieCN" resname="donate">
<source>donate</source> <source>donate</source>
<target>Podpořte budoucnost Kimai</target> <target>Podpořte budoucnost Kimai</target>
</trans-unit> </trans-unit>
<trans-unit id="published_under"> <trans-unit id="DwbowBh" resname="published_under">
<source>published_under</source> <source>published_under</source>
<target>%kimai% je zveřejněno pod</target> <target>%kimai% je zveřejněno pod</target>
</trans-unit> </trans-unit>
<trans-unit id="special_thanks"> <trans-unit id="A0nWoXZ" resname="special_thanks">
<source>special_thanks</source> <source>special_thanks</source>
<target>Zvláští poděkování patří …</target> <target>Zvláští poděkování patří …</target>
</trans-unit> </trans-unit>
<trans-unit id="library_authors"> <trans-unit id="L7cff3Q" resname="library_authors">
<source>library_authors</source> <source>library_authors</source>
<target>… autoři následujících softwarových knihoven, Kimai, by bez tebe nebyo možné 👍</target> <target>… autoři následujících softwarových knihoven, Kimai, by bez tebe nebyo možné 👍</target>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View file

@ -1,39 +1,39 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file source-language="en" target-language="da" datatype="plaintext" original="about.en.xlf"> <file source-language="en" target-language="da" datatype="plaintext" original="about.en.xlf">
<body> <body>
<trans-unit id="about.title"> <trans-unit id="3Clo55j" resname="about.title">
<source>about.title</source> <source>about.title</source>
<target>Om Kimai</target> <target>Om Kimai</target>
</trans-unit> </trans-unit>
<trans-unit id="support"> <trans-unit id="oYYDCG5" resname="support">
<source>support</source> <source>support</source>
<target>Support</target> <target>Support</target>
</trans-unit> </trans-unit>
<trans-unit id="website"> <trans-unit id="dHqPOYO" resname="website">
<source>website</source> <source>website</source>
<target>Hjemmeside</target> <target>Hjemmeside</target>
</trans-unit> </trans-unit>
<trans-unit id="help"> <trans-unit id="EGpYQvx" resname="help">
<source>help</source> <source>help</source>
<target>Dokumentation</target> <target>Dokumentation</target>
</trans-unit> </trans-unit>
<trans-unit id="donate"> <trans-unit id="mz4ieCN" resname="donate">
<source>donate</source> <source>donate</source>
<target>Støt Kimai's fremtid</target> <target>Støt Kimai's fremtid</target>
</trans-unit> </trans-unit>
<trans-unit id="published_under"> <trans-unit id="DwbowBh" resname="published_under">
<source>published_under</source> <source>published_under</source>
<target>%kimai% er udgivet under</target> <target>%kimai% er udgivet under</target>
</trans-unit> </trans-unit>
<trans-unit id="special_thanks"> <trans-unit id="A0nWoXZ" resname="special_thanks">
<source>special_thanks</source> <source>special_thanks</source>
<target>Tak til …</target> <target>Tak til …</target>
</trans-unit> </trans-unit>
<trans-unit id="library_authors"> <trans-unit id="L7cff3Q" resname="library_authors">
<source>library_authors</source> <source>library_authors</source>
<target>… udviklerne af følgende softwarebiblioteker, Kimai ville ikke være muligt uden jer 👍</target> <target>… udviklerne af følgende softwarebiblioteker, Kimai ville ikke være muligt uden jer 👍</target>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View file

@ -1,36 +1,36 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2"> <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file source-language="en" target-language="de" datatype="plaintext" original="about.en.xlf"> <file source-language="en" target-language="de" datatype="plaintext" original="about.en.xlf">
<body> <body>
<trans-unit id="about.title"> <trans-unit id="3Clo55j" resname="about.title">
<source>about.title</source> <source>about.title</source>
<target>Über Kimai</target> <target>Über Kimai</target>
</trans-unit> </trans-unit>
<trans-unit id="support"> <trans-unit id="oYYDCG5" resname="support">
<source>support</source> <source>support</source>
<target>Support</target> <target>Support</target>
</trans-unit> </trans-unit>
<trans-unit id="website" approved="yes"> <trans-unit id="dHqPOYO" approved="yes" resname="website">
<source>website</source> <source>website</source>
<target>Homepage</target> <target>Homepage</target>
</trans-unit> </trans-unit>
<trans-unit id="help"> <trans-unit id="EGpYQvx" resname="help">
<source>help</source> <source>help</source>
<target>Dokumentation</target> <target>Dokumentation</target>
</trans-unit> </trans-unit>
<trans-unit id="donate"> <trans-unit id="mz4ieCN" resname="donate">
<source>donate</source> <source>donate</source>
<target>Spende für die Zukunft von Kimai</target> <target>Spende für die Zukunft von Kimai</target>
</trans-unit> </trans-unit>
<trans-unit id="published_under"> <trans-unit id="DwbowBh" resname="published_under">
<source>published_under</source> <source>published_under</source>
<target>%kimai% wird veröffentlicht unter</target> <target>%kimai% wird veröffentlicht unter</target>
</trans-unit> </trans-unit>
<trans-unit id="special_thanks"> <trans-unit id="A0nWoXZ" resname="special_thanks">
<source>special_thanks</source> <source>special_thanks</source>
<target>Besonderer Dank geht an …</target> <target>Besonderer Dank geht an …</target>
</trans-unit> </trans-unit>
<trans-unit id="library_authors"> <trans-unit id="L7cff3Q" resname="library_authors">
<source>library_authors</source> <source>library_authors</source>
<target>… die Autoren der folgenden Software-Bibliotheken, ohne Euch wäre Kimai nicht möglich 👍</target> <target>… die Autoren der folgenden Software-Bibliotheken, ohne Euch wäre Kimai nicht möglich 👍</target>
</trans-unit> </trans-unit>

View file

@ -1,36 +1,36 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2"> <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file source-language="en" target-language="el" datatype="plaintext" original="about.en.xlf"> <file source-language="en" target-language="el" datatype="plaintext" original="about.en.xlf">
<body> <body>
<trans-unit id="about.title"> <trans-unit id="3Clo55j" resname="about.title">
<source>about.title</source> <source>about.title</source>
<target>Σχετικά με το Kimai</target> <target>Σχετικά με το Kimai</target>
</trans-unit> </trans-unit>
<trans-unit id="support"> <trans-unit id="oYYDCG5" resname="support">
<source>support</source> <source>support</source>
<target>Υποστήριξη</target> <target>Υποστήριξη</target>
</trans-unit> </trans-unit>
<trans-unit id="website"> <trans-unit id="dHqPOYO" resname="website">
<source>website</source> <source>website</source>
<target>Αρχική σελίδα</target> <target>Αρχική σελίδα</target>
</trans-unit> </trans-unit>
<trans-unit id="help"> <trans-unit id="EGpYQvx" resname="help">
<source>help</source> <source>help</source>
<target>Τεκμηρίωση</target> <target>Τεκμηρίωση</target>
</trans-unit> </trans-unit>
<trans-unit id="donate"> <trans-unit id="mz4ieCN" resname="donate">
<source>donate</source> <source>donate</source>
<target>Δωρίστε για το μέλλον του Kimai</target> <target>Δωρίστε για το μέλλον του Kimai</target>
</trans-unit> </trans-unit>
<trans-unit id="published_under"> <trans-unit id="DwbowBh" resname="published_under">
<source>published_under</source> <source>published_under</source>
<target>%kimai% δημοσιεύτηκε κάτω από</target> <target>%kimai% δημοσιεύτηκε κάτω από</target>
</trans-unit> </trans-unit>
<trans-unit id="special_thanks"> <trans-unit id="A0nWoXZ" resname="special_thanks">
<source>special_thanks</source> <source>special_thanks</source>
<target>Ιδιαίτερες ευχαριστίες πηγαίνουν …</target> <target>Ιδιαίτερες ευχαριστίες πηγαίνουν …</target>
</trans-unit> </trans-unit>
<trans-unit id="library_authors"> <trans-unit id="L7cff3Q" resname="library_authors">
<source>library_authors</source> <source>library_authors</source>
<target>… στους συγγραφείς των παρακάτω βιβλιοθηκών-λογισμικού, δεν θα ήταν πραγματοποιήσιμο το Kimai χωρίς εσάς 👍</target> <target>… στους συγγραφείς των παρακάτω βιβλιοθηκών-λογισμικού, δεν θα ήταν πραγματοποιήσιμο το Kimai χωρίς εσάς 👍</target>
</trans-unit> </trans-unit>

View file

@ -1,39 +1,39 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file source-language="en" target-language="en" datatype="plaintext" original="about.en.xlf"> <file source-language="en" target-language="en" datatype="plaintext" original="about.en.xlf">
<body> <body>
<trans-unit id="about.title"> <trans-unit id="3Clo55j" resname="about.title">
<source>about.title</source> <source>about.title</source>
<target>About Kimai</target> <target>About Kimai</target>
</trans-unit> </trans-unit>
<trans-unit id="support"> <trans-unit id="oYYDCG5" resname="support">
<source>support</source> <source>support</source>
<target>Support</target> <target>Support</target>
</trans-unit> </trans-unit>
<trans-unit id="website"> <trans-unit id="dHqPOYO" resname="website">
<source>website</source> <source>website</source>
<target>Homepage</target> <target>Homepage</target>
</trans-unit> </trans-unit>
<trans-unit id="help"> <trans-unit id="EGpYQvx" resname="help">
<source>help</source> <source>help</source>
<target>Documentation</target> <target>Documentation</target>
</trans-unit> </trans-unit>
<trans-unit id="donate"> <trans-unit id="mz4ieCN" resname="donate">
<source>donate</source> <source>donate</source>
<target>Donate for the future of Kimai</target> <target>Donate for the future of Kimai</target>
</trans-unit> </trans-unit>
<trans-unit id="published_under"> <trans-unit id="DwbowBh" resname="published_under">
<source>published_under</source> <source>published_under</source>
<target>%kimai% is published under</target> <target>%kimai% is published under</target>
</trans-unit> </trans-unit>
<trans-unit id="special_thanks"> <trans-unit id="A0nWoXZ" resname="special_thanks">
<source>special_thanks</source> <source>special_thanks</source>
<target>Special thanks go to …</target> <target>Special thanks go to …</target>
</trans-unit> </trans-unit>
<trans-unit id="library_authors"> <trans-unit id="L7cff3Q" resname="library_authors">
<source>library_authors</source> <source>library_authors</source>
<target>… the authors of the following software-libraries, Kimai wouldn't be possible without you 👍</target> <target>… the authors of the following software-libraries, Kimai wouldn't be possible without you 👍</target>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View file

@ -1,39 +1,39 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file source-language="en" target-language="eo" datatype="plaintext" original="about.en.xlf"> <file source-language="en" target-language="eo" datatype="plaintext" original="about.en.xlf">
<body> <body>
<trans-unit id="about.title"> <trans-unit id="3Clo55j" resname="about.title">
<source>about.title</source> <source>about.title</source>
<target>Pri Kimai</target> <target>Pri Kimai</target>
</trans-unit> </trans-unit>
<trans-unit id="support"> <trans-unit id="oYYDCG5" resname="support">
<source>support</source> <source>support</source>
<target>Subteno</target> <target>Subteno</target>
</trans-unit> </trans-unit>
<trans-unit id="website"> <trans-unit id="dHqPOYO" resname="website">
<source>website</source> <source>website</source>
<target>Hejmpaĝo</target> <target>Hejmpaĝo</target>
</trans-unit> </trans-unit>
<trans-unit id="help"> <trans-unit id="EGpYQvx" resname="help">
<source>help</source> <source>help</source>
<target>Dokumentado</target> <target>Dokumentado</target>
</trans-unit> </trans-unit>
<trans-unit id="donate"> <trans-unit id="mz4ieCN" resname="donate">
<source>donate</source> <source>donate</source>
<target>Donacu por la estonteco de Kimai</target> <target>Donacu por la estonteco de Kimai</target>
</trans-unit> </trans-unit>
<trans-unit id="published_under"> <trans-unit id="DwbowBh" resname="published_under">
<source>published_under</source> <source>published_under</source>
<target>%kimai% estas publikigita sub</target> <target>%kimai% estas publikigita sub</target>
</trans-unit> </trans-unit>
<trans-unit id="special_thanks"> <trans-unit id="A0nWoXZ" resname="special_thanks">
<source>special_thanks</source> <source>special_thanks</source>
<target>Speciala danko al …</target> <target>Speciala danko al …</target>
</trans-unit> </trans-unit>
<trans-unit id="library_authors"> <trans-unit id="L7cff3Q" resname="library_authors">
<source>library_authors</source> <source>library_authors</source>
<target>… la aŭtoroj de la sekvaj programaraj bibliotekoj, Kimai ne estus ebla sen vi 👍</target> <target>… la aŭtoroj de la sekvaj programaraj bibliotekoj, Kimai ne estus ebla sen vi 👍</target>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View file

@ -1,36 +1,36 @@
<?xml version='1.0' encoding='UTF-8'?> <?xml version="1.0" encoding="UTF-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2"> <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file source-language="en" target-language="es" datatype="plaintext" original="about.en.xlf"> <file source-language="en" target-language="es" datatype="plaintext" original="about.en.xlf">
<body> <body>
<trans-unit id="about.title"> <trans-unit id="3Clo55j" resname="about.title">
<source>about.title</source> <source>about.title</source>
<target>Acerca de Kimai</target> <target>Acerca de Kimai</target>
</trans-unit> </trans-unit>
<trans-unit id="support"> <trans-unit id="oYYDCG5" resname="support">
<source>support</source> <source>support</source>
<target>Asistencia</target> <target>Asistencia</target>
</trans-unit> </trans-unit>
<trans-unit id="website"> <trans-unit id="dHqPOYO" resname="website">
<source>website</source> <source>website</source>
<target>Sitio web</target> <target>Sitio web</target>
</trans-unit> </trans-unit>
<trans-unit id="help"> <trans-unit id="EGpYQvx" resname="help">
<source>help</source> <source>help</source>
<target>Documentación</target> <target>Documentación</target>
</trans-unit> </trans-unit>
<trans-unit id="donate"> <trans-unit id="mz4ieCN" resname="donate">
<source>donate</source> <source>donate</source>
<target>Haga una donación por el futuro de Kimai</target> <target>Haga una donación por el futuro de Kimai</target>
</trans-unit> </trans-unit>
<trans-unit id="published_under"> <trans-unit id="DwbowBh" resname="published_under">
<source>published_under</source> <source>published_under</source>
<target>%kimai% está publicado en virtud de la</target> <target>%kimai% está publicado en virtud de la</target>
</trans-unit> </trans-unit>
<trans-unit id="special_thanks"> <trans-unit id="A0nWoXZ" resname="special_thanks">
<source>special_thanks</source> <source>special_thanks</source>
<target>Agradecimientos especiales para…</target> <target>Agradecimientos especiales para…</target>
</trans-unit> </trans-unit>
<trans-unit id="library_authors"> <trans-unit id="L7cff3Q" resname="library_authors">
<source>library_authors</source> <source>library_authors</source>
<target>… los autores de las bibliotecas de software siguientes; Kimai no sería posible sin vosotros 👍</target> <target>… los autores de las bibliotecas de software siguientes; Kimai no sería posible sin vosotros 👍</target>
</trans-unit> </trans-unit>

View file

@ -1,36 +1,36 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2"> <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file source-language="en" target-language="eu" datatype="plaintext" original="about.en.xlf"> <file source-language="en" target-language="eu" datatype="plaintext" original="about.en.xlf">
<body> <body>
<trans-unit id="about.title"> <trans-unit id="3Clo55j" resname="about.title">
<source>about.title</source> <source>about.title</source>
<target>Kimairi buruz</target> <target>Kimairi buruz</target>
</trans-unit> </trans-unit>
<trans-unit id="support"> <trans-unit id="oYYDCG5" resname="support">
<source>support</source> <source>support</source>
<target>Mantenua</target> <target>Mantenua</target>
</trans-unit> </trans-unit>
<trans-unit id="website"> <trans-unit id="dHqPOYO" resname="website">
<source>website</source> <source>website</source>
<target>Web orrialdea</target> <target>Web orrialdea</target>
</trans-unit> </trans-unit>
<trans-unit id="help"> <trans-unit id="EGpYQvx" resname="help">
<source>help</source> <source>help</source>
<target>Laguntza</target> <target>Laguntza</target>
</trans-unit> </trans-unit>
<trans-unit id="donate"> <trans-unit id="mz4ieCN" resname="donate">
<source>donate</source> <source>donate</source>
<target>Egin dohaintza bat Kimai-en etorkizunerako</target> <target>Egin dohaintza bat Kimai-en etorkizunerako</target>
</trans-unit> </trans-unit>
<trans-unit id="published_under"> <trans-unit id="DwbowBh" resname="published_under">
<source>published_under</source> <source>published_under</source>
<target>%kimai% lizentzia</target> <target>%kimai% lizentzia</target>
</trans-unit> </trans-unit>
<trans-unit id="special_thanks"> <trans-unit id="A0nWoXZ" resname="special_thanks">
<source>special_thanks</source> <source>special_thanks</source>
<target>Mila esker …</target> <target>Mila esker …</target>
</trans-unit> </trans-unit>
<trans-unit id="library_authors"> <trans-unit id="L7cff3Q" resname="library_authors">
<source>library_authors</source> <source>library_authors</source>
<target>… Kimai ez lebilke ondorengo kideen laguntzarik gabe: 👍</target> <target>… Kimai ez lebilke ondorengo kideen laguntzarik gabe: 👍</target>
</trans-unit> </trans-unit>

View file

@ -1,24 +1,24 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2"> <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"> <file source-language="en" target-language="fa" datatype="plaintext" original="about.en.xlf">
<body> <body>
<trans-unit id="about.title"> <trans-unit id="3Clo55j" resname="about.title">
<source>about.title</source> <source>about.title</source>
<target>درباره ی کیمیایْ</target> <target>درباره ی کیمیایْ</target>
</trans-unit> </trans-unit>
<trans-unit id="support"> <trans-unit id="oYYDCG5" resname="support">
<source>support</source> <source>support</source>
<target>پشتیبانی</target> <target>پشتیبانی</target>
</trans-unit> </trans-unit>
<trans-unit id="website"> <trans-unit id="dHqPOYO" resname="website">
<source>website</source> <source>website</source>
<target>خانه</target> <target>خانه</target>
</trans-unit> </trans-unit>
<trans-unit id="help"> <trans-unit id="EGpYQvx" resname="help">
<source>help</source> <source>help</source>
<target>مستندات</target> <target>مستندات</target>
</trans-unit> </trans-unit>
<trans-unit id="special_thanks"> <trans-unit id="A0nWoXZ" resname="special_thanks">
<source>special_thanks</source> <source>special_thanks</source>
<target>سپاس ویژه از…</target> <target>سپاس ویژه از…</target>
</trans-unit> </trans-unit>

View file

@ -1,39 +1,39 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file source-language="en" target-language="fi" datatype="plaintext" original="about.en.xlf"> <file source-language="en" target-language="fi" datatype="plaintext" original="about.en.xlf">
<body> <body>
<trans-unit id="about.title"> <trans-unit id="3Clo55j" resname="about.title">
<source>about.title</source> <source>about.title</source>
<target state="translated">Tietoa Kimaista</target> <target state="translated">Tietoa Kimaista</target>
</trans-unit> </trans-unit>
<trans-unit id="support"> <trans-unit id="oYYDCG5" resname="support">
<source>support</source> <source>support</source>
<target state="translated">Tuki</target> <target state="translated">Tuki</target>
</trans-unit> </trans-unit>
<trans-unit id="website"> <trans-unit id="dHqPOYO" resname="website">
<source>website</source> <source>website</source>
<target state="translated">Kotisivu</target> <target state="translated">Kotisivu</target>
</trans-unit> </trans-unit>
<trans-unit id="help"> <trans-unit id="EGpYQvx" resname="help">
<source>help</source> <source>help</source>
<target state="translated">Dokumentit</target> <target state="translated">Dokumentit</target>
</trans-unit> </trans-unit>
<trans-unit id="donate"> <trans-unit id="mz4ieCN" resname="donate">
<source>donate</source> <source>donate</source>
<target state="translated">Tee lahjoitus Kimain tulevaisuudelle</target> <target state="translated">Tee lahjoitus Kimain tulevaisuudelle</target>
</trans-unit> </trans-unit>
<trans-unit id="published_under"> <trans-unit id="DwbowBh" resname="published_under">
<source>published_under</source> <source>published_under</source>
<target state="translated">%kimai% julkaisu on</target> <target state="translated">%kimai% julkaisu on</target>
</trans-unit> </trans-unit>
<trans-unit id="special_thanks"> <trans-unit id="A0nWoXZ" resname="special_thanks">
<source>special_thanks</source> <source>special_thanks</source>
<target state="translated">Erityiskiitokset menee …</target> <target state="translated">Erityiskiitokset menee …</target>
</trans-unit> </trans-unit>
<trans-unit id="library_authors"> <trans-unit id="L7cff3Q" resname="library_authors">
<source>library_authors</source> <source>library_authors</source>
<target state="translated">… seuraavien ohjelmistokirjastojen tekijöille, Kimai ei olisi mahdollinen ilman teitä 👍</target> <target state="translated">… seuraavien ohjelmistokirjastojen tekijöille, Kimai ei olisi mahdollinen ilman teitä 👍</target>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View file

@ -1,36 +1,36 @@
<?xml version='1.0' encoding='UTF-8'?> <?xml version="1.0" encoding="UTF-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2"> <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file source-language="en" target-language="fr" datatype="plaintext" original="about.en.xlf"> <file source-language="en" target-language="fr" datatype="plaintext" original="about.en.xlf">
<body> <body>
<trans-unit id="about.title"> <trans-unit id="3Clo55j" resname="about.title">
<source>about.title</source> <source>about.title</source>
<target>À propos de Kimai</target> <target>À propos de Kimai</target>
</trans-unit> </trans-unit>
<trans-unit id="support"> <trans-unit id="oYYDCG5" resname="support">
<source>support</source> <source>support</source>
<target>Assistance</target> <target>Assistance</target>
</trans-unit> </trans-unit>
<trans-unit id="website"> <trans-unit id="dHqPOYO" resname="website">
<source>website</source> <source>website</source>
<target>Accueil</target> <target>Accueil</target>
</trans-unit> </trans-unit>
<trans-unit id="help"> <trans-unit id="EGpYQvx" resname="help">
<source>help</source> <source>help</source>
<target>Documentation</target> <target>Documentation</target>
</trans-unit> </trans-unit>
<trans-unit id="donate"> <trans-unit id="mz4ieCN" resname="donate">
<source>donate</source> <source>donate</source>
<target>Faites un don pour l'avenir de Kimai</target> <target>Faites un don pour l'avenir de Kimai</target>
</trans-unit> </trans-unit>
<trans-unit id="published_under"> <trans-unit id="DwbowBh" resname="published_under">
<source>published_under</source> <source>published_under</source>
<target>%kimai% est publié sous</target> <target>%kimai% est publié sous</target>
</trans-unit> </trans-unit>
<trans-unit id="special_thanks"> <trans-unit id="A0nWoXZ" resname="special_thanks">
<source>special_thanks</source> <source>special_thanks</source>
<target>Des remerciements particuliers vont à …</target> <target>Des remerciements particuliers vont à …</target>
</trans-unit> </trans-unit>
<trans-unit id="library_authors"> <trans-unit id="L7cff3Q" resname="library_authors">
<source>library_authors</source> <source>library_authors</source>
<target>…l'ensemble des auteurs des logiciels et bibliothèques suivants, Kimai n'existerait pas sans vous 👍</target> <target>…l'ensemble des auteurs des logiciels et bibliothèques suivants, Kimai n'existerait pas sans vous 👍</target>
</trans-unit> </trans-unit>

View file

@ -1,39 +1,36 @@
<?xml version='1.0' encoding='UTF-8'?> <?xml version="1.0" encoding="UTF-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2"> <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file source-language="en" target-language="he" datatype="plaintext" original="about.en.xlf"> <file source-language="en" target-language="he" datatype="plaintext" original="about.en.xlf">
<header>
<tool tool-id="symfony" tool-name="Symfony"/>
</header>
<body> <body>
<trans-unit id="about.title"> <trans-unit id="3Clo55j" resname="about.title">
<source>about.title</source> <source>about.title</source>
<target>על אודות קימאי</target> <target>על אודות קימאי</target>
</trans-unit> </trans-unit>
<trans-unit id="support"> <trans-unit id="oYYDCG5" resname="support">
<source>support</source> <source>support</source>
<target>תמיכה</target> <target>תמיכה</target>
</trans-unit> </trans-unit>
<trans-unit id="website"> <trans-unit id="dHqPOYO" resname="website">
<source>website</source> <source>website</source>
<target>דף הבית</target> <target>דף הבית</target>
</trans-unit> </trans-unit>
<trans-unit id="help"> <trans-unit id="EGpYQvx" resname="help">
<source>help</source> <source>help</source>
<target>תיעוד</target> <target>תיעוד</target>
</trans-unit> </trans-unit>
<trans-unit id="donate"> <trans-unit id="mz4ieCN" resname="donate">
<source>donate</source> <source>donate</source>
<target>תרומה לעתיד של קימאי</target> <target>תרומה לעתיד של קימאי</target>
</trans-unit> </trans-unit>
<trans-unit id="published_under"> <trans-unit id="DwbowBh" resname="published_under">
<source>published_under</source> <source>published_under</source>
<target>%kimai% מופץ בכפוף</target> <target>%kimai% מופץ בכפוף</target>
</trans-unit> </trans-unit>
<trans-unit id="special_thanks"> <trans-unit id="A0nWoXZ" resname="special_thanks">
<source>special_thanks</source> <source>special_thanks</source>
<target>תודה מיוחדת מגיעה ל…</target> <target>תודה מיוחדת מגיעה ל…</target>
</trans-unit> </trans-unit>
<trans-unit id="library_authors"> <trans-unit id="L7cff3Q" resname="library_authors">
<source>library_authors</source> <source>library_authors</source>
<target>… המפתחים של רכיבי התכנה האלה, קימאי לא הייתה קיימת בלעדיכם 👍</target> <target>… המפתחים של רכיבי התכנה האלה, קימאי לא הייתה קיימת בלעדיכם 👍</target>
</trans-unit> </trans-unit>

View file

@ -1,36 +1,36 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2"> <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"> <file source-language="en" target-language="hr" datatype="plaintext" original="about.en.xlf">
<body> <body>
<trans-unit id="about.title"> <trans-unit id="3Clo55j" resname="about.title">
<source>about.title</source> <source>about.title</source>
<target>Kimai informacije</target> <target>Kimai informacije</target>
</trans-unit> </trans-unit>
<trans-unit id="support"> <trans-unit id="oYYDCG5" resname="support">
<source>support</source> <source>support</source>
<target>Podrška</target> <target>Podrška</target>
</trans-unit> </trans-unit>
<trans-unit id="website"> <trans-unit id="dHqPOYO" resname="website">
<source>website</source> <source>website</source>
<target>Web-stranica</target> <target>Web-stranica</target>
</trans-unit> </trans-unit>
<trans-unit id="help"> <trans-unit id="EGpYQvx" resname="help">
<source>help</source> <source>help</source>
<target>Dokumentacija</target> <target>Dokumentacija</target>
</trans-unit> </trans-unit>
<trans-unit id="donate"> <trans-unit id="mz4ieCN" resname="donate">
<source>donate</source> <source>donate</source>
<target>Doniraj za budućnost Kimaija</target> <target>Doniraj za budućnost Kimaija</target>
</trans-unit> </trans-unit>
<trans-unit id="published_under"> <trans-unit id="DwbowBh" resname="published_under">
<source>published_under</source> <source>published_under</source>
<target>%kimai% je izdan pod</target> <target>%kimai% je izdan pod</target>
</trans-unit> </trans-unit>
<trans-unit id="special_thanks"> <trans-unit id="A0nWoXZ" resname="special_thanks">
<source>special_thanks</source> <source>special_thanks</source>
<target>Posebno zahvaljujemo …</target> <target>Posebno zahvaljujemo …</target>
</trans-unit> </trans-unit>
<trans-unit id="library_authors"> <trans-unit id="L7cff3Q" resname="library_authors">
<source>library_authors</source> <source>library_authors</source>
<target>… autorima sljedećih softverskih biblioteka. Kimai ne bi bio moguć bez vas 👍</target> <target>… autorima sljedećih softverskih biblioteka. Kimai ne bi bio moguć bez vas 👍</target>
</trans-unit> </trans-unit>

View file

@ -1,39 +1,39 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file source-language="en" target-language="hu" datatype="plaintext" original="about.en.xlf"> <file source-language="en" target-language="hu" datatype="plaintext" original="about.en.xlf">
<body> <body>
<trans-unit id="about.title"> <trans-unit id="3Clo55j" resname="about.title">
<source>about.title</source> <source>about.title</source>
<target>A Kimai névjegye</target> <target>A Kimai névjegye</target>
</trans-unit> </trans-unit>
<trans-unit id="support"> <trans-unit id="oYYDCG5" resname="support">
<source>support</source> <source>support</source>
<target>Támogatás</target> <target>Támogatás</target>
</trans-unit> </trans-unit>
<trans-unit id="website"> <trans-unit id="dHqPOYO" resname="website">
<source>website</source> <source>website</source>
<target>Weboldal</target> <target>Weboldal</target>
</trans-unit> </trans-unit>
<trans-unit id="help"> <trans-unit id="EGpYQvx" resname="help">
<source>help</source> <source>help</source>
<target>Súgó</target> <target>Súgó</target>
</trans-unit> </trans-unit>
<trans-unit id="donate"> <trans-unit id="mz4ieCN" resname="donate">
<source>donate</source> <source>donate</source>
<target>Támogasd a Kimai jövőjét</target> <target>Támogasd a Kimai jövőjét</target>
</trans-unit> </trans-unit>
<trans-unit id="published_under"> <trans-unit id="DwbowBh" resname="published_under">
<source>published_under</source> <source>published_under</source>
<target>%kimai% licensz</target> <target>%kimai% licensz</target>
</trans-unit> </trans-unit>
<trans-unit id="special_thanks"> <trans-unit id="A0nWoXZ" resname="special_thanks">
<source>special_thanks</source> <source>special_thanks</source>
<target>Külön köszönet …</target> <target>Külön köszönet …</target>
</trans-unit> </trans-unit>
<trans-unit id="library_authors"> <trans-unit id="L7cff3Q" resname="library_authors">
<source>library_authors</source> <source>library_authors</source>
<target>… a következő szoftver-könyvtárak fejlesztőinek, Kimai nem jöhetett volna létre nélkületek 👍</target> <target>… a következő szoftver-könyvtárak fejlesztőinek, Kimai nem jöhetett volna létre nélkületek 👍</target>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View file

@ -1,39 +1,39 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file source-language="en" target-language="it" datatype="plaintext" original="about.en.xlf"> <file source-language="en" target-language="it" datatype="plaintext" original="about.en.xlf">
<body> <body>
<trans-unit id="about.title"> <trans-unit id="3Clo55j" resname="about.title">
<source>about.title</source> <source>about.title</source>
<target>Approposito di Kimai</target> <target>Approposito di Kimai</target>
</trans-unit> </trans-unit>
<trans-unit id="support"> <trans-unit id="oYYDCG5" resname="support">
<source>support</source> <source>support</source>
<target>Supporto</target> <target>Supporto</target>
</trans-unit> </trans-unit>
<trans-unit id="website"> <trans-unit id="dHqPOYO" resname="website">
<source>website</source> <source>website</source>
<target>Sito web</target> <target>Sito web</target>
</trans-unit> </trans-unit>
<trans-unit id="help"> <trans-unit id="EGpYQvx" resname="help">
<source>help</source> <source>help</source>
<target>Aiuto</target> <target>Aiuto</target>
</trans-unit> </trans-unit>
<trans-unit id="donate"> <trans-unit id="mz4ieCN" resname="donate">
<source>donate</source> <source>donate</source>
<target>Donazione per lo sviluppo Kimai</target> <target>Donazione per lo sviluppo Kimai</target>
</trans-unit> </trans-unit>
<trans-unit id="published_under"> <trans-unit id="DwbowBh" resname="published_under">
<source>published_under</source> <source>published_under</source>
<target>%kimai% è pubblicato su</target> <target>%kimai% è pubblicato su</target>
</trans-unit> </trans-unit>
<trans-unit id="special_thanks"> <trans-unit id="A0nWoXZ" resname="special_thanks">
<source>special_thanks</source> <source>special_thanks</source>
<target>Ringraziamenti speciali vanno a …</target> <target>Ringraziamenti speciali vanno a …</target>
</trans-unit> </trans-unit>
<trans-unit id="library_authors"> <trans-unit id="L7cff3Q" resname="library_authors">
<source>library_authors</source> <source>library_authors</source>
<target>… gli autori delle seguenti librerie software, Kimai non esisterebbe senza loro 👍</target> <target>… gli autori delle seguenti librerie software, Kimai non esisterebbe senza loro 👍</target>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View file

@ -1,39 +1,39 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file source-language="en" target-language="ja" datatype="plaintext" original="about.en.xlf"> <file source-language="en" target-language="ja" datatype="plaintext" original="about.en.xlf">
<body> <body>
<trans-unit id="about.title"> <trans-unit id="3Clo55j" resname="about.title">
<source>about.title</source> <source>about.title</source>
<target>Kimai について</target> <target>Kimai について</target>
</trans-unit> </trans-unit>
<trans-unit id="support"> <trans-unit id="oYYDCG5" resname="support">
<source>support</source> <source>support</source>
<target>サポート</target> <target>サポート</target>
</trans-unit> </trans-unit>
<trans-unit id="website"> <trans-unit id="dHqPOYO" resname="website">
<source>website</source> <source>website</source>
<target>ホームページ</target> <target>ホームページ</target>
</trans-unit> </trans-unit>
<trans-unit id="help"> <trans-unit id="EGpYQvx" resname="help">
<source>help</source> <source>help</source>
<target>ドキュメント</target> <target>ドキュメント</target>
</trans-unit> </trans-unit>
<trans-unit id="donate"> <trans-unit id="mz4ieCN" resname="donate">
<source>donate</source> <source>donate</source>
<target>Kimai の発展のために寄付を行う</target> <target>Kimai の発展のために寄付を行う</target>
</trans-unit> </trans-unit>
<trans-unit id="published_under"> <trans-unit id="DwbowBh" resname="published_under">
<source>published_under</source> <source>published_under</source>
<target>%kimai% は次のもとに公開されています</target> <target>%kimai% は次のもとに公開されています</target>
</trans-unit> </trans-unit>
<trans-unit id="special_thanks"> <trans-unit id="A0nWoXZ" resname="special_thanks">
<source>special_thanks</source> <source>special_thanks</source>
<target>Special thanks go to …</target> <target>Special thanks go to …</target>
</trans-unit> </trans-unit>
<trans-unit id="library_authors"> <trans-unit id="L7cff3Q" resname="library_authors">
<source>library_authors</source> <source>library_authors</source>
<target>… これらのソフトウェア・ライブラリなくして Kimai は実現できませんでした。作成者の方々に感謝します 👍</target> <target>… これらのソフトウェア・ライブラリなくして Kimai は実現できませんでした。作成者の方々に感謝します 👍</target>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

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