Skip to content

Commit

Permalink
An superadmin can see active users #216
Browse files Browse the repository at this point in the history
* upgrade vue2-daterange-picker
* add WalksTimeRangeFilter
* add integration tests for walks.timeRange filter
* add acceptance tests for /benutzer
* restricted to superadmin by now; will be enabled for admin too if considered useful
  • Loading branch information
robertfausk committed Oct 28, 2021
1 parent f30ac4d commit db7467e
Show file tree
Hide file tree
Showing 13 changed files with 526 additions and 147 deletions.
15 changes: 15 additions & 0 deletions web/assets/js/components/Users.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@
>
<user-list />
</content-collapse>
<content-collapse
v-if="isSuperAdmin"
title="Liste der aktiven Benutzer"
collapse-key="active-users"
is-visible-by-default
>
<active-user-list />
</content-collapse>
<content-collapse
title="Neuen Benutzer erstellen"
collapse-key="user-create"
Expand All @@ -19,17 +27,24 @@

<script>
"use strict";
import ActiveUserList from './Users/ActiveUserList';
import UserCreate from './Users/UserCreate';
import UserList from './Users/UserList';
import ContentCollapse from './ContentCollapse.vue';
export default {
name: "Users",
components: {
ActiveUserList,
ContentCollapse,
UserCreate,
UserList,
},
computed: {
isSuperAdmin() {
return this.$store.getters['security/isSuperAdmin'];
},
},
async mounted() {
await this.$store.dispatch('client/findAll');
},
Expand Down
270 changes: 270 additions & 0 deletions web/assets/js/components/Users/ActiveUserList.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
<template>
<div class="p-2">
<b-row>
<b-col
class="mb-1"
xs="12"
sm="12"
md="12"
>
<b-input-group
size="sm"
>
<b-input-group-prepend
@click.stop="togglePicker"
>
<b-input-group-text>
Zeitraum
</b-input-group-text>
</b-input-group-prepend>
<date-range-picker
ref="picker"
class="form-control form-control-m"
v-model="dateRange"
:ranges="ranges"
:locale-data="locale"
auto-apply
show-dropdowns
opens="right"
:readonly="isLoadingEntries.length > 0"
:disabled="isLoadingEntries.length > 0"
>
</date-range-picker>
<b-input-group-append
@click.stop="togglePicker"
>
<b-input-group-text>
<mdicon
v-if="isLoading || isLoadingEntries.length > 0"
name="loading"
size="18"
spin
/>
<mdicon
v-else
size="18"
name="calendar"
/>
</b-input-group-text>
</b-input-group-append>
<b-input-group-append>
<b-button
@click="resetDefaultDateRange"
:disabled="(dateRange.startDate.getTime() === defaultDateRange.startDate.getTime() && dateRange.endDate.getTime() === defaultDateRange.endDate.getTime()) || isLoading || isLoadingEntries.length > 0"
>
<mdicon name="CloseCircleOutline" size="18" />
</b-button>
</b-input-group-append>
</b-input-group>
</b-col>
</b-row>
<b-table
v-show="!isLoading"
:items="tableData"
:fields="fields"
small
striped
foot-clone
class="mb-0"
:stacked="this.fields.length > 11 ? 'lg' : 'sm'"
>
<template v-slot:cell()="row">
<template v-if="row.field.key === 'username'">
{{ row.value }}
</template>
<mdicon
v-else-if="isLoadingEntries.includes(row.field.key)"
name="loading"
class="text-muted"
spin
size="18"
/>
<mdicon
v-else-if="row.value"
name="account-check-outline"
title="Benutzer hat in diesem Monat an mindestens einer Runde teilgenommen."
class="text-info"
size="18"
/>
</template>

<template #foot(username)="data">
Summe
</template>

<template #foot()="data">
{{ getSumOfColumn(data.column)}}
</template>
</b-table>
</div>
</template>

<script>
'use strict';
import DateRangePicker from 'vue2-daterange-picker';
import 'vue2-daterange-picker/dist/vue2-daterange-picker.css';
import UserAPI from '../../api/user';
import dayjs from 'dayjs';
export default {
name: 'ActiveUserList',
components: {
DateRangePicker,
},
data: function () {
let now = dayjs();
let defaultStartDate = now.subtract(5, 'month').startOf('month').toDate();
let defaultEndDate = now.endOf('month').toDate();
return {
isLoadingEntries: [],
locale: {
direction: 'ltr',
format: 'dd.mm.yyyy',
separator: ' - ',
applyLabel: 'Übernehmen',
cancelLabel: 'Abbrechen',
weekLabel: 'W',
customRangeLabel: 'Custom Range',
daysOfWeek: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'],
monthNames: ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'],
firstDay: 1
},
defaultDateRange: {
startDate: defaultStartDate,
endDate: defaultEndDate,
},
dateRange: {
startDate: new Date(this.$localStorage.get('aktive-benutzer-startDate', defaultStartDate)),
endDate: new Date(this.$localStorage.get('aktive-benutzer-endDate', defaultEndDate)),
},
ranges: {
'Dieser Monat': [dayjs().startOf('month').toDate(), dayjs().endOf('month').toDate()],
'Letzter Monat': [dayjs().subtract(1, 'month').startOf('month').toDate(), dayjs().subtract(1, 'month').endOf('month').toDate()],
'Letzte 6 Monate': [dayjs().subtract(5, 'month').startOf('month').toDate(), dayjs().endOf('month').toDate()],
'Dieses Jahr': [dayjs().startOf('year').toDate(), dayjs().endOf('year').toDate()],
// 'Letztes Jahr': [dayjs().subtract(1, 'year').startOf('year').toDate(), dayjs().subtract(1, 'year').endOf('year').toDate()],
// 'Vorletztes Jahr': [dayjs().subtract(2, 'year').startOf('year').toDate(), dayjs().subtract(2, 'year').endOf('year').toDate()],
},
dateFrom: now.subtract(6, 'month').startOf('month'),
dateTo: now.add(1, 'month').endOf('month'),
entries: [],
};
},
computed: {
tableData() {
return this.entries;
},
fields() {
let fields = [];
let start = dayjs(this.dateRange.startDate);
let dateTo = dayjs(this.dateRange.endDate);
fields.push(
{
key: 'username',
label: 'Benutzername',
sortable: true,
class: 'text-center',
},
);
while (start.isBefore(dateTo)) {
fields.push({
key: this.getKeyOfDayjs(start),
label: `${start.month() + 1}/${start.year()}`,
sortable: true,
class: 'text-center',
});
start = start.startOf('month').add(1, 'month');
}
return fields;
},
users() {
return this.$store.getters['user/users'];
},
isLoading() {
return this.$store.getters['user/isLoading'];
},
},
watch: {
dateRange: async function (dateRange) {
this.$localStorage.set('aktive-benutzer-startDate', dateRange.startDate);
this.$localStorage.set('aktive-benutzer-endDate', dateRange.endDate);
await this.updateEntries();
}
},
async created() {
await this.$store.dispatch('user/findAll');
this.users.forEach((user) => {
let item = {
username: user.username,
};
this.entries.push(item);
});
await this.updateEntries();
},
methods: {
getKeyOfDayjs(date) {
return `${date.month()}-${date.year()}`;
},
getSumOfColumn(columnKey) {
let sum = 0;
this.tableData.forEach((tableDataValue) => {
if (tableDataValue[columnKey] === true) {
sum++;
}
});
return sum;
},
resetDefaultDateRange() {
this.dateRange = this.defaultDateRange;
},
togglePicker() {
this.$refs.picker.togglePicker(!this.$refs.picker.open);
},
async updateEntries() {
let start = dayjs(this.dateRange.startDate);
const end = dayjs(this.dateRange.endDate);
while (start.isBefore(end)) {
let key = this.getKeyOfDayjs(start);
this.isLoadingEntries.push(key);
let users = await UserAPI.findAll({
page: 1,
itemsPerPage: 1000,
filter: {
'walks.timeRange': `${start.toISOString()}..${start.endOf('month').toISOString()}`
}
});
users.data['hydra:member'].forEach((user) => {
this.entries.forEach((oldObject, key) => {
if (oldObject['username'] === user.username) {
oldObject[this.getKeyOfDayjs(start)] = true;
// see: https://vuejs.org/v2/guide/reactivity.html#For-Arrays
this.entries.splice(key, 1, oldObject);
}
});
});
this.isLoadingEntries = this.isLoadingEntries.filter(function(value){
return value !== key;
});
start = start.add(1, 'month');
}
},
},
};
</script>

<style lang="scss">
.vue-daterange-picker {
.form-control {
padding: 0;
border: 0;
height: auto;
width: 100%;
}
}
</style>
2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"vue-router": "^3.3.4",
"vue-template-compiler": "^2.6",
"vue-web-storage": "^4.0.2",
"vue2-daterange-picker": "^0.6.5",
"vue2-daterange-picker": "^0.6.7",
"vuex": "^3.4.0",
"webpack-notifier": "^1.8"
},
Expand Down
3 changes: 3 additions & 0 deletions web/phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,16 @@
<rule ref="SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingAnyTypeHint">
<exclude-pattern>src/Security/Voter/*Voter.php</exclude-pattern>
<exclude-pattern>src/DataTransformer/*.php</exclude-pattern>
<exclude-pattern>src/Filter/*Filter.php</exclude-pattern>
</rule>

<rule ref="SlevomatCodingStandard.TypeHints.NullableTypeForNullDefaultValue.NullabilityTypeMissing">
<exclude-pattern>src/Notifier/*Notification.php</exclude-pattern>
<exclude-pattern>src/Filter/*Filter.php</exclude-pattern>
</rule>
<rule ref="SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingTraversableTypeHintSpecification">
<exclude-pattern>tests/Context/EmailTrait.php</exclude-pattern>
<exclude-pattern>src/DataTransformer/*.php</exclude-pattern>
<exclude-pattern>src/Filter/*Filter.php</exclude-pattern>
</rule>
</ruleset>
2 changes: 2 additions & 0 deletions web/phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ parameters:
ignoreErrors:
- '#Access to an undefined property Symfony\\Component\\Validator\\Constraint::\$message#'
- '#Method App\\Serializer\\Normalizer\\Base64DataUriNormalizer::denormalize\(\) has parameter \$context with no value type specified in iterable type array.#'
- '#Method App\\Filter\\WalksTimeRangeFilter::filterProperty\(\) has parameter \$value with no typehint specified.#'
# - '#Method [a-zA-Z0-9\\_]+OpenApiFactory::__invoke\(\) has parameter \$context with no value type specified in iterable type array.#'
- '#Method [a-zA-Z0-9\\_]+Requirements::getConstraints\(\) has parameter \$options with no value type specified in iterable type array.#'
# - '#Method [a-zA-Z0-9\\_]+Extension::applyToCollection\(\) has no return typehint specified.#'
# - '#Method [a-zA-Z0-9\\_]+Extension::applyToItem\(\) has no return typehint specified.#'
- '#Method [a-zA-Z0-9\\_]+Extension::applyToItem\(\) has parameter \$[a-zA-Z0-9\\_]+ with no value type specified in iterable type array.#'
# - '#Method [a-zA-Z0-9\\_]+DataTransformer::[a-zA-Z0-9\\_]+\(\) has parameter \$[a-zA-Z0-9\\_]+ with no value type specified in iterable type array.#'
- '#Method [a-zA-Z0-9\\_]+WalksTimeRangeFilter::getDescription\(\) return type has no value type specified in iterable type array\.#'
Loading

0 comments on commit db7467e

Please sign in to comment.