diff --git a/UPGRADING.md b/UPGRADING.md index 79786956..358a11f2 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -10,9 +10,12 @@ Perform EACH version specific task between your version and the new one, otherwi ## [1.15](https://github.com/kevinpapst/kimai2/releases/tag/1.15) +**Many database changes: don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).** + Updating the database might take quite a while, depending on the amount of timesheet entries and speed of your database server (~1 minute per 100k records). -**ATTENTION** +**ATTENTION** + - This release bumps the minimum required [PHP version to 7.3](https://www.kimai.org/blog/2021/php8-support-php72-dropped/) - Self-registration is disabled by default - Self-registration now always requires email confirmation @@ -20,11 +23,13 @@ Updating the database might take quite a while, depending on the amount of times - Removed the YearChart widget and the related configs named `userRecapThisYear`, `userRecapLastYear`, `userRecapTwoYears`, `userRecapThreeYears` **LDAP & SAML** + - SAML users need to activate SAML by setting the new `kimai.saml.activate: true` config key - LDAP and SAML users need to remove the complete `security` section from their `local.yaml` - Please verify your config with the [LDAP](https://www.kimai.org/documentation/ldap.html) and [SAML](https://www.kimai.org/documentation/saml.html) documentation -**DEVELOPER** +**DEVELOPER** + PHP 8 compatibility forced to upgrade MANY libraries, including but not limited to: - Removed FOSUserBundle and hslavich/oneloginsaml @@ -32,6 +37,8 @@ PHP 8 compatibility forced to upgrade MANY libraries, including but not limited - Gedmo v3 (which include BC breaks in definitions) - Doctrine DBAL and others, which required PHP 7.3 as well +**API BC break**: Due to team structure changes, it was impossible to keep the (writing) API structure. Please adjust your code accordingly! + ## [1.14](https://github.com/kevinpapst/kimai2/releases/tag/1.14) **CRITICAL BC break**: SQLite support was removed. If you are using SQLite, you have to [read this blog post](https://www.kimai.org/blog/2021/sqlite-and-ftp-support-removed/) and migrate to MySQL/MariaDB first! diff --git a/assets/js/KimaiLoader.js b/assets/js/KimaiLoader.js index 3c484433..35e42c72 100644 --- a/assets/js/KimaiLoader.js +++ b/assets/js/KimaiLoader.js @@ -54,7 +54,7 @@ export default class KimaiLoader { kimai.registerPlugin(new KimaiDateUtils()); kimai.registerPlugin(new KimaiFormSelect('.selectpicker')); kimai.registerPlugin(new KimaiConfirmationLink('confirmation-link')); - kimai.registerPlugin(new KimaiActiveRecordsDuration('[data-since]')); + kimai.registerPlugin(new KimaiActiveRecordsDuration()); kimai.registerPlugin(new KimaiDatatableColumnView('data-column-visibility')); kimai.registerPlugin(new KimaiDateRangePicker('input[data-daterangepickerenable="on"]')); kimai.registerPlugin(new KimaiDateTimePicker('input[data-datetimepicker="on"]')); diff --git a/assets/js/plugins/KimaiActiveRecords.js b/assets/js/plugins/KimaiActiveRecords.js index c5d96b71..fd0e5f17 100644 --- a/assets/js/plugins/KimaiActiveRecords.js +++ b/assets/js/plugins/KimaiActiveRecords.js @@ -48,6 +48,13 @@ export default class KimaiActiveRecords extends KimaiPlugin { _toggleMenu(hasEntries) { this.menu.style.display = hasEntries ? 'inline-block' : 'none'; + if (!hasEntries) { + // make sure that template entries in the menu are removed, otherwise they + // might still be shown in the browsers title + for (let record of this.menu.querySelectorAll('[data-since]')) { + record.dataset['since'] = ''; + } + } const menuEmpty = document.querySelector(this.selectorEmpty); if (menuEmpty !== null) { diff --git a/assets/js/plugins/KimaiActiveRecordsDuration.js b/assets/js/plugins/KimaiActiveRecordsDuration.js index 37f6ed3d..4dda4a41 100644 --- a/assets/js/plugins/KimaiActiveRecordsDuration.js +++ b/assets/js/plugins/KimaiActiveRecordsDuration.js @@ -9,16 +9,10 @@ * [KIMAI] KimaiActiveRecordsDuration: activate the updates for all active timesheet records on this page */ -import moment from 'moment'; import KimaiPlugin from '../KimaiPlugin'; export default class KimaiActiveRecordsDuration extends KimaiPlugin { - constructor(selector) { - super(); - this.selector = selector; - } - getId() { return 'timesheet-duration'; } @@ -39,7 +33,7 @@ export default class KimaiActiveRecordsDuration extends KimaiPlugin { updateRecords() { let durations = []; - const activeRecords = document.querySelectorAll(this.selector); + const activeRecords = document.querySelectorAll('[data-since]:not([data-since=""])'); if (activeRecords.length === 0) { if (this.updateBrowserTitle) { @@ -48,12 +42,16 @@ export default class KimaiActiveRecordsDuration extends KimaiPlugin { return; } - for(let record of activeRecords) { - const since = record.getAttribute('data-since'); - const duration = this.formatDuration(since); - if (record.getAttribute('data-title') !== null && duration !== '?') { + const DATE = this.getPlugin('date'); + + for (let record of activeRecords) { + const since = record.dataset['since']; + const duration = DATE.formatDuration(since); + // only use the ones from the menu for the title + if (record.dataset['replacer'] !== undefined && record.dataset['title'] !== null && duration !== '?') { durations.push(duration); } + // but update all on the page (running entries in list pages) record.textContent = duration; } @@ -73,8 +71,4 @@ export default class KimaiActiveRecordsDuration extends KimaiPlugin { } document.title = title; } - - formatDuration(since) { - return this.getPlugin('date').formatDuration(since); - } } diff --git a/assets/sass/avatar.scss b/assets/sass/avatar.scss index 767ff0fb..233e15c4 100644 --- a/assets/sass/avatar.scss +++ b/assets/sass/avatar.scss @@ -16,7 +16,7 @@ $avatar-base-size: 30; position: relative; font-weight: bold; font-size: ceil($avatar-base-size / 2 - 2)+px; - top: ceil($avatar-base-size / 4 - 1)+px; + top: floor($avatar-base-size / 4 - 1)+px; } $avatarSizes: "xs" .75, "sm" 1.25, "md" 1.5, "lg" 2; @@ -54,3 +54,7 @@ table.dataTable.table > tbody > tr > td { } } } + +.avatar.teamlead { + box-shadow: 0px 0px 5px 1px rgba(0,0,0,.4),0 0px 3px 0 rgba(0,0,0,0.3); +} \ No newline at end of file diff --git a/assets/sass/datatable-extensions.scss b/assets/sass/datatable-extensions.scss index a63ed4d1..71e9ebdc 100644 --- a/assets/sass/datatable-extensions.scss +++ b/assets/sass/datatable-extensions.scss @@ -30,6 +30,12 @@ span.label-activity, span.label-project, span.label-customer { display: inline-block; + .name { + white-space: nowrap; + .name-inner { + white-space: normal; + } + } .dot { font-size: 9px; margin-right: 3px; diff --git a/config/packages/nelmio_api_doc.yaml b/config/packages/nelmio_api_doc.yaml index bc6a76df..4cab00bf 100644 --- a/config/packages/nelmio_api_doc.yaml +++ b/config/packages/nelmio_api_doc.yaml @@ -37,6 +37,8 @@ nelmio_api_doc: - { alias: TeamEditForm, type: App\Form\API\TeamApiEditForm, groups: [Default, Entity, Team, Team_Entity] } - { alias: TeamEntity, type: App\Entity\Team, groups: [Default, Entity, Team, Team_Entity] } - { alias: TeamCollection, type: App\Entity\Team, groups: [Default, Collection, Team] } + - { alias: TeamMember, type: App\Entity\TeamMember, groups: [Team_Entity] } + - { alias: TeamMembership, type: App\Entity\TeamMember, groups: [User_Entity] } areas: path_patterns: - ^/api(?!/doc) diff --git a/public/build/app.39dfa7e1.js b/public/build/app.39dfa7e1.js new file mode 100644 index 00000000..10fc849b --- /dev/null +++ b/public/build/app.39dfa7e1.js @@ -0,0 +1,2 @@ +/*! For license information please see app.39dfa7e1.js.LICENSE.txt */ +(self.webpackChunkkimai2=self.webpackChunkkimai2||[]).push([[143],{3752:function(e,t,n){var r=n(9755);if(void 0===r)throw new Error("AdminLTE requires jQuery");!function(e){"use strict";function t(t,n){if(this.element=t,this.options=n,this.$overlay=e(n.overlayTemplate),""===n.source)throw new Error("Source url was not defined. Please specify a url in your BoxRefresh source option.");this._setUpListeners(),this.load()}var n="lte.boxrefresh",r={source:"",params:{},trigger:".refresh-btn",content:".box-body",loadInContent:!0,responseType:"",overlayTemplate:'
',onLoadStart:function(){},onLoadDone:function(e){return e}};function i(i){return this.each((function(){var a=e(this),o=a.data(n);if(!o){var s=e.extend({},r,a.data(),"object"==typeof i&&i);a.data(n,o=new t(a,s))}if("string"==typeof o){if(void 0===o[i])throw new Error("No method named "+i);o[i]()}}))}t.prototype.load=function(){this._addOverlay(),this.options.onLoadStart.call(e(this)),e.get(this.options.source,this.options.params,function(t){this.options.loadInContent&&e(this.element).find(this.options.content).html(t),this.options.onLoadDone.call(e(this),t),this._removeOverlay()}.bind(this),""!==this.options.responseType&&this.options.responseType)},t.prototype._setUpListeners=function(){e(this.element).on("click",this.options.trigger,function(e){e&&e.preventDefault(),this.load()}.bind(this))},t.prototype._addOverlay=function(){e(this.element).append(this.$overlay)},t.prototype._removeOverlay=function(){e(this.$overlay).remove()};var a=e.fn.boxRefresh;e.fn.boxRefresh=i,e.fn.boxRefresh.Constructor=t,e.fn.boxRefresh.noConflict=function(){return e.fn.boxRefresh=a,this},e(window).on("load",(function(){e('[data-widget="box-refresh"]').each((function(){i.call(e(this))}))}))}(r),function(e){"use strict";function t(e,t){this.element=e,this.options=t,this._setUpListeners()}var n="lte.boxwidget",r={animationSpeed:500,collapseTrigger:'[data-widget="collapse"]',removeTrigger:'[data-widget="remove"]',collapseIcon:"fa-minus",expandIcon:"fa-plus",removeIcon:"fa-times"},i=".box-header",a=".box-body",o=".box-footer",s=".box-tools",l="collapsed-box";function u(i){return this.each((function(){var a=e(this),o=a.data(n);if(!o){var s=e.extend({},r,a.data(),"object"==typeof i&&i);a.data(n,o=new t(a,s))}if("string"==typeof i){if(void 0===o[i])throw new Error("No method named "+i);o[i]()}}))}t.prototype.toggle=function(){e(this.element).is(".collapsed-box")?this.expand():this.collapse()},t.prototype.expand=function(){var t=e.Event("expanded.boxwidget"),n=e.Event("expanding.boxwidget"),r=this.options.collapseIcon,u=this.options.expandIcon;e(this.element).removeClass(l),e(this.element).children(i+", "+a+", "+o).children(s).find("."+u).removeClass(u).addClass(r),e(this.element).children(a+", "+o).slideDown(this.options.animationSpeed,function(){e(this.element).trigger(t)}.bind(this)).trigger(n)},t.prototype.collapse=function(){var t=e.Event("collapsed.boxwidget"),n=e.Event("collapsing.boxwidget"),r=this.options.collapseIcon,u=this.options.expandIcon;e(this.element).children(i+", "+a+", "+o).children(s).find("."+r).removeClass(r).addClass(u),e(this.element).children(a+", "+o).slideUp(this.options.animationSpeed,function(){e(this.element).addClass(l),e(this.element).trigger(t)}.bind(this)).trigger(n)},t.prototype.remove=function(){var t=e.Event("removed.boxwidget"),n=e.Event("removing.boxwidget");e(this.element).slideUp(this.options.animationSpeed,function(){e(this.element).trigger(t),e(this.element).remove()}.bind(this)).trigger(n)},t.prototype._setUpListeners=function(){var t=this;e(this.element).on("click",this.options.collapseTrigger,(function(n){return n&&n.preventDefault(),t.toggle(e(this)),!1})),e(this.element).on("click",this.options.removeTrigger,(function(n){return n&&n.preventDefault(),t.remove(e(this)),!1}))};var c=e.fn.boxWidget;e.fn.boxWidget=u,e.fn.boxWidget.Constructor=t,e.fn.boxWidget.noConflict=function(){return e.fn.boxWidget=c,this},e(window).on("load",(function(){e(".box").each((function(){u.call(e(this))}))}))}(r),function(e){"use strict";function t(e,t){this.element=e,this.options=t,this.hasBindedResize=!1,this.init()}var n="lte.controlsidebar",r={controlsidebarSlide:!0},i=".control-sidebar",a='[data-toggle="control-sidebar"]',o=".control-sidebar-open",s="control-sidebar-open",l="control-sidebar-hold-transition";function u(i){return this.each((function(){var a=e(this),o=a.data(n);if(!o){var s=e.extend({},r,a.data(),"object"==typeof i&&i);a.data(n,o=new t(a,s))}"string"==typeof i&&o.toggle()}))}t.prototype.init=function(){e(this.element).is(a)||e(this).on("click",this.toggle),this.fix(),e(window).resize(function(){this.fix()}.bind(this))},t.prototype.toggle=function(t){t&&t.preventDefault(),this.fix(),e(i).is(o)||e("body").is(o)?this.collapse():this.expand()},t.prototype.expand=function(){e(i).show(),this.options.controlsidebarSlide?e(i).addClass(s):e("body").addClass(l).addClass(s).delay(50).queue((function(){e("body").removeClass(l),e(this).dequeue()})),e(this.element).trigger(e.Event("expanded.controlsidebar"))},t.prototype.collapse=function(){this.options.controlsidebarSlide?e(i).removeClass(s):e("body").addClass(l).removeClass(s).delay(50).queue((function(){e("body").removeClass(l),e(this).dequeue()})),e(i).fadeOut(),e(this.element).trigger(e.Event("collapsed.controlsidebar"))},t.prototype.fix=function(){e("body").is(".layout-boxed")&&this._fixForBoxed(e(".control-sidebar-bg"))},t.prototype._fixForBoxed=function(t){t.css({position:"absolute",height:e(".wrapper").height()})};var c=e.fn.controlSidebar;e.fn.controlSidebar=u,e.fn.controlSidebar.Constructor=t,e.fn.controlSidebar.noConflict=function(){return e.fn.controlSidebar=c,this},e(document).on("click",a,(function(t){t&&t.preventDefault(),u.call(e(this),"toggle")}))}(r),function(e){"use strict";function t(e){this.element=e}var n="lte.directchat";function r(r){return this.each((function(){var i=e(this),a=i.data(n);a||i.data(n,a=new t(i)),"string"==typeof r&&a.toggle(i)}))}t.prototype.toggle=function(e){e.parents(".direct-chat").first().toggleClass("direct-chat-contacts-open")};var i=e.fn.directChat;e.fn.directChat=r,e.fn.directChat.Constructor=t,e.fn.directChat.noConflict=function(){return e.fn.directChat=i,this},e(document).on("click",'[data-widget="chat-pane-toggle"]',(function(t){t&&t.preventDefault(),r.call(e(this),"toggle")}))}(r),function(e){"use strict";function t(e){this.options=e,this.init()}var n="lte.pushmenu",r={collapseScreenSize:767,expandOnHover:!1,expandTransitionDelay:200},i='[data-toggle="push-menu"]',a=".sidebar-mini",o="sidebar-collapse",s="sidebar-open",l="sidebar-expanded-on-hover",u="expanded.pushMenu",c="collapsed.pushMenu";function d(i){return this.each((function(){var a=e(this),o=a.data(n);if(!o){var s=e.extend({},r,a.data(),"object"==typeof i&&i);a.data(n,o=new t(s))}"toggle"===i&&o.toggle()}))}t.prototype.init=function(){(this.options.expandOnHover||e("body").is(a+".fixed"))&&(this.expandOnHover(),e("body").addClass("sidebar-mini-expand-feature")),e(".content-wrapper").click(function(){e(window).width()<=this.options.collapseScreenSize&&e("body").hasClass(s)&&this.close()}.bind(this)),e(".sidebar-form .form-control").click((function(e){e.stopPropagation()}))},t.prototype.toggle=function(){var t=e(window).width(),n=!e("body").hasClass(o);t<=this.options.collapseScreenSize&&(n=e("body").hasClass(s)),n?this.close():this.open()},t.prototype.open=function(){e(window).width()>this.options.collapseScreenSize?e("body").removeClass(o).trigger(e.Event(u)):e("body").addClass(s).trigger(e.Event(u))},t.prototype.close=function(){e(window).width()>this.options.collapseScreenSize?e("body").addClass(o).trigger(e.Event(c)):e("body").removeClass(s+" "+o).trigger(e.Event(c))},t.prototype.expandOnHover=function(){e(".main-sidebar").hover(function(){e("body").is(a+".sidebar-collapse")&&e(window).width()>this.options.collapseScreenSize&&this.expand()}.bind(this),function(){e("body").is(".sidebar-expanded-on-hover")&&this.collapse()}.bind(this))},t.prototype.expand=function(){setTimeout((function(){e("body").removeClass(o).addClass(l)}),this.options.expandTransitionDelay)},t.prototype.collapse=function(){setTimeout((function(){e("body").removeClass(l).addClass(o)}),this.options.expandTransitionDelay)};var f=e.fn.pushMenu;e.fn.pushMenu=d,e.fn.pushMenu.Constructor=t,e.fn.pushMenu.noConflict=function(){return e.fn.pushMenu=f,this},e(document).on("click",i,(function(t){t.preventDefault(),d.call(e(this),"toggle")})),e(window).on("load",(function(){d.call(e(i))}))}(r),function(e){"use strict";function t(e,t){this.element=e,this.options=t,this._setUpListeners()}var n="lte.todolist",r={onCheck:function(e){return e},onUnCheck:function(e){return e}},i={data:'[data-widget="todo-list"]'};function a(i){return this.each((function(){var a=e(this),o=a.data(n);if(!o){var s=e.extend({},r,a.data(),"object"==typeof i&&i);a.data(n,o=new t(a,s))}if("string"==typeof o){if(void 0===o[i])throw new Error("No method named "+i);o[i]()}}))}t.prototype.toggle=function(e){e.parents(i.li).first().toggleClass("done"),e.prop("checked")?this.check(e):this.unCheck(e)},t.prototype.check=function(e){this.options.onCheck.call(e)},t.prototype.unCheck=function(e){this.options.onUnCheck.call(e)},t.prototype._setUpListeners=function(){var t=this;e(this.element).on("change ifChanged","input:checkbox",(function(){t.toggle(e(this))}))};var o=e.fn.todoList;e.fn.todoList=a,e.fn.todoList.Constructor=t,e.fn.todoList.noConflict=function(){return e.fn.todoList=o,this},e(window).on("load",(function(){e(i.data).each((function(){a.call(e(this))}))}))}(r),function(e){"use strict";function t(t,n){this.element=t,this.options=n,e(this.element).addClass(l),e(i+o,this.element).addClass(s),this._setUpListeners()}var n="lte.tree",r={animationSpeed:500,accordion:!0,followLink:!1,trigger:".treeview a"},i=".treeview",a=".treeview-menu",o=".active",s="menu-open",l="tree";function u(i){return this.each((function(){var a=e(this);if(!a.data(n)){var o=e.extend({},r,a.data(),"object"==typeof i&&i);a.data(n,new t(a,o))}}))}t.prototype.toggle=function(e,t){var n=e.next(a),r=e.parent(),o=r.hasClass(s);r.is(i)&&(this.options.followLink&&"#"!==e.attr("href")||t.preventDefault(),o?this.collapse(n,r):this.expand(n,r))},t.prototype.expand=function(t,n){var r=e.Event("expanded.tree");if(this.options.accordion){var i=n.siblings(".menu-open, .active"),o=i.children(a);this.collapse(o,i)}n.addClass(s),t.stop().slideDown(this.options.animationSpeed,function(){e(this.element).trigger(r),n.height("auto")}.bind(this))},t.prototype.collapse=function(t,n){var r=e.Event("collapsed.tree");n.removeClass(s),t.stop().slideUp(this.options.animationSpeed,function(){e(this.element).trigger(r),n.find(i).removeClass(s).find(a).hide()}.bind(this))},t.prototype._setUpListeners=function(){var t=this;e(this.element).on("click",this.options.trigger,(function(n){t.toggle(e(this),n)}))};var c=e.fn.tree;e.fn.tree=u,e.fn.tree.Constructor=t,e.fn.tree.noConflict=function(){return e.fn.tree=c,this},e(window).on("load",(function(){e('[data-widget="tree"]').each((function(){u.call(e(this))}))}))}(r),function(e){"use strict";function t(e){this.options=e,this.bindedResize=!1,this.activate()}var n="lte.layout",r={slimscroll:!0,resetHeight:!0},i=".wrapper",a=".content-wrapper",o=".main-header",s=".sidebar",l=".sidebar-menu",u="fixed";function c(i){return this.each((function(){var a=e(this),o=a.data(n);if(!o){var s=e.extend({},r,a.data(),"object"==typeof i&&i);a.data(n,o=new t(s))}if("string"==typeof i){if(void 0===o[i])throw new Error("No method named "+i);o[i]()}}))}t.prototype.activate=function(){this.fix(),this.fixSidebar(),e("body").removeClass("hold-transition"),this.options.resetHeight&&e("body, html, "+i).css({height:"auto","min-height":"100%"}),this.bindedResize||(e(window).resize(function(){this.fix(),this.fixSidebar(),e(".main-header .logo, "+s).one("webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd transitionend",function(){this.fix(),this.fixSidebar()}.bind(this))}.bind(this)),this.bindedResize=!0),e(l).on("expanded.tree",function(){this.fix(),this.fixSidebar()}.bind(this)),e(l).on("collapsed.tree",function(){this.fix(),this.fixSidebar()}.bind(this))},t.prototype.fix=function(){e(".layout-boxed > "+i).css("overflow","hidden");var t=e(".main-footer").outerHeight()||0,n=e(o).outerHeight()||0,r=n+t,l=e(window).height(),c=e(s).outerHeight()||0;if(e("body").hasClass(u))e(a).css("min-height",l-t);else{var d;d=c+n<=l?(e(a).css("min-height",l-r),l-r):(e(a).css("min-height",c),c);var f=e(".control-sidebar");void 0!==f&&f.height()>d&&e(a).css("min-height",f.height())}},t.prototype.fixSidebar=function(){e("body").hasClass(u)?this.options.slimscroll&&void 0!==e.fn.slimScroll&&0===e(".main-sidebar").find("slimScrollDiv").length&&e(s).slimScroll({height:e(window).height()-e(o).height()+"px"}):void 0!==e.fn.slimScroll&&e(s).slimScroll({destroy:!0}).height("auto")};var d=e.fn.layout;e.fn.layout=c,e.fn.layout.Constuctor=t,e.fn.layout.noConflict=function(){return e.fn.layout=d,this},e(window).on("load",(function(){c.call(e("body"))}))}(r)},8144:function(e,t,n){var r=n(9755);n.g.$=n.g.jQuery=r,n(3002),n(1402),n(686),n(5405),n(4037),n(2322),n(6823),n(4159),n(4752),n(2876),n(143),n(9850),n(2285),n(7665),n(1808),n(1311),n(3256),n(3912),n(6757),n(6806),n(818),n(6453),n(1339),n(7120),n(2985),n(7049),n(2984),n(4525);var i=n(381);n.g.moment=i,n(867),n(5822),n(4780),n(9740),n(217),n(894),n(837),n(2915),n(5655),n(7763),n(1897),n(4694),n(4470),n(4206),n(2138),n(626),n(9183),n(3730),n(3901),n(4495),n(9520),n(7971),n(6459),n(1793),n(4249),n(8760),n(2397),n(5666),n(3839),n(932),n(8749),n(2945),n(958),n(157),n(2404),n.g.$.AdminLTE={},n.g.$.AdminLTE.options={},n(3752),n(1639),n(5290),n.g.KimaiPaginatedBoxWidget=n(985).Z,n.g.KimaiReloadPageWidget=n(8227).Z,n.g.KimaiCookies=n(3954).Z,n.g.KimaiStorage=n(6654).Z,n(2993)},5290:function(e,t,n){"use strict";n.r(t);n(2526),n(1817),n(1539),n(2165),n(6992),n(8783),n(3948),n(489),n(2419),n(4916),n(5306),n(1249);var r=n(381),i=n.n(r);function a(e,t){for(var n=0;n=e.length?{done:!0}:{done:!1,value:e[r++]}},e:function(e){throw e},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,o=!0,s=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return o=e.done,e},e:function(e){s=!0,a=e},f:function(){try{o||null==n.return||n.return()}finally{if(s)throw a}}}}function f(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n=e.length?{done:!0}:{done:!1,value:e[r++]}},e:function(e){throw e},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,o=!0,s=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return o=e.done,e},e:function(e){s=!0,a=e},f:function(){try{o||null==n.return||n.return()}finally{if(s)throw a}}}}function y(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n=e.length?{done:!0}:{done:!1,value:e[r++]}},e:function(e){throw e},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,o=!0,s=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return o=e.done,e},e:function(e){s=!0,a=e},f:function(){try{o||null==n.return||n.return()}finally{if(s)throw a}}}}function O(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);nx()(window).height()&&(t.drops="up",t.move())})),x()(this).on("apply.daterangepicker",(function(e,t){x()(this).val(t.startDate.format(a)+" - "+t.endDate.format(a)),x()(this).data("begin",t.startDate.format(a)),x()(this).data("end",t.endDate.format(a)),x()(this).trigger("change")}))}))}},{key:"destroyDateRangePicker",value:function(e){x()(e+" "+this.selector).each((function(e){void 0!==x()(this).data("daterangepicker")&&(x()(this).daterangepicker("destroy"),x()(this).data("daterangepicker").remove())}))}}])&&J(t.prototype,n),r&&J(t,r),o}(c);n(3123);function Q(e){return(Q="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function ee(e,t){var n="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(!n){if(Array.isArray(e)||(n=function(e,t){if(!e)return;if("string"==typeof e)return te(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);"Object"===n&&e.constructor&&(n=e.constructor.name);if("Map"===n||"Set"===n)return Array.from(e);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return te(e,t)}(e))||t&&e&&"number"==typeof e.length){n&&(e=n);var r=0,i=function(){};return{s:i,n:function(){return r>=e.length?{done:!0}:{done:!1,value:e[r++]}},e:function(e){throw e},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,o=!0,s=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return o=e.done,e},e:function(e){s=!0,a=e},f:function(){try{o||null==n.return||n.return()}finally{if(s)throw a}}}}function te(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n');var a=x()(r+" :input").filter((function(e,t){return""!==x()(t).val()})).serialize();x().ajax({url:i.attr("action"),type:i.attr("method"),data:a,success:function(r){x()(t).replaceWith(x()(r).find(t)),n.updateRecords(),e.fixDropdowns()},error:function(e,t){i.submit()}})}},{key:"fixDropdowns",value:function(){var e=x()(document).height();x()(this.selector+" [data-toggle=dropdown]").each((function(){var t=x()(this).parent(),n=t.find(".dropdown-menu");t&&n&&t.offset().top+t.outerHeight()+n.outerHeight()>e&&t.addClass("dropup").removeClass("dropdown")}))}}])&&ne(t.prototype,n),r&&ne(t,r),a}(c);function le(e){return(le="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function ue(e,t){for(var n=0;n0&&(r=!1);break;case"project":x()(e+" select#activity").length>0&&(r=!1)}x()(e+" input#page").val(1),r&&t.triggerChange()}))}},{key:"_registerAlternativeSubmitActions",value:function(e,t){document.addEventListener("click",(function(n){for(var r=n.target;null!==r&&!r.matches("body");){if(r.classList.contains(t)){var i=document.querySelector(e);if(null===i)return;var a=i.action,o=i.method;i.target="_blank",i.action=r.href,void 0!==r.dataset.method&&(i.method=r.dataset.method),i.submit(),i.target="",i.action=a,i.method=o,n.preventDefault(),n.stopPropagation()}r=r.parentNode}}))}},{key:"_registerSortableTables",value:function(e,t){x()("body").on("click","th.sortable",(function(n){var r=x()(n.target),i="DESC",a=r.data("order");r.hasClass("sorting_desc")&&(i="ASC"),x()(e+" #orderBy").val(a),x()(e+" #order").val(i),x()(e+" #orderBy").trigger("change"),x()(e+" #order").trigger("change"),t.trigger("filter-change")}))}},{key:"_registerPagination",value:function(e,t){x()("body").on("click","div.navigation ul.pagination li a",(function(n){var r=x()(e+" input#page");if(0!==r.length){n.preventDefault(),n.stopPropagation();var i=x()(this).attr("href").split("/"),a=i[i.length-1];return r.val(a),r.trigger("change"),t.trigger("pagination-change"),!1}}))}},{key:"triggerChange",value:function(){this.getContainer().getPlugin("event").trigger("toolbar-change")}},{key:"getSelector",value:function(){return this.formSelector}}])&&ue(t.prototype,n),r&&ue(t,r),a}(c);function pe(e){return(pe="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function _e(e,t){var n="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(!n){if(Array.isArray(e)||(n=function(e,t){if(!e)return;if("string"==typeof e)return ye(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);"Object"===n&&e.constructor&&(n=e.constructor.name);if("Map"===n||"Set"===n)return Array.from(e);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return ye(e,t)}(e))||t&&e&&"number"==typeof e.length){n&&(e=n);var r=0,i=function(){};return{s:i,n:function(){return r>=e.length?{done:!0}:{done:!1,value:e[r++]}},e:function(e){throw e},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,o=!0,s=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return o=e.done,e},e:function(e){s=!0,a=e},f:function(){try{o||null==n.return||n.return()}finally{if(s)throw a}}}}function ye(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n"+r+""];if(t.responseJSON.errors.errors){var a,o=_e(t.responseJSON.errors.errors);try{for(o.s();!(a=o.n()).done;){var s=a.value;i.push(s)}}catch(n){o.e(n)}finally{o.f()}}if(t.responseJSON.errors.children)for(var l in t.responseJSON.errors.children){var u=t.responseJSON.errors.children[l];if(u.hasOwnProperty("errors")&&u.errors.length>0){var c,d=_e(u.errors);try{for(d.s();!(c=d.n()).done;){var f=c.value;i.push(f)}}catch(n){d.e(n)}finally{d.f()}}}i.length>0&&(r=i)}}else t.status&&t.statusText&&(r="["+t.status+"] "+t.statusText);this.getPlugin("alert").error(e,r)}}])&&ve(t.prototype,n),r&&ve(t,r),a}(c);n(4747),n(4723),n(9600),n(7941);function Ye(e){return(Ye="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function De(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var n=e&&("undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"]);if(null==n)return;var r,i,a=[],o=!0,s=!1;try{for(n=n.call(e);!(o=(r=n.next()).done)&&(a.push(r.value),!t||a.length!==t);o=!0);}catch(e){s=!0,i=e}finally{try{o||null==n.return||n.return()}finally{if(s)throw i}}return a}(e,t)||Te(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function Te(e,t){if(e){if("string"==typeof e)return xe(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?xe(e,t):void 0}}function xe(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n=e.length?{done:!0}:{done:!1,value:e[r++]}},e:function(e){throw e},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,o=!0,s=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return o=e.done,e},e:function(e){s=!0,a=e},f:function(){try{o||null==n.return||n.return()}finally{if(s)throw a}}}}(t);try{for(i.s();!(n=i.n()).done;){var a=n.value,o="__empty__";a.hasOwnProperty("parentTitle")&&null!==a.parentTitle&&(o=a.parentTitle),r.hasOwnProperty(o)||(r[o]=[]),r[o].push(a)}}catch(e){i.e(e)}finally{i.f()}var s={};Object.keys(r).sort().forEach((function(e){s[e]=r[e]})),this.getContainer().getPlugin("form-select").updateOptions(e,s)}}])&&Se(t.prototype,n),r&&Se(t,r),o}(c);function Pe(e){return(Pe="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function Ae(e,t){for(var n=0;nx()(window).height()&&(t.drops="up",t.move())})),x()(this).on("apply.daterangepicker",(function(e,t){x()(this).val(t.startDate.format(a)),x()(this).trigger("change")}))}))}},{key:"destroyDateTimePicker",value:function(e){x()(e+" "+this.selector).each((function(e){void 0!==x()(this).data("daterangepicker")&&(x()(this).daterangepicker("destroy"),x()(this).data("daterangepicker").remove())}))}}])&&Ae(t.prototype,n),r&&Ae(t,r),a}(c);function Fe(e){return(Fe="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function ze(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function Be(e,t){for(var n=0;n'+n+"

")}t.preventDefault()}else x()(e._getFormIdentifier()).off("change",e._isDirtyHandler),e.isDirty=!1,e.getContainer().getPlugin("event").trigger("modal-hide")})).on("hidden.bs.modal",(function(){e.getContainer().getPlugin("form").destroyForm(e._getFormIdentifier()),x()("#remote_form_modal .modal-body").replaceWith("")})).on("show.bs.modal",(function(){e.getContainer().getPlugin("event").trigger("modal-show")})),this._addClickHandler(this.selector,(function(t){e.openUrlInModal(t)}))}},{key:"openUrlInModal",value:function(e,t){var n=this;void 0===t&&(t=function(t,n){void 0!==t.status&&403===t.status||(window.location=e)}),x().ajax({url:e,success:function(e){n._openFormInModal(e)},error:t})}},{key:"_getFormIdentifier",value:function(){return"#remote_form_modal .modal-content form"}},{key:"_openFormInModal",value:function(e){var t=this,n=this._getFormIdentifier(),r=x()(n),i=this.modal;r.off("submit"),x()(e).find("#form_modal .modal-content").length>0&&(i.on("hidden.bs.modal",(function(){i.hasClass("modal-danger")&&i.removeClass("modal-danger")})),x()(e).find("#form_modal").hasClass("modal-danger")&&i.addClass("modal-danger"),x()("#remote_form_modal .modal-content").replaceWith(x()(e).find("#form_modal .modal-content")),x()("#remote_form_modal [data-dismiss=modal]").on("click",(function(){t.isDirty=!1})),t.getContainer().getPlugin("form").activateForm(n));var a=x()(e).find("div.alert");a.length>0&&x()("#remote_form_modal .modal-body").prepend(a);var o=x().fn.modal.Constructor.prototype.enforceFocus;x().fn.modal.Constructor.prototype.enforceFocus=function(){},i.on("hidden.bs.modal",(function(){x().fn.modal.Constructor.prototype.enforceFocus=o})),i.modal("show"),r=x()(n),this._isDirtyHandler=function(e){t.isDirty=!0},r.on("change",this._isDirtyHandler),r.on("submit",(function(e){var a=x()(n+" button[type=submit]").button("loading"),o=r.attr("data-form-event"),s=t.getContainer().getPlugin("event"),l=t.getContainer().getPlugin("alert");e.preventDefault(),e.stopPropagation(),x().ajax({url:r.attr("action"),type:r.attr("method"),data:r.serialize(),success:function(e){a.button("reset");var n=x()(e).find("#form_modal .modal-content .has-error").length>0,u=x()(e).find("#form_modal .modal-content ul.list-unstyled li.text-danger").length>0,c=x()(e).find("div.alert-error").length>0;if(n||u||c)t._openFormInModal(e);else{s.trigger(o);var d=r.attr("data-msg-success");if(null==d){var f=x()(e).find("section.content div.row div.alert.alert-success");if(f.length>0){var h=f.contents();3===h.length&&(d=h[2].textContent)}}null==d&&(d="action.update.success"),t.isDirty=!1,i.modal("hide"),l.success(d)}return!1},error:function(e,t){var n=r.attr("data-msg-error");null==n&&(n="action.update.error"),e.responseJSON&&e.responseJSON.message?t=e.responseJSON.message:e.status&&e.statusText&&(t="["+e.status+"] "+e.statusText),l.error(n,t),setTimeout((function(){a.button("reset")}),1500)}})}))}}])&&it(t.prototype,n),r&&it(t,r),a}(Ge);function ct(e){return(ct="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function dt(e,t){var n="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(!n){if(Array.isArray(e)||(n=function(e,t){if(!e)return;if("string"==typeof e)return ft(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);"Object"===n&&e.constructor&&(n=e.constructor.name);if("Map"===n||"Set"===n)return Array.from(e);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return ft(e,t)}(e))||t&&e&&"number"==typeof e.length){n&&(e=n);var r=0,i=function(){};return{s:i,n:function(){return r>=e.length?{done:!0}:{done:!1,value:e[r++]}},e:function(e){throw e},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,o=!0,s=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return o=e.done,e},e:function(e){s=!0,a=e},f:function(){try{o||null==n.return||n.return()}finally{if(s)throw a}}}}function ft(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n0);var t=this.menu.querySelector('[data-template="active-record"]'),n=this.menu.querySelector("a > span.label");if(null!==n&&(n.innerText=0===e.length?"":e.length),0!==e.length){if(null===t)this._replaceInNode(this.menu,e[0]);else{var r=t.parentElement;r.innerHTML="";var i,a=dt(e);try{for(a.s();!(i=a.n()).done;){var o=i.value,s=t.cloneNode(!0);r.appendChild(this._replaceInNode(s,o))}}catch(e){a.e(e)}finally{a.f()}}this.getContainer().getPlugin("timesheet-duration").updateRecords()}}},{key:"_replaceInNode",value:function(e,t){var n,r=this.getContainer().getPlugin("date"),i=dt(e.querySelectorAll("[data-replacer]"));try{for(i.s();!(n=i.n()).done;){var a=n.value,o=a.dataset.replacer;"url"===o?a.href=this.attributes.href.replace("000",t.id):"activity"===o?a.innerText=t.activity.name:"project"===o?a.innerText=t.project.name:"customer"===o?a.innerText=t.project.customer.name:"duration"===o&&(a.dataset.since=t.begin,a.innerText=r.formatDuration(t.duration))}}catch(e){i.e(e)}finally{i.f()}return e}},{key:"reloadActiveRecords",value:function(){var e=this;this.getContainer().getPlugin("api").get(this.attributes.api,{},(function(t){e.setEntries(t)}))}}])&&ht(t.prototype,n),r&&ht(t,r),a}(c);n(2222);function vt(e){return(vt="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function bt(e,t){var n="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(!n){if(Array.isArray(e)||(n=function(e,t){if(!e)return;if("string"==typeof e)return wt(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);"Object"===n&&e.constructor&&(n=e.constructor.name);if("Map"===n||"Set"===n)return Array.from(e);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return wt(e,t)}(e))||t&&e&&"number"==typeof e.length){n&&(e=n);var r=0,i=function(){};return{s:i,n:function(){return r>=e.length?{done:!0}:{done:!1,value:e[r++]}},e:function(e){throw e},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,o=!0,s=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return o=e.done,e},e:function(e){s=!0,a=e},f:function(){try{o||null==n.return||n.return()}finally{if(s)throw a}}}}function wt(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n ul.menu");var n=this,r=function(){n.reloadRecentActivities()};setTimeout(r,500),document.addEventListener("kimai.recentActivities",r),document.addEventListener("kimai.timesheetUpdate",r),document.addEventListener("kimai.timesheetDelete",r),document.addEventListener("kimai.activityUpdate",r),document.addEventListener("kimai.activityDelete",r),document.addEventListener("kimai.projectUpdate",r),document.addEventListener("kimai.projectDelete",r),document.addEventListener("kimai.customerUpdate",r),document.addEventListener("kimai.customerDelete",r)}}},{key:"emptyList",value:function(){this.itemList.innerHTML=""}},{key:"setEntries",value:function(e){if(0!==e.length){var t,n="",r=bt(e);try{for(r.s();!(t=r.n()).done;){var i=t.value,a=this.attributes.template.replace("%customer%",i.project.customer.name).replace("%project%",i.project.name).replace("%activity%",i.activity.name);n+="
  • "+'')+' ').concat(a)+"
  • "}}catch(e){r.e(e)}finally{r.f()}this.itemList.innerHTML=n}else this.emptyList()}},{key:"reloadRecentActivities",value:function(){var e=this;this.getContainer().getPlugin("api").get(this.attributes.api,{},(function(t){e.setEntries(t)}))}}])&&Mt(t.prototype,n),r&&Mt(t,r),a}(c);function xt(e){return(xt="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function St(e,t){var n="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(!n){if(Array.isArray(e)||(n=function(e,t){if(!e)return;if("string"==typeof e)return jt(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);"Object"===n&&e.constructor&&(n=e.constructor.name);if("Map"===n||"Set"===n)return Array.from(e);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return jt(e,t)}(e))||t&&e&&"number"==typeof e.length){n&&(e=n);var r=0,i=function(){};return{s:i,n:function(){return r>=e.length?{done:!0}:{done:!1,value:e[r++]}},e:function(e){throw e},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,o=!0,s=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return o=e.done,e},e:function(e){s=!0,a=e},f:function(){try{o||null==n.return||n.return()}finally{if(s)throw a}}}}function jt(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n")}):Ut().fire({icon:"error",title:e.replace("%reason%",""),text:t})}},{key:"warning",value:function(e){this._show("warning",e)}},{key:"success",value:function(e){this._toast("success",e)}},{key:"info",value:function(e){this._show("info",e)}},{key:"_show",value:function(e,t){var n=this.getContainer().getTranslation();n.has(t)&&(t=n.get(t)),Ut().fire({icon:e,title:t})}},{key:"_toast",value:function(e,t){var n=this.getContainer().getTranslation();n.has(t)&&(t=n.get(t)),Ut().fire({timer:2e3,timerProgressBar:!0,toast:!0,position:"top",showConfirmButton:!1,icon:e,title:t})}},{key:"question",value:function(e,t){var n=this.getContainer().getTranslation();n.has(e)&&(e=n.get(e)),Ut().fire({title:e,icon:"question",showCancelButton:!0,confirmButtonText:n.get("confirm"),cancelButtonText:n.get("cancel")}).then((function(e){t(e.value)}))}}])&&Gt(t.prototype,n),r&&Gt(t,r),a}(c),tn=(n(6699),n(2023),n(9755));function nn(e){return(nn="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function rn(e,t){for(var n=0;n=e.length?{done:!0}:{done:!1,value:e[r++]}},e:function(e){throw e},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,o=!0,s=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return o=e.done,e},e:function(e){s=!0,a=e},f:function(){try{o||null==n.return||n.return()}finally{if(s)throw a}}}}function hn(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var n=e&&("undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"]);if(null==n)return;var r,i,a=[],o=!0,s=!1;try{for(n=n.call(e);!(o=(r=n.next()).done)&&(a.push(r.value),!t||a.length!==t);o=!0);}catch(e){s=!0,i=e}finally{try{o||null==n.return||n.return()}finally{if(s)throw i}}return a}(e,t)||mn(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function mn(e,t){if(e){if("string"==typeof e)return pn(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?pn(e,t):void 0}}function pn(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n0,i=t.term.toUpperCase().split(" "),a=n.text.toUpperCase(),o=!0,s=!1,l=[];if(i.forEach((function(e,t){a.indexOf(e)>-1?s=!0:(o=!1,l.push(e))})),o)return n;if(r){var u=x().extend(!0,{},t);u.term=s?l.join(" "):t.term;for(var c=x().extend(!0,{},n),d=n.children.length-1;d>=0;d--)null===e(u,n.children[d])&&c.children.splice(d,1);if(c.children.length>0)return c}return null}});var i=function(e){return x()(' '+e.text+"")},a=yn(yn({},r),{templateSelection:i,templateResult:i});x()(e+" "+n+":not([data-renderer=color])").select2(r),x()(e+" "+n+"[data-renderer=color]").select2(a),x()("body").on("reset","form",(function(e){setTimeout((function(){x()(e.target).find(n).trigger("change")}),10)}))}},{key:"destroySelectPicker",value:function(e){x()(e+" "+this.selector).select2("destroy")}},{key:"updateOptions",value:function(e,t){var n=x()(e),r=x()(e+' option[value=""]'),i=n.val();n.find("option").remove().end().find("optgroup").remove().end(),0!==r.length&&n.append(this._createOption(r.text(),""));for(var a=[],o=[],s=0,l=Object.entries(t);s=e.length?{done:!0}:{done:!1,value:e[r++]}},e:function(e){throw e},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,o=!0,s=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return o=e.done,e},e:function(e){s=!0,a=e},f:function(){try{o||null==n.return||n.return()}finally{if(s)throw a}}}}function Tn(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);nx()(window).height()&&(t.drops="up",t.move())})),x()(this).on("apply.daterangepicker",(function(e,t){x()(this).val(t.startDate.format(i)),x()(this).trigger("change")}))}))}},{key:"destroyDatePicker",value:function(e){x()(e+" "+this.selector).each((function(e){void 0!==x()(this).data("daterangepicker")&&(x()(this).daterangepicker("destroy"),x()(this).data("daterangepicker").remove())}))}}])&&An(t.prototype,n),r&&An(t,r),a}(c);function Fn(e){return(Fn="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function zn(e,t){for(var n=0;n0?x()("#multi_update_form").show():x()("#multi_update_form").hide()}}])&&Xn(t.prototype,n),r&&Xn(t,r),a}(c);n(9714);function ir(e){return(ir="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function ar(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function or(e,t){for(var n=0;n=e.length?{done:!0}:{done:!1,value:e[r++]}},e:function(e){throw e},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,s=!0,l=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return s=e.done,e},e:function(e){l=!0,a=e},f:function(){try{s||null==n.return||n.return()}finally{if(l)throw a}}}}function o(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n
    '),this.widget=i()(this.selector),this.href=this.widget.data("href"),this.events=this.widget.data("reload").split(" ");var n,r=this,o=function(e){var t=i()(r.selector+" .box-tools").data("page"),n=r._buildUrl(t);r.loadPage(n)},s=a(this.events);try{for(s.s();!(n=s.n()).done;){var l=n.value;document.addEventListener(l,o)}}catch(e){s.e(e)}finally{s.f()}this.widget.on("click",".box-tools ul.pagination a",(function(e){e.preventDefault(),r.loadPage(i()(e.currentTarget).attr("href"))}))}var t,n,r;return t=e,r=[{key:"create",value:function(t){return new e(t)}}],(n=[{key:"_showOverlay",value:function(){this.widget.append(this.overlay)}},{key:"_hideOverlay",value:function(){i()(this.overlay).remove()}},{key:"loadPage",value:function(e){var t=this,n=this.selector;t._showOverlay(),i().ajax({url:e,data:{},success:function(e){var r=i()(e);i()(n+" .box-tools").replaceWith(r.find(".box-tools")),i()(n+" .box-body").replaceWith(r.find(".box-body")),i()(n+" .box-title").replaceWith(r.find(".box-title")),i()(n+" .box-footer").length>0&&i()(n+" .box-footer").replaceWith(r.find(".box-footer")),i()(n+' .box-body [data-toggle="tooltip"]').tooltip(),t.widget.removeData("error-retry"),t._hideOverlay()},dataType:"html",error:function(e,r,a){if(void 0!==e.status&&500===e.status){if(void 0!==t.widget.data("error-retry"))return;var o=i()(n+" .box-tools").data("page");if(o>1){t.widget.data("error-retry",1);var s=t._buildUrl(o-1);t.loadPage(s)}}t._hideOverlay()}})}},{key:"_buildUrl",value:function(e){return this.href.replace("1",e)}}])&&s(t.prototype,n),r&&s(t,r),e}()},8227:function(e,t,n){"use strict";n.d(t,{Z:function(){return l}});n(4916),n(3123),n(9826),n(7042),n(1539),n(8309),n(1038),n(8783),n(2526),n(1817),n(2165),n(6992),n(3948);var r=n(9755),i=n.n(r);function a(e,t){var n="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(!n){if(Array.isArray(e)||(n=function(e,t){if(!e)return;if("string"==typeof e)return o(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);"Object"===n&&e.constructor&&(n=e.constructor.name);if("Map"===n||"Set"===n)return Array.from(e);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return o(e,t)}(e))||t&&e&&"number"==typeof e.length){n&&(e=n);var r=0,i=function(){};return{s:i,n:function(){return r>=e.length?{done:!0}:{done:!1,value:e[r++]}},e:function(e){throw e},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,s=!0,l=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return s=e.done,e},e:function(e){l=!0,a=e},f:function(){try{s||null==n.return||n.return()}finally{if(l)throw a}}}}function o(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n
    '),this.widget=i()("div.content-wrapper");var r,o=this,s=function(e){n?document.location.reload(!0):o.loadPage(document.location)},l=a(t.split(" "));try{for(l.s();!(r=l.n()).done;){var u=r.value;document.addEventListener(u,s)}}catch(e){l.e(e)}finally{l.f()}}var t,n,r;return t=e,r=[{key:"create",value:function(t,n){return null==n&&(n=!1),new e(t,n)}}],(n=[{key:"_showOverlay",value:function(){this.widget.append(this.overlay)}},{key:"_hideOverlay",value:function(){i()(this.overlay).remove()}},{key:"loadPage",value:function(e){var t=this;t._showOverlay(),i().ajax({url:e,data:{},success:function(e){i()("section.content").replaceWith(i()(e).find("section.content")),document.dispatchEvent(new Event("kimai.reloadPage")),t._hideOverlay()},dataType:"html",error:function(n,r,i){t._hideOverlay(),document.location=e}})}}])&&s(t.prototype,n),r&&s(t,r),e}()},6654:function(e,t,n){"use strict";function r(e,t){for(var n=0;n3)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher, but lower than version 4")}(r),function(e){"use strict";e.fn.emulateTransitionEnd=function(t){var n=!1,r=this;e(this).one("bsTransitionEnd",(function(){n=!0}));return setTimeout((function(){n||e(r).trigger(e.support.transition.end)}),t),this},e((function(){e.support.transition=function(){var e=document.createElement("bootstrap"),t={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var n in t)if(void 0!==e.style[n])return{end:t[n]};return!1}(),e.support.transition&&(e.event.special.bsTransitionEnd={bindType:e.support.transition.end,delegateType:e.support.transition.end,handle:function(t){if(e(t.target).is(this))return t.handleObj.handler.apply(this,arguments)}})}))}(r),function(e){"use strict";var t='[data-dismiss="alert"]',n=function(n){e(n).on("click",t,this.close)};n.VERSION="3.4.1",n.TRANSITION_DURATION=150,n.prototype.close=function(t){var r=e(this),i=r.attr("data-target");i||(i=(i=r.attr("href"))&&i.replace(/.*(?=#[^\s]*$)/,"")),i="#"===i?[]:i;var a=e(document).find(i);function o(){a.detach().trigger("closed.bs.alert").remove()}t&&t.preventDefault(),a.length||(a=r.closest(".alert")),a.trigger(t=e.Event("close.bs.alert")),t.isDefaultPrevented()||(a.removeClass("in"),e.support.transition&&a.hasClass("fade")?a.one("bsTransitionEnd",o).emulateTransitionEnd(n.TRANSITION_DURATION):o())};var r=e.fn.alert;e.fn.alert=function(t){return this.each((function(){var r=e(this),i=r.data("bs.alert");i||r.data("bs.alert",i=new n(this)),"string"==typeof t&&i[t].call(r)}))},e.fn.alert.Constructor=n,e.fn.alert.noConflict=function(){return e.fn.alert=r,this},e(document).on("click.bs.alert.data-api",t,n.prototype.close)}(r),function(e){"use strict";var t=function(n,r){this.$element=e(n),this.options=e.extend({},t.DEFAULTS,r),this.isLoading=!1};function n(n){return this.each((function(){var r=e(this),i=r.data("bs.button"),a="object"==typeof n&&n;i||r.data("bs.button",i=new t(this,a)),"toggle"==n?i.toggle():n&&i.setState(n)}))}t.VERSION="3.4.1",t.DEFAULTS={loadingText:"loading..."},t.prototype.setState=function(t){var n="disabled",r=this.$element,i=r.is("input")?"val":"html",a=r.data();t+="Text",null==a.resetText&&r.data("resetText",r[i]()),setTimeout(e.proxy((function(){r[i](null==a[t]?this.options[t]:a[t]),"loadingText"==t?(this.isLoading=!0,r.addClass(n).attr(n,n).prop(n,!0)):this.isLoading&&(this.isLoading=!1,r.removeClass(n).removeAttr(n).prop(n,!1))}),this),0)},t.prototype.toggle=function(){var e=!0,t=this.$element.closest('[data-toggle="buttons"]');if(t.length){var n=this.$element.find("input");"radio"==n.prop("type")?(n.prop("checked")&&(e=!1),t.find(".active").removeClass("active"),this.$element.addClass("active")):"checkbox"==n.prop("type")&&(n.prop("checked")!==this.$element.hasClass("active")&&(e=!1),this.$element.toggleClass("active")),n.prop("checked",this.$element.hasClass("active")),e&&n.trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active")),this.$element.toggleClass("active")};var r=e.fn.button;e.fn.button=n,e.fn.button.Constructor=t,e.fn.button.noConflict=function(){return e.fn.button=r,this},e(document).on("click.bs.button.data-api",'[data-toggle^="button"]',(function(t){var r=e(t.target).closest(".btn");n.call(r,"toggle"),e(t.target).is('input[type="radio"], input[type="checkbox"]')||(t.preventDefault(),r.is("input,button")?r.trigger("focus"):r.find("input:visible,button:visible").first().trigger("focus"))})).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',(function(t){e(t.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(t.type))}))}(r),function(e){"use strict";var t=function(t,n){this.$element=e(t),this.$indicators=this.$element.find(".carousel-indicators"),this.options=n,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",e.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",e.proxy(this.pause,this)).on("mouseleave.bs.carousel",e.proxy(this.cycle,this))};function n(n){return this.each((function(){var r=e(this),i=r.data("bs.carousel"),a=e.extend({},t.DEFAULTS,r.data(),"object"==typeof n&&n),o="string"==typeof n?n:a.slide;i||r.data("bs.carousel",i=new t(this,a)),"number"==typeof n?i.to(n):o?i[o]():a.interval&&i.pause().cycle()}))}t.VERSION="3.4.1",t.TRANSITION_DURATION=600,t.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},t.prototype.keydown=function(e){if(!/input|textarea/i.test(e.target.tagName)){switch(e.which){case 37:this.prev();break;case 39:this.next();break;default:return}e.preventDefault()}},t.prototype.cycle=function(t){return t||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(e.proxy(this.next,this),this.options.interval)),this},t.prototype.getItemIndex=function(e){return this.$items=e.parent().children(".item"),this.$items.index(e||this.$active)},t.prototype.getItemForDirection=function(e,t){var n=this.getItemIndex(t);if(("prev"==e&&0===n||"next"==e&&n==this.$items.length-1)&&!this.options.wrap)return t;var r=(n+("prev"==e?-1:1))%this.$items.length;return this.$items.eq(r)},t.prototype.to=function(e){var t=this,n=this.getItemIndex(this.$active=this.$element.find(".item.active"));if(!(e>this.$items.length-1||e<0))return this.sliding?this.$element.one("slid.bs.carousel",(function(){t.to(e)})):n==e?this.pause().cycle():this.slide(e>n?"next":"prev",this.$items.eq(e))},t.prototype.pause=function(t){return t||(this.paused=!0),this.$element.find(".next, .prev").length&&e.support.transition&&(this.$element.trigger(e.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},t.prototype.next=function(){if(!this.sliding)return this.slide("next")},t.prototype.prev=function(){if(!this.sliding)return this.slide("prev")},t.prototype.slide=function(n,r){var i=this.$element.find(".item.active"),a=r||this.getItemForDirection(n,i),o=this.interval,s="next"==n?"left":"right",l=this;if(a.hasClass("active"))return this.sliding=!1;var u=a[0],c=e.Event("slide.bs.carousel",{relatedTarget:u,direction:s});if(this.$element.trigger(c),!c.isDefaultPrevented()){if(this.sliding=!0,o&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var d=e(this.$indicators.children()[this.getItemIndex(a)]);d&&d.addClass("active")}var f=e.Event("slid.bs.carousel",{relatedTarget:u,direction:s});return e.support.transition&&this.$element.hasClass("slide")?(a.addClass(n),"object"==typeof a&&a.length&&a[0].offsetWidth,i.addClass(s),a.addClass(s),i.one("bsTransitionEnd",(function(){a.removeClass([n,s].join(" ")).addClass("active"),i.removeClass(["active",s].join(" ")),l.sliding=!1,setTimeout((function(){l.$element.trigger(f)}),0)})).emulateTransitionEnd(t.TRANSITION_DURATION)):(i.removeClass("active"),a.addClass("active"),this.sliding=!1,this.$element.trigger(f)),o&&this.cycle(),this}};var r=e.fn.carousel;e.fn.carousel=n,e.fn.carousel.Constructor=t,e.fn.carousel.noConflict=function(){return e.fn.carousel=r,this};var i=function(t){var r=e(this),i=r.attr("href");i&&(i=i.replace(/.*(?=#[^\s]+$)/,""));var a=r.attr("data-target")||i,o=e(document).find(a);if(o.hasClass("carousel")){var s=e.extend({},o.data(),r.data()),l=r.attr("data-slide-to");l&&(s.interval=!1),n.call(o,s),l&&o.data("bs.carousel").to(l),t.preventDefault()}};e(document).on("click.bs.carousel.data-api","[data-slide]",i).on("click.bs.carousel.data-api","[data-slide-to]",i),e(window).on("load",(function(){e('[data-ride="carousel"]').each((function(){var t=e(this);n.call(t,t.data())}))}))}(r),function(e){"use strict";var t=function(n,r){this.$element=e(n),this.options=e.extend({},t.DEFAULTS,r),this.$trigger=e('[data-toggle="collapse"][href="#'+n.id+'"],[data-toggle="collapse"][data-target="#'+n.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};function n(t){var n,r=t.attr("data-target")||(n=t.attr("href"))&&n.replace(/.*(?=#[^\s]+$)/,"");return e(document).find(r)}function r(n){return this.each((function(){var r=e(this),i=r.data("bs.collapse"),a=e.extend({},t.DEFAULTS,r.data(),"object"==typeof n&&n);!i&&a.toggle&&/show|hide/.test(n)&&(a.toggle=!1),i||r.data("bs.collapse",i=new t(this,a)),"string"==typeof n&&i[n]()}))}t.VERSION="3.4.1",t.TRANSITION_DURATION=350,t.DEFAULTS={toggle:!0},t.prototype.dimension=function(){return this.$element.hasClass("width")?"width":"height"},t.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var n,i=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(i&&i.length&&(n=i.data("bs.collapse"))&&n.transitioning)){var a=e.Event("show.bs.collapse");if(this.$element.trigger(a),!a.isDefaultPrevented()){i&&i.length&&(r.call(i,"hide"),n||i.data("bs.collapse",null));var o=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[o](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var s=function(){this.$element.removeClass("collapsing").addClass("collapse in")[o](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!e.support.transition)return s.call(this);var l=e.camelCase(["scroll",o].join("-"));this.$element.one("bsTransitionEnd",e.proxy(s,this)).emulateTransitionEnd(t.TRANSITION_DURATION)[o](this.$element[0][l])}}}},t.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var n=e.Event("hide.bs.collapse");if(this.$element.trigger(n),!n.isDefaultPrevented()){var r=this.dimension();this.$element[r](this.$element[r]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var i=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};if(!e.support.transition)return i.call(this);this.$element[r](0).one("bsTransitionEnd",e.proxy(i,this)).emulateTransitionEnd(t.TRANSITION_DURATION)}}},t.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},t.prototype.getParent=function(){return e(document).find(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(e.proxy((function(t,r){var i=e(r);this.addAriaAndCollapsedClass(n(i),i)}),this)).end()},t.prototype.addAriaAndCollapsedClass=function(e,t){var n=e.hasClass("in");e.attr("aria-expanded",n),t.toggleClass("collapsed",!n).attr("aria-expanded",n)};var i=e.fn.collapse;e.fn.collapse=r,e.fn.collapse.Constructor=t,e.fn.collapse.noConflict=function(){return e.fn.collapse=i,this},e(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',(function(t){var i=e(this);i.attr("data-target")||t.preventDefault();var a=n(i),o=a.data("bs.collapse")?"toggle":i.data();r.call(a,o)}))}(r),function(e){"use strict";var t='[data-toggle="dropdown"]',n=function(t){e(t).on("click.bs.dropdown",this.toggle)};function r(t){var n=t.attr("data-target");n||(n=(n=t.attr("href"))&&/#[A-Za-z]/.test(n)&&n.replace(/.*(?=#[^\s]*$)/,""));var r="#"!==n?e(document).find(n):null;return r&&r.length?r:t.parent()}function i(n){n&&3===n.which||(e(".dropdown-backdrop").remove(),e(t).each((function(){var t=e(this),i=r(t),a={relatedTarget:this};i.hasClass("open")&&(n&&"click"==n.type&&/input|textarea/i.test(n.target.tagName)&&e.contains(i[0],n.target)||(i.trigger(n=e.Event("hide.bs.dropdown",a)),n.isDefaultPrevented()||(t.attr("aria-expanded","false"),i.removeClass("open").trigger(e.Event("hidden.bs.dropdown",a)))))})))}n.VERSION="3.4.1",n.prototype.toggle=function(t){var n=e(this);if(!n.is(".disabled, :disabled")){var a=r(n),o=a.hasClass("open");if(i(),!o){"ontouchstart"in document.documentElement&&!a.closest(".navbar-nav").length&&e(document.createElement("div")).addClass("dropdown-backdrop").insertAfter(e(this)).on("click",i);var s={relatedTarget:this};if(a.trigger(t=e.Event("show.bs.dropdown",s)),t.isDefaultPrevented())return;n.trigger("focus").attr("aria-expanded","true"),a.toggleClass("open").trigger(e.Event("shown.bs.dropdown",s))}return!1}},n.prototype.keydown=function(n){if(/(38|40|27|32)/.test(n.which)&&!/input|textarea/i.test(n.target.tagName)){var i=e(this);if(n.preventDefault(),n.stopPropagation(),!i.is(".disabled, :disabled")){var a=r(i),o=a.hasClass("open");if(!o&&27!=n.which||o&&27==n.which)return 27==n.which&&a.find(t).trigger("focus"),i.trigger("click");var s=a.find(".dropdown-menu li:not(.disabled):visible a");if(s.length){var l=s.index(n.target);38==n.which&&l>0&&l--,40==n.which&&ldocument.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&e?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!e?this.scrollbarWidth:""})},t.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},t.prototype.checkScrollbar=function(){var e=window.innerWidth;if(!e){var t=document.documentElement.getBoundingClientRect();e=t.right-Math.abs(t.left)}this.bodyIsOverflowing=document.body.clientWidth
    ',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0},sanitize:!0,sanitizeFn:null,whiteList:r},l.prototype.init=function(t,n,r){if(this.enabled=!0,this.type=t,this.$element=e(n),this.options=this.getOptions(r),this.$viewport=this.options.viewport&&e(document).find(e.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var i=this.options.trigger.split(" "),a=i.length;a--;){var o=i[a];if("click"==o)this.$element.on("click."+this.type,this.options.selector,e.proxy(this.toggle,this));else if("manual"!=o){var s="hover"==o?"mouseenter":"focusin",l="hover"==o?"mouseleave":"focusout";this.$element.on(s+"."+this.type,this.options.selector,e.proxy(this.enter,this)),this.$element.on(l+"."+this.type,this.options.selector,e.proxy(this.leave,this))}}this.options.selector?this._options=e.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},l.prototype.getDefaults=function(){return l.DEFAULTS},l.prototype.getOptions=function(n){var r=this.$element.data();for(var i in r)r.hasOwnProperty(i)&&-1!==e.inArray(i,t)&&delete r[i];return(n=e.extend({},this.getDefaults(),r,n)).delay&&"number"==typeof n.delay&&(n.delay={show:n.delay,hide:n.delay}),n.sanitize&&(n.template=s(n.template,n.whiteList,n.sanitizeFn)),n},l.prototype.getDelegateOptions=function(){var t={},n=this.getDefaults();return this._options&&e.each(this._options,(function(e,r){n[e]!=r&&(t[e]=r)})),t},l.prototype.enter=function(t){var n=t instanceof this.constructor?t:e(t.currentTarget).data("bs."+this.type);if(n||(n=new this.constructor(t.currentTarget,this.getDelegateOptions()),e(t.currentTarget).data("bs."+this.type,n)),t instanceof e.Event&&(n.inState["focusin"==t.type?"focus":"hover"]=!0),n.tip().hasClass("in")||"in"==n.hoverState)n.hoverState="in";else{if(clearTimeout(n.timeout),n.hoverState="in",!n.options.delay||!n.options.delay.show)return n.show();n.timeout=setTimeout((function(){"in"==n.hoverState&&n.show()}),n.options.delay.show)}},l.prototype.isInStateTrue=function(){for(var e in this.inState)if(this.inState[e])return!0;return!1},l.prototype.leave=function(t){var n=t instanceof this.constructor?t:e(t.currentTarget).data("bs."+this.type);if(n||(n=new this.constructor(t.currentTarget,this.getDelegateOptions()),e(t.currentTarget).data("bs."+this.type,n)),t instanceof e.Event&&(n.inState["focusout"==t.type?"focus":"hover"]=!1),!n.isInStateTrue()){if(clearTimeout(n.timeout),n.hoverState="out",!n.options.delay||!n.options.delay.hide)return n.hide();n.timeout=setTimeout((function(){"out"==n.hoverState&&n.hide()}),n.options.delay.hide)}},l.prototype.show=function(){var t=e.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(t);var n=e.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(t.isDefaultPrevented()||!n)return;var r=this,i=this.tip(),a=this.getUID(this.type);this.setContent(),i.attr("id",a),this.$element.attr("aria-describedby",a),this.options.animation&&i.addClass("fade");var o="function"==typeof this.options.placement?this.options.placement.call(this,i[0],this.$element[0]):this.options.placement,s=/\s?auto?\s?/i,u=s.test(o);u&&(o=o.replace(s,"")||"top"),i.detach().css({top:0,left:0,display:"block"}).addClass(o).data("bs."+this.type,this),this.options.container?i.appendTo(e(document).find(this.options.container)):i.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var c=this.getPosition(),d=i[0].offsetWidth,f=i[0].offsetHeight;if(u){var h=o,m=this.getPosition(this.$viewport);o="bottom"==o&&c.bottom+f>m.bottom?"top":"top"==o&&c.top-fm.width?"left":"left"==o&&c.left-do.top+o.height&&(i.top=o.top+o.height-l)}else{var u=t.left-a,c=t.left+a+n;uo.right&&(i.left=o.left+o.width-c)}return i},l.prototype.getTitle=function(){var e=this.$element,t=this.options;return e.attr("data-original-title")||("function"==typeof t.title?t.title.call(e[0]):t.title)},l.prototype.getUID=function(e){do{e+=~~(1e6*Math.random())}while(document.getElementById(e));return e},l.prototype.tip=function(){if(!this.$tip&&(this.$tip=e(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},l.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},l.prototype.enable=function(){this.enabled=!0},l.prototype.disable=function(){this.enabled=!1},l.prototype.toggleEnabled=function(){this.enabled=!this.enabled},l.prototype.toggle=function(t){var n=this;t&&((n=e(t.currentTarget).data("bs."+this.type))||(n=new this.constructor(t.currentTarget,this.getDelegateOptions()),e(t.currentTarget).data("bs."+this.type,n))),t?(n.inState.click=!n.inState.click,n.isInStateTrue()?n.enter(n):n.leave(n)):n.tip().hasClass("in")?n.leave(n):n.enter(n)},l.prototype.destroy=function(){var e=this;clearTimeout(this.timeout),this.hide((function(){e.$element.off("."+e.type).removeData("bs."+e.type),e.$tip&&e.$tip.detach(),e.$tip=null,e.$arrow=null,e.$viewport=null,e.$element=null}))},l.prototype.sanitizeHtml=function(e){return s(e,this.options.whiteList,this.options.sanitizeFn)};var u=e.fn.tooltip;e.fn.tooltip=function(t){return this.each((function(){var n=e(this),r=n.data("bs.tooltip"),i="object"==typeof t&&t;!r&&/destroy|hide/.test(t)||(r||n.data("bs.tooltip",r=new l(this,i)),"string"==typeof t&&r[t]())}))},e.fn.tooltip.Constructor=l,e.fn.tooltip.noConflict=function(){return e.fn.tooltip=u,this}}(r),function(e){"use strict";var t=function(e,t){this.init("popover",e,t)};if(!e.fn.tooltip)throw new Error("Popover requires tooltip.js");t.VERSION="3.4.1",t.DEFAULTS=e.extend({},e.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),(t.prototype=e.extend({},e.fn.tooltip.Constructor.prototype)).constructor=t,t.prototype.getDefaults=function(){return t.DEFAULTS},t.prototype.setContent=function(){var e=this.tip(),t=this.getTitle(),n=this.getContent();if(this.options.html){var r=typeof n;this.options.sanitize&&(t=this.sanitizeHtml(t),"string"===r&&(n=this.sanitizeHtml(n))),e.find(".popover-title").html(t),e.find(".popover-content").children().detach().end()["string"===r?"html":"append"](n)}else e.find(".popover-title").text(t),e.find(".popover-content").children().detach().end().text(n);e.removeClass("fade top bottom left right in"),e.find(".popover-title").html()||e.find(".popover-title").hide()},t.prototype.hasContent=function(){return this.getTitle()||this.getContent()},t.prototype.getContent=function(){var e=this.$element,t=this.options;return e.attr("data-content")||("function"==typeof t.content?t.content.call(e[0]):t.content)},t.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var n=e.fn.popover;e.fn.popover=function(n){return this.each((function(){var r=e(this),i=r.data("bs.popover"),a="object"==typeof n&&n;!i&&/destroy|hide/.test(n)||(i||r.data("bs.popover",i=new t(this,a)),"string"==typeof n&&i[n]())}))},e.fn.popover.Constructor=t,e.fn.popover.noConflict=function(){return e.fn.popover=n,this}}(r),function(e){"use strict";function t(n,r){this.$body=e(document.body),this.$scrollElement=e(n).is(document.body)?e(window):e(n),this.options=e.extend({},t.DEFAULTS,r),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",e.proxy(this.process,this)),this.refresh(),this.process()}function n(n){return this.each((function(){var r=e(this),i=r.data("bs.scrollspy"),a="object"==typeof n&&n;i||r.data("bs.scrollspy",i=new t(this,a)),"string"==typeof n&&i[n]()}))}t.VERSION="3.4.1",t.DEFAULTS={offset:10},t.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},t.prototype.refresh=function(){var t=this,n="offset",r=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),e.isWindow(this.$scrollElement[0])||(n="position",r=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map((function(){var t=e(this),i=t.data("target")||t.attr("href"),a=/^#./.test(i)&&e(i);return a&&a.length&&a.is(":visible")&&[[a[n]().top+r,i]]||null})).sort((function(e,t){return e[0]-t[0]})).each((function(){t.offsets.push(this[0]),t.targets.push(this[1])}))},t.prototype.process=function(){var e,t=this.$scrollElement.scrollTop()+this.options.offset,n=this.getScrollHeight(),r=this.options.offset+n-this.$scrollElement.height(),i=this.offsets,a=this.targets,o=this.activeTarget;if(this.scrollHeight!=n&&this.refresh(),t>=r)return o!=(e=a[a.length-1])&&this.activate(e);if(o&&t=i[e]&&(void 0===i[e+1]||t .active"),o=i&&e.support.transition&&(a.length&&a.hasClass("fade")||!!r.find("> .fade").length);function s(){a.removeClass("active").find("> .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),n.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),o?(n[0].offsetWidth,n.addClass("in")):n.removeClass("fade"),n.parent(".dropdown-menu").length&&n.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),i&&i()}a.length&&o?a.one("bsTransitionEnd",s).emulateTransitionEnd(t.TRANSITION_DURATION):s(),a.removeClass("in")};var r=e.fn.tab;e.fn.tab=n,e.fn.tab.Constructor=t,e.fn.tab.noConflict=function(){return e.fn.tab=r,this};var i=function(t){t.preventDefault(),n.call(e(this),"show")};e(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',i).on("click.bs.tab.data-api",'[data-toggle="pill"]',i)}(r),function(e){"use strict";var t=function(n,r){this.options=e.extend({},t.DEFAULTS,r);var i=this.options.target===t.DEFAULTS.target?e(this.options.target):e(document).find(this.options.target);this.$target=i.on("scroll.bs.affix.data-api",e.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",e.proxy(this.checkPositionWithEventLoop,this)),this.$element=e(n),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};function n(n){return this.each((function(){var r=e(this),i=r.data("bs.affix"),a="object"==typeof n&&n;i||r.data("bs.affix",i=new t(this,a)),"string"==typeof n&&i[n]()}))}t.VERSION="3.4.1",t.RESET="affix affix-top affix-bottom",t.DEFAULTS={offset:0,target:window},t.prototype.getState=function(e,t,n,r){var i=this.$target.scrollTop(),a=this.$element.offset(),o=this.$target.height();if(null!=n&&"top"==this.affixed)return i=e-r&&"bottom"},t.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(t.RESET).addClass("affix");var e=this.$target.scrollTop(),n=this.$element.offset();return this.pinnedOffset=n.top-e},t.prototype.checkPositionWithEventLoop=function(){setTimeout(e.proxy(this.checkPosition,this),1)},t.prototype.checkPosition=function(){if(this.$element.is(":visible")){var n=this.$element.height(),r=this.options.offset,i=r.top,a=r.bottom,o=Math.max(e(document).height(),e(document.body).height());"object"!=typeof r&&(a=i=r),"function"==typeof i&&(i=r.top(this.$element)),"function"==typeof a&&(a=r.bottom(this.$element));var s=this.getState(o,n,i,a);if(this.affixed!=s){null!=this.unpin&&this.$element.css("top","");var l="affix"+(s?"-"+s:""),u=e.Event(l+".bs.affix");if(this.$element.trigger(u),u.isDefaultPrevented())return;this.affixed=s,this.unpin="bottom"==s?this.getPinnedOffset():null,this.$element.removeClass(t.RESET).addClass(l).trigger(l.replace("affix","affixed")+".bs.affix")}"bottom"==s&&this.$element.offset({top:o-n-a})}};var r=e.fn.affix;e.fn.affix=n,e.fn.affix.Constructor=t,e.fn.affix.noConflict=function(){return e.fn.affix=r,this},e(window).on("load",(function(){e('[data-spy="affix"]').each((function(){var t=e(this),r=t.data();r.offset=r.offset||{},null!=r.offsetBottom&&(r.offset.bottom=r.offsetBottom),null!=r.offsetTop&&(r.offset.top=r.offsetTop),n.call(t,r)}))}))}(r)},3099:function(e){e.exports=function(e){if("function"!=typeof e)throw TypeError(String(e)+" is not a function");return e}},6077:function(e,t,n){var r=n(111);e.exports=function(e){if(!r(e)&&null!==e)throw TypeError("Can't set "+String(e)+" as a prototype");return e}},1223:function(e,t,n){var r=n(5112),i=n(30),a=n(3070),o=r("unscopables"),s=Array.prototype;null==s[o]&&a.f(s,o,{configurable:!0,value:i(null)}),e.exports=function(e){s[o][e]=!0}},1530:function(e,t,n){"use strict";var r=n(8710).charAt;e.exports=function(e,t,n){return t+(n?r(e,t).length:1)}},9670:function(e,t,n){var r=n(111);e.exports=function(e){if(!r(e))throw TypeError(String(e)+" is not an object");return e}},8533:function(e,t,n){"use strict";var r=n(2092).forEach,i=n(9341)("forEach");e.exports=i?[].forEach:function(e){return r(this,e,arguments.length>1?arguments[1]:void 0)}},8457:function(e,t,n){"use strict";var r=n(9974),i=n(7908),a=n(3411),o=n(7659),s=n(7466),l=n(7493),u=n(1246);e.exports=function(e){var t,n,c,d,f,h,m=i(e),p="function"==typeof this?this:Array,_=arguments.length,y=_>1?arguments[1]:void 0,g=void 0!==y,v=u(m),b=0;if(g&&(y=r(y,_>2?arguments[2]:void 0,2)),null==v||p==Array&&o(v))for(n=new p(t=s(m.length));t>b;b++)h=g?y(m[b],b):m[b],l(n,b,h);else for(f=(d=v.call(m)).next,n=new p;!(c=f.call(d)).done;b++)h=g?a(d,y,[c.value,b],!0):c.value,l(n,b,h);return n.length=b,n}},1318:function(e,t,n){var r=n(5656),i=n(7466),a=n(1400),o=function(e){return function(t,n,o){var s,l=r(t),u=i(l.length),c=a(o,u);if(e&&n!=n){for(;u>c;)if((s=l[c++])!=s)return!0}else for(;u>c;c++)if((e||c in l)&&l[c]===n)return e||c||0;return!e&&-1}};e.exports={includes:o(!0),indexOf:o(!1)}},2092:function(e,t,n){var r=n(9974),i=n(8361),a=n(7908),o=n(7466),s=n(5417),l=[].push,u=function(e){var t=1==e,n=2==e,u=3==e,c=4==e,d=6==e,f=7==e,h=5==e||d;return function(m,p,_,y){for(var g,v,b=a(m),w=i(b),M=r(p,_,3),L=o(w.length),k=0,Y=y||s,D=t?Y(m,L):n||f?Y(m,0):void 0;L>k;k++)if((h||k in w)&&(v=M(g=w[k],k,b),e))if(t)D[k]=v;else if(v)switch(e){case 3:return!0;case 5:return g;case 6:return k;case 2:l.call(D,g)}else switch(e){case 4:return!1;case 7:l.call(D,g)}return d?-1:u||c?c:D}};e.exports={forEach:u(0),map:u(1),filter:u(2),some:u(3),every:u(4),find:u(5),findIndex:u(6),filterOut:u(7)}},1194:function(e,t,n){var r=n(7293),i=n(5112),a=n(7392),o=i("species");e.exports=function(e){return a>=51||!r((function(){var t=[];return(t.constructor={})[o]=function(){return{foo:1}},1!==t[e](Boolean).foo}))}},9341:function(e,t,n){"use strict";var r=n(7293);e.exports=function(e,t){var n=[][e];return!!n&&r((function(){n.call(null,t||function(){throw 1},1)}))}},5417:function(e,t,n){var r=n(111),i=n(3157),a=n(5112)("species");e.exports=function(e,t){var n;return i(e)&&("function"!=typeof(n=e.constructor)||n!==Array&&!i(n.prototype)?r(n)&&null===(n=n[a])&&(n=void 0):n=void 0),new(void 0===n?Array:n)(0===t?0:t)}},3411:function(e,t,n){var r=n(9670),i=n(9212);e.exports=function(e,t,n,a){try{return a?t(r(n)[0],n[1]):t(n)}catch(t){throw i(e),t}}},7072:function(e,t,n){var r=n(5112)("iterator"),i=!1;try{var a=0,o={next:function(){return{done:!!a++}},return:function(){i=!0}};o[r]=function(){return this},Array.from(o,(function(){throw 2}))}catch(e){}e.exports=function(e,t){if(!t&&!i)return!1;var n=!1;try{var a={};a[r]=function(){return{next:function(){return{done:n=!0}}}},e(a)}catch(e){}return n}},4326:function(e){var t={}.toString;e.exports=function(e){return t.call(e).slice(8,-1)}},648:function(e,t,n){var r=n(1694),i=n(4326),a=n(5112)("toStringTag"),o="Arguments"==i(function(){return arguments}());e.exports=r?i:function(e){var t,n,r;return void 0===e?"Undefined":null===e?"Null":"string"==typeof(n=function(e,t){try{return e[t]}catch(e){}}(t=Object(e),a))?n:o?i(t):"Object"==(r=i(t))&&"function"==typeof t.callee?"Arguments":r}},9920:function(e,t,n){var r=n(6656),i=n(3887),a=n(1236),o=n(3070);e.exports=function(e,t){for(var n=i(t),s=o.f,l=a.f,u=0;u=74)&&(r=o.match(/Chrome\/(\d+)/))&&(i=r[1]),e.exports=i&&+i},748:function(e){e.exports=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"]},2109:function(e,t,n){var r=n(7854),i=n(1236).f,a=n(8880),o=n(1320),s=n(3505),l=n(9920),u=n(4705);e.exports=function(e,t){var n,c,d,f,h,m=e.target,p=e.global,_=e.stat;if(n=p?r:_?r[m]||s(m,{}):(r[m]||{}).prototype)for(c in t){if(f=t[c],d=e.noTargetGet?(h=i(n,c))&&h.value:n[c],!u(p?c:m+(_?".":"#")+c,e.forced)&&void 0!==d){if(typeof f==typeof d)continue;l(f,d)}(e.sham||d&&d.sham)&&a(f,"sham",!0),o(n,c,f,e)}}},7293:function(e){e.exports=function(e){try{return!!e()}catch(e){return!0}}},7007:function(e,t,n){"use strict";n(4916);var r=n(1320),i=n(7293),a=n(5112),o=n(8880),s=a("species"),l=!i((function(){var e=/./;return e.exec=function(){var e=[];return e.groups={a:"7"},e},"7"!=="".replace(e,"$")})),u="$0"==="a".replace(/./,"$0"),c=a("replace"),d=!!/./[c]&&""===/./[c]("a","$0"),f=!i((function(){var e=/(?:)/,t=e.exec;e.exec=function(){return t.apply(this,arguments)};var n="ab".split(e);return 2!==n.length||"a"!==n[0]||"b"!==n[1]}));e.exports=function(e,t,n,c){var h=a(e),m=!i((function(){var t={};return t[h]=function(){return 7},7!=""[e](t)})),p=m&&!i((function(){var t=!1,n=/a/;return"split"===e&&((n={}).constructor={},n.constructor[s]=function(){return n},n.flags="",n[h]=/./[h]),n.exec=function(){return t=!0,null},n[h](""),!t}));if(!m||!p||"replace"===e&&(!l||!u||d)||"split"===e&&!f){var _=/./[h],y=n(h,""[e],(function(e,t,n,r,i){return t.exec===RegExp.prototype.exec?m&&!i?{done:!0,value:_.call(t,n,r)}:{done:!0,value:e.call(n,t,r)}:{done:!1}}),{REPLACE_KEEPS_$0:u,REGEXP_REPLACE_SUBSTITUTES_UNDEFINED_CAPTURE:d}),g=y[0],v=y[1];r(String.prototype,e,g),r(RegExp.prototype,h,2==t?function(e,t){return v.call(e,this,t)}:function(e){return v.call(e,this)})}c&&o(RegExp.prototype[h],"sham",!0)}},9974:function(e,t,n){var r=n(3099);e.exports=function(e,t,n){if(r(e),void 0===t)return e;switch(n){case 0:return function(){return e.call(t)};case 1:return function(n){return e.call(t,n)};case 2:return function(n,r){return e.call(t,n,r)};case 3:return function(n,r,i){return e.call(t,n,r,i)}}return function(){return e.apply(t,arguments)}}},7065:function(e,t,n){"use strict";var r=n(3099),i=n(111),a=[].slice,o={},s=function(e,t,n){if(!(t in o)){for(var r=[],i=0;i]*>)/g,s=/\$([$&'`]|\d{1,2})/g;e.exports=function(e,t,n,l,u,c){var d=n+e.length,f=l.length,h=s;return void 0!==u&&(u=r(u),h=o),a.call(c,h,(function(r,a){var o;switch(a.charAt(0)){case"$":return"$";case"&":return e;case"`":return t.slice(0,n);case"'":return t.slice(d);case"<":o=u[a.slice(1,-1)];break;default:var s=+a;if(0===s)return r;if(s>f){var c=i(s/10);return 0===c?r:c<=f?void 0===l[c-1]?a.charAt(1):l[c-1]+a.charAt(1):r}o=l[s-1]}return void 0===o?"":o}))}},7854:function(e,t,n){var r=function(e){return e&&e.Math==Math&&e};e.exports=r("object"==typeof globalThis&&globalThis)||r("object"==typeof window&&window)||r("object"==typeof self&&self)||r("object"==typeof n.g&&n.g)||function(){return this}()||Function("return this")()},6656:function(e,t,n){var r=n(7908),i={}.hasOwnProperty;e.exports=function(e,t){return i.call(r(e),t)}},3501:function(e){e.exports={}},1003:function(e,t,n){var r=n(5005);e.exports=r("document","documentElement")},4664:function(e,t,n){var r=n(9781),i=n(7293),a=n(317);e.exports=!r&&!i((function(){return 7!=Object.defineProperty(a("div"),"a",{get:function(){return 7}}).a}))},8361:function(e,t,n){var r=n(7293),i=n(4326),a="".split;e.exports=r((function(){return!Object("z").propertyIsEnumerable(0)}))?function(e){return"String"==i(e)?a.call(e,""):Object(e)}:Object},2788:function(e,t,n){var r=n(5465),i=Function.toString;"function"!=typeof r.inspectSource&&(r.inspectSource=function(e){return i.call(e)}),e.exports=r.inspectSource},9909:function(e,t,n){var r,i,a,o=n(8536),s=n(7854),l=n(111),u=n(8880),c=n(6656),d=n(5465),f=n(6200),h=n(3501),m="Object already initialized",p=s.WeakMap;if(o){var _=d.state||(d.state=new p),y=_.get,g=_.has,v=_.set;r=function(e,t){if(g.call(_,e))throw new TypeError(m);return t.facade=e,v.call(_,e,t),t},i=function(e){return y.call(_,e)||{}},a=function(e){return g.call(_,e)}}else{var b=f("state");h[b]=!0,r=function(e,t){if(c(e,b))throw new TypeError(m);return t.facade=e,u(e,b,t),t},i=function(e){return c(e,b)?e[b]:{}},a=function(e){return c(e,b)}}e.exports={set:r,get:i,has:a,enforce:function(e){return a(e)?i(e):r(e,{})},getterFor:function(e){return function(t){var n;if(!l(t)||(n=i(t)).type!==e)throw TypeError("Incompatible receiver, "+e+" required");return n}}}},7659:function(e,t,n){var r=n(5112),i=n(7497),a=r("iterator"),o=Array.prototype;e.exports=function(e){return void 0!==e&&(i.Array===e||o[a]===e)}},3157:function(e,t,n){var r=n(4326);e.exports=Array.isArray||function(e){return"Array"==r(e)}},4705:function(e,t,n){var r=n(7293),i=/#|\.prototype\./,a=function(e,t){var n=s[o(e)];return n==u||n!=l&&("function"==typeof t?r(t):!!t)},o=a.normalize=function(e){return String(e).replace(i,".").toLowerCase()},s=a.data={},l=a.NATIVE="N",u=a.POLYFILL="P";e.exports=a},111:function(e){e.exports=function(e){return"object"==typeof e?null!==e:"function"==typeof e}},1913:function(e){e.exports=!1},7850:function(e,t,n){var r=n(111),i=n(4326),a=n(5112)("match");e.exports=function(e){var t;return r(e)&&(void 0!==(t=e[a])?!!t:"RegExp"==i(e))}},9212:function(e,t,n){var r=n(9670);e.exports=function(e){var t=e.return;if(void 0!==t)return r(t.call(e)).value}},3383:function(e,t,n){"use strict";var r,i,a,o=n(7293),s=n(9518),l=n(8880),u=n(6656),c=n(5112),d=n(1913),f=c("iterator"),h=!1;[].keys&&("next"in(a=[].keys())?(i=s(s(a)))!==Object.prototype&&(r=i):h=!0);var m=null==r||o((function(){var e={};return r[f].call(e)!==e}));m&&(r={}),d&&!m||u(r,f)||l(r,f,(function(){return this})),e.exports={IteratorPrototype:r,BUGGY_SAFARI_ITERATORS:h}},7497:function(e){e.exports={}},133:function(e,t,n){var r=n(5268),i=n(7392),a=n(7293);e.exports=!!Object.getOwnPropertySymbols&&!a((function(){return!Symbol.sham&&(r?38===i:i>37&&i<41)}))},8536:function(e,t,n){var r=n(7854),i=n(2788),a=r.WeakMap;e.exports="function"==typeof a&&/native code/.test(i(a))},3929:function(e,t,n){var r=n(7850);e.exports=function(e){if(r(e))throw TypeError("The method doesn't accept regular expressions");return e}},30:function(e,t,n){var r,i=n(9670),a=n(6048),o=n(748),s=n(3501),l=n(1003),u=n(317),c=n(6200),d=c("IE_PROTO"),f=function(){},h=function(e){return" + {% endblock %} +{% endembed %} \ No newline at end of file diff --git a/templates/user/stats.html.twig b/templates/user/stats.html.twig index b43722fe..77e0f8c6 100644 --- a/templates/user/stats.html.twig +++ b/templates/user/stats.html.twig @@ -48,7 +48,7 @@ {% endembed %} {% endfor %} - {% if user.teams is not empty and (is_granted('teams', user) or (app.user == user and is_granted('view_team_member'))) %} + {% if user.teams is not empty and (is_granted('teams', user) or is_granted('view_team_member', user)) %} {% embed '@AdminLTE/Widgets/box-widget.html.twig' %} {% import "macros/widgets.html.twig" as widgets %} {% block box_title %}{{ 'label.my_teams'|trans }}{% endblock %} diff --git a/templates/user/teams.html.twig b/templates/user/teams.html.twig deleted file mode 100644 index 6db897ab..00000000 --- a/templates/user/teams.html.twig +++ /dev/null @@ -1,54 +0,0 @@ -{% extends 'user/layout.html.twig' %} -{% import "macros/widgets.html.twig" as widgets %} - -{% block profile_content %} - -
    -
    - -
    -
    -
    -
    -
    - - - - - - - - - - {% for team in user.teams %} - - - - - - {% endfor %} - -
    {{ 'label.team'|trans }}{{ 'label.teamlead'|trans }}{{ 'label.user'|trans }}
    - {{ team.name }} - - {{ widgets.user_avatar(team.teamlead) }} - {# - {{ widgets.label(team.teamlead.displayName, 'success', 'label.teamlead'|trans) }} - #} - - {% for user in team.users %} - {{ widgets.user_avatar(user) }} - {# - {{ widgets.label_user(user) }} - #} - {% endfor %} -
    -
    -
    -
    -
    -
    - -
    -
    -{% endblock %} diff --git a/tests/API/APIControllerBaseTest.php b/tests/API/APIControllerBaseTest.php index 704abbcd..7ae3d94d 100644 --- a/tests/API/APIControllerBaseTest.php +++ b/tests/API/APIControllerBaseTest.php @@ -235,6 +235,8 @@ abstract class APIControllerBaseTest extends ControllerBaseTest self::assertArrayHasKey('children', $result['errors']); $data = $result['errors']['children']; + $foundErrors = []; + foreach ($failedFields as $key => $value) { $messages = []; $fieldName = $value; @@ -245,17 +247,25 @@ abstract class APIControllerBaseTest extends ControllerBaseTest $messages = [$value]; } } + + while (stripos($fieldName, '.') !== false) { + $parts = explode('.', $fieldName); + $tmp = array_shift($parts); + self::assertArrayHasKey($tmp, $data, sprintf('Could not find field "%s" in result', $tmp)); + $data = $data[$tmp]; + if (\count($data) === 1 && \array_key_exists('children', $data)) { + $data = $data['children']; + } + $fieldName = implode('.', $parts); + } + self::assertArrayHasKey($fieldName, $data, sprintf('Could not find validation error for field "%s" in list: %s', $fieldName, implode(', ', $failedFields))); self::assertArrayHasKey('errors', $data[$fieldName], sprintf('Field %s has no validation problem', $fieldName)); foreach ($messages as $i => $message) { self::assertEquals($message, $data[$fieldName]['errors'][$i]); } - } - - $foundErrors = []; - foreach ($data as $fieldName => $field) { - if (\array_key_exists('errors', $field) && \count($field['errors']) > 0) { - $foundErrors[$fieldName] = \count($field['errors']); + if (\array_key_exists('errors', $data[$fieldName]) && \count($data[$fieldName]['errors']) > 0) { + $foundErrors[$fieldName] = \count($data[$fieldName]['errors']); } } @@ -310,6 +320,7 @@ abstract class APIControllerBaseTest extends ControllerBaseTest 'language' => 'string', 'timezone' => 'string', 'accountNumber' => '@string', + 'memberships' => ['result' => 'array', 'type' => 'TeamMembership'], ]; // if a team is embedded @@ -328,13 +339,28 @@ abstract class APIControllerBaseTest extends ControllerBaseTest 'id' => 'int', 'name' => 'string', 'color' => '@string', - 'teamlead' => ['result' => 'object', 'type' => 'User'], + 'teamlead' => ['result' => 'object', 'type' => '@User'], + 'members' => ['result' => 'array', 'type' => 'TeamMember'], 'users' => ['result' => 'array', 'type' => 'User'], 'customers' => ['result' => 'array', 'type' => '@Customer'], 'projects' => ['result' => 'array', 'type' => '@Project'], 'activities' => ['result' => 'array', 'type' => '@Activity'], ]; + // if the team is used inside the team context + case 'TeamMember': + return [ + 'user' => ['result' => 'object', 'type' => 'User'], + 'teamlead' => 'bool', + ]; + + // if the team is used inside the user context + case 'TeamMembership': + return [ + 'team' => ['result' => 'object', 'type' => 'Team'], + 'teamlead' => 'bool', + ]; + // if a customer is embedded in other objects case 'Customer': return [ diff --git a/tests/API/CustomerControllerTest.php b/tests/API/CustomerControllerTest.php index 350e7f8d..64c5a72f 100644 --- a/tests/API/CustomerControllerTest.php +++ b/tests/API/CustomerControllerTest.php @@ -160,7 +160,7 @@ class CustomerControllerTest extends APIControllerBaseTest // and finally a team $team = new Team(); $team->setName('Testing customer 1 team'); - $team->setTeamLead($this->getUserByRole(User::ROLE_USER)); + $team->addTeamlead($this->getUserByRole(User::ROLE_USER)); $team->addCustomer($customer); $team->addProject($project); $team->addUser($this->getUserByRole(User::ROLE_TEAMLEAD)); diff --git a/tests/API/ProjectControllerTest.php b/tests/API/ProjectControllerTest.php index 3e09903b..092399e8 100644 --- a/tests/API/ProjectControllerTest.php +++ b/tests/API/ProjectControllerTest.php @@ -144,7 +144,7 @@ class ProjectControllerTest extends APIControllerBaseTest // and a team $team = new Team(); $team->setName('Testing project team'); - $team->setTeamLead($this->getUserByRole(User::ROLE_USER)); + $team->addTeamlead($this->getUserByRole(User::ROLE_USER)); $team->addCustomer($customer); $team->addProject($project); $team->addUser($this->getUserByRole(User::ROLE_TEAMLEAD)); diff --git a/tests/API/TeamControllerTest.php b/tests/API/TeamControllerTest.php index 6379e657..ed6b1a45 100644 --- a/tests/API/TeamControllerTest.php +++ b/tests/API/TeamControllerTest.php @@ -92,7 +92,9 @@ class TeamControllerTest extends APIControllerBaseTest $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $data = [ 'name' => 'foo', - 'teamlead' => 1, + 'members' => [ + ['user' => 1, 'teamlead' => 1] + ], ]; $this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->assertTrue($client->getResponse()->isSuccessful()); @@ -123,39 +125,62 @@ class TeamControllerTest extends APIControllerBaseTest $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $data = [ 'name' => '', - 'teamlead' => 9999, + 'members' => [ + ['user' => 9999, 'teamlead' => 1] + ], ]; $this->request($client, '/api/teams', 'POST', [], json_encode($data)); $response = $client->getResponse(); $this->assertEquals(400, $response->getStatusCode()); - $this->assertApiCallValidationError($response, ['name', 'teamlead']); + $this->assertApiCallValidationError($response, ['name', 'members.0.user']); } public function testPatchAction() { $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); + $data = [ 'name' => 'foo', - 'teamlead' => 1, + 'members' => [ + ['user' => 1, 'teamlead' => true], + ['user' => 5, 'teamlead' => true], + ] ]; $this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->assertTrue($client->getResponse()->isSuccessful()); $result = json_decode($client->getResponse()->getContent(), true); + $updateId = $result['id']; $data = [ 'name' => 'foo', - 'teamlead' => 2, - 'users' => [1, 5, 4] + 'members' => [ + ['user' => 2, 'teamlead' => true], + ['user' => 1, 'teamlead' => false], + ['user' => 4, 'teamlead' => true], + ] ]; - $this->request($client, '/api/teams/' . $result['id'], 'PATCH', [], json_encode($data)); + + $this->request($client, '/api/teams/' . $updateId, 'PATCH', [], json_encode($data)); $this->assertTrue($client->getResponse()->isSuccessful()); $result = json_decode($client->getResponse()->getContent(), true); $this->assertIsArray($result); self::assertApiResponseTypeStructure('TeamEntity', $result); $this->assertNotEmpty($result['id']); - self::assertCount(4, $result['users']); + self::assertCount(3, $result['users']); + + self::assertFalse($result['members'][1]['teamlead']); + self::assertEquals(1, $result['members'][1]['user']['id']); + self::assertEquals('clara_customer', $result['members'][1]['user']['username']); + + self::assertTrue($result['members'][2]['teamlead']); + self::assertEquals(4, $result['members'][2]['user']['id']); + self::assertEquals('tony_teamlead', $result['members'][2]['user']['username']); + + self::assertTrue(true, $result['members'][0]['teamlead']); + self::assertEquals(2, $result['members'][0]['user']['id']); + self::assertEquals('john_user', $result['members'][0]['user']['username']); } public function testPatchActionWithValidationErrors() @@ -163,7 +188,9 @@ class TeamControllerTest extends APIControllerBaseTest $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $data = [ 'name' => 'foo', - 'teamlead' => 1, + 'members' => [ + ['user' => 1, 'teamlead' => 1], + ], ]; $this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->assertTrue($client->getResponse()->isSuccessful()); @@ -171,14 +198,15 @@ class TeamControllerTest extends APIControllerBaseTest $data = [ 'name' => '1', - 'teamlead' => 9999, - 'users' => [9999] + 'members' => [ + ['user' => 9999, 'teamlead' => 1], + ], ]; $this->request($client, '/api/teams/' . $result['id'], 'PATCH', [], json_encode($data)); $response = $client->getResponse(); $this->assertEquals(400, $response->getStatusCode()); - $this->assertApiCallValidationError($response, ['name', 'teamlead', 'users']); + $this->assertApiCallValidationError($response, ['name', 'members.0.user']); } public function testDeleteAction() @@ -205,7 +233,9 @@ class TeamControllerTest extends APIControllerBaseTest $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $data = [ 'name' => 'foo', - 'teamlead' => 1, + 'members' => [ + ['user' => 1, 'teamlead' => 1] + ], ]; $this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->assertTrue($client->getResponse()->isSuccessful()); @@ -226,8 +256,10 @@ class TeamControllerTest extends APIControllerBaseTest $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $data = [ 'name' => 'foo', - 'teamlead' => 1, - 'users' => [2] + 'members' => [ + ['user' => 1, 'teamlead' => 1], + ['user' => 2, 'teamlead' => 0], + ], ]; $this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->assertTrue($client->getResponse()->isSuccessful()); @@ -261,8 +293,12 @@ class TeamControllerTest extends APIControllerBaseTest $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $data = [ 'name' => 'foo', - 'teamlead' => 1, - 'users' => [2, 4, 5] + 'members' => [ + ['user' => 1, 'teamlead' => 1], + ['user' => 2, 'teamlead' => 0], + ['user' => 4, 'teamlead' => 0], + ['user' => 5, 'teamlead' => 0], + ], ]; $this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->assertTrue($client->getResponse()->isSuccessful()); @@ -283,8 +319,12 @@ class TeamControllerTest extends APIControllerBaseTest $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $data = [ 'name' => 'foo', - 'teamlead' => 1, - 'users' => [2, 4, 5] + 'members' => [ + ['user' => 1, 'teamlead' => 1], + ['user' => 2, 'teamlead' => 0], + ['user' => 4, 'teamlead' => 0], + ['user' => 5, 'teamlead' => 0], + ], ]; $this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->assertTrue($client->getResponse()->isSuccessful()); @@ -324,7 +364,9 @@ class TeamControllerTest extends APIControllerBaseTest $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $data = [ 'name' => 'foo', - 'teamlead' => 1, + 'members' => [ + ['user' => 1, 'teamlead' => 1], + ], ]; $this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->assertTrue($client->getResponse()->isSuccessful()); @@ -346,8 +388,10 @@ class TeamControllerTest extends APIControllerBaseTest $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $data = [ 'name' => 'foo', - 'teamlead' => 1, - 'users' => [2] + 'members' => [ + ['user' => 1, 'teamlead' => 1], + ['user' => 2, 'teamlead' => 0], + ], ]; $this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->assertTrue($client->getResponse()->isSuccessful()); @@ -383,8 +427,12 @@ class TeamControllerTest extends APIControllerBaseTest $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $data = [ 'name' => 'foo', - 'teamlead' => 1, - 'users' => [2, 4, 5] + 'members' => [ + ['user' => 1, 'teamlead' => 1], + ['user' => 2, 'teamlead' => 0], + ['user' => 4, 'teamlead' => 0], + ['user' => 5, 'teamlead' => 0], + ], ]; $this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->assertTrue($client->getResponse()->isSuccessful()); @@ -411,8 +459,12 @@ class TeamControllerTest extends APIControllerBaseTest $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $data = [ 'name' => 'foo', - 'teamlead' => 1, - 'users' => [2, 4, 5] + 'members' => [ + ['user' => 1, 'teamlead' => 1], + ['user' => 2, 'teamlead' => 0], + ['user' => 4, 'teamlead' => 0], + ['user' => 5, 'teamlead' => 0], + ], ]; $this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->assertTrue($client->getResponse()->isSuccessful()); @@ -442,7 +494,9 @@ class TeamControllerTest extends APIControllerBaseTest $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $data = [ 'name' => 'foo', - 'teamlead' => 1, + 'members' => [ + ['user' => 1, 'teamlead' => 1], + ], ]; $this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->assertTrue($client->getResponse()->isSuccessful()); @@ -463,8 +517,10 @@ class TeamControllerTest extends APIControllerBaseTest $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $data = [ 'name' => 'foo', - 'teamlead' => 1, - 'users' => [2] + 'members' => [ + ['user' => 1, 'teamlead' => 1], + ['user' => 2, 'teamlead' => 0], + ], ]; $this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->assertTrue($client->getResponse()->isSuccessful()); @@ -500,8 +556,12 @@ class TeamControllerTest extends APIControllerBaseTest $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $data = [ 'name' => 'foo', - 'teamlead' => 1, - 'users' => [2, 4, 5] + 'members' => [ + ['user' => 1, 'teamlead' => 1], + ['user' => 2, 'teamlead' => 0], + ['user' => 4, 'teamlead' => 0], + ['user' => 5, 'teamlead' => 0], + ], ]; $this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->assertTrue($client->getResponse()->isSuccessful()); @@ -528,8 +588,12 @@ class TeamControllerTest extends APIControllerBaseTest $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $data = [ 'name' => 'foo', - 'teamlead' => 1, - 'users' => [2, 4, 5] + 'members' => [ + ['user' => 1, 'teamlead' => 1], + ['user' => 2, 'teamlead' => 0], + ['user' => 4, 'teamlead' => 0], + ['user' => 5, 'teamlead' => 0], + ], ]; $this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->assertTrue($client->getResponse()->isSuccessful()); @@ -559,7 +623,9 @@ class TeamControllerTest extends APIControllerBaseTest $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $data = [ 'name' => 'foo', - 'teamlead' => 1, + 'members' => [ + ['user' => 1, 'teamlead' => 1], + ], ]; $this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->assertTrue($client->getResponse()->isSuccessful()); @@ -580,8 +646,10 @@ class TeamControllerTest extends APIControllerBaseTest $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $data = [ 'name' => 'foo', - 'teamlead' => 1, - 'users' => [2] + 'members' => [ + ['user' => 1, 'teamlead' => 1], + ['user' => 2, 'teamlead' => 0], + ], ]; $this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->assertTrue($client->getResponse()->isSuccessful()); @@ -617,8 +685,12 @@ class TeamControllerTest extends APIControllerBaseTest $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $data = [ 'name' => 'foo', - 'teamlead' => 1, - 'users' => [2, 4, 5] + 'members' => [ + ['user' => 1, 'teamlead' => 1], + ['user' => 2, 'teamlead' => 0], + ['user' => 4, 'teamlead' => 0], + ['user' => 5, 'teamlead' => 0], + ], ]; $this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->assertTrue($client->getResponse()->isSuccessful()); @@ -645,8 +717,12 @@ class TeamControllerTest extends APIControllerBaseTest $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $data = [ 'name' => 'foo', - 'teamlead' => 1, - 'users' => [2, 4, 5] + 'members' => [ + ['user' => 1, 'teamlead' => 1], + ['user' => 2, 'teamlead' => 0], + ['user' => 4, 'teamlead' => 0], + ['user' => 5, 'teamlead' => 0], + ], ]; $this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->assertTrue($client->getResponse()->isSuccessful()); diff --git a/tests/Command/DemoteUserCommandTest.php b/tests/Command/DemoteUserCommandTest.php index 68f59608..669478ee 100644 --- a/tests/Command/DemoteUserCommandTest.php +++ b/tests/Command/DemoteUserCommandTest.php @@ -90,7 +90,7 @@ class DemoteUserCommandTest extends KernelTestCase $userRepository = $container->get('doctrine')->getRepository(User::class); $user = $userRepository->loadUserByUsername('tony_teamlead'); self::assertInstanceOf(User::class, $user); - self::assertFalse($user->isTeamlead()); + self::assertFalse($user->hasTeamleadRole()); } public function testDemoteSuper() diff --git a/tests/Command/PromoteUserCommandTest.php b/tests/Command/PromoteUserCommandTest.php index 9746c4c4..1f251a05 100644 --- a/tests/Command/PromoteUserCommandTest.php +++ b/tests/Command/PromoteUserCommandTest.php @@ -90,7 +90,7 @@ class PromoteUserCommandTest extends KernelTestCase $userRepository = $container->get('doctrine')->getRepository(User::class); $user = $userRepository->loadUserByUsername('john_user'); self::assertInstanceOf(User::class, $user); - self::assertTrue($user->isTeamlead()); + self::assertTrue($user->hasTeamleadRole()); } public function testPromoteSuper() diff --git a/tests/Controller/ControllerBaseTest.php b/tests/Controller/ControllerBaseTest.php index ce92dfeb..ffeff392 100644 --- a/tests/Controller/ControllerBaseTest.php +++ b/tests/Controller/ControllerBaseTest.php @@ -364,11 +364,7 @@ abstract class ControllerBaseTest extends WebTestCase */ protected function assertHasFlashSuccess(HttpKernelBrowser $client, string $message = null) { - $content = $client->getResponse()->getContent(); - self::assertStringContainsString('ALERT.success(\'', $content, 'Could not find flash success message'); - if (null !== $message) { - self::assertStringContainsString($message, $content); - } + $this->assertHasFlashMessage($client, 'success', $message); } /** @@ -376,10 +372,18 @@ abstract class ControllerBaseTest extends WebTestCase * @param string|null $message */ protected function assertHasFlashError(HttpKernelBrowser $client, string $message = null) + { + $this->assertHasFlashMessage($client, 'error', $message); + } + + private function assertHasFlashMessage(HttpKernelBrowser $client, string $type, string $message = null) { $content = $client->getResponse()->getContent(); - self::assertStringContainsString('ALERT.error(\'', $content, 'Could not find flash error message'); + self::assertStringContainsString('ALERT.' . $type . '(\'', $content, 'Could not find flash ' . $type . ' message'); if (null !== $message) { + // this is a lazy workaround, the templates use the javascript escape filter: |e('js') + // if you ever want to test more complex strings, this logic has to be enhanced + $message = str_replace([' ', ':'], ['\u0020', '\u003A'], $message); self::assertStringContainsString($message, $content); } } diff --git a/tests/Controller/ExportControllerTest.php b/tests/Controller/ExportControllerTest.php index b71a9cdd..b0932c80 100644 --- a/tests/Controller/ExportControllerTest.php +++ b/tests/Controller/ExportControllerTest.php @@ -50,7 +50,7 @@ class ExportControllerTest extends ControllerBaseTest /** @var Team $team */ $team = new Team(); $team->setName('fooo'); - $team->setTeamLead($teamlead); + $team->addTeamlead($teamlead); $team->addUser($user); $em->persist($team); $em->persist($user); diff --git a/tests/Controller/ProfileControllerTest.php b/tests/Controller/ProfileControllerTest.php index ae2e9c90..3425bdb8 100644 --- a/tests/Controller/ProfileControllerTest.php +++ b/tests/Controller/ProfileControllerTest.php @@ -355,7 +355,6 @@ class ProfileControllerTest extends ControllerBaseTest public function testTeamsAction() { $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN); - $em = $this->getEntityManager(); /** @var User $user */ $user = $this->getUserByRole(User::ROLE_USER); @@ -371,7 +370,7 @@ class ProfileControllerTest extends ControllerBaseTest /** @var User $user */ $user = $this->getUserByRole(User::ROLE_USER); - $this->assertEquals([], $user->getTeams()->toArray()); + $this->assertEquals([], $user->getTeams()); $form = $client->getCrawler()->filter('form[name=user_teams]')->form(); /** @var ChoiceFormField $team */ @@ -386,10 +385,9 @@ class ProfileControllerTest extends ControllerBaseTest $this->assertHasFlashSuccess($client); - $em = $this->getEntityManager(); $user = $this->getUserByRole(User::ROLE_USER); - $this->assertEquals(1, $user->getTeams()->count()); + $this->assertCount(1, $user->getTeams()); } public function getPreferencesTestData() diff --git a/tests/Controller/ProjectControllerTest.php b/tests/Controller/ProjectControllerTest.php index 2e76b516..a631058f 100644 --- a/tests/Controller/ProjectControllerTest.php +++ b/tests/Controller/ProjectControllerTest.php @@ -197,7 +197,7 @@ class ProjectControllerTest extends ControllerBaseTest $project->setEnd(new \DateTime()); $em->persist($project); $team = new Team(); - $team->setTeamLead($this->getUserByRole(User::ROLE_ADMIN)); + $team->addTeamlead($this->getUserByRole(User::ROLE_ADMIN)); $team->addProject($project); $team->setName('project 1'); $em->persist($team); diff --git a/tests/Controller/TeamControllerTest.php b/tests/Controller/TeamControllerTest.php index 12f4bcf0..7632d79f 100644 --- a/tests/Controller/TeamControllerTest.php +++ b/tests/Controller/TeamControllerTest.php @@ -79,13 +79,13 @@ class TeamControllerTest extends ControllerBaseTest $form = $client->getCrawler()->filter('form[name=team_edit_form]')->form(); $this->assertEquals('', $form->get('team_edit_form[name]')->getValue()); - $this->assertEquals('5', $form->get('team_edit_form[teamlead]')->getValue()); - $client->submit($form, [ - 'team_edit_form' => [ - 'name' => 'Test Team' . uniqid(), - ] - ]); + $values = $form->getPhpValues(); + $values['team_edit_form']['name'] = 'Test Team' . uniqid(); + $values['team_edit_form']['members'][0]['user'] = 5; + $values['team_edit_form']['members'][0]['teamlead'] = 1; + $client->request($form->getMethod(), $form->getUri(), $values, $form->getPhpFiles()); + $this->assertIsRedirect($client, '/edit'); $client->followRedirect(); $this->assertHasFlashSuccess($client); @@ -105,14 +105,13 @@ class TeamControllerTest extends ControllerBaseTest { $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); - $em = $this->getEntityManager(); $fixture = new TeamFixtures(); $fixture->setAmount(2); $this->importFixture($fixture); $this->assertAccessIsGranted($client, '/admin/teams/1/edit'); $form = $client->getCrawler()->filter('form[name=team_edit_form]')->form(); - $this->assertNotEmpty($form->get('team_edit_form[name]')->getValue()); + $client->submit($form, [ 'team_edit_form' => [ 'name' => 'Test Team 2' @@ -135,7 +134,6 @@ class TeamControllerTest extends ControllerBaseTest $this->assertAccessIsGranted($client, '/admin/teams/1/edit_member'); $form = $client->getCrawler()->filter('form[name=team_edit_form]')->form(); - $this->assertNotEmpty($form->get('team_edit_form[name]')->getValue()); $client->submit($form, [ 'team_edit_form' => [ 'name' => 'Test Team 2' diff --git a/tests/DataFixtures/TeamFixtures.php b/tests/DataFixtures/TeamFixtures.php index c7c665cd..3bd62599 100644 --- a/tests/DataFixtures/TeamFixtures.php +++ b/tests/DataFixtures/TeamFixtures.php @@ -11,9 +11,9 @@ namespace App\Tests\DataFixtures; use App\Entity\Customer; use App\Entity\Team; +use App\Entity\TeamMember; use App\Entity\User; use Doctrine\Persistence\ObjectManager; -use Faker\Factory; /** * Defines the sample data to load in during controller tests. @@ -95,7 +95,6 @@ final class TeamFixtures implements TestFixture { $created = []; - $faker = Factory::create(); $user = $this->getAllUsers($manager); $customer = $this->getAllCustomers($manager); @@ -108,10 +107,14 @@ final class TeamFixtures implements TestFixture } } + $member = new TeamMember(); + $member->setUser($lead); + $member->setTeamlead(true); + $team = new Team(); $team - ->setName($faker->name) - ->setTeamLead($lead) + ->setName('Testing: ' . uniqid()) + ->addMember($member) ; if ($this->addUser) { diff --git a/tests/Entity/TeamTest.php b/tests/Entity/TeamTest.php index 0bbf93a9..99298114 100644 --- a/tests/Entity/TeamTest.php +++ b/tests/Entity/TeamTest.php @@ -14,6 +14,7 @@ use App\Entity\Activity; use App\Entity\Customer; use App\Entity\Project; use App\Entity\Team; +use App\Entity\TeamMember; use App\Entity\User; use Doctrine\Common\Collections\Collection; use PHPUnit\Framework\TestCase; @@ -28,9 +29,10 @@ class TeamTest extends TestCase $sut = new Team(); self::assertNull($sut->getId()); self::assertNull($sut->getName()); - self::assertNull($sut->getTeamLead()); - self::assertInstanceOf(Collection::class, $sut->getUsers()); - self::assertEquals(0, $sut->getUsers()->count()); + self::assertFalse($sut->hasUsers()); + self::assertFalse($sut->hasTeamleads()); + self::assertIsArray($sut->getTeamleads()); + self::assertEmpty($sut->getTeamleads()); self::assertInstanceOf(Collection::class, $sut->getCustomers()); self::assertEquals(0, $sut->getCustomers()->count()); self::assertInstanceOf(Collection::class, $sut->getProjects()); @@ -54,6 +56,63 @@ class TeamTest extends TestCase self::assertTrue($sut->hasColor()); } + public function testTeamMemberships() + { + $user = new User(); + $user2 = new User(); + + $member = new TeamMember(); + $member->setUser($user); + + $member2 = new TeamMember(); + $member2->setUser($user); + $member2->setTeam(new Team()); + + $sut = new Team(); + self::assertFalse($sut->isTeamlead($user)); + self::assertCount(0, $sut->getTeamleads()); + self::assertCount(0, $sut->getMembers()); + self::assertFalse($sut->hasMember($member)); + $sut->removeMember($member); + $sut->removeMember($member2); + $sut->addMember($member); + self::assertCount(1, $sut->getMembers()); + $sut->removeMember($member2); + self::assertCount(1, $sut->getMembers()); + $sut->removeMember($member); + self::assertCount(0, $sut->getMembers()); + self::assertFalse($sut->isTeamlead($user)); + + $sut->addMember($member); + $member->setTeamlead(true); + self::assertTrue($sut->isTeamlead($user)); + self::assertCount(1, $sut->getMembers()); + $sut->addTeamlead($user2); + self::assertCount(2, $sut->getMembers()); + self::assertTrue($sut->isTeamlead($user2)); + $sut->removeTeamlead($user2); + self::assertCount(2, $sut->getMembers()); + self::assertFalse($sut->isTeamlead($user2)); + + $member21 = new TeamMember(); + $member21->setUser($user); + + self::assertNull($member21->getTeam()); + // this will not actually add it + $sut->addMember($member21); + self::assertSame($sut, $member21->getTeam()); + self::assertCount(2, $sut->getMembers()); + } + + public function testTeamMembershipsException() + { + $this->expectException(\InvalidArgumentException::class); + $sut = new Team(); + $member = new TeamMember(); + $member->setTeam(new Team()); + $sut->addMember($member); + } + public function testSetterAndGetter() { $sut = new Team(); @@ -61,12 +120,51 @@ class TeamTest extends TestCase self::assertEquals('foo-bar', $sut->getName()); self::assertEquals('foo-bar', (string) $sut); - $user = (new User())->setAlias('Foo!'); - self::assertInstanceOf(Team::class, $sut->setTeamLead($user)); - self::assertSame($user, $sut->getTeamLead()); + $user = $this->createMock(User::class); + $user->method('getId')->willReturn(99); + $user->method('getAlias')->willReturn('Foo!'); + $sut->addTeamlead($user); + self::assertSame($user, $sut->getTeamleads()[0]); + self::assertTrue($sut->hasTeamleads()); + + $user1 = $this->createMock(User::class); + $user1->method('getId')->willReturn(1); + $user2 = $this->createMock(User::class); + $user2->method('getId')->willReturn(2); self::assertFalse($sut->isTeamlead(new User())); self::assertTrue($sut->isTeamlead($user)); + self::assertCount(1, $sut->getTeamleads()); + $sut->addTeamlead($user1); + self::assertCount(2, $sut->getTeamleads()); + self::assertCount(2, $sut->getMembers()); + $sut->addUser($user2); + self::assertCount(3, $sut->getMembers()); + self::assertCount(2, $sut->getTeamleads()); + $sut->addUser($user2); + self::assertCount(3, $sut->getMembers()); + self::assertCount(2, $sut->getTeamleads()); + } + + /** + * @group legacy + */ + public function testSetterAndGetterDeprecated() + { + $sut = new Team(); + + self::assertNull($sut->getTeamlead()); + self::assertIsArray($sut->getUsers()); + self::assertEmpty($sut->getUsers()); + + $user = (new User())->setAlias('Foo!'); + self::assertCount(0, $sut->getUsers()); + $sut->setTeamlead($user); + self::assertSame($user, $sut->getTeamlead()); + self::assertTrue($sut->hasTeamleads()); + self::assertCount(1, $sut->getUsers()); + self::assertCount(1, $sut->getTeamleads()); + self::assertCount(1, $sut->getMembers()); } public function testCustomer() @@ -137,15 +235,69 @@ class TeamTest extends TestCase $sut = new Team(); $sut->addUser($user); - self::assertEquals(1, $sut->getUsers()->count()); - $actual = $sut->getUsers()[0]; + self::assertCount(1, $sut->getUsers()); + + $users = $sut->getUsers(); + $actual = $users[0]; self::assertSame($actual, $user); - self::assertSame($sut, $user->getTeams()[0]); + + $teams = $user->getTeams(); + self::assertSame($sut, $teams[0]); self::assertFalse($sut->hasUser(new User())); self::assertTrue($sut->hasUser($user)); $sut->removeUser(new User()); - self::assertEquals(1, $sut->getUsers()->count()); + self::assertCount(1, $sut->getUsers()); $sut->removeUser($user); - self::assertEquals(0, $sut->getUsers()->count()); + self::assertCount(0, $sut->getUsers()); + $sut->addTeamlead(new User()); + self::assertCount(1, $sut->getUsers()); + } + + public function testClone() + { + $c = new Customer(); + $c->setName('Foo'); + $p = new Project(); + $p->setName('Bar'); + $a = new Activity(); + $a->setName('Hello'); + $u = new User(); + $u->setAlias('World'); + $member = new TeamMember(); + $member->setUser(new User()); + + $team = new Team(); + $team->addCustomer($c); + $team->addCustomer(new Customer()); + $team->addProject($p); + $team->addProject(new Project()); + $team->addActivity($a); + $team->addActivity(new Activity()); + + $team->addTeamlead($u); + $team->addMember($member); + $team->addUser(new User()); + + $reflection = new \ReflectionClass($team); + $property = $reflection->getProperty('id'); + $property->setAccessible(true); + $property->setValue($team, 99); + $property->setAccessible(false); + self::assertEquals(99, $team->getId()); + + $sut = clone $team; + self::assertNull($sut->getId()); + self::assertCount(2, $sut->getCustomers()); + self::assertCount(2, $sut->getProjects()); + self::assertCount(2, $sut->getActivities()); + self::assertCount(1, $sut->getTeamleads()); + self::assertCount(3, $sut->getMembers()); + self::assertCount(3, $sut->getUsers()); + self::assertSame($c, $sut->getCustomers()[0]); + self::assertSame($p, $sut->getProjects()[0]); + self::assertSame($a, $sut->getActivities()[0]); + self::assertSame($u, $sut->getTeamleads()[0]); + self::assertSame($u, $sut->getMembers()[0]->getUser()); + self::assertSame($member->getUser(), $sut->getUsers()[1]); } } diff --git a/tests/Entity/UserTest.php b/tests/Entity/UserTest.php index 8639e2d8..820b0002 100644 --- a/tests/Entity/UserTest.php +++ b/tests/Entity/UserTest.php @@ -11,6 +11,7 @@ namespace App\Tests\Entity; use App\Constants; use App\Entity\Team; +use App\Entity\TeamMember; use App\Entity\User; use App\Entity\UserPreference; use App\Export\Spreadsheet\ColumnDefinition; @@ -198,7 +199,11 @@ class UserTest extends TestCase self::assertEmpty($sut->getTeams()); self::assertEmpty($team->getUsers()); - $sut->addTeam($team); + $member1 = new TeamMember(); + $member1->setUser($sut); + $member1->setTeam($team); + + $sut->addMembership($member1); self::assertCount(1, $sut->getTeams()); self::assertSame($team, $sut->getTeams()[0]); self::assertSame($sut, $team->getUsers()[0]); @@ -218,12 +223,13 @@ class UserTest extends TestCase self::assertTrue($sut->isTeamleadOf($team2)); self::assertTrue($sut->isInTeam($team2)); - $sut->removeTeam(new Team()); self::assertCount(2, $sut->getTeams()); - $sut->removeTeam($team); + $sut->removeMembership(new TeamMember()); + self::assertCount(2, $sut->getTeams()); + $sut->removeMembership($member1); self::assertCount(1, $sut->getTeams()); self::assertTrue($sut->hasTeamAssignment()); - $sut->removeTeam($team2); + $team2->removeUser($sut); self::assertCount(0, $sut->getTeams()); self::assertFalse($sut->hasTeamAssignment()); } @@ -241,7 +247,7 @@ class UserTest extends TestCase self::assertFalse($sut->isTeamlead()); $sut->addRole(User::ROLE_TEAMLEAD); - self::assertTrue($sut->isTeamlead()); + self::assertTrue($sut->hasTeamleadRole()); self::assertFalse($sut->canSeeAllData()); $sut->removeRole(User::ROLE_ADMIN); @@ -256,7 +262,7 @@ class UserTest extends TestCase $sut->removeRole(User::ROLE_SUPER_ADMIN); self::assertFalse($sut->canSeeAllData()); self::assertFalse($sut->isSuperAdmin()); - self::assertTrue($sut->isTeamlead()); + self::assertTrue($sut->hasTeamleadRole()); $sut->setSuperAdmin(true); self::assertTrue($sut->isSuperAdmin()); @@ -405,12 +411,74 @@ class UserTest extends TestCase $unserialized = unserialize($data); $actual = [ - $sut->getUsername(), - $sut->isEnabled(), - $sut->getId(), - $sut->getEmail(), + $unserialized->getUsername(), + $unserialized->isEnabled(), + $unserialized->getId(), + $unserialized->getEmail(), ]; self::assertEquals($expected, $actual); } + + public function testTeamMemberships() + { + $team = new Team(); + $team->setName('Foo'); + + $member = new TeamMember(); + $member->setTeam($team); + + $member2 = new TeamMember(); + $member2->setUser(new User()); + $member2->setTeam($team); + + $sut = new User(); + self::assertFalse($sut->isTeamleadOf($team)); + self::assertFalse($sut->isTeamlead()); + self::assertCount(0, $sut->getMemberships()); + self::assertFalse($sut->hasMembership($member)); + $sut->removeMembership($member); + $sut->removeMembership($member2); + $sut->addMembership($member); + self::assertCount(1, $sut->getMemberships()); + $sut->removeMembership($member2); + self::assertCount(1, $sut->getMemberships()); + $sut->removeMembership($member); + self::assertCount(0, $sut->getMemberships()); + + self::assertFalse($sut->isTeamleadOf($team)); + + $sut->addMembership($member); + self::assertCount(1, $sut->getMemberships()); + self::assertFalse($sut->isTeamleadOf($team)); + + $member->setTeamlead(true); + self::assertTrue($sut->isTeamleadOf($team)); + + $member21 = new TeamMember(); + $member21->setTeam($team); + + self::assertNull($member21->getUser()); + // this will not be added, because $team is already assigned + $sut->addMembership($member21); + + self::assertCount(1, $sut->getMemberships()); + self::assertSame($sut, $member21->getUser()); + + $sut->addTeam(new Team()); + self::assertCount(2, $sut->getTeams()); + self::assertCount(2, $sut->getMemberships()); + + $sut->removeTeam($team); + self::assertCount(1, $sut->getMemberships()); + } + + public function testTeamMembershipsException() + { + $this->expectException(\InvalidArgumentException::class); + $sut = new User(); + $member = new TeamMember(); + $member->setUser(new User()); + $sut->addMembership($member); + } } diff --git a/tests/Repository/Loader/AbstractLoaderTest.php b/tests/Repository/Loader/AbstractLoaderTest.php index 61bbf003..44568773 100644 --- a/tests/Repository/Loader/AbstractLoaderTest.php +++ b/tests/Repository/Loader/AbstractLoaderTest.php @@ -17,13 +17,18 @@ use PHPUnit\Framework\TestCase; abstract class AbstractLoaderTest extends TestCase { - protected function getEntityManagerMock(int $createQueryBuilderCount) + protected function getEntityManagerMock(int $createQueryBuilderCount, $resultMock = null) { $em = $this->createMock(EntityManager::class); $qb = $this->createMock(QueryBuilder::class); + $query = $this->createMock(AbstractQuery::class); $expr = $this->createMock(Expr::class); + if ($resultMock !== null) { + $query->expects($this->any())->method('execute')->willReturn($resultMock); + } + $expr->expects($this->any())->method('isNotNull')->willReturn(''); $expr->expects($this->any())->method('in')->willReturn(''); diff --git a/tests/Repository/Loader/ProjectLoaderTest.php b/tests/Repository/Loader/ProjectLoaderTest.php index d7507958..bc5de181 100644 --- a/tests/Repository/Loader/ProjectLoaderTest.php +++ b/tests/Repository/Loader/ProjectLoaderTest.php @@ -9,6 +9,7 @@ namespace App\Tests\Repository\Loader; +use App\Entity\Customer; use App\Entity\Project; use App\Repository\Loader\ProjectLoader; @@ -20,12 +21,18 @@ class ProjectLoaderTest extends AbstractLoaderTest { public function testLoadResults() { - $em = $this->getEntityManagerMock(4); - - $sut = new ProjectLoader($em); + $customer = $this->createMock(Customer::class); + $customer->expects($this->once())->method('getId')->willReturn(13); $entity = $this->createMock(Project::class); $entity->expects($this->once())->method('getId')->willReturn(1); + $entity->expects($this->once())->method('getCustomer')->willReturn($customer); + + $results = [$entity]; + + $em = $this->getEntityManagerMock(4, $results); + + $sut = new ProjectLoader($em); $sut->loadResults([$entity]); } diff --git a/tests/Repository/Query/UserQueryTest.php b/tests/Repository/Query/UserQueryTest.php index d7eff860..001e29ee 100644 --- a/tests/Repository/Query/UserQueryTest.php +++ b/tests/Repository/Query/UserQueryTest.php @@ -9,6 +9,7 @@ namespace App\Tests\Repository\Query; +use App\Entity\Team; use App\Repository\Query\UserQuery; use App\Repository\Query\VisibilityInterface; @@ -23,6 +24,7 @@ class UserQueryTest extends BaseQueryTest $this->assertBaseQuery($sut, 'username'); $this->assertInstanceOf(VisibilityInterface::class, $sut); $this->assertRole($sut); + $this->assertSearchTeam($sut); $this->assertResetByFormError(new UserQuery(), 'username'); } @@ -33,4 +35,14 @@ class UserQueryTest extends BaseQueryTest $sut->setRole('ROLE_USER'); $this->assertEquals('ROLE_USER', $sut->getRole()); } + + protected function assertSearchTeam(UserQuery $sut) + { + $team = new Team(); + $this->assertIsArray($sut->getSearchTeams()); + $this->assertEmpty($sut->getSearchTeams()); + $sut->setSearchTeams([$team, new Team()]); + $this->assertCount(2, $sut->getSearchTeams()); + $this->assertSame($team, $sut->getSearchTeams()[0]); + } } diff --git a/tests/Validator/Constraints/TeamValidatorTest.php b/tests/Validator/Constraints/TeamValidatorTest.php new file mode 100644 index 00000000..c223b7d7 --- /dev/null +++ b/tests/Validator/Constraints/TeamValidatorTest.php @@ -0,0 +1,54 @@ +expectException(UnexpectedTypeException::class); + + $this->validator->validate('foo', new NotBlank()); + } + + public function testMissingTeamlead() + { + $member = new TeamMember(); + $member->setTeamlead(false); + $member->setUser(new User()); + + $team = new Team(); + $team->addMember($member); + + $this->validator->validate($team, new TeamConstraint()); + + $this->buildViolation('At least one team leader must be assigned to the team.') + ->atPath('property.path.teamleads') + ->setCode(TeamConstraint::MISSING_TEAMLEAD) + ->assertRaised(); + } +} diff --git a/tests/Voter/AbstractVoterTest.php b/tests/Voter/AbstractVoterTest.php index 60e5147c..854f0165 100644 --- a/tests/Voter/AbstractVoterTest.php +++ b/tests/Voter/AbstractVoterTest.php @@ -75,10 +75,10 @@ abstract class AbstractVoterTest extends TestCase $rateOther = ['view_rate_other_timesheet', 'edit_rate_other_timesheet']; $teams = ['view_team', 'create_team', 'edit_team', 'delete_team']; - $roleUser = ['edit_team_activity', 'edit_team_project', 'edit_team_customer']; - $roleTeamlead = ['view_rate_own_timesheet', 'view_rate_other_timesheet', 'hourly-rate_own_profile']; - $roleAdmin = ['hourly-rate_own_profile', 'edit_exported_timesheet']; - $roleSuperAdmin = ['hourly-rate_own_profile', 'hourly-rate_other_profile', 'roles_own_profile', 'system_information', 'system_configuration', 'plugins', 'edit_exported_timesheet']; + $roleUser = ['view_team_member', 'edit_team_activity', 'edit_team_project', 'edit_team_customer']; + $roleTeamlead = ['view_team_member', 'view_rate_own_timesheet', 'view_rate_other_timesheet', 'hourly-rate_own_profile']; + $roleAdmin = ['view_team_member', 'hourly-rate_own_profile', 'edit_exported_timesheet']; + $roleSuperAdmin = ['view_team_member', 'hourly-rate_own_profile', 'hourly-rate_other_profile', 'roles_own_profile', 'system_information', 'system_configuration', 'plugins', 'edit_exported_timesheet']; $permissions = [ 'ROLE_USER' => array_merge($timesheet, $profile, $roleUser), diff --git a/tests/Voter/ActivityVoterTest.php b/tests/Voter/ActivityVoterTest.php index 2c1cff5a..9112fdcb 100644 --- a/tests/Voter/ActivityVoterTest.php +++ b/tests/Voter/ActivityVoterTest.php @@ -90,7 +90,7 @@ class ActivityVoterTest extends AbstractVoterTest $team = new Team(); $user = new User(); $user->addRole(User::ROLE_TEAMLEAD); - $team->setTeamLead($user); + $team->addTeamlead($user); $activity = new Activity(); $project = new Project(); @@ -129,7 +129,7 @@ class ActivityVoterTest extends AbstractVoterTest $team = new Team(); $user = new User(); $user->addRole(User::ROLE_USER); - $team->setTeamLead($user); + $team->addTeamlead($user); $activity = new Activity(); $project = new Project(); diff --git a/tests/Voter/CustomerVoterTest.php b/tests/Voter/CustomerVoterTest.php index e8c9c80b..108d3f4c 100644 --- a/tests/Voter/CustomerVoterTest.php +++ b/tests/Voter/CustomerVoterTest.php @@ -47,7 +47,7 @@ class CustomerVoterTest extends AbstractVoterTest } $team = new Team(); - $team->setTeamLead($userTeamlead); + $team->addTeamlead($userTeamlead); foreach ([$userTeamlead] as $user) { $customer = new Customer(); $team->addCustomer($customer); @@ -88,7 +88,7 @@ class CustomerVoterTest extends AbstractVoterTest $team = new Team(); $user = new User(); $user->addRole(User::ROLE_TEAMLEAD); - $team->setTeamLead($user); + $team->addTeamlead($user); $customer = new Customer(); $customer->addTeam($team); @@ -101,7 +101,7 @@ class CustomerVoterTest extends AbstractVoterTest $team = new Team(); $user = new User(); $user->addRole(User::ROLE_USER); - $team->setTeamLead($user); + $team->addTeamlead($user); $customer = new Customer(); $customer->addTeam($team); diff --git a/tests/Voter/ProjectVoterTest.php b/tests/Voter/ProjectVoterTest.php index 6ae1404a..bba17c5c 100644 --- a/tests/Voter/ProjectVoterTest.php +++ b/tests/Voter/ProjectVoterTest.php @@ -52,7 +52,7 @@ class ProjectVoterTest extends AbstractVoterTest } $team = new Team(); - $team->setTeamLead($userTeamlead); + $team->addTeamlead($userTeamlead); foreach ([$userTeamlead] as $user) { $project = new Project(); $team->addProject($project); @@ -95,7 +95,7 @@ class ProjectVoterTest extends AbstractVoterTest $team = new Team(); $user = new User(); $user->addRole(User::ROLE_TEAMLEAD); - $team->setTeamLead($user); + $team->addTeamlead($user); $project = new Project(); $customer = new Customer(); @@ -117,7 +117,7 @@ class ProjectVoterTest extends AbstractVoterTest $team = new Team(); $user = new User(); $user->addRole(User::ROLE_USER); - $team->setTeamLead($user); + $team->addTeamlead($user); $project = new Project(); $customer = new Customer(); diff --git a/tests/Voter/UserVoterTest.php b/tests/Voter/UserVoterTest.php index 466d31de..f682d8d1 100644 --- a/tests/Voter/UserVoterTest.php +++ b/tests/Voter/UserVoterTest.php @@ -111,4 +111,16 @@ class UserVoterTest extends AbstractVoterTest [User::AUTH_SAML, VoterInterface::ACCESS_DENIED], ]; } + + public function testViewTeamMember() + { + $userMock = $this->createMock(User::class); + $userMock->method('getId')->willReturn(1); + $user = new User(); + $token = new UsernamePasswordToken($user, 'foo', 'bar', $user->getRoles()); + $sut = $this->getVoter(UserVoter::class); + + $this->assertEquals(VoterInterface::ACCESS_GRANTED, $sut->vote($token, $user, ['view_team_member'])); + $this->assertEquals(VoterInterface::ACCESS_DENIED, $sut->vote($token, $userMock, ['view_team_member'])); + } } diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 262d27b4..a5022981 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -1240,6 +1240,14 @@ delete_warning.short_stats Momentan existieren insgesamt %records% Zeiteinträge, welche sich auf eine Gesamtdauer von %duration% belaufen. + + add_user.label + Benutzer hinzufügen + + + team.add_user.help + Durch Auswahl wird dem Team ein neuer Benutzer hinzugefügt. Sie können im Anschluss festlegen, ob der Benutzer ein Teamleiter sein soll. + diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index b8708438..9ab02a44 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -1240,6 +1240,14 @@ delete_warning.short_stats Currently %records% time-records exists, which sum up to a total duration of %duration%. + + add_user.label + Add user + + + team.add_user.help + Add a new user to the team by selecting it from the list. Afterwards you can decide if the user should become a teamlead. + diff --git a/translations/validators.de.xlf b/translations/validators.de.xlf index 04d048cf..d32b8647 100644 --- a/translations/validators.de.xlf +++ b/translations/validators.de.xlf @@ -2,6 +2,10 @@ + + At least one team leader must be assigned to the team. + Dem Team muss mindestens ein Teamleiter zugeordnet werden. + The entered passwords don't match. Die Passwörter stimmen nicht überein. diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index 5cf91f4f..bd79b263 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -2,6 +2,10 @@ + + At least one team leader must be assigned to the team. + At least one team leader must be assigned to the team. + The entered passwords don't match. The entered passwords don't match.