beta 3 (#3780)
* merge master - allow to upload twig invoice templates via UI * support adding existing teams with same name * permissions cannot be set right after role was created - fixes #3777 * allow to deactivate unique customer number validation - fixes #3762 * invalid message when trying to edit locked or exported timesheets in calendar - fixes #3766 * updated icons and manifest - fixes #3761
@@ -71,7 +71,11 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
|
|||||||
return Modal.getOrCreateInstance(this._getModalElement())
|
return Modal.getOrCreateInstance(this._getModalElement())
|
||||||
}
|
}
|
||||||
|
|
||||||
openUrlInModal(url)
|
/**
|
||||||
|
* @param {string} url
|
||||||
|
* @param {function(Response)} error the callback to execute if the fetch failed
|
||||||
|
*/
|
||||||
|
openUrlInModal(url, error)
|
||||||
{
|
{
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
headers.append('X-Requested-With', 'Kimai-Modal');
|
headers.append('X-Requested-With', 'Kimai-Modal');
|
||||||
@@ -91,8 +95,12 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
|
|||||||
this._openFormInModal(html);
|
this._openFormInModal(html);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((reason) => {
|
||||||
window.location = url;
|
if (error === undefined || error === null) {
|
||||||
|
window.location = url;
|
||||||
|
} else {
|
||||||
|
error(reason);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -376,7 +376,18 @@ export default class KimaiCalendar {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.hidePopover(eventClickInfo.el);
|
this.hidePopover(eventClickInfo.el);
|
||||||
MODAL.openUrlInModal(this.options.url.edit(event.id));
|
|
||||||
|
if (!event.extendedProps.exported || this.hasPermission('edit_exported')) {
|
||||||
|
MODAL.openUrlInModal(
|
||||||
|
this.options.url.edit(event.id), (reason) => {
|
||||||
|
// 403 = user is not allowed to edit the entry (e.g. lockdown mode)
|
||||||
|
if (reason.status !== 403) {
|
||||||
|
// keep the log, it might help with debugging
|
||||||
|
console.log(reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}};
|
}};
|
||||||
|
|
||||||
@@ -506,6 +517,8 @@ export default class KimaiCalendar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Only used on manipulated timesheets!
|
||||||
|
*
|
||||||
* @param {object} apiItem
|
* @param {object} apiItem
|
||||||
* @return {{activity, color: *, start, description, project, end, id, title: *, textColor: *, customer, tags: ([number,number,[],string,string]|*)}}
|
* @return {{activity, color: *, start, description, project, end, id, title: *, textColor: *, customer, tags: ([number,number,[],string,string]|*)}}
|
||||||
* @private
|
* @private
|
||||||
@@ -538,6 +551,7 @@ export default class KimaiCalendar {
|
|||||||
timesheet: apiItem.id,
|
timesheet: apiItem.id,
|
||||||
title: title,
|
title: title,
|
||||||
description: apiItem.description,
|
description: apiItem.description,
|
||||||
|
exported: apiItem.exported,
|
||||||
start: apiItem.begin,
|
start: apiItem.begin,
|
||||||
end: apiItem.end,
|
end: apiItem.end,
|
||||||
activity: apiItem.activity.name,
|
activity: apiItem.activity.name,
|
||||||
@@ -591,6 +605,12 @@ export default class KimaiCalendar {
|
|||||||
changeHandler(eventArg) {
|
changeHandler(eventArg) {
|
||||||
/** @type {EventApi} event */
|
/** @type {EventApi} event */
|
||||||
const event = eventArg.event;
|
const event = eventArg.event;
|
||||||
|
|
||||||
|
if (event.extendedProps.exported && !this.hasPermission('edit_exported')) {
|
||||||
|
eventArg.revert();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/** @type {KimaiAPI} API */
|
/** @type {KimaiAPI} API */
|
||||||
const API = this.kimai.getPlugin('api');
|
const API = this.kimai.getPlugin('api');
|
||||||
/** @type {KimaiAlert} ALERT */
|
/** @type {KimaiAlert} ALERT */
|
||||||
|
|||||||
175
phpstan.neon
@@ -990,21 +990,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/Controller/ActivityController.php
|
path: src/Controller/ActivityController.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Controller\\\\ActivityController\\:\\:detailsAction\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/ActivityController.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Controller\\\\ActivityController\\:\\:indexAction\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/ActivityController.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Controller\\\\ActivityController\\:\\:indexAction\\(\\) has parameter \\$page with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/ActivityController.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#1 \\$name of class App\\\\Entity\\\\Team constructor expects string, string\\|null given\\.$#"
|
message: "#^Parameter \\#1 \\$name of class App\\\\Entity\\\\Team constructor expects string, string\\|null given\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -1075,71 +1060,6 @@ parameters:
|
|||||||
count: 2
|
count: 2
|
||||||
path: src/Controller/CalendarController.php
|
path: src/Controller/CalendarController.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Controller\\\\CustomerController\\:\\:addCommentAction\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/CustomerController.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Controller\\\\CustomerController\\:\\:createAction\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/CustomerController.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Controller\\\\CustomerController\\:\\:createDefaultTeamAction\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/CustomerController.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Controller\\\\CustomerController\\:\\:deleteAction\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/CustomerController.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Controller\\\\CustomerController\\:\\:deleteCommentAction\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/CustomerController.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Controller\\\\CustomerController\\:\\:detailsAction\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/CustomerController.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Controller\\\\CustomerController\\:\\:editAction\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/CustomerController.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Controller\\\\CustomerController\\:\\:exportAction\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/CustomerController.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Controller\\\\CustomerController\\:\\:indexAction\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/CustomerController.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Controller\\\\CustomerController\\:\\:indexAction\\(\\) has parameter \\$page with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/CustomerController.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Controller\\\\CustomerController\\:\\:pinCommentAction\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/CustomerController.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Controller\\\\CustomerController\\:\\:projectsAction\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/CustomerController.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Controller\\\\CustomerController\\:\\:teamPermissionsAction\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/CustomerController.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#1 \\$address of method JeroenDesloovere\\\\VCard\\\\VCard\\:\\:addEmail\\(\\) expects string, string\\|null given\\.$#"
|
message: "#^Parameter \\#1 \\$address of method JeroenDesloovere\\\\VCard\\\\VCard\\:\\:addEmail\\(\\) expects string, string\\|null given\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -1290,11 +1210,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/Controller/InvoiceController.php
|
path: src/Controller/InvoiceController.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Controller\\\\InvoiceController\\:\\:uploadDocumentAction\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/InvoiceController.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#1 \\$string of function substr expects string, string\\|false given\\.$#"
|
message: "#^Parameter \\#1 \\$string of function substr expects string, string\\|false given\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -1340,86 +1255,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/Controller/ProjectController.php
|
path: src/Controller/ProjectController.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Controller\\\\ProjectController\\:\\:activitiesAction\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/ProjectController.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Controller\\\\ProjectController\\:\\:addCommentAction\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/ProjectController.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Controller\\\\ProjectController\\:\\:createAction\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/ProjectController.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Controller\\\\ProjectController\\:\\:createDefaultTeamAction\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/ProjectController.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Controller\\\\ProjectController\\:\\:createProject\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/ProjectController.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Controller\\\\ProjectController\\:\\:createWithCustomerAction\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/ProjectController.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Controller\\\\ProjectController\\:\\:deleteAction\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/ProjectController.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Controller\\\\ProjectController\\:\\:deleteCommentAction\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/ProjectController.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Controller\\\\ProjectController\\:\\:detailsAction\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/ProjectController.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Controller\\\\ProjectController\\:\\:duplicateAction\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/ProjectController.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Controller\\\\ProjectController\\:\\:editAction\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/ProjectController.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Controller\\\\ProjectController\\:\\:exportAction\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/ProjectController.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Controller\\\\ProjectController\\:\\:indexAction\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/ProjectController.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Controller\\\\ProjectController\\:\\:indexAction\\(\\) has parameter \\$page with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/ProjectController.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Controller\\\\ProjectController\\:\\:pinCommentAction\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/ProjectController.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Controller\\\\ProjectController\\:\\:teamPermissions\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/ProjectController.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#1 \\$name of class App\\\\Entity\\\\Team constructor expects string, string\\|null given\\.$#"
|
message: "#^Parameter \\#1 \\$name of class App\\\\Entity\\\\Team constructor expects string, string\\|null given\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -1980,11 +1815,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/Controller/UserController.php
|
path: src/Controller/UserController.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Controller\\\\UserController\\:\\:indexAction\\(\\) has parameter \\$page with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/UserController.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#1 \\$entries of method App\\\\Export\\\\Spreadsheet\\\\UserExporter\\:\\:export\\(\\) expects array\\<App\\\\Entity\\\\User\\>, iterable\\<App\\\\Entity\\\\User\\> given\\.$#"
|
message: "#^Parameter \\#1 \\$entries of method App\\\\Export\\\\Spreadsheet\\\\UserExporter\\:\\:export\\(\\) expects array\\<App\\\\Entity\\\\User\\>, iterable\\<App\\\\Entity\\\\User\\> given\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -3840,11 +3670,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/Form/Helper/ProjectHelper.php
|
path: src/Form/Helper/ProjectHelper.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Form\\\\InvoiceDocumentUploadForm\\:\\:validateDocument\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Form/InvoiceDocumentUploadForm.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Method App\\\\Form\\\\InvoiceDocumentUploadForm\\:\\:validateDocument\\(\\) has parameter \\$value with no type specified\\.$#"
|
message: "#^Method App\\\\Form\\\\InvoiceDocumentUploadForm\\:\\:validateDocument\\(\\) has parameter \\$value with no type specified\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
|
|||||||
BIN
public/apple-touch-icon-precomposed.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 30 KiB |
@@ -2,9 +2,7 @@
|
|||||||
<browserconfig>
|
<browserconfig>
|
||||||
<msapplication>
|
<msapplication>
|
||||||
<tile>
|
<tile>
|
||||||
<square70x70logo src="favicon/mstile-small.jpg"/>
|
<square150x150logo src="favicon/mstile-150x150.png"/>
|
||||||
<square150x150logo src="favicon/mstile-medium.jpg"/>
|
|
||||||
<wide310x150logo src="favicon/mstile-wide.jpg"/>
|
|
||||||
<square310x310logo src="favicon/mstile-large.jpg"/>
|
<square310x310logo src="favicon/mstile-large.jpg"/>
|
||||||
<TileColor>#00a300</TileColor>
|
<TileColor>#00a300</TileColor>
|
||||||
</tile>
|
</tile>
|
||||||
|
|||||||
2
public/build/app.8b0c21dc.js
Normal file
@@ -3,7 +3,7 @@
|
|||||||
"app": {
|
"app": {
|
||||||
"js": [
|
"js": [
|
||||||
"/build/runtime.f0079159.js",
|
"/build/runtime.f0079159.js",
|
||||||
"/build/app.694c6cb5.js"
|
"/build/app.8b0c21dc.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"/build/app.73c27235.css"
|
"/build/app.73c27235.css"
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
"calendar": {
|
"calendar": {
|
||||||
"js": [
|
"js": [
|
||||||
"/build/runtime.f0079159.js",
|
"/build/runtime.f0079159.js",
|
||||||
"/build/calendar.414914c7.js"
|
"/build/calendar.7dd54a5f.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"/build/calendar.bb473428.css"
|
"/build/calendar.bb473428.css"
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
},
|
},
|
||||||
"integrity": {
|
"integrity": {
|
||||||
"/build/runtime.f0079159.js": "sha384-H22sAW1aTvyIPqvHOvGXWSWTxf0y6mptp+MsVmyXCfjx/WJjBbhX9gbUZ+qIuihV",
|
"/build/runtime.f0079159.js": "sha384-H22sAW1aTvyIPqvHOvGXWSWTxf0y6mptp+MsVmyXCfjx/WJjBbhX9gbUZ+qIuihV",
|
||||||
"/build/app.694c6cb5.js": "sha384-RJ7fX6EM951Vhf6E63ZqGb9G+9fFVvC3V8xZt4CZDSk6TUFs7ITkLwwiKoj1xlAW",
|
"/build/app.8b0c21dc.js": "sha384-k0LUmYHZUK6bFVymGMzcHSq+utW09iR+YG6VMzqCiXfBGtVvb2Sdr0IB7w6hR+I6",
|
||||||
"/build/app.73c27235.css": "sha384-0vdzXrL3JuXTfiYJUdY17wIGFIbUTzQWfBQXbhcXcFRVmpmJYY1TZYSauxVUNwev",
|
"/build/app.73c27235.css": "sha384-0vdzXrL3JuXTfiYJUdY17wIGFIbUTzQWfBQXbhcXcFRVmpmJYY1TZYSauxVUNwev",
|
||||||
"/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",
|
||||||
@@ -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.414914c7.js": "sha384-v0La1MlAfoR6x87S4ucwp70e6ma0Y8uIf77i9ADTq/xB+D0Sn3/L67XQDXW1bwY6",
|
"/build/calendar.7dd54a5f.js": "sha384-oUWTIvkHr+k1UrlriT4+iA+irValKNLFoxva7ljQ/bsbczzaql3uWD0dlC4mF0ss",
|
||||||
"/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.73c27235.css",
|
||||||
"build/app.js": "/build/app.694c6cb5.js",
|
"build/app.js": "/build/app.8b0c21dc.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.414914c7.js",
|
"build/calendar.js": "/build/calendar.7dd54a5f.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"
|
||||||
|
|||||||
BIN
public/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 15 KiB |
BIN
public/favicon/mstile-150x150.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/favicon/mstile-310x310.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 13 KiB |
@@ -1,21 +1,31 @@
|
|||||||
{
|
{
|
||||||
"name": "Kimai Time-Tracker",
|
"name": "Kimai Time-Tracker",
|
||||||
"short_name": "Kimai 2",
|
"short_name": "Kimai",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "favicon-32x32.png",
|
"src": "/apple-touch-icon.png",
|
||||||
"sizes": "32x32",
|
"sizes": "152x152",
|
||||||
"type": "image/png",
|
"type": "image/png"
|
||||||
"density": "0.75"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "touch-icon-192x192.png",
|
"src": "/touch-icon-180x180.png",
|
||||||
|
"sizes": "180x180",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/touch-icon-192x192.png",
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
"type": "image/png",
|
"type": "image/png"
|
||||||
"density": "4.0"
|
},
|
||||||
|
{
|
||||||
|
"src": "/touch-icon-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"theme_color": "#ffffff",
|
"scope": "./",
|
||||||
|
"start_url": "./",
|
||||||
|
"theme-color": "#1d273b",
|
||||||
"background_color": "#ffffff",
|
"background_color": "#ffffff",
|
||||||
"display": "standalone"
|
"display": "standalone"
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,2 @@
|
|||||||
# www.robotstxt.org/
|
|
||||||
# www.google.com/support/webmasters/bin/answer.py?hl=en&answer=156449
|
|
||||||
|
|
||||||
User-agent: *
|
User-agent: *
|
||||||
Disallow: /
|
Disallow: /
|
||||||
|
|||||||
BIN
public/touch-icon-180x180.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
public/touch-icon-512x512.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
@@ -56,7 +56,7 @@ final class ActivityController extends AbstractController
|
|||||||
|
|
||||||
#[Route(path: '/', defaults: ['page' => 1], name: 'admin_activity', methods: ['GET'])]
|
#[Route(path: '/', defaults: ['page' => 1], name: 'admin_activity', methods: ['GET'])]
|
||||||
#[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'admin_activity_paginated', methods: ['GET'])]
|
#[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'admin_activity_paginated', methods: ['GET'])]
|
||||||
public function indexAction($page, Request $request)
|
public function indexAction(int $page, Request $request): Response
|
||||||
{
|
{
|
||||||
$query = new ActivityQuery();
|
$query = new ActivityQuery();
|
||||||
$query->setCurrentUser($this->getUser());
|
$query->setCurrentUser($this->getUser());
|
||||||
@@ -124,7 +124,7 @@ final class ActivityController extends AbstractController
|
|||||||
|
|
||||||
#[Route(path: '/{id}/details', name: 'activity_details', methods: ['GET', 'POST'])]
|
#[Route(path: '/{id}/details', name: 'activity_details', methods: ['GET', 'POST'])]
|
||||||
#[IsGranted('view', 'activity')]
|
#[IsGranted('view', 'activity')]
|
||||||
public function detailsAction(Activity $activity, TeamRepository $teamRepository, ActivityRateRepository $rateRepository, ActivityStatisticService $statisticService)
|
public function detailsAction(Activity $activity, TeamRepository $teamRepository, ActivityRateRepository $rateRepository, ActivityStatisticService $statisticService): Response
|
||||||
{
|
{
|
||||||
$event = new ActivityMetaDefinitionEvent($activity);
|
$event = new ActivityMetaDefinitionEvent($activity);
|
||||||
$this->dispatcher->dispatch($event);
|
$this->dispatcher->dispatch($event);
|
||||||
@@ -297,13 +297,11 @@ final class ActivityController extends AbstractController
|
|||||||
public function createDefaultTeamAction(Activity $activity, TeamRepository $teamRepository): Response
|
public function createDefaultTeamAction(Activity $activity, TeamRepository $teamRepository): Response
|
||||||
{
|
{
|
||||||
$defaultTeam = $teamRepository->findOneBy(['name' => $activity->getName()]);
|
$defaultTeam = $teamRepository->findOneBy(['name' => $activity->getName()]);
|
||||||
if (null !== $defaultTeam) {
|
|
||||||
$this->flashError('action.update.error', 'Team already existing');
|
|
||||||
|
|
||||||
return $this->redirectToRoute('activity_details', ['id' => $activity->getId()]);
|
if (null === $defaultTeam) {
|
||||||
|
$defaultTeam = new Team($activity->getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
$defaultTeam = new Team($activity->getName());
|
|
||||||
$defaultTeam->addTeamlead($this->getUser());
|
$defaultTeam->addTeamlead($this->getUser());
|
||||||
$defaultTeam->addActivity($activity);
|
$defaultTeam->addActivity($activity);
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ final class CustomerController extends AbstractController
|
|||||||
|
|
||||||
#[Route(path: '/', defaults: ['page' => 1], name: 'admin_customer', methods: ['GET'])]
|
#[Route(path: '/', defaults: ['page' => 1], name: 'admin_customer', methods: ['GET'])]
|
||||||
#[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'admin_customer_paginated', methods: ['GET'])]
|
#[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'admin_customer_paginated', methods: ['GET'])]
|
||||||
public function indexAction($page, Request $request)
|
public function indexAction(int $page, Request $request): Response
|
||||||
{
|
{
|
||||||
$query = new CustomerQuery();
|
$query = new CustomerQuery();
|
||||||
$query->setCurrentUser($this->getUser());
|
$query->setCurrentUser($this->getUser());
|
||||||
@@ -141,7 +141,7 @@ final class CustomerController extends AbstractController
|
|||||||
|
|
||||||
#[Route(path: '/create', name: 'admin_customer_create', methods: ['GET', 'POST'])]
|
#[Route(path: '/create', name: 'admin_customer_create', methods: ['GET', 'POST'])]
|
||||||
#[IsGranted('create_customer')]
|
#[IsGranted('create_customer')]
|
||||||
public function createAction(Request $request, CustomerService $customerService)
|
public function createAction(Request $request, CustomerService $customerService): Response
|
||||||
{
|
{
|
||||||
$customer = $customerService->createNewCustomer('');
|
$customer = $customerService->createNewCustomer('');
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ final class CustomerController extends AbstractController
|
|||||||
|
|
||||||
#[Route(path: '/{id}/permissions', name: 'admin_customer_permissions', methods: ['GET', 'POST'])]
|
#[Route(path: '/{id}/permissions', name: 'admin_customer_permissions', methods: ['GET', 'POST'])]
|
||||||
#[IsGranted('permissions', 'customer')]
|
#[IsGranted('permissions', 'customer')]
|
||||||
public function teamPermissionsAction(Customer $customer, Request $request)
|
public function teamPermissionsAction(Customer $customer, Request $request): Response
|
||||||
{
|
{
|
||||||
$form = $this->createForm(CustomerTeamPermissionForm::class, $customer, [
|
$form = $this->createForm(CustomerTeamPermissionForm::class, $customer, [
|
||||||
'action' => $this->generateUrl('admin_customer_permissions', ['id' => $customer->getId()]),
|
'action' => $this->generateUrl('admin_customer_permissions', ['id' => $customer->getId()]),
|
||||||
@@ -183,7 +183,7 @@ final class CustomerController extends AbstractController
|
|||||||
|
|
||||||
#[Route(path: '/{id}/comment_delete/{token}', name: 'customer_comment_delete', methods: ['GET'])]
|
#[Route(path: '/{id}/comment_delete/{token}', name: 'customer_comment_delete', methods: ['GET'])]
|
||||||
#[IsGranted(new Expression("is_granted('edit', subject.getCustomer()) and is_granted('comments', subject.getCustomer())"), 'comment')]
|
#[IsGranted(new Expression("is_granted('edit', subject.getCustomer()) and is_granted('comments', subject.getCustomer())"), 'comment')]
|
||||||
public function deleteCommentAction(CustomerComment $comment, string $token, CsrfTokenManagerInterface $csrfTokenManager)
|
public function deleteCommentAction(CustomerComment $comment, string $token, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||||
{
|
{
|
||||||
$customerId = $comment->getCustomer()->getId();
|
$customerId = $comment->getCustomer()->getId();
|
||||||
|
|
||||||
@@ -206,7 +206,7 @@ final class CustomerController extends AbstractController
|
|||||||
|
|
||||||
#[Route(path: '/{id}/comment_add', name: 'customer_comment_add', methods: ['POST'])]
|
#[Route(path: '/{id}/comment_add', name: 'customer_comment_add', methods: ['POST'])]
|
||||||
#[IsGranted('comments', 'customer')]
|
#[IsGranted('comments', 'customer')]
|
||||||
public function addCommentAction(Customer $customer, Request $request)
|
public function addCommentAction(Customer $customer, Request $request): Response
|
||||||
{
|
{
|
||||||
$comment = new CustomerComment($customer);
|
$comment = new CustomerComment($customer);
|
||||||
$form = $this->getCommentForm($comment);
|
$form = $this->getCommentForm($comment);
|
||||||
@@ -226,7 +226,7 @@ final class CustomerController extends AbstractController
|
|||||||
|
|
||||||
#[Route(path: '/{id}/comment_pin/{token}', name: 'customer_comment_pin', methods: ['GET'])]
|
#[Route(path: '/{id}/comment_pin/{token}', name: 'customer_comment_pin', methods: ['GET'])]
|
||||||
#[IsGranted(new Expression("is_granted('edit', subject.getCustomer()) and is_granted('comments', subject.getCustomer())"), 'comment')]
|
#[IsGranted(new Expression("is_granted('edit', subject.getCustomer()) and is_granted('comments', subject.getCustomer())"), 'comment')]
|
||||||
public function pinCommentAction(CustomerComment $comment, string $token, CsrfTokenManagerInterface $csrfTokenManager)
|
public function pinCommentAction(CustomerComment $comment, string $token, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||||
{
|
{
|
||||||
$customerId = $comment->getCustomer()->getId();
|
$customerId = $comment->getCustomer()->getId();
|
||||||
|
|
||||||
@@ -251,16 +251,14 @@ final class CustomerController extends AbstractController
|
|||||||
#[Route(path: '/{id}/create_team', name: 'customer_team_create', methods: ['GET'])]
|
#[Route(path: '/{id}/create_team', name: 'customer_team_create', methods: ['GET'])]
|
||||||
#[IsGranted('create_team')]
|
#[IsGranted('create_team')]
|
||||||
#[IsGranted('permissions', 'customer')]
|
#[IsGranted('permissions', 'customer')]
|
||||||
public function createDefaultTeamAction(Customer $customer, TeamRepository $teamRepository)
|
public function createDefaultTeamAction(Customer $customer, TeamRepository $teamRepository): Response
|
||||||
{
|
{
|
||||||
$defaultTeam = $teamRepository->findOneBy(['name' => $customer->getName()]);
|
$defaultTeam = $teamRepository->findOneBy(['name' => $customer->getName()]);
|
||||||
if (null !== $defaultTeam) {
|
|
||||||
$this->flashError('action.update.error', 'Team already existing');
|
|
||||||
|
|
||||||
return $this->redirectToRoute('customer_details', ['id' => $customer->getId()]);
|
if (null === $defaultTeam) {
|
||||||
|
$defaultTeam = new Team($customer->getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
$defaultTeam = new Team($customer->getName());
|
|
||||||
$defaultTeam->addTeamlead($this->getUser());
|
$defaultTeam->addTeamlead($this->getUser());
|
||||||
$defaultTeam->addCustomer($customer);
|
$defaultTeam->addCustomer($customer);
|
||||||
|
|
||||||
@@ -275,7 +273,7 @@ final class CustomerController extends AbstractController
|
|||||||
|
|
||||||
#[Route(path: '/{id}/projects/{page}', defaults: ['page' => 1], name: 'customer_projects', methods: ['GET', 'POST'])]
|
#[Route(path: '/{id}/projects/{page}', defaults: ['page' => 1], name: 'customer_projects', methods: ['GET', 'POST'])]
|
||||||
#[IsGranted('view', 'customer')]
|
#[IsGranted('view', 'customer')]
|
||||||
public function projectsAction(Customer $customer, int $page, ProjectRepository $projectRepository)
|
public function projectsAction(Customer $customer, int $page, ProjectRepository $projectRepository): Response
|
||||||
{
|
{
|
||||||
$query = new ProjectQuery();
|
$query = new ProjectQuery();
|
||||||
$query->setCurrentUser($this->getUser());
|
$query->setCurrentUser($this->getUser());
|
||||||
@@ -298,7 +296,7 @@ final class CustomerController extends AbstractController
|
|||||||
|
|
||||||
#[Route(path: '/{id}/details', name: 'customer_details', methods: ['GET', 'POST'])]
|
#[Route(path: '/{id}/details', name: 'customer_details', methods: ['GET', 'POST'])]
|
||||||
#[IsGranted('view', 'customer')]
|
#[IsGranted('view', 'customer')]
|
||||||
public function detailsAction(Customer $customer, TeamRepository $teamRepository, CustomerRateRepository $rateRepository, CustomerStatisticService $statisticService)
|
public function detailsAction(Customer $customer, TeamRepository $teamRepository, CustomerRateRepository $rateRepository, CustomerStatisticService $statisticService): Response
|
||||||
{
|
{
|
||||||
$event = new CustomerMetaDefinitionEvent($customer);
|
$event = new CustomerMetaDefinitionEvent($customer);
|
||||||
$this->dispatcher->dispatch($event);
|
$this->dispatcher->dispatch($event);
|
||||||
@@ -462,14 +460,14 @@ final class CustomerController extends AbstractController
|
|||||||
|
|
||||||
#[Route(path: '/{id}/edit', name: 'admin_customer_edit', methods: ['GET', 'POST'])]
|
#[Route(path: '/{id}/edit', name: 'admin_customer_edit', methods: ['GET', 'POST'])]
|
||||||
#[IsGranted('edit', 'customer')]
|
#[IsGranted('edit', 'customer')]
|
||||||
public function editAction(Customer $customer, Request $request)
|
public function editAction(Customer $customer, Request $request): Response
|
||||||
{
|
{
|
||||||
return $this->renderCustomerForm($customer, $request);
|
return $this->renderCustomerForm($customer, $request);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route(path: '/{id}/delete', name: 'admin_customer_delete', methods: ['GET', 'POST'])]
|
#[Route(path: '/{id}/delete', name: 'admin_customer_delete', methods: ['GET', 'POST'])]
|
||||||
#[IsGranted('delete', 'customer')]
|
#[IsGranted('delete', 'customer')]
|
||||||
public function deleteAction(Customer $customer, Request $request, CustomerStatisticService $statisticService)
|
public function deleteAction(Customer $customer, Request $request, CustomerStatisticService $statisticService): Response
|
||||||
{
|
{
|
||||||
$stats = $statisticService->getCustomerStatistics($customer);
|
$stats = $statisticService->getCustomerStatistics($customer);
|
||||||
|
|
||||||
@@ -511,7 +509,7 @@ final class CustomerController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[Route(path: '/export', name: 'customer_export', methods: ['GET'])]
|
#[Route(path: '/export', name: 'customer_export', methods: ['GET'])]
|
||||||
public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter)
|
public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter): Response
|
||||||
{
|
{
|
||||||
$query = new CustomerQuery();
|
$query = new CustomerQuery();
|
||||||
$query->setCurrentUser($this->getUser());
|
$query->setCurrentUser($this->getUser());
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Configuration\SystemConfiguration;
|
||||||
use App\Entity\Customer;
|
use App\Entity\Customer;
|
||||||
use App\Entity\Invoice;
|
use App\Entity\Invoice;
|
||||||
use App\Entity\InvoiceTemplate;
|
use App\Entity\InvoiceTemplate;
|
||||||
@@ -48,6 +49,7 @@ use Symfony\Component\Security\Csrf\CsrfToken;
|
|||||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Twig\Environment;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controller used to create invoices and manage invoice templates.
|
* Controller used to create invoices and manage invoice templates.
|
||||||
@@ -447,7 +449,7 @@ final class InvoiceController extends AbstractController
|
|||||||
|
|
||||||
#[Route(path: '/document_upload', name: 'admin_invoice_document_upload', methods: ['GET', 'POST'])]
|
#[Route(path: '/document_upload', name: 'admin_invoice_document_upload', methods: ['GET', 'POST'])]
|
||||||
#[IsGranted('upload_invoice_template')]
|
#[IsGranted('upload_invoice_template')]
|
||||||
public function uploadDocumentAction(Request $request, string $projectDirectory, InvoiceDocumentRepository $documentRepository)
|
public function uploadDocumentAction(Request $request, string $projectDirectory, InvoiceDocumentRepository $documentRepository, Environment $twig, SystemConfiguration $systemConfiguration): Response
|
||||||
{
|
{
|
||||||
$dir = $documentRepository->getUploadDirectory();
|
$dir = $documentRepository->getUploadDirectory();
|
||||||
$invoiceDir = $dir;
|
$invoiceDir = $dir;
|
||||||
@@ -456,6 +458,7 @@ final class InvoiceController extends AbstractController
|
|||||||
if ($invoiceDir[0] !== '/') {
|
if ($invoiceDir[0] !== '/') {
|
||||||
$invoiceDir = $projectDirectory . DIRECTORY_SEPARATOR . $dir;
|
$invoiceDir = $projectDirectory . DIRECTORY_SEPARATOR . $dir;
|
||||||
}
|
}
|
||||||
|
$invoiceDir = rtrim($invoiceDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
|
||||||
|
|
||||||
$used = [];
|
$used = [];
|
||||||
foreach ($this->templateRepository->findAll() as $template) {
|
foreach ($this->templateRepository->findAll() as $template) {
|
||||||
@@ -511,23 +514,56 @@ final class InvoiceController extends AbstractController
|
|||||||
/** @var UploadedFile $uploadedFile */
|
/** @var UploadedFile $uploadedFile */
|
||||||
$uploadedFile = $form->get('document')->getData();
|
$uploadedFile = $form->get('document')->getData();
|
||||||
|
|
||||||
$originalFilename = pathinfo($uploadedFile->getClientOriginalName(), PATHINFO_FILENAME);
|
$originalName = $uploadedFile->getClientOriginalName();
|
||||||
$safeFilename = transliterator_transliterate(
|
$safeFilename = null;
|
||||||
'Any-Latin; Latin-ASCII; [^A-Za-z0-9_] remove; Lower()',
|
$extension = null;
|
||||||
$originalFilename
|
$success = true;
|
||||||
);
|
|
||||||
|
|
||||||
$extension = $uploadedFile->guessExtension();
|
$allowed = InvoiceDocumentUploadForm::EXTENSIONS_NO_TWIG;
|
||||||
|
if ((bool) $systemConfiguration->find('invoice.upload_twig') === true) {
|
||||||
|
$allowed = InvoiceDocumentUploadForm::EXTENSIONS;
|
||||||
|
}
|
||||||
|
|
||||||
$newFilename = substr($safeFilename, 0, 20) . '.' . $extension;
|
foreach ($allowed as $ext) {
|
||||||
|
$len = \strlen($ext);
|
||||||
|
if (substr_compare($originalName, $ext, -$len) === 0) {
|
||||||
|
$extension = $ext;
|
||||||
|
$withoutExtension = str_replace($ext, '', $originalName);
|
||||||
|
$safeFilename = transliterator_transliterate(InvoiceDocumentUploadForm::FILENAME_RULE, $withoutExtension);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
if ($safeFilename === null || $extension === null) {
|
||||||
$uploadedFile->move($invoiceDir, $newFilename);
|
$success = false;
|
||||||
|
$this->flashError('Invalid file given');
|
||||||
|
} else {
|
||||||
|
$newFilename = substr($safeFilename, 0, 20) . $extension;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$uploadedFile->move($invoiceDir, $newFilename);
|
||||||
|
|
||||||
|
// if this is a twig file, we directly try to compile the template
|
||||||
|
if (stripos($newFilename, '.twig') !== false) {
|
||||||
|
try {
|
||||||
|
$twig->enableAutoReload();
|
||||||
|
$twig->load('@invoice/' . $newFilename);
|
||||||
|
$twig->disableAutoReload();
|
||||||
|
} catch (Exception $ex) {
|
||||||
|
unlink($invoiceDir . $newFilename);
|
||||||
|
$success = false;
|
||||||
|
$this->flashException($ex, 'File was deleted, as Twig template is broken: ' . $ex->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception $ex) {
|
||||||
|
$this->flashException($ex, 'action.upload.error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($success) {
|
||||||
$this->flashSuccess('action.update.success');
|
$this->flashSuccess('action.update.success');
|
||||||
|
|
||||||
return $this->redirectToRoute('admin_invoice_document_upload');
|
return $this->redirectToRoute('admin_invoice_document_upload');
|
||||||
} catch (Exception $ex) {
|
|
||||||
$this->flashException($ex, 'action.upload.error');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ final class ProjectController extends AbstractController
|
|||||||
|
|
||||||
#[Route(path: '/', defaults: ['page' => 1], name: 'admin_project', methods: ['GET'])]
|
#[Route(path: '/', defaults: ['page' => 1], name: 'admin_project', methods: ['GET'])]
|
||||||
#[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'admin_project_paginated', methods: ['GET'])]
|
#[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'admin_project_paginated', methods: ['GET'])]
|
||||||
public function indexAction($page, Request $request)
|
public function indexAction(int $page, Request $request): Response
|
||||||
{
|
{
|
||||||
$query = new ProjectQuery();
|
$query = new ProjectQuery();
|
||||||
$query->setCurrentUser($this->getUser());
|
$query->setCurrentUser($this->getUser());
|
||||||
@@ -134,7 +134,7 @@ final class ProjectController extends AbstractController
|
|||||||
|
|
||||||
#[Route(path: '/{id}/permissions', name: 'admin_project_permissions', methods: ['GET', 'POST'])]
|
#[Route(path: '/{id}/permissions', name: 'admin_project_permissions', methods: ['GET', 'POST'])]
|
||||||
#[IsGranted('permissions', 'project')]
|
#[IsGranted('permissions', 'project')]
|
||||||
public function teamPermissions(Project $project, Request $request)
|
public function teamPermissions(Project $project, Request $request): Response
|
||||||
{
|
{
|
||||||
$form = $this->createForm(ProjectTeamPermissionForm::class, $project, [
|
$form = $this->createForm(ProjectTeamPermissionForm::class, $project, [
|
||||||
'action' => $this->generateUrl('admin_project_permissions', ['id' => $project->getId()]),
|
'action' => $this->generateUrl('admin_project_permissions', ['id' => $project->getId()]),
|
||||||
@@ -167,19 +167,19 @@ final class ProjectController extends AbstractController
|
|||||||
|
|
||||||
#[Route(path: '/create/{customer}', name: 'admin_project_create_with_customer', methods: ['GET', 'POST'])]
|
#[Route(path: '/create/{customer}', name: 'admin_project_create_with_customer', methods: ['GET', 'POST'])]
|
||||||
#[IsGranted('create_project')]
|
#[IsGranted('create_project')]
|
||||||
public function createWithCustomerAction(Request $request, Customer $customer)
|
public function createWithCustomerAction(Request $request, Customer $customer): Response
|
||||||
{
|
{
|
||||||
return $this->createProject($request, $customer);
|
return $this->createProject($request, $customer);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route(path: '/create', name: 'admin_project_create', methods: ['GET', 'POST'])]
|
#[Route(path: '/create', name: 'admin_project_create', methods: ['GET', 'POST'])]
|
||||||
#[IsGranted('create_project')]
|
#[IsGranted('create_project')]
|
||||||
public function createAction(Request $request)
|
public function createAction(Request $request): Response
|
||||||
{
|
{
|
||||||
return $this->createProject($request, null);
|
return $this->createProject($request, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createProject(Request $request, ?Customer $customer = null)
|
private function createProject(Request $request, ?Customer $customer = null): Response
|
||||||
{
|
{
|
||||||
$project = $this->projectService->createNewProject($customer);
|
$project = $this->projectService->createNewProject($customer);
|
||||||
|
|
||||||
@@ -206,7 +206,7 @@ final class ProjectController extends AbstractController
|
|||||||
|
|
||||||
#[Route(path: '/{id}/comment_delete/{token}', name: 'project_comment_delete', methods: ['GET'])]
|
#[Route(path: '/{id}/comment_delete/{token}', name: 'project_comment_delete', methods: ['GET'])]
|
||||||
#[IsGranted(new Expression("is_granted('edit', subject.getProject()) and is_granted('comments', subject.getProject())"), 'comment')]
|
#[IsGranted(new Expression("is_granted('edit', subject.getProject()) and is_granted('comments', subject.getProject())"), 'comment')]
|
||||||
public function deleteCommentAction(ProjectComment $comment, string $token, CsrfTokenManagerInterface $csrfTokenManager)
|
public function deleteCommentAction(ProjectComment $comment, string $token, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||||
{
|
{
|
||||||
$projectId = $comment->getProject()->getId();
|
$projectId = $comment->getProject()->getId();
|
||||||
|
|
||||||
@@ -229,7 +229,7 @@ final class ProjectController extends AbstractController
|
|||||||
|
|
||||||
#[Route(path: '/{id}/comment_add', name: 'project_comment_add', methods: ['POST'])]
|
#[Route(path: '/{id}/comment_add', name: 'project_comment_add', methods: ['POST'])]
|
||||||
#[IsGranted('comments', 'project')]
|
#[IsGranted('comments', 'project')]
|
||||||
public function addCommentAction(Project $project, Request $request)
|
public function addCommentAction(Project $project, Request $request): Response
|
||||||
{
|
{
|
||||||
$comment = new ProjectComment($project);
|
$comment = new ProjectComment($project);
|
||||||
$form = $this->getCommentForm($comment);
|
$form = $this->getCommentForm($comment);
|
||||||
@@ -249,7 +249,7 @@ final class ProjectController extends AbstractController
|
|||||||
|
|
||||||
#[Route(path: '/{id}/comment_pin/{token}', name: 'project_comment_pin', methods: ['GET'])]
|
#[Route(path: '/{id}/comment_pin/{token}', name: 'project_comment_pin', methods: ['GET'])]
|
||||||
#[IsGranted(new Expression("is_granted('edit', subject.getProject()) and is_granted('comments', subject.getProject())"), 'comment')]
|
#[IsGranted(new Expression("is_granted('edit', subject.getProject()) and is_granted('comments', subject.getProject())"), 'comment')]
|
||||||
public function pinCommentAction(ProjectComment $comment, string $token, CsrfTokenManagerInterface $csrfTokenManager)
|
public function pinCommentAction(ProjectComment $comment, string $token, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||||
{
|
{
|
||||||
$projectId = $comment->getProject()->getId();
|
$projectId = $comment->getProject()->getId();
|
||||||
|
|
||||||
@@ -274,16 +274,14 @@ final class ProjectController extends AbstractController
|
|||||||
#[Route(path: '/{id}/create_team', name: 'project_team_create', methods: ['GET'])]
|
#[Route(path: '/{id}/create_team', name: 'project_team_create', methods: ['GET'])]
|
||||||
#[IsGranted('create_team')]
|
#[IsGranted('create_team')]
|
||||||
#[IsGranted('permissions', 'project')]
|
#[IsGranted('permissions', 'project')]
|
||||||
public function createDefaultTeamAction(Project $project, TeamRepository $teamRepository)
|
public function createDefaultTeamAction(Project $project, TeamRepository $teamRepository): Response
|
||||||
{
|
{
|
||||||
$defaultTeam = $teamRepository->findOneBy(['name' => $project->getName()]);
|
$defaultTeam = $teamRepository->findOneBy(['name' => $project->getName()]);
|
||||||
if (null !== $defaultTeam) {
|
|
||||||
$this->flashError('action.update.error', 'Team already existing');
|
|
||||||
|
|
||||||
return $this->redirectToRoute('project_details', ['id' => $project->getId()]);
|
if (null === $defaultTeam) {
|
||||||
|
$defaultTeam = new Team($project->getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
$defaultTeam = new Team($project->getName());
|
|
||||||
$defaultTeam->addTeamlead($this->getUser());
|
$defaultTeam->addTeamlead($this->getUser());
|
||||||
$defaultTeam->addProject($project);
|
$defaultTeam->addProject($project);
|
||||||
|
|
||||||
@@ -298,7 +296,7 @@ final class ProjectController extends AbstractController
|
|||||||
|
|
||||||
#[Route(path: '/{id}/activities/{page}', defaults: ['page' => 1], name: 'project_activities', methods: ['GET', 'POST'])]
|
#[Route(path: '/{id}/activities/{page}', defaults: ['page' => 1], name: 'project_activities', methods: ['GET', 'POST'])]
|
||||||
#[IsGranted('view', 'project')]
|
#[IsGranted('view', 'project')]
|
||||||
public function activitiesAction(Project $project, int $page, ActivityRepository $activityRepository)
|
public function activitiesAction(Project $project, int $page, ActivityRepository $activityRepository): Response
|
||||||
{
|
{
|
||||||
$query = new ActivityQuery();
|
$query = new ActivityQuery();
|
||||||
$query->setCurrentUser($this->getUser());
|
$query->setCurrentUser($this->getUser());
|
||||||
@@ -322,7 +320,7 @@ final class ProjectController extends AbstractController
|
|||||||
|
|
||||||
#[Route(path: '/{id}/details', name: 'project_details', methods: ['GET', 'POST'])]
|
#[Route(path: '/{id}/details', name: 'project_details', methods: ['GET', 'POST'])]
|
||||||
#[IsGranted('view', 'project')]
|
#[IsGranted('view', 'project')]
|
||||||
public function detailsAction(Project $project, TeamRepository $teamRepository, ProjectRateRepository $rateRepository, ProjectStatisticService $statisticService, CsrfTokenManagerInterface $csrfTokenManager)
|
public function detailsAction(Project $project, TeamRepository $teamRepository, ProjectRateRepository $rateRepository, ProjectStatisticService $statisticService, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||||
{
|
{
|
||||||
$event = new ProjectMetaDefinitionEvent($project);
|
$event = new ProjectMetaDefinitionEvent($project);
|
||||||
$this->dispatcher->dispatch($event);
|
$this->dispatcher->dispatch($event);
|
||||||
@@ -427,7 +425,7 @@ final class ProjectController extends AbstractController
|
|||||||
|
|
||||||
#[Route(path: '/{id}/edit', name: 'admin_project_edit', methods: ['GET', 'POST'])]
|
#[Route(path: '/{id}/edit', name: 'admin_project_edit', methods: ['GET', 'POST'])]
|
||||||
#[IsGranted('edit', 'project')]
|
#[IsGranted('edit', 'project')]
|
||||||
public function editAction(Project $project, Request $request)
|
public function editAction(Project $project, Request $request): Response
|
||||||
{
|
{
|
||||||
$editForm = $this->createEditForm($project);
|
$editForm = $this->createEditForm($project);
|
||||||
$editForm->handleRequest($request);
|
$editForm->handleRequest($request);
|
||||||
@@ -452,7 +450,7 @@ final class ProjectController extends AbstractController
|
|||||||
|
|
||||||
#[Route(path: '/{id}/duplicate/{token}', name: 'admin_project_duplicate', methods: ['GET', 'POST'])]
|
#[Route(path: '/{id}/duplicate/{token}', name: 'admin_project_duplicate', methods: ['GET', 'POST'])]
|
||||||
#[IsGranted('edit', 'project')]
|
#[IsGranted('edit', 'project')]
|
||||||
public function duplicateAction(Project $project, string $token, ProjectDuplicationService $projectDuplicationService, CsrfTokenManagerInterface $csrfTokenManager)
|
public function duplicateAction(Project $project, string $token, ProjectDuplicationService $projectDuplicationService, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||||
{
|
{
|
||||||
if (!$csrfTokenManager->isTokenValid(new CsrfToken('project.duplicate', $token))) {
|
if (!$csrfTokenManager->isTokenValid(new CsrfToken('project.duplicate', $token))) {
|
||||||
$this->flashError('action.csrf.error');
|
$this->flashError('action.csrf.error');
|
||||||
@@ -471,7 +469,7 @@ final class ProjectController extends AbstractController
|
|||||||
|
|
||||||
#[Route(path: '/{id}/delete', name: 'admin_project_delete', methods: ['GET', 'POST'])]
|
#[Route(path: '/{id}/delete', name: 'admin_project_delete', methods: ['GET', 'POST'])]
|
||||||
#[IsGranted('delete', 'project')]
|
#[IsGranted('delete', 'project')]
|
||||||
public function deleteAction(Project $project, Request $request, ProjectStatisticService $statisticService)
|
public function deleteAction(Project $project, Request $request, ProjectStatisticService $statisticService): Response
|
||||||
{
|
{
|
||||||
$stats = $statisticService->getProjectStatistics($project);
|
$stats = $statisticService->getProjectStatistics($project);
|
||||||
|
|
||||||
@@ -514,7 +512,7 @@ final class ProjectController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[Route(path: '/export', name: 'project_export', methods: ['GET'])]
|
#[Route(path: '/export', name: 'project_export', methods: ['GET'])]
|
||||||
public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter)
|
public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter): Response
|
||||||
{
|
{
|
||||||
$query = new ProjectQuery();
|
$query = new ProjectQuery();
|
||||||
$query->setCurrentUser($this->getUser());
|
$query->setCurrentUser($this->getUser());
|
||||||
|
|||||||
@@ -432,6 +432,7 @@ final class SystemConfigurationController extends AbstractController
|
|||||||
])
|
])
|
||||||
->setRequired(true)
|
->setRequired(true)
|
||||||
->setType(TextType::class)
|
->setType(TextType::class)
|
||||||
|
->setConstraints([new NotBlank()])
|
||||||
->setTranslationDomain('system-configuration'),
|
->setTranslationDomain('system-configuration'),
|
||||||
]),
|
]),
|
||||||
$authentication,
|
$authentication,
|
||||||
@@ -459,6 +460,10 @@ final class SystemConfigurationController extends AbstractController
|
|||||||
->setRequired(true)
|
->setRequired(true)
|
||||||
->setType(TextType::class)
|
->setType(TextType::class)
|
||||||
->setTranslationDomain('system-configuration'),
|
->setTranslationDomain('system-configuration'),
|
||||||
|
(new Configuration('customer.rules.allow_duplicate_number'))
|
||||||
|
->setLabel('customer.allow_duplicate_number')
|
||||||
|
->setType(YesNoType::class)
|
||||||
|
->setTranslationDomain('system-configuration'),
|
||||||
]),
|
]),
|
||||||
(new SystemConfigurationModel('project'))
|
(new SystemConfigurationModel('project'))
|
||||||
->setConfiguration([
|
->setConfiguration([
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ final class UserController extends AbstractController
|
|||||||
|
|
||||||
#[Route(path: '/', defaults: ['page' => 1], name: 'admin_user', methods: ['GET'])]
|
#[Route(path: '/', defaults: ['page' => 1], name: 'admin_user', methods: ['GET'])]
|
||||||
#[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'admin_user_paginated', methods: ['GET'])]
|
#[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'admin_user_paginated', methods: ['GET'])]
|
||||||
public function indexAction($page, Request $request): Response
|
public function indexAction(int $page, Request $request): Response
|
||||||
{
|
{
|
||||||
$query = new UserQuery();
|
$query = new UserQuery();
|
||||||
$query->setCurrentUser($this->getUser());
|
$query->setCurrentUser($this->getUser());
|
||||||
|
|||||||
@@ -322,6 +322,9 @@ final class Configuration implements ConfigurationInterface
|
|||||||
->scalarNode('number_format')
|
->scalarNode('number_format')
|
||||||
->defaultValue('{Y}/{cy,3}')
|
->defaultValue('{Y}/{cy,3}')
|
||||||
->end()
|
->end()
|
||||||
|
->booleanNode('upload_twig')
|
||||||
|
->defaultTrue()
|
||||||
|
->end()
|
||||||
->end()
|
->end()
|
||||||
;
|
;
|
||||||
|
|
||||||
@@ -531,6 +534,14 @@ final class Configuration implements ConfigurationInterface
|
|||||||
->scalarNode('number_format')
|
->scalarNode('number_format')
|
||||||
->defaultValue('{cc,4}')
|
->defaultValue('{cc,4}')
|
||||||
->end()
|
->end()
|
||||||
|
->arrayNode('rules')
|
||||||
|
->addDefaultsIfNotSet()
|
||||||
|
->children()
|
||||||
|
->booleanNode('allow_duplicate_number')
|
||||||
|
->defaultFalse()
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
->end()
|
->end()
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|||||||
@@ -10,21 +10,21 @@
|
|||||||
namespace App\Entity;
|
namespace App\Entity;
|
||||||
|
|
||||||
use App\Export\Annotation as Exporter;
|
use App\Export\Annotation as Exporter;
|
||||||
|
use App\Validator\Constraints as Constraints;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use JMS\Serializer\Annotation as Serializer;
|
use JMS\Serializer\Annotation as Serializer;
|
||||||
use OpenApi\Attributes as OA;
|
use OpenApi\Attributes as OA;
|
||||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
|
||||||
use Symfony\Component\Validator\Constraints as Assert;
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
#[ORM\Table(name: 'kimai2_customers')]
|
#[ORM\Table(name: 'kimai2_customers')]
|
||||||
#[ORM\Index(columns: ['visible'])]
|
#[ORM\Index(columns: ['visible'])]
|
||||||
#[ORM\Entity(repositoryClass: 'App\Repository\CustomerRepository')]
|
#[ORM\Entity(repositoryClass: 'App\Repository\CustomerRepository')]
|
||||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||||
#[UniqueEntity('number')]
|
|
||||||
#[Serializer\ExclusionPolicy('all')]
|
#[Serializer\ExclusionPolicy('all')]
|
||||||
#[Exporter\Order(['id', 'name', 'company', 'number', 'vatId', 'address', 'contact', 'email', 'phone', 'mobile', 'fax', 'homepage', 'country', 'currency', 'timezone', 'budget', 'timeBudget', 'budgetType', 'color', 'visible', 'teams', 'comment', 'billable'])]
|
#[Exporter\Order(['id', 'name', 'company', 'number', 'vatId', 'address', 'contact', 'email', 'phone', 'mobile', 'fax', 'homepage', 'country', 'currency', 'timezone', 'budget', 'timeBudget', 'budgetType', 'color', 'visible', 'teams', 'comment', 'billable'])]
|
||||||
|
#[Constraints\Customer]
|
||||||
class Customer implements EntityWithMetaFields, EntityWithBudget
|
class Customer implements EntityWithMetaFields, EntityWithBudget
|
||||||
{
|
{
|
||||||
public const DEFAULT_CURRENCY = 'EUR';
|
public const DEFAULT_CURRENCY = 'EUR';
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
|
|
||||||
namespace App\EventSubscriber;
|
namespace App\EventSubscriber;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
|
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
|
||||||
@@ -19,6 +21,10 @@ use Symfony\Component\Security\Core\Exception\AuthenticationExpiredException;
|
|||||||
|
|
||||||
final class AjaxAuthenticationSubscriber implements EventSubscriberInterface
|
final class AjaxAuthenticationSubscriber implements EventSubscriberInterface
|
||||||
{
|
{
|
||||||
|
public function __construct(private Security $security)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public static function getSubscribedEvents(): array
|
public static function getSubscribedEvents(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
@@ -29,6 +35,12 @@ final class AjaxAuthenticationSubscriber implements EventSubscriberInterface
|
|||||||
public function onCoreException(ExceptionEvent $event): void
|
public function onCoreException(ExceptionEvent $event): void
|
||||||
{
|
{
|
||||||
$request = $event->getRequest();
|
$request = $event->getRequest();
|
||||||
|
|
||||||
|
// do not act upon requests which were triggered by fully logged-in users
|
||||||
|
if ($this->security->getUser() instanceof User && $this->security->isGranted('IS_AUTHENTICATED_FULLY')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$header = $request->headers->get('X-Requested-With');
|
$header = $request->headers->get('X-Requested-With');
|
||||||
|
|
||||||
if ($request->isXmlHttpRequest() || ($header !== null && str_contains(strtolower($header), 'kimai'))) {
|
if ($request->isXmlHttpRequest() || ($header !== null && str_contains(strtolower($header), 'kimai'))) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
namespace App\Form;
|
namespace App\Form;
|
||||||
|
|
||||||
|
use App\Configuration\SystemConfiguration;
|
||||||
use App\Repository\InvoiceDocumentRepository;
|
use App\Repository\InvoiceDocumentRepository;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\FileType;
|
use Symfony\Component\Form\Extension\Core\Type\FileType;
|
||||||
@@ -21,28 +22,45 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
|||||||
|
|
||||||
final class InvoiceDocumentUploadForm extends AbstractType
|
final class InvoiceDocumentUploadForm extends AbstractType
|
||||||
{
|
{
|
||||||
public function __construct(private InvoiceDocumentRepository $repository)
|
public const EXTENSIONS = ['.html.twig', '.pdf.twig', '.docx', '.xlsx', '.ods'];
|
||||||
|
public const EXTENSIONS_NO_TWIG = ['.docx', '.xlsx', '.ods'];
|
||||||
|
public const FILENAME_RULE = 'Any-Latin; Latin-ASCII; [^A-Za-z0-9_\-] remove; Lower()';
|
||||||
|
|
||||||
|
/** @var array<string> */
|
||||||
|
private array $extensions = [];
|
||||||
|
|
||||||
|
public function __construct(private InvoiceDocumentRepository $repository, private SystemConfiguration $systemConfiguration)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
{
|
{
|
||||||
|
$this->extensions = self::EXTENSIONS_NO_TWIG;
|
||||||
|
$extensions = 'DOCX, ODS, XLSX';
|
||||||
$mimetypes = [
|
$mimetypes = [
|
||||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
'application/vnd.oasis.opendocument.spreadsheet',
|
'application/vnd.oasis.opendocument.spreadsheet',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if ((bool) $this->systemConfiguration->find('invoice.upload_twig') === true) {
|
||||||
|
$this->extensions = self::EXTENSIONS;
|
||||||
|
$extensions = 'DOCX, ODS, XLSX, TWIG (PDF & HTML)';
|
||||||
|
$mimetypes = array_merge($mimetypes, [
|
||||||
|
'application/octet-stream', // needed for twig templates
|
||||||
|
'text/html', // needed for twig templates
|
||||||
|
'text/plain', // needed for twig templates
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
$builder
|
$builder
|
||||||
->add('document', FileType::class, [
|
->add('document', FileType::class, [
|
||||||
'label' => 'invoice_renderer',
|
'label' => 'invoice_renderer',
|
||||||
'translation_domain' => 'invoice-renderer',
|
'translation_domain' => 'invoice-renderer',
|
||||||
'help' => 'help.upload',
|
'help' => 'help.upload',
|
||||||
|
'help_translation_parameters' => ['%extensions%' => $extensions],
|
||||||
'mapped' => false,
|
'mapped' => false,
|
||||||
'required' => true,
|
'required' => true,
|
||||||
'attr' => [
|
|
||||||
'accept' => implode(',', $mimetypes)
|
|
||||||
],
|
|
||||||
'constraints' => [
|
'constraints' => [
|
||||||
new File([
|
new File([
|
||||||
'mimeTypes' => $mimetypes,
|
'mimeTypes' => $mimetypes,
|
||||||
@@ -54,7 +72,7 @@ final class InvoiceDocumentUploadForm extends AbstractType
|
|||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function validateDocument($value, ExecutionContextInterface $context)
|
public function validateDocument($value, ExecutionContextInterface $context): void
|
||||||
{
|
{
|
||||||
if (!($value instanceof UploadedFile)) {
|
if (!($value instanceof UploadedFile)) {
|
||||||
return;
|
return;
|
||||||
@@ -71,6 +89,48 @@ final class InvoiceDocumentUploadForm extends AbstractType
|
|||||||
->setTranslationDomain('validators')
|
->setTranslationDomain('validators')
|
||||||
->setCode('kimai-invoice-document-upload-01')
|
->setCode('kimai-invoice-document-upload-01')
|
||||||
->addViolation();
|
->addViolation();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$extension = null;
|
||||||
|
$nameWithoutExtension = null;
|
||||||
|
|
||||||
|
foreach ($this->extensions as $ext) {
|
||||||
|
$len = \strlen($ext);
|
||||||
|
if (substr_compare($name, $ext, -$len) === 0) {
|
||||||
|
$extension = $ext;
|
||||||
|
$nameWithoutExtension = str_replace($ext, '', $name);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($extension === null || $nameWithoutExtension === null) {
|
||||||
|
$context->buildViolation('This invoice document cannot be used, allowed file extensions are: %extensions%')
|
||||||
|
->setParameters(['%extensions%' => implode(', ', $this->extensions)])
|
||||||
|
->setTranslationDomain('validators')
|
||||||
|
->setCode('kimai-invoice-document-upload-02')
|
||||||
|
->addViolation();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$safeFilename = transliterator_transliterate(self::FILENAME_RULE, $nameWithoutExtension);
|
||||||
|
|
||||||
|
if ($safeFilename !== $nameWithoutExtension) {
|
||||||
|
$context->buildViolation('This invoice document cannot be used, filename may only contain the following ascii character: %character%')
|
||||||
|
->setParameters(['%character%' => 'A-Z a-z 0-9 _ -'])
|
||||||
|
->setTranslationDomain('validators')
|
||||||
|
->setCode('kimai-invoice-document-upload-03')
|
||||||
|
->addViolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mb_strlen($nameWithoutExtension) > 20) {
|
||||||
|
$context->buildViolation('This invoice document cannot be used, allowed filename length without extension is %character% character.')
|
||||||
|
->setParameters(['%character%' => 20])
|
||||||
|
->setTranslationDomain('validators')
|
||||||
|
->setCode('kimai-invoice-document-upload-04')
|
||||||
|
->addViolation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
29
src/Validator/Constraints/Customer.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Validator\Constraints;
|
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
|
||||||
|
#[\Attribute(\Attribute::TARGET_CLASS)]
|
||||||
|
final class Customer extends Constraint
|
||||||
|
{
|
||||||
|
public const CUSTOMER_NUMBER_EXISTING = 'kimai-customer-00';
|
||||||
|
|
||||||
|
protected const ERROR_NAMES = [
|
||||||
|
self::CUSTOMER_NUMBER_EXISTING => 'This account number is already used.',
|
||||||
|
];
|
||||||
|
|
||||||
|
public string $message = 'This customer has invalid settings.';
|
||||||
|
|
||||||
|
public function getTargets(): string|array
|
||||||
|
{
|
||||||
|
return self::CLASS_CONSTRAINT;
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/Validator/Constraints/CustomerValidator.php
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Validator\Constraints;
|
||||||
|
|
||||||
|
use App\Configuration\SystemConfiguration;
|
||||||
|
use App\Entity\Customer as CustomerEntity;
|
||||||
|
use App\Repository\CustomerRepository;
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
use Symfony\Component\Validator\ConstraintValidator;
|
||||||
|
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||||
|
|
||||||
|
final class CustomerValidator extends ConstraintValidator
|
||||||
|
{
|
||||||
|
public function __construct(private SystemConfiguration $systemConfiguration, private CustomerRepository $customerRepository)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param CustomerEntity|mixed $value
|
||||||
|
* @param Constraint $constraint
|
||||||
|
*/
|
||||||
|
public function validate(mixed $value, Constraint $constraint): void
|
||||||
|
{
|
||||||
|
if (!($constraint instanceof Customer)) {
|
||||||
|
throw new UnexpectedTypeException($constraint, Customer::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!($value instanceof CustomerEntity)) {
|
||||||
|
throw new UnexpectedTypeException($value, CustomerEntity::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((bool) $this->systemConfiguration->find('customer.rules.allow_duplicate_number') === false && (($number = $value->getNumber()) !== null)) {
|
||||||
|
$tmp = $this->customerRepository->findOneBy(['number' => $number]);
|
||||||
|
if ($tmp !== null && $tmp->getId() !== $value->getId()) {
|
||||||
|
$this->context->buildViolation(Customer::getErrorName(Customer::CUSTOMER_NUMBER_EXISTING))
|
||||||
|
->atPath('number')
|
||||||
|
->setTranslationDomain('validators')
|
||||||
|
->setCode(Customer::CUSTOMER_NUMBER_EXISTING)
|
||||||
|
->addViolation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,6 +69,7 @@
|
|||||||
{% set editRoute = 'timesheet_edit' %}
|
{% set editRoute = 'timesheet_edit' %}
|
||||||
{% set canCreate = is_granted('create_own_timesheet') %}
|
{% set canCreate = is_granted('create_own_timesheet') %}
|
||||||
{% set canEdit = is_granted('edit_own_timesheet') %}
|
{% set canEdit = is_granted('edit_own_timesheet') %}
|
||||||
|
{% set canEditExported = is_granted('edit_exported_timesheet') %}
|
||||||
{% set isForeignEdit = false %}
|
{% set isForeignEdit = false %}
|
||||||
{% if user != app.user %}
|
{% if user != app.user %}
|
||||||
{% set isForeignEdit = true %}
|
{% set isForeignEdit = true %}
|
||||||
@@ -104,6 +105,7 @@
|
|||||||
},
|
},
|
||||||
permissions: {
|
permissions: {
|
||||||
edit: {% if canEdit %}true{% else %}false{% endif %},
|
edit: {% if canEdit %}true{% else %}false{% endif %},
|
||||||
|
edit_exported: {% if canEditExported %}true{% else %}false{% endif %},
|
||||||
create: {% if canCreate %}true{% else %}false{% endif %},
|
create: {% if canCreate %}true{% else %}false{% endif %},
|
||||||
edit_begin: {% if can_edit_begin %}true{% else %}false{% endif %},
|
edit_begin: {% if can_edit_begin %}true{% else %}false{% endif %},
|
||||||
edit_end: {% if can_edit_end %}true{% else %}false{% endif %},
|
edit_end: {% if can_edit_end %}true{% else %}false{% endif %},
|
||||||
|
|||||||
@@ -8,13 +8,17 @@
|
|||||||
{% block box_title %}
|
{% block box_title %}
|
||||||
{{ 'upload'|trans }}
|
{{ 'upload'|trans }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block box_body %}
|
{% block box_before %}
|
||||||
{{ form_start(form) }}
|
{{ form_start(form) }}
|
||||||
|
{% endblock %}
|
||||||
|
{% block box_after %}
|
||||||
|
{{ form_end(form) }}
|
||||||
|
{% endblock %}
|
||||||
|
{% block box_body %}
|
||||||
{{ form_row(form.document) }}
|
{{ form_row(form.document) }}
|
||||||
<p>
|
<p>
|
||||||
<a href="https://github.com/kimai/invoice-templates" target="_blank">{{ 'download_invoice_renderer'|trans({}, 'invoice-renderer') }}</a>
|
<a href="https://github.com/kimai/invoice-templates" target="_blank">{{ 'download_invoice_renderer'|trans({}, 'invoice-renderer') }}</a>
|
||||||
</p>
|
</p>
|
||||||
{{ form_end(form) }}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block box_footer %}
|
{% block box_footer %}
|
||||||
<button type="submit" class="btn btn-primary">{{ 'action.save'|trans }}</button>
|
<button type="submit" class="btn btn-primary">{{ 'action.save'|trans }}</button>
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
<link rel="shortcut icon" type="image/x-icon" href="{{ asset('favicon.ico') }}">
|
|
||||||
<link rel="icon" type="image/x-icon" href="{{ asset('favicon.ico') }}">
|
<link rel="icon" type="image/x-icon" href="{{ asset('favicon.ico') }}">
|
||||||
|
<link rel="icon" type="image/png" href="{{ asset('favicon-16x16.png') }}" sizes="16x16">
|
||||||
<link rel="icon" type="image/png" href="{{ asset('favicon-32x32.png') }}" sizes="32x32">
|
<link rel="icon" type="image/png" href="{{ asset('favicon-32x32.png') }}" sizes="32x32">
|
||||||
<link rel="apple-touch-icon" href="{{ asset('apple-touch-icon.png') }}">
|
<link rel="apple-touch-icon" href="{{ asset('apple-touch-icon.png') }}">
|
||||||
|
<link rel="apple-touch-icon-precomposed" href="{{ asset('apple-touch-icon-precomposed.png') }}">
|
||||||
|
<link rel="apple-touch-icon" sizes="152x152" href="{{ asset('apple-touch-icon.png') }}">
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="{{ asset('touch-icon-180x180.png') }}">
|
||||||
<link rel="manifest" href="{{ asset('manifest.json') }}">
|
<link rel="manifest" href="{{ asset('manifest.json') }}">
|
||||||
<meta name="apple-mobile-web-app-title" content="{{ constant('App\\Constants::SOFTWARE') }}">
|
<meta name="apple-mobile-web-app-title" content="{{ constant('App\\Constants::SOFTWARE') }}">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<meta name="application-name" content="{{ constant('App\\Constants::SOFTWARE') }}">
|
<meta name="application-name" content="{{ constant('App\\Constants::SOFTWARE') }}">
|
||||||
<meta name="msapplication-config" content="{{ asset('browserconfig.xml') }}">
|
<meta name="msapplication-config" content="{{ asset('browserconfig.xml') }}">
|
||||||
|
<meta name="theme-color" content="#1d273b">
|
||||||
|
|||||||
@@ -120,7 +120,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
KimaiReloadPageWidget.create('kimai.userRoleUpdate');
|
KimaiReloadPageWidget.create('kimai.userRoleUpdate', true);
|
||||||
});
|
});
|
||||||
|
|
||||||
function toggleLabel(element, showTrue) {
|
function toggleLabel(element, showTrue) {
|
||||||
|
|||||||
@@ -308,12 +308,6 @@ class ActivityControllerTest extends ControllerBaseTest
|
|||||||
self::assertStringContainsString('Only visible to the following teams and all admins.', $node->text());
|
self::assertStringContainsString('Only visible to the following teams and all admins.', $node->text());
|
||||||
$node = $client->getCrawler()->filter('div.card#team_listing_box .card-body table tbody tr');
|
$node = $client->getCrawler()->filter('div.card#team_listing_box .card-body table tbody tr');
|
||||||
self::assertEquals(1, $node->count());
|
self::assertEquals(1, $node->count());
|
||||||
|
|
||||||
// creating the default team a second time fails, as the name already exists
|
|
||||||
$this->request($client, '/admin/activity/1/create_team');
|
|
||||||
$this->assertIsRedirect($client, $this->createUrl('/admin/activity/1/details'));
|
|
||||||
$client->followRedirect();
|
|
||||||
$this->assertHasFlashError($client, 'Changes could not be saved: Team already existing');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDeleteAction()
|
public function testDeleteAction()
|
||||||
|
|||||||
@@ -283,12 +283,6 @@ class CustomerControllerTest extends ControllerBaseTest
|
|||||||
self::assertStringContainsString('Only visible to the following teams and all admins.', $node->text(null, true));
|
self::assertStringContainsString('Only visible to the following teams and all admins.', $node->text(null, true));
|
||||||
$node = $client->getCrawler()->filter('div.card#team_listing_box .card-body table tbody tr');
|
$node = $client->getCrawler()->filter('div.card#team_listing_box .card-body table tbody tr');
|
||||||
self::assertEquals(1, $node->count());
|
self::assertEquals(1, $node->count());
|
||||||
|
|
||||||
// creating the default team a second time fails, as the name already exists
|
|
||||||
$this->request($client, '/admin/customer/1/create_team');
|
|
||||||
$this->assertIsRedirect($client, $this->createUrl('/admin/customer/1/details'));
|
|
||||||
$client->followRedirect();
|
|
||||||
$this->assertHasFlashError($client, 'Changes could not be saved: Team already existing');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testProjectsAction()
|
public function testProjectsAction()
|
||||||
|
|||||||
@@ -356,12 +356,6 @@ class ProjectControllerTest extends ControllerBaseTest
|
|||||||
self::assertStringContainsString('Only visible to the following teams and all admins.', $node->text(null, true));
|
self::assertStringContainsString('Only visible to the following teams and all admins.', $node->text(null, true));
|
||||||
$node = $client->getCrawler()->filter('div.card#team_listing_box .card-body table tbody tr');
|
$node = $client->getCrawler()->filter('div.card#team_listing_box .card-body table tbody tr');
|
||||||
self::assertEquals(1, $node->count());
|
self::assertEquals(1, $node->count());
|
||||||
|
|
||||||
// creating the default team a second time fails, as the name already exists
|
|
||||||
$this->request($client, '/admin/project/1/create_team');
|
|
||||||
$this->assertIsRedirect($client, $this->createUrl('/admin/project/1/details'));
|
|
||||||
$client->followRedirect();
|
|
||||||
$this->assertHasFlashError($client, 'Changes could not be saved: Team already existing');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testActivitiesAction()
|
public function testActivitiesAction()
|
||||||
|
|||||||
@@ -309,6 +309,7 @@ class ConfigurationTest extends TestCase
|
|||||||
1 => 'templates/invoice/renderer/',
|
1 => 'templates/invoice/renderer/',
|
||||||
],
|
],
|
||||||
'number_format' => '{Y}/{cy,3}',
|
'number_format' => '{Y}/{cy,3}',
|
||||||
|
'upload_twig' => true,
|
||||||
],
|
],
|
||||||
'export' => [
|
'export' => [
|
||||||
'documents' => [
|
'documents' => [
|
||||||
@@ -430,6 +431,9 @@ class ConfigurationTest extends TestCase
|
|||||||
],
|
],
|
||||||
'customer' => [
|
'customer' => [
|
||||||
'number_format' => '{cc,4}',
|
'number_format' => '{cc,4}',
|
||||||
|
'rules' => [
|
||||||
|
'allow_duplicate_number' => false,
|
||||||
|
],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,10 @@
|
|||||||
|
|
||||||
namespace App\Tests\EventSubscriber;
|
namespace App\Tests\EventSubscriber;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
use App\EventSubscriber\AjaxAuthenticationSubscriber;
|
use App\EventSubscriber\AjaxAuthenticationSubscriber;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
|
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
|
||||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||||
@@ -24,26 +26,46 @@ use Symfony\Component\Security\Core\Exception\AuthenticationExpiredException;
|
|||||||
*/
|
*/
|
||||||
class AjaxAuthenticationSubscriberTest extends TestCase
|
class AjaxAuthenticationSubscriberTest extends TestCase
|
||||||
{
|
{
|
||||||
public function testGetSubscribedEvents()
|
public function testGetSubscribedEvents(): void
|
||||||
{
|
{
|
||||||
$events = AjaxAuthenticationSubscriber::getSubscribedEvents();
|
$events = AjaxAuthenticationSubscriber::getSubscribedEvents();
|
||||||
$this->assertArrayHasKey(KernelEvents::EXCEPTION, $events);
|
$this->assertArrayHasKey(KernelEvents::EXCEPTION, $events);
|
||||||
|
/** @var string $methodName */
|
||||||
$methodName = $events[KernelEvents::EXCEPTION][0];
|
$methodName = $events[KernelEvents::EXCEPTION][0];
|
||||||
$this->assertTrue(method_exists(AjaxAuthenticationSubscriber::class, $methodName));
|
$this->assertTrue(method_exists(AjaxAuthenticationSubscriber::class, $methodName));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getTestHeader()
|
/**
|
||||||
|
* @return array<array<string>>
|
||||||
|
*/
|
||||||
|
public function getTestHeader(): array
|
||||||
{
|
{
|
||||||
yield ['XMLHttpRequest'];
|
return [
|
||||||
yield ['Kimai'];
|
['XMLHttpRequest'],
|
||||||
|
['Kimai']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getSut(bool $loggedIn = false): AjaxAuthenticationSubscriber
|
||||||
|
{
|
||||||
|
$security = $this->createMock(Security::class);
|
||||||
|
if ($loggedIn) {
|
||||||
|
$user = new User();
|
||||||
|
$security->method('getUser')->willReturn($user);
|
||||||
|
$security->method('isGranted')->willReturn(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sut = new AjaxAuthenticationSubscriber($security);
|
||||||
|
|
||||||
|
return $sut;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dataProvider getTestHeader
|
* @dataProvider getTestHeader
|
||||||
*/
|
*/
|
||||||
public function testAuthenticationExpiredException(string $requestedWith)
|
public function testAuthenticationExpiredException(string $requestedWith): void
|
||||||
{
|
{
|
||||||
$sut = new AjaxAuthenticationSubscriber();
|
$sut = $this->getSut();
|
||||||
|
|
||||||
$exception = new AuthenticationExpiredException();
|
$exception = new AuthenticationExpiredException();
|
||||||
$kernel = $this->createMock(HttpKernelInterface::class);
|
$kernel = $this->createMock(HttpKernelInterface::class);
|
||||||
@@ -65,9 +87,9 @@ class AjaxAuthenticationSubscriberTest extends TestCase
|
|||||||
/**
|
/**
|
||||||
* @dataProvider getTestHeader
|
* @dataProvider getTestHeader
|
||||||
*/
|
*/
|
||||||
public function testAuthenticationException(string $requestedWith)
|
public function testAuthenticationException(string $requestedWith): void
|
||||||
{
|
{
|
||||||
$sut = new AjaxAuthenticationSubscriber();
|
$sut = $this->getSut();
|
||||||
|
|
||||||
$exception = new AuthenticationException();
|
$exception = new AuthenticationException();
|
||||||
$kernel = $this->createMock(HttpKernelInterface::class);
|
$kernel = $this->createMock(HttpKernelInterface::class);
|
||||||
@@ -89,9 +111,9 @@ class AjaxAuthenticationSubscriberTest extends TestCase
|
|||||||
/**
|
/**
|
||||||
* @dataProvider getTestHeader
|
* @dataProvider getTestHeader
|
||||||
*/
|
*/
|
||||||
public function testAccessDeniedException(string $requestedWith)
|
public function testAccessDeniedException(string $requestedWith): void
|
||||||
{
|
{
|
||||||
$sut = new AjaxAuthenticationSubscriber();
|
$sut = $this->getSut();
|
||||||
|
|
||||||
$exception = new AccessDeniedException();
|
$exception = new AccessDeniedException();
|
||||||
$kernel = $this->createMock(HttpKernelInterface::class);
|
$kernel = $this->createMock(HttpKernelInterface::class);
|
||||||
@@ -109,4 +131,23 @@ class AjaxAuthenticationSubscriberTest extends TestCase
|
|||||||
self::assertTrue($response->headers->has('Login-Required'));
|
self::assertTrue($response->headers->has('Login-Required'));
|
||||||
self::assertEquals('1', $response->headers->get('Login-Required'));
|
self::assertEquals('1', $response->headers->get('Login-Required'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider getTestHeader
|
||||||
|
*/
|
||||||
|
public function testAccessDeniedExceptionWithLoggedInUser(string $requestedWith): void
|
||||||
|
{
|
||||||
|
$sut = $this->getSut(true);
|
||||||
|
|
||||||
|
$exception = new AccessDeniedException();
|
||||||
|
$kernel = $this->createMock(HttpKernelInterface::class);
|
||||||
|
$request = new Request();
|
||||||
|
$request->initialize([], [], [], [], [], ['HTTP_X-Requested-With' => $requestedWith]);
|
||||||
|
|
||||||
|
$event = new ExceptionEvent($kernel, $request, 1, $exception);
|
||||||
|
|
||||||
|
$sut->onCoreException($event);
|
||||||
|
|
||||||
|
self::assertNull($event->getResponse());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6037,36 +6037,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: EventSubscriber/Actions/UsersSubscriberTest.php
|
path: EventSubscriber/Actions/UsersSubscriberTest.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Tests\\\\EventSubscriber\\\\AjaxAuthenticationSubscriberTest\\:\\:getTestHeader\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: EventSubscriber/AjaxAuthenticationSubscriberTest.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Tests\\\\EventSubscriber\\\\AjaxAuthenticationSubscriberTest\\:\\:testAccessDeniedException\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: EventSubscriber/AjaxAuthenticationSubscriberTest.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Tests\\\\EventSubscriber\\\\AjaxAuthenticationSubscriberTest\\:\\:testAuthenticationException\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: EventSubscriber/AjaxAuthenticationSubscriberTest.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Tests\\\\EventSubscriber\\\\AjaxAuthenticationSubscriberTest\\:\\:testAuthenticationExpiredException\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: EventSubscriber/AjaxAuthenticationSubscriberTest.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Tests\\\\EventSubscriber\\\\AjaxAuthenticationSubscriberTest\\:\\:testGetSubscribedEvents\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: EventSubscriber/AjaxAuthenticationSubscriberTest.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#2 \\$method of function method_exists expects string, array\\<int\\|string, int\\|string\\>\\|int\\|string given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: EventSubscriber/AjaxAuthenticationSubscriberTest.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Method App\\\\Tests\\\\EventSubscriber\\\\EmailSubscriberTest\\:\\:testGetSubscribedEvents\\(\\) has no return type specified\\.$#"
|
message: "#^Method App\\\\Tests\\\\EventSubscriber\\\\EmailSubscriberTest\\:\\:testGetSubscribedEvents\\(\\) has no return type specified\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||||
<source>help.upload</source>
|
<source>help.upload</source>
|
||||||
<target state="translated">انتباه: سيتم الكتابة فوق الملفات الموجودة. أنواع الملفات المسموح بها هي: DOCX و ODS و XLSX.</target>
|
<target state="translated">انتباه: سيتم الكتابة فوق الملفات الموجودة. أنواع الملفات المسموح بها هي: %extensions%.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||||
<source>programmatic</source>
|
<source>programmatic</source>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||||
<source>help.upload</source>
|
<source>help.upload</source>
|
||||||
<target state="translated">Pozor: existující soubory budou přepsány. Povolené typy jsou: DOCX, ODS, XLSX.</target>
|
<target state="translated">Pozor: existující soubory budou přepsány. Povolené typy jsou: %extensions%.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||||
<source>programmatic</source>
|
<source>programmatic</source>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||||
<source>help.upload</source>
|
<source>help.upload</source>
|
||||||
<target state="translated">Achtung: existierende Dateien werden überschrieben. Erlaubte Dateitypen sind: DOCX, ODS, XLSX.</target>
|
<target state="translated">Achtung: existierende Dateien werden überschrieben. Erlaubte Dateitypen sind: %extensions%.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="22pt8Gs" resname="download_invoice_renderer">
|
<trans-unit id="22pt8Gs" resname="download_invoice_renderer">
|
||||||
<source>download_invoice_renderer</source>
|
<source>download_invoice_renderer</source>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||||
<source>help.upload</source>
|
<source>help.upload</source>
|
||||||
<target state="needs-translation">Achtung: existierende Dateien werden überschrieben. Erlaubte Dateitypen sind: DOCX, ODS, XLSX</target>
|
<target state="needs-translation">Achtung: existierende Dateien werden überschrieben. Erlaubte Dateitypen sind: %extensions%</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<!-- Optgroups -->
|
<!-- Optgroups -->
|
||||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||||
<source>help.upload</source>
|
<source>help.upload</source>
|
||||||
<target state="translated">Προσοχή: τα υπάρχοντα αρχεία θα αντικατασταθούν. Επιτρεπόμενοι τύποι αρχείων είναι: DOCX, ODS, XLSX.</target>
|
<target state="translated">Προσοχή: τα υπάρχοντα αρχεία θα αντικατασταθούν. Επιτρεπόμενοι τύποι αρχείων είναι: %extensions%.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<!-- Optgroups -->
|
<!-- Optgroups -->
|
||||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||||
<source>help.upload</source>
|
<source>help.upload</source>
|
||||||
<target>Attention: existing files will be overwritten. Allowed file types are: DOCX, ODS, XLSX.</target>
|
<target>Attention: existing files will be overwritten. Allowed file types are: %extensions%.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="22pt8Gs" resname="download_invoice_renderer">
|
<trans-unit id="22pt8Gs" resname="download_invoice_renderer">
|
||||||
<source>download_invoice_renderer</source>
|
<source>download_invoice_renderer</source>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||||
<source>help.upload</source>
|
<source>help.upload</source>
|
||||||
<target>Atentu: ekzistantaj dosieroj estos anstataŭigitaj. Permesitaj tipoj de dosieroj estas: DOCX, ODS, XLSX.</target>
|
<target>Atentu: ekzistantaj dosieroj estos anstataŭigitaj. Permesitaj tipoj de dosieroj estas: %extensions%.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||||
<source>help.upload</source>
|
<source>help.upload</source>
|
||||||
<target state="translated">Atención: Los archivos existentes serán sobrescritos. Los tipos de archivo permitidos son: DOCX, ODS, XLSX.</target>
|
<target state="translated">Atención: Los archivos existentes serán sobrescritos. Los tipos de archivo permitidos son: %extensions%.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||||
<source>programmatic</source>
|
<source>programmatic</source>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||||
<source>help.upload</source>
|
<source>help.upload</source>
|
||||||
<target state="translated">توجه: فایل های موجود رونویسی خواهند شد. فایل های مجاز عبارتند از: DOCX، ODS، XLSX.</target>
|
<target state="translated">توجه: فایل های موجود رونویسی خواهند شد. فایل های مجاز عبارتند از: %extensions%.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<!-- Optgroups -->
|
<!-- Optgroups -->
|
||||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||||
<source>help.upload</source>
|
<source>help.upload</source>
|
||||||
<target state="translated">Huomioitavaa: olemassa olevat tiedostot korvataan. Sallitut tiedosto tyypit on: DOCX, ODS, XLSX.</target>
|
<target state="translated">Huomioitavaa: olemassa olevat tiedostot korvataan. Sallitut tiedosto tyypit on: %extensions%.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<!-- Optgroups -->
|
<!-- Optgroups -->
|
||||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||||
<source>help.upload</source>
|
<source>help.upload</source>
|
||||||
<target state="needs-translation">Attention: existing files will be overwritten. Allowed file types are: DOCX, ODS, XLSX.</target>
|
<target state="needs-translation">Attention: existing files will be overwritten. Allowed file types are: %extensions%.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<!-- Optgroups -->
|
<!-- Optgroups -->
|
||||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||||
<source>help.upload</source>
|
<source>help.upload</source>
|
||||||
<target>Attention : les fichiers existants seront écrasés. Les types de fichiers autorisés sont : DOCX, ODS, XLSX.</target>
|
<target>Attention : les fichiers existants seront écrasés. Les types de fichiers autorisés sont : %extensions%.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||||
<source>programmatic</source>
|
<source>programmatic</source>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||||
<source>help.upload</source>
|
<source>help.upload</source>
|
||||||
<target>שים לב: קבצים קיימים ידרסו וימחקו. סוגי קבצים מותרים: DOCX, ODS, XLSX.</target>
|
<target>שים לב: קבצים קיימים ידרסו וימחקו. סוגי קבצים מותרים: %extensions%.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||||
<source>programmatic</source>
|
<source>programmatic</source>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||||
<source>help.upload</source>
|
<source>help.upload</source>
|
||||||
<target>Oprez: postojeće datoteke će se prepisati. Dopuštene vrste datoteka su: DOCX, ODS, XLSX.</target>
|
<target>Oprez: postojeće datoteke će se prepisati. Dopuštene vrste datoteka su: %extensions%.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<!-- Optgroups -->
|
<!-- Optgroups -->
|
||||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||||
<source>help.upload</source>
|
<source>help.upload</source>
|
||||||
<target>Figyelem: a meglévő fájlok felülíródnak. A megengedett fájltípusok: DOCX, ODS, XLSX.</target>
|
<target>Figyelem: a meglévő fájlok felülíródnak. A megengedett fájltípusok: %extensions%.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||||
<source>programmatic</source>
|
<source>programmatic</source>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||||
<source>help.upload</source>
|
<source>help.upload</source>
|
||||||
<target>주의: 기존 파일을 덮어씁니다. 허용되는 파일 형식은 DOCX, ODS, XLSX입니다.</target>
|
<target>주의: 기존 파일을 덮어씁니다. 허용되는 파일 형식은 %extensions%입니다.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="nA.KOyM" resname="Html">
|
<trans-unit id="nA.KOyM" resname="Html">
|
||||||
<source>Html</source>
|
<source>Html</source>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||||
<source>help.upload</source>
|
<source>help.upload</source>
|
||||||
<target>Obs: Eksisterende filer vil bli overskrevet. Filtypene DOCX, ODS og XLSX støttes.</target>
|
<target>Obs: Eksisterende filer vil bli overskrevet. Filtypene %extensions% støttes.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<!-- Optgroups -->
|
<!-- Optgroups -->
|
||||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||||
<source>help.upload</source>
|
<source>help.upload</source>
|
||||||
<target>Attentie: Bestaande bestanden zullen overschreven worden. Toegestaande bestandstypes zijn: DOCX, ODS, XLSX.</target>
|
<target>Attentie: Bestaande bestanden zullen overschreven worden. Toegestaande bestandstypes zijn: %extensions%.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<!-- Optgroups -->
|
<!-- Optgroups -->
|
||||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||||
<source>help.upload</source>
|
<source>help.upload</source>
|
||||||
<target state="translated">Uwaga: istniejące pliki zostaną nadpisane. Dozwolone typu plików to: DOCX, ODS, XLSX.</target>
|
<target state="translated">Uwaga: istniejące pliki zostaną nadpisane. Dozwolone typu plików to: %extensions%.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||||
<source>programmatic</source>
|
<source>programmatic</source>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||||
<source>help.upload</source>
|
<source>help.upload</source>
|
||||||
<target state="translated">Atenção: os ficheiros já existentes serão substituídos. Os tipos de ficheiros permitidos são: DOCX, ODS, XLSX.</target>
|
<target state="translated">Atenção: os ficheiros já existentes serão substituídos. Os tipos de ficheiros permitidos são: %extensions%.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<!-- Optgroups -->
|
<!-- Optgroups -->
|
||||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||||
<source>help.upload</source>
|
<source>help.upload</source>
|
||||||
<target>Atenção: os arquivos já existentes serão substituídos. Os tipos dos arquivos permitidos são: DOCX, ODS, XLSX.</target>
|
<target>Atenção: os arquivos já existentes serão substituídos. Os tipos dos arquivos permitidos são: %extensions%.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="nA.KOyM" resname="Html">
|
<trans-unit id="nA.KOyM" resname="Html">
|
||||||
<source>Html</source>
|
<source>Html</source>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||||
<source>help.upload</source>
|
<source>help.upload</source>
|
||||||
<target>Atenție: fișierele existente vor fi suprascrise. Tipuri de fișiere permise: DOCX, ODS, XLSX.</target>
|
<target>Atenție: fișierele existente vor fi suprascrise. Tipuri de fișiere permise: %extensions%.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||||
<source>programmatic</source>
|
<source>programmatic</source>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||||
<source>help.upload</source>
|
<source>help.upload</source>
|
||||||
<target state="translated">Внимание: существующие файлы будут перезаписаны. Допустимые типы файлов: DOCX, ODS, XLSX.</target>
|
<target state="translated">Внимание: существующие файлы будут перезаписаны. Допустимые типы файлов: %extensions%.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||||
<source>programmatic</source>
|
<source>programmatic</source>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||||
<source>help.upload</source>
|
<source>help.upload</source>
|
||||||
<target state="translated">Obs: redan exsiterande filer kommer att skrivas över. Tillåtna fil typer är: DOCX, ODS, XLSX.</target>
|
<target state="translated">Obs: redan exsiterande filer kommer att skrivas över. Tillåtna fil typer är: %extensions%.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Utbj3k_" resname="invoice">
|
<trans-unit id="Utbj3k_" resname="invoice">
|
||||||
<source>invoice</source>
|
<source>invoice</source>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||||
<source>help.upload</source>
|
<source>help.upload</source>
|
||||||
<target>Dikkat: mevcut dosyaların üzerine yazılacaktır. İzin verilen dosya türleri: DOCX, ODS, XLSX.</target>
|
<target>Dikkat: mevcut dosyaların üzerine yazılacaktır. İzin verilen dosya türleri: %extensions%.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="nA.KOyM" resname="Html">
|
<trans-unit id="nA.KOyM" resname="Html">
|
||||||
<source>Html</source>
|
<source>Html</source>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||||
<source>help.upload</source>
|
<source>help.upload</source>
|
||||||
<target state="translated">Увага: існуючі файли будуть перезаписані. Дозволені типи файлів: DOCX, ODS, XLSX.</target>
|
<target state="translated">Увага: існуючі файли будуть перезаписані. Дозволені типи файлів: %extensions%.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<!-- Optgroups -->
|
<!-- Optgroups -->
|
||||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||||
<source>help.upload</source>
|
<source>help.upload</source>
|
||||||
<target>Chú ý: các tệp hiện có sẽ bị ghi đè. Các loại tệp được phép là: DOCX, ODS, XLSX.</target>
|
<target>Chú ý: các tệp hiện có sẽ bị ghi đè. Các loại tệp được phép là: %extensions%.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||||
<source>programmatic</source>
|
<source>programmatic</source>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||||
<source>help.upload</source>
|
<source>help.upload</source>
|
||||||
<target state="translated">注意:现有文件将被覆盖。允许的文件类型是:DOCX, ODS, XLSX.</target>
|
<target state="translated">注意:现有文件将被覆盖。允许的文件类型是:%extensions%.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||||
<source>programmatic</source>
|
<source>programmatic</source>
|
||||||
|
|||||||
@@ -210,6 +210,10 @@
|
|||||||
<source>customer.number_format</source>
|
<source>customer.number_format</source>
|
||||||
<target>Kundennummer Format</target>
|
<target>Kundennummer Format</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="1cAQPNS" resname="customer.allow_duplicate_number">
|
||||||
|
<source>customer.allow_duplicate_number</source>
|
||||||
|
<target>Erlaube Kundennummer mehrfach zu verwenden</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="IZY5kqL" resname="allowed_replacer">
|
<trans-unit id="IZY5kqL" resname="allowed_replacer">
|
||||||
<source>allowed_replacer</source>
|
<source>allowed_replacer</source>
|
||||||
<target>Erlaubte Ersetzer: %replacer%</target>
|
<target>Erlaubte Ersetzer: %replacer%</target>
|
||||||
|
|||||||
@@ -210,6 +210,10 @@
|
|||||||
<source>customer.number_format</source>
|
<source>customer.number_format</source>
|
||||||
<target>Customer number Format</target>
|
<target>Customer number Format</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="1cAQPNS" resname="customer.allow_duplicate_number">
|
||||||
|
<source>customer.allow_duplicate_number</source>
|
||||||
|
<target>Allow duplicate account number</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="IZY5kqL" resname="allowed_replacer">
|
<trans-unit id="IZY5kqL" resname="allowed_replacer">
|
||||||
<source>allowed_replacer</source>
|
<source>allowed_replacer</source>
|
||||||
<target>Allowed replacer: %replacer%</target>
|
<target>Allowed replacer: %replacer%</target>
|
||||||
|
|||||||
@@ -3007,9 +3007,9 @@ __metadata:
|
|||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001359, caniuse-lite@npm:^1.0.30001400":
|
"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001359, caniuse-lite@npm:^1.0.30001400":
|
||||||
version: 1.0.30001441
|
version: 1.0.30001446
|
||||||
resolution: "caniuse-lite@npm:1.0.30001441"
|
resolution: "caniuse-lite@npm:1.0.30001446"
|
||||||
checksum: 0f5aa8f7ea4d165e88e0d1eaa44564c5bfee66641f265a1fd959e74f0a7e6bc0207db6c28e2fb63dc8b2cd23e0e3cee06c4f372de11c93c57ff5ff4207962c3f
|
checksum: b31a7e1837783afd7f3d4cb742689996c0a09d67394ddaa0609fd2bce00ceea65c448e25f91c03ba0f2d0e345b7e28fd5bc636c6760c949621a654c0effe74b5
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|||||||