Client - init users administration

This commit is contained in:
Sam 2021-10-31 11:21:21 +01:00
parent 6a002592f0
commit 1fa8f0af19
13 changed files with 373 additions and 11 deletions

View File

@ -24,14 +24,18 @@
</span>
</dd>
<dt>
<router-link to="/admin/SPORTS">
<router-link to="/admin/sports">
{{ capitalize($t('workouts.SPORT', 0)) }}
</router-link>
</dt>
<dd>
{{ $t('admin.ENABLE_DISABLE_SPORTS') }}
</dd>
<dt>{{ capitalize($t('admin.USER', 0)) }}</dt>
<dt>
<router-link to="/admin/users">
{{ capitalize($t('admin.USER', 0)) }}
</router-link>
</dt>
<dd>
{{ $t('admin.ADMIN_RIGHTS_DELETE_USER_ACCOUNT') }}
</dd>

View File

@ -17,7 +17,7 @@
</th>
<th>{{ $t('admin.SPORTS.TABLE.ACTIVE') }}</th>
<th class="text-left sport-action">
{{ $t('admin.SPORTS.TABLE.ACTION') }}
{{ $t('admin.ACTION') }}
</th>
<th />
</tr>
@ -123,9 +123,6 @@
font-size: 1.1em;
}
}
.center-text {
text-align: center;
}
.sport-img {
height: 35px;
width: 35px;

View File

@ -0,0 +1,209 @@
<template>
<div id="admin-users" class="admin-card">
<Card>
<template #title>{{ capitalize($t('admin.USER', 0)) }}</template>
<template #content>
<button class="top-button" @click.prevent="$router.push('/admin')">
{{ $t('admin.BACK_TO_ADMIN') }}
</button>
<div class="responsive-table">
<table>
<thead>
<tr>
<th>#</th>
<th class="left-text">{{ $t('user.USERNAME') }}</th>
<th class="left-text">{{ $t('user.EMAIL') }}</th>
<th class="left-text">
{{ $t('user.PROFILE.REGISTRATION_DATE') }}
</th>
<th>
{{ capitalize($t('workouts.WORKOUT', 0)) }}
</th>
<th>{{ $t('user.ADMIN') }}</th>
<th>{{ $t('admin.ACTION') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.username">
<td></td>
<td>
<span class="cell-heading">
{{ $t('user.USERNAME') }}
</span>
{{ user.username }}
</td>
<td>
<span class="cell-heading">
{{ $t('user.EMAIL') }}
</span>
{{ user.email }}
</td>
<td>
<span class="cell-heading">
{{ $t('user.PROFILE.REGISTRATION_DATE') }}
</span>
{{ user.created_at }}
</td>
<td class="center-text">
<span class="cell-heading">
{{ capitalize($t('workouts.WORKOUT', 0)) }}
</span>
{{ user.nb_workouts }}
</td>
<td class="center-text">
<span class="cell-heading">
{{ $t('user.ADMIN') }}
</span>
<i
:class="`fa fa${user.admin ? '-check' : ''}-square-o`"
aria-hidden="true"
/>
</td>
<td class="center-text">
<span class="cell-heading">
{{ $t('admin.ACTION') }}
</span>
<button :class="{ danger: user.admin }">
{{
$t(
`admin.USERS.TABLE.${
user.admin ? 'REMOVE' : 'ADD'
}_ADMIN_RIGHTS`
)
}}
</button>
</td>
</tr>
</tbody>
</table>
<Pagination
v-if="pagination.page"
path="/admin/users"
:pagination="pagination"
:query="query"
/>
<ErrorMessage :message="errorMessages" v-if="errorMessages" />
<button @click.prevent="$router.push('/admin')">
{{ $t('admin.BACK_TO_ADMIN') }}
</button>
</div>
</template>
</Card>
</div>
</template>
<script lang="ts">
import {
ComputedRef,
computed,
defineComponent,
watch,
capitalize,
onBeforeMount,
} from 'vue'
import { useRoute, LocationQuery } from 'vue-router'
import Pagination from '@/components/Common/Pagination.vue'
import { ROOT_STORE, USERS_STORE } from '@/store/constants'
import { IPagination, IPaginationPayload } from '@/types/api'
import { IUserProfile } from '@/types/user'
import { useStore } from '@/use/useStore'
export default defineComponent({
name: 'AdminUsers',
components: {
Pagination,
},
setup() {
const store = useStore()
const route = useRoute()
const orders: string[] = ['asc', 'desc']
const order_types: string[] = [
'admin',
'created_at',
'username',
'workouts_count',
]
let query: IPaginationPayload = getQuery(route.query)
const users: ComputedRef<IUserProfile[]> = computed(
() => store.getters[USERS_STORE.GETTERS.USERS]
)
const pagination: ComputedRef<IPagination> = computed(
() => store.getters[USERS_STORE.GETTERS.USERS_PAGINATION]
)
const errorMessages: ComputedRef<string | string[] | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
)
function loadUsers(queryParams: IPaginationPayload) {
store.dispatch(USERS_STORE.ACTIONS.GET_USERS, queryParams)
}
function getPage(page: string | (string | null)[] | null): number {
return page && typeof page === 'string' && +page > 0 ? +page : 1
}
function getPerPage(perPage: string | (string | null)[] | null): number {
return perPage && typeof perPage === 'string' && +perPage > 0
? +perPage
: 10
}
function getOrder(order: string | (string | null)[] | null): string {
return order && typeof order === 'string' && orders.includes(order)
? order
: 'asc'
}
function getOrderBy(order_by: string | (string | null)[] | null): string {
return order_by &&
typeof order_by === 'string' &&
order_types.includes(order_by)
? order_by
: 'created_at'
}
function getQuery(query: LocationQuery): IPaginationPayload {
return {
page: getPage(query.page),
per_page: getPerPage(query.per_page),
order: getOrder(query.order),
order_by: getOrderBy(query.order_by),
}
}
onBeforeMount(() => loadUsers(query))
watch(
() => route.query,
(newQuery) => {
query = getQuery(newQuery)
loadUsers(getQuery(newQuery))
}
)
return { errorMessages, pagination, query, users, capitalize }
},
})
</script>
<style lang="scss" scoped>
@import '~@/scss/base.scss';
#admin-users {
.top-button {
display: none;
}
table {
td {
font-size: 1.1em;
}
}
.left-text {
text-align: left;
}
@media screen and (max-width: $small-limit) {
.top-button {
display: block;
margin-bottom: $default-margin * 2;
}
}
}
</style>

View File

@ -0,0 +1,120 @@
<template>
<nav class="pagination-center" aria-label="navigation">
<ul class="pagination">
<li class="page-prev" :class="{ disabled: !pagination.has_prev }">
<router-link
class="page-link"
:to="{ path, query: getQuery(pagination.page, -1) }"
:event="pagination.has_prev ? 'click' : ''"
:disabled="!pagination.has_prev"
>
{{ $t('api.PAGINATION.PREVIOUS') }}
</router-link>
</li>
<li
v-for="page in rangePagination(pagination.pages)"
:key="page"
class="page"
:class="{ active: page === pagination.page }"
>
<router-link class="page-link" :to="{ path, query: getQuery(page) }">
{{ page }}
</router-link>
</li>
<li class="page-next" :class="{ disabled: !pagination.has_next }">
<router-link
class="page-link"
:to="{ path, query: getQuery(pagination.page, 1) }"
:event="pagination.has_next ? 'click' : ''"
:disabled="!pagination.has_next"
>
{{ $t('api.PAGINATION.NEXT') }}
</router-link>
</li>
</ul>
</nav>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue'
import { IPagination, IPaginationPayload } from '@/types/api'
export default defineComponent({
name: 'Pagination',
props: {
pagination: {
type: Object as PropType<IPagination>,
required: true,
},
path: {
type: String,
required: true,
},
query: {
type: Object as PropType<IPaginationPayload>,
required: true,
},
},
setup(props) {
function rangePagination(pages: number): number[] {
return Array.from({ length: pages }, (_, i) => i + 1)
}
function getQuery(page: number, cursor?: number): IPaginationPayload {
const newQuery = Object.assign({}, props.query)
newQuery.page = cursor ? page + cursor : page
return newQuery
}
return { rangePagination, getQuery }
},
})
</script>
<style lang="scss" scoped>
@import '~@/scss/base.scss';
.pagination-center {
display: flex;
justify-content: center;
font-size: 0.9em;
.pagination {
display: flex;
padding-left: 0;
list-style: none;
border-radius: 0.25rem;
.page-prev,
.page-next,
.page {
border: solid 1px var(--card-border-color);
padding: $default-padding $default-padding * 1.5;
&.active {
font-weight: bold;
}
&.disabled {
cursor: default;
a {
color: var(--disabled-color);
}
}
}
.page {
margin-left: -1px;
}
.page-prev {
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
}
.page-next {
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
margin-left: -1px;
}
}
}
</style>

View File

@ -1,4 +1,5 @@
{
"ACTION": "Action",
"ADMIN_RIGHTS_DELETE_USER_ACCOUNT": "Add/remove admin rights, delete user account.",
"ADMIN": "Admin",
"ADMINISTRATION": "Administration",
@ -17,7 +18,6 @@
"REGISTRATION_ENABLED": "Registration is currently enabled.",
"SPORTS": {
"TABLE": {
"ACTION": "Action",
"ACTIVE": "Active",
"HAS_WORKOUTS": "workouts exist",
"IMAGE": "Image",
@ -26,5 +26,11 @@
"TITLE": "Sports administration"
},
"UPDATE_APPLICATION_DESCRIPTION": "Update application configuration (maximum number of registered users, maximum files size).",
"USER": "user | users"
"USER": "user | users",
"USERS": {
"TABLE": {
"ADD_ADMIN_RIGHTS": "Add admin rights",
"REMOVE_ADMIN_RIGHTS": "Remove admin rights"
}
}
}

View File

@ -16,5 +16,9 @@
"Signature expired, Please log in again": "Signature expired. Please log in again.",
"Username: 3 to 12 characters required": "Username: 3 to 12 characters required.",
"Valid email must be provided": "Valid email must be provided."
},
"PAGINATION": {
"PREVIOUS": "previous",
"NEXT": "next"
}
}

View File

@ -1,4 +1,5 @@
{
"ADMIN": "Admin",
"CONFIRM_ACCOUNT_DELETION": "Are you sure you want to delete your account? All data will be deleted, this cannot be undone",
"EMAIL": "Email",
"ENTER_EMAIL": "Enter an email address",

View File

@ -1,4 +1,5 @@
{
"ACTION": "Action",
"ADMIN_RIGHTS_DELETE_USER_ACCOUNT": "Ajouter/retirer des droits d'adminsitration, supprimer des comptes utilisateurs.",
"ADMIN": "Admin",
"ADMINISTRATION": "Administration",
@ -17,7 +18,6 @@
"REGISTRATION_ENABLED": "Les inscriptions sont actuellement activées.",
"SPORTS": {
"TABLE": {
"ACTION": "Action",
"ACTIVE": "Actif",
"HAS_WORKOUTS": "des séances existent",
"IMAGE": "Image",
@ -26,5 +26,11 @@
"TITLE": "Administration - Sports"
},
"UPDATE_APPLICATION_DESCRIPTION": "Configurer l'application (nombre maximum d'utilisateurs inscrits, taille maximale des fichers).",
"USER": "utilisateur | utilisateurs"
"USER": "utilisateur | utilisateurs",
"USERS": {
"TABLE": {
"ADD_ADMIN_RIGHTS": "Ajouter les drois d'admin",
"REMOVE_ADMIN_RIGHTS": "Retirer les drois d'admin"
}
}
}

View File

@ -16,5 +16,9 @@
"Signature expired, Please log in again": "Signature expirée. Merci de vous reconnecter.",
"Username: 3 to 12 characters required": "3 à 12 caractères requis pour le nom.",
"Valid email must be provided": "L'email fourni n'est pas valide."
},
"PAGINATION": {
"PREVIOUS": "précédent",
"NEXT": "suivant"
}
}

View File

@ -1,4 +1,5 @@
{
"ADMIN": "Admin",
"CONFIRM_ACCOUNT_DELETION": "Etes-vous sûr de vouloir supprimer votre compte ? Toutes les données seront définitivement effacés.",
"EMAIL": "Email",
"ENTER_EMAIL": "Saisir une adresse email",

View File

@ -3,6 +3,7 @@ import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import AdminApplication from '@/components/Administration/AdminApplication.vue'
import AdminMenu from '@/components/Administration/AdminMenu.vue'
import AdminSports from '@/components/Administration/AdminSports.vue'
import AdminUsers from '@/components/Administration/AdminUsers.vue'
import Profile from '@/components/User/ProfileDisplay/index.vue'
import UserInfos from '@/components/User/ProfileDisplay/UserInfos.vue'
import UserPreferences from '@/components/User/ProfileDisplay/UserPreferences.vue'
@ -192,6 +193,11 @@ const routes: Array<RouteRecordRaw> = [
name: 'SportsAdministration',
component: AdminSports,
},
{
path: 'users',
name: 'UsersAdministration',
component: AdminUsers,
},
],
},
{

View File

@ -232,6 +232,10 @@ button {
}
}
.center-text {
text-align: center;
}
.responsive-table {
margin-bottom: 15px;
/* responsive table, adapted from: */

View File

@ -24,7 +24,7 @@ export const actions: ActionTree<IUsersState, IRootState> & IUsersActions = {
)
context.commit(
USERS_STORE.MUTATIONS.UPDATE_USERS_PAGINATION,
res.data.data.pagination
res.data.pagination
)
} else {
handleError(context, null)