Client - add Sport administration
This commit is contained in:
parent
95dec79814
commit
e4434acc94
@ -23,7 +23,11 @@
|
||||
}}
|
||||
</span>
|
||||
</dd>
|
||||
<dt>{{ capitalize($t('workouts.SPORT', 0)) }}</dt>
|
||||
<dt>
|
||||
<router-link to="/admin/SPORTS">
|
||||
{{ capitalize($t('workouts.SPORT', 0)) }}
|
||||
</router-link>
|
||||
</dt>
|
||||
<dd>
|
||||
{{ $t('admin.ENABLE_DISABLE_SPORTS') }}
|
||||
</dd>
|
||||
|
171
fittrackee_client/src/components/Administration/AdminSports.vue
Normal file
171
fittrackee_client/src/components/Administration/AdminSports.vue
Normal file
@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<div id="admin-sports" class="admin-card">
|
||||
<Card>
|
||||
<template #title>{{ $t('admin.SPORTS.TITLE') }}</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>{{ $t('admin.SPORTS.TABLE.IMAGE') }}</th>
|
||||
<th class="text-left">
|
||||
{{ $t('admin.SPORTS.TABLE.LABEL') }}
|
||||
</th>
|
||||
<th>{{ $t('admin.SPORTS.TABLE.ACTIVE') }}</th>
|
||||
<th class="text-left sport-action">
|
||||
{{ $t('admin.SPORTS.TABLE.ACTION') }}
|
||||
</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="sport in translatedSports" :key="sport.id">
|
||||
<td class="center-text">
|
||||
<span class="cell-heading">id</span>
|
||||
{{ sport.id }}
|
||||
</td>
|
||||
<td>
|
||||
<span class="cell-heading">
|
||||
{{ $t('admin.SPORTS.TABLE.IMAGE') }}
|
||||
</span>
|
||||
<SportImage
|
||||
:title="sport.translatedLabel"
|
||||
:sport-label="sport.label"
|
||||
/>
|
||||
</td>
|
||||
<td class="sport-label">
|
||||
<span class="cell-heading">
|
||||
{{ $t('admin.SPORTS.TABLE.LABEL') }}
|
||||
</span>
|
||||
{{ sport.translatedLabel }}
|
||||
</td>
|
||||
<td class="center-text">
|
||||
<span class="cell-heading">
|
||||
{{ $t('admin.SPORTS.TABLE.ACTIVE') }}
|
||||
</span>
|
||||
<i
|
||||
:class="`fa fa${sport.is_active ? '-check' : ''}-square-o`"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</td>
|
||||
<td class="sport-action">
|
||||
<span class="cell-heading">
|
||||
{{ $t('admin.SPORTS.TABLE.ACTION') }}
|
||||
</span>
|
||||
<div class="action-button">
|
||||
<button
|
||||
:class="{ danger: sport.is_active }"
|
||||
@click="updateSportStatus(sport.id, !sport.is_active)"
|
||||
>
|
||||
{{ $t(`buttons.${sport.is_active ? 'DIS' : 'EN'}ABLE`) }}
|
||||
</button>
|
||||
<span v-if="sport.has_workouts" class="has-workouts">
|
||||
<i class="fa fa-warning" aria-hidden="true" />
|
||||
{{ $t('admin.SPORTS.TABLE.HAS_WORKOUTS') }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<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 } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { ROOT_STORE, SPORTS_STORE } from '@/store/constants'
|
||||
import { ITranslatedSport } from '@/types/sports'
|
||||
import { useStore } from '@/use/useStore'
|
||||
import { translateSports } from '@/utils/sports'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AdminSports',
|
||||
setup() {
|
||||
const { t } = useI18n()
|
||||
const store = useStore()
|
||||
const translatedSports: ComputedRef<ITranslatedSport[]> = computed(() =>
|
||||
translateSports(store.getters[SPORTS_STORE.GETTERS.SPORTS], t)
|
||||
)
|
||||
const errorMessages: ComputedRef<string | string[] | null> = computed(
|
||||
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
|
||||
)
|
||||
|
||||
function updateSportStatus(id: number, isActive: boolean) {
|
||||
store.dispatch(SPORTS_STORE.ACTIONS.UPDATE_SPORTS, {
|
||||
id,
|
||||
isActive,
|
||||
})
|
||||
}
|
||||
|
||||
return { errorMessages, translatedSports, updateSportStatus }
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/scss/base.scss';
|
||||
#admin-sports {
|
||||
table {
|
||||
td {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
}
|
||||
.center-text {
|
||||
text-align: center;
|
||||
}
|
||||
.sport-img {
|
||||
height: 35px;
|
||||
width: 35px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.has-workouts {
|
||||
font-size: 0.95em;
|
||||
font-style: italic;
|
||||
padding: 0 $default-padding;
|
||||
}
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
.sport-action {
|
||||
padding-left: $default-padding * 4;
|
||||
}
|
||||
.action-button {
|
||||
display: block;
|
||||
}
|
||||
.top-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $small-limit) {
|
||||
.sport-action {
|
||||
padding-left: $default-padding;
|
||||
}
|
||||
.has-workouts {
|
||||
padding-top: $default-padding * 0.5;
|
||||
}
|
||||
.action-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: column;
|
||||
}
|
||||
.top-button {
|
||||
display: block;
|
||||
margin-bottom: $default-margin * 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="workouts-list">
|
||||
<div class="box" :class="{ 'empty-table': workouts.length === 0 }">
|
||||
<div class="workouts-table">
|
||||
<div class="workouts-table responsive-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@ -210,122 +210,35 @@
|
||||
}
|
||||
}
|
||||
.workouts-table {
|
||||
margin-bottom: 15px;
|
||||
/* responsive table, adapted from: */
|
||||
/* https://uglyduck.ca/making-tables-responsive-with-minimal-css/ */
|
||||
table {
|
||||
width: 100%;
|
||||
padding: $default-padding;
|
||||
font-size: 0.9em;
|
||||
border-collapse: collapse;
|
||||
|
||||
thead th {
|
||||
vertical-align: center;
|
||||
padding: $default-padding;
|
||||
border-bottom: 2px solid var(--card-border-color);
|
||||
.sport-col {
|
||||
padding-right: 0;
|
||||
}
|
||||
.workout-title {
|
||||
max-width: 90px;
|
||||
position: relative;
|
||||
.fa-map-o {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
tbody {
|
||||
font-size: 0.95em;
|
||||
td {
|
||||
padding: $default-padding;
|
||||
border-bottom: 1px solid var(--card-border-color);
|
||||
}
|
||||
tr:last-child td {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sport-col {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.workout-title {
|
||||
max-width: 90px;
|
||||
position: relative;
|
||||
|
||||
.fa-map-o {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
.static-map {
|
||||
display: none;
|
||||
box-shadow: 3px 3px 3px 1px lightgrey;
|
||||
}
|
||||
}
|
||||
|
||||
.workout-title:hover .static-map {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cell-heading {
|
||||
background: var(--cell-heading-bg-color);
|
||||
color: var(--cell-heading-color);
|
||||
.static-map {
|
||||
display: none;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
padding: 5px;
|
||||
position: absolute;
|
||||
text-transform: uppercase;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.sport-img {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
box-shadow: 3px 3px 3px 1px lightgrey;
|
||||
}
|
||||
}
|
||||
|
||||
.workout-title:hover .static-map {
|
||||
display: block;
|
||||
}
|
||||
.sport-img {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
@media screen and (max-width: $small-limit) {
|
||||
table {
|
||||
thead {
|
||||
left: -9999px;
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
tr {
|
||||
border-bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
td {
|
||||
border: 1px solid var(--card-border-color);
|
||||
margin: 0 -1px -1px 0;
|
||||
padding-top: 25px !important;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr:last-child td {
|
||||
border: 1px solid var(--card-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
.sport-col {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: $default-padding;
|
||||
}
|
||||
.cell-heading {
|
||||
display: flex;
|
||||
}
|
||||
.workout-title {
|
||||
max-width: initial;
|
||||
}
|
||||
.sport-col {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: $default-padding;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: $x-small-limit) {
|
||||
table {
|
||||
td {
|
||||
width: 100%;
|
||||
}
|
||||
.workout-title {
|
||||
max-width: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,16 @@
|
||||
"ENABLE_DISABLE_SPORTS": "Enable/disable sports.",
|
||||
"REGISTRATION_DISABLED": "Registration is currently disabled.",
|
||||
"REGISTRATION_ENABLED": "Registration is currently enabled.",
|
||||
"SPORTS": {
|
||||
"TABLE": {
|
||||
"ACTION": "Action",
|
||||
"ACTIVE": "Active",
|
||||
"HAS_WORKOUTS": "workouts exist",
|
||||
"IMAGE": "Image",
|
||||
"LABEL": "Label"
|
||||
},
|
||||
"TITLE": "Sports administration"
|
||||
},
|
||||
"UPDATE_APPLICATION_DESCRIPTION": "Update application configuration (maximum number of registered users, maximum files size).",
|
||||
"USER": "user | users"
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
{
|
||||
"CANCEL": "Cancel",
|
||||
"DELETE_MY_ACCOUNT": "Delete my account",
|
||||
"DISABLE": "Disable",
|
||||
"EDIT": "Edit",
|
||||
"ENABLE": "Enable",
|
||||
"FILTER": "Filter",
|
||||
"LOGIN": "Log in",
|
||||
"NO": "No",
|
||||
|
@ -15,6 +15,16 @@
|
||||
"ENABLE_DISABLE_SPORTS": "Activer/désactiver des sports.",
|
||||
"REGISTRATION_DISABLED": "Les inscriptions sont actuellement désactivées.",
|
||||
"REGISTRATION_ENABLED": "Les inscriptions sont actuellement activées.",
|
||||
"SPORTS": {
|
||||
"TABLE": {
|
||||
"ACTION": "Action",
|
||||
"ACTIVE": "Actif",
|
||||
"HAS_WORKOUTS": "des séances existent",
|
||||
"IMAGE": "Image",
|
||||
"LABEL": "Label"
|
||||
},
|
||||
"TITLE": "Administration - Sports"
|
||||
},
|
||||
"UPDATE_APPLICATION_DESCRIPTION": "Configurer l'application (nombre maximum d'utilisateurs inscrits, taille maximale des fichers).",
|
||||
"USER": "utilisateur | utilisateurs"
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
{
|
||||
"CANCEL": "Annuler",
|
||||
"DELETE_MY_ACCOUNT": "Supprimer mon compte",
|
||||
"DISABLE": "Désactiver",
|
||||
"EDIT": "Modifier",
|
||||
"ENABLE": "Activer",
|
||||
"FILTER": "Filtrer",
|
||||
"LOGIN": "Se connecter",
|
||||
"NO": "Non",
|
||||
|
@ -2,6 +2,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 Profile from '@/components/User/ProfileDisplay/index.vue'
|
||||
import UserInfos from '@/components/User/ProfileDisplay/UserInfos.vue'
|
||||
import UserPreferences from '@/components/User/ProfileDisplay/UserPreferences.vue'
|
||||
@ -186,6 +187,11 @@ const routes: Array<RouteRecordRaw> = [
|
||||
component: AdminApplication,
|
||||
props: { edition: true },
|
||||
},
|
||||
{
|
||||
path: 'sports',
|
||||
name: 'SportsAdministration',
|
||||
component: AdminSports,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -231,3 +231,83 @@ button {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.responsive-table {
|
||||
margin-bottom: 15px;
|
||||
/* responsive table, adapted from: */
|
||||
/* https://uglyduck.ca/making-tables-responsive-with-minimal-css/ */
|
||||
table {
|
||||
width: 100%;
|
||||
padding: $default-padding;
|
||||
font-size: 0.9em;
|
||||
border-collapse: collapse;
|
||||
|
||||
thead th {
|
||||
vertical-align: center;
|
||||
padding: $default-padding;
|
||||
border-bottom: 2px solid var(--card-border-color);
|
||||
}
|
||||
|
||||
tbody {
|
||||
font-size: 0.95em;
|
||||
td {
|
||||
padding: $default-padding;
|
||||
border-bottom: 1px solid var(--card-border-color);
|
||||
}
|
||||
tr:last-child td {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
.cell-heading {
|
||||
background: var(--cell-heading-bg-color);
|
||||
color: var(--cell-heading-color);
|
||||
display: none;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
padding: 5px;
|
||||
position: absolute;
|
||||
text-transform: uppercase;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: $small-limit) {
|
||||
table {
|
||||
thead {
|
||||
left: -9999px;
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
}
|
||||
tr {
|
||||
border-bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
td {
|
||||
border: 1px solid var(--card-border-color);
|
||||
margin: 0 -1px -1px 0;
|
||||
padding-top: 25px !important;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
width: 45%;
|
||||
}
|
||||
tbody {
|
||||
tr:last-child td {
|
||||
border: 1px solid var(--card-border-color);
|
||||
}
|
||||
}
|
||||
.cell-heading {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: $x-small-limit) {
|
||||
table {
|
||||
td {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import authApi from '@/api/authApi'
|
||||
import { ROOT_STORE, SPORTS_STORE } from '@/store/constants'
|
||||
import { IRootState } from '@/store/modules/root/types'
|
||||
import { ISportsActions, ISportsState } from '@/store/modules/sports/types'
|
||||
import { ISportPayload } from '@/types/sports'
|
||||
import { handleError } from '@/utils'
|
||||
|
||||
export const actions: ActionTree<ISportsState, IRootState> & ISportsActions = {
|
||||
@ -25,4 +26,20 @@ export const actions: ActionTree<ISportsState, IRootState> & ISportsActions = {
|
||||
})
|
||||
.catch((error) => handleError(context, error))
|
||||
},
|
||||
[SPORTS_STORE.ACTIONS.UPDATE_SPORTS](
|
||||
context: ActionContext<ISportsState, IRootState>,
|
||||
payload: ISportPayload
|
||||
): void {
|
||||
context.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
|
||||
authApi
|
||||
.patch(`sports/${payload.id}`, { is_active: payload.isActive })
|
||||
.then((res) => {
|
||||
if (res.data.status === 'success') {
|
||||
context.dispatch(SPORTS_STORE.ACTIONS.GET_SPORTS)
|
||||
} else {
|
||||
handleError(context, null)
|
||||
}
|
||||
})
|
||||
.catch((error) => handleError(context, error))
|
||||
},
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
export enum SportsActions {
|
||||
GET_SPORTS = 'GET_SPORTS',
|
||||
UPDATE_SPORTS = 'UPDATE_SPORTS',
|
||||
}
|
||||
|
||||
export enum SportsGetters {
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
|
||||
import { SPORTS_STORE } from '@/store/constants'
|
||||
import { IRootState } from '@/store/modules/root/types'
|
||||
import { ISport } from '@/types/sports'
|
||||
import { ISport, ISportPayload } from '@/types/sports'
|
||||
|
||||
export interface ISportsState {
|
||||
sports: ISport[]
|
||||
@ -17,6 +17,10 @@ export interface ISportsActions {
|
||||
[SPORTS_STORE.ACTIONS.GET_SPORTS](
|
||||
context: ActionContext<ISportsState, IRootState>
|
||||
): void
|
||||
[SPORTS_STORE.ACTIONS.UPDATE_SPORTS](
|
||||
context: ActionContext<ISportsState, IRootState>,
|
||||
payload: ISportPayload
|
||||
): void
|
||||
}
|
||||
|
||||
export interface ISportsGetters {
|
||||
|
@ -5,6 +5,12 @@ export interface ISport {
|
||||
is_active: boolean
|
||||
label: string
|
||||
}
|
||||
|
||||
export interface ITranslatedSport extends ISport {
|
||||
translatedLabel: string
|
||||
}
|
||||
|
||||
export interface ISportPayload {
|
||||
id: number
|
||||
isActive: boolean
|
||||
}
|
||||
|
@ -7,6 +7,7 @@
|
||||
:appStatistics="appStatistics"
|
||||
/>
|
||||
<NotFound v-else />
|
||||
<div id="bottom" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -64,6 +65,8 @@
|
||||
#admin {
|
||||
.admin-card {
|
||||
width: 100%;
|
||||
margin-bottom: 55px;
|
||||
|
||||
::v-deep(.card) {
|
||||
.admin-form {
|
||||
display: flex;
|
||||
|
Loading…
Reference in New Issue
Block a user