From b2d3272151523106027da1d31311bf6fbaf4c7d4 Mon Sep 17 00:00:00 2001
From: Kevin Papst
Date: Sat, 7 Aug 2021 18:05:41 +0200
Subject: [PATCH] support multiple teamleads in each team (#2702)
* fix jumping avatars
* fix line-break after color dot for long names
---
UPGRADING.md | 11 +-
assets/js/KimaiLoader.js | 2 +-
assets/js/plugins/KimaiActiveRecords.js | 7 +
.../js/plugins/KimaiActiveRecordsDuration.js | 24 +-
assets/sass/avatar.scss | 6 +-
assets/sass/datatable-extensions.scss | 6 +
config/packages/nelmio_api_doc.yaml | 2 +
public/build/app.39dfa7e1.js | 2 +
...ICENSE.txt => app.39dfa7e1.js.LICENSE.txt} | 0
public/build/app.65f97645.js | 2 -
.../{app.047d688b.css => app.c80d7236.css} | 2 +-
public/build/entrypoints.json | 4 +-
public/build/manifest.json | 4 +-
src/API/TeamController.php | 26 +-
src/Calendar/CalendarService.php | 1 +
src/Command/ImportProjectCommand.php | 2 +-
src/Command/KimaiImporterCommand.php | 10 +-
src/Command/ResetTestCommand.php | 2 +-
src/Controller/ActivityController.php | 2 +-
src/Controller/CustomerController.php | 2 +-
src/Controller/ProfileController.php | 18 +-
src/Controller/ProjectController.php | 2 +-
src/Controller/TeamController.php | 53 +--
src/Controller/TimesheetTeamController.php | 8 +-
src/DataFixtures/TeamFixtures.php | 2 +-
src/Entity/Team.php | 319 ++++++++++++++----
src/Entity/TeamMember.php | 113 +++++++
src/Entity/User.php | 221 +++++++++---
src/Form/API/TeamApiEditForm.php | 2 +-
src/Form/TeamEditForm.php | 66 ++--
src/Form/TimesheetMultiUserEditForm.php | 4 +-
src/Form/Toolbar/AbstractToolbarForm.php | 4 +-
src/Form/Toolbar/UserToolbarForm.php | 1 +
src/Form/Type/TeamMemberType.php | 62 ++--
src/Form/Type/TeamType.php | 2 +-
src/Form/Type/UserType.php | 6 +
src/Migrations/Version20210802174318.php | 46 +++
src/Migrations/Version20210802174319.php | 52 +++
src/Migrations/Version20210802174320.php | 39 +++
src/Repository/ActivityRepository.php | 6 +-
src/Repository/CustomerRepository.php | 4 +-
src/Repository/InvoiceRepository.php | 2 +-
src/Repository/Loader/ActivityIdLoader.php | 89 +++--
src/Repository/Loader/ActivityLoader.php | 6 +-
src/Repository/Loader/CustomerIdLoader.php | 37 +-
src/Repository/Loader/CustomerLoader.php | 6 +-
src/Repository/Loader/ProjectIdLoader.php | 70 ++--
src/Repository/Loader/ProjectLoader.php | 6 +-
src/Repository/Loader/TeamIdLoader.php | 26 +-
src/Repository/Loader/TimesheetIdLoader.php | 12 +-
src/Repository/Loader/TimesheetLoader.php | 8 +-
src/Repository/Loader/UserIdLoader.php | 65 ++--
src/Repository/Loader/UserLoader.php | 6 +-
src/Repository/ProjectRepository.php | 6 +-
src/Repository/Query/TeamQuery.php | 2 +-
src/Repository/Query/UserQuery.php | 19 ++
src/Repository/TeamRepository.php | 52 ++-
src/Repository/TimesheetRepository.php | 2 +-
src/Repository/UserRepository.php | 67 ++--
src/Validator/Constraints/Team.php | 33 ++
src/Validator/Constraints/TeamValidator.php | 41 +++
src/Voter/TeamVoter.php | 9 +
src/Voter/UserVoter.php | 10 +
src/Widget/Type/UserTeamProjects.php | 2 +-
templates/base.html.twig | 14 +-
templates/default/_form.html.twig | 8 +-
templates/form/kimai-theme.html.twig | 25 ++
templates/macros/widgets.html.twig | 22 +-
templates/team/edit.html.twig | 24 +-
templates/team/edit_member.html.twig | 6 +-
templates/team/index.html.twig | 14 +-
templates/team/member-form.html.twig | 44 +++
templates/user/stats.html.twig | 2 +-
templates/user/teams.html.twig | 54 ---
tests/API/APIControllerBaseTest.php | 40 ++-
tests/API/CustomerControllerTest.php | 2 +-
tests/API/ProjectControllerTest.php | 2 +-
tests/API/TeamControllerTest.php | 156 ++++++---
tests/Command/DemoteUserCommandTest.php | 2 +-
tests/Command/PromoteUserCommandTest.php | 2 +-
tests/Controller/ControllerBaseTest.php | 16 +-
tests/Controller/ExportControllerTest.php | 2 +-
tests/Controller/ProfileControllerTest.php | 6 +-
tests/Controller/ProjectControllerTest.php | 2 +-
tests/Controller/TeamControllerTest.php | 16 +-
tests/DataFixtures/TeamFixtures.php | 11 +-
tests/Entity/TeamTest.php | 174 +++++++++-
tests/Entity/UserTest.php | 88 ++++-
.../Repository/Loader/AbstractLoaderTest.php | 7 +-
tests/Repository/Loader/ProjectLoaderTest.php | 13 +-
tests/Repository/Query/UserQueryTest.php | 12 +
.../Constraints/TeamValidatorTest.php | 54 +++
tests/Voter/AbstractVoterTest.php | 8 +-
tests/Voter/ActivityVoterTest.php | 4 +-
tests/Voter/CustomerVoterTest.php | 6 +-
tests/Voter/ProjectVoterTest.php | 6 +-
tests/Voter/UserVoterTest.php | 12 +
translations/messages.de.xlf | 8 +
translations/messages.en.xlf | 8 +
translations/validators.de.xlf | 4 +
translations/validators.en.xlf | 4 +
101 files changed, 1952 insertions(+), 649 deletions(-)
create mode 100644 public/build/app.39dfa7e1.js
rename public/build/{app.65f97645.js.LICENSE.txt => app.39dfa7e1.js.LICENSE.txt} (100%)
delete mode 100644 public/build/app.65f97645.js
rename public/build/{app.047d688b.css => app.c80d7236.css} (96%)
create mode 100644 src/Entity/TeamMember.php
create mode 100644 src/Migrations/Version20210802174318.php
create mode 100644 src/Migrations/Version20210802174319.php
create mode 100644 src/Migrations/Version20210802174320.php
create mode 100644 src/Validator/Constraints/Team.php
create mode 100644 src/Validator/Constraints/TeamValidator.php
create mode 100644 templates/team/member-form.html.twig
delete mode 100644 templates/user/teams.html.twig
create mode 100644 tests/Validator/Constraints/TeamValidatorTest.php
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 %}
-
-
-{% 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.
+