Release 2.0.2 (#3856)
* allow to overwrite global spreadsheet styles * bump version * fix deprecations in vcard download * bump composer packages * added help page for all registered locales * fix menu id's, cleanup times route, fix configurable homepage redirect * format duration without leading zero in hours (unify javascript with php behavior) * fix active records in all screen sizes * improved responsiveness in XS * fixed invoice number for customer null fields * fix javascript respects multiple recent-activity dropdowns * show recent activities on small screens * fix user-profile layout column in XS * fix search is always marked as active
This commit is contained in:
@@ -83,7 +83,7 @@ export default class KimaiLoader {
|
|||||||
kimai.registerPlugin(new KimaiAlternativeLinks('.alternative-link'));
|
kimai.registerPlugin(new KimaiAlternativeLinks('.alternative-link'));
|
||||||
kimai.registerPlugin(new KimaiAjaxModalForm('.modal-ajax-form'));
|
kimai.registerPlugin(new KimaiAjaxModalForm('.modal-ajax-form'));
|
||||||
kimai.registerPlugin(new KimaiRecentActivities());
|
kimai.registerPlugin(new KimaiRecentActivities());
|
||||||
kimai.registerPlugin(new KimaiActiveRecords('header .messages-menu', 'header .messages-menu-empty'));
|
kimai.registerPlugin(new KimaiActiveRecords());
|
||||||
kimai.registerPlugin(new KimaiAPILink('api-link'));
|
kimai.registerPlugin(new KimaiAPILink('api-link'));
|
||||||
kimai.registerPlugin(new KimaiMultiUpdateTable());
|
kimai.registerPlugin(new KimaiMultiUpdateTable());
|
||||||
kimai.registerPlugin(new KimaiThemeInitializer());
|
kimai.registerPlugin(new KimaiThemeInitializer());
|
||||||
|
|||||||
@@ -13,26 +13,28 @@ import KimaiPlugin from '../KimaiPlugin';
|
|||||||
|
|
||||||
export default class KimaiActiveRecords extends KimaiPlugin {
|
export default class KimaiActiveRecords extends KimaiPlugin {
|
||||||
|
|
||||||
constructor(selector, selectorEmpty) {
|
constructor()
|
||||||
|
{
|
||||||
super();
|
super();
|
||||||
this._selector = selector;
|
this._selector = '.ticktac-menu';
|
||||||
this._selectorEmpty = selectorEmpty;
|
this._selectorEmpty = '.ticktac-menu-empty';
|
||||||
}
|
}
|
||||||
|
|
||||||
getId() {
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
getId()
|
||||||
|
{
|
||||||
return 'active-records';
|
return 'active-records';
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init()
|
||||||
this._menu = document.querySelector(this._selector);
|
{
|
||||||
|
|
||||||
// the menu can be hidden if user has no permissions to see it
|
// the menu can be hidden if user has no permissions to see it
|
||||||
if (this._menu === null) {
|
if (document.querySelector(this._selector) === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.attributes = this._menu.dataset;
|
|
||||||
|
|
||||||
const handleUpdate = () => {
|
const handleUpdate = () => {
|
||||||
this.reloadActiveRecords();
|
this.reloadActiveRecords();
|
||||||
};
|
};
|
||||||
@@ -49,7 +51,9 @@ export default class KimaiActiveRecords extends KimaiPlugin {
|
|||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// handle duration in the visible UI
|
// handle duration in the visible UI
|
||||||
this._updateBrowserTitle = !!this.getConfiguration('updateBrowserTitle');
|
this._updateBrowserTitle = !!this.getConfiguration('updateBrowserTitle');
|
||||||
this._updateDuration();
|
|
||||||
|
// deactivated direct duration update, becuase it is unclear
|
||||||
|
// this._updateDuration();
|
||||||
const handle = () => {
|
const handle = () => {
|
||||||
this._updateDuration();
|
this._updateDuration();
|
||||||
};
|
};
|
||||||
@@ -63,8 +67,15 @@ export default class KimaiActiveRecords extends KimaiPlugin {
|
|||||||
// clearInterval(this._updatesHandler);
|
// clearInterval(this._updatesHandler);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
_updateDuration() {
|
/**
|
||||||
const activeRecords = this._menu.querySelectorAll('[data-since]:not([data-since=""])');
|
* Updates the duration of all running entries, both in the ticktac menus and in the listing pages.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_updateDuration()
|
||||||
|
{
|
||||||
|
// needs to search in document, to find all running entries, both in "ticktac" and listing pages
|
||||||
|
const activeRecords = document.querySelectorAll('[data-since]:not([data-since=""])');
|
||||||
|
|
||||||
if (activeRecords.length === 0) {
|
if (activeRecords.length === 0) {
|
||||||
if (this._updateBrowserTitle) {
|
if (this._updateBrowserTitle) {
|
||||||
@@ -94,57 +105,70 @@ export default class KimaiActiveRecords extends KimaiPlugin {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this._updateBrowserTitle) {
|
if (this._updateBrowserTitle) {
|
||||||
return;
|
// only show the first found record, even if we have more
|
||||||
|
document.title = durations.shift();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let title = durations.shift();
|
/**
|
||||||
for (const duration of durations.slice(0, 2)) {
|
* Adapts the ticktac menus accordingg to the given entries (amount and duration).
|
||||||
title += ' | ' + duration;
|
* Does not influence listing pages, as those refresh themselves.
|
||||||
}
|
*
|
||||||
document.title = title;
|
* @param {array} entries
|
||||||
}
|
* @private
|
||||||
|
*/
|
||||||
_setEntries(entries) {
|
_setEntries(entries)
|
||||||
|
{
|
||||||
const hasEntries = entries.length > 0;
|
const hasEntries = entries.length > 0;
|
||||||
|
|
||||||
this._menu.style.display = hasEntries ? 'inline-block' : 'none';
|
// these contain the "start" button
|
||||||
|
for (let menuEmpty of document.querySelectorAll(this._selectorEmpty)) {
|
||||||
|
menuEmpty.style.display = !hasEntries ? 'inline-block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// and they contain the "stop" button
|
||||||
|
for (let menu of document.querySelectorAll(this._selector)) {
|
||||||
|
menu.style.display = hasEntries ? 'inline-block' : 'none';
|
||||||
if (!hasEntries) {
|
if (!hasEntries) {
|
||||||
// make sure that template entries in the menu are removed, otherwise they
|
// make sure that template entries in the menu are removed, otherwise they
|
||||||
// might still be shown in the browsers title
|
// might still be shown in the browsers title
|
||||||
for (let record of this._menu.querySelectorAll('[data-since]')) {
|
for (let record of menu.querySelectorAll('[data-since]')) {
|
||||||
record.dataset['since'] = '';
|
record.dataset['since'] = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const menuEmpty = document.querySelector(this._selectorEmpty);
|
const stop = menu.querySelector('.ticktac-stop');
|
||||||
if (menuEmpty !== null) {
|
|
||||||
menuEmpty.style.display = !hasEntries ? 'inline-block' : 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
const stop = this._menu.querySelector('.ticktac-stop');
|
|
||||||
|
|
||||||
if (!hasEntries) {
|
if (!hasEntries) {
|
||||||
if (stop) {
|
if (stop) {
|
||||||
stop.accesskey = null;
|
stop.accesskey = null;
|
||||||
}
|
}
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stop) {
|
if (stop) {
|
||||||
stop.accesskey = 's';
|
stop.accesskey = 's';
|
||||||
}
|
}
|
||||||
this._replaceInNode(this._menu, entries[0]);
|
this._replaceInNode(menu, entries[0]);
|
||||||
|
}
|
||||||
|
|
||||||
this._updateDuration();
|
this._updateDuration();
|
||||||
}
|
}
|
||||||
|
|
||||||
_replaceInNode(node, timesheet) {
|
/**
|
||||||
|
* @param {HTMLElement} node
|
||||||
|
* @param {object} timesheet
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_replaceInNode(node, timesheet)
|
||||||
|
{
|
||||||
const date = this.getDateUtils();
|
const date = this.getDateUtils();
|
||||||
const allReplacer = node.querySelectorAll('[data-replacer]');
|
const allReplacer = node.querySelectorAll('[data-replacer]');
|
||||||
for (let link of allReplacer) {
|
for (let link of allReplacer) {
|
||||||
const replacerName = link.dataset['replacer'];
|
const replacerName = link.dataset['replacer'];
|
||||||
if (replacerName === 'url') {
|
if (replacerName === 'url') {
|
||||||
link.href = this.attributes['href'].replace('000', timesheet.id);
|
link.href = node.dataset['href'].replace('000', timesheet.id);
|
||||||
} else if (replacerName === 'activity') {
|
} else if (replacerName === 'activity') {
|
||||||
link.innerText = timesheet.activity.name;
|
link.innerText = timesheet.activity.name;
|
||||||
} else if (replacerName === 'project') {
|
} else if (replacerName === 'project') {
|
||||||
@@ -158,11 +182,15 @@ export default class KimaiActiveRecords extends KimaiPlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reloadActiveRecords() {
|
reloadActiveRecords()
|
||||||
|
{
|
||||||
/** @type {KimaiAPI} API */
|
/** @type {KimaiAPI} API */
|
||||||
const API = this.getContainer().getPlugin('api');
|
const API = this.getContainer().getPlugin('api');
|
||||||
|
|
||||||
API.get(this.attributes['api'], {}, (result) => {
|
// TODO using the first found "ticktac" menu is working, but can be done better
|
||||||
|
const apiUrl = document.querySelector(this._selector).dataset['api'];
|
||||||
|
|
||||||
|
API.get(apiUrl, {}, (result) => {
|
||||||
this._setEntries(result);
|
this._setEntries(result);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -243,8 +243,7 @@ export default class KimaiDateUtils extends KimaiPlugin {
|
|||||||
format = '-' + format;
|
format = '-' + format;
|
||||||
}
|
}
|
||||||
|
|
||||||
return format.replace('%h', (hours < 10 ? '0' + hours : hours)).replace('%m', ('0' + minutes).slice(-2));
|
return format.replace('%h', hours.toString()).replace('%m', ('0' + minutes).slice(-2));
|
||||||
//return format.replace('%h', (hours < 10 ? '0' + hours : hours)).replace('%m', ('0' + minutes).slice(-2)).replace('%s', ('0' + seconds).slice(-2));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -13,6 +13,15 @@ import KimaiPlugin from '../KimaiPlugin';
|
|||||||
|
|
||||||
export default class KimaiRecentActivities extends KimaiPlugin {
|
export default class KimaiRecentActivities extends KimaiPlugin {
|
||||||
|
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this._selector = '.notifications-menu';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
getId()
|
getId()
|
||||||
{
|
{
|
||||||
return 'recent-activities';
|
return 'recent-activities';
|
||||||
@@ -20,15 +29,17 @@ export default class KimaiRecentActivities extends KimaiPlugin {
|
|||||||
|
|
||||||
init()
|
init()
|
||||||
{
|
{
|
||||||
this.menu = document.querySelector('header .notifications-menu');
|
const menus = document.querySelectorAll(this._selector);
|
||||||
|
|
||||||
// the menu can be hidden if user has no permissions to see it
|
// the menu can be hidden if user has no permissions to see it
|
||||||
// or no timesheet was recorded yet
|
// or no timesheet was recorded yet
|
||||||
if (this.menu === null || this.menu.dataset['reload'] === undefined) {
|
if (menus.length === 0 || menus[0].dataset['reload'] === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handle = () => {
|
const handle = () => {
|
||||||
this._reloadMenu(this.menu.dataset['reload']);
|
// TODO this works but using the first menu is not ideal, pass in the reload URL?
|
||||||
|
this._reloadMenu(menus[0].dataset['reload']);
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('kimai.recentActivities', handle);
|
document.addEventListener('kimai.recentActivities', handle);
|
||||||
@@ -44,9 +55,12 @@ export default class KimaiRecentActivities extends KimaiPlugin {
|
|||||||
this._attachAddRemoveFavorite();
|
this._attachAddRemoveFavorite();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
_attachAddRemoveFavorite()
|
_attachAddRemoveFavorite()
|
||||||
{
|
{
|
||||||
[].slice.call(this.menu.querySelectorAll('a.list-group-item-actions')).map((element) => {
|
[].slice.call(document.querySelectorAll(this._selector + ' a.list-group-item-actions')).map((element) => {
|
||||||
element.addEventListener('click', (event) => {
|
element.addEventListener('click', (event) => {
|
||||||
this._reloadMenu(event.currentTarget.href);
|
this._reloadMenu(event.currentTarget.href);
|
||||||
|
|
||||||
@@ -58,26 +72,33 @@ export default class KimaiRecentActivities extends KimaiPlugin {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload all ercent activities and update the existing menus.
|
||||||
|
*
|
||||||
|
* @param {string} url
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
_reloadMenu(url)
|
_reloadMenu(url)
|
||||||
{
|
{
|
||||||
this.fetch(url, {method: 'GET'})
|
this.fetch(url, {method: 'GET'})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
//this.menu.remove();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.text().then(html => {
|
return response.text().then(html => {
|
||||||
const newFormHtml = document.createElement('div');
|
const newFormHtml = document.createElement('div');
|
||||||
newFormHtml.innerHTML = html;
|
newFormHtml.innerHTML = html;
|
||||||
this.menu.replaceWith(newFormHtml.firstElementChild);
|
|
||||||
|
|
||||||
this.menu = document.querySelector('header .notifications-menu');
|
for (let menu of document.querySelectorAll(this._selector)) {
|
||||||
|
menu.replaceWith(newFormHtml.firstElementChild.cloneNode(true));
|
||||||
|
}
|
||||||
|
|
||||||
this._attachAddRemoveFavorite();
|
this._attachAddRemoveFavorite();
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((reason) => {
|
||||||
//this.menu.remove();
|
console.log('Failed to log recent activities', reason);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
assets/sass/fontawesome.scss
vendored
8
assets/sass/fontawesome.scss
vendored
@@ -6,10 +6,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
$fa-font-path: "~@fortawesome/fontawesome-free/webfonts/";
|
$fa-font-path: "~@fortawesome/fontawesome-free/webfonts/";
|
||||||
@import '~@fortawesome/fontawesome-free/scss/fontawesome';
|
@import "@fortawesome/fontawesome-free/scss/fontawesome";
|
||||||
@import '~@fortawesome/fontawesome-free/scss/regular';
|
@import "@fortawesome/fontawesome-free/scss/regular";
|
||||||
@import '~@fortawesome/fontawesome-free/scss/solid';
|
@import "@fortawesome/fontawesome-free/scss/solid";
|
||||||
@import '~@fortawesome/fontawesome-free/scss/brands';
|
@import "@fortawesome/fontawesome-free/scss/brands";
|
||||||
|
|
||||||
/* all icon-buttons with fontawesome icons */
|
/* all icon-buttons with fontawesome icons */
|
||||||
.btn-icon i.icon {
|
.btn-icon i.icon {
|
||||||
|
|||||||
@@ -5,17 +5,17 @@
|
|||||||
* file that was distributed with this source code.
|
* file that was distributed with this source code.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@import "~bootstrap/scss/functions";
|
@import "bootstrap/scss/functions";
|
||||||
@import "~bootstrap/scss/variables";
|
@import "bootstrap/scss/variables";
|
||||||
@import "~bootstrap/scss/maps";
|
@import "bootstrap/scss/maps";
|
||||||
@import "~bootstrap/scss/mixins";
|
@import "bootstrap/scss/mixins";
|
||||||
@import "~bootstrap/scss/reboot";
|
@import "bootstrap/scss/reboot";
|
||||||
@import "~bootstrap/scss/type";
|
@import "bootstrap/scss/type";
|
||||||
@import "~bootstrap/scss/grid";
|
@import "bootstrap/scss/grid";
|
||||||
@import "~bootstrap/scss/tables";
|
@import "bootstrap/scss/tables";
|
||||||
@import "~bootstrap/scss/list-group";
|
@import "bootstrap/scss/list-group";
|
||||||
@import "~bootstrap/scss/card";
|
@import "bootstrap/scss/card";
|
||||||
@import "~bootstrap/scss/utilities";
|
@import "bootstrap/scss/utilities";
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: $font-family-sans-serif;
|
font-family: $font-family-sans-serif;
|
||||||
|
|||||||
@@ -5,11 +5,22 @@
|
|||||||
border-right: 0;
|
border-right: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@include media-breakpoint-down(sm) {
|
||||||
|
.notifications-menu .dropdown-menu-card {
|
||||||
|
width: 100vw;
|
||||||
|
right: 0;
|
||||||
|
position: fixed;
|
||||||
|
top: 56px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 360px) {
|
@media (min-width: 360px) {
|
||||||
.inline-search {
|
.inline-search {
|
||||||
max-width: 235px;
|
max-width: 235px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@include media-breakpoint-up(md) {
|
@include media-breakpoint-up(md) {
|
||||||
.inline-search {
|
.inline-search {
|
||||||
max-width: 325px;
|
max-width: 325px;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import '~tom-select/dist/scss/tom-select.bootstrap5';
|
@import "tom-select/dist/scss/tom-select.bootstrap5";
|
||||||
|
|
||||||
/* bootstrap thead.sticky-top has z-index 1020 and tomselect dropdowns often hide behind that */
|
/* bootstrap thead.sticky-top has z-index 1020 and tomselect dropdowns often hide behind that */
|
||||||
.ts-dropdown {
|
.ts-dropdown {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import '~@tabler/core/src/scss/variables';
|
@import "@tabler/core/src/scss/variables";
|
||||||
|
|
||||||
/* fix for vertical labels in date-time pickers, if they are used in half-column forms, e.g. "create tasks" or "create expenses" */
|
/* fix for vertical labels in date-time pickers, if they are used in half-column forms, e.g. "create tasks" or "create expenses" */
|
||||||
fieldset legend.col-form-label {
|
fieldset legend.col-form-label {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
.ticktac-stop {
|
.ticktac-stop {
|
||||||
i {
|
i {
|
||||||
animation: ticktac-blink 2s step-end infinite;
|
animation: ticktac-blink 2s step-end infinite;
|
||||||
|
font-size: 140%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,12 @@
|
|||||||
* file that was distributed with this source code.
|
* file that was distributed with this source code.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@import "~bootstrap/scss/functions";
|
@import "bootstrap/scss/functions";
|
||||||
@import "~bootstrap/scss/variables";
|
@import "bootstrap/scss/variables";
|
||||||
@import "~bootstrap/scss/mixins";
|
@import "bootstrap/scss/mixins";
|
||||||
|
|
||||||
@import '~@tabler/core/src/scss/variables';
|
@import "@tabler/core/src/scss/variables";
|
||||||
@import '~@tabler/core/src/scss/mixins';
|
@import "@tabler/core/src/scss/mixins";
|
||||||
|
|
||||||
/* For highlighting rows, fieldsets and so on */
|
/* For highlighting rows, fieldsets and so on */
|
||||||
$highlight-bg: #f5f5f5;
|
$highlight-bg: #f5f5f5;
|
||||||
|
|||||||
42
composer.lock
generated
42
composer.lock
generated
@@ -2055,16 +2055,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "gedmo/doctrine-extensions",
|
"name": "gedmo/doctrine-extensions",
|
||||||
"version": "v3.11.0",
|
"version": "v3.11.1",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/doctrine-extensions/DoctrineExtensions.git",
|
"url": "https://github.com/doctrine-extensions/DoctrineExtensions.git",
|
||||||
"reference": "a2364710d2f367f74d9f041c3356f2929b8c6690"
|
"reference": "ae4bdf0d567e06b6bb1902a560ee78961b230953"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/doctrine-extensions/DoctrineExtensions/zipball/a2364710d2f367f74d9f041c3356f2929b8c6690",
|
"url": "https://api.github.com/repos/doctrine-extensions/DoctrineExtensions/zipball/ae4bdf0d567e06b6bb1902a560ee78961b230953",
|
||||||
"reference": "a2364710d2f367f74d9f041c3356f2929b8c6690",
|
"reference": "ae4bdf0d567e06b6bb1902a560ee78961b230953",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -2136,16 +2136,18 @@
|
|||||||
"email": "david@liip.ch"
|
"email": "david@liip.ch"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Doctrine2 behavioral extensions",
|
"description": "Doctrine behavioral extensions",
|
||||||
"homepage": "http://gediminasm.org/",
|
"homepage": "http://gediminasm.org/",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"Blameable",
|
"Blameable",
|
||||||
"behaviors",
|
"behaviors",
|
||||||
"doctrine2",
|
"doctrine",
|
||||||
"extensions",
|
"extensions",
|
||||||
"gedmo",
|
"gedmo",
|
||||||
"loggable",
|
"loggable",
|
||||||
"nestedset",
|
"nestedset",
|
||||||
|
"odm",
|
||||||
|
"orm",
|
||||||
"sluggable",
|
"sluggable",
|
||||||
"sortable",
|
"sortable",
|
||||||
"timestampable",
|
"timestampable",
|
||||||
@@ -2156,10 +2158,10 @@
|
|||||||
"support": {
|
"support": {
|
||||||
"email": "gediminas.morkevicius@gmail.com",
|
"email": "gediminas.morkevicius@gmail.com",
|
||||||
"issues": "https://github.com/doctrine-extensions/DoctrineExtensions/issues",
|
"issues": "https://github.com/doctrine-extensions/DoctrineExtensions/issues",
|
||||||
"source": "https://github.com/doctrine-extensions/DoctrineExtensions/tree/v3.11.0",
|
"source": "https://github.com/doctrine-extensions/DoctrineExtensions/tree/v3.11.1",
|
||||||
"wiki": "https://github.com/Atlantic18/DoctrineExtensions/tree/main/doc"
|
"wiki": "https://github.com/Atlantic18/DoctrineExtensions/tree/main/doc"
|
||||||
},
|
},
|
||||||
"time": "2023-01-26T00:14:07+00:00"
|
"time": "2023-02-20T19:24:07+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "jeroendesloovere/vcard",
|
"name": "jeroendesloovere/vcard",
|
||||||
@@ -6179,16 +6181,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/flex",
|
"name": "symfony/flex",
|
||||||
"version": "v2.2.4",
|
"version": "v2.2.5",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/flex.git",
|
"url": "https://github.com/symfony/flex.git",
|
||||||
"reference": "52baff1adb29faf443c6710cb775bd88b9627381"
|
"reference": "2ff8465e7172790a47ab3c129f2b514eb2d8a286"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/flex/zipball/52baff1adb29faf443c6710cb775bd88b9627381",
|
"url": "https://api.github.com/repos/symfony/flex/zipball/2ff8465e7172790a47ab3c129f2b514eb2d8a286",
|
||||||
"reference": "52baff1adb29faf443c6710cb775bd88b9627381",
|
"reference": "2ff8465e7172790a47ab3c129f2b514eb2d8a286",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -6224,7 +6226,7 @@
|
|||||||
"description": "Composer plugin for Symfony",
|
"description": "Composer plugin for Symfony",
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/symfony/flex/issues",
|
"issues": "https://github.com/symfony/flex/issues",
|
||||||
"source": "https://github.com/symfony/flex/tree/v2.2.4"
|
"source": "https://github.com/symfony/flex/tree/v2.2.5"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -6240,7 +6242,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2022-12-20T07:19:39+00:00"
|
"time": "2023-02-18T08:03:15+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/form",
|
"name": "symfony/form",
|
||||||
@@ -10360,16 +10362,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "zircote/swagger-php",
|
"name": "zircote/swagger-php",
|
||||||
"version": "4.6.2",
|
"version": "4.7.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/zircote/swagger-php.git",
|
"url": "https://github.com/zircote/swagger-php.git",
|
||||||
"reference": "14d6c5e2bde957e03a3a5496a3b18419a0480199"
|
"reference": "f2bcbd5aa9d838d28bf5665c70f88eda6adc22cb"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/zircote/swagger-php/zipball/14d6c5e2bde957e03a3a5496a3b18419a0480199",
|
"url": "https://api.github.com/repos/zircote/swagger-php/zipball/f2bcbd5aa9d838d28bf5665c70f88eda6adc22cb",
|
||||||
"reference": "14d6c5e2bde957e03a3a5496a3b18419a0480199",
|
"reference": "f2bcbd5aa9d838d28bf5665c70f88eda6adc22cb",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -10432,9 +10434,9 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/zircote/swagger-php/issues",
|
"issues": "https://github.com/zircote/swagger-php/issues",
|
||||||
"source": "https://github.com/zircote/swagger-php/tree/4.6.2"
|
"source": "https://github.com/zircote/swagger-php/tree/4.7.0"
|
||||||
},
|
},
|
||||||
"time": "2023-02-14T22:54:06+00:00"
|
"time": "2023-02-20T23:05:35+00:00"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"packages-dev": [
|
"packages-dev": [
|
||||||
|
|||||||
205
phpstan.neon
205
phpstan.neon
@@ -1040,21 +1040,6 @@ parameters:
|
|||||||
count: 2
|
count: 2
|
||||||
path: src/Controller/CalendarController.php
|
path: src/Controller/CalendarController.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#1 \\$address of method JeroenDesloovere\\\\VCard\\\\VCard\\:\\:addEmail\\(\\) expects string, string\\|null given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/CustomerController.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#1 \\$company of method JeroenDesloovere\\\\VCard\\\\VCard\\:\\:addCompany\\(\\) expects string, string\\|null given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/CustomerController.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#1 \\$country of static method Symfony\\\\Component\\\\Intl\\\\Countries\\:\\:getName\\(\\) expects string, string\\|null given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/CustomerController.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#1 \\$filename of static method App\\\\Utils\\\\FileHelper\\:\\:convertToAsciiFilename\\(\\) expects string, string\\|null given\\.$#"
|
message: "#^Parameter \\#1 \\$filename of static method App\\\\Utils\\\\FileHelper\\:\\:convertToAsciiFilename\\(\\) expects string, string\\|null given\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -1075,11 +1060,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/Controller/CustomerController.php
|
path: src/Controller/CustomerController.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#2 \\$string of function explode expects string, string\\|null given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/CustomerController.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#"
|
message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#"
|
||||||
count: 2
|
count: 2
|
||||||
@@ -1165,11 +1145,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/Controller/ExportController.php
|
path: src/Controller/ExportController.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#1 \\$route of method Symfony\\\\Bundle\\\\FrameworkBundle\\\\Controller\\\\AbstractController\\:\\:redirectToRoute\\(\\) expects string, bool\\|float\\|int\\|string\\|null given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/HomepageController.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Cannot call method getEntries\\(\\) on App\\\\Invoice\\\\CalculatorInterface\\|null\\.$#"
|
message: "#^Cannot call method getEntries\\(\\) on App\\\\Invoice\\\\CalculatorInterface\\|null\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -2900,151 +2875,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Export\\\\Base\\\\AbstractSpreadsheetRenderer\\:\\:getColumns\\(\\) has parameter \\$columns with no value type specified in iterable type array\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Export\\\\Base\\\\AbstractSpreadsheetRenderer\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Export\\\\Base\\\\AbstractSpreadsheetRenderer\\:\\:setDuration\\(\\) has parameter \\$column with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Export\\\\Base\\\\AbstractSpreadsheetRenderer\\:\\:setDuration\\(\\) has parameter \\$duration with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Export\\\\Base\\\\AbstractSpreadsheetRenderer\\:\\:setDuration\\(\\) has parameter \\$row with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Export\\\\Base\\\\AbstractSpreadsheetRenderer\\:\\:setDurationTotal\\(\\) has parameter \\$column with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Export\\\\Base\\\\AbstractSpreadsheetRenderer\\:\\:setDurationTotal\\(\\) has parameter \\$endCoordinate with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Export\\\\Base\\\\AbstractSpreadsheetRenderer\\:\\:setDurationTotal\\(\\) has parameter \\$row with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Export\\\\Base\\\\AbstractSpreadsheetRenderer\\:\\:setDurationTotal\\(\\) has parameter \\$startCoordinate with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Export\\\\Base\\\\AbstractSpreadsheetRenderer\\:\\:setFormattedDate\\(\\) has parameter \\$column with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Export\\\\Base\\\\AbstractSpreadsheetRenderer\\:\\:setFormattedDate\\(\\) has parameter \\$row with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Export\\\\Base\\\\AbstractSpreadsheetRenderer\\:\\:setFormattedDateTime\\(\\) has parameter \\$column with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Export\\\\Base\\\\AbstractSpreadsheetRenderer\\:\\:setFormattedDateTime\\(\\) has parameter \\$row with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Export\\\\Base\\\\AbstractSpreadsheetRenderer\\:\\:setFormattedTime\\(\\) has parameter \\$column with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Export\\\\Base\\\\AbstractSpreadsheetRenderer\\:\\:setFormattedTime\\(\\) has parameter \\$row with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Export\\\\Base\\\\AbstractSpreadsheetRenderer\\:\\:setRate\\(\\) has parameter \\$column with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Export\\\\Base\\\\AbstractSpreadsheetRenderer\\:\\:setRate\\(\\) has parameter \\$currency with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Export\\\\Base\\\\AbstractSpreadsheetRenderer\\:\\:setRate\\(\\) has parameter \\$rate with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Export\\\\Base\\\\AbstractSpreadsheetRenderer\\:\\:setRate\\(\\) has parameter \\$row with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Export\\\\Base\\\\AbstractSpreadsheetRenderer\\:\\:setRateStyle\\(\\) has parameter \\$column with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Export\\\\Base\\\\AbstractSpreadsheetRenderer\\:\\:setRateStyle\\(\\) has parameter \\$currency with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Export\\\\Base\\\\AbstractSpreadsheetRenderer\\:\\:setRateStyle\\(\\) has parameter \\$rate with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Export\\\\Base\\\\AbstractSpreadsheetRenderer\\:\\:setRateStyle\\(\\) has parameter \\$row with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Export\\\\Base\\\\AbstractSpreadsheetRenderer\\:\\:setRateTotal\\(\\) has parameter \\$column with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Export\\\\Base\\\\AbstractSpreadsheetRenderer\\:\\:setRateTotal\\(\\) has parameter \\$endCoordinate with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Export\\\\Base\\\\AbstractSpreadsheetRenderer\\:\\:setRateTotal\\(\\) has parameter \\$row with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Export\\\\Base\\\\AbstractSpreadsheetRenderer\\:\\:setRateTotal\\(\\) has parameter \\$startCoordinate with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#1 \\$columnIndex of method PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\:\\:getCellByColumnAndRow\\(\\) expects int, float\\|int given\\.$#"
|
|
||||||
count: 6
|
|
||||||
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#1 \\$columnIndex1 of method PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\:\\:getStyleByColumnAndRow\\(\\) expects int, float\\|int given\\.$#"
|
|
||||||
count: 3
|
|
||||||
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#1 \\$file of class Symfony\\\\Component\\\\HttpFoundation\\\\BinaryFileResponse constructor expects SplFileInfo\\|string, mixed given\\.$#"
|
message: "#^Parameter \\#1 \\$file of class Symfony\\\\Component\\\\HttpFoundation\\\\BinaryFileResponse constructor expects SplFileInfo\\|string, mixed given\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -3080,41 +2910,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Export\\\\Base\\\\CsvRenderer\\:\\:setDuration\\(\\) has parameter \\$column with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Export/Base/CsvRenderer.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Export\\\\Base\\\\CsvRenderer\\:\\:setDuration\\(\\) has parameter \\$duration with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Export/Base/CsvRenderer.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Export\\\\Base\\\\CsvRenderer\\:\\:setDuration\\(\\) has parameter \\$row with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Export/Base/CsvRenderer.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Export\\\\Base\\\\CsvRenderer\\:\\:setRateStyle\\(\\) has parameter \\$column with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Export/Base/CsvRenderer.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Export\\\\Base\\\\CsvRenderer\\:\\:setRateStyle\\(\\) has parameter \\$currency with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Export/Base/CsvRenderer.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Export\\\\Base\\\\CsvRenderer\\:\\:setRateStyle\\(\\) has parameter \\$rate with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Export/Base/CsvRenderer.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Export\\\\Base\\\\CsvRenderer\\:\\:setRateStyle\\(\\) has parameter \\$row with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Export/Base/CsvRenderer.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Cannot call method getBudget\\(\\) on App\\\\Entity\\\\Project\\|null\\.$#"
|
message: "#^Cannot call method getBudget\\(\\) on App\\\\Entity\\\\Project\\|null\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!-- https://phpunit.de/manual/current/en/appendixes.configuration.html -->
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" backupGlobals="false" colors="true" bootstrap="tests/bootstrap.php">
|
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
|
||||||
|
backupGlobals="false"
|
||||||
|
colors="true"
|
||||||
|
bootstrap="tests/bootstrap.php">
|
||||||
<php>
|
<php>
|
||||||
<ini name="error_reporting" value="-1"/>
|
<ini name="error_reporting" value="-1"/>
|
||||||
<ini name="max_execution_time" value="-1"/>
|
<ini name="max_execution_time" value="-1"/>
|
||||||
@@ -12,12 +15,6 @@
|
|||||||
<env name="APP_ENV" value="test" force="true"/>
|
<env name="APP_ENV" value="test" force="true"/>
|
||||||
<env name="APP_DEBUG" value="0" force="true"/>
|
<env name="APP_DEBUG" value="0" force="true"/>
|
||||||
<env name="APP_SECRET" value="5a79a1c866efef9ca1800f971d689f3e" force="true"/>
|
<env name="APP_SECRET" value="5a79a1c866efef9ca1800f971d689f3e" force="true"/>
|
||||||
<!--
|
|
||||||
CREATE DATABASE IF NOT EXISTS `kimai2_test`;
|
|
||||||
CREATE USER IF NOT EXISTS `kimai2_test`@127.0.0.1;
|
|
||||||
ALTER USER `kimai2_test`@127.0.0.1 IDENTIFIED BY "kimai2_test";
|
|
||||||
GRANT execute,select,insert,update,delete,create,alter,drop,index,references ON `kimai2_test`.* TO kimai2_test@127.0.0.1;
|
|
||||||
-->
|
|
||||||
<env name="DATABASE_URL" value="mysql://kimai2_test:kimai2_test@127.0.0.1:3306/kimai2_test"/>
|
<env name="DATABASE_URL" value="mysql://kimai2_test:kimai2_test@127.0.0.1:3306/kimai2_test"/>
|
||||||
<env name="CORS_ALLOW_ORIGIN" value="^https?://localhost(:[0-9]+)?$"/>
|
<env name="CORS_ALLOW_ORIGIN" value="^https?://localhost(:[0-9]+)?$"/>
|
||||||
<env name="MAILER_URL" value="null://null"/>
|
<env name="MAILER_URL" value="null://null"/>
|
||||||
@@ -25,10 +22,14 @@
|
|||||||
<!--
|
<!--
|
||||||
REINSTALL THE TEST DATABASE, eg. AFTER CHANGING STRUCTURE!
|
REINSTALL THE TEST DATABASE, eg. AFTER CHANGING STRUCTURE!
|
||||||
|
|
||||||
Simple:
|
|
||||||
- change to "true"
|
- change to "true"
|
||||||
- composer tests
|
- composer tests
|
||||||
- change back to "false"
|
- change back to "false"
|
||||||
|
|
||||||
|
CREATE DATABASE IF NOT EXISTS `kimai2_test`;
|
||||||
|
CREATE USER IF NOT EXISTS `kimai2_test`@127.0.0.1;
|
||||||
|
ALTER USER `kimai2_test`@127.0.0.1 IDENTIFIED BY "kimai2_test";
|
||||||
|
GRANT execute,select,insert,update,delete,create,alter,drop,index,references ON `kimai2_test`.* TO kimai2_test@127.0.0.1;
|
||||||
-->
|
-->
|
||||||
<env name="BOOTSTRAP_RESET_DATABASE" value="true"/>
|
<env name="BOOTSTRAP_RESET_DATABASE" value="true"/>
|
||||||
</php>
|
</php>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
2
public/build/app.50e94a4a.js
Normal file
2
public/build/app.50e94a4a.js
Normal file
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
@@ -3,10 +3,10 @@
|
|||||||
"app": {
|
"app": {
|
||||||
"js": [
|
"js": [
|
||||||
"/build/runtime.f0079159.js",
|
"/build/runtime.f0079159.js",
|
||||||
"/build/app.8b0c21dc.js"
|
"/build/app.50e94a4a.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"/build/app.73c27235.css"
|
"/build/app.2c6c5920.css"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"export-pdf": {
|
"export-pdf": {
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
"calendar": {
|
"calendar": {
|
||||||
"js": [
|
"js": [
|
||||||
"/build/runtime.f0079159.js",
|
"/build/runtime.f0079159.js",
|
||||||
"/build/calendar.7dd54a5f.js"
|
"/build/calendar.3041e41e.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"/build/calendar.bb473428.css"
|
"/build/calendar.bb473428.css"
|
||||||
@@ -63,8 +63,8 @@
|
|||||||
},
|
},
|
||||||
"integrity": {
|
"integrity": {
|
||||||
"/build/runtime.f0079159.js": "sha384-H22sAW1aTvyIPqvHOvGXWSWTxf0y6mptp+MsVmyXCfjx/WJjBbhX9gbUZ+qIuihV",
|
"/build/runtime.f0079159.js": "sha384-H22sAW1aTvyIPqvHOvGXWSWTxf0y6mptp+MsVmyXCfjx/WJjBbhX9gbUZ+qIuihV",
|
||||||
"/build/app.8b0c21dc.js": "sha384-k0LUmYHZUK6bFVymGMzcHSq+utW09iR+YG6VMzqCiXfBGtVvb2Sdr0IB7w6hR+I6",
|
"/build/app.50e94a4a.js": "sha384-HR3cG9DvdBAyo08dhybZE8eNOvFY+3Yvp2zxFC43hcwTK6dGoAOLTyNhTdoGi8CR",
|
||||||
"/build/app.73c27235.css": "sha384-0vdzXrL3JuXTfiYJUdY17wIGFIbUTzQWfBQXbhcXcFRVmpmJYY1TZYSauxVUNwev",
|
"/build/app.2c6c5920.css": "sha384-qiubu7JONCsPTbVOb8bSJ4GYxioCwIUvlsI0usMmIUyoTxawn6jsjG7KK64iej5S",
|
||||||
"/build/export-pdf.587575e7.js": "sha384-J50GStmmfVwUTN4dIRQ02eg9hyzGFPSzpTtpPody92j0V6zCqw+s5l8+ZhVTugeW",
|
"/build/export-pdf.587575e7.js": "sha384-J50GStmmfVwUTN4dIRQ02eg9hyzGFPSzpTtpPody92j0V6zCqw+s5l8+ZhVTugeW",
|
||||||
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
|
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
|
||||||
"/build/invoice.c8ae95ad.js": "sha384-2eVY7MBiMQxo1vhfizU+fYfEZbz9bYUdzqxnpTQhxpYiLaxeSV4WnaObq2G7/Pks",
|
"/build/invoice.c8ae95ad.js": "sha384-2eVY7MBiMQxo1vhfizU+fYfEZbz9bYUdzqxnpTQhxpYiLaxeSV4WnaObq2G7/Pks",
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
"/build/invoice-pdf.d86b82ee.js": "sha384-A0HJqP+MvEqQr1uG8wViCeEWxBRKyS6l8D+Ao4pFYHUA12gCC1gRYhk9I+SJPvZq",
|
"/build/invoice-pdf.d86b82ee.js": "sha384-A0HJqP+MvEqQr1uG8wViCeEWxBRKyS6l8D+Ao4pFYHUA12gCC1gRYhk9I+SJPvZq",
|
||||||
"/build/invoice-pdf.c88953bb.css": "sha384-ZvSi1e+ZKGzvZJUtAPLjzOSTh13N9zRevq44GKdYdBja/DAplGE55saY2Ur+83yv",
|
"/build/invoice-pdf.c88953bb.css": "sha384-ZvSi1e+ZKGzvZJUtAPLjzOSTh13N9zRevq44GKdYdBja/DAplGE55saY2Ur+83yv",
|
||||||
"/build/chart.f5becfac.js": "sha384-GSqETm8wULiVXyizvwRompfwu63r/C0Qd/AvrHDE4cqAKiIGCssb3QyBtGu1WN+W",
|
"/build/chart.f5becfac.js": "sha384-GSqETm8wULiVXyizvwRompfwu63r/C0Qd/AvrHDE4cqAKiIGCssb3QyBtGu1WN+W",
|
||||||
"/build/calendar.7dd54a5f.js": "sha384-oUWTIvkHr+k1UrlriT4+iA+irValKNLFoxva7ljQ/bsbczzaql3uWD0dlC4mF0ss",
|
"/build/calendar.3041e41e.js": "sha384-vdX0khIprtUhM4P8aiSsY5H3YSjHM1OMNdiYIhoKy/fm//5gwaEPkSbZDsTbe5dT",
|
||||||
"/build/calendar.bb473428.css": "sha384-900W9o8666Qpw21rLP7hn5Ql8tWd40k0IcE3QpFm7E7V6bKva7xQgGlyzVuuh4GK",
|
"/build/calendar.bb473428.css": "sha384-900W9o8666Qpw21rLP7hn5Ql8tWd40k0IcE3QpFm7E7V6bKva7xQgGlyzVuuh4GK",
|
||||||
"/build/dashboard.6774a712.js": "sha384-lBwkNqUPv+IBbznagiFakY2BOIueq/Bg89wHO2XeM9YAZ51rPkrvdMd+XmFupHsM",
|
"/build/dashboard.6774a712.js": "sha384-lBwkNqUPv+IBbznagiFakY2BOIueq/Bg89wHO2XeM9YAZ51rPkrvdMd+XmFupHsM",
|
||||||
"/build/dashboard.18f5a8b7.css": "sha384-PBD8ftb2yBoSjRe2sN5Xb4dEz045kOEUfcIufdSis+tWoWdAx5j4Yic+F/6CYbrP"
|
"/build/dashboard.18f5a8b7.css": "sha384-PBD8ftb2yBoSjRe2sN5Xb4dEz045kOEUfcIufdSis+tWoWdAx5j4Yic+F/6CYbrP"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"build/app.css": "/build/app.73c27235.css",
|
"build/app.css": "/build/app.2c6c5920.css",
|
||||||
"build/app.js": "/build/app.8b0c21dc.js",
|
"build/app.js": "/build/app.50e94a4a.js",
|
||||||
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
|
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
|
||||||
"build/export-pdf.js": "/build/export-pdf.587575e7.js",
|
"build/export-pdf.js": "/build/export-pdf.587575e7.js",
|
||||||
"build/invoice.css": "/build/invoice.3c80ee80.css",
|
"build/invoice.css": "/build/invoice.3c80ee80.css",
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
"build/invoice-pdf.js": "/build/invoice-pdf.d86b82ee.js",
|
"build/invoice-pdf.js": "/build/invoice-pdf.d86b82ee.js",
|
||||||
"build/chart.js": "/build/chart.f5becfac.js",
|
"build/chart.js": "/build/chart.f5becfac.js",
|
||||||
"build/calendar.css": "/build/calendar.bb473428.css",
|
"build/calendar.css": "/build/calendar.bb473428.css",
|
||||||
"build/calendar.js": "/build/calendar.7dd54a5f.js",
|
"build/calendar.js": "/build/calendar.3041e41e.js",
|
||||||
"build/dashboard.css": "/build/dashboard.18f5a8b7.css",
|
"build/dashboard.css": "/build/dashboard.18f5a8b7.css",
|
||||||
"build/dashboard.js": "/build/dashboard.6774a712.js",
|
"build/dashboard.js": "/build/dashboard.6774a712.js",
|
||||||
"build/runtime.js": "/build/runtime.f0079159.js"
|
"build/runtime.js": "/build/runtime.f0079159.js"
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ class Constants
|
|||||||
/**
|
/**
|
||||||
* The current release version
|
* The current release version
|
||||||
*/
|
*/
|
||||||
public const VERSION = '2.0.1';
|
public const VERSION = '2.0.2';
|
||||||
/**
|
/**
|
||||||
* The current release: major * 10000 + minor * 100 + patch
|
* The current release: major * 10000 + minor * 100 + patch
|
||||||
*/
|
*/
|
||||||
public const VERSION_ID = 20001;
|
public const VERSION_ID = 20002;
|
||||||
/**
|
/**
|
||||||
* The software name
|
* The software name
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -368,20 +368,39 @@ final class CustomerController extends AbstractController
|
|||||||
$vcard = new VCard();
|
$vcard = new VCard();
|
||||||
|
|
||||||
$contact = $customer->getContact() ?? $customer->getName();
|
$contact = $customer->getContact() ?? $customer->getName();
|
||||||
|
if ($contact === null || \strlen($contact) === 0) {
|
||||||
|
$contact = 'Unknown';
|
||||||
|
}
|
||||||
|
|
||||||
$contact = explode(' ', $contact);
|
$contact = explode(' ', $contact);
|
||||||
$lastname = array_pop($contact);
|
$lastname = array_pop($contact);
|
||||||
$firstname = \count($contact) > 0 ? $contact[0] : $lastname;
|
$firstname = \count($contact) > 0 ? $contact[0] : '';
|
||||||
|
$vcard->addName($lastname, $firstname);
|
||||||
|
|
||||||
$note = $customer->getComment();
|
$note = $customer->getComment();
|
||||||
if ($note !== null) {
|
if ($note !== null) {
|
||||||
$note .= PHP_EOL;
|
$note .= PHP_EOL;
|
||||||
}
|
}
|
||||||
|
|
||||||
$vcard->addName($lastname, $firstname);
|
$address = $customer->getAddress();
|
||||||
$vcard->addNote($note . $customer->getAddress());
|
if (!empty($note) || !empty($address)) {
|
||||||
$vcard->addAddress(null, null, null, null, null, null, Countries::getName($customer->getCountry()));
|
$vcard->addNote($note . $address);
|
||||||
|
}
|
||||||
|
|
||||||
$vcard->addCompany($customer->getCompany() ?? $customer->getName());
|
$country = $customer->getCountry();
|
||||||
$vcard->addEmail($customer->getEmail());
|
if ($country !== null) {
|
||||||
|
$vcard->addAddress(null, null, null, null, null, null, Countries::getName($country));
|
||||||
|
}
|
||||||
|
|
||||||
|
$company = $customer->getCompany();
|
||||||
|
if ($company !== null) {
|
||||||
|
$vcard->addCompany($company);
|
||||||
|
}
|
||||||
|
|
||||||
|
$email = $customer->getEmail();
|
||||||
|
if ($email !== null) {
|
||||||
|
$vcard->addEmail($email);
|
||||||
|
}
|
||||||
|
|
||||||
$hasPref = false;
|
$hasPref = false;
|
||||||
|
|
||||||
|
|||||||
86
src/Controller/HelpController.php
Normal file
86
src/Controller/HelpController.php
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Configuration\LocaleService;
|
||||||
|
use App\Repository\Query\BaseQuery;
|
||||||
|
use App\Utils\DataTable;
|
||||||
|
use App\Utils\LocaleFormatter;
|
||||||
|
use App\Utils\PageSetup;
|
||||||
|
use App\Utils\Pagination;
|
||||||
|
use Pagerfanta\Adapter\ArrayAdapter;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Intl\Locales;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[Route(path: '/help')]
|
||||||
|
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
|
||||||
|
final class HelpController extends AbstractController
|
||||||
|
{
|
||||||
|
#[Route(path: '/locales', defaults: [], name: 'help_locales', methods: ['GET'])]
|
||||||
|
public function helpLocale(Request $request, LocaleService $service): Response
|
||||||
|
{
|
||||||
|
$table = new DataTable('help_locales', new BaseQuery());
|
||||||
|
$table->addColumn('name', ['class' => 'alwaysVisible', 'orderBy' => false]);
|
||||||
|
$table->addColumn('description', ['class' => 'd-none', 'orderBy' => false]);
|
||||||
|
$table->addColumn('language', ['class' => 'd-none w-min', 'orderBy' => false]);
|
||||||
|
$table->addColumn('date', ['class' => 'alwaysVisible w-min', 'orderBy' => false]);
|
||||||
|
$table->addColumn('time', ['class' => 'alwaysVisible w-min text-center', 'orderBy' => false]);
|
||||||
|
$table->addColumn('duration', ['class' => 'd-none w-min text-end', 'orderBy' => false]);
|
||||||
|
$table->addColumn('money', ['class' => 'w-min text-end', 'orderBy' => false, 'title' => 'rate']);
|
||||||
|
$table->addColumn('hour_24', ['class' => 'alwaysVisible w-min text-center', 'orderBy' => false]);
|
||||||
|
$table->addColumn('rtl', ['class' => 'd-none w-min text-center', 'orderBy' => false, 'title' => 'RTL']);
|
||||||
|
|
||||||
|
$page = new PageSetup('help_locales');
|
||||||
|
$page->setDataTable($table);
|
||||||
|
$page->setActionName('help_locales');
|
||||||
|
|
||||||
|
$data = $this->buildLocales($request, $service);
|
||||||
|
$pagination = new Pagination(new ArrayAdapter($data));
|
||||||
|
$pagination->setMaxPerPage(9999);
|
||||||
|
$table->setPagination($pagination);
|
||||||
|
|
||||||
|
return $this->render('help/index.html.twig', [
|
||||||
|
'page_setup' => $page,
|
||||||
|
'dataTable' => $table,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Request $request
|
||||||
|
* @param LocaleService $service
|
||||||
|
* @return array<string, array<string, string|bool|null>>
|
||||||
|
*/
|
||||||
|
private function buildLocales(Request $request, LocaleService $service): array
|
||||||
|
{
|
||||||
|
$requestLocale = $request->getLocale();
|
||||||
|
$data = [];
|
||||||
|
$now = $this->getDateTimeFactory()->createDateTime();
|
||||||
|
|
||||||
|
foreach ($service->getAllLocales() as $locale) {
|
||||||
|
$formatter = new LocaleFormatter($service, $locale);
|
||||||
|
$data[$locale] = [
|
||||||
|
'language' => $locale,
|
||||||
|
'name' => Locales::getName($locale, $locale),
|
||||||
|
'description' => Locales::getName($locale, $requestLocale),
|
||||||
|
'date' => $formatter->dateShort($now),
|
||||||
|
'time' => $formatter->time($now),
|
||||||
|
'duration' => $formatter->duration(46120),
|
||||||
|
'money' => $formatter->money(2794.83, 'EUR'),
|
||||||
|
'hour_24' => $service->is24Hour($locale),
|
||||||
|
'rtl' => $service->isRightToLeft($locale),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,8 @@ namespace App\Controller;
|
|||||||
|
|
||||||
use App\Configuration\LocaleService;
|
use App\Configuration\LocaleService;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
|
use App\Event\ConfigureMainMenuEvent;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
@@ -26,11 +28,9 @@ final class HomepageController extends AbstractController
|
|||||||
public const DEFAULT_ROUTE = 'timesheet';
|
public const DEFAULT_ROUTE = 'timesheet';
|
||||||
|
|
||||||
#[Route(path: '', defaults: [], name: 'homepage', methods: ['GET'])]
|
#[Route(path: '', defaults: [], name: 'homepage', methods: ['GET'])]
|
||||||
public function indexAction(Request $request, LocaleService $service): Response
|
public function indexAction(Request $request, LocaleService $service, EventDispatcherInterface $eventDispatcher): Response
|
||||||
{
|
{
|
||||||
/** @var User $user */
|
|
||||||
$user = $this->getUser();
|
$user = $this->getUser();
|
||||||
$userRoute = $user->getPreferenceValue('login_initial_view', self::DEFAULT_ROUTE, false);
|
|
||||||
$userLanguage = $user->getLanguage();
|
$userLanguage = $user->getLanguage();
|
||||||
$requestLanguage = $request->getLocale();
|
$requestLanguage = $request->getLocale();
|
||||||
|
|
||||||
@@ -48,13 +48,23 @@ final class HomepageController extends AbstractController
|
|||||||
$userLanguage = $service->getDefaultLocale();
|
$userLanguage = $service->getDefaultLocale();
|
||||||
}
|
}
|
||||||
|
|
||||||
$routes = [
|
$routes = [];
|
||||||
[$userRoute, $userLanguage],
|
|
||||||
[$userRoute, $requestLanguage],
|
$userRoute = $user->getPreferenceValue('login_initial_view');
|
||||||
[$userRoute, User::DEFAULT_LANGUAGE],
|
if (\is_string($userRoute)) {
|
||||||
[self::DEFAULT_ROUTE, $userLanguage],
|
$event = new ConfigureMainMenuEvent();
|
||||||
[self::DEFAULT_ROUTE, $requestLanguage],
|
$eventDispatcher->dispatch($event);
|
||||||
];
|
$menu = $event->findById($userRoute);
|
||||||
|
if ($menu !== null && \count($menu->getRouteArgs()) === 0 && $menu->getRoute() !== null) {
|
||||||
|
$userRoute = $menu->getRoute();
|
||||||
|
}
|
||||||
|
$routes[] = [$userRoute, $userLanguage];
|
||||||
|
$routes[] = [$userRoute, $requestLanguage];
|
||||||
|
$routes[] = [$userRoute, User::DEFAULT_LANGUAGE];
|
||||||
|
}
|
||||||
|
|
||||||
|
$routes[] = [self::DEFAULT_ROUTE, $userLanguage];
|
||||||
|
$routes[] = [self::DEFAULT_ROUTE, $requestLanguage];
|
||||||
|
|
||||||
foreach ($routes as $routeSettings) {
|
foreach ($routes as $routeSettings) {
|
||||||
$route = $routeSettings[0];
|
$route = $routeSettings[0];
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ final class ConfigureMainMenuEvent extends Event
|
|||||||
private MenuItemModel $apps;
|
private MenuItemModel $apps;
|
||||||
private MenuItemModel $admin;
|
private MenuItemModel $admin;
|
||||||
private MenuItemModel $system;
|
private MenuItemModel $system;
|
||||||
|
private ?MenuItemModel $root = null;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
@@ -30,6 +31,19 @@ final class ConfigureMainMenuEvent extends Event
|
|||||||
$this->system = new MenuItemModel('system', 'menu.system', '', [], 'configuration');
|
$this->system = new MenuItemModel('system', 'menu.system', '', [], 'configuration');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function findById(string $identifier): ?MenuItemModel
|
||||||
|
{
|
||||||
|
if ($this->root === null) {
|
||||||
|
$this->root = new MenuItemModel('root', 'root');
|
||||||
|
$this->root->addChild($this->menu);
|
||||||
|
$this->root->addChild($this->apps);
|
||||||
|
$this->root->addChild($this->admin);
|
||||||
|
$this->root->addChild($this->system);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->root->findChild($identifier);
|
||||||
|
}
|
||||||
|
|
||||||
public function getMenu(): MenuItemModel
|
public function getMenu(): MenuItemModel
|
||||||
{
|
{
|
||||||
return $this->menu;
|
return $this->menu;
|
||||||
@@ -37,7 +51,7 @@ final class ConfigureMainMenuEvent extends Event
|
|||||||
|
|
||||||
public function getTimesheetMenu(): ?MenuItemModel
|
public function getTimesheetMenu(): ?MenuItemModel
|
||||||
{
|
{
|
||||||
return $this->menu->getChild('timesheet');
|
return $this->menu->getChild('times');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getInvoiceMenu(): ?MenuItemModel
|
public function getInvoiceMenu(): ?MenuItemModel
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ namespace App\EventSubscriber;
|
|||||||
|
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Event\ConfigureMainMenuEvent;
|
use App\Event\ConfigureMainMenuEvent;
|
||||||
use App\Utils\MenuItemModel;
|
|
||||||
use Symfony\Bundle\SecurityBundle\Security;
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
|
|
||||||
@@ -30,38 +29,25 @@ final class MenuFavoritesSubscriber implements EventSubscriberInterface
|
|||||||
|
|
||||||
public function onMainMenuConfigure(ConfigureMainMenuEvent $menuEvent): void
|
public function onMainMenuConfigure(ConfigureMainMenuEvent $menuEvent): void
|
||||||
{
|
{
|
||||||
/** @var User $user */
|
/** @var User|null $user */
|
||||||
$user = $this->security->getUser();
|
$user = $this->security->getUser();
|
||||||
if (null === $user) {
|
if (null === $user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$favMenu = $menuEvent->getMenu()->findChild('favorites');
|
|
||||||
if ($favMenu === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$userFavorites = $user->getPreferenceValue('favorite_routes');
|
$userFavorites = $user->getPreferenceValue('favorite_routes');
|
||||||
if (!\is_string($userFavorites) || trim($userFavorites) === '') {
|
if (!\is_string($userFavorites) || trim($userFavorites) === '') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$root = new MenuItemModel('', '');
|
$favMenu = $menuEvent->findById('favorites');
|
||||||
$root->addChild($menuEvent->getMenu());
|
if ($favMenu === null) {
|
||||||
|
return;
|
||||||
if ($menuEvent->getAppsMenu()->hasChildren()) {
|
|
||||||
$root->addChild($menuEvent->getAppsMenu());
|
|
||||||
}
|
|
||||||
if ($menuEvent->getAdminMenu()->hasChildren()) {
|
|
||||||
$root->addChild($menuEvent->getAdminMenu());
|
|
||||||
}
|
|
||||||
if ($menuEvent->getSystemMenu()->hasChildren()) {
|
|
||||||
$root->addChild($menuEvent->getSystemMenu());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$userFavorites = explode(',', $userFavorites);
|
$userFavorites = explode(',', $userFavorites);
|
||||||
foreach ($userFavorites as $fav) {
|
foreach ($userFavorites as $fav) {
|
||||||
$tmp = $root->findChild($fav);
|
$tmp = $menuEvent->findById($fav);
|
||||||
if ($tmp !== null && !$tmp->hasChildren()) {
|
if ($tmp !== null && !$tmp->hasChildren()) {
|
||||||
$favMenu->addChild(clone $tmp);
|
$favMenu->addChild(clone $tmp);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,11 +50,11 @@ final class MenuSubscriber implements EventSubscriberInterface
|
|||||||
$menu->addChild(new MenuItemModel('favorites', 'favorite_routes', null, [], 'bookmarked'));
|
$menu->addChild(new MenuItemModel('favorites', 'favorite_routes', null, [], 'bookmarked'));
|
||||||
|
|
||||||
// ------------------- timesheet menu -------------------
|
// ------------------- timesheet menu -------------------
|
||||||
$times = new MenuItemModel('timesheet', 'time_tracking', null, [], 'timesheet');
|
$times = new MenuItemModel('times', 'time_tracking', null, [], 'timesheet');
|
||||||
|
|
||||||
if ($auth->isGranted('view_own_timesheet')) {
|
if ($auth->isGranted('view_own_timesheet')) {
|
||||||
$timesheets = new MenuItemModel('times', 'my_times', 'timesheet', [], 'timesheet');
|
$timesheets = new MenuItemModel('timesheet', 'my_times', 'timesheet', [], 'timesheet');
|
||||||
$timesheets->setChildRoutes(['times', 'timesheet_export', 'timesheet_edit', 'timesheet_create', 'timesheet_multi_update']);
|
$timesheets->setChildRoutes(['timesheet_export', 'timesheet_edit', 'timesheet_create', 'timesheet_multi_update']);
|
||||||
$times->addChild($timesheets);
|
$times->addChild($timesheets);
|
||||||
|
|
||||||
if ($auth->isGranted('quick-entry')) {
|
if ($auth->isGranted('quick-entry')) {
|
||||||
@@ -93,14 +93,15 @@ final class MenuSubscriber implements EventSubscriberInterface
|
|||||||
$menu->addChild($reporting);
|
$menu->addChild($reporting);
|
||||||
}
|
}
|
||||||
|
|
||||||
$invoice = new MenuItemModel('invoice', 'invoices', null, [], 'invoice');
|
// ------------------- invoice menu -------------------
|
||||||
|
$invoice = new MenuItemModel('invoices', 'invoices', null, [], 'invoice');
|
||||||
|
|
||||||
if ($auth->isGranted('create_invoice')) {
|
if ($auth->isGranted('create_invoice')) {
|
||||||
$invoice->addChild(new MenuItemModel('invoice', 'invoice_form.title', 'invoice', [], 'invoice'));
|
$invoice->addChild(new MenuItemModel('invoice', 'invoice_form.title', 'invoice', [], 'invoice'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($auth->isGranted('view_invoice')) {
|
if ($auth->isGranted('view_invoice')) {
|
||||||
$tmpMenu = new MenuItemModel('invoice-listing', 'all_invoices', 'admin_invoice_list', [], 'list');
|
$tmpMenu = new MenuItemModel('invoice_listing', 'all_invoices', 'admin_invoice_list', [], 'list');
|
||||||
$tmpMenu->setChildRoutes(['admin_invoice_edit']);
|
$tmpMenu->setChildRoutes(['admin_invoice_edit']);
|
||||||
$invoice->addChild($tmpMenu);
|
$invoice->addChild($tmpMenu);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ use App\Form\Type\CalendarViewType;
|
|||||||
use App\Form\Type\FavoriteMenuType;
|
use App\Form\Type\FavoriteMenuType;
|
||||||
use App\Form\Type\FirstWeekDayType;
|
use App\Form\Type\FirstWeekDayType;
|
||||||
use App\Form\Type\InitialViewType;
|
use App\Form\Type\InitialViewType;
|
||||||
use App\Form\Type\LanguageType;
|
|
||||||
use App\Form\Type\SkinType;
|
use App\Form\Type\SkinType;
|
||||||
use App\Form\Type\TimezoneType;
|
use App\Form\Type\TimezoneType;
|
||||||
|
use App\Form\Type\UserLanguageType;
|
||||||
use App\Form\Type\YesNoType;
|
use App\Form\Type\YesNoType;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
@@ -87,7 +87,7 @@ final class UserPreferenceSubscriber implements EventSubscriberInterface
|
|||||||
(new UserPreference(UserPreference::LOCALE, $this->systemConfiguration->getUserDefaultLanguage()))
|
(new UserPreference(UserPreference::LOCALE, $this->systemConfiguration->getUserDefaultLanguage()))
|
||||||
->setOrder(250)
|
->setOrder(250)
|
||||||
->setSection('locale')
|
->setSection('locale')
|
||||||
->setType(LanguageType::class),
|
->setType(UserLanguageType::class),
|
||||||
|
|
||||||
(new UserPreference(UserPreference::FIRST_WEEKDAY, User::DEFAULT_FIRST_WEEKDAY))
|
(new UserPreference(UserPreference::FIRST_WEEKDAY, User::DEFAULT_FIRST_WEEKDAY))
|
||||||
->setOrder(300)
|
->setOrder(300)
|
||||||
@@ -118,7 +118,7 @@ final class UserPreferenceSubscriber implements EventSubscriberInterface
|
|||||||
->setOrder(710)
|
->setOrder(710)
|
||||||
->setSection('behaviour')
|
->setSection('behaviour')
|
||||||
->setOptions(['required' => false])
|
->setOptions(['required' => false])
|
||||||
->addConstraint(new Length(['max' => 75]))
|
->addConstraint(new Length(['max' => 150]))
|
||||||
->setType(FavoriteMenuType::class),
|
->setType(FavoriteMenuType::class),
|
||||||
|
|
||||||
(new UserPreference('daily_stats', false))
|
(new UserPreference('daily_stats', false))
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ use App\Repository\Query\TimesheetQuery;
|
|||||||
use App\Twig\LocaleFormatExtensions;
|
use App\Twig\LocaleFormatExtensions;
|
||||||
use App\Utils\StringHelper;
|
use App\Utils\StringHelper;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
|
||||||
use PhpOffice\PhpSpreadsheet\Cell\DataType;
|
use PhpOffice\PhpSpreadsheet\Cell\DataType;
|
||||||
use PhpOffice\PhpSpreadsheet\Shared\Date;
|
use PhpOffice\PhpSpreadsheet\Shared\Date;
|
||||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
@@ -37,7 +38,7 @@ use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
|||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal means no BC promise whatsoever!
|
||||||
*/
|
*/
|
||||||
abstract class AbstractSpreadsheetRenderer
|
abstract class AbstractSpreadsheetRenderer
|
||||||
{
|
{
|
||||||
@@ -50,12 +51,17 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
// Part 1 = positive; Part 2 = negative; Part 3 = zero; Part 4 = Text
|
// Part 1 = positive; Part 2 = negative; Part 3 = zero; Part 4 = Text
|
||||||
public const RATE_FORMAT_DEFAULT = '#.##0,00 [$%1$s];-#.##0,00 [$%1$s]';
|
public const RATE_FORMAT_DEFAULT = '#.##0,00 [$%1$s];-#.##0,00 [$%1$s]';
|
||||||
public const RATE_FORMAT_LEFT = '_("%1$s"* #,##0.00_);_("%1$s"* -#,##0.00;_("%1$s"* "-"??_);_(@_)';
|
public const RATE_FORMAT_LEFT = '_("%1$s"* #,##0.00_);_("%1$s"* -#,##0.00;_("%1$s"* "-"??_);_(@_)';
|
||||||
public const RATE_FORMAT = self::RATE_FORMAT_LEFT;
|
public const RATE_FORMAT_RIGHT = '_(* "%1$s" #,##0.00_);_(* "%1$s" -#,##0.00;_(* "%1$s" "-"??_);_(@_)';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal used in html to excel exporter
|
* @internal used in html to excel exporter
|
||||||
*/
|
*/
|
||||||
public const RATE_FORMAT_NO_CURRENCY = '#,##0.00;-#,##0.00';
|
public const RATE_FORMAT_NO_CURRENCY = '#,##0.00;-#,##0.00';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see self:RATE_FORMAT_*
|
||||||
|
*/
|
||||||
|
protected string $rateFormat = self::RATE_FORMAT_LEFT;
|
||||||
protected string $durationFormat = self::DURATION_FORMAT;
|
protected string $durationFormat = self::DURATION_FORMAT;
|
||||||
protected int $durationBase = 86400;
|
protected int $durationBase = 86400;
|
||||||
/**
|
/**
|
||||||
@@ -117,10 +123,10 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
return $this->voter->isGranted('view_rate_other_timesheet');
|
return $this->voter->isGranted('view_rate_other_timesheet');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function setFormattedDateTime(Worksheet $sheet, $column, $row, ?DateTime $date): void
|
protected function setFormattedDateTime(Worksheet $sheet, int $column, int $row, ?DateTime $date): void
|
||||||
{
|
{
|
||||||
if (null === $date) {
|
if (null === $date) {
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, '');
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), '');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -128,20 +134,20 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
$excelDate = Date::PHPToExcel($date);
|
$excelDate = Date::PHPToExcel($date);
|
||||||
|
|
||||||
if ($excelDate === false) {
|
if ($excelDate === false) {
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, $date);
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $date);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, $excelDate);
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $excelDate);
|
||||||
// TODO why is that format hardcoded and does not depend on the users locale?
|
// TODO why is that format hardcoded and does not depend on the users locale?
|
||||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::DATETIME_FORMAT);
|
$sheet->getStyle(CellAddress::fromColumnAndRow($column, $row))->getNumberFormat()->setFormatCode(self::DATETIME_FORMAT);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function setFormattedTime(Worksheet $sheet, $column, $row, ?DateTime $date): void
|
protected function setFormattedTime(Worksheet $sheet, int $column, int $row, ?DateTime $date): void
|
||||||
{
|
{
|
||||||
if (null === $date) {
|
if (null === $date) {
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, '');
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), '');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -149,19 +155,19 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
$excelDate = Date::PHPToExcel($date);
|
$excelDate = Date::PHPToExcel($date);
|
||||||
|
|
||||||
if ($excelDate === false) {
|
if ($excelDate === false) {
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, $date);
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $date);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, $excelDate);
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $excelDate);
|
||||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::TIME_FORMAT);
|
$sheet->getStyle(CellAddress::fromColumnAndRow($column, $row))->getNumberFormat()->setFormatCode(self::TIME_FORMAT);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function setFormattedDate(Worksheet $sheet, $column, $row, ?DateTime $date): void
|
protected function setFormattedDate(Worksheet $sheet, int $column, int $row, ?DateTime $date): void
|
||||||
{
|
{
|
||||||
if (null === $date) {
|
if (null === $date) {
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, '');
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), '');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -169,48 +175,48 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
$excelDate = Date::PHPToExcel($date);
|
$excelDate = Date::PHPToExcel($date);
|
||||||
|
|
||||||
if ($excelDate === false) {
|
if ($excelDate === false) {
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, $date);
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $date);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, $excelDate);
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $excelDate);
|
||||||
// TODO why is that format hardcoded and does not depend on the users locale?
|
// TODO why is that format hardcoded and does not depend on the users locale?
|
||||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD2);
|
$sheet->getStyle(CellAddress::fromColumnAndRow($column, $row))->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD2);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function setDurationTotal(Worksheet $sheet, $column, $row, $startCoordinate, $endCoordinate): void
|
protected function setDurationTotal(Worksheet $sheet, int $column, int $row, string $startCoordinate, string $endCoordinate): void
|
||||||
{
|
{
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, sprintf('=SUBTOTAL(9,%s:%s)', $startCoordinate, $endCoordinate));
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), sprintf('=SUBTOTAL(9,%s:%s)', $startCoordinate, $endCoordinate));
|
||||||
$style = $sheet->getStyleByColumnAndRow($column, $row);
|
$style = $sheet->getStyle(CellAddress::fromColumnAndRow($column, $row));
|
||||||
$style->getNumberFormat()->setFormatCode($this->durationFormat);
|
$style->getNumberFormat()->setFormatCode($this->durationFormat);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function setDuration(Worksheet $sheet, $column, $row, $duration): void
|
protected function setDuration(Worksheet $sheet, int $column, int $row, ?int $duration): void
|
||||||
{
|
{
|
||||||
if (null === $duration) {
|
if (null === $duration) {
|
||||||
$duration = 0;
|
$duration = 0;
|
||||||
}
|
}
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, sprintf('=%s/%s', $duration, $this->durationBase));
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), sprintf('=%s/%s', $duration, $this->durationBase));
|
||||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode($this->durationFormat);
|
$sheet->getStyle(CellAddress::fromColumnAndRow($column, $row))->getNumberFormat()->setFormatCode($this->durationFormat);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function setRateTotal(Worksheet $sheet, $column, $row, $startCoordinate, $endCoordinate): void
|
protected function setRateTotal(Worksheet $sheet, int $column, int $row, string $startCoordinate, string $endCoordinate): void
|
||||||
{
|
{
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, sprintf('=SUBTOTAL(9,%s:%s)', $startCoordinate, $endCoordinate));
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), sprintf('=SUBTOTAL(9,%s:%s)', $startCoordinate, $endCoordinate));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function setRateStyle(Worksheet $sheet, $column, $row, $rate, $currency): void
|
protected function setRateStyle(Worksheet $sheet, int $column, int $row, ?string $currency): void
|
||||||
{
|
{
|
||||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(
|
$sheet->getStyle(CellAddress::fromColumnAndRow($column, $row))->getNumberFormat()->setFormatCode(
|
||||||
sprintf(self::RATE_FORMAT_LEFT, $currency)
|
sprintf($this->rateFormat, $currency ?? '')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function setRate(Worksheet $sheet, $column, $row, $rate, $currency): void
|
protected function setRate(Worksheet $sheet, int $column, int $row, ?float $rate, ?string $currency): void
|
||||||
{
|
{
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, $rate);
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $rate ?? 0);
|
||||||
$this->setRateStyle($sheet, $column, $row, $rate, $currency);
|
$this->setRateStyle($sheet, $column, $row, $currency);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -227,8 +233,8 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
/**
|
/**
|
||||||
* @param ExportableItem[] $exportItems
|
* @param ExportableItem[] $exportItems
|
||||||
* @param TimesheetQuery $query
|
* @param TimesheetQuery $query
|
||||||
* @param array $columns
|
* @param array<string, array<string, callable|int|float|false|null>> $columns
|
||||||
* @return array
|
* @return array<string, array<string, callable|int|float|false|null>>
|
||||||
*/
|
*/
|
||||||
protected function getColumns(array $exportItems, TimesheetQuery $query, array $columns): array
|
protected function getColumns(array $exportItems, TimesheetQuery $query, array $columns): array
|
||||||
{
|
{
|
||||||
@@ -289,7 +295,7 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
if (null !== $entity->getUser()) {
|
if (null !== $entity->getUser()) {
|
||||||
$user = $entity->getUser()->getDisplayName();
|
$user = $entity->getUser()->getDisplayName();
|
||||||
}
|
}
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, $user);
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $user);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,12 +306,12 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
if (null !== $entity->getUser()) {
|
if (null !== $entity->getUser()) {
|
||||||
$username = $entity->getUser()->getUserIdentifier();
|
$username = $entity->getUser()->getUserIdentifier();
|
||||||
}
|
}
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, $username);
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $username);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (!isset($columns['username']['header'])) {
|
if (!isset($columns['username']['header'])) {
|
||||||
$columns['username']['header'] = function (Worksheet $sheet, $row, $column): int {
|
$columns['username']['header'] = function (Worksheet $sheet, $row, $column): int {
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, $this->translator->trans('name'));
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $this->translator->trans('name'));
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
};
|
};
|
||||||
@@ -318,7 +324,7 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
if (null !== $entity->getProject()) {
|
if (null !== $entity->getProject()) {
|
||||||
$customer = $entity->getProject()->getCustomer()->getName();
|
$customer = $entity->getProject()->getCustomer()->getName();
|
||||||
}
|
}
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, $customer);
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $customer);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,7 +334,7 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
if (null !== $entity->getProject()) {
|
if (null !== $entity->getProject()) {
|
||||||
$project = $entity->getProject()->getName();
|
$project = $entity->getProject()->getName();
|
||||||
}
|
}
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, $project);
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $project);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,7 +344,7 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
if (null !== $entity->getActivity()) {
|
if (null !== $entity->getActivity()) {
|
||||||
$activity = $entity->getActivity()->getName();
|
$activity = $entity->getActivity()->getName();
|
||||||
}
|
}
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, $activity);
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $activity);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,7 +358,7 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
$isColumnFormatted = false;
|
$isColumnFormatted = false;
|
||||||
|
|
||||||
$columns['description']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) use (&$isColumnFormatted, $maxWidth, $wrapText, $sanitizeText) {
|
$columns['description']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) use (&$isColumnFormatted, $maxWidth, $wrapText, $sanitizeText) {
|
||||||
$cell = $sheet->getCellByColumnAndRow($column, $row);
|
$cell = $sheet->getCell(CellAddress::fromColumnAndRow($column, $row));
|
||||||
$desc = $entity->getDescription();
|
$desc = $entity->getDescription();
|
||||||
|
|
||||||
if ($sanitizeText && null !== $desc) {
|
if ($sanitizeText && null !== $desc) {
|
||||||
@@ -379,20 +385,20 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
if (isset($columns['exported']) && !isset($columns['exported']['render'])) {
|
if (isset($columns['exported']) && !isset($columns['exported']['render'])) {
|
||||||
$columns['exported']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) {
|
$columns['exported']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) {
|
||||||
$exported = $entity->isExported() ? 'yes' : 'no';
|
$exported = $entity->isExported() ? 'yes' : 'no';
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, $this->translator->trans($exported));
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $this->translator->trans($exported));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($columns['billable']) && !isset($columns['billable']['render'])) {
|
if (isset($columns['billable']) && !isset($columns['billable']['render'])) {
|
||||||
$columns['billable']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) {
|
$columns['billable']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) {
|
||||||
$exported = $entity->isBillable() ? 'yes' : 'no';
|
$exported = $entity->isBillable() ? 'yes' : 'no';
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, $this->translator->trans($exported));
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $this->translator->trans($exported));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($columns['tags']) && !isset($columns['tags']['render'])) {
|
if (isset($columns['tags']) && !isset($columns['tags']['render'])) {
|
||||||
$columns['tags']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) {
|
$columns['tags']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) {
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, implode(',', $entity->getTagsAsArray()));
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), implode(',', $entity->getTagsAsArray()));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,7 +428,7 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
$columns['timesheet-meta'] = [
|
$columns['timesheet-meta'] = [
|
||||||
'header' => function (Worksheet $sheet, $row, $column) use ($timesheetMetaFields): int {
|
'header' => function (Worksheet $sheet, $row, $column) use ($timesheetMetaFields): int {
|
||||||
foreach ($timesheetMetaFields as $metaField) {
|
foreach ($timesheetMetaFields as $metaField) {
|
||||||
$sheet->setCellValueByColumnAndRow($column++, $row, $this->translator->trans($metaField->getLabel()));
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $this->translator->trans($metaField->getLabel()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return \count($timesheetMetaFields);
|
return \count($timesheetMetaFields);
|
||||||
@@ -434,7 +440,7 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
if (null !== $metaField) {
|
if (null !== $metaField) {
|
||||||
$metaFieldValue = $metaField->getValue();
|
$metaFieldValue = $metaField->getValue();
|
||||||
}
|
}
|
||||||
$sheet->setCellValueByColumnAndRow($column++, $row, $metaFieldValue);
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $metaFieldValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
return \count($timesheetMetaFields);
|
return \count($timesheetMetaFields);
|
||||||
@@ -450,7 +456,7 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
$columns['customer-meta'] = [
|
$columns['customer-meta'] = [
|
||||||
'header' => function (Worksheet $sheet, $row, $column) use ($customerMetaFields): int {
|
'header' => function (Worksheet $sheet, $row, $column) use ($customerMetaFields): int {
|
||||||
foreach ($customerMetaFields as $metaField) {
|
foreach ($customerMetaFields as $metaField) {
|
||||||
$sheet->setCellValueByColumnAndRow($column++, $row, $this->translator->trans($metaField->getLabel()));
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $this->translator->trans($metaField->getLabel()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return \count($customerMetaFields);
|
return \count($customerMetaFields);
|
||||||
@@ -464,7 +470,7 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
$metaFieldValue = $metaField->getValue();
|
$metaFieldValue = $metaField->getValue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$sheet->setCellValueByColumnAndRow($column++, $row, $metaFieldValue);
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $metaFieldValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
return \count($customerMetaFields);
|
return \count($customerMetaFields);
|
||||||
@@ -477,7 +483,7 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
$columns['project-meta'] = [
|
$columns['project-meta'] = [
|
||||||
'header' => function (Worksheet $sheet, $row, $column) use ($projectMetaFields): int {
|
'header' => function (Worksheet $sheet, $row, $column) use ($projectMetaFields): int {
|
||||||
foreach ($projectMetaFields as $metaField) {
|
foreach ($projectMetaFields as $metaField) {
|
||||||
$sheet->setCellValueByColumnAndRow($column++, $row, $this->translator->trans($metaField->getLabel()));
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $this->translator->trans($metaField->getLabel()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return \count($projectMetaFields);
|
return \count($projectMetaFields);
|
||||||
@@ -491,7 +497,7 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
$metaFieldValue = $metaField->getValue();
|
$metaFieldValue = $metaField->getValue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$sheet->setCellValueByColumnAndRow($column++, $row, $metaFieldValue);
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $metaFieldValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
return \count($projectMetaFields);
|
return \count($projectMetaFields);
|
||||||
@@ -504,7 +510,7 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
$columns['activity-meta'] = [
|
$columns['activity-meta'] = [
|
||||||
'header' => function (Worksheet $sheet, $row, $column) use ($activityMetaFields): int {
|
'header' => function (Worksheet $sheet, $row, $column) use ($activityMetaFields): int {
|
||||||
foreach ($activityMetaFields as $metaField) {
|
foreach ($activityMetaFields as $metaField) {
|
||||||
$sheet->setCellValueByColumnAndRow($column++, $row, $this->translator->trans($metaField->getLabel()));
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $this->translator->trans($metaField->getLabel()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return \count($activityMetaFields);
|
return \count($activityMetaFields);
|
||||||
@@ -518,7 +524,7 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
$metaFieldValue = $metaField->getValue();
|
$metaFieldValue = $metaField->getValue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$sheet->setCellValueByColumnAndRow($column++, $row, $metaFieldValue);
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $metaFieldValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
return \count($activityMetaFields);
|
return \count($activityMetaFields);
|
||||||
@@ -533,7 +539,7 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
$columns['user-meta'] = [
|
$columns['user-meta'] = [
|
||||||
'header' => function (Worksheet $sheet, $row, $column) use ($userPreferences): int {
|
'header' => function (Worksheet $sheet, $row, $column) use ($userPreferences): int {
|
||||||
foreach ($userPreferences as $metaField) {
|
foreach ($userPreferences as $metaField) {
|
||||||
$sheet->setCellValueByColumnAndRow($column++, $row, $this->translator->trans($metaField->getLabel()));
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $this->translator->trans($metaField->getLabel()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return \count($userPreferences);
|
return \count($userPreferences);
|
||||||
@@ -547,7 +553,7 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
$metaFieldValue = $metaField->getValue();
|
$metaFieldValue = $metaField->getValue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$sheet->setCellValueByColumnAndRow($column++, $row, $metaFieldValue);
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $metaFieldValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
return \count($userPreferences);
|
return \count($userPreferences);
|
||||||
@@ -557,20 +563,20 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
|
|
||||||
if (isset($columns['type']) && !isset($columns['type']['render'])) {
|
if (isset($columns['type']) && !isset($columns['type']['render'])) {
|
||||||
$columns['type']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) {
|
$columns['type']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) {
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, $entity->getType());
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $entity->getType());
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($columns['category']) && !isset($columns['category']['render'])) {
|
if (isset($columns['category']) && !isset($columns['category']['render'])) {
|
||||||
$columns['category']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) {
|
$columns['category']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) {
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, $entity->getCategory());
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $entity->getCategory());
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($columns['customer_number'])) {
|
if (isset($columns['customer_number'])) {
|
||||||
if (!isset($columns['customer_number']['header'])) {
|
if (!isset($columns['customer_number']['header'])) {
|
||||||
$columns['customer_number']['header'] = function (Worksheet $sheet, $row, $column): int {
|
$columns['customer_number']['header'] = function (Worksheet $sheet, $row, $column): int {
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, $this->translator->trans('number'));
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $this->translator->trans('number'));
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
};
|
};
|
||||||
@@ -582,7 +588,7 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
if (null !== $entity->getProject()) {
|
if (null !== $entity->getProject()) {
|
||||||
$customerId = $entity->getProject()->getCustomer()->getNumber();
|
$customerId = $entity->getProject()->getCustomer()->getNumber();
|
||||||
}
|
}
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, $customerId);
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $customerId);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -590,7 +596,7 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
if (isset($columns['customer_vat']) && !isset($columns['customer_vat']['render'])) {
|
if (isset($columns['customer_vat']) && !isset($columns['customer_vat']['render'])) {
|
||||||
if (!isset($columns['customer_vat']['header'])) {
|
if (!isset($columns['customer_vat']['header'])) {
|
||||||
$columns['customer_vat']['header'] = function (Worksheet $sheet, $row, $column): int {
|
$columns['customer_vat']['header'] = function (Worksheet $sheet, $row, $column): int {
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, $this->translator->trans('vat_id'));
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $this->translator->trans('vat_id'));
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
};
|
};
|
||||||
@@ -602,7 +608,7 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
if (null !== $entity->getProject()) {
|
if (null !== $entity->getProject()) {
|
||||||
$customerVat = $entity->getProject()->getCustomer()->getVatId();
|
$customerVat = $entity->getProject()->getCustomer()->getVatId();
|
||||||
}
|
}
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, $customerVat);
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $customerVat);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -610,7 +616,7 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
if (isset($columns['order_number']) && !isset($columns['order_number']['render'])) {
|
if (isset($columns['order_number']) && !isset($columns['order_number']['render'])) {
|
||||||
if (!isset($columns['order_number']['header'])) {
|
if (!isset($columns['order_number']['header'])) {
|
||||||
$columns['order_number']['header'] = function (Worksheet $sheet, $row, $column): int {
|
$columns['order_number']['header'] = function (Worksheet $sheet, $row, $column): int {
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, $this->translator->trans('orderNumber'));
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $this->translator->trans('orderNumber'));
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
};
|
};
|
||||||
@@ -622,7 +628,7 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
if (null !== $entity->getProject()) {
|
if (null !== $entity->getProject()) {
|
||||||
$orderNumber = $entity->getProject()->getOrderNumber();
|
$orderNumber = $entity->getProject()->getOrderNumber();
|
||||||
}
|
}
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, $orderNumber);
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $orderNumber);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -663,10 +669,13 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
|
|
||||||
foreach ($columns as $label => $settings) {
|
foreach ($columns as $label => $settings) {
|
||||||
if (isset($settings['header'])) {
|
if (isset($settings['header'])) {
|
||||||
|
if (!\is_callable($settings['header'])) {
|
||||||
|
throw new \RuntimeException('Invalid header renderer given for: ' . $label);
|
||||||
|
}
|
||||||
$amount = $settings['header']($sheet, $recordsHeaderRow, $recordsHeaderColumn);
|
$amount = $settings['header']($sheet, $recordsHeaderRow, $recordsHeaderColumn);
|
||||||
$recordsHeaderColumn += $amount;
|
$recordsHeaderColumn += $amount;
|
||||||
} else {
|
} else {
|
||||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans($label));
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow), $this->translator->trans($label));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -689,11 +698,11 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!\array_key_exists('render', $settings) || !\is_callable($settings['render'])) {
|
if (!\array_key_exists('render', $settings) || !\is_callable($settings['render'])) {
|
||||||
throw new \RuntimeException(sprintf('Missing renderer for export column %s', $label));
|
throw new \RuntimeException(sprintf('Missing or invalid renderer for export column %s', $label));
|
||||||
}
|
}
|
||||||
|
|
||||||
$amount = $settings['render']($sheet, $entryHeaderRow, $entryHeaderColumn, $exportItem);
|
$amount = $settings['render']($sheet, $entryHeaderRow, $entryHeaderColumn, $exportItem);
|
||||||
$entryHeaderColumn += (null === $amount) ? 1 : $amount;
|
$entryHeaderColumn += (null === $amount) ? 1 : (int) $amount;
|
||||||
}
|
}
|
||||||
|
|
||||||
$entryHeaderRow++;
|
$entryHeaderRow++;
|
||||||
@@ -701,28 +710,28 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
|
|
||||||
if ($this->isTotalRowSupported()) {
|
if ($this->isTotalRowSupported()) {
|
||||||
if (null !== $durationColumn) {
|
if (null !== $durationColumn) {
|
||||||
$startCoordinate = $sheet->getCellByColumnAndRow($durationColumn, 2)->getCoordinate();
|
$startCoordinate = $sheet->getCell(CellAddress::fromColumnAndRow($durationColumn, 2))->getCoordinate();
|
||||||
$endCoordinate = $sheet->getCellByColumnAndRow($durationColumn, $entryHeaderRow - 1)->getCoordinate();
|
$endCoordinate = $sheet->getCell(CellAddress::fromColumnAndRow($durationColumn, $entryHeaderRow - 1))->getCoordinate();
|
||||||
$this->setDurationTotal($sheet, $durationColumn, $entryHeaderRow, $startCoordinate, $endCoordinate);
|
$this->setDurationTotal($sheet, $durationColumn, $entryHeaderRow, $startCoordinate, $endCoordinate);
|
||||||
$style = $sheet->getStyleByColumnAndRow($durationColumn, $entryHeaderRow);
|
$style = $sheet->getStyle(CellAddress::fromColumnAndRow($durationColumn, $entryHeaderRow));
|
||||||
$style->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
$style->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
||||||
$style->getFont()->setBold(true);
|
$style->getFont()->setBold(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null !== $rateColumn) {
|
if (null !== $rateColumn) {
|
||||||
$startCoordinate = $sheet->getCellByColumnAndRow($rateColumn, 2)->getCoordinate();
|
$startCoordinate = $sheet->getCell(CellAddress::fromColumnAndRow($rateColumn, 2))->getCoordinate();
|
||||||
$endCoordinate = $sheet->getCellByColumnAndRow($rateColumn, $entryHeaderRow - 1)->getCoordinate();
|
$endCoordinate = $sheet->getCell(CellAddress::fromColumnAndRow($rateColumn, $entryHeaderRow - 1))->getCoordinate();
|
||||||
$this->setRateTotal($sheet, $rateColumn, $entryHeaderRow, $startCoordinate, $endCoordinate);
|
$this->setRateTotal($sheet, $rateColumn, $entryHeaderRow, $startCoordinate, $endCoordinate);
|
||||||
$style = $sheet->getStyleByColumnAndRow($rateColumn, $entryHeaderRow);
|
$style = $sheet->getStyle(CellAddress::fromColumnAndRow($rateColumn, $entryHeaderRow));
|
||||||
$style->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
$style->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
||||||
$style->getFont()->setBold(true);
|
$style->getFont()->setBold(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null !== $internalRateColumn) {
|
if (null !== $internalRateColumn) {
|
||||||
$startCoordinate = $sheet->getCellByColumnAndRow($internalRateColumn, 2)->getCoordinate();
|
$startCoordinate = $sheet->getCell(CellAddress::fromColumnAndRow($internalRateColumn, 2))->getCoordinate();
|
||||||
$endCoordinate = $sheet->getCellByColumnAndRow($internalRateColumn, $entryHeaderRow - 1)->getCoordinate();
|
$endCoordinate = $sheet->getCell(CellAddress::fromColumnAndRow($internalRateColumn, $entryHeaderRow - 1))->getCoordinate();
|
||||||
$this->setRateTotal($sheet, $internalRateColumn, $entryHeaderRow, $startCoordinate, $endCoordinate);
|
$this->setRateTotal($sheet, $internalRateColumn, $entryHeaderRow, $startCoordinate, $endCoordinate);
|
||||||
$style = $sheet->getStyleByColumnAndRow($internalRateColumn, $entryHeaderRow);
|
$style = $sheet->getStyle(CellAddress::fromColumnAndRow($internalRateColumn, $entryHeaderRow));
|
||||||
$style->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
$style->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
||||||
$style->getFont()->setBold(true);
|
$style->getFont()->setBold(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
namespace App\Export\Base;
|
namespace App\Export\Base;
|
||||||
|
|
||||||
|
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
|
||||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||||
@@ -54,16 +55,17 @@ class CsvRenderer extends AbstractSpreadsheetRenderer
|
|||||||
return 'csv';
|
return 'csv';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function setDuration(Worksheet $sheet, $column, $row, $duration): void
|
protected function setDuration(Worksheet $sheet, int $column, int $row, ?int $duration): void
|
||||||
{
|
{
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, sprintf('=%s', $duration));
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), sprintf('=%s', $duration ?? 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function setRateStyle(Worksheet $sheet, $column, $row, $rate, $currency): void
|
protected function setRate(Worksheet $sheet, int $column, int $row, ?float $rate, ?string $currency): void
|
||||||
{
|
{
|
||||||
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $rate);
|
||||||
if ($rate === 0.00) {
|
if ($rate === 0.00) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
parent::setRateStyle($sheet, $column, $row, $rate, $currency);
|
$this->setRateStyle($sheet, $column, $row, $currency);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,16 @@ class XlsxRenderer extends AbstractSpreadsheetRenderer
|
|||||||
throw new \Exception('Could not open temporary file');
|
throw new \Exception('Could not open temporary file');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->applyStyles($spreadsheet);
|
||||||
|
|
||||||
|
$writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
|
||||||
|
$writer->save($filename);
|
||||||
|
|
||||||
|
return $filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function applyStyles(Spreadsheet $spreadsheet): void
|
||||||
|
{
|
||||||
// Store expensive calculations for later
|
// Store expensive calculations for later
|
||||||
$sheet = $spreadsheet->getActiveSheet();
|
$sheet = $spreadsheet->getActiveSheet();
|
||||||
$highestRow = $sheet->getHighestRow();
|
$highestRow = $sheet->getHighestRow();
|
||||||
@@ -75,11 +85,6 @@ class XlsxRenderer extends AbstractSpreadsheetRenderer
|
|||||||
->getAlignment()
|
->getAlignment()
|
||||||
->setVertical(Alignment::VERTICAL_TOP)
|
->setVertical(Alignment::VERTICAL_TOP)
|
||||||
->setHorizontal(Alignment::HORIZONTAL_LEFT);
|
->setHorizontal(Alignment::HORIZONTAL_LEFT);
|
||||||
|
|
||||||
$writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
|
|
||||||
$writer->save($filename);
|
|
||||||
|
|
||||||
return $filename;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): string
|
public function getId(): string
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
namespace App\Export\Spreadsheet\CellFormatter;
|
namespace App\Export\Spreadsheet\CellFormatter;
|
||||||
|
|
||||||
|
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
|
||||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||||
|
|
||||||
final class ArrayFormatter implements CellFormatterInterface
|
final class ArrayFormatter implements CellFormatterInterface
|
||||||
@@ -16,7 +17,7 @@ final class ArrayFormatter implements CellFormatterInterface
|
|||||||
public function setFormattedValue(Worksheet $sheet, int $column, int $row, $value): void
|
public function setFormattedValue(Worksheet $sheet, int $column, int $row, $value): void
|
||||||
{
|
{
|
||||||
if (null === $value) {
|
if (null === $value) {
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, '');
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), '');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -25,6 +26,6 @@ final class ArrayFormatter implements CellFormatterInterface
|
|||||||
throw new \InvalidArgumentException('Unsupported value given, only array is supported');
|
throw new \InvalidArgumentException('Unsupported value given, only array is supported');
|
||||||
}
|
}
|
||||||
|
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, implode(';', $value));
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), implode(';', $value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
namespace App\Export\Spreadsheet\CellFormatter;
|
namespace App\Export\Spreadsheet\CellFormatter;
|
||||||
|
|
||||||
|
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
|
||||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||||
|
|
||||||
final class BooleanFormatter implements CellFormatterInterface
|
final class BooleanFormatter implements CellFormatterInterface
|
||||||
@@ -16,7 +17,7 @@ final class BooleanFormatter implements CellFormatterInterface
|
|||||||
public function setFormattedValue(Worksheet $sheet, int $column, int $row, $value): void
|
public function setFormattedValue(Worksheet $sheet, int $column, int $row, $value): void
|
||||||
{
|
{
|
||||||
if (null === $value) {
|
if (null === $value) {
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, '');
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), '');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -25,6 +26,6 @@ final class BooleanFormatter implements CellFormatterInterface
|
|||||||
throw new \InvalidArgumentException('Unsupported value given, only boolean is supported');
|
throw new \InvalidArgumentException('Unsupported value given, only boolean is supported');
|
||||||
}
|
}
|
||||||
|
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, $value);
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
namespace App\Export\Spreadsheet\CellFormatter;
|
namespace App\Export\Spreadsheet\CellFormatter;
|
||||||
|
|
||||||
|
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
|
||||||
use PhpOffice\PhpSpreadsheet\Shared\Date;
|
use PhpOffice\PhpSpreadsheet\Shared\Date;
|
||||||
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
|
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
|
||||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||||
@@ -18,7 +19,7 @@ final class DateFormatter implements CellFormatterInterface
|
|||||||
public function setFormattedValue(Worksheet $sheet, int $column, int $row, $value): void
|
public function setFormattedValue(Worksheet $sheet, int $column, int $row, $value): void
|
||||||
{
|
{
|
||||||
if (null === $value) {
|
if (null === $value) {
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, '');
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), '');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -27,7 +28,7 @@ final class DateFormatter implements CellFormatterInterface
|
|||||||
throw new \InvalidArgumentException('Unsupported value given, only DateTime is supported');
|
throw new \InvalidArgumentException('Unsupported value given, only DateTime is supported');
|
||||||
}
|
}
|
||||||
|
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, Date::PHPToExcel($value));
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), Date::PHPToExcel($value));
|
||||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD2);
|
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
namespace App\Export\Spreadsheet\CellFormatter;
|
namespace App\Export\Spreadsheet\CellFormatter;
|
||||||
|
|
||||||
|
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
|
||||||
use PhpOffice\PhpSpreadsheet\Shared\Date;
|
use PhpOffice\PhpSpreadsheet\Shared\Date;
|
||||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ final class DateTimeFormatter implements CellFormatterInterface
|
|||||||
public function setFormattedValue(Worksheet $sheet, int $column, int $row, $value): void
|
public function setFormattedValue(Worksheet $sheet, int $column, int $row, $value): void
|
||||||
{
|
{
|
||||||
if (null === $value) {
|
if (null === $value) {
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, '');
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), '');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -28,7 +29,7 @@ final class DateTimeFormatter implements CellFormatterInterface
|
|||||||
throw new \InvalidArgumentException('Unsupported value given, only DateTime is supported');
|
throw new \InvalidArgumentException('Unsupported value given, only DateTime is supported');
|
||||||
}
|
}
|
||||||
|
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, Date::PHPToExcel($value));
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), Date::PHPToExcel($value));
|
||||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::DATETIME_FORMAT);
|
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::DATETIME_FORMAT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
namespace App\Export\Spreadsheet\CellFormatter;
|
namespace App\Export\Spreadsheet\CellFormatter;
|
||||||
|
|
||||||
|
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
|
||||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||||
|
|
||||||
final class DurationFormatter implements CellFormatterInterface
|
final class DurationFormatter implements CellFormatterInterface
|
||||||
@@ -25,7 +26,7 @@ final class DurationFormatter implements CellFormatterInterface
|
|||||||
throw new \InvalidArgumentException('Unsupported value given, only int is supported');
|
throw new \InvalidArgumentException('Unsupported value given, only int is supported');
|
||||||
}
|
}
|
||||||
|
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, sprintf('=%s/86400', $value));
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), sprintf('=%s/86400', $value));
|
||||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::DURATION_FORMAT);
|
$sheet->getStyle(CellAddress::fromColumnAndRow($column, $row))->getNumberFormat()->setFormatCode(self::DURATION_FORMAT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
namespace App\Export\Spreadsheet\CellFormatter;
|
namespace App\Export\Spreadsheet\CellFormatter;
|
||||||
|
|
||||||
|
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
|
||||||
use PhpOffice\PhpSpreadsheet\Shared\Date;
|
use PhpOffice\PhpSpreadsheet\Shared\Date;
|
||||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ final class TimeFormatter implements CellFormatterInterface
|
|||||||
public function setFormattedValue(Worksheet $sheet, int $column, int $row, $value): void
|
public function setFormattedValue(Worksheet $sheet, int $column, int $row, $value): void
|
||||||
{
|
{
|
||||||
if (null === $value) {
|
if (null === $value) {
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, '');
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), '');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -28,7 +29,7 @@ final class TimeFormatter implements CellFormatterInterface
|
|||||||
throw new \InvalidArgumentException('Unsupported value given, only DateTime is supported');
|
throw new \InvalidArgumentException('Unsupported value given, only DateTime is supported');
|
||||||
}
|
}
|
||||||
|
|
||||||
$sheet->setCellValueByColumnAndRow($column, $row, Date::PHPToExcel($value));
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), Date::PHPToExcel($value));
|
||||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::TIME_FORMAT);
|
$sheet->getStyle(CellAddress::fromColumnAndRow($column, $row))->getNumberFormat()->setFormatCode(self::TIME_FORMAT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use App\Export\Spreadsheet\CellFormatter\DateFormatter;
|
|||||||
use App\Export\Spreadsheet\CellFormatter\DateTimeFormatter;
|
use App\Export\Spreadsheet\CellFormatter\DateTimeFormatter;
|
||||||
use App\Export\Spreadsheet\CellFormatter\DurationFormatter;
|
use App\Export\Spreadsheet\CellFormatter\DurationFormatter;
|
||||||
use App\Export\Spreadsheet\CellFormatter\TimeFormatter;
|
use App\Export\Spreadsheet\CellFormatter\TimeFormatter;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
|
||||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
@@ -65,7 +66,7 @@ class SpreadsheetExporter
|
|||||||
$recordsHeaderRow = 1;
|
$recordsHeaderRow = 1;
|
||||||
|
|
||||||
foreach ($columns as $settings) {
|
foreach ($columns as $settings) {
|
||||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans($settings->getLabel()));
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow), $this->translator->trans($settings->getLabel()));
|
||||||
}
|
}
|
||||||
|
|
||||||
$entryHeaderRow = $recordsHeaderRow + 1;
|
$entryHeaderRow = $recordsHeaderRow + 1;
|
||||||
@@ -77,7 +78,7 @@ class SpreadsheetExporter
|
|||||||
$value = \call_user_func($settings->getAccessor(), $entry);
|
$value = \call_user_func($settings->getAccessor(), $entry);
|
||||||
|
|
||||||
if (!\array_key_exists($settings->getType(), $this->formatter)) {
|
if (!\array_key_exists($settings->getType(), $this->formatter)) {
|
||||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $value);
|
$sheet->setCellValue(CellAddress::fromColumnAndRow($entryHeaderColumn, $entryHeaderRow), $value);
|
||||||
} else {
|
} else {
|
||||||
$formatter = $this->formatter[$settings->getType()];
|
$formatter = $this->formatter[$settings->getType()];
|
||||||
$formatter->setFormattedValue($sheet, $entryHeaderColumn, $entryHeaderRow, $value);
|
$formatter->setFormattedValue($sheet, $entryHeaderColumn, $entryHeaderRow, $value);
|
||||||
|
|||||||
@@ -9,9 +9,10 @@
|
|||||||
|
|
||||||
namespace App\Form\Model;
|
namespace App\Form\Model;
|
||||||
|
|
||||||
|
use App\Utils\EquatableInterface;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
|
|
||||||
final class DateRange
|
final class DateRange implements EquatableInterface
|
||||||
{
|
{
|
||||||
private ?DateTime $begin = null;
|
private ?DateTime $begin = null;
|
||||||
private ?DateTime $end = null;
|
private ?DateTime $end = null;
|
||||||
@@ -49,4 +50,29 @@ final class DateRange
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isEqualTo(object $compare): bool
|
||||||
|
{
|
||||||
|
if (!$compare instanceof DateRange) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($this->getBegin() === null && $compare->getBegin() !== null) || ($this->getBegin() !== null && $compare->getBegin() === null)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($this->getEnd() === null && $compare->getEnd() !== null) || ($this->getEnd() !== null && $compare->getEnd() === null)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->getBegin() !== null && $compare->getBegin() !== null && $this->getBegin()->getTimestamp() !== $compare->getBegin()->getTimestamp()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->getEnd() !== null && $compare->getEnd() !== null && $this->getEnd()->getTimestamp() !== $compare->getEnd()->getTimestamp()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ final class FavoriteMenuType extends AbstractType
|
|||||||
{
|
{
|
||||||
$resolver->setDefaults([
|
$resolver->setDefaults([
|
||||||
'multiple' => true,
|
'multiple' => true,
|
||||||
'include_reports' => false,
|
|
||||||
'filter_menus' => ['favorites', 'dashboard'],
|
'filter_menus' => ['favorites', 'dashboard'],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,20 +9,17 @@
|
|||||||
|
|
||||||
namespace App\Form\Type;
|
namespace App\Form\Type;
|
||||||
|
|
||||||
use App\Entity\User;
|
|
||||||
use App\Event\ConfigureMainMenuEvent;
|
use App\Event\ConfigureMainMenuEvent;
|
||||||
use App\Reporting\ReportingService;
|
|
||||||
use App\Utils\MenuItemModel;
|
use App\Utils\MenuItemModel;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
use Symfony\Component\OptionsResolver\Options;
|
use Symfony\Component\OptionsResolver\Options;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
|
||||||
|
|
||||||
final class MenuChoiceType extends AbstractType
|
final class MenuChoiceType extends AbstractType
|
||||||
{
|
{
|
||||||
public function __construct(private EventDispatcherInterface $eventDispatcher, private ReportingService $reportingService, private TranslatorInterface $translator)
|
public function __construct(private EventDispatcherInterface $eventDispatcher)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,25 +27,19 @@ final class MenuChoiceType extends AbstractType
|
|||||||
{
|
{
|
||||||
$resolver->setDefaults([
|
$resolver->setDefaults([
|
||||||
'required' => true,
|
'required' => true,
|
||||||
'include_reports' => true,
|
|
||||||
'filter_menus' => [],
|
'filter_menus' => [],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$resolver->setDefault('choices', function (Options $options): array {
|
$resolver->setDefault('choices', function (Options $options): array {
|
||||||
/** @var User $user */
|
return $this->getChoices($options['filter_menus']);
|
||||||
$user = $options['user'];
|
|
||||||
|
|
||||||
return $this->getChoices($user, $options['include_reports'], $options['filter_menus']);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param User $user
|
|
||||||
* @param bool $withReports
|
|
||||||
* @param array<string> $filter
|
* @param array<string> $filter
|
||||||
* @return array<string, string>
|
* @return array<string, string>
|
||||||
*/
|
*/
|
||||||
private function getChoices(User $user, bool $withReports, array $filter): array
|
private function getChoices(array $filter): array
|
||||||
{
|
{
|
||||||
$event = new ConfigureMainMenuEvent();
|
$event = new ConfigureMainMenuEvent();
|
||||||
$this->eventDispatcher->dispatch($event);
|
$this->eventDispatcher->dispatch($event);
|
||||||
@@ -58,13 +49,6 @@ final class MenuChoiceType extends AbstractType
|
|||||||
$choices += $this->getChoicesFromMenu($event->getAdminMenu(), $filter);
|
$choices += $this->getChoicesFromMenu($event->getAdminMenu(), $filter);
|
||||||
$choices += $this->getChoicesFromMenu($event->getSystemMenu(), $filter);
|
$choices += $this->getChoicesFromMenu($event->getSystemMenu(), $filter);
|
||||||
|
|
||||||
if ($withReports) {
|
|
||||||
foreach ($this->reportingService->getAvailableReports($user) as $report) {
|
|
||||||
$label = $this->translator->trans('menu.reporting') . ': ' . $this->translator->trans($report->getLabel(), [], 'reporting');
|
|
||||||
$choices[$label] = $report->getId();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $choices;
|
return $choices;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,13 +66,13 @@ final class MenuChoiceType extends AbstractType
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!$child->hasChildren()) {
|
if (!$child->hasChildren()) {
|
||||||
if (\count($child->getRouteArgs()) === 0) {
|
if (\count($child->getRouteArgs()) === 0 && $child->getRoute() !== null) {
|
||||||
$choices[$child->getLabel()] = $child->getIdentifier();
|
$choices[$child->getLabel()] = $child->getIdentifier();
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
foreach ($child->getChildren() as $subChild) {
|
foreach ($child->getChildren() as $subChild) {
|
||||||
if (\count($subChild->getRouteArgs()) === 0) {
|
if (\count($subChild->getRouteArgs()) === 0 && $subChild->getRoute() !== null) {
|
||||||
$choices[$subChild->getLabel()] = $subChild->getIdentifier();
|
$choices[$subChild->getLabel()] = $subChild->getIdentifier();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
41
src/Form/Type/UserLanguageType.php
Normal file
41
src/Form/Type/UserLanguageType.php
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?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\Form\Type;
|
||||||
|
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom form field type to select the user language.
|
||||||
|
*/
|
||||||
|
final class UserLanguageType extends AbstractType
|
||||||
|
{
|
||||||
|
public function __construct(private UrlGeneratorInterface $router, private TranslatorInterface $translator)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
|
{
|
||||||
|
$route = $this->router->generate('help_locales');
|
||||||
|
$message = $this->translator->trans('user.language.help');
|
||||||
|
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'help_html' => true,
|
||||||
|
'help' => sprintf('<a href="%s" target="help_locales">%s</a>', $route, $message)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getParent(): string
|
||||||
|
{
|
||||||
|
return LanguageType::class;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,6 +53,14 @@ final class ConfigurableNumberGenerator implements NumberGeneratorInterface
|
|||||||
throw new \InvalidArgumentException('Missing invoice model, cannot calculate invoice number');
|
throw new \InvalidArgumentException('Missing invoice model, cannot calculate invoice number');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($format === 'cname' && $this->model->getCustomer()?->getName() === null) {
|
||||||
|
throw new \InvalidArgumentException('Customer has no name, replacer {cname} failed evaluation');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($format === 'cnumber' && $this->model->getCustomer()?->getNumber() === null) {
|
||||||
|
throw new \InvalidArgumentException('Customer has no number, replacer {cnumber} failed evaluation');
|
||||||
|
}
|
||||||
|
|
||||||
return match ($format) {
|
return match ($format) {
|
||||||
'Y' => $invoiceDate->format('Y'),
|
'Y' => $invoiceDate->format('Y'),
|
||||||
'y' => $invoiceDate->format('y'),
|
'y' => $invoiceDate->format('y'),
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use App\Entity\Bookmark;
|
|||||||
use App\Entity\Team;
|
use App\Entity\Team;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Form\Model\DateRange;
|
use App\Form\Model\DateRange;
|
||||||
|
use App\Utils\EquatableInterface;
|
||||||
use App\Utils\SearchTerm;
|
use App\Utils\SearchTerm;
|
||||||
use Symfony\Component\Form\FormError;
|
use Symfony\Component\Form\FormError;
|
||||||
use Symfony\Component\Form\FormErrorIterator;
|
use Symfony\Component\Form\FormErrorIterator;
|
||||||
@@ -330,7 +331,13 @@ class BaseQuery
|
|||||||
$currentValue = $this->get($filter);
|
$currentValue = $this->get($filter);
|
||||||
|
|
||||||
if (\is_object($currentValue)) {
|
if (\is_object($currentValue)) {
|
||||||
if ($currentValue !== $expectedValue) {
|
if ($currentValue instanceof EquatableInterface) {
|
||||||
|
return $currentValue->isEqualTo($expectedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is a loose comparison by choice, as == compares object type and content
|
||||||
|
// instead of === which only compares if both sides are the same instance
|
||||||
|
if ($currentValue != $expectedValue) { // @phpstan-ignore-line
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -113,8 +113,23 @@ final class DataTable implements \Countable, \IteratorAggregate
|
|||||||
$this->columns = $columns;
|
$this->columns = $columns;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function addColumn(string $name, array $column): void
|
/**
|
||||||
|
* Supported $column options:
|
||||||
|
* - class
|
||||||
|
* - title
|
||||||
|
* - translation_domain
|
||||||
|
* - orderBy (string|false)
|
||||||
|
* - order (desc, asc)
|
||||||
|
*
|
||||||
|
* @param string $name
|
||||||
|
* @param array $column
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function addColumn(string $name, array $column = []): void
|
||||||
{
|
{
|
||||||
|
if (!\array_key_exists('class', $column)) {
|
||||||
|
$column['class'] = '';
|
||||||
|
}
|
||||||
$this->columns[$name] = $column;
|
$this->columns[$name] = $column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
15
src/Utils/EquatableInterface.php
Normal file
15
src/Utils/EquatableInterface.php
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?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;
|
||||||
|
|
||||||
|
interface EquatableInterface
|
||||||
|
{
|
||||||
|
public function isEqualTo(object $compare): bool;
|
||||||
|
}
|
||||||
24
templates/help/index.html.twig
Normal file
24
templates/help/index.html.twig
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{% extends 'datatable.html.twig' %}
|
||||||
|
{% import "macros/widgets.html.twig" as widgets %}
|
||||||
|
|
||||||
|
{% block datatable_column_value %}
|
||||||
|
{% if column == 'name' %}
|
||||||
|
{{ entry.name }}
|
||||||
|
{% elseif column == 'description' %}
|
||||||
|
{{ entry.description }}
|
||||||
|
{% elseif column == 'language' %}
|
||||||
|
{{ entry.language }}
|
||||||
|
{% elseif column == 'date' %}
|
||||||
|
{{ entry.date }}
|
||||||
|
{% elseif column == 'time' %}
|
||||||
|
{{ entry.time }}
|
||||||
|
{% elseif column == 'duration' %}
|
||||||
|
{{ entry.duration }}
|
||||||
|
{% elseif column == 'money' %}
|
||||||
|
{{ entry.money }}
|
||||||
|
{% elseif column == 'hour_24' %}
|
||||||
|
{{ widgets.label_boolean(entry.hour_24) }}
|
||||||
|
{% elseif column == 'rtl' %}
|
||||||
|
{{ widgets.label_boolean(entry.rtl) }}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
@@ -6,13 +6,18 @@
|
|||||||
{% if tabler_bundle.isDarkMode() or tabler_bundle.isNavbarOverlapping() %}
|
{% if tabler_bundle.isDarkMode() or tabler_bundle.isNavbarOverlapping() %}
|
||||||
{% set class = "btn-dark" %}
|
{% set class = "btn-dark" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="nav-item dropdown d-none d-md-flex me-3 notifications-menu" data-reload="{{ path('favorites_timesheets') }}">
|
<div class="nav-item dropdown d-flex me-sm-3 me-1 notifications-menu" data-reload="{{ path('favorites_timesheets') }}">
|
||||||
<a href="#" class="btn btn-icon {{ class}} px-0" data-bs-toggle="dropdown" tabindex="-1" aria-label="{{ 'recent.activities'|trans }}">
|
<a href="#" class="btn btn-icon {{ class}} px-0" data-bs-toggle="dropdown" tabindex="-1" aria-label="{{ 'recent.activities'|trans }}">
|
||||||
{{ icon('repeat') }}
|
{{ icon('repeat') }}
|
||||||
</a>
|
</a>
|
||||||
<div class="dropdown-menu dropdown-menu-end dropdown-menu-card">
|
<div class="dropdown-menu dropdown-menu-end dropdown-menu-card">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">{{ 'recent.activities'|trans }}</div>
|
<div class="card-header">
|
||||||
|
{{ 'recent.activities'|trans }}
|
||||||
|
<div class="card-actions">
|
||||||
|
<button type="button" class="btn-close{% if tabler_bundle.isDarkMode() %} btn-close-white{% endif %}" data-bs-dismiss="modal" aria-label="{{ 'action.close'|trans }}"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="list-group list-group-flush list-group-hoverable menu">
|
<div class="list-group list-group-flush list-group-hoverable menu">
|
||||||
{% for favorite in favorites %}
|
{% for favorite in favorites %}
|
||||||
{% set entry = favorite.getTimesheet() %}
|
{% set entry = favorite.getTimesheet() %}
|
||||||
|
|||||||
@@ -4,23 +4,23 @@
|
|||||||
{# fake entry, because at least one html template node is needed #}
|
{# fake entry, because at least one html template node is needed #}
|
||||||
{% set active_timesheets = [{'id': '000', 'begin': null, 'activity': {'name': ''}, 'project': {'name': '', 'customer': {'name': ''}}}] %}
|
{% set active_timesheets = [{'id': '000', 'begin': null, 'activity': {'name': ''}, 'project': {'name': '', 'customer': {'name': ''}}}] %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="nav-item d-flex me-3">
|
<div class="nav-item d-flex me-sm-3 me-1">
|
||||||
<div class="btn-list">
|
<div class="btn-list">
|
||||||
{% set class = "btn-outline-white" %}
|
{% set class = "btn-outline-white" %}
|
||||||
{% if tabler_bundle.isDarkMode() or tabler_bundle.isNavbarOverlapping() %}
|
{% if tabler_bundle.isDarkMode() or tabler_bundle.isNavbarOverlapping() %}
|
||||||
{% set class = "btn-dark" %}
|
{% set class = "btn-dark" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% set entry = active_timesheets[0] %}
|
{% set entry = active_timesheets[0] %}
|
||||||
<div class="messages-menu" data-api="{{ path('active_timesheet') }}" data-href="{{ path('stop_timesheet', {'id' : '000'}) }}" style="{% if not hasActiveRecords %}display:none{% endif %}">
|
<div class="ticktac-menu" data-api="{{ path('active_timesheet') }}" data-href="{{ path('stop_timesheet', {'id' : '000'}) }}" style="{% if not hasActiveRecords %}display:none{% endif %}">
|
||||||
<a data-replacer="url" class="api-link ticktac-running ticktac-stop btn {{ class }}" href="{{ path('stop_timesheet', {'id' : entry.id}) }}" data-event="kimai.timesheetStop kimai.timesheetUpdate" data-method="PATCH" data-msg-error="timesheet.stop.error" data-msg-success="timesheet.stop.success"{% if hasActiveRecords %} accesskey="s"{% endif %}>
|
<a data-replacer="url" class="api-link ticktac-running ticktac-stop btn {{ class }} btn-icon px-sm-2" href="{{ path('stop_timesheet', {'id' : entry.id}) }}" data-event="kimai.timesheetStop kimai.timesheetUpdate" data-method="PATCH" data-msg-error="timesheet.stop.error" data-msg-success="timesheet.stop.success"{% if hasActiveRecords %} accesskey="s"{% endif %}>
|
||||||
<i class="icon text-red {{ 'stop-small'|icon(true) }}"></i>
|
<i class="text-red {{ 'stop-small'|icon(false) }} me-0 me-sm-1"></i>
|
||||||
<span data-replacer="duration" data-title="true" data-since="{{ entry.begin is null ? '' : entry.begin|date_format(constant('DATE_ISO8601')) }}">{{ entry is iterable ? 0|duration : entry|duration }}</span>
|
<span class="d-none d-sm-block" data-replacer="duration" data-title="true" data-since="{{ entry.begin is null ? '' : entry.begin|date_format(constant('DATE_ISO8601')) }}">{{ entry is iterable ? 0|duration : entry|duration }}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="messages-menu-empty" style="{% if hasActiveRecords %}display:none{% endif %}">
|
<div class="ticktac-menu-empty" style="{% if hasActiveRecords %}display:none{% endif %}">
|
||||||
<a href="{{ path('timesheet_create') }}" class="modal-ajax-form ticktac-start btn {{ class }}" accesskey="n">
|
<a href="{{ path('timesheet_create') }}" class="modal-ajax-form ticktac-start btn {{ class }} btn-icon px-sm-2" accesskey="n">
|
||||||
<i class="icon text-green {{ 'start'|icon(true) }}"></i>
|
<i class="text-green {{ 'start'|icon(false) }} me-0 me-sm-1"></i>
|
||||||
<span>{{ 0|duration }}</span>
|
<span class="d-none d-sm-block">{{ 0|duration }}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,11 +11,11 @@
|
|||||||
{% block profile_intro %}
|
{% block profile_intro %}
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-body box-user-profile">
|
<div class="card-body box-user-profile">
|
||||||
<div class="row g-2 align-items-center">
|
<div class="row row-sm-2 align-items-center">
|
||||||
<div class="col-auto" data-toggle="tooltip" title="{{ 'id'|trans }}: {{ user.id }}">
|
<div class="col-auto" data-toggle="tooltip" title="{{ 'id'|trans }}: {{ user.id }}">
|
||||||
{{ widgets.user_avatar(user, false, 'avatar-lg') }}
|
{{ widgets.user_avatar(user, false, 'avatar-lg') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col" style="min-width: 100px">
|
||||||
<h4 class="card-title m-0">{{ widgets.username(user) }}</h4>
|
<h4 class="card-title m-0">{{ widgets.username(user) }}</h4>
|
||||||
{% if user.title is not empty %}
|
{% if user.title is not empty %}
|
||||||
<div class="text-muted">
|
<div class="text-muted">
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto" id="profile-buttons">
|
<div class="col-auto text-end mt-2 mt-sm-1" id="profile-buttons">
|
||||||
{% from '@theme/components/buttons.html.twig' import link_button %}
|
{% from '@theme/components/buttons.html.twig' import link_button %}
|
||||||
{% if app.user == user %}
|
{% if app.user == user %}
|
||||||
{{ link_button('notifications'|trans, 'javascript:notificationStatus()', 'fas fa-bell', 'default') }}
|
{{ link_button('notifications'|trans, 'javascript:notificationStatus()', 'fas fa-bell', 'default') }}
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ use App\Tests\DataFixtures\TimesheetFixtures;
|
|||||||
*/
|
*/
|
||||||
class FavoriteControllerTest extends ControllerBaseTest
|
class FavoriteControllerTest extends ControllerBaseTest
|
||||||
{
|
{
|
||||||
public function testIsSecure()
|
public function testIsSecure(): void
|
||||||
{
|
{
|
||||||
$this->assertUrlIsSecured('/favorite/timesheet/');
|
$this->assertUrlIsSecured('/favorite/timesheet/');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testIndexAction()
|
public function testIndexAction(): void
|
||||||
{
|
{
|
||||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||||
$start = new \DateTime('first day of this month');
|
$start = new \DateTime('first day of this month');
|
||||||
@@ -38,7 +38,9 @@ class FavoriteControllerTest extends ControllerBaseTest
|
|||||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
|
||||||
$content = $client->getResponse()->getContent();
|
$content = $client->getResponse()->getContent();
|
||||||
self::assertStringContainsString('<div class="nav-item dropdown d-none d-md-flex me-3 notifications-menu" data-reload="/en/favorite/timesheet/">', $content);
|
self::assertNotFalse($content);
|
||||||
self::assertStringContainsString('<div class="card-header">Restart one of your last activities</div>', $content);
|
self::assertStringContainsString('<div class="nav-item dropdown d-flex me-sm-3 me-1 notifications-menu" data-reload="/en/favorite/timesheet/">', $content);
|
||||||
|
self::assertStringContainsString('<div class="card-header">', $content);
|
||||||
|
self::assertStringContainsString('Restart one of your last activities', $content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
tests/Controller/HelpControllerTest.php
Normal file
25
tests/Controller/HelpControllerTest.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Controller;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @group integration
|
||||||
|
*/
|
||||||
|
class HelpControllerTest extends ControllerBaseTest
|
||||||
|
{
|
||||||
|
public function testHelpLocalesAction(): void
|
||||||
|
{
|
||||||
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||||
|
$this->assertAccessIsGranted($client, '/help/locales');
|
||||||
|
$this->assertDataTableRowCount($client, 'datatable_help_locales', 25); // @see services_test.yaml
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,9 +42,7 @@ class HomepageControllerTest extends ControllerBaseTest
|
|||||||
|
|
||||||
$em->persist($pref);
|
$em->persist($pref);
|
||||||
$user->addPreference($pref);
|
$user->addPreference($pref);
|
||||||
|
|
||||||
$user->setLanguage('ar');
|
$user->setLanguage('ar');
|
||||||
|
|
||||||
$em->flush();
|
$em->flush();
|
||||||
|
|
||||||
$this->request($client, '/homepage');
|
$this->request($client, '/homepage');
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class ConfigureMainMenuEventTest extends TestCase
|
|||||||
self::assertNull($sut->getInvoiceMenu());
|
self::assertNull($sut->getInvoiceMenu());
|
||||||
self::assertNull($sut->getReportingMenu());
|
self::assertNull($sut->getReportingMenu());
|
||||||
|
|
||||||
$timesheet = new MenuItemModel('timesheet', 'timesheet');
|
$timesheet = new MenuItemModel('times', 'timesheet');
|
||||||
$sut->getMenu()->addChild($timesheet);
|
$sut->getMenu()->addChild($timesheet);
|
||||||
self::assertNotNull($sut->getTimesheetMenu());
|
self::assertNotNull($sut->getTimesheetMenu());
|
||||||
self::assertSame($timesheet, $sut->getTimesheetMenu());
|
self::assertSame($timesheet, $sut->getTimesheetMenu());
|
||||||
@@ -41,9 +41,19 @@ class ConfigureMainMenuEventTest extends TestCase
|
|||||||
self::assertNotNull($sut->getInvoiceMenu());
|
self::assertNotNull($sut->getInvoiceMenu());
|
||||||
self::assertSame($invoice, $sut->getInvoiceMenu());
|
self::assertSame($invoice, $sut->getInvoiceMenu());
|
||||||
|
|
||||||
|
self::assertNull($sut->findById('reporting'));
|
||||||
|
self::assertNull($sut->findById('foo'));
|
||||||
|
self::assertNull($sut->findById('bar'));
|
||||||
|
|
||||||
$reporting = new MenuItemModel('reporting', 'reporting');
|
$reporting = new MenuItemModel('reporting', 'reporting');
|
||||||
$sut->getMenu()->addChild($reporting);
|
$sut->getMenu()->addChild($reporting);
|
||||||
|
$reporting->addChild(new MenuItemModel('foo', 'foo'));
|
||||||
|
$reporting->addChild(new MenuItemModel('bar', 'bar'));
|
||||||
self::assertNotNull($sut->getReportingMenu());
|
self::assertNotNull($sut->getReportingMenu());
|
||||||
self::assertSame($reporting, $sut->getReportingMenu());
|
self::assertSame($reporting, $sut->getReportingMenu());
|
||||||
|
|
||||||
|
self::assertSame($reporting, $sut->findById('reporting'));
|
||||||
|
self::assertNotNull($sut->findById('foo'));
|
||||||
|
self::assertNotNull($sut->findById('bar'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,14 +17,14 @@ use PHPUnit\Framework\TestCase;
|
|||||||
*/
|
*/
|
||||||
class DateRangeTest extends TestCase
|
class DateRangeTest extends TestCase
|
||||||
{
|
{
|
||||||
public function testDefaultValues()
|
public function testDefaultValues(): void
|
||||||
{
|
{
|
||||||
$sut = new DateRange();
|
$sut = new DateRange();
|
||||||
self::assertNull($sut->getBegin());
|
self::assertNull($sut->getBegin());
|
||||||
self::assertNull($sut->getEnd());
|
self::assertNull($sut->getEnd());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSetterAndGetter()
|
public function testSetterAndGetter(): void
|
||||||
{
|
{
|
||||||
$begin = new \DateTime('now');
|
$begin = new \DateTime('now');
|
||||||
$end = new \DateTime('2018-11-25 18:45:32');
|
$end = new \DateTime('2018-11-25 18:45:32');
|
||||||
@@ -34,7 +34,73 @@ class DateRangeTest extends TestCase
|
|||||||
self::assertInstanceOf(DateRange::class, $sut->setBegin($begin));
|
self::assertInstanceOf(DateRange::class, $sut->setBegin($begin));
|
||||||
self::assertInstanceOf(DateRange::class, $sut->setEnd($end));
|
self::assertInstanceOf(DateRange::class, $sut->setEnd($end));
|
||||||
|
|
||||||
|
self::assertInstanceOf(\DateTime::class, $sut->getBegin());
|
||||||
|
self::assertInstanceOf(\DateTime::class, $sut->getEnd());
|
||||||
|
|
||||||
self::assertEquals($begin->format('Y-m-d') . ' 00:00:00', $sut->getBegin()->format('Y-m-d H:i:s'));
|
self::assertEquals($begin->format('Y-m-d') . ' 00:00:00', $sut->getBegin()->format('Y-m-d H:i:s'));
|
||||||
self::assertEquals('2018-11-25 23:59:59', $sut->getEnd()->format('Y-m-d H:i:s'));
|
self::assertEquals('2018-11-25 23:59:59', $sut->getEnd()->format('Y-m-d H:i:s'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testEquatableInterface(): void
|
||||||
|
{
|
||||||
|
self::assertTrue((new DateRange())->isEqualTo(new DateRange()));
|
||||||
|
|
||||||
|
$sut = new DateRange();
|
||||||
|
$sut->setBegin(new \DateTime('now'));
|
||||||
|
$sut->setEnd(new \DateTime('+2 minutes'));
|
||||||
|
|
||||||
|
$sut1 = new DateRange();
|
||||||
|
$sut1->setBegin(new \DateTime('+2 minutes'));
|
||||||
|
$sut1->setEnd(new \DateTime('now'));
|
||||||
|
|
||||||
|
self::assertTrue($sut->isEqualTo($sut1));
|
||||||
|
self::assertTrue($sut1->isEqualTo($sut));
|
||||||
|
|
||||||
|
$sut = new DateRange(false);
|
||||||
|
$sut->setBegin(new \DateTime('now'));
|
||||||
|
$sut->setEnd(new \DateTime('+2 minutes'));
|
||||||
|
|
||||||
|
$sut1 = new DateRange();
|
||||||
|
$sut1->setBegin(new \DateTime('+2 minutes'));
|
||||||
|
$sut1->setEnd(new \DateTime('now'));
|
||||||
|
|
||||||
|
self::assertFalse($sut->isEqualTo($sut1));
|
||||||
|
self::assertFalse($sut1->isEqualTo($sut));
|
||||||
|
|
||||||
|
$sut = new DateRange();
|
||||||
|
$sut->setBegin(new \DateTime('now'));
|
||||||
|
|
||||||
|
$sut1 = new DateRange();
|
||||||
|
$sut1->setBegin(new \DateTime('+1 day'));
|
||||||
|
|
||||||
|
self::assertFalse($sut->isEqualTo($sut1));
|
||||||
|
self::assertFalse($sut1->isEqualTo($sut));
|
||||||
|
|
||||||
|
$sut = new DateRange();
|
||||||
|
$sut->setBegin(new \DateTime('now'));
|
||||||
|
|
||||||
|
$sut1 = new DateRange();
|
||||||
|
$sut1->setEnd(new \DateTime('now'));
|
||||||
|
|
||||||
|
self::assertFalse($sut->isEqualTo($sut1));
|
||||||
|
self::assertFalse($sut1->isEqualTo($sut));
|
||||||
|
|
||||||
|
$sut = new DateRange();
|
||||||
|
$sut->setBegin(new \DateTime('now'));
|
||||||
|
|
||||||
|
$sut1 = new DateRange();
|
||||||
|
$sut1->setBegin(new \DateTime('now'));
|
||||||
|
|
||||||
|
self::assertTrue($sut->isEqualTo($sut1));
|
||||||
|
self::assertTrue($sut1->isEqualTo($sut));
|
||||||
|
|
||||||
|
$sut = new DateRange();
|
||||||
|
$sut->setEnd(new \DateTime('now'));
|
||||||
|
|
||||||
|
$sut1 = new DateRange();
|
||||||
|
$sut1->setEnd(new \DateTime('now'));
|
||||||
|
|
||||||
|
self::assertTrue($sut->isEqualTo($sut1));
|
||||||
|
self::assertTrue($sut1->isEqualTo($sut));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ use PHPUnit\Framework\TestCase;
|
|||||||
*/
|
*/
|
||||||
class ConfigurableNumberGeneratorTest extends TestCase
|
class ConfigurableNumberGeneratorTest extends TestCase
|
||||||
{
|
{
|
||||||
private function getSut(string $format, int $counter = 1)
|
private function getSut(string $format, int $counter = 1): ConfigurableNumberGenerator
|
||||||
{
|
{
|
||||||
$config = SystemConfigurationFactory::createStub(['invoice' => ['number_format' => $format]]);
|
$config = SystemConfigurationFactory::createStub(['invoice' => ['number_format' => $format]]);
|
||||||
|
|
||||||
@@ -53,7 +53,10 @@ class ConfigurableNumberGeneratorTest extends TestCase
|
|||||||
return new ConfigurableNumberGenerator($repository, $config);
|
return new ConfigurableNumberGenerator($repository, $config);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getTestData()
|
/**
|
||||||
|
* @return array<int, array<int, string|\DateTime|int>>
|
||||||
|
*/
|
||||||
|
public function getTestData(): array
|
||||||
{
|
{
|
||||||
$invoiceDate = new \DateTime();
|
$invoiceDate = new \DateTime();
|
||||||
|
|
||||||
@@ -143,7 +146,7 @@ class ConfigurableNumberGeneratorTest extends TestCase
|
|||||||
/**
|
/**
|
||||||
* @dataProvider getTestData
|
* @dataProvider getTestData
|
||||||
*/
|
*/
|
||||||
public function testGetInvoiceNumber(string $format, string $expectedInvoiceNumber, \DateTime $invoiceDate, int $counter = 1)
|
public function testGetInvoiceNumber(string $format, string $expectedInvoiceNumber, \DateTime $invoiceDate, int $counter = 1): void
|
||||||
{
|
{
|
||||||
$customer = new Customer('Acme company');
|
$customer = new Customer('Acme company');
|
||||||
$customer->setNumber('0815');
|
$customer->setNumber('0815');
|
||||||
@@ -163,7 +166,10 @@ class ConfigurableNumberGeneratorTest extends TestCase
|
|||||||
$this->assertEquals('default', $sut->getId());
|
$this->assertEquals('default', $sut->getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getInvalidTestData()
|
/**
|
||||||
|
* @return array<int, array<int, string|\DateTime>>
|
||||||
|
*/
|
||||||
|
public function getInvalidTestData(): array
|
||||||
{
|
{
|
||||||
$invoiceDate = new \DateTime();
|
$invoiceDate = new \DateTime();
|
||||||
|
|
||||||
@@ -188,7 +194,7 @@ class ConfigurableNumberGeneratorTest extends TestCase
|
|||||||
/**
|
/**
|
||||||
* @dataProvider getInvalidTestData
|
* @dataProvider getInvalidTestData
|
||||||
*/
|
*/
|
||||||
public function testInvalidGetInvoiceNumber(string $format, \DateTime $invoiceDate, string $brokenPart)
|
public function testInvalidGetInvoiceNumber(string $format, \DateTime $invoiceDate, string $brokenPart): void
|
||||||
{
|
{
|
||||||
$this->expectException(\InvalidArgumentException::class);
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
$this->expectExceptionMessage(sprintf('Unknown %s found', $brokenPart));
|
$this->expectExceptionMessage(sprintf('Unknown %s found', $brokenPart));
|
||||||
@@ -201,4 +207,40 @@ class ConfigurableNumberGeneratorTest extends TestCase
|
|||||||
|
|
||||||
$sut->getInvoiceNumber();
|
$sut->getInvoiceNumber();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array<int, string>>
|
||||||
|
*/
|
||||||
|
public function getMissingFieldTestData(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
['{Y}/{cnumber}_{ccy,3}', 'Customer has no number, replacer {cnumber} failed evaluation'],
|
||||||
|
['{Y}/{cname}_{ccy,3}', 'Customer has no name, replacer {cname} failed evaluation'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider getMissingFieldTestData
|
||||||
|
*/
|
||||||
|
public function testCustomerHasMissingField(string $format, string $message): void
|
||||||
|
{
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
$this->expectExceptionMessage($message);
|
||||||
|
|
||||||
|
$user = $this->createMock(User::class);
|
||||||
|
$user->method('getId')->willReturn(13);
|
||||||
|
$user->method('getAccountNumber')->willReturn('0815');
|
||||||
|
|
||||||
|
$customer = new Customer('');
|
||||||
|
$customer->setName(null);
|
||||||
|
|
||||||
|
$sut = $this->getSut($format);
|
||||||
|
$model = (new InvoiceModelFactoryFactory($this))->create()->createModel(new DebugFormatter());
|
||||||
|
$model->setInvoiceDate(new \DateTime());
|
||||||
|
$model->setCustomer($customer);
|
||||||
|
$model->setUser($user);
|
||||||
|
$sut->setModel($model);
|
||||||
|
|
||||||
|
$sut->getInvoiceNumber();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3342,21 +3342,6 @@ parameters:
|
|||||||
count: 2
|
count: 2
|
||||||
path: Controller/ExportControllerTest.php
|
path: Controller/ExportControllerTest.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Tests\\\\Controller\\\\FavoriteControllerTest\\:\\:testIndexAction\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: Controller/FavoriteControllerTest.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Tests\\\\Controller\\\\FavoriteControllerTest\\:\\:testIsSecure\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: Controller/FavoriteControllerTest.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#2 \\$haystack of static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertStringContainsString\\(\\) expects string, string\\|false given\\.$#"
|
|
||||||
count: 2
|
|
||||||
path: Controller/FavoriteControllerTest.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Method App\\\\Tests\\\\Controller\\\\HomepageControllerTest\\:\\:testIndexAction\\(\\) has no return type specified\\.$#"
|
message: "#^Method App\\\\Tests\\\\Controller\\\\HomepageControllerTest\\:\\:testIndexAction\\(\\) has no return type specified\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -6957,21 +6942,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: Form/Extension/IconExtensionTest.php
|
path: Form/Extension/IconExtensionTest.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Cannot call method format\\(\\) on DateTime\\|null\\.$#"
|
|
||||||
count: 2
|
|
||||||
path: Form/Model/DateRangeTest.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Tests\\\\Form\\\\Model\\\\DateRangeTest\\:\\:testDefaultValues\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: Form/Model/DateRangeTest.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Tests\\\\Form\\\\Model\\\\DateRangeTest\\:\\:testSetterAndGetter\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: Form/Model/DateRangeTest.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Method App\\\\Tests\\\\Form\\\\Model\\\\SystemConfigurationTest\\:\\:testDefaultValues\\(\\) has no return type specified\\.$#"
|
message: "#^Method App\\\\Tests\\\\Form\\\\Model\\\\SystemConfigurationTest\\:\\:testDefaultValues\\(\\) has no return type specified\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -7507,31 +7477,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: Invoice/InvoiceModelTest.php
|
path: Invoice/InvoiceModelTest.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Tests\\\\Invoice\\\\NumberGenerator\\\\ConfigurableNumberGeneratorTest\\:\\:getInvalidTestData\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: Invoice/NumberGenerator/ConfigurableNumberGeneratorTest.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Tests\\\\Invoice\\\\NumberGenerator\\\\ConfigurableNumberGeneratorTest\\:\\:getSut\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: Invoice/NumberGenerator/ConfigurableNumberGeneratorTest.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Tests\\\\Invoice\\\\NumberGenerator\\\\ConfigurableNumberGeneratorTest\\:\\:getTestData\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: Invoice/NumberGenerator/ConfigurableNumberGeneratorTest.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Tests\\\\Invoice\\\\NumberGenerator\\\\ConfigurableNumberGeneratorTest\\:\\:testGetInvoiceNumber\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: Invoice/NumberGenerator/ConfigurableNumberGeneratorTest.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Tests\\\\Invoice\\\\NumberGenerator\\\\ConfigurableNumberGeneratorTest\\:\\:testInvalidGetInvoiceNumber\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: Invoice/NumberGenerator/ConfigurableNumberGeneratorTest.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Method App\\\\Tests\\\\Invoice\\\\NumberGenerator\\\\DateNumberGeneratorTest\\:\\:getSut\\(\\) has no return type specified\\.$#"
|
message: "#^Method App\\\\Tests\\\\Invoice\\\\NumberGenerator\\\\DateNumberGeneratorTest\\:\\:getSut\\(\\) has no return type specified\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
|
|||||||
@@ -1409,6 +1409,22 @@
|
|||||||
<source>about.title</source>
|
<source>about.title</source>
|
||||||
<target>Über Kimai</target>
|
<target>Über Kimai</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="i_XOmN4" resname="help_locales">
|
||||||
|
<source>help_locales</source>
|
||||||
|
<target>Unterstützte Sprachen mit Formatierungsregeln</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="JmkKf48" resname="user.language.help">
|
||||||
|
<source>user.language.help</source>
|
||||||
|
<target>Diese Einstellung ist neben der Übersetzung der Oberfläche ebenso zuständig für die Zeit-, Datums-, und Währungsformate</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="ulgdXdi" resname="hour_24">
|
||||||
|
<source>hour_24</source>
|
||||||
|
<target>24-Stunden</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="M2B0gF." resname="time">
|
||||||
|
<source>time</source>
|
||||||
|
<target>Uhrzeit</target>
|
||||||
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
@@ -1409,6 +1409,22 @@
|
|||||||
<source>about.title</source>
|
<source>about.title</source>
|
||||||
<target>About Kimai</target>
|
<target>About Kimai</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="i_XOmN4" resname="help_locales">
|
||||||
|
<source>help_locales</source>
|
||||||
|
<target>Supported languages with formatting rules</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="JmkKf48" resname="user.language.help">
|
||||||
|
<source>user.language.help</source>
|
||||||
|
<target>In addition to translating the interface, this setting is also responsible for the time, date and currency formats.</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="ulgdXdi" resname="hour_24">
|
||||||
|
<source>hour_24</source>
|
||||||
|
<target>24-hour</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="M2B0gF." resname="time">
|
||||||
|
<source>time</source>
|
||||||
|
<target>Time</target>
|
||||||
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
Reference in New Issue
Block a user