diff --git a/.scrutinizer.yml b/.scrutinizer.yml
index 150b79fb..b16e2c35 100644
--- a/.scrutinizer.yml
+++ b/.scrutinizer.yml
@@ -35,6 +35,6 @@ filter:
- 'vendor/'
build_failure_conditions:
- - 'project.metric("scrutinizer.quality", < 9.30)'
+ - 'project.metric("scrutinizer.quality", < 9.0)'
- 'project.metric("scrutinizer.test_coverage", < 0.9)'
- 'project.metric_change("scrutinizer.test_coverage", < -0.01)'
\ No newline at end of file
diff --git a/UPGRADING.md b/UPGRADING.md
index 81277fbd..c22ac8ee 100644
--- a/UPGRADING.md
+++ b/UPGRADING.md
@@ -22,11 +22,12 @@ And make sure to **create a backup before you start**.
Follow the normal update and database migration process (see above).
-### Apply necessary changes to your `local.yaml`:
+### Apply necessary changes to your local.yaml:
-New permissions are available:
+New permissions are available. If you configured custom permissions in `local.yaml`, you have to add those, otherwise you can't use the new features:
- `view_tag` - view all tags
- `delete_tag` - delete tags
+- `edit_exported_timesheet` - allows to edit records which were exported
### BC BREAKS
diff --git a/assets/js/plugins/KimaiAjaxModalForm.js b/assets/js/plugins/KimaiAjaxModalForm.js
index 9141ea16..6dc66cd9 100644
--- a/assets/js/plugins/KimaiAjaxModalForm.js
+++ b/assets/js/plugins/KimaiAjaxModalForm.js
@@ -52,7 +52,10 @@ export default class KimaiAjaxModalForm extends KimaiClickHandlerReducedInTableR
// the modal that we use to render the form in
let formIdentifier = '#remote_form_modal .modal-content form';
+ // if any of these is found in a response, the form will be re-displayed
let flashErrorIdentifier = 'div.alert-error';
+ // messages to show above the form
+ let flashMessageIdentifier = 'div.alert';
let form = jQuery(formIdentifier);
let remoteModal = jQuery('#remote_form_modal');
@@ -88,10 +91,9 @@ export default class KimaiAjaxModalForm extends KimaiClickHandlerReducedInTableR
}
// show error flash messages
- if (jQuery(html).find(flashErrorIdentifier).length > 0) {
- jQuery('#remote_form_modal .modal-body').prepend(
- jQuery(html).find(flashErrorIdentifier)
- );
+ let flashMessages = jQuery(html).find(flashMessageIdentifier);
+ if (flashMessages.length > 0) {
+ jQuery('#remote_form_modal .modal-body').prepend(flashMessages);
}
// -----------------------------------------------------------------------
diff --git a/config/packages/kimai.yaml b/config/packages/kimai.yaml
index 19bc1e1f..521d131a 100644
--- a/config/packages/kimai.yaml
+++ b/config/packages/kimai.yaml
@@ -101,8 +101,8 @@ kimai:
# adding single permissions to user roles, extending the definition from "sets" ("role name" = [array of "permissions"])
ROLE_USER: []
ROLE_TEAMLEAD: [view_invoice_template,create_invoice_template,edit_invoice_template,view_rate_own_timesheet,view_rate_other_timesheet,hourly-rate_own_profile]
- ROLE_ADMIN: [hourly-rate_own_profile]
- ROLE_SUPER_ADMIN: [hourly-rate_own_profile,hourly-rate_other_profile,delete_own_profile,roles_own_profile,system_information,system_actions,system_configuration,plugins]
+ ROLE_ADMIN: [hourly-rate_own_profile,edit_exported_timesheet]
+ ROLE_SUPER_ADMIN: [hourly-rate_own_profile,hourly-rate_other_profile,delete_own_profile,roles_own_profile,system_information,system_actions,system_configuration,plugins,edit_exported_timesheet]
# --------------------------------------------------------------------------------
diff --git a/public/build/app.js b/public/build/app.js
index 6ec4ef2c..ff6040b2 100644
--- a/public/build/app.js
+++ b/public/build/app.js
@@ -81,7 +81,7 @@ if(void 0===t)throw new Error("AdminLTE requires jQuery");+function(t){"use stri
* @license: Licensed under the MIT license. See http://www.opensource.org/licenses/mit-license.php
* @website: http://www.daterangepicker.com/
*/
-!function(r,a){i=[n("PJh5"),n("7t+N")],void 0!==(o=function(t,e){return e.fn||(e.fn={}),"function"!=typeof t&&t.default&&(t=t.default),a(t,e)}.apply(e,i))&&(t.exports=o)}(0,function(t,e){var n=function(n,i,o){if(this.parentEl="body",this.element=e(n),this.startDate=t().startOf("day"),this.endDate=t().endOf("day"),this.minDate=!1,this.maxDate=!1,this.maxSpan=!1,this.autoApply=!1,this.singleDatePicker=!1,this.showDropdowns=!1,this.minYear=t().subtract(100,"year").format("YYYY"),this.maxYear=t().add(100,"year").format("YYYY"),this.showWeekNumbers=!1,this.showISOWeekNumbers=!1,this.showCustomRangeLabel=!0,this.timePicker=!1,this.timePicker24Hour=!1,this.timePickerIncrement=1,this.timePickerSeconds=!1,this.linkedCalendars=!0,this.autoUpdateInput=!0,this.alwaysShowCalendars=!1,this.ranges={},this.opens="right",this.element.hasClass("pull-right")&&(this.opens="left"),this.drops="down",this.element.hasClass("dropup")&&(this.drops="up"),this.buttonClasses="btn btn-sm",this.applyButtonClasses="btn-primary",this.cancelButtonClasses="btn-default",this.locale={direction:"ltr",format:t.localeData().longDateFormat("L"),separator:" - ",applyLabel:"Apply",cancelLabel:"Cancel",weekLabel:"W",customRangeLabel:"Custom Range",daysOfWeek:t.weekdaysMin(),monthNames:t.monthsShort(),firstDay:t.localeData().firstDayOfWeek()},this.callback=function(){},this.isShowing=!1,this.leftCalendar={},this.rightCalendar={},"object"==typeof i&&null!==i||(i={}),i=e.extend(this.element.data(),i),"string"==typeof i.template||i.template instanceof e||(i.template='
",this.container.find(".drp-calendar."+n+" .calendar-table").html(D)},renderTimePicker:function(t){if("right"!=t||this.endDate){var e,n,i,o=this.maxDate;if(!this.maxSpan||this.maxDate&&!this.startDate.clone().add(this.maxSpan).isBefore(this.maxDate)||(o=this.startDate.clone().add(this.maxSpan)),"left"==t)n=this.startDate.clone(),i=this.minDate;else if("right"==t){n=this.endDate.clone(),i=this.startDate;var r=this.container.find(".drp-calendar.right .calendar-time");if(""!=r.html()&&(n.hour(isNaN(n.hour())?r.find(".hourselect option:selected").val():n.hour()),n.minute(isNaN(n.minute())?r.find(".minuteselect option:selected").val():n.minute()),n.second(isNaN(n.second())?r.find(".secondselect option:selected").val():n.second()),!this.timePicker24Hour)){var a=r.find(".ampmselect option:selected").val();"PM"===a&&n.hour()<12&&n.hour(n.hour()+12),"AM"===a&&12===n.hour()&&n.hour(0)}n.isBefore(this.startDate)&&(n=this.startDate.clone()),o&&n.isAfter(o)&&(n=o.clone())}e=' ",e+=': ",this.timePickerSeconds){e+=': "}if(!this.timePicker24Hour){e+='"}this.container.find(".drp-calendar."+t+" .calendar-time").html(e)}},updateFormInputs:function(){this.singleDatePicker||this.endDate&&(this.startDate.isBefore(this.endDate)||this.startDate.isSame(this.endDate))?this.container.find("button.applyBtn").removeAttr("disabled"):this.container.find("button.applyBtn").attr("disabled","disabled")},move:function(){var t,n={top:0,left:0},i=e(window).width();this.parentEl.is("body")||(n={top:this.parentEl.offset().top-this.parentEl.scrollTop(),left:this.parentEl.offset().left-this.parentEl.scrollLeft()},i=this.parentEl[0].clientWidth+this.parentEl.offset().left),t="up"==this.drops?this.element.offset().top-this.container.outerHeight()-n.top:this.element.offset().top+this.element.outerHeight()-n.top,this.container.css({top:0,left:0,right:"auto"});var o=this.container.outerWidth();if(this.container["up"==this.drops?"addClass":"removeClass"]("drop-up"),"left"==this.opens){var r=i-this.element.offset().left-this.element.outerWidth();o+r>e(window).width()?this.container.css({top:t,right:"auto",left:9}):this.container.css({top:t,right:r,left:"auto"})}else if("center"==this.opens){var a=this.element.offset().left-n.left+this.element.outerWidth()/2-o/2;a<0?this.container.css({top:t,right:"auto",left:9}):a+o>e(window).width()?this.container.css({top:t,left:"auto",right:0}):this.container.css({top:t,left:a,right:"auto"})}else{var a=this.element.offset().left-n.left;a+o>e(window).width()?this.container.css({top:t,left:"auto",right:0}):this.container.css({top:t,left:a,right:"auto"})}},show:function(t){this.isShowing||(this._outsideClickProxy=e.proxy(function(t){this.outsideClick(t)},this),e(document).on("mousedown.daterangepicker",this._outsideClickProxy).on("touchend.daterangepicker",this._outsideClickProxy).on("click.daterangepicker","[data-toggle=dropdown]",this._outsideClickProxy).on("focusin.daterangepicker",this._outsideClickProxy),e(window).on("resize.daterangepicker",e.proxy(function(t){this.move(t)},this)),this.oldStartDate=this.startDate.clone(),this.oldEndDate=this.endDate.clone(),this.previousRightTime=this.endDate.clone(),this.updateView(),this.container.show(),this.move(),this.element.trigger("show.daterangepicker",this),this.isShowing=!0)},hide:function(t){this.isShowing&&(this.endDate||(this.startDate=this.oldStartDate.clone(),this.endDate=this.oldEndDate.clone()),this.startDate.isSame(this.oldStartDate)&&this.endDate.isSame(this.oldEndDate)||this.callback(this.startDate.clone(),this.endDate.clone(),this.chosenLabel),this.updateElement(),e(document).off(".daterangepicker"),e(window).off(".daterangepicker"),this.container.hide(),this.element.trigger("hide.daterangepicker",this),this.isShowing=!1)},toggle:function(t){this.isShowing?this.hide():this.show()},outsideClick:function(t){var n=e(t.target);"focusin"==t.type||n.closest(this.element).length||n.closest(this.container).length||n.closest(".calendar-table").length||(this.hide(),this.element.trigger("outsideClick.daterangepicker",this))},showCalendars:function(){this.container.addClass("show-calendar"),this.move(),this.element.trigger("showCalendar.daterangepicker",this)},hideCalendars:function(){this.container.removeClass("show-calendar"),this.element.trigger("hideCalendar.daterangepicker",this)},clickRange:function(t){var e=t.target.getAttribute("data-range-key");if(this.chosenLabel=e,e==this.locale.customRangeLabel)this.showCalendars();else{var n=this.ranges[e];this.startDate=n[0],this.endDate=n[1],this.timePicker||(this.startDate.startOf("day"),this.endDate.endOf("day")),this.alwaysShowCalendars||this.hideCalendars(),this.clickApply()}},clickPrev:function(t){e(t.target).parents(".drp-calendar").hasClass("left")?(this.leftCalendar.month.subtract(1,"month"),this.linkedCalendars&&this.rightCalendar.month.subtract(1,"month")):this.rightCalendar.month.subtract(1,"month"),this.updateCalendars()},clickNext:function(t){e(t.target).parents(".drp-calendar").hasClass("left")?this.leftCalendar.month.add(1,"month"):(this.rightCalendar.month.add(1,"month"),this.linkedCalendars&&this.leftCalendar.month.add(1,"month")),this.updateCalendars()},hoverDate:function(t){if(e(t.target).hasClass("available")){var n=e(t.target).attr("data-title"),i=n.substr(1,1),o=n.substr(3,1),r=e(t.target).parents(".drp-calendar"),a=r.hasClass("left")?this.leftCalendar.calendar[i][o]:this.rightCalendar.calendar[i][o],s=this.leftCalendar,l=this.rightCalendar,u=this.startDate;this.endDate||this.container.find(".drp-calendar tbody td").each(function(t,n){if(!e(n).hasClass("week")){var i=e(n).attr("data-title"),o=i.substr(1,1),r=i.substr(3,1),c=e(n).parents(".drp-calendar"),d=c.hasClass("left")?s.calendar[o][r]:l.calendar[o][r];d.isAfter(u)&&d.isBefore(a)||d.isSame(a,"day")?e(n).addClass("in-range"):e(n).removeClass("in-range")}})}},clickDate:function(t){if(e(t.target).hasClass("available")){var n=e(t.target).attr("data-title"),i=n.substr(1,1),o=n.substr(3,1),r=e(t.target).parents(".drp-calendar"),a=r.hasClass("left")?this.leftCalendar.calendar[i][o]:this.rightCalendar.calendar[i][o];if(this.endDate||a.isBefore(this.startDate,"day")){if(this.timePicker){var s=parseInt(this.container.find(".left .hourselect").val(),10);if(!this.timePicker24Hour){var l=this.container.find(".left .ampmselect").val();"PM"===l&&s<12&&(s+=12),"AM"===l&&12===s&&(s=0)}var u=parseInt(this.container.find(".left .minuteselect").val(),10);isNaN(u)&&(u=parseInt(this.container.find(".left .minuteselect option:last").val(),10));var c=this.timePickerSeconds?parseInt(this.container.find(".left .secondselect").val(),10):0;a=a.clone().hour(s).minute(u).second(c)}this.endDate=null,this.setStartDate(a.clone())}else if(!this.endDate&&a.isBefore(this.startDate))this.setEndDate(this.startDate.clone());else{if(this.timePicker){var s=parseInt(this.container.find(".right .hourselect").val(),10);if(!this.timePicker24Hour){var l=this.container.find(".right .ampmselect").val();"PM"===l&&s<12&&(s+=12),"AM"===l&&12===s&&(s=0)}var u=parseInt(this.container.find(".right .minuteselect").val(),10);isNaN(u)&&(u=parseInt(this.container.find(".right .minuteselect option:last").val(),10));var c=this.timePickerSeconds?parseInt(this.container.find(".right .secondselect").val(),10):0;a=a.clone().hour(s).minute(u).second(c)}this.setEndDate(a.clone()),this.autoApply&&(this.calculateChosenLabel(),this.clickApply())}this.singleDatePicker&&(this.setEndDate(this.startDate),this.timePicker||this.clickApply()),this.updateView(),t.stopPropagation()}},calculateChosenLabel:function(){var t=!0,e=0;for(var n in this.ranges){if(this.timePicker){var i=this.timePickerSeconds?"YYYY-MM-DD HH:mm:ss":"YYYY-MM-DD HH:mm";if(this.startDate.format(i)==this.ranges[n][0].format(i)&&this.endDate.format(i)==this.ranges[n][1].format(i)){t=!1,this.chosenLabel=this.container.find(".ranges li:eq("+e+")").addClass("active").attr("data-range-key");break}}else if(this.startDate.format("YYYY-MM-DD")==this.ranges[n][0].format("YYYY-MM-DD")&&this.endDate.format("YYYY-MM-DD")==this.ranges[n][1].format("YYYY-MM-DD")){t=!1,this.chosenLabel=this.container.find(".ranges li:eq("+e+")").addClass("active").attr("data-range-key");break}e++}t&&(this.showCustomRangeLabel?this.chosenLabel=this.container.find(".ranges li:last").addClass("active").attr("data-range-key"):this.chosenLabel=null,this.showCalendars())},clickApply:function(t){this.hide(),this.element.trigger("apply.daterangepicker",this)},clickCancel:function(t){this.startDate=this.oldStartDate,this.endDate=this.oldEndDate,this.hide(),this.element.trigger("cancel.daterangepicker",this)},monthOrYearChanged:function(t){var n=e(t.target).closest(".drp-calendar").hasClass("left"),i=n?"left":"right",o=this.container.find(".drp-calendar."+i),r=parseInt(o.find(".monthselect").val(),10),a=o.find(".yearselect").val();n||(athis.maxDate.year()||a==this.maxDate.year()&&r>this.maxDate.month())&&(r=this.maxDate.month(),a=this.maxDate.year()),n?(this.leftCalendar.month.month(r).year(a),this.linkedCalendars&&(this.rightCalendar.month=this.leftCalendar.month.clone().add(1,"month"))):(this.rightCalendar.month.month(r).year(a),this.linkedCalendars&&(this.leftCalendar.month=this.rightCalendar.month.clone().subtract(1,"month"))),this.updateCalendars()},timeChanged:function(t){var n=e(t.target).closest(".drp-calendar"),i=n.hasClass("left"),o=parseInt(n.find(".hourselect").val(),10),r=parseInt(n.find(".minuteselect").val(),10);isNaN(r)&&(r=parseInt(n.find(".minuteselect option:last").val(),10));var a=this.timePickerSeconds?parseInt(n.find(".secondselect").val(),10):0;if(!this.timePicker24Hour){var s=n.find(".ampmselect").val();"PM"===s&&o<12&&(o+=12),"AM"===s&&12===o&&(o=0)}if(i){var l=this.startDate.clone();l.hour(o),l.minute(r),l.second(a),this.setStartDate(l),this.singleDatePicker?this.endDate=this.startDate.clone():this.endDate&&this.endDate.format("YYYY-MM-DD")==l.format("YYYY-MM-DD")&&this.endDate.isBefore(l)&&this.setEndDate(l.clone())}else if(this.endDate){var u=this.endDate.clone();u.hour(o),u.minute(r),u.second(a),this.setEndDate(u)}this.updateCalendars(),this.updateFormInputs(),this.renderTimePicker("left"),this.renderTimePicker("right")},elementChanged:function(){if(this.element.is("input")&&this.element.val().length){var e=this.element.val().split(this.locale.separator),n=null,i=null;2===e.length&&(n=t(e[0],this.locale.format),i=t(e[1],this.locale.format)),(this.singleDatePicker||null===n||null===i)&&(n=t(this.element.val(),this.locale.format),i=n),n.isValid()&&i.isValid()&&(this.setStartDate(n),this.setEndDate(i),this.updateView())}},keydown:function(t){9!==t.keyCode&&13!==t.keyCode||this.hide(),27===t.keyCode&&(t.preventDefault(),t.stopPropagation(),this.hide())},updateElement:function(){if(this.element.is("input")&&this.autoUpdateInput){var t=this.startDate.format(this.locale.format);this.singleDatePicker||(t+=this.locale.separator+this.endDate.format(this.locale.format)),t!==this.element.val()&&this.element.val(t).trigger("change")}},remove:function(){this.container.remove(),this.element.off(".daterangepicker"),this.element.removeData()}},e.fn.daterangepicker=function(t,i){var o=e.extend(!0,{},e.fn.daterangepicker.defaultOptions,t);return this.each(function(){var t=e(this);t.data("daterangepicker")&&t.data("daterangepicker").remove(),t.data("daterangepicker",new n(t,o,i))}),this},n})},R6CI:function(t,e){},RvrG:function(t,e,n){"use strict";function i(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function o(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}function r(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}var a=n("7t+N"),s=n.n(a),l=n("ITav"),u=function(){function t(t,e){for(var n=0;n0&&(o.on("hidden.bs.modal",function(){o.hasClass("modal-danger")&&o.removeClass("modal-danger")}),s()(t).find("#form_modal").hasClass("modal-danger")&&o.addClass("modal-danger"),s()("#remote_form_modal .modal-content").replaceWith(s()(t).find("#form_modal .modal-content")),e.getContainer().getPlugin("date-time-picker").activateDateTimePicker(n),e.getContainer().getPlugin("autocomplete").activateAutocomplete(n+" .js-autocomplete"),s()(".selectpicker").selectpicker("refresh"));var r=s()(t).find("div.alert");r.length>0&&s()("#remote_form_modal .modal-body").prepend(r);var a=s.a.fn.modal.Constructor.prototype.enforceFocus;s.a.fn.modal.Constructor.prototype.enforceFocus=function(){},o.on("hidden.bs.modal",function(){s.a.fn.modal.Constructor.prototype.enforceFocus=a}),o.on("shown.bs.modal",function(){s()(this).find("input[type=text],textarea,select").filter(':not("[data-datetimepicker=on]")').filter(":visible:first").focus().delay(1e3).focus()}),o.modal("show"),i=s()(n),i.on("submit",function(t){var r=s()(n+" button[type=submit]").button("loading"),a=i.attr("data-form-event"),l=e.getContainer().getPlugin("event"),u=e.getContainer().getPlugin("alert");t.preventDefault(),t.stopPropagation(),s.a.ajax({url:i.attr("action"),type:i.attr("method"),data:i.serialize(),success:function(t){r.button("reset");var n=s()(t).find("#form_modal .modal-content .has-error").length>0,c=s()(t).find("#form_modal .modal-content ul.list-unstyled li.text-danger").length>0,d=s()(t).find("div.alert-error").length>0;if(n||c||d)e._openFormInModal(t);else{l.trigger(a);var h=i.attr("data-msg-success");if(null===h||void 0===h){var f=s()(t).find("section.content div.row div.alert.alert-success");if(f.length>0){var p=f.contents();3===p.length&&(h=p[2].textContent)}}null!==h&&void 0!==h||(h="action.update.success"),o.modal("hide"),u.success(h)}return!1},error:function(t,e){var n=i.attr("data-msg-error");null!==n&&void 0!==n||(n="action.update.error"),t.responseJSON&&t.responseJSON.message?e=t.responseJSON.message:t.status&&t.statusText&&(e="["+t.status+"] "+t.statusText),u.error(n,e)}})})}}]),e}(l.a);e.a=c},T4Pq:function(t,e,n){"use strict";function i(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function o(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}function r(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}var a=n("7t+N"),s=n.n(a),l=n("4klR"),u=function(){function t(t,e){for(var n=0;n'+i.text()+""),s.a.each(e,function(t,e){n.append('")}),n.trigger("change"),s()(".selectpicker").selectpicker("refresh")}}]),e}(l.a);e.a=c},TqGs:function(t,e,n){"use strict";function i(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}var o=n("+NP7"),r=n("awG4"),a=n("4klR"),s=function(){function t(t,e){for(var n=0;nviewHandler->handle($view);
}
+
+ /**
+ * Switch the export state of a timesheet record to (un-)lock it
+ *
+ * @SWG\Response(
+ * response=200,
+ * description="Switches the exported state on the record and therefor locks / unlocks it for further updates. Needs edit_export_*_timesheet permission.",
+ * @SWG\Schema(ref="#/definitions/TimesheetEntity")
+ * )
+ * @SWG\Parameter(
+ * name="id",
+ * in="path",
+ * type="integer",
+ * description="Timesheet record ID to switch export state",
+ * required=true,
+ * )
+ *
+ * @Security("is_granted('edit_export_own_timesheet') or is_granted('edit_export_other_timesheet')")
+ *
+ * @param int $id
+ * @return Response
+ */
+ public function exportAction($id)
+ {
+ /** @var Timesheet $timesheet */
+ $timesheet = $this->repository->find($id);
+
+ if (null === $timesheet) {
+ throw new NotFoundException();
+ }
+
+ if (!$this->isGranted('edit_export', $timesheet)) {
+ throw new AccessDeniedHttpException(
+ sprintf('You are not allowed to %s this timesheet', ($timesheet->isExported() ? 'unlock' : 'lock'))
+ );
+ }
+
+ $timesheet->setExported(!$timesheet->isExported());
+
+ $entityManager = $this->getDoctrine()->getManager();
+ $entityManager->persist($timesheet);
+ $entityManager->flush();
+
+ $view = new View($timesheet, 200);
+ $view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']);
+
+ return $this->viewHandler->handle($view);
+ }
}
diff --git a/src/Command/CreateReleaseCommand.php b/src/Command/CreateReleaseCommand.php
index 363d5441..971d213e 100644
--- a/src/Command/CreateReleaseCommand.php
+++ b/src/Command/CreateReleaseCommand.php
@@ -18,6 +18,8 @@ use Symfony\Component\Console\Style\SymfonyStyle;
/**
* Command used to create a release package with pre-installed composer, SQLite database and user.
+ *
+ * @codeCoverageIgnore
*/
class CreateReleaseCommand extends Command
{
diff --git a/src/Command/KimaiImporterCommand.php b/src/Command/KimaiImporterCommand.php
index c4115135..9b19f4e3 100644
--- a/src/Command/KimaiImporterCommand.php
+++ b/src/Command/KimaiImporterCommand.php
@@ -37,6 +37,7 @@ use Symfony\Component\Validator\Validator\ValidatorInterface;
* Command used to import data from a Kimai v1 installation.
* Getting help in improving this script would be fantastic, it currently only handles the most basic use-cases.
*
+ * This command is way to messy and complex to be tested ... so we use something, which I actually don't like:
* @codeCoverageIgnore
*/
class KimaiImporterCommand extends Command
diff --git a/src/Command/ResetCommand.php b/src/Command/ResetCommand.php
index bbae5732..43465fa8 100644
--- a/src/Command/ResetCommand.php
+++ b/src/Command/ResetCommand.php
@@ -19,6 +19,11 @@ use Symfony\Component\Console\Style\SymfonyStyle;
/**
* Command used to execute all the basic application bootstrapping AFTER "composer install" was executed.
+ *
+ * This command is NOT used during runtime and only meant for developers on their local machines.
+ * I am too lazy to think about how this could be tested ... and this is one of the rare edge cases where I don't
+ * feel like it is necessary, so I "cheat" with:
+ * @codeCoverageIgnore
*/
class ResetCommand extends Command
{
diff --git a/src/Twig/Extensions.php b/src/Twig/Extensions.php
index bbda63ab..356ce832 100644
--- a/src/Twig/Extensions.php
+++ b/src/Twig/Extensions.php
@@ -93,6 +93,7 @@ class Extensions extends AbstractExtension
'debug' => 'far fa-file-alt',
'profile-stats' => 'far fa-chart-bar',
'profile' => 'fas fa-user-edit',
+ 'warning' => 'fas fa-exclamation-triangle',
];
/**
diff --git a/src/Voter/TimesheetVoter.php b/src/Voter/TimesheetVoter.php
index 95ce5ce9..cc6f6696 100644
--- a/src/Voter/TimesheetVoter.php
+++ b/src/Voter/TimesheetVoter.php
@@ -85,12 +85,24 @@ class TimesheetVoter extends AbstractVoter
$permission .= $attribute;
break;
+ case self::EDIT:
+ if (!$this->canEdit($user, $subject)) {
+ return false;
+ }
+ $permission .= $attribute;
+ break;
+
+ case self::DELETE:
+ if (!$this->canDelete($user, $subject)) {
+ return false;
+ }
+ $permission .= $attribute;
+ break;
+
case self::VIEW_RATE:
case self::EDIT_RATE:
case self::STOP:
- case self::EDIT:
case self::VIEW:
- case self::DELETE:
case self::EXPORT:
case self::EDIT_EXPORT:
$permission .= $attribute;
@@ -114,11 +126,7 @@ class TimesheetVoter extends AbstractVoter
return $this->hasRolePermission($user, $permission);
}
- /**
- * @param Timesheet $timesheet
- * @return bool
- */
- protected function canStart(Timesheet $timesheet)
+ protected function canStart(Timesheet $timesheet): bool
{
// possible improvements for the future:
// we could check the amount of active entries (maybe slow)
@@ -142,4 +150,22 @@ class TimesheetVoter extends AbstractVoter
return true;
}
+
+ protected function canEdit(User $user, Timesheet $timesheet): bool
+ {
+ if ($timesheet->isExported() && !$this->hasRolePermission($user, 'edit_exported_timesheet')) {
+ return false;
+ }
+
+ return true;
+ }
+
+ protected function canDelete(User $user, Timesheet $timesheet): bool
+ {
+ if ($timesheet->isExported() && !$this->hasRolePermission($user, 'edit_exported_timesheet')) {
+ return false;
+ }
+
+ return true;
+ }
}
diff --git a/templates/export/index.html.twig b/templates/export/index.html.twig
index a77288a9..162caa95 100644
--- a/templates/export/index.html.twig
+++ b/templates/export/index.html.twig
@@ -119,42 +119,56 @@
{{ parent() }}