- Complete GGZ Ecademy Nuxt.js user portal - Learning products browser and management - Member management interface - User authentication and roles - Multi-language support (NL/EN) - Vuex store for state management - Component-based architecture
This commit is contained in:
81
pages/auth/_auth.vue
Normal file
81
pages/auth/_auth.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<v-row no-gutters>
|
||||
<v-col cols="12" md="4" sm="12" class="mx-auto">
|
||||
<v-container class="fill-height d-flex flex-column justify-space-between" fluid>
|
||||
<div>
|
||||
<app-logo class="app-logo d-flex mr-10" theme="light" width="180" />
|
||||
</div>
|
||||
<auth :mode="mode" />
|
||||
<span class="tertiary--text overline">
|
||||
<Copyright />
|
||||
</span>
|
||||
</v-container>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="8" class="d-none d-lg-flex">
|
||||
<v-img
|
||||
:src="welcomeImgSrc"
|
||||
height="100vh"
|
||||
|
||||
>
|
||||
<div class="d-flex flex-column justify-end pa-4 fill-height">
|
||||
<span class="display-3 white--text font-italic ml-7">
|
||||
Samen leren
|
||||
<br>in de ggz
|
||||
</span>
|
||||
</div>
|
||||
</v-img>
|
||||
</v-col>
|
||||
<global-snackbar />
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AppLogo from '~/components/Logo'
|
||||
import Auth from '~/components/Auth/Auth'
|
||||
import Copyright from '@/components/Admin/ggz/Footer/Copyright'
|
||||
import globalSnackbar from '~/components/UI/GlobalSnackbar/GlobalSnackbar'
|
||||
|
||||
export default {
|
||||
auth: 'guest',
|
||||
layout: 'empty',
|
||||
components: {
|
||||
AppLogo,
|
||||
Auth,
|
||||
Copyright,
|
||||
globalSnackbar,
|
||||
},
|
||||
mounted() {
|
||||
this.$vuetify.theme.dark = false
|
||||
},
|
||||
computed: {
|
||||
slug() {
|
||||
return this.$route.params.auth
|
||||
},
|
||||
mode() {
|
||||
const modes = ['login', 'password-forgotten', 'password-reset']
|
||||
if (!modes.includes(this.slug)) return 'login'
|
||||
return this.slug
|
||||
},
|
||||
welcomeImgSrc(){
|
||||
const customerLowercase = process.env.CUSTOMER.toLowerCase()
|
||||
return require(`@/assets/img/${customerLowercase}/welcome.jpg`)
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
div{
|
||||
min-width: 400px;
|
||||
}
|
||||
.app-logo {
|
||||
margin-top: 20px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
.v-image >>>.v-image__image{
|
||||
background-position: 44% 16% !important;
|
||||
background-size: 220% !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
13
pages/index.vue
Normal file
13
pages/index.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div>GGZ</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
layout: `${process.env.CUSTOMER}Default`,
|
||||
components: {},
|
||||
mounted () {
|
||||
this.$router.push(this.localePath('/auth/login'));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
568
pages/manager/accounts/_user.vue
Normal file
568
pages/manager/accounts/_user.vue
Normal file
@@ -0,0 +1,568 @@
|
||||
<template>
|
||||
<v-form ref="form" v-model="valid" lazy-validation>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<div class="d-flex justify-space-between">
|
||||
<h2 class="ml-6 txt--text">
|
||||
{{
|
||||
localUser.fullName && !isNewUser
|
||||
? localUser.fullName
|
||||
: $t('user.create')
|
||||
}}
|
||||
</h2>
|
||||
<current-date-time showIcon class="mr-6" />
|
||||
</div>
|
||||
|
||||
<accordion-card :title="$t('user.profile.title') | capitalize">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="3">
|
||||
<v-subheader class="txt--text font-weight-black">{{
|
||||
$t('user.profile.photo')
|
||||
}}</v-subheader>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-avatar
|
||||
size="150"
|
||||
class="mr-4 secondary has-outline"
|
||||
v-if="showDropzone"
|
||||
>
|
||||
<vue-dropzone
|
||||
ref="imgDropZone"
|
||||
id="customdropzone"
|
||||
:options="dropzoneOptions"
|
||||
@vdropzone-complete="afterComplete"
|
||||
class="secondary"
|
||||
/>
|
||||
</v-avatar>
|
||||
|
||||
<v-badge
|
||||
:style="{ cursor: 'pointer' }"
|
||||
offset-x="10"
|
||||
offset-y="15"
|
||||
class="pl-sm-3"
|
||||
>
|
||||
<template v-slot:badge>
|
||||
<v-icon>icon-close</v-icon>
|
||||
</template>
|
||||
|
||||
<v-avatar
|
||||
size="150"
|
||||
class="primary has-outline"
|
||||
@click="showDropzone = true"
|
||||
>
|
||||
<v-img :src="profilePic || noImage" contain />
|
||||
</v-avatar>
|
||||
</v-badge>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="3"></v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="3">
|
||||
<v-subheader class="txt--text font-weight-black">{{
|
||||
$t('general.name')
|
||||
}}</v-subheader>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field
|
||||
class="pl-sm-3"
|
||||
outlined
|
||||
type="text"
|
||||
required
|
||||
v-model="localUser.first_name"
|
||||
:rules="firstNameRules"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="3"></v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="3">
|
||||
<v-subheader class="txt--text font-weight-black">{{
|
||||
$t('general.surname')
|
||||
}}</v-subheader>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field
|
||||
class="pl-sm-3"
|
||||
outlined
|
||||
type="text"
|
||||
required
|
||||
v-model="localUser.last_name"
|
||||
:rules="lastNameRules"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="3"></v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="3">
|
||||
<v-subheader class="txt--text font-weight-black">{{
|
||||
$t('general.email') | capitalize
|
||||
}}</v-subheader>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field
|
||||
class="pl-sm-3"
|
||||
outlined
|
||||
v-model="localUser.email"
|
||||
:rules="emailRules"
|
||||
type="email"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="3"></v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="3">
|
||||
<v-subheader class="txt--text font-weight-black"
|
||||
>Nieuw wachtwoord</v-subheader
|
||||
>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field
|
||||
class="pl-sm-3"
|
||||
outlined
|
||||
type="password"
|
||||
placeholder=" "
|
||||
v-model="localUser.password"
|
||||
:rules="passwordRules"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="3"></v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="3">
|
||||
<v-subheader class="txt--text font-weight-black"
|
||||
>Herhaal nieuw wachtwoord
|
||||
</v-subheader>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field
|
||||
class="pl-sm-3"
|
||||
outlined
|
||||
type="password"
|
||||
placeholder=" "
|
||||
v-model="localUser.password_confirmation"
|
||||
:rules="
|
||||
passwordConfirmationRules.concat(passwordConfirmationRule)
|
||||
"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="3"></v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="$store.getters.isAdmin">
|
||||
<v-col cols="12" sm="12" md="3">
|
||||
<v-subheader class="txt--text font-weight-black">{{
|
||||
$t('general.roles')
|
||||
}}</v-subheader>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<div class="d-flex">
|
||||
<v-switch
|
||||
inset
|
||||
class="toggle pl-sm-3"
|
||||
v-model="selected"
|
||||
:label="role.name"
|
||||
:value="role.id"
|
||||
v-for="role in roles"
|
||||
:key="role.id"
|
||||
/>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="3"></v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="localUser.isMemberEditor">
|
||||
<v-col cols="12" sm="12" md="3">
|
||||
<v-subheader class="txt--text font-weight-black"
|
||||
>Leden Editor</v-subheader
|
||||
>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<div>
|
||||
<v-icon color="info"> icon-checkmark </v-icon>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="3"></v-col>
|
||||
</v-row>
|
||||
</accordion-card>
|
||||
|
||||
<!-- <accordion-card :title="$t('user.profile.notification')"></accordion-card> -->
|
||||
|
||||
<v-footer fixed style="z-index: 4" height="90" color="primary">
|
||||
<v-btn class="ma-4" text @click="$router.back()">
|
||||
<v-icon class="mx-2">icon-arrow-left</v-icon>
|
||||
</v-btn>
|
||||
<div>
|
||||
<v-btn
|
||||
class="ma-2 white--text"
|
||||
:color="$vuetify.theme.dark ? 'secondary' : 'txt'"
|
||||
depressed
|
||||
@click="save(true)"
|
||||
v-if="canSave"
|
||||
rounded
|
||||
>{{ $t('general.save') }}</v-btn
|
||||
>
|
||||
<v-btn
|
||||
class="ma-2 white--text"
|
||||
color="accent"
|
||||
depressed
|
||||
@click="save(false)"
|
||||
v-if="canSave"
|
||||
rounded
|
||||
>{{ $t('general.save_and_close') }}</v-btn
|
||||
>
|
||||
</div>
|
||||
|
||||
<v-spacer />
|
||||
<v-dialog v-model="dialog" persistent max-width="500">
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn class="ma-4" text v-on="on">
|
||||
<v-icon class="mx-2">icon-remove</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-card-title class="headline"
|
||||
>Do you want to delete {{ localUser.fullName }}</v-card-title
|
||||
>
|
||||
<v-card-text>
|
||||
Once deleted won't be possible to restore his/her data anymore.
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn color="info" @click="dialog = false" rounded depressed
|
||||
>back</v-btn
|
||||
>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="error" @click="deleteUser()" rounded depressed
|
||||
>Yes, Delete</v-btn
|
||||
>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-footer>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Util from '@/util'
|
||||
import PageHeader from '~/components/UI/PageHeader/PageHeader'
|
||||
import accordionCard from '@/components/UI/AccordionCard/AccordionCard'
|
||||
import currentDateTime from '@/components/CurrentDateTime/CurrentDateTime'
|
||||
import vueDropzone from 'vue2-dropzone'
|
||||
import 'vue2-dropzone/dist/vue2Dropzone.min.css'
|
||||
|
||||
export default {
|
||||
layout: `${process.env.CUSTOMER}Admin`,
|
||||
// middleware: 'onlySuperAdmins',
|
||||
components: {
|
||||
PageHeader,
|
||||
accordionCard,
|
||||
currentDateTime,
|
||||
vueDropzone,
|
||||
},
|
||||
data: () => ({
|
||||
valid: true,
|
||||
name: '',
|
||||
dialog: false,
|
||||
roles: [],
|
||||
selected: [],
|
||||
downloadedUser: {},
|
||||
localUser: {},
|
||||
loading: false,
|
||||
showDropzone: false,
|
||||
dropzoneOptions: {
|
||||
url: 'https://httpbin.org/post',
|
||||
maxFilesize: 1.2, // MB
|
||||
maxFiles: 1,
|
||||
thumbnailWidth: 150,
|
||||
thumbnailHeight: 150,
|
||||
addRemoveLinks: true,
|
||||
acceptedFiles: '.jpg, .jpeg, .png',
|
||||
dictDefaultMessage: `<span class="txt--text">Upload afbeelding</span>`,
|
||||
},
|
||||
images: [],
|
||||
img: {
|
||||
name: '',
|
||||
url: '',
|
||||
file: null,
|
||||
},
|
||||
firstNameRules: [
|
||||
(v) => !!v || 'First name is required',
|
||||
(v) => (v && v.length <= 30) || 'Name must be less than 30 characters',
|
||||
],
|
||||
lastNameRules: [
|
||||
(v) => !!v || 'Last name is required',
|
||||
(v) => (v && v.length <= 30) || 'Name must be less than 30 characters',
|
||||
],
|
||||
emailRules: [
|
||||
(v) => !!v || 'E-mail is required',
|
||||
(v) =>
|
||||
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
|
||||
v
|
||||
) || 'Invalid Email',
|
||||
],
|
||||
passwordRules: [
|
||||
(v) => !v || v.length <= 30 || 'password can be max 30 characters',
|
||||
(v) =>
|
||||
!v || v.length >= 8 || 'Het wachtwoord moet minimaal 8 tekens bevatten',
|
||||
],
|
||||
passwordConfirmationRules: [
|
||||
(v) =>
|
||||
!v ||
|
||||
v.length <= 30 ||
|
||||
'password confirmation can be max 30 characters',
|
||||
(v) =>
|
||||
!v ||
|
||||
v.length >= 8 ||
|
||||
'Het bevestigingswachtwoord moet minimaal 8 tekens bevatten',
|
||||
// (v) => (v && v === this.localUser.password) || `password and password confirmation don't match`,
|
||||
],
|
||||
}),
|
||||
|
||||
computed: {
|
||||
userSlug() {
|
||||
return this.$route.params.user
|
||||
},
|
||||
isUserLoggedProfilePage() {
|
||||
return +this.userSlug === +this.$auth.user.id
|
||||
},
|
||||
isNewUser() {
|
||||
return this.userSlug.toLowerCase() === 'new'
|
||||
},
|
||||
userDownloaded() {
|
||||
return Object.keys(this.downloadedUser).length > 0
|
||||
},
|
||||
isSlugNumber() {
|
||||
return !isNaN(this.userSlug)
|
||||
},
|
||||
isValidSlug() {
|
||||
return this.isNewUser || this.isSlugNumber
|
||||
},
|
||||
remoteRolesIds() {
|
||||
return this.downloadedUser.roles.map(({ id }) => id)
|
||||
},
|
||||
hasRolesChanges() {
|
||||
return !Util.arraysMatch(this.selected, this.remoteRolesIds)
|
||||
},
|
||||
userHasChanges() {
|
||||
return (
|
||||
!Util.areEqualInputs(this.downloadedUser, this.localUser) ||
|
||||
this.hasRolesChanges ||
|
||||
this.img.url
|
||||
)
|
||||
},
|
||||
passwordConfirmationRule() {
|
||||
return () =>
|
||||
this.localUser.password === this.localUser.password_confirmation ||
|
||||
'Password must match'
|
||||
},
|
||||
profilePic() {
|
||||
if (this.img.url) return this.img.url
|
||||
if (this.localUser.image) return this.localUser.image.full
|
||||
return null
|
||||
},
|
||||
noImage() {
|
||||
return require(`@/assets/img/no_image.png`)
|
||||
},
|
||||
canSave() {
|
||||
if (this.isNewUser) return true
|
||||
return (
|
||||
this.isSlugNumber &&
|
||||
this.userDownloaded &&
|
||||
this.userHasChanges &&
|
||||
this.valid
|
||||
)
|
||||
},
|
||||
},
|
||||
async asyncData({ $axios, store, $auth, params }) {
|
||||
// If a normal user tries to edit another profile, gets disconnected
|
||||
if (!store.getters.isSuperAdmin) {
|
||||
if (+params.user !== +store.getters.loggedInUser.id) {
|
||||
$auth.logout()
|
||||
}
|
||||
}
|
||||
|
||||
const response = await $axios.get('/roles')
|
||||
return { roles: response.data }
|
||||
},
|
||||
mounted() {
|
||||
if (!this.isValidSlug) this.$router.push('/')
|
||||
if (this.isNewUser) return
|
||||
if (this.isSlugNumber) this.getUser(this.userSlug)
|
||||
},
|
||||
methods: {
|
||||
validate() {
|
||||
this.$refs.form.validate()
|
||||
},
|
||||
reset() {
|
||||
this.$refs.form.reset()
|
||||
},
|
||||
resetValidation() {
|
||||
this.$refs.form.resetValidation()
|
||||
},
|
||||
|
||||
async getUser(userId) {
|
||||
if (!this.isSlugNumber) return
|
||||
|
||||
try {
|
||||
const response = await this.$axios.get(`/admin/users/${userId}`)
|
||||
|
||||
this.downloadedUser = response.data
|
||||
this.localUser = { ...response.data }
|
||||
this.selected = response.data.roles.map(({ id }) => id)
|
||||
} catch (error) {
|
||||
console.log('TCL: getUser -> error', error)
|
||||
}
|
||||
},
|
||||
|
||||
async save(stay = false) {
|
||||
const formData = new FormData()
|
||||
|
||||
if (!this.isNewUser) formData.append('id', this.downloadedUser.id)
|
||||
formData.append('first_name', this.localUser.first_name)
|
||||
formData.append('last_name', this.localUser.last_name)
|
||||
formData.append('email', this.localUser.email)
|
||||
|
||||
if (this.localUser.password) {
|
||||
formData.append('password', this.localUser.password)
|
||||
}
|
||||
if (this.localUser.password_confirmation) {
|
||||
formData.append(
|
||||
'password_confirmation',
|
||||
this.localUser.password_confirmation
|
||||
)
|
||||
}
|
||||
|
||||
if (this.selected.length > 0) {
|
||||
for (let i = 0; i < this.selected.length; i++) {
|
||||
formData.append('roles[]', this.selected[i])
|
||||
}
|
||||
}
|
||||
|
||||
if (this.img.file) formData.append('image', this.img.file)
|
||||
else formData.delete('image')
|
||||
|
||||
try {
|
||||
// console.log([...formData])
|
||||
const response = await this.$axios.post('/admin/users', formData)
|
||||
|
||||
const user = response.data
|
||||
|
||||
this.downloadedUser = response.data
|
||||
this.localUser = { ...response.data }
|
||||
|
||||
this.img.name = ''
|
||||
this.img.url = ''
|
||||
this.img.file = null
|
||||
|
||||
// If i'm editing myself, update mydata
|
||||
if (user.id === this.$auth.user.id) {
|
||||
this.$auth.setUser(user)
|
||||
}
|
||||
|
||||
this.$notifier.showMessage({
|
||||
content: `User updated`,
|
||||
color: 'success',
|
||||
icon: 'icon-checkmark',
|
||||
})
|
||||
|
||||
if (!stay) this.$router.push('/manager/accounts')
|
||||
} catch (error) {
|
||||
this.$notifier.showMessage({
|
||||
content:
|
||||
`${
|
||||
this.$route.params.user.toLowerCase() === 'new'
|
||||
? 'Creating'
|
||||
: 'Editing'
|
||||
} user: ` + error.message,
|
||||
color: 'error',
|
||||
icon: 'icon-message',
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
async deleteUser(userId = this.downloadedUser.id) {
|
||||
if (!userId) {
|
||||
this.$notifier.showMessage({
|
||||
content: `No User to delete selected`,
|
||||
color: 'error',
|
||||
icon: 'icon-message',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.$axios.delete(`/manager/accounts/${userId}`)
|
||||
|
||||
this.$notifier.showMessage({
|
||||
content: `User deleted`,
|
||||
color: 'success',
|
||||
icon: 'mdi-delete',
|
||||
})
|
||||
this.$router.push('/manager/accounts')
|
||||
} catch (error) {
|
||||
this.$notifier.showMessage({
|
||||
content: `Error deleting user: ${error}`,
|
||||
color: 'error',
|
||||
icon: 'icon-message',
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
async afterComplete(upload) {
|
||||
const file = upload
|
||||
|
||||
if (file === undefined) return
|
||||
if (file.name.lastIndexOf('.') <= 0) return
|
||||
|
||||
if (file.size > 2000000) {
|
||||
this.$notifier.showMessage({
|
||||
content: `Exceeded Max filesize`,
|
||||
color: 'error',
|
||||
icon: 'mdi-alert-circle',
|
||||
})
|
||||
this.img.file = null
|
||||
return
|
||||
}
|
||||
|
||||
this.isLoading = true
|
||||
|
||||
try {
|
||||
const fr = new FileReader()
|
||||
fr.readAsDataURL(file)
|
||||
fr.addEventListener('load', () => {
|
||||
this.img.name = file.name
|
||||
this.img.url = fr.result
|
||||
this.img.file = file // Image file ready to be sent to server
|
||||
})
|
||||
|
||||
this.loading = false
|
||||
this.$refs.imgDropZone.removeFile(upload)
|
||||
this.showDropzone = false
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.has-outline {
|
||||
box-shadow: 0 0 0 3px #eef7f9;
|
||||
}
|
||||
.v-badge .v-badge_wrapper .v-badge__badge {
|
||||
background-color: var(--v-primary-base) !important;
|
||||
}
|
||||
.icon-close {
|
||||
color: var(--v-tertiary-base) !important;
|
||||
}
|
||||
.v-text-field .v-input__control {
|
||||
padding-left: 14px !important;
|
||||
}
|
||||
</style>
|
||||
186
pages/manager/accounts/index.vue
Normal file
186
pages/manager/accounts/index.vue
Normal file
@@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<div>
|
||||
<page-header v-if="users" class="d-flex justify-space-between">
|
||||
<span> {{ $t('auth.account.management') }} ({{ users.length }}) </span>
|
||||
<v-btn
|
||||
color="accent"
|
||||
depressed
|
||||
rounded
|
||||
:to="localePath('/manager/accounts/new')"
|
||||
v-if="$store.getters.isAdmin"
|
||||
>
|
||||
<v-icon left x-small>icon-add</v-icon>
|
||||
<small>{{ $t('general.add') }}</small>
|
||||
</v-btn>
|
||||
</page-header>
|
||||
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="users"
|
||||
:items-per-page="5"
|
||||
class="pa-4 secondary"
|
||||
>
|
||||
<template v-slot:item.fullName="{ item }">
|
||||
<v-avatar size="40" class="primary has-outline">
|
||||
<img :src="item.image.thumb || noImage" :alt="item.fullName" />
|
||||
</v-avatar>
|
||||
<span class="ml-4">
|
||||
{{ item.fullName }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.roles="{ item }">
|
||||
<v-chip
|
||||
class="ma-2"
|
||||
v-for="role in item.roles"
|
||||
:color="role.color"
|
||||
:key="role.name"
|
||||
outlined
|
||||
>{{ role.name }}</v-chip
|
||||
>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-menu offset-y>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn v-on="on" fab small outlined color="info" x-small>
|
||||
<v-icon small>icon-options</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item nuxt :to="localePath(`/manager/accounts/${item.id}`)">
|
||||
<v-list-item-icon class="mr-1">
|
||||
<v-icon small>icon-edit</v-icon>
|
||||
</v-list-item-icon>
|
||||
|
||||
<v-list-item-title>{{ $t('general.edit') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-dialog v-model="dialog" persistent max-width="500">
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-list-item v-on="on">
|
||||
<v-list-item-icon class="mr-1">
|
||||
<v-icon small>icon-remove</v-icon>
|
||||
</v-list-item-icon>
|
||||
|
||||
<v-list-item-title>{{
|
||||
$t('general.delete')
|
||||
}}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<v-card class="primary" flat>
|
||||
<v-card-title class="headline">{{
|
||||
$t('user.delete.confirmation')
|
||||
}}</v-card-title>
|
||||
<v-card-text>{{ $t('user.delete.info') }}</v-card-text>
|
||||
<v-card-actions>
|
||||
<div class="ma-4">
|
||||
<v-btn
|
||||
color="accent"
|
||||
class="mx-2"
|
||||
@click="deleteUser(item.id)"
|
||||
rounded
|
||||
depressed
|
||||
>{{ $t('user.delete.yes') }}</v-btn
|
||||
>
|
||||
<v-btn
|
||||
class="mx-2"
|
||||
color="info"
|
||||
@click="close"
|
||||
rounded
|
||||
depressed
|
||||
>{{ $t('general.back') }}</v-btn
|
||||
>
|
||||
</div>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PageHeader from '~/components/UI/PageHeader/PageHeader'
|
||||
|
||||
export default {
|
||||
layout: `${process.env.CUSTOMER}Admin`,
|
||||
middleware: 'onlySuperAdmins',
|
||||
components: {
|
||||
PageHeader,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialog: false,
|
||||
users: [],
|
||||
headers: [
|
||||
{ text: this.$t('general.name'), sortable: true, value: 'fullName' },
|
||||
{ text: this.$t('general.email'), value: 'email', sortable: true },
|
||||
{ text: this.$t('general.roles'), value: 'roles' },
|
||||
{ text: this.$t('general.actions'), value: 'actions', sortable: false },
|
||||
],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
noImage() {
|
||||
return require(`@/assets/img/no_image.png`)
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
dialog(val) {
|
||||
val || this.close()
|
||||
},
|
||||
},
|
||||
|
||||
async asyncData({ $axios }) {
|
||||
const response = await $axios.get('/admin/users')
|
||||
return { users: response.data }
|
||||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.dialog = false
|
||||
},
|
||||
|
||||
save() {
|
||||
this.close()
|
||||
},
|
||||
|
||||
async deleteUser(userId) {
|
||||
if (!userId) {
|
||||
this.$notifier.showMessage({
|
||||
content: `No User to delete selected`,
|
||||
color: 'error',
|
||||
icon: 'icon-message',
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.$axios.delete(`/admin/users/${userId}`)
|
||||
this.users = this.users.filter((user) => user.id !== userId)
|
||||
this.dialog = false
|
||||
|
||||
this.$notifier.showMessage({
|
||||
content: `User deleted`,
|
||||
color: 'success',
|
||||
icon: 'mdi-delete',
|
||||
})
|
||||
} catch (error) {
|
||||
this.$notifier.showMessage({
|
||||
content: `Error trying to delete the selected user`,
|
||||
color: 'error',
|
||||
icon: 'mdi-delete',
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.has-outline {
|
||||
box-shadow: 0 0 0 3px #eef7f9;
|
||||
}
|
||||
</style>
|
||||
139
pages/manager/builder.vue
Normal file
139
pages/manager/builder.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<v-container fluid>
|
||||
<v-toolbar dense flat>
|
||||
<v-overflow-btn
|
||||
:items="components"
|
||||
v-model="componentSelected"
|
||||
item-text="name"
|
||||
return-object
|
||||
editable
|
||||
label="Choose a component"
|
||||
hide-details
|
||||
class="pa-0"
|
||||
overflow
|
||||
></v-overflow-btn>
|
||||
|
||||
<v-btn icon>
|
||||
<v-icon @click="addComponent(componentSelected)" size="15">icon-add</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
<!-- Dynamic Component -->
|
||||
<component :is="componentSelected.name" :data="componentSelected.data" />
|
||||
<!-- End Dynamic Component -->
|
||||
|
||||
<!-- Components Sort -->
|
||||
<v-card outlined tile v-if="tmpPage.length > 0">
|
||||
<v-toolbar dark dense flat color="red">
|
||||
<v-toolbar-title>Sort Components</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-icon>mdi-sort</v-icon>
|
||||
</v-toolbar>
|
||||
|
||||
<v-card-text class="my-0 py-0">
|
||||
<v-list>
|
||||
<draggable v-model="tmpPage">
|
||||
<transition-group>
|
||||
<v-list-item v-for="(component, index) in tmpPage" :key="component">
|
||||
<v-list-item-action>
|
||||
<v-badge overlap>
|
||||
<template v-slot:badge>
|
||||
<span class="white--text">{{index+1}}</span>
|
||||
</template>
|
||||
</v-badge>
|
||||
</v-list-item-action>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-text="`${component.name}`"></v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</transition-group>
|
||||
</draggable>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-btn block color="red" depressed dark @click>Save</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
<!-- end Components Sort -->
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* Builder
|
||||
*
|
||||
* Load a list of custom components from dynamicComponents dir?
|
||||
*
|
||||
* Clicking on ADD, we put in the preview page that component
|
||||
*
|
||||
* clicking on that component, we can pass some data
|
||||
*
|
||||
* https://www.storyblok.com/tp/vue-dynamic-component-from-json
|
||||
*
|
||||
* https://baianat.github.io/vuse/example.html
|
||||
*
|
||||
* -------
|
||||
* https://github.com/tommcclean/Vue-PageBuilder
|
||||
* https://www.youtube.com/watch?v=Iwwv0A7ttpQ&feature=youtu.be
|
||||
* -------
|
||||
*
|
||||
*/
|
||||
|
||||
import Draggable from 'vuedraggable'
|
||||
import Calendar from '@/components/DynamicComponents/Calendar'
|
||||
import Carousel from '@/components/DynamicComponents/Carousel'
|
||||
import Card from '@/components/DynamicComponents/Card'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Calendar,
|
||||
Card,
|
||||
Carousel,
|
||||
Draggable
|
||||
},
|
||||
layout: `${process.env.CUSTOMER}Admin`,
|
||||
data: () => ({
|
||||
tmpPage: [],
|
||||
componentSelected: '',
|
||||
components: [
|
||||
{
|
||||
name: 'Carousel',
|
||||
data: {
|
||||
items: [
|
||||
{
|
||||
src: 'https://cdn.vuetifyjs.com/images/carousel/squirrel.jpg'
|
||||
},
|
||||
{
|
||||
src: 'https://cdn.vuetifyjs.com/images/carousel/sky.jpg'
|
||||
},
|
||||
{
|
||||
src: 'https://cdn.vuetifyjs.com/images/carousel/bird.jpg'
|
||||
},
|
||||
{
|
||||
src: 'https://cdn.vuetifyjs.com/images/carousel/planet.jpg'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Card',
|
||||
data: {
|
||||
title: '3110 Card',
|
||||
image: 'https://cdn.vuetifyjs.com/images/cards/cooking.png'
|
||||
}
|
||||
}
|
||||
],
|
||||
carousel: {
|
||||
name: 'Carousel'
|
||||
}
|
||||
}),
|
||||
methods: {
|
||||
addComponent(component) {
|
||||
this.tmpPage.push(component)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
168
pages/manager/components/_component.vue
Normal file
168
pages/manager/components/_component.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<v-card tile>
|
||||
<v-toolbar dark color="primary" flat>
|
||||
<v-toolbar-title>Add Component</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-toolbar-items>
|
||||
<v-btn dark text @click="saveComponent">Save</v-btn>
|
||||
</v-toolbar-items>
|
||||
</v-toolbar>
|
||||
|
||||
<!-- Components -->
|
||||
<v-overflow-btn
|
||||
:items="component_types"
|
||||
v-model="componentSelected"
|
||||
item-text="name"
|
||||
return-object
|
||||
label="Choose one"
|
||||
hide-details
|
||||
class="ma-4"
|
||||
overflow
|
||||
dense
|
||||
flat
|
||||
solo
|
||||
></v-overflow-btn>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6">
|
||||
<!-- Dynamic Component -->
|
||||
<component :is="component" :data="model" />
|
||||
<!-- End Dynamic Component -->
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" class="pr-4">
|
||||
<v-form
|
||||
ref="form"
|
||||
v-model="valid"
|
||||
:lazy-validation="lazy"
|
||||
v-if="componentSelected"
|
||||
class="my-4"
|
||||
>
|
||||
<v-text-field v-model="name" label="Name" :rules="nameRules" required></v-text-field>
|
||||
<v-text-field
|
||||
v-model="description"
|
||||
label="Description"
|
||||
:rules="descriptionRules"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-form>
|
||||
|
||||
<vue-form-generator
|
||||
v-if="componentSelected"
|
||||
:schema="componentSelected.schema"
|
||||
:model="model"
|
||||
:options="formOptions"
|
||||
></vue-form-generator>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-tabs vertical v-if="componentSelected">
|
||||
<v-tab>Data</v-tab>
|
||||
<v-tab>Schema</v-tab>
|
||||
<v-tab-item>
|
||||
<pre v-if="model" v-html="model"></pre>
|
||||
</v-tab-item>
|
||||
<v-tab-item>
|
||||
<pre v-if="componentSelected.schema" v-html="componentSelected.schema"></pre>
|
||||
</v-tab-item>
|
||||
</v-tabs>
|
||||
|
||||
<div class="d-flex ma-4">
|
||||
<v-btn class="ma-2" color="info" tile depressed to="/manager/components">Back to components</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn class="ma-2" color="success" tile depressed @click="saveComponent">Save Component</v-btn>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import VueFormGenerator from 'vue-form-generator'
|
||||
import 'vue-form-generator/dist/vfg.css'
|
||||
|
||||
export default {
|
||||
layout: `${process.env.CUSTOMER}Admin`,
|
||||
components: {
|
||||
'vue-form-generator': VueFormGenerator.component
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
components: [],
|
||||
dialog: false,
|
||||
componentSelected: '',
|
||||
component_types: [],
|
||||
model: {},
|
||||
name: '',
|
||||
description: '',
|
||||
valid: true,
|
||||
lazy: false,
|
||||
nameRules: [
|
||||
v => !!v || 'Name is required',
|
||||
v => (v && v.length <= 20) || 'Name must be less than 20 characters'
|
||||
],
|
||||
descriptionRules: [
|
||||
v => !!v || 'Description is required',
|
||||
v =>
|
||||
(v && v.length <= 50) || 'Description must be less than 50 characters'
|
||||
],
|
||||
formOptions: {
|
||||
validateAfterLoad: true,
|
||||
validateAfterChanged: true,
|
||||
validateAsync: true
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pageSlug() {
|
||||
return this.$route.params.component
|
||||
},
|
||||
isNewComponent() {
|
||||
return this.pageSlug.toLowerCase() === 'new'
|
||||
},
|
||||
hasComponents() {
|
||||
return Object.keys(this.components).length > 0
|
||||
},
|
||||
isSlugNumber() {
|
||||
return !isNaN(this.pageSlug)
|
||||
},
|
||||
isValidSlug() {
|
||||
return this.isNewComponent || this.isSlugNumber
|
||||
},
|
||||
component() {
|
||||
if (!this.componentSelected) return
|
||||
return this.componentSelected.name
|
||||
? () =>
|
||||
import(
|
||||
`@/components/DynamicComponents/${this.componentSelected.name}`
|
||||
)
|
||||
: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getComponentTypes()
|
||||
},
|
||||
methods: {
|
||||
async getComponentTypes() {
|
||||
try {
|
||||
const response = await this.$axios.get(`/component-types`)
|
||||
this.component_types = response.data
|
||||
} catch (error) {
|
||||
console.log('TCL: getComponentTypes -> error', error)
|
||||
}
|
||||
},
|
||||
async saveComponent() {
|
||||
const data = {
|
||||
name: this.name,
|
||||
description: this.description || null,
|
||||
content: this.model,
|
||||
component_type_id: this.componentSelected.id
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.$axios.post('/components', data)
|
||||
this.$router.push('/manager/components')
|
||||
} catch (error) {
|
||||
console.log('TCL: saveComponent -> error', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
132
pages/manager/components/index.vue
Normal file
132
pages/manager/components/index.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<div>
|
||||
<page-header v-if="components">COMPONENTS ({{components.length}})</page-header>
|
||||
|
||||
<v-sheet class="mx-auto pa-4" tile>
|
||||
<v-data-table :search="search" :headers="headers" :items="components" :items-per-page="10">
|
||||
<template v-slot:top>
|
||||
<v-toolbar flat>
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
class="hidden-sm-and-down"
|
||||
hide-details
|
||||
flat
|
||||
append-icon="mdi-filter"
|
||||
placeholder="Filter..."
|
||||
light
|
||||
outlined
|
||||
dense
|
||||
></v-text-field>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn depressed to="/manager/components/new">
|
||||
<v-icon small>icon-add</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
</template>
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn
|
||||
color="info"
|
||||
class="mr-2"
|
||||
small
|
||||
dark
|
||||
tile
|
||||
depressed
|
||||
nuxt
|
||||
:to="localePath(`/manager/components/${item.id}`)"
|
||||
>
|
||||
<v-icon small>mdi-file-edit</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-dialog v-model="dialog" persistent max-width="500">
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn color="error" v-on="on" small dark tile depressed>
|
||||
<v-icon small>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-card-title class="headline">Do you want to delete this component?</v-card-title>
|
||||
<v-card-text>Deleting this component it will disappear from all the pages where it is attached and won't be possible to restore it anymore.</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn color="info" @click="dialog = false" tile depressed>back</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="error" @click="deleteComponent(item.id)" tile depressed>Yes, Delete</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-sheet>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PageHeader from '~/components/UI/PageHeader/PageHeader'
|
||||
|
||||
export default {
|
||||
layout: `${process.env.CUSTOMER}Admin`,
|
||||
components: {
|
||||
PageHeader
|
||||
},
|
||||
data: () => ({
|
||||
dialog: false,
|
||||
search: '',
|
||||
components: [],
|
||||
componentToDelete: null,
|
||||
headers: [
|
||||
{ text: 'id', sortable: true, value: 'id' },
|
||||
{ text: 'Name', sortable: true, value: 'name' },
|
||||
// { text: 'Description', value: 'description' },
|
||||
// { text: 'Created', value: 'created_at', sortable: true },
|
||||
{ text: 'Updated', value: 'updated_at', sortable: true },
|
||||
{ text: 'Actions', value: 'actions', sortable: false }
|
||||
]
|
||||
}),
|
||||
|
||||
watch: {
|
||||
dialog(val) {
|
||||
val || this.close()
|
||||
}
|
||||
},
|
||||
|
||||
async asyncData({ $axios }) {
|
||||
const response = await $axios.get('/components')
|
||||
return { components: response.data }
|
||||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.dialog = false
|
||||
},
|
||||
|
||||
save() {
|
||||
this.close()
|
||||
},
|
||||
|
||||
async deleteComponent(id) {
|
||||
try {
|
||||
const response = await this.$axios.delete(`/components/${id}`)
|
||||
this.components = this.components.filter(
|
||||
component => component.id !== id
|
||||
)
|
||||
this.dialog = false
|
||||
|
||||
this.$store.commit('snackbar/set', {
|
||||
color: 'success',
|
||||
display: true,
|
||||
text: 'Component deleted',
|
||||
icon: 'mdi-delete'
|
||||
})
|
||||
} catch (error) {
|
||||
this.dialog = false
|
||||
this.$store.commit('snackbar/set', {
|
||||
color: 'error',
|
||||
display: true,
|
||||
text: 'Error on deleting component',
|
||||
icon: 'mdi-delete'
|
||||
})
|
||||
console.log('TCL: deleteComponent -> error', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
100
pages/manager/index.vue
Normal file
100
pages/manager/index.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<v-container fluid>
|
||||
<v-row>
|
||||
<page-header>Dashboard</page-header>
|
||||
<v-spacer />
|
||||
<current-date-time />
|
||||
<v-col cols="12">
|
||||
<welcome />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="4" v-if="!$store.getters.isOnlyMemberEditor">
|
||||
<card
|
||||
:count="learningProductsCount"
|
||||
label="Leerproducten"
|
||||
icon="icon-learningproducts"
|
||||
:route="localePath('/manager/learning')"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col v-if="!$store.getters.isOnlyMemberEditor" cols="4">
|
||||
<card
|
||||
:count="membersCountComputed"
|
||||
label="Leden"
|
||||
icon="icon-members"
|
||||
:route="localePath('/manager/members')"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="4" v-if="$store.getters.isMember && !$store.getters.isOnlyMemberEditor">
|
||||
<card
|
||||
:count="2"
|
||||
label="Rapportages"
|
||||
icon="icon-members"
|
||||
:route="localePath('/manager/members/report')"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col v-if="$store.getters.isOnlyMemberEditor" cols="4">
|
||||
<card
|
||||
label="Lidmaatschapgegevens"
|
||||
icon="icon-attributes"
|
||||
:route="localePath('/manager/members')"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col v-if="$store.getters.isOnlyMemberEditor" cols="4">
|
||||
<card
|
||||
label="Managementinformatie"
|
||||
icon="icon-managementinfo"
|
||||
:route="localePath('/manager/members/report')"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PageHeader from '~/components/UI/PageHeader/PageHeader'
|
||||
import Welcome from '~/components/Admin/Welcome/'
|
||||
import currentDateTime from '~/components/CurrentDateTime/CurrentDateTime'
|
||||
import Card from '~/components/Card'
|
||||
|
||||
export default {
|
||||
layout: `${process.env.CUSTOMER}Admin`,
|
||||
components: {
|
||||
PageHeader,
|
||||
Welcome,
|
||||
currentDateTime,
|
||||
Card,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
learningProductsCount: 0,
|
||||
membersCount: 0,
|
||||
primary: 'primary',
|
||||
secondary: 'secondary',
|
||||
}
|
||||
},
|
||||
async asyncData({ $axios, store }) {
|
||||
try {
|
||||
const responseProducts = await $axios.get('/learning-products/count')
|
||||
const responseMembers = await $axios.get('/members/count')
|
||||
return {
|
||||
learningProductsCount: responseProducts.data,
|
||||
membersCount: responseMembers.data,
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Data -> error', error)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
membersCountComputed() {
|
||||
return this.$store.getters.isOnlyMemberEditor
|
||||
? this.$store.getters.loggedInUser.membersManagedCount
|
||||
: this.membersCount
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
42
pages/manager/items/index.vue
Normal file
42
pages/manager/items/index.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<v-container fluid>
|
||||
<v-row>
|
||||
<v-col cols="6">
|
||||
<v-row>
|
||||
<v-card class="ma-3 pa-6" outlined tile width="100%">
|
||||
<v-card-title>Data</v-card-title>
|
||||
<pre v-if="model" v-html="model"></pre>
|
||||
</v-card>
|
||||
</v-row>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-row>
|
||||
<v-card class="ma-3 pa-6" outlined tile width="100%">
|
||||
<v-card-title>Schema</v-card-title>
|
||||
<pre v-html="jsonData"></pre>
|
||||
</v-card>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
middleware: 'auth',
|
||||
layout: `${process.env.CUSTOMER}Admin`,
|
||||
components: {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
model: {},
|
||||
jsonData: {
|
||||
name: 'mike',
|
||||
age: 23,
|
||||
phone: '18552129932',
|
||||
address: ['AAA C1', 'BBB C2']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
520
pages/manager/learning/_product.vue
Normal file
520
pages/manager/learning/_product.vue
Normal file
@@ -0,0 +1,520 @@
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<h2 class="ma-4">{{ title }}</h2>
|
||||
<!-- <h2 class="ma-4">{{ title }} [{{ $store.getters.productHasChanges.toString() }}]</h2> -->
|
||||
|
||||
<div class="d-flex justify-space-between">
|
||||
<div class="d-flex">
|
||||
<small
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="index"
|
||||
@click="selectedTab = tab"
|
||||
class="ma-lg-4 ma-sm-2 dot"
|
||||
:class="{ 'accent--text activeTab': selectedTab === tab }"
|
||||
>{{ $t(`learning.product_overview.${tab}`) }}</small
|
||||
>
|
||||
</div>
|
||||
<current-date-time showIcon />
|
||||
</div>
|
||||
|
||||
<button ref="scrollStartingPoint" width="0.1px" height="0.1px"></button>
|
||||
|
||||
<basic v-if="isTabSelected('basic')" :editMode="canEdit" />
|
||||
<texts v-if="isTabSelected('texts')" :editMode="canEdit" />
|
||||
|
||||
<notifications
|
||||
v-if="isTabSelected('notifications')"
|
||||
:editMode="canEdit"
|
||||
:users="users"
|
||||
:isCreateMode="isNew"
|
||||
/>
|
||||
<organize v-if="isTabSelected('organize')" :editMode="canEdit" />
|
||||
<accreditation
|
||||
v-if="isTabSelected('accreditation')"
|
||||
:editMode="canEdit"
|
||||
:isCreateMode="isNew"
|
||||
/>
|
||||
<administration
|
||||
v-if="isTabSelected('administration')"
|
||||
:editMode="canEdit"
|
||||
/>
|
||||
<versions
|
||||
v-if="isTabSelected('version')"
|
||||
:editMode="canEdit"
|
||||
:isCreateMode="isNew"
|
||||
/>
|
||||
<links v-if="isTabSelected('links') && canEdit" :editMode="canEdit" />
|
||||
</v-col>
|
||||
|
||||
<v-footer
|
||||
fixed
|
||||
style="z-index: 4"
|
||||
height="90"
|
||||
color="primary"
|
||||
v-if="$store.getters.isAdmin || $store.getters.isOperator"
|
||||
>
|
||||
<v-btn title text small :to="localePath('/manager/learning')">
|
||||
<v-icon>icon-arrow-left</v-icon>
|
||||
</v-btn>
|
||||
<!-- <div class="mx-10" v-if="$store.getters.isLearningProductValidated"> -->
|
||||
<div class="mx-10">
|
||||
<v-btn
|
||||
v-cloak
|
||||
class="ma-2 white--text"
|
||||
color="accent"
|
||||
depressed
|
||||
v-if="!canEdit && !isNew"
|
||||
rounded
|
||||
nuxt
|
||||
:to="localePath(`${$nuxt.$route.path}?edit`)"
|
||||
>
|
||||
{{ $t('general.edit') }}</v-btn
|
||||
>
|
||||
<v-btn
|
||||
v-cloak
|
||||
class="ma-2 white--text"
|
||||
color="txt"
|
||||
depressed
|
||||
v-if="!canEdit && !isNew"
|
||||
rounded
|
||||
nuxt
|
||||
:to="localePath('/manager/learning')"
|
||||
>
|
||||
{{ $t('general.close') }}</v-btn
|
||||
>
|
||||
|
||||
<v-btn
|
||||
class="ma-2 white--text"
|
||||
:color="$vuetify.theme.dark ? 'secondary' : 'txt'"
|
||||
depressed
|
||||
@click="
|
||||
$router.push(localePath(`/manager/learning/${local.draft.slug}`))
|
||||
"
|
||||
v-if="canSave && local.draft"
|
||||
rounded
|
||||
>{{ $t('learning.product_overview.open_existing_draft') }}</v-btn
|
||||
>
|
||||
|
||||
<v-btn
|
||||
class="ma-2 white--text"
|
||||
:color="$vuetify.theme.dark ? 'secondary' : 'txt'"
|
||||
depressed
|
||||
@click="save(true)"
|
||||
v-if="canSave && !local.draft"
|
||||
rounded
|
||||
>{{ $t('general.save') }}</v-btn
|
||||
>
|
||||
<v-btn
|
||||
class="ma-2 white--text"
|
||||
:color="$vuetify.theme.dark ? 'secondary' : 'txt'"
|
||||
depressed
|
||||
@click="save(false)"
|
||||
v-if="canSave && !local.draft"
|
||||
rounded
|
||||
>{{ $t('general.save_and_close') }}</v-btn
|
||||
>
|
||||
|
||||
<v-btn
|
||||
v-cloak
|
||||
class="ma-2 white--text"
|
||||
color="accent"
|
||||
depressed
|
||||
@click="save(false, true)"
|
||||
v-if="canSave || isDraft"
|
||||
rounded
|
||||
>{{ $t('general.publish_and_close') }}</v-btn
|
||||
>
|
||||
</div>
|
||||
|
||||
<v-spacer />
|
||||
|
||||
<v-btn
|
||||
class="ma-2"
|
||||
tile
|
||||
text
|
||||
small
|
||||
v-if="remote.sharepoint_link"
|
||||
:href="remote.sharepoint_link"
|
||||
target="_blank"
|
||||
>
|
||||
<v-icon class="mx-2">icon-sharepoint</v-icon>
|
||||
{{ $t('footer_bar.documents') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
class="ma-2"
|
||||
tile
|
||||
text
|
||||
small
|
||||
v-if="remote.support_link"
|
||||
:href="remote.support_link"
|
||||
target="_blank"
|
||||
>
|
||||
<v-icon class="mx-2">icon-info</v-icon>
|
||||
{{ $t('footer_bar.support_site') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
class="ma-2"
|
||||
tile
|
||||
text
|
||||
small
|
||||
v-if="remote.support_tickets_link"
|
||||
:href="remote.support_tickets_link"
|
||||
target="_blank"
|
||||
>
|
||||
<v-icon class="mx-2">icon-support</v-icon>
|
||||
{{ $t('footer_bar.support_tickets') }}
|
||||
</v-btn>
|
||||
|
||||
<!-- <v-btn class="ma-2" tile text small>
|
||||
<v-icon class="mx-2" small>icon-share</v-icon>
|
||||
{{ $t('general.share_url') }}
|
||||
</v-btn> -->
|
||||
|
||||
<v-dialog v-model="dialog" persistent max-width="740">
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn
|
||||
class="ma-2"
|
||||
tile
|
||||
text
|
||||
small
|
||||
v-if="!isNew && canEdit"
|
||||
v-on="on"
|
||||
>
|
||||
<v-icon class="mx-2">icon-remove</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card class="primary pa-10" flat>
|
||||
<v-card-title class="headline">
|
||||
{{
|
||||
$t('learning.product_overview.delete_confirmation', {
|
||||
productName: local.title,
|
||||
})
|
||||
}}
|
||||
</v-card-title>
|
||||
<v-card-actions>
|
||||
<div class="ma-4">
|
||||
<v-btn
|
||||
color="accent"
|
||||
class="mx-2"
|
||||
@click="deleteProduct()"
|
||||
rounded
|
||||
depressed
|
||||
>{{ $t('general.delete') }}</v-btn
|
||||
>
|
||||
<v-btn
|
||||
class="mx-2"
|
||||
color="info"
|
||||
@click="close"
|
||||
rounded
|
||||
depressed
|
||||
>{{ $t('general.cancel') }}</v-btn
|
||||
>
|
||||
</div>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-footer>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import currentDateTime from '@/components/CurrentDateTime/CurrentDateTime'
|
||||
import basic from '@/components/Learning/ProductOverview/Basic'
|
||||
import texts from '@/components/Learning/ProductOverview/Texts'
|
||||
import notifications from '@/components/Learning/ProductOverview/Notifications'
|
||||
import organize from '@/components/Learning/ProductOverview/Organize'
|
||||
import accreditation from '@/components/Learning/ProductOverview/Accreditation'
|
||||
import administration from '@/components/Learning/ProductOverview/Administration'
|
||||
import versions from '@/components/Learning/ProductOverview/Versions'
|
||||
import links from '@/components/Learning/ProductOverview/Links'
|
||||
|
||||
export default {
|
||||
layout: `${process.env.CUSTOMER}Admin`,
|
||||
middleware: ['denyToOnlyMembers'],
|
||||
components: {
|
||||
currentDateTime,
|
||||
basic,
|
||||
texts,
|
||||
notifications,
|
||||
organize,
|
||||
accreditation,
|
||||
administration,
|
||||
versions,
|
||||
links,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialog: false,
|
||||
users: [],
|
||||
tabs: [
|
||||
'all',
|
||||
'basic',
|
||||
'texts',
|
||||
'notifications',
|
||||
'organize',
|
||||
'accreditation',
|
||||
'administration',
|
||||
'version',
|
||||
'links',
|
||||
],
|
||||
scrollFlag: false,
|
||||
selectedTab: 'all',
|
||||
}
|
||||
},
|
||||
|
||||
async asyncData({ $axios, store }) {
|
||||
try {
|
||||
let response = await $axios.get('/filters')
|
||||
await store.commit('learning/SORT_FILTERS', response.data)
|
||||
|
||||
response = await $axios.get('/synonyms')
|
||||
await store.commit('learning/SET_SYNONYMS_LIST', response.data)
|
||||
|
||||
response = await $axios.get('/checklist-categories')
|
||||
await store.commit('learning/SET_CHECKLIST', response.data)
|
||||
|
||||
if (store.getters.isAdmin || store.getters.isOperator) {
|
||||
response = await $axios.get('/admin/users/getList')
|
||||
return { users: response.data }
|
||||
}
|
||||
return { users: [] }
|
||||
} catch (error) {
|
||||
console.log('Data -> error', error)
|
||||
}
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
if (!this.isValidSlug) this.$router.push('/manager')
|
||||
if (this.isNew) return await this.$store.dispatch('learning/resetProduct')
|
||||
if (this.isSlugNumber) this.getItem(this.slug)
|
||||
},
|
||||
|
||||
updated() {
|
||||
if (!this.scrollFlag) {
|
||||
this.scrollToTop()
|
||||
this.scrollFlag = true
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
canEdit() {
|
||||
return (
|
||||
this.isNew ||
|
||||
(this.$route.query.edit === null &&
|
||||
(this.$store.getters.isAdmin || this.$store.getters.isOperator))
|
||||
)
|
||||
},
|
||||
remote() {
|
||||
return this.$store.state.learning.remote
|
||||
},
|
||||
local() {
|
||||
return this.$store.state.learning.local
|
||||
},
|
||||
title() {
|
||||
if (this.isNew) return 'New'
|
||||
return this.local.title || ''
|
||||
},
|
||||
slug() {
|
||||
return this.$route.params.product
|
||||
},
|
||||
isNew() {
|
||||
return this.slug.toLowerCase() === 'new'
|
||||
},
|
||||
isDraft() {
|
||||
return !this.local.published
|
||||
},
|
||||
isSlugNumber() {
|
||||
return !isNaN(this.slug.split(['-'], 1))
|
||||
},
|
||||
isValidSlug() {
|
||||
return this.isNew || this.isSlugNumber
|
||||
},
|
||||
isDownloaded() {
|
||||
return Object.keys(this.remote).length > 0
|
||||
},
|
||||
canSave() {
|
||||
if (this.isNew) return true
|
||||
return this.isSlugNumber && this.isDownloaded && this.hasChanges
|
||||
},
|
||||
hasChanges() {
|
||||
return this.$store.getters.productHasChanges
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
dialog(val) {
|
||||
val || this.close()
|
||||
},
|
||||
},
|
||||
|
||||
async beforeRouteLeave(to, from, next) {
|
||||
await this.$store.dispatch('learning/resetProduct')
|
||||
next()
|
||||
},
|
||||
|
||||
methods: {
|
||||
scrollToTop() {
|
||||
this.$nextTick(() => this.$refs.scrollStartingPoint.focus())
|
||||
},
|
||||
|
||||
close() {
|
||||
this.dialog = false
|
||||
},
|
||||
|
||||
isTabSelected(tab) {
|
||||
return this.selectedTab === tab || this.selectedTab === 'all'
|
||||
},
|
||||
|
||||
async getItem(url) {
|
||||
if (!this.isSlugNumber) return
|
||||
|
||||
try {
|
||||
this.$nextTick(() => this.$nuxt.$loading.start())
|
||||
let response = await this.$axios.get(`/learning-products/${url}`)
|
||||
await this.$store.commit('learning/SET_PRODUCT', response.data)
|
||||
if (response.data.synonyms.length > 0) {
|
||||
const attachedSynonymsIds = response.data.synonyms.map(({ id }) => id)
|
||||
this.$store.commit('learning/SELECT_SYNONYM', attachedSynonymsIds)
|
||||
}
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
} catch (error) {
|
||||
this.$nuxt.$loading.finish()
|
||||
this.$notifier.showMessage({
|
||||
content: `Error getting the learning product: ` + error.message,
|
||||
color: 'error',
|
||||
icon: 'icon-message',
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Scenarios
|
||||
* ---------------------------------------------------
|
||||
* Make draft without parent_id - Done
|
||||
* Make published - Done
|
||||
*
|
||||
* Edit draft without parent_id
|
||||
* Edit published without parent_id
|
||||
*
|
||||
* Make draft with parent_id (from published) - Done
|
||||
* Edit draft with parent_id (from published)
|
||||
|
||||
* Make published from draft with parent_id - backend
|
||||
*
|
||||
*/
|
||||
|
||||
async save(stay = false, published = false) {
|
||||
this.$nextTick(() => this.$nuxt.$loading.start())
|
||||
|
||||
try {
|
||||
const slug = await this.$store.dispatch(
|
||||
'learning/storeProduct',
|
||||
published
|
||||
)
|
||||
|
||||
await this.$store.dispatch('learning/pullProducts')
|
||||
|
||||
await this.$nuxt.$loading.finish()
|
||||
this.$notifier.showMessage({
|
||||
content: `Data saved successfully`,
|
||||
color: 'success',
|
||||
icon: 'mdi-check',
|
||||
})
|
||||
|
||||
if (!stay) {
|
||||
await this.$router.push(this.localePath('/manager/learning'))
|
||||
} else {
|
||||
await this.$router.push(
|
||||
this.localePath(`/manager/learning/${slug}?edit`)
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('save -> error', error)
|
||||
this.$nuxt.$loading.finish()
|
||||
this.$notifier.showMessage({
|
||||
content: `${
|
||||
this.slug.toLowerCase() === 'new' ? 'Creating' : 'Editing'
|
||||
} page: ${error.response ? error.response.data.message : error}.`,
|
||||
...(error.response && {
|
||||
errors: error.response.data.errors,
|
||||
}),
|
||||
color: 'error',
|
||||
icon: 'mdi-alert',
|
||||
})
|
||||
}
|
||||
this.close()
|
||||
},
|
||||
|
||||
async deleteProduct(productId = this.local.id) {
|
||||
if (!productId) return
|
||||
this.dialog = false
|
||||
await this.$store.dispatch('learning/deleteProduct', productId)
|
||||
this.$router.push(this.localePath('/manager/learning'))
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-card >>> .v-btn__content,
|
||||
.v-select .v-select__selection--comma,
|
||||
.v-chip__content,
|
||||
.v-list-item:not(.v-list-item--active):not(.v-list-item--disabled),
|
||||
.v-select.v-select--chips:not(.v-text-field--single-line).v-text-field--enclosed
|
||||
.v-select__selections,
|
||||
.v-list-item .v-list-item__title,
|
||||
.v-list-item .v-list-item__subtitle,
|
||||
.v-list-item span,
|
||||
.v-input--is-disabled input,
|
||||
.v-input--is-disable,
|
||||
.dot,
|
||||
h2,
|
||||
p {
|
||||
/* color: var(--v-txt-base); */
|
||||
}
|
||||
.v-card >>> .v-chip--disabled {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.v-card >>> .v-chip {
|
||||
/* padding-left: 0 !important; */
|
||||
background-color: var(--v-secondary-base) !important;
|
||||
font-size: 16px !important;
|
||||
}
|
||||
.v-card >>> .v-chip .mdi-close-circle::before {
|
||||
font-family: 'mijnggz';
|
||||
content: '\e907' !important;
|
||||
color: var(--v-secAccent-base) !important;
|
||||
font-size: 8px;
|
||||
margin-left: 2px;
|
||||
margin-right: 4px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.v-card >>> .v-chip .mdi-close-circle:hover::before {
|
||||
color: #bb1d28 !important;
|
||||
}
|
||||
|
||||
.dot {
|
||||
cursor: pointer;
|
||||
}
|
||||
.dot::before {
|
||||
content: '\A';
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: #e54e0f;
|
||||
display: block;
|
||||
position: relative;
|
||||
top: 30px;
|
||||
left: 50%;
|
||||
opacity: 0;
|
||||
}
|
||||
.dot:hover::before {
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
.dot.activeTab::before {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
</style>
|
||||
324
pages/manager/learning/filters/_filter.vue
Normal file
324
pages/manager/learning/filters/_filter.vue
Normal file
@@ -0,0 +1,324 @@
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<div class="d-flex justify-space-between">
|
||||
<h2 class="ml-6 txt--text">
|
||||
{{ $t(`learning.filters.${filter.title}`) | capitalize }}
|
||||
({{ filter.items.length }})
|
||||
</h2>
|
||||
<current-date-time showIcon class="mr-6" />
|
||||
</div>
|
||||
|
||||
<accordion-card :title="$t('general.list') | capitalize">
|
||||
<v-row v-for="(item, index) in filter.items" :key="item.title + index">
|
||||
<v-col cols="12" sm="3" md="3">
|
||||
<v-subheader class="ml-10 txt--text font-weight-black"
|
||||
>{{ index + 1 }}.
|
||||
</v-subheader>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="6">
|
||||
<v-text-field
|
||||
outlined
|
||||
:name="item.title + index"
|
||||
:value="item.title"
|
||||
append-icon="icon-close"
|
||||
@change="setDynamicTitle($event, index)"
|
||||
@click:append="askForDelete(item)"
|
||||
:append-outer-icon="
|
||||
hasItemChanges(index) ? 'mdi-content-save' : ''
|
||||
"
|
||||
@click:append-outer="update(item.id, index)"
|
||||
:error="hasItemChanges(index)"
|
||||
>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="3" md="3">
|
||||
<!-- <v-btn color="success" :to="`/manager/learning/filters/${slug}/${item.id}`">edit</v-btn> -->
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="3">
|
||||
<v-subheader class="ml-10 txt--text font-weight-black">
|
||||
Nieuw
|
||||
</v-subheader>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field v-model="newItem" outlined> </v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="3"> </v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="3"> </v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-btn
|
||||
class="cta-secondary"
|
||||
block
|
||||
depressed
|
||||
@click="addFilterItem"
|
||||
min-height="60px"
|
||||
>
|
||||
<v-icon x-small class="mx-4">icon-add</v-icon>
|
||||
|
||||
{{ $t('general.add') | capitalize }}</v-btn
|
||||
>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="3"> </v-col>
|
||||
</v-row>
|
||||
</accordion-card>
|
||||
|
||||
<v-dialog v-model="dialog" persistent max-width="740">
|
||||
<v-card class="primary pa-10" flat>
|
||||
<v-card-title class="headline">
|
||||
{{
|
||||
$t('learning.filters.delete_item_confirmation', {
|
||||
itemName: itemSelected.title,
|
||||
})
|
||||
}}
|
||||
</v-card-title>
|
||||
<v-card-actions>
|
||||
<div class="ma-4">
|
||||
<v-btn
|
||||
color="accent"
|
||||
class="mx-2"
|
||||
@click="deleteItem(itemSelected.id)"
|
||||
rounded
|
||||
depressed
|
||||
>{{ $t('general.delete') }}</v-btn
|
||||
>
|
||||
<v-btn
|
||||
class="mx-2"
|
||||
color="info"
|
||||
@click="close"
|
||||
rounded
|
||||
depressed
|
||||
>{{ $t('general.cancel') }}</v-btn
|
||||
>
|
||||
</div>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-col>
|
||||
|
||||
<v-footer fixed style="z-index: 4" height="90" color="primary">
|
||||
<v-btn text nuxt :to="localePath('/manager/learning/filters')">
|
||||
<v-icon>icon-arrow-left</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<!-- <v-btn
|
||||
v-cloak
|
||||
class="ma-2 white--text"
|
||||
color="accent"
|
||||
depressed
|
||||
rounded
|
||||
v-if="itemsHaveChanges"
|
||||
@click="saveAll"
|
||||
>
|
||||
SAVE</v-btn
|
||||
> -->
|
||||
</v-footer>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import accordionCard from '@/components/UI/AccordionCard/AccordionCard'
|
||||
import currentDateTime from '@/components/CurrentDateTime/CurrentDateTime'
|
||||
import PageHeader from '~/components/UI/PageHeader/PageHeader'
|
||||
|
||||
import Vue from 'vue'
|
||||
|
||||
export default {
|
||||
layout: `${process.env.CUSTOMER}Admin`,
|
||||
middleware: 'adminsOrOperators',
|
||||
components: {
|
||||
accordionCard,
|
||||
PageHeader,
|
||||
currentDateTime,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
newItem: '',
|
||||
filter: { items: [] },
|
||||
items: [],
|
||||
itemSelected: {},
|
||||
dialog: false,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (!this.isSlugNumber) this.$router.push('/manager')
|
||||
if (this.isNew) return
|
||||
if (this.isSlugNumber) this.getFilter(this.slug)
|
||||
},
|
||||
|
||||
computed: {
|
||||
slug() {
|
||||
return this.$route.params.filter
|
||||
},
|
||||
isSlugNumber() {
|
||||
return !isNaN(this.slug.split(['-'], 1))
|
||||
},
|
||||
remoteItemsTitles() {
|
||||
if (!this.filter || !(this.filter.items.length > 0)) return []
|
||||
return this.filter.items.map(({ title }) => title)
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async getFilter(url) {
|
||||
if (!this.isSlugNumber) return
|
||||
|
||||
this.$nextTick(() => this.$nuxt.$loading.start())
|
||||
|
||||
try {
|
||||
const response = await this.$axios.get(`/filters/${url}`)
|
||||
|
||||
this.filter = { ...response.data }
|
||||
|
||||
this.items = response.data.items.map(({ title }) => title)
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
} catch (error) {
|
||||
this.$nuxt.$loading.finish()
|
||||
this.$notifier.showMessage({
|
||||
content: `Error getting the filter: ` + error.message,
|
||||
color: 'error',
|
||||
icon: 'icon-message',
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
async getFilters() {
|
||||
const response = await this.$axios.get('/filters')
|
||||
const filters = response.data
|
||||
await this.$store.commit('learning/SORT_FILTERS', filters)
|
||||
},
|
||||
|
||||
async addFilterItem() {
|
||||
const data = {
|
||||
filter_id: this.filter.id,
|
||||
title: this.newItem,
|
||||
}
|
||||
this.$nextTick(() => this.$nuxt.$loading.start())
|
||||
|
||||
try {
|
||||
await this.$axios.post(`/filter-items`, data)
|
||||
await this.getFilters()
|
||||
this.getFilter(this.slug)
|
||||
this.newItem = ''
|
||||
this.$nuxt.$loading.finish()
|
||||
} catch (error) {
|
||||
this.$nuxt.$loading.finish()
|
||||
this.$notifier.showMessage({
|
||||
content: `Error creating the filter item: ` + error.message,
|
||||
color: 'error',
|
||||
icon: 'icon-message',
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
setDynamicTitle(title, index) {
|
||||
Vue.set(this.items, index, title)
|
||||
},
|
||||
|
||||
hasItemChanges(index) {
|
||||
return this.items[index] !== this.remoteItemsTitles[index]
|
||||
},
|
||||
|
||||
close() {
|
||||
this.dialog = false
|
||||
},
|
||||
|
||||
save() {
|
||||
this.close()
|
||||
},
|
||||
|
||||
async update(id, index) {
|
||||
if (
|
||||
id === null ||
|
||||
id === undefined ||
|
||||
index === null ||
|
||||
index === undefined ||
|
||||
!this.items[index]
|
||||
)
|
||||
return
|
||||
|
||||
const data = {
|
||||
id: id,
|
||||
title: this.items[index],
|
||||
}
|
||||
this.$nextTick(() => this.$nuxt.$loading.start())
|
||||
|
||||
try {
|
||||
await this.$axios.post(`/filter-items`, data)
|
||||
|
||||
await this.getFilters()
|
||||
this.getFilter(this.slug)
|
||||
this.$nuxt.$loading.finish()
|
||||
this.$notifier.showMessage({
|
||||
content: `Filter Item updated successfully`,
|
||||
color: 'success',
|
||||
icon: 'icon-message',
|
||||
})
|
||||
} catch (error) {
|
||||
this.$nuxt.$loading.finish()
|
||||
this.$notifier.showMessage({
|
||||
content: `Error updating the filter item: ` + error.message,
|
||||
color: 'error',
|
||||
icon: 'icon-message',
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
askForDelete(item) {
|
||||
this.itemSelected = item
|
||||
this.dialog = true
|
||||
},
|
||||
|
||||
async deleteItem(itemId) {
|
||||
if (!itemId) {
|
||||
this.$notifier.showMessage({
|
||||
content: `No item to delete selected`,
|
||||
color: 'error',
|
||||
icon: 'icon-message',
|
||||
})
|
||||
}
|
||||
|
||||
this.$nextTick(() => this.$nuxt.$loading.start())
|
||||
|
||||
try {
|
||||
await this.$axios.delete(`/filter-items/${itemId}`)
|
||||
|
||||
this.dialog = false
|
||||
this.itemSelected = {}
|
||||
|
||||
await this.getFilter(this.slug)
|
||||
await this.getFilters()
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
|
||||
this.$notifier.showMessage({
|
||||
content: `Filter Item deleted`,
|
||||
color: 'success',
|
||||
icon: 'mdi-delete',
|
||||
})
|
||||
} catch (error) {
|
||||
this.dialog = false
|
||||
this.$nuxt.$loading.finish()
|
||||
|
||||
this.$notifier.showMessage({
|
||||
content: `Error trying to delete the selected Filter Item`,
|
||||
color: 'error',
|
||||
icon: 'mdi-delete',
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.v-dialog >>> .v-cardcard__title {
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
136
pages/manager/learning/filters/index.vue
Normal file
136
pages/manager/learning/filters/index.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<div>
|
||||
<page-header class="d-flex justify-space-between">
|
||||
<span>
|
||||
{{ $t('learning.filters.title') }}
|
||||
</span>
|
||||
|
||||
<!-- <v-btn
|
||||
depressed
|
||||
:to="localePath('/manager/learning/filters/new')"
|
||||
color="accent"
|
||||
rounded
|
||||
>
|
||||
<v-icon left>mdi-plus</v-icon>
|
||||
<small>{{ $t('general.add') }}</small>
|
||||
</v-btn> -->
|
||||
</page-header>
|
||||
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="filters"
|
||||
:items-per-page="100"
|
||||
hide-default-footer
|
||||
class="pa-4 secondary"
|
||||
>
|
||||
<!-- @click:row="handleClick" -->
|
||||
<!-- Translates dynamically headers -->
|
||||
<template v-for="h in headers" v-slot:[`header.${h.value}`]="{ header }">
|
||||
{{ $t(h.text) }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.title="{ item }">
|
||||
<strong class="txt--text">{{
|
||||
$t(`learning.filters.${item.title}`) | capitalize
|
||||
}}</strong>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn
|
||||
class="mx-4 white--text button-hover"
|
||||
style="height: 100%"
|
||||
:color="$vuetify.theme.dark ? 'info' : 'txt'"
|
||||
rounded
|
||||
depressed
|
||||
nuxt
|
||||
small
|
||||
@click="handleClick(item)"
|
||||
>{{ $t('general.edit') }}</v-btn
|
||||
>
|
||||
|
||||
<v-menu offset-y>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-hover v-slot:default="{ hover }">
|
||||
<v-btn
|
||||
class="dots"
|
||||
v-on="on"
|
||||
depressed
|
||||
:outlined="hover"
|
||||
small
|
||||
fab
|
||||
:color="hover ? 'info' : ''"
|
||||
>
|
||||
<v-icon>mdi-dots-horizontal</v-icon>
|
||||
</v-btn>
|
||||
</v-hover>
|
||||
</template>
|
||||
<v-list width="200">
|
||||
<v-list-item @click="handleClick(item)">
|
||||
<v-list-item-icon class="mr-1">
|
||||
<v-icon small>mdi-pencil</v-icon>
|
||||
</v-list-item-icon>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-subtitle>
|
||||
{{ $t('general.edit') | capitalize }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PageHeader from '~/components/UI/PageHeader/PageHeader'
|
||||
import accordionCard from '@/components/UI/AccordionCard/AccordionCard'
|
||||
|
||||
export default {
|
||||
layout: `${process.env.CUSTOMER}Admin`,
|
||||
middleware: 'adminsOrOperators',
|
||||
components: {
|
||||
PageHeader,
|
||||
accordionCard,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
headers: [
|
||||
{ text: 'naam', value: 'title' },
|
||||
{ text: 'aantal', value: 'items_count' },
|
||||
// { text: 'sinds', value: 'created_at' },
|
||||
// { text: 'update', value: 'last_updated_at' },
|
||||
{ text: '', value: 'actions', sortable: false },
|
||||
],
|
||||
filters: [],
|
||||
}
|
||||
},
|
||||
async asyncData({ $axios }) {
|
||||
const { data } = await $axios.get('/filters-with-count')
|
||||
|
||||
const filtersFiltered = data.filter((f) => f.title !== 'quality_standards')
|
||||
|
||||
return { filters: filtersFiltered }
|
||||
},
|
||||
methods: {
|
||||
handleClick(item) {
|
||||
this.$router.push(
|
||||
this.localePath(`/manager/learning/filters/${item.slug}`)
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
table tr .button-hover.v-btn {
|
||||
opacity: 0;
|
||||
}
|
||||
table tr:hover .button-hover.v-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
.dots {
|
||||
background-color: unset !important;
|
||||
}
|
||||
</style>
|
||||
328
pages/manager/learning/index.vue
Normal file
328
pages/manager/learning/index.vue
Normal file
@@ -0,0 +1,328 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="pa-lg-6">
|
||||
<page-header class="d-flex justify-space-between">
|
||||
<div class="subtitle-1">
|
||||
<span
|
||||
:class="{ 'display-1': selector === 'published' }"
|
||||
:style="{ cursor: 'pointer' }"
|
||||
@click="selector = 'published'"
|
||||
class="mr-lg-4"
|
||||
>
|
||||
{{ $t('learning.products') }}
|
||||
<small class="font-weight-light">({{ published.length }})</small>
|
||||
</span>
|
||||
|
||||
<span
|
||||
:class="{ 'display-1': selector === 'drafts' }"
|
||||
:style="{ cursor: 'pointer' }"
|
||||
@click="selector = 'drafts'"
|
||||
class="mx-lg-4"
|
||||
v-if="$store.getters.isAdmin || $store.getters.isOperator"
|
||||
>
|
||||
{{ $t('learning.drafts') }}
|
||||
<small class="font-weight-light">({{ drafts.length }})</small>
|
||||
</span>
|
||||
<span
|
||||
:class="{ 'display-1': selector === 'deleted' }"
|
||||
:style="{ cursor: 'pointer' }"
|
||||
@click="selector = 'deleted'"
|
||||
class="mx-lg-4"
|
||||
v-if="$store.getters.isAdmin || $store.getters.isOperator"
|
||||
>
|
||||
{{ $t('general.deleted') }}
|
||||
<small class="font-weight-light">({{ deleted.length }})</small>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<settings />
|
||||
|
||||
<v-btn
|
||||
color="accent"
|
||||
depressed
|
||||
rounded
|
||||
to="/manager/learning/new"
|
||||
v-if="$store.getters.isAdmin || $store.getters.isOperator"
|
||||
>
|
||||
<v-icon left x-small>icon-add</v-icon>
|
||||
<small>{{ $t('general.add') }}</small>
|
||||
</v-btn>
|
||||
</div>
|
||||
</page-header>
|
||||
|
||||
<div class="d-flex justify-space-between">
|
||||
<filters />
|
||||
<filters-selected />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<courses-table
|
||||
:deleted="deleted"
|
||||
:drafts="drafts"
|
||||
:published="published"
|
||||
:selector="selector"
|
||||
@reload-learning-products="getProducts()"
|
||||
/>
|
||||
|
||||
<v-footer
|
||||
fixed
|
||||
style="z-index: 4"
|
||||
height="90"
|
||||
color="primary"
|
||||
v-if="$store.getters.isAdmin || $store.getters.isOperator"
|
||||
>
|
||||
<v-btn text nuxt :to="localePath('/manager/learning')">
|
||||
<v-icon>icon-arrow-left</v-icon>
|
||||
</v-btn>
|
||||
<div class="mx-10">
|
||||
<v-btn
|
||||
color="accent"
|
||||
depressed
|
||||
rounded
|
||||
to="/manager/learning/new"
|
||||
class="ml-10"
|
||||
>
|
||||
<v-icon left size="8">icon-add</v-icon>
|
||||
<small>{{ $t('general.add') }}</small>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-spacer />
|
||||
|
||||
<!-- <v-btn
|
||||
class="ma-2"
|
||||
tile
|
||||
text
|
||||
small
|
||||
color="success"
|
||||
nuxt
|
||||
target="_blank"
|
||||
:to="`${this.localePath(
|
||||
'/manager/learning'
|
||||
)}?filters=${sharedUrlSuffix}`"
|
||||
id="url_to_share"
|
||||
v-show="showCopiedButton"
|
||||
>
|
||||
<v-icon class="mx-2" small>icon-selectionbox-checked</v-icon>
|
||||
URL gekopieerd
|
||||
</v-btn>
|
||||
|
||||
<v-tooltip top v-if="!showCopiedButton">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn
|
||||
class="ma-2"
|
||||
tile
|
||||
text
|
||||
small
|
||||
@click="copyUrl"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
>
|
||||
<v-icon class="mx-2" small>icon-share</v-icon>
|
||||
Delen Lijst
|
||||
</v-btn>
|
||||
</template>
|
||||
<span> {{ $t('general.tooltip_share') }}</span>
|
||||
</v-tooltip> -->
|
||||
|
||||
<v-btn class="ma-2" tile text small @click="exportCsv">
|
||||
<v-icon class="mx-2" x-small>icon-export</v-icon>
|
||||
{{ $t('general.export_csv') | capitalize }}
|
||||
</v-btn>
|
||||
<settings class="ml-10" />
|
||||
</v-footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Util from '@/util'
|
||||
import PageHeader from '~/components/UI/PageHeader/PageHeader'
|
||||
import Settings from '~/components/Learning/Settings'
|
||||
import Filters from '~/components/Learning/Filters'
|
||||
import FiltersSelected from '~/components/Learning/FiltersSelected'
|
||||
import CoursesTable from '~/components/Learning/CoursesTable'
|
||||
|
||||
export default {
|
||||
layout: `${process.env.CUSTOMER}Admin`,
|
||||
middleware: ['denyToOnlyMembers'],
|
||||
components: {
|
||||
PageHeader,
|
||||
Settings,
|
||||
Filters,
|
||||
CoursesTable,
|
||||
FiltersSelected,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selector: 'published',
|
||||
showCopiedButton: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
learningProducts() {
|
||||
return this.$store.getters.learningProducts
|
||||
},
|
||||
hasProducts() {
|
||||
return this.$store.getters.hasLearningProducts
|
||||
},
|
||||
|
||||
published() {
|
||||
return this.learningProductsFiltered.filter(
|
||||
(product) => product.published && !product.deleted_at
|
||||
)
|
||||
},
|
||||
drafts() {
|
||||
return this.learningProductsFiltered.filter(
|
||||
(product) => !product.deleted_at && !product.published
|
||||
)
|
||||
},
|
||||
deleted() {
|
||||
return this.learningProductsFiltered.filter(
|
||||
(product) => product.deleted_at
|
||||
)
|
||||
},
|
||||
filtersSelected() {
|
||||
return this.$store.getters.filtersSelected
|
||||
},
|
||||
learningProductsFiltered() {
|
||||
if (!this.filtersSelected.length > 0) return this.learningProducts
|
||||
|
||||
return this.learningProducts.filter((product) => {
|
||||
return Util.findItemsWithExactValuesInArray(
|
||||
this.filtersSelected,
|
||||
product.filterItemsSelected
|
||||
)
|
||||
})
|
||||
},
|
||||
|
||||
// sharedUrlSuffix() {
|
||||
// let filtersSelectedIdsStringyfied = ''
|
||||
// this.filtersSelected.map(
|
||||
// (id) => (filtersSelectedIdsStringyfied += `${id},`)
|
||||
// )
|
||||
// return filtersSelectedIdsStringyfied
|
||||
// },
|
||||
// sharedUrlParsed() {
|
||||
// // if is a shared Url
|
||||
// if (!this.$route.query.filters) return null
|
||||
|
||||
// // parse content, convert every element to number
|
||||
// let filterItemIds = this.$route.query.filters.split(',').map(Number)
|
||||
|
||||
// // validate it and return
|
||||
// return filterItemIds.filter(
|
||||
// (el) => !isNaN(el) && this.$store.getters.filtersItemsMap.has(el)
|
||||
// )
|
||||
// },
|
||||
},
|
||||
|
||||
async asyncData({ $axios, store }) {
|
||||
try {
|
||||
if (!store.getters.hasLearningProducts) {
|
||||
await store.dispatch('learning/pullProducts')
|
||||
}
|
||||
|
||||
const response = await $axios.get('/filters')
|
||||
const filters = response.data
|
||||
await store.commit('learning/SORT_FILTERS', filters)
|
||||
} catch (error) {
|
||||
console.log('Data -> error', error)
|
||||
}
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
if (this.sharedUrlParsed) {
|
||||
this.$store.commit('learning/SELECT_FILTERS', this.sharedUrlParsed)
|
||||
}
|
||||
|
||||
// If filters sorting preferences saved in localstorage
|
||||
if (localStorage.getItem('learning_products_filters')) {
|
||||
// Get them
|
||||
const filtersOrder = JSON.parse(
|
||||
localStorage.getItem('learning_products_filters')
|
||||
)
|
||||
|
||||
// Get filters from store
|
||||
const filters = [...this.$store.state.learning.filters]
|
||||
|
||||
// Sort by preferences
|
||||
const filtersSorted = filters.sort(
|
||||
(a, b) => filtersOrder.indexOf(a.id) - filtersOrder.indexOf(b.id)
|
||||
)
|
||||
|
||||
// send to store, sorted
|
||||
await this.$store.commit('learning/SORT_FILTERS', filtersSorted)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async getProducts() {
|
||||
try {
|
||||
const response = await this.$axios.get('/learning-products')
|
||||
await this.$store.commit('learning/STORE_PRODUCTS', response.data)
|
||||
} catch (error) {
|
||||
console.log('getProducts -> error', error)
|
||||
}
|
||||
},
|
||||
async copyUrl() {
|
||||
if (this.showCopiedButton) return
|
||||
|
||||
// Create <a> link with v-show false and Copy from there to have the full path?
|
||||
const url = document.getElementById('url_to_share').href
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(url)
|
||||
|
||||
this.showCopiedButton = true
|
||||
setTimeout(() => (this.showCopiedButton = false), 5000)
|
||||
} catch (error) {
|
||||
console.log('copyUrl -> error', error)
|
||||
}
|
||||
},
|
||||
|
||||
exportCsv() {
|
||||
const data = [...this.learningProductsFiltered]
|
||||
|
||||
const headersNeeded = [
|
||||
'title',
|
||||
'code',
|
||||
'partner',
|
||||
'owner',
|
||||
'status',
|
||||
'lead_time',
|
||||
'product_type',
|
||||
'theme',
|
||||
'course',
|
||||
]
|
||||
|
||||
const dataFiltered = this.$store.getters[
|
||||
'utils/filterArrayObjsByArrayOfProperties'
|
||||
](data, headersNeeded)
|
||||
|
||||
let dataCsv =
|
||||
this.$store.getters['utils/arrayOfObjectToCsv'](dataFiltered)
|
||||
|
||||
// Translate every entry in 'headersNeeded'
|
||||
let headersTranslated =
|
||||
headersNeeded.map((h) => this.$t(`csv.learning.${h}`)).join(',') + '\n'
|
||||
|
||||
// Remove 1st row (string)
|
||||
dataCsv = dataCsv.split('\n').slice(1).join('\n')
|
||||
|
||||
// Add as 1st row the one translated
|
||||
dataCsv = headersTranslated + dataCsv
|
||||
|
||||
// Simulate click to download file
|
||||
const universalBOM = '\uFEFF'
|
||||
const blob = new Blob([universalBOM + dataCsv], { type: 'text/csv' })
|
||||
const link = document.createElement('a')
|
||||
link.href = URL.createObjectURL(blob)
|
||||
link.setAttribute('hidden', '')
|
||||
link.setAttribute('download', 'producten.csv')
|
||||
link.click()
|
||||
URL.revokeObjectURL(link.href)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
272
pages/manager/learning/quality-standards/_filter.vue
Normal file
272
pages/manager/learning/quality-standards/_filter.vue
Normal file
@@ -0,0 +1,272 @@
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<div class="d-flex justify-space-between">
|
||||
<h2 class="ml-6 txt--text">{{ title }}</h2>
|
||||
<current-date-time showIcon class="mr-6" />
|
||||
</div>
|
||||
|
||||
<accordion-card disableAccordion>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="3">
|
||||
<v-subheader class="ml-10 txt--text font-weight-black">
|
||||
Kwaliteitsstandaard
|
||||
</v-subheader>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field v-model="item.title" outlined> </v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="3"> </v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="3">
|
||||
<v-subheader class="ml-10 txt--text font-weight-black">
|
||||
Externe link
|
||||
</v-subheader>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field
|
||||
v-model="item.link"
|
||||
outlined
|
||||
prepend-inner-icon="icon-link"
|
||||
>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="3"> Voeg link toe</v-col>
|
||||
</v-row>
|
||||
</accordion-card>
|
||||
</v-col>
|
||||
|
||||
<v-footer fixed style="z-index: 4" height="90" color="primary">
|
||||
<v-btn text nuxt :to="localePath('/manager/learning/quality-standards')">
|
||||
<v-icon>icon-arrow-left</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
v-cloak
|
||||
class="ma-2 white--text"
|
||||
color="txt"
|
||||
depressed
|
||||
rounded
|
||||
@click="save(true)"
|
||||
v-if="canSave && !isNew"
|
||||
>
|
||||
Opslaan</v-btn
|
||||
>
|
||||
|
||||
<v-btn
|
||||
v-cloak
|
||||
class="ma-2 white--text"
|
||||
color="accent"
|
||||
depressed
|
||||
rounded
|
||||
@click="save(false)"
|
||||
v-if="canSave"
|
||||
>
|
||||
Opslaan en sluiten</v-btn
|
||||
>
|
||||
|
||||
<v-spacer />
|
||||
|
||||
<v-dialog v-model="dialog" persistent max-width="740">
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn class="ma-2" tile text small v-if="!isNew" v-on="on">
|
||||
<v-icon class="mx-2">icon-remove</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card class="primary pa-10" flat>
|
||||
<v-card-title class="headline">
|
||||
{{
|
||||
$t('learning.product_overview.delete_confirmation', {
|
||||
productName: item.title,
|
||||
})
|
||||
}}
|
||||
</v-card-title>
|
||||
<v-card-actions>
|
||||
<div class="ma-4">
|
||||
<v-btn
|
||||
color="accent"
|
||||
class="mx-2"
|
||||
@click="deleteItem(item.id)"
|
||||
rounded
|
||||
depressed
|
||||
>{{ $t('general.delete') }}</v-btn
|
||||
>
|
||||
<v-btn
|
||||
class="mx-2"
|
||||
color="info"
|
||||
@click="close"
|
||||
rounded
|
||||
depressed
|
||||
>{{ $t('general.cancel') }}</v-btn
|
||||
>
|
||||
</div>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-footer>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import accordionCard from '@/components/UI/AccordionCard/AccordionCard'
|
||||
import currentDateTime from '@/components/CurrentDateTime/CurrentDateTime'
|
||||
import PageHeader from '~/components/UI/PageHeader/PageHeader'
|
||||
|
||||
export default {
|
||||
layout: `${process.env.CUSTOMER}Admin`,
|
||||
middleware: 'adminsOrOperators',
|
||||
components: {
|
||||
accordionCard,
|
||||
PageHeader,
|
||||
currentDateTime,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
remoteItem: {},
|
||||
item: { title: null },
|
||||
dialog: false,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.isSlugNumber) this.getFilter(this.slug)
|
||||
// if (!this.isNew) this.$router.push('/manager')
|
||||
},
|
||||
|
||||
computed: {
|
||||
slug() {
|
||||
return this.$route.params.filter
|
||||
},
|
||||
isSlugNumber() {
|
||||
return !isNaN(this.slug.split(['-'], 1))
|
||||
},
|
||||
isNew() {
|
||||
return this.slug.toLowerCase() === 'new'
|
||||
},
|
||||
|
||||
title() {
|
||||
if (!this.item || !this.item.title) return 'Nieuw'
|
||||
return this.item.title || ''
|
||||
},
|
||||
canSave() {
|
||||
if (this.isNew && this.itemFilled) return true
|
||||
if (this.itemFilled && this.itemHasChanges) return true
|
||||
return false
|
||||
},
|
||||
|
||||
itemFilled() {
|
||||
if (!this.item || !this.item.title) return false
|
||||
return true
|
||||
},
|
||||
|
||||
itemHasChanges() {
|
||||
return JSON.stringify(this.remoteItem) !== JSON.stringify(this.item)
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async getFilter(url) {
|
||||
if (!this.isSlugNumber) return
|
||||
|
||||
this.$nextTick(() => this.$nuxt.$loading.start())
|
||||
|
||||
try {
|
||||
const { data } = await this.$axios.get(`/filter-items/${url}`)
|
||||
|
||||
this.remoteItem = { ...data }
|
||||
this.item = { ...data }
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
} catch (error) {
|
||||
this.$nuxt.$loading.finish()
|
||||
this.$notifier.showMessage({
|
||||
content: `Error getting the filter: ` + error.message,
|
||||
color: 'error',
|
||||
icon: 'icon-message',
|
||||
})
|
||||
this.$router.push('/manager/learning/quality-standards/')
|
||||
}
|
||||
},
|
||||
|
||||
close() {
|
||||
this.dialog = false
|
||||
},
|
||||
|
||||
async save(stay = false) {
|
||||
if (this.dialog) this.close()
|
||||
|
||||
this.$nextTick(() => this.$nuxt.$loading.start())
|
||||
|
||||
const QUALITY_STANDARD_ID = 14
|
||||
|
||||
const data = {
|
||||
...this.item,
|
||||
filter_id: this.item.filter_id || QUALITY_STANDARD_ID,
|
||||
}
|
||||
|
||||
try {
|
||||
await this.$axios.post(`/filter-items`, data)
|
||||
|
||||
this.remoteItem = { ...data }
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
|
||||
if (!stay) this.$router.push('/manager/learning/quality-standards/')
|
||||
} catch (error) {
|
||||
this.$nuxt.$loading.finish()
|
||||
this.$notifier.showMessage({
|
||||
content: `Error creating the filter item: ` + error.message,
|
||||
color: 'error',
|
||||
icon: 'icon-message',
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
async deleteItem(itemId) {
|
||||
if (!itemId) {
|
||||
this.$notifier.showMessage({
|
||||
content: `No item to delete selected`,
|
||||
color: 'error',
|
||||
icon: 'icon-message',
|
||||
})
|
||||
}
|
||||
|
||||
this.$nextTick(() => this.$nuxt.$loading.start())
|
||||
|
||||
try {
|
||||
await this.$axios.delete(`/filter-items/${itemId}`)
|
||||
|
||||
this.dialog = false
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
|
||||
this.$notifier.showMessage({
|
||||
content: `Filter Item deleted`,
|
||||
color: 'success',
|
||||
icon: 'mdi-delete',
|
||||
})
|
||||
|
||||
this.$router.push('/manager/learning/quality-standards/')
|
||||
} catch (error) {
|
||||
this.dialog = false
|
||||
this.$nuxt.$loading.finish()
|
||||
|
||||
this.$notifier.showMessage({
|
||||
content: `Error trying to delete the selected Filter Item`,
|
||||
color: 'error',
|
||||
icon: 'mdi-delete',
|
||||
})
|
||||
|
||||
this.$router.push('/manager/learning/quality-standards/')
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.v-dialog >>> .v-cardcard__title {
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
202
pages/manager/learning/quality-standards/index.vue
Normal file
202
pages/manager/learning/quality-standards/index.vue
Normal file
@@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<div>
|
||||
<page-header class="d-flex justify-space-between">
|
||||
<span>
|
||||
{{ $t('learning.quality_standards') }}
|
||||
</span>
|
||||
|
||||
<v-btn
|
||||
depressed
|
||||
:to="localePath('/manager/learning/quality-standards/new')"
|
||||
color="accent"
|
||||
rounded
|
||||
>
|
||||
<v-icon left>mdi-plus</v-icon>
|
||||
<small>{{ $t('general.add') }}</small>
|
||||
</v-btn>
|
||||
</page-header>
|
||||
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="filter.items"
|
||||
:items-per-page="10"
|
||||
class="pa-4 secondary"
|
||||
>
|
||||
<!-- Translates dynamically headers -->
|
||||
<template v-for="h in headers" v-slot:[`header.${h.value}`]="{ header }">
|
||||
{{ $t(h.text) }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.name="{ item }">
|
||||
<strong class="txt--text" v-text="item.name"></strong>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-menu offset-y>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-hover v-slot:default="{ hover }">
|
||||
<v-btn
|
||||
v-on="on"
|
||||
depressed
|
||||
:outlined="hover"
|
||||
small
|
||||
fab
|
||||
:color="hover ? 'info' : ''"
|
||||
>
|
||||
<v-icon>mdi-dots-horizontal</v-icon>
|
||||
</v-btn>
|
||||
</v-hover>
|
||||
</template>
|
||||
<v-list width="200">
|
||||
<v-list-item
|
||||
nuxt
|
||||
:to="localePath(`/manager/learning/quality-standards/${item.id}`)"
|
||||
>
|
||||
<v-list-item-icon class="mr-1">
|
||||
<v-icon small>mdi-pencil</v-icon>
|
||||
</v-list-item-icon>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-subtitle>
|
||||
{{ $t('general.edit') | capitalize }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<v-list-item-icon class="mr-1">
|
||||
<v-icon small>icon-link</v-icon>
|
||||
</v-list-item-icon>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-subtitle>
|
||||
Link naar informatie
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-dialog max-width="740" persistent v-model="dialog">
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-list-item v-on="on">
|
||||
<v-list-item-icon class="mr-1">
|
||||
<v-icon small>icon-remove</v-icon>
|
||||
</v-list-item-icon>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-subtitle>{{
|
||||
$t('general.delete') | capitalize
|
||||
}}</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<v-card class="primary pa-10" flat>
|
||||
<v-card-title class="headline">
|
||||
{{
|
||||
$t('learning.product_overview.delete_confirmation', {
|
||||
productName: item.title,
|
||||
})
|
||||
}}
|
||||
</v-card-title>
|
||||
<v-card-actions>
|
||||
<div class="ma-4">
|
||||
<v-btn
|
||||
@click="deleteItem(item.id)"
|
||||
class="mx-2"
|
||||
color="accent"
|
||||
depressed
|
||||
rounded
|
||||
>{{ $t('general.delete') }}</v-btn
|
||||
>
|
||||
<v-btn
|
||||
@click="close"
|
||||
class="mx-2"
|
||||
color="info"
|
||||
depressed
|
||||
rounded
|
||||
>{{ $t('general.cancel') }}</v-btn
|
||||
>
|
||||
</div>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PageHeader from '~/components/UI/PageHeader/PageHeader'
|
||||
import accordionCard from '@/components/UI/AccordionCard/AccordionCard'
|
||||
|
||||
export default {
|
||||
layout: `${process.env.CUSTOMER}Admin`,
|
||||
components: {
|
||||
PageHeader,
|
||||
accordionCard,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
headers: [
|
||||
{ text: 'naam', sortable: false, value: 'title' },
|
||||
{ text: '', value: 'actions' },
|
||||
],
|
||||
filter: {},
|
||||
dialog: false,
|
||||
}
|
||||
},
|
||||
async asyncData({ $axios }) {
|
||||
try {
|
||||
const { data } = await $axios.get(`/filters`)
|
||||
|
||||
const qualityStandards = data.find((f) => f.title === 'quality_standards')
|
||||
|
||||
return { filter: { ...qualityStandards } }
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
this.dialog = false
|
||||
},
|
||||
async deleteItem(itemId) {
|
||||
if (!itemId) {
|
||||
this.$notifier.showMessage({
|
||||
content: `No item to delete selected`,
|
||||
color: 'error',
|
||||
icon: 'icon-message',
|
||||
})
|
||||
}
|
||||
|
||||
this.$nextTick(() => this.$nuxt.$loading.start())
|
||||
|
||||
try {
|
||||
await this.$axios.delete(`/filter-items/${itemId}`)
|
||||
|
||||
this.dialog = false
|
||||
|
||||
this.filter.items = this.filter.items.filter((i) => i.id !== itemId)
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
|
||||
this.$notifier.showMessage({
|
||||
content: `Filter Item deleted`,
|
||||
color: 'success',
|
||||
icon: 'mdi-delete',
|
||||
})
|
||||
} catch (error) {
|
||||
this.dialog = false
|
||||
this.$nuxt.$loading.finish()
|
||||
|
||||
this.$notifier.showMessage({
|
||||
content: `Error trying to delete the selected Filter Item`,
|
||||
color: 'error',
|
||||
icon: 'mdi-delete',
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
179
pages/manager/learning/synonyms/index.vue
Normal file
179
pages/manager/learning/synonyms/index.vue
Normal file
@@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<div>
|
||||
<page-header class="d-flex justify-space-between">
|
||||
<span>
|
||||
{{ $t('learning.synonyms') }}
|
||||
</span>
|
||||
</page-header>
|
||||
|
||||
<accordion-card disableAccordion>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="3"></v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-chip
|
||||
v-for="(synonym, i) in synonyms"
|
||||
:key="`synonym-key${i}`"
|
||||
@click:close="askForDelete(synonym)"
|
||||
class="ma-2"
|
||||
color="secondary"
|
||||
close
|
||||
>
|
||||
{{ synonym.title }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="3"> </v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="3">
|
||||
<v-subheader class="txt--text font-weight-black ml-10">
|
||||
Nieuw
|
||||
</v-subheader>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field v-model="newSynonym" outlined v-on:keyup.enter="add">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="3"> </v-col>
|
||||
</v-row>
|
||||
</accordion-card>
|
||||
|
||||
<v-dialog v-model="dialog" persistent max-width="740" v-if="itemSelected">
|
||||
<v-card class="primary pa-10" flat>
|
||||
<v-card-title class="headline">
|
||||
{{
|
||||
$t('learning.filters.delete_item_confirmation', {
|
||||
itemName: itemSelected.title || null,
|
||||
})
|
||||
}}
|
||||
</v-card-title>
|
||||
<v-card-actions>
|
||||
<div class="ma-4">
|
||||
<v-btn
|
||||
color="accent"
|
||||
class="mx-2"
|
||||
@click="deleteItem(itemSelected)"
|
||||
rounded
|
||||
depressed
|
||||
>{{ $t('general.delete') }}</v-btn
|
||||
>
|
||||
<v-btn class="mx-2" color="info" @click="close" rounded depressed>{{
|
||||
$t('general.cancel')
|
||||
}}</v-btn>
|
||||
</div>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PageHeader from '~/components/UI/PageHeader/PageHeader'
|
||||
import accordionCard from '@/components/UI/AccordionCard/AccordionCard'
|
||||
|
||||
export default {
|
||||
layout: `${process.env.CUSTOMER}Admin`,
|
||||
middleware: 'adminsOrOperators',
|
||||
components: {
|
||||
PageHeader,
|
||||
accordionCard,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
synonyms: [],
|
||||
newSynonym: '',
|
||||
dialog: false,
|
||||
itemSelected: null,
|
||||
}
|
||||
},
|
||||
async asyncData({ $axios }) {
|
||||
const response = await $axios.get('/synonyms')
|
||||
return { synonyms: response.data }
|
||||
},
|
||||
methods: {
|
||||
async add() {
|
||||
if (!this.newSynonym) return
|
||||
if (this.synonyms.includes(this.newSynonym)) return
|
||||
|
||||
this.$nextTick(() => this.$nuxt.$loading.start())
|
||||
|
||||
try {
|
||||
const response = await this.$axios.post(`/synonyms`, {
|
||||
title: this.newSynonym,
|
||||
})
|
||||
this.synonyms.push(response.data)
|
||||
this.newSynonym = ''
|
||||
this.$nuxt.$loading.finish()
|
||||
this.$notifier.showMessage({
|
||||
content: `Synonym '${this.newSynonym}' added`,
|
||||
color: 'success',
|
||||
icon: 'icon-message',
|
||||
})
|
||||
} catch (error) {
|
||||
console.log('add -> error', error)
|
||||
this.$nuxt.$loading.finish()
|
||||
this.$notifier.showMessage({
|
||||
content: `Error creating the filter item: ` + error.message,
|
||||
color: 'error',
|
||||
icon: 'icon-message',
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
close() {
|
||||
this.dialog = false
|
||||
},
|
||||
|
||||
askForDelete(item) {
|
||||
this.itemSelected = item
|
||||
this.dialog = true
|
||||
},
|
||||
|
||||
async deleteItem(synonym) {
|
||||
if (!synonym) {
|
||||
this.$notifier.showMessage({
|
||||
content: `No synonym to delete passed`,
|
||||
color: 'error',
|
||||
icon: 'icon-message',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const indexSynonym = this.synonyms.findIndex(
|
||||
(s) => s.title === synonym.title
|
||||
)
|
||||
|
||||
if (indexSynonym === -1) return
|
||||
|
||||
this.$nextTick(() => this.$nuxt.$loading.start())
|
||||
|
||||
try {
|
||||
await this.$axios.delete(`/synonyms/${synonym.id}`)
|
||||
|
||||
this.dialog = false
|
||||
this.itemSelected = null
|
||||
|
||||
this.synonyms.splice(indexSynonym, 1)
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
|
||||
this.$notifier.showMessage({
|
||||
content: `Synonym deleted`,
|
||||
color: 'success',
|
||||
icon: 'mdi-delete',
|
||||
})
|
||||
} catch (error) {
|
||||
this.dialog = false
|
||||
console.log('remove -> error', error)
|
||||
this.$nuxt.$loading.finish()
|
||||
|
||||
this.$notifier.showMessage({
|
||||
content: `Error trying to delete the selected Synonym`,
|
||||
color: 'error',
|
||||
icon: 'mdi-delete',
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
483
pages/manager/members/_member.vue
Normal file
483
pages/manager/members/_member.vue
Normal file
@@ -0,0 +1,483 @@
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<div class="d-flex justify-space-between">
|
||||
<h2 class="ma-4" v-if="isNew">Nieuw</h2>
|
||||
<span v-else>
|
||||
<h2 class="ma-4">{{ local.informal_name || '' }}</h2>
|
||||
<h3 class="ma-4">{{ local.formal_name || '' }}</h3>
|
||||
</span>
|
||||
<span class="justify-center d-flex flex-column" v-if="local.revision">
|
||||
<small>
|
||||
<v-icon color="success" class="mr-2">icon-checkmark</v-icon>
|
||||
bijgewerkt op
|
||||
<strong>{{ formatDate(local.revision.updated_at) }}</strong> door
|
||||
<strong>{{ local.revision.user.fullName }}</strong>
|
||||
</small>
|
||||
|
||||
<small v-if="local.revision.accepted_at">
|
||||
<v-icon color="success">icon-checkmark</v-icon>
|
||||
<v-icon color="success" class="mr-2">icon-checkmark</v-icon>
|
||||
gecontroleerd op
|
||||
<strong>{{ formatDate(local.revision.accepted_at) }}</strong> door
|
||||
<strong>{{ local.revision.revisor.fullName }}</strong>
|
||||
</small>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-space-between">
|
||||
<div class="d-flex">
|
||||
<small
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="index"
|
||||
@click="selectedTab = tab"
|
||||
class="ma-4 dot"
|
||||
:class="{ 'accent--text activeTab': selectedTab === tab }"
|
||||
>{{ $t(`members.tabs.${tab}`) }}</small
|
||||
>
|
||||
</div>
|
||||
<!-- here checks -->
|
||||
</div>
|
||||
|
||||
<basic-members
|
||||
v-if="isTabSelected('basic')"
|
||||
:editMode="canEditSuperAdminsAndAdmins"
|
||||
:isCreateMode="isNew"
|
||||
:users="users"
|
||||
/>
|
||||
|
||||
<address-members
|
||||
v-if="isTabSelected('address')"
|
||||
:editMode="canEditSuperAdminsAndAdmins"
|
||||
:isCreateMode="isNew"
|
||||
/>
|
||||
<contacts-members
|
||||
v-if="isTabSelected('contacts')"
|
||||
:editMode="canEditSuperAdminsAdminsAndDelegated"
|
||||
:isCreateMode="isNew"
|
||||
/>
|
||||
|
||||
<contribution-members
|
||||
v-if="isTabSelected('contribution')"
|
||||
:editMode="canEditSuperAdminsAndAdmins"
|
||||
:isCreateMode="isNew"
|
||||
/>
|
||||
|
||||
<employees-members
|
||||
v-if="isTabSelected('employees')"
|
||||
:editMode="canEditSuperAdminsAdminsAndDelegated"
|
||||
:isCreateMode="isNew"
|
||||
/>
|
||||
<page-members
|
||||
v-if="isTabSelected('member_page')"
|
||||
:editMode="canEditSuperAdminsAdminsAndDelegated"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-footer
|
||||
fixed
|
||||
style="z-index: 4"
|
||||
height="90"
|
||||
color="primary"
|
||||
v-if="$store.getters['members/isSuperAdminAdminOrDelegated']"
|
||||
>
|
||||
<v-btn title text small :to="localePath('/manager/members')">
|
||||
<v-icon>icon-arrow-left</v-icon>
|
||||
</v-btn>
|
||||
<div class="mx-10">
|
||||
<v-btn
|
||||
class="ma-2 white--text"
|
||||
color="accent"
|
||||
depressed
|
||||
v-if="
|
||||
$store.getters['members/isSuperAdminAdminOrDelegated'] &&
|
||||
!$store.getters.isOnlyMemberEditor &&
|
||||
!isNew &&
|
||||
!isEditMode
|
||||
"
|
||||
rounded
|
||||
@click="switchToEdit"
|
||||
>
|
||||
<!-- :to="localePath(`${$nuxt.$route.path}?edit`)"
|
||||
nuxt -->
|
||||
{{ $t('general.edit') }}</v-btn
|
||||
>
|
||||
|
||||
<v-btn
|
||||
class="ma-2 white--text"
|
||||
:color="$vuetify.theme.dark ? 'secondary' : 'txt'"
|
||||
depressed
|
||||
@click="save()"
|
||||
v-if="
|
||||
canSave && ($store.getters.isSuperAdmin || $store.getters.isAdmin)
|
||||
"
|
||||
rounded
|
||||
>Tussentijds opslaan</v-btn
|
||||
>
|
||||
|
||||
<!-- Accept Revision - only super admins or admins -->
|
||||
<template v-if="$store.getters.isSuperAdmin || $store.getters.isAdmin">
|
||||
<v-btn
|
||||
v-cloak
|
||||
class="ma-2 white--text"
|
||||
color="accent"
|
||||
depressed
|
||||
rounded
|
||||
v-if="
|
||||
!isNew &&
|
||||
$store.getters['members/revision'] &&
|
||||
local.revision.hasChanges &&
|
||||
isEditMode
|
||||
"
|
||||
@click="save(true)"
|
||||
>Opslaan en indienen</v-btn
|
||||
>
|
||||
</template>
|
||||
<!-- Store revision - only delegated users -->
|
||||
<template v-else>
|
||||
<v-btn
|
||||
v-cloak
|
||||
class="ma-2 white--text"
|
||||
color="accent"
|
||||
depressed
|
||||
v-if="canSave && !isNew"
|
||||
rounded
|
||||
@click="storeRevision()"
|
||||
>Opslaan en indienen</v-btn
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<v-btn
|
||||
class="ma-2"
|
||||
tile
|
||||
text
|
||||
v-if="canSave && !isNew"
|
||||
color="accent"
|
||||
depressed
|
||||
>
|
||||
<v-icon class="mx-2" size="26">mdi-alert-circle-outline</v-icon>
|
||||
wijziging
|
||||
</v-btn>
|
||||
|
||||
<v-spacer />
|
||||
|
||||
<!-- <v-btn class="ma-2" tile text small>
|
||||
<v-icon class="mx-2" small>icon-sharepoint</v-icon>
|
||||
Documenten
|
||||
</v-btn> -->
|
||||
|
||||
<!-- <v-dialog v-model="dialog" persistent max-width="740">
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn
|
||||
class="ma-2"
|
||||
tile
|
||||
text
|
||||
small
|
||||
v-if="!isNew && canEdit"
|
||||
v-on="on"
|
||||
>
|
||||
<v-icon class="mx-2">icon-remove</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card class="primary pa-10" flat>
|
||||
<v-card-title class="headline">
|
||||
{{
|
||||
$t('learning.product_overview.delete_confirmation', {
|
||||
productName: local.title,
|
||||
})
|
||||
}}
|
||||
</v-card-title>
|
||||
<v-card-actions>
|
||||
<div class="ma-4">
|
||||
<v-btn
|
||||
color="accent"
|
||||
class="mx-2"
|
||||
@click="deleteMember()"
|
||||
rounded
|
||||
depressed
|
||||
>{{ $t('general.delete') }}</v-btn
|
||||
>
|
||||
<v-btn
|
||||
class="mx-2"
|
||||
color="info"
|
||||
@click="close"
|
||||
rounded
|
||||
depressed
|
||||
>{{ $t('general.cancel') }}</v-btn
|
||||
>
|
||||
</div>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog> -->
|
||||
</v-footer>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import dayjs from 'dayjs'
|
||||
import PageHeader from '~/components/UI/PageHeader/PageHeader'
|
||||
import basicMembers from '@/components/Members/BasicMembers'
|
||||
import addressMembers from '@/components/Members/AddressMembers'
|
||||
import contactsMembers from '@/components/Members/ContactsMembers'
|
||||
import employeesMembers from '@/components/Members/EmployeesMembers'
|
||||
import pageMembers from '@/components/Members/PageMembers'
|
||||
import moreMembers from '@/components/Members/MoreMembers'
|
||||
import contributionMembers from '~/components/Members/ContributionMembers.vue'
|
||||
|
||||
export default {
|
||||
layout: `${process.env.CUSTOMER}Admin`,
|
||||
components: {
|
||||
PageHeader,
|
||||
basicMembers,
|
||||
addressMembers,
|
||||
contactsMembers,
|
||||
employeesMembers,
|
||||
pageMembers,
|
||||
moreMembers,
|
||||
contributionMembers,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tabs: [
|
||||
'all',
|
||||
'basic',
|
||||
'address',
|
||||
'contacts',
|
||||
'contribution',
|
||||
'employees',
|
||||
'member_page',
|
||||
],
|
||||
selectedTab: 'all',
|
||||
dialog: false,
|
||||
users: [],
|
||||
}
|
||||
},
|
||||
|
||||
async asyncData({ $axios, store }) {
|
||||
try {
|
||||
await store.dispatch('members/pullBranches')
|
||||
await store.dispatch('members/pullTypes')
|
||||
|
||||
if (store.getters.isAdmin || store.getters.isOperator) {
|
||||
const response = await $axios.get('/admin/users/getList')
|
||||
return { users: response.data }
|
||||
}
|
||||
return { users: [] }
|
||||
} catch (error) {
|
||||
console.log('asyncData -> error', error)
|
||||
}
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
if (!this.isValidSlug) {
|
||||
this.$nuxt.error({ statusCode: 404, message: 'URL niet geldig' })
|
||||
// this.$router.push('/manager')
|
||||
}
|
||||
if (this.isSlugNumber) {
|
||||
await this.$store.dispatch('members/getAndSetMember', this.slug)
|
||||
}
|
||||
if (this.isNew && !this.$store.getters.isSuperAdminOrAdmin) {
|
||||
this.$nuxt.error({ statusCode: 401, message: 'Je mag dit niet doen' })
|
||||
// this.$router.push('/manager')
|
||||
}
|
||||
|
||||
// #warning Laravel Echo has been manually disabled until broadcasting issue is fixed.
|
||||
// this.$echo.channel('updates').listen(`.members-updated`, async (e) => {
|
||||
// if (this.isSlugNumber) {
|
||||
// await this.$store.dispatch('members/getAndSetMember', this.slug)
|
||||
// }
|
||||
//
|
||||
// this.$notifier.showMessage({
|
||||
// content: 'Member updated',
|
||||
// color: 'success',
|
||||
// icon: 'icon-message',
|
||||
// })
|
||||
// })
|
||||
},
|
||||
|
||||
computed: {
|
||||
canEditSuperAdminsAdminsAndDelegated() {
|
||||
return (
|
||||
(this.isNew || this.isEditMode) &&
|
||||
this.$store.getters['members/isSuperAdminAdminOrDelegated']
|
||||
)
|
||||
},
|
||||
canEditSuperAdminsAndAdmins() {
|
||||
return (
|
||||
(this.isNew || this.isEditMode) &&
|
||||
(this.$store.getters.isSuperAdmin || this.$store.getters.isAdmin)
|
||||
)
|
||||
},
|
||||
canSave() {
|
||||
if (this.isNew && this.isMemberValidated) return true
|
||||
// if (this.isNew) return true
|
||||
return (
|
||||
this.isSlugNumber &&
|
||||
this.isDownloaded &&
|
||||
this.$store.getters['members/hasChanges'] &&
|
||||
this.isMemberValidated
|
||||
)
|
||||
},
|
||||
isMemberValidated() {
|
||||
return this.$store.getters['members/isMemberValidated']
|
||||
},
|
||||
slug() {
|
||||
return this.$route.params.member
|
||||
},
|
||||
isNew() {
|
||||
return this.slug.toLowerCase() === 'new'
|
||||
},
|
||||
isEditMode() {
|
||||
return this.$route.query.edit === null
|
||||
},
|
||||
remote() {
|
||||
return this.$store.state.members.remote
|
||||
},
|
||||
local() {
|
||||
return this.$store.state.members.local
|
||||
},
|
||||
title() {
|
||||
if (this.isNew) return `<h2 class="ma-4">Nieuw</h2>`
|
||||
return `${this.local.informal_name || ''} <h2 class="ma-4">${
|
||||
this.local.formal_name || ''
|
||||
}`
|
||||
},
|
||||
isSlugNumber() {
|
||||
return !isNaN(this.slug.split(['-'], 1))
|
||||
},
|
||||
isValidSlug() {
|
||||
return this.isNew || this.isSlugNumber
|
||||
},
|
||||
isDownloaded() {
|
||||
return Object.keys(this.remote).length > 0
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
formatDate(date) {
|
||||
return dayjs(date).format('D MMM YYYY hh:mm').toLowerCase()
|
||||
},
|
||||
|
||||
close() {
|
||||
this.dialog = false
|
||||
},
|
||||
isTabSelected(tab) {
|
||||
return this.selectedTab === tab || this.selectedTab === 'all'
|
||||
},
|
||||
|
||||
async save(revision = false) {
|
||||
this.$nextTick(() => this.$nuxt.$loading.start())
|
||||
|
||||
try {
|
||||
const response = await this.$store.dispatch('members/store', revision)
|
||||
this.$nuxt.$loading.finish()
|
||||
this.$router.push(this.localePath('/manager/members'))
|
||||
$nuxt.$notifier.showMessage({
|
||||
content: `Member stored`,
|
||||
color: 'success',
|
||||
icon: 'mdi-check',
|
||||
})
|
||||
} catch (error) {
|
||||
this.$nuxt.$loading.finish()
|
||||
console.log('save -> error', error)
|
||||
this.$notifier.showMessage({
|
||||
content: `${
|
||||
this.slug.toLowerCase() === 'new' ? 'Creating' : 'Editing'
|
||||
} page: ${error.response ? error.response.data.message : error}.`,
|
||||
...(error.response && {
|
||||
errors: error.response.data.errors,
|
||||
}),
|
||||
color: 'error',
|
||||
icon: 'mdi-alert',
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
switchToEdit() {
|
||||
this.$router.push(this.localePath(`${$nuxt.$route.path}?edit`))
|
||||
},
|
||||
|
||||
async storeRevision() {
|
||||
try {
|
||||
await this.$store.dispatch('members/storeRevision')
|
||||
this.$nuxt.$loading.finish()
|
||||
this.$router.push(this.localePath('/manager/members'))
|
||||
$nuxt.$notifier.showMessage({
|
||||
content: `Revision stored`,
|
||||
color: 'success',
|
||||
icon: 'mdi-check',
|
||||
})
|
||||
} catch (error) {
|
||||
this.$nuxt.$loading.finish()
|
||||
console.log('storeRevision -> error', error)
|
||||
this.$notifier.showMessage({
|
||||
content: `${error.response ? error.response.data.message : error}.`,
|
||||
...(error.response && {
|
||||
errors: error.response.data.errors,
|
||||
}),
|
||||
color: 'error',
|
||||
icon: 'mdi-alert',
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
async deleteMember() {
|
||||
this.dialog = false
|
||||
await this.$store.dispatch('members/deleteMember')
|
||||
this.$router.push(this.localePath('/manager/members'))
|
||||
},
|
||||
},
|
||||
|
||||
async beforeRouteLeave(to, from, next) {
|
||||
await this.$store.dispatch('members/resetMember')
|
||||
next()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-card >>> .v-btn__content,
|
||||
.v-select .v-select__selection--comma,
|
||||
.v-chip__content,
|
||||
.v-list-item:not(.v-list-item--active):not(.v-list-item--disabled),
|
||||
.v-select.v-select--chips:not(.v-text-field--single-line).v-text-field--enclosed
|
||||
.v-select__selections,
|
||||
.v-list-item .v-list-item__title,
|
||||
.v-list-item .v-list-item__subtitle,
|
||||
.v-list-item span,
|
||||
.v-input--is-disabled input,
|
||||
.v-input--is-disable,
|
||||
.dot,
|
||||
h2,
|
||||
p {
|
||||
/* color: var(--v-txt-base); */
|
||||
}
|
||||
.v-card >>> .v-chip--disabled {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.v-card >>> .v-chip {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
.dot {
|
||||
cursor: pointer;
|
||||
}
|
||||
.dot::before {
|
||||
content: '\A';
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: #e54e0f;
|
||||
display: block;
|
||||
position: relative;
|
||||
top: 30px;
|
||||
left: 50%;
|
||||
opacity: 0;
|
||||
}
|
||||
.dot:hover::before {
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
.dot.activeTab::before {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
</style>
|
||||
254
pages/manager/members/branches/index.vue
Normal file
254
pages/manager/members/branches/index.vue
Normal file
@@ -0,0 +1,254 @@
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<div class="d-flex justify-space-between">
|
||||
<h2 class="ml-6 txt--text">Branches ({{ items.length }})</h2>
|
||||
<current-date-time showIcon class="mr-6" />
|
||||
</div>
|
||||
|
||||
<accordion-card :title="$t('general.list') | capitalize">
|
||||
<v-row v-for="(item, index) in items" :key="item.title + index">
|
||||
<v-col cols="12" sm="3" md="3">
|
||||
<v-subheader class="ml-10 txt--text font-weight-black"
|
||||
>{{ index + 1 }}.
|
||||
</v-subheader>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="6">
|
||||
<v-text-field
|
||||
outlined
|
||||
:name="item.title + index"
|
||||
:value="item.title"
|
||||
@change="setDynamicTitle($event, index)"
|
||||
append-icon="icon-close"
|
||||
@click:append="askForDelete(item)"
|
||||
@click:append-outer="update(item.id, index)"
|
||||
:append-outer-icon="
|
||||
hasItemChanges(index) ? 'mdi-content-save' : ''
|
||||
"
|
||||
:error="hasItemChanges(index)"
|
||||
>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="3" md="3"> </v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="3">
|
||||
<v-subheader class="ml-10 txt--text font-weight-black">
|
||||
Nieuw
|
||||
</v-subheader>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field v-model="newItem" outlined> </v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="3"> </v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="3"> </v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-btn
|
||||
class="cta-secondary"
|
||||
min-height="60px"
|
||||
block
|
||||
depressed
|
||||
@click="addBranch"
|
||||
:disabled="!newItem"
|
||||
>
|
||||
<v-icon x-small class="mx-4">icon-add</v-icon>
|
||||
|
||||
{{ $t('general.add') | capitalize }}</v-btn
|
||||
>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="3"> </v-col>
|
||||
</v-row>
|
||||
</accordion-card>
|
||||
|
||||
<v-dialog v-model="dialog" persistent max-width="740">
|
||||
<v-card class="primary pa-10" flat>
|
||||
<v-card-title class="headline">
|
||||
{{
|
||||
$t('learning.filters.delete_item_confirmation', {
|
||||
itemName: itemSelected.title,
|
||||
})
|
||||
}}
|
||||
</v-card-title>
|
||||
<v-card-actions>
|
||||
<div class="ma-4">
|
||||
<v-btn
|
||||
color="accent"
|
||||
class="mx-2"
|
||||
@click="deleteItem(itemSelected.id)"
|
||||
rounded
|
||||
depressed
|
||||
>{{ $t('general.delete') }}</v-btn
|
||||
>
|
||||
<v-btn
|
||||
class="mx-2"
|
||||
color="info"
|
||||
@click="close"
|
||||
rounded
|
||||
depressed
|
||||
>{{ $t('general.cancel') }}</v-btn
|
||||
>
|
||||
</div>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-col>
|
||||
|
||||
<v-footer fixed style="z-index: 4" height="90" color="primary">
|
||||
<v-btn text nuxt :to="localePath('/manager/learning/filters')">
|
||||
<v-icon>icon-arrow-left</v-icon>
|
||||
</v-btn>
|
||||
</v-footer>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import accordionCard from '@/components/UI/AccordionCard/AccordionCard'
|
||||
import currentDateTime from '@/components/CurrentDateTime/CurrentDateTime'
|
||||
import PageHeader from '~/components/UI/PageHeader/PageHeader'
|
||||
|
||||
import Vue from 'vue'
|
||||
|
||||
export default {
|
||||
layout: `${process.env.CUSTOMER}Admin`,
|
||||
middleware: 'allowSuperAdminOrAdmin',
|
||||
components: {
|
||||
accordionCard,
|
||||
PageHeader,
|
||||
currentDateTime,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
newItem: '',
|
||||
branches: [],
|
||||
items: [],
|
||||
itemsRemote: [],
|
||||
itemSelected: {},
|
||||
dialog: false,
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.getBranches()
|
||||
},
|
||||
|
||||
computed: {},
|
||||
methods: {
|
||||
async getBranches() {
|
||||
const response = await this.$axios.get('/members/branches')
|
||||
this.items = [...response.data]
|
||||
this.itemsRemote = [...response.data]
|
||||
},
|
||||
close() {
|
||||
this.dialog = false
|
||||
},
|
||||
save() {
|
||||
this.close()
|
||||
},
|
||||
hasItemChanges(index) {
|
||||
return this.items[index].title !== this.itemsRemote[index].title
|
||||
},
|
||||
setDynamicTitle(title, index) {
|
||||
this.items[index] = Object.assign({}, { ...this.items[index], title })
|
||||
Vue.set(this.items, index, this.items[index])
|
||||
},
|
||||
|
||||
async addBranch() {
|
||||
const data = { title: this.newItem }
|
||||
|
||||
this.$nextTick(() => this.$nuxt.$loading.start())
|
||||
|
||||
try {
|
||||
await this.$axios.post(`/members/branches`, data)
|
||||
await this.getBranches()
|
||||
this.newItem = ''
|
||||
this.$nuxt.$loading.finish()
|
||||
} catch (error) {
|
||||
this.$nuxt.$loading.finish()
|
||||
this.$notifier.showMessage({
|
||||
content: `Error creating the filter item: ` + error.message,
|
||||
color: 'error',
|
||||
icon: 'icon-message',
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
async update(id, index) {
|
||||
if (
|
||||
id === null ||
|
||||
id === undefined ||
|
||||
index === null ||
|
||||
index === undefined ||
|
||||
!this.items[index]
|
||||
)
|
||||
return
|
||||
|
||||
this.$nextTick(() => this.$nuxt.$loading.start())
|
||||
|
||||
try {
|
||||
await this.$axios.post(`/members/branches`, this.items[index])
|
||||
|
||||
await this.getBranches()
|
||||
this.$nuxt.$loading.finish()
|
||||
this.$notifier.showMessage({
|
||||
content: `Branch updated successfully`,
|
||||
color: 'success',
|
||||
icon: 'icon-message',
|
||||
})
|
||||
} catch (error) {
|
||||
this.$nuxt.$loading.finish()
|
||||
this.$notifier.showMessage({
|
||||
content: `Error updating the Branch: ` + error.message,
|
||||
color: 'error',
|
||||
icon: 'icon-message',
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
askForDelete(item) {
|
||||
this.itemSelected = item
|
||||
this.dialog = true
|
||||
},
|
||||
|
||||
async deleteItem(itemId) {
|
||||
if (!itemId) {
|
||||
this.$notifier.showMessage({
|
||||
content: `No item to delete selected`,
|
||||
color: 'error',
|
||||
icon: 'icon-message',
|
||||
})
|
||||
}
|
||||
|
||||
this.$nextTick(() => this.$nuxt.$loading.start())
|
||||
|
||||
try {
|
||||
await this.$axios.delete(`/members/branches/${itemId}`)
|
||||
|
||||
this.dialog = false
|
||||
this.itemSelected = {}
|
||||
|
||||
await this.getBranches()
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
|
||||
this.$notifier.showMessage({
|
||||
content: `Branch deleted`,
|
||||
color: 'success',
|
||||
icon: 'mdi-delete',
|
||||
})
|
||||
} catch (error) {
|
||||
this.dialog = false
|
||||
this.$nuxt.$loading.finish()
|
||||
|
||||
this.$notifier.showMessage({
|
||||
content: `Error trying to delete the selected Branch`,
|
||||
color: 'error',
|
||||
icon: 'mdi-delete',
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
137
pages/manager/members/control.vue
Normal file
137
pages/manager/members/control.vue
Normal file
@@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="pa-6">
|
||||
<page-header class="d-flex justify-space-between">
|
||||
<div class="subtitle-1">
|
||||
<span class="mr-4 display-1">
|
||||
Ledencontrole
|
||||
<small class="font-weight-light">( {{ changes.length }})</small>
|
||||
</span>
|
||||
</div>
|
||||
</page-header>
|
||||
</div>
|
||||
|
||||
<members-changes :items="changes" :users="users" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PageHeader from '~/components/UI/PageHeader/PageHeader'
|
||||
import MembersChanges from '~/components/Members/MembersChanges'
|
||||
|
||||
export default {
|
||||
layout: `${process.env.CUSTOMER}Admin`,
|
||||
middleware: 'allowSuperAdminOrAdmin',
|
||||
components: {
|
||||
PageHeader,
|
||||
MembersChanges,
|
||||
},
|
||||
|
||||
async asyncData({ $axios, store }) {
|
||||
try {
|
||||
if (store.getters.isSuperAdminOrAdmin) {
|
||||
await store.dispatch('members/pullData')
|
||||
const response = await $axios.get('/admin/users/getList')
|
||||
return { users: response.data }
|
||||
}
|
||||
return { users: [] }
|
||||
} catch (error) {
|
||||
console.log('Data -> error', error)
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
users: [],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
allMembers() {
|
||||
return this.$store.getters['members/membersFiltered']
|
||||
},
|
||||
|
||||
changes() {
|
||||
const tmp = []
|
||||
|
||||
// cycle members
|
||||
this.allMembers.forEach((member) => {
|
||||
member['changes'] = []
|
||||
let found = null
|
||||
|
||||
// cycle addresses, contacts, summaries
|
||||
// const properties = [
|
||||
// 'addresses',
|
||||
// 'contacts',
|
||||
// 'contributions',
|
||||
// 'summaries',
|
||||
// ]
|
||||
|
||||
// Doesn't work due to a vuex error
|
||||
// properties.forEach((property) => {
|
||||
// if (member[property].length) {
|
||||
// member[property].some((item) => {
|
||||
// if (!item.approved_at) {
|
||||
// if (!member[property].includes(property)) {
|
||||
// member[property].push(property)
|
||||
// }
|
||||
// found = true
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
|
||||
if (member.addresses.length) {
|
||||
member.addresses.some((a) => {
|
||||
if (!a.approved_at) {
|
||||
if (!member['changes'].includes('Addresses')) {
|
||||
member['changes'].push('Addresses')
|
||||
}
|
||||
found = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (member.contacts.length) {
|
||||
member.contacts.some((c) => {
|
||||
if (!c.approved_at) {
|
||||
if (!member['changes'].includes('Contacts')) {
|
||||
member['changes'].push('Contacts')
|
||||
}
|
||||
found = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (member.contributions.length) {
|
||||
member.contributions.some((c) => {
|
||||
if (!c.approved_at) {
|
||||
if (!member['changes'].includes('Contributions')) {
|
||||
member['changes'].push('Contributions')
|
||||
}
|
||||
found = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (member.summaries.length) {
|
||||
member.summaries.some((s) => {
|
||||
if (!s.approved_at) {
|
||||
if (!member['changes'].includes('Werknemers')) {
|
||||
member['changes'].push('Werknemers')
|
||||
}
|
||||
found = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (found) {
|
||||
tmp.push(member)
|
||||
}
|
||||
})
|
||||
|
||||
return tmp
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
236
pages/manager/members/index.vue
Normal file
236
pages/manager/members/index.vue
Normal file
@@ -0,0 +1,236 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="pa-6">
|
||||
<page-header class="d-flex justify-space-between">
|
||||
<div class="subtitle-1">
|
||||
<span
|
||||
:class="{ 'display-1': selector === 'allMembers' }"
|
||||
:style="{ cursor: 'pointer' }"
|
||||
@click="selector = 'allMembers'"
|
||||
class="mr-4"
|
||||
>
|
||||
Leden
|
||||
<small class="font-weight-light">({{ allMembers.length }})</small>
|
||||
</span>
|
||||
|
||||
<span
|
||||
:class="{ 'display-1': selector === 'active' }"
|
||||
:style="{ cursor: 'pointer' }"
|
||||
@click="selector = 'active'"
|
||||
class="mx-4"
|
||||
v-if="$store.getters.isAdmin || $store.getters.isOperator"
|
||||
>
|
||||
Actieve leden
|
||||
<small class="font-weight-light">({{ active.length }})</small>
|
||||
</span>
|
||||
|
||||
<span
|
||||
:class="{ 'display-1': selector === 'inactive' }"
|
||||
:style="{ cursor: 'pointer' }"
|
||||
@click="selector = 'inactive'"
|
||||
class="mx-4"
|
||||
v-if="$store.getters.isAdmin || $store.getters.isOperator"
|
||||
>
|
||||
Inactieve leden
|
||||
<small class="font-weight-light">({{ inactive.length }})</small>
|
||||
</span>
|
||||
|
||||
<span
|
||||
:class="{ 'display-1': selector === 'deleted' }"
|
||||
:style="{ cursor: 'pointer' }"
|
||||
@click="selector = 'deleted'"
|
||||
class="mx-4"
|
||||
v-if="$store.getters.isAdmin || $store.getters.isOperator"
|
||||
>
|
||||
Verwijderd
|
||||
<small class="font-weight-light">({{ deleted.length }})</small>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<!-- here checks -->
|
||||
</div>
|
||||
</page-header>
|
||||
</div>
|
||||
|
||||
<members-table :members="membersComputed" />
|
||||
|
||||
<v-footer
|
||||
fixed
|
||||
style="z-index: 4"
|
||||
height="90"
|
||||
color="primary"
|
||||
v-if="$store.getters.isSuperAdminOrAdmin"
|
||||
>
|
||||
<v-btn text nuxt :to="localePath('/manager')">
|
||||
<v-icon>icon-arrow-left</v-icon>
|
||||
</v-btn>
|
||||
<div class="mx-10">
|
||||
<v-btn
|
||||
color="accent"
|
||||
depressed
|
||||
rounded
|
||||
to="/manager/members/new"
|
||||
class="ml-10"
|
||||
>
|
||||
<v-icon left size="8">icon-add</v-icon>
|
||||
<small>Lid toevoegen</small>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-spacer />
|
||||
<v-btn class="ma-2" tile text small @click="exportCsv">
|
||||
<v-icon class="mx-2" x-small>icon-export</v-icon>
|
||||
{{ $t('general.export_csv') | capitalize }}
|
||||
</v-btn>
|
||||
</v-footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import csv from 'csv'
|
||||
import {DateTime} from 'luxon'
|
||||
import dayjs from 'dayjs'
|
||||
import download from 'downloadjs'
|
||||
import MembersTable from '~/components/Members/MembersTable'
|
||||
import PageHeader from '~/components/UI/PageHeader/PageHeader'
|
||||
|
||||
const I18N_CSV_WRAPPER_KEY = 'csv.members'
|
||||
const I18N_CSV_HEADER_KEYS = [
|
||||
'id',
|
||||
'type',
|
||||
'informal_name',
|
||||
'formal_name',
|
||||
'start_membership',
|
||||
'main_branch',
|
||||
'sub_branches',
|
||||
]
|
||||
const CSV_VALUE_NONE = '-'
|
||||
|
||||
export default {
|
||||
layout: `${process.env.CUSTOMER}Admin`,
|
||||
// middleware: 'adminsOrOperatorsOrMemberEditors',
|
||||
|
||||
components: {
|
||||
PageHeader,
|
||||
MembersTable,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selector: 'allMembers',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
exportCsv() {
|
||||
let inputData = [
|
||||
I18N_CSV_HEADER_KEYS.map((key) => {
|
||||
return this.$t(`${I18N_CSV_WRAPPER_KEY}.${key}`)
|
||||
}),
|
||||
]
|
||||
|
||||
this.allMembers.forEach((member) => {
|
||||
let subBranchTitles = member.sub_branches.map(branch => branch.title)
|
||||
subBranchTitles.sort()
|
||||
|
||||
inputData.push([
|
||||
member.id,
|
||||
member.type ?? CSV_VALUE_NONE,
|
||||
member.informal_name ?? CSV_VALUE_NONE,
|
||||
member.formal_name ?? CSV_VALUE_NONE,
|
||||
this.formatCsvDate(
|
||||
member.start_membership,
|
||||
'yyyy-LL-dd HH:mm:ss',
|
||||
) ?? CSV_VALUE_NONE,
|
||||
member.main_branch ?? CSV_VALUE_NONE,
|
||||
subBranchTitles.length
|
||||
? subBranchTitles.join(', ')
|
||||
: CSV_VALUE_NONE,
|
||||
])
|
||||
})
|
||||
|
||||
inputData.sort((rowLeft, rowRight) => rowRight.id - rowLeft.id);
|
||||
|
||||
csv.stringify(inputData, { quoted: true }, (err, output) => {
|
||||
if (err) {
|
||||
// TODO: handle
|
||||
} else {
|
||||
download(
|
||||
output,
|
||||
this.$t(`${I18N_CSV_WRAPPER_KEY}.filename`),
|
||||
'text/csv'
|
||||
)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
formatCsvDate: (input, inputFormat) => {
|
||||
if (input) {
|
||||
return DateTime.fromFormat(input, inputFormat).toFormat('yyyy-LL-dd')
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
allMembers() {
|
||||
return this.$store.getters['members/membersFiltered']
|
||||
},
|
||||
published() {
|
||||
return this.allMembers.filter((member) => !member.deleted_at)
|
||||
},
|
||||
active() {
|
||||
return this.published.filter(
|
||||
(member) =>
|
||||
!member.end_membership || dayjs().isBefore(member.end_membership)
|
||||
)
|
||||
},
|
||||
inactive() {
|
||||
return this.published.filter(
|
||||
(member) =>
|
||||
member.end_membership && !dayjs().isBefore(member.end_membership)
|
||||
)
|
||||
},
|
||||
deleted() {
|
||||
return this.allMembers.filter((member) => member.deleted_at)
|
||||
},
|
||||
membersComputed() {
|
||||
if (
|
||||
!['allMembers', 'active', 'inactive', 'deleted'].includes(this.selector)
|
||||
) {
|
||||
return []
|
||||
}
|
||||
return this[this.selector]
|
||||
},
|
||||
},
|
||||
|
||||
async asyncData({ $axios, store }) {
|
||||
try {
|
||||
await store.dispatch('members/pullBranches')
|
||||
|
||||
if (!store.getters['members/hasMembers']) {
|
||||
await store.dispatch('members/pullData')
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Data -> error', error)
|
||||
}
|
||||
},
|
||||
|
||||
// #warning Laravel Echo has been manually disabled until broadcasting issue is fixed.
|
||||
// async mounted() {
|
||||
// this.$echo.channel('updates').listen(`.members-updated`, async (e) => {
|
||||
// await this.$store.dispatch('members/pullData')
|
||||
//
|
||||
// this.$notifier.showMessage({
|
||||
// content: 'Members updated',
|
||||
// color: 'success',
|
||||
// icon: 'icon-message',
|
||||
// })
|
||||
// })
|
||||
// },
|
||||
|
||||
async beforeRouteLeave(to, from, next) {
|
||||
await this.$store.commit('members/RESET_MEMBERS')
|
||||
next()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
341
pages/manager/members/managementinfo.vue
Normal file
341
pages/manager/members/managementinfo.vue
Normal file
@@ -0,0 +1,341 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="pa-6">
|
||||
<page-header class="d-flex justify-space-between">
|
||||
<div class="subtitle-1">
|
||||
<span
|
||||
:class="{ 'display-1': selector === 'published' }"
|
||||
:style="{ cursor: 'pointer' }"
|
||||
@click="selector = 'published'"
|
||||
class="mr-4"
|
||||
>Managementinformatie</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<!-- here checks -->
|
||||
</div>
|
||||
</page-header>
|
||||
</div>
|
||||
<!-- kaal -->
|
||||
<div class="management-block-container">
|
||||
<div class="row header-top">
|
||||
<div class="col-md-2 titel">#</div>
|
||||
<div class="col-md-3 titel">lid</div>
|
||||
<div class="col-md-3 titel">rapportages</div>
|
||||
</div>
|
||||
<div class="management-block" v-for="member in members" :key="member.formal_name" >
|
||||
<div class="header row">
|
||||
<div class="col-md-2 titel">{{ member.id }}</div>
|
||||
<div class="col-md-3 titel">{{ member.formal_name }}</div>
|
||||
<div class="col-md-3 titel">{{ member.management_links.length }}</div>
|
||||
</div>
|
||||
|
||||
<div class="body row">
|
||||
<div class="link col-md-12" v-for="link in member.management_links" :key="link.url">
|
||||
<div class="flex space-between h-50 space-between">
|
||||
<div>
|
||||
<strong class="titel">Rapportage</strong>
|
||||
<span class="titel">{{member.formal_name}} (#{{member.id}})</span>
|
||||
</div>
|
||||
<div>
|
||||
<button class="delete" @click="deleteLink(link.id)">
|
||||
<span class="icon-close"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form row">
|
||||
<div class="col-md-4 flex flex-col">
|
||||
<label>
|
||||
<b class="titel">Titel</b>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="link"
|
||||
:value="link.title"
|
||||
@change="setField($event, link.id, 'title')"
|
||||
/>
|
||||
|
||||
<!-- <button class="demo" @click="openWindow(link.url)">
|
||||
<span class="icon-download"></span>
|
||||
</button>-->
|
||||
</div>
|
||||
<div class="col-md-4 flex flex-col">
|
||||
<label>
|
||||
<b class="url">Koppeling url</b>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="link"
|
||||
:value="link.url"
|
||||
@change="setField($event, link.id, 'url')"
|
||||
/>
|
||||
</div>
|
||||
<!-- <input type="text" :value="link.target" @change="setField($event, link.id, 'target')" /><br />-->
|
||||
<div class="col-md-4">
|
||||
<div class="flex flex-col p-0">
|
||||
<label>
|
||||
<b class="display">Weergave</b>
|
||||
</label>
|
||||
<div class="flex flex-row p-0">
|
||||
<div class="col-md-6 p-0">
|
||||
<label>
|
||||
<input
|
||||
class="radio"
|
||||
type="radio"
|
||||
value="self"
|
||||
@change="setField($event, link.id, 'target')"
|
||||
:checked="link.target === 'self'"
|
||||
/>
|
||||
IFrame
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-md-6 p-0">
|
||||
<label>
|
||||
<input
|
||||
class="radio"
|
||||
type="radio"
|
||||
value="blank"
|
||||
@change="setField($event, link.id, 'target')"
|
||||
:checked="link.target === 'blank'"
|
||||
/>
|
||||
Externe link
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button @click="addLink(member.id)">
|
||||
<span class="icon-add"></span>
|
||||
<b class="toevoegen">Nog een rapportage koppeling toevoegen</b>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-footer
|
||||
fixed
|
||||
style="z-index: 4"
|
||||
height="90"
|
||||
color="primary"
|
||||
v-if="$store.getters.isAdmin || $store.getters.isOperator"
|
||||
>
|
||||
<v-btn text nuxt :to="localePath('/manager')">
|
||||
<v-icon>icon-arrow-left</v-icon>
|
||||
</v-btn>
|
||||
<div class="mx-10"></div>
|
||||
|
||||
<v-spacer />
|
||||
</v-footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import Util from '@/util'
|
||||
import PageHeader from '~/components/UI/PageHeader/PageHeader'
|
||||
|
||||
export default {
|
||||
layout: `${process.env.CUSTOMER}Admin`,
|
||||
middleware: 'allowSuperAdminOrAdmin',
|
||||
|
||||
components: {
|
||||
PageHeader,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selector: 'published',
|
||||
showAddButton: false,
|
||||
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
members() {
|
||||
return this.$store.getters['members/membersFiltered']
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async addLink(memberId) {
|
||||
this.$nextTick(() => this.$nuxt.$loading.start())
|
||||
try {
|
||||
await this.$store.dispatch('members/addManagementLink', memberId)
|
||||
this.$nuxt.$loading.finish()
|
||||
} catch (error) {
|
||||
console.log('addLink -> error', error)
|
||||
this.$nuxt.$loading.finish()
|
||||
}
|
||||
},
|
||||
async setField(e, linkId, target) {
|
||||
let data = { value: e.target.value, link_id: linkId, field: target }
|
||||
try {
|
||||
await this.$store.dispatch('members/changeManagementLink', data)
|
||||
} catch (error) {
|
||||
console.log('addLink -> error', error)
|
||||
}
|
||||
},
|
||||
async deleteLink(linkId) {
|
||||
try {
|
||||
await this.$store.dispatch('members/deleteManagementLink', linkId)
|
||||
} catch (error) {
|
||||
console.log('deleteLink -> error', error)
|
||||
}
|
||||
},
|
||||
openWindow(url) {
|
||||
window.open(url, '_blank').focus()
|
||||
},
|
||||
|
||||
openBlock(){
|
||||
this.showAddButton = !this.showAddButton;
|
||||
}
|
||||
},
|
||||
|
||||
async asyncData({ $axios, store }) {
|
||||
try {
|
||||
if (!store.getters['members/hasMembers']) {
|
||||
await store.dispatch('members/pullData')
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Data -> error', error)
|
||||
}
|
||||
},
|
||||
|
||||
async mounted() {},
|
||||
|
||||
async beforeRouteLeave(to, from, next) {
|
||||
next()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.management-block-container {
|
||||
background-color: var(--v-primary-base);
|
||||
border-radius: 5px;
|
||||
padding: 20px 40px;
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
.flex-row {
|
||||
flex-direction: row;
|
||||
}
|
||||
.p-0 {
|
||||
padding: 0;
|
||||
}
|
||||
.h-50 {
|
||||
height: 50px;
|
||||
}
|
||||
.space-between {
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
span.icon-add {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.titel,
|
||||
.url,
|
||||
.display,
|
||||
.toevoegen,
|
||||
.icon-add{
|
||||
color: var(--v-txt-base);
|
||||
}
|
||||
}
|
||||
|
||||
.header-top {
|
||||
font-size: 14px;
|
||||
color: var(--v-txt-base);
|
||||
font-weight: bold;
|
||||
margin-bottom: 15px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 10px;
|
||||
border: 1px solid var(--v-lines-base);
|
||||
}
|
||||
|
||||
.body {
|
||||
// border-bottom: 1px solid #eef7f9;
|
||||
// border-left: 1px solid #eef7f9;
|
||||
// border-right: 1px solid #eef7f9;
|
||||
|
||||
border: 1px solid var(--v-lines-base);
|
||||
border-top: unset;
|
||||
padding: 25px 10px;
|
||||
margin-bottom: 35px;
|
||||
}
|
||||
|
||||
.form {
|
||||
margin-top: 15px;
|
||||
margin-bottom: 35px;
|
||||
}
|
||||
|
||||
.form div {
|
||||
padding-top: 10px;
|
||||
}
|
||||
.form div {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.form {
|
||||
background-color: var(--v-secondary-base);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form input {
|
||||
width: calc(100% - 30px);
|
||||
background-color: var(--v-primary-base);
|
||||
color: var(--v-txt-base);
|
||||
border-radius: 5px;
|
||||
padding: 4px;
|
||||
border: 1px solid var(--v-lines-base);
|
||||
}
|
||||
|
||||
.form input.link {
|
||||
width: calc(100% - 46px);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.form label {
|
||||
display: block;
|
||||
line-height: 2.5;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
margin: 15px auto 30px auto;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
text-align: center;
|
||||
border: 2px dashed var(--v-lines-base);
|
||||
}
|
||||
|
||||
.delete {
|
||||
right: -40px;
|
||||
top: 8px;
|
||||
border: 0px solid transparent;
|
||||
width: auto;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.demo {
|
||||
border: 0px solid transparent;
|
||||
width: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form input.radio {
|
||||
width: 30px;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
200
pages/manager/members/report.vue
Normal file
200
pages/manager/members/report.vue
Normal file
@@ -0,0 +1,200 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="pa-6">
|
||||
<page-header class="d-flex justify-space-between">
|
||||
<div class="subtitle-1">
|
||||
<span class="mr-4 display-1"> Rapportage </span>
|
||||
</div>
|
||||
</page-header>
|
||||
</div>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="managementLinks"
|
||||
:search="search"
|
||||
item-key="informal_name"
|
||||
hide-default-footer
|
||||
:pagination="settings"
|
||||
>
|
||||
<template v-slot:item.title="{ item }">
|
||||
<a
|
||||
v-if="item.target === 'blank'"
|
||||
:href="item.url"
|
||||
@click="setActiveLink(item)"
|
||||
target="_BLANK"
|
||||
class="ma-4 dot"
|
||||
:class="{ 'accent--text activeTab': activeLink === item }"
|
||||
>{{ item.title || item.id }}</a
|
||||
>
|
||||
<a
|
||||
v-else
|
||||
@click="setActiveLink(item)"
|
||||
class="ma-4 dot"
|
||||
:class="{ 'accent--text activeTab': activeLink === item }"
|
||||
>{{ item.title || item.id }}</a
|
||||
>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.updated_at="{ item }">
|
||||
{{ formatDate(item.updated_at) }}
|
||||
</template>
|
||||
</v-data-table>
|
||||
|
||||
<div
|
||||
class="frame"
|
||||
v-if="activeLink.url && activeLink.target === 'self'"
|
||||
>
|
||||
<iframe
|
||||
:src="activeLink.url"
|
||||
width="100%"
|
||||
frameBorder="0"
|
||||
height="800px"
|
||||
/>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-footer fixed style="z-index: 4" height="90" color="primary">
|
||||
<v-btn text nuxt :to="localePath('/manager')">
|
||||
<v-icon>icon-arrow-left</v-icon>
|
||||
</v-btn>
|
||||
<div class="mx-10"></div>
|
||||
<v-spacer />
|
||||
</v-footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import dayjs from 'dayjs'
|
||||
import PageHeader from '~/components/UI/PageHeader/PageHeader'
|
||||
|
||||
export default {
|
||||
layout: `${process.env.CUSTOMER}Admin`,
|
||||
middleware: 'adminsOrOperatorsOrMemberEditors',
|
||||
|
||||
components: {
|
||||
PageHeader,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeLink: {
|
||||
id: 0,
|
||||
url: '',
|
||||
title: '',
|
||||
target: '',
|
||||
},
|
||||
dialog: false,
|
||||
search: '',
|
||||
headers: [
|
||||
{ text: 'titel', value: 'title' },
|
||||
{
|
||||
text: 'leden',
|
||||
align: 'start',
|
||||
sortable: true,
|
||||
value: 'informal_name',
|
||||
},
|
||||
{ text: 'bijgewerkt', value: 'updated_at' },
|
||||
// { text: 'links', value: 'management_links', sortable: false },
|
||||
],
|
||||
settings: {
|
||||
itemsPerPage: 5,
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
members() {
|
||||
return this.$store.getters['members/membersFiltered']
|
||||
},
|
||||
|
||||
managementLinks() {
|
||||
const links = []
|
||||
|
||||
this.members.forEach((member) => {
|
||||
member.management_links.forEach((link) => {
|
||||
links.push({ ...link, informal_name: member.informal_name })
|
||||
})
|
||||
})
|
||||
|
||||
return links
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
setActiveLink(link) {
|
||||
this.activeLink = link
|
||||
this.dialog = true
|
||||
},
|
||||
formatDate(date) {
|
||||
return dayjs(date).format('D MMM YYYY, HH:mm').toLowerCase()
|
||||
},
|
||||
},
|
||||
|
||||
async asyncData({ $axios, store }) {
|
||||
try {
|
||||
if (!store.getters['members/hasMembers']) {
|
||||
await store.dispatch('members/pullData')
|
||||
this.activeLink = this.members[0]
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Data -> error', error)
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-card >>> .v-btn__content,
|
||||
.v-select .v-select__selection--comma,
|
||||
.v-chip__content,
|
||||
.v-list-item:not(.v-list-item--active):not(.v-list-item--disabled),
|
||||
.v-select.v-select--chips:not(.v-text-field--single-line).v-text-field--enclosed
|
||||
.v-select__selections,
|
||||
.v-list-item .v-list-item__title,
|
||||
.v-list-item .v-list-item__subtitle,
|
||||
.v-list-item span,
|
||||
.v-input--is-disabled input,
|
||||
.v-input--is-disable,
|
||||
.dot,
|
||||
h2,
|
||||
p {
|
||||
/* color: var(--v-txt-base); */
|
||||
}
|
||||
.v-card >>> .v-chip--disabled {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.v-card >>> .v-chip {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
.dot {
|
||||
cursor: pointer;
|
||||
color: #333333;
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
}
|
||||
.dot::before {
|
||||
content: '\A';
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: #e54e0f;
|
||||
display: block;
|
||||
position: relative;
|
||||
top: 35px;
|
||||
left: 50%;
|
||||
opacity: 0;
|
||||
}
|
||||
.dot:hover::before {
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
.dot.activeTab::before {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.frame {
|
||||
margin-top: 35px;
|
||||
}
|
||||
</style>
|
||||
276
pages/manager/pages/_page.vue
Normal file
276
pages/manager/pages/_page.vue
Normal file
@@ -0,0 +1,276 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="8">
|
||||
<v-card class="mx-auto" tile>
|
||||
<v-toolbar color="info" dark flat>
|
||||
<v-toolbar-title
|
||||
>Page {{ isNewPage ? 'creation' : 'editing' }}</v-toolbar-title
|
||||
>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
depressed
|
||||
color="white"
|
||||
light
|
||||
v-if="!isNewPage"
|
||||
nuxt
|
||||
:to="localePath(`/pages/${downloadedPage.slug}`)"
|
||||
target="_blank"
|
||||
text
|
||||
>
|
||||
<v-icon>mdi-open-in-new</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
depressed
|
||||
color="white"
|
||||
light
|
||||
to="/manager/pages/new"
|
||||
v-if="!isNewPage"
|
||||
text
|
||||
>
|
||||
<v-icon small>icon-add</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="12">
|
||||
<v-text-field
|
||||
v-model="localPage.title"
|
||||
label="Title"
|
||||
placeholder="Add here your title"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="12" md="12">
|
||||
<v-text-field
|
||||
v-model="localPage.subtitle"
|
||||
label="Subtitle"
|
||||
placeholder="Add here your subtitle"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="12" md="12">
|
||||
<v-text-field
|
||||
v-model="downloadedPage.slug"
|
||||
label="Url"
|
||||
disabled
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-actions class="my-6">
|
||||
<v-btn class="ma-2" color="info" tile depressed to="/manager/pages"
|
||||
>Back to pages</v-btn
|
||||
>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
class="ma-2"
|
||||
color="success"
|
||||
tile
|
||||
depressed
|
||||
@click="save"
|
||||
:disabled="!pageHasChanges"
|
||||
>Save</v-btn
|
||||
>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="4">
|
||||
<ComponentsListToggler
|
||||
:componentsAttachedIds="componentsSortedIds"
|
||||
:model_id="this.downloadedPage.id"
|
||||
model="Page"
|
||||
@reload-resource="getPage(pageSlug)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<draggable v-model="componentsSorted" @change="saveComponentsOrder">
|
||||
<transition-group>
|
||||
<v-card
|
||||
class="mx-auto my-4"
|
||||
v-for="(componentAttached, index) in componentsSorted"
|
||||
:key="componentAttached.name"
|
||||
tile
|
||||
>
|
||||
<v-toolbar color="indigo" dark flat>
|
||||
<v-toolbar-title>
|
||||
<v-avatar color="white" class="indigo--text mr-4" size="18">{{
|
||||
index + 1
|
||||
}}</v-avatar>
|
||||
{{ componentAttached.component_type.name }}
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn icon>
|
||||
<v-icon small>icon-dragdrop</v-icon>
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-btn icon>
|
||||
<v-icon>icon-edit</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
<v-container fluid>
|
||||
<page-component :componentProp="componentAttached" />
|
||||
</v-container>
|
||||
</v-card>
|
||||
</transition-group>
|
||||
</draggable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Get page slug name and make an api call to fetch components for that page
|
||||
|
||||
// Based on https://github.com/vue-generators/vue-form-generator
|
||||
// DOCS https://vue-generators.gitbook.io/vue-generators/
|
||||
|
||||
import PageComponent from '@/components/PageComponent/PageComponent'
|
||||
import ComponentsBar from '@/components/ComponentsBar/ComponentsBar'
|
||||
import ComponentsListToggler from '@/components/ComponentsListToggler/ComponentsListToggler'
|
||||
import Draggable from 'vuedraggable'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PageComponent,
|
||||
ComponentsBar,
|
||||
Draggable,
|
||||
ComponentsListToggler,
|
||||
},
|
||||
|
||||
layout: `${process.env.CUSTOMER}Admin`,
|
||||
data: () => ({
|
||||
downloadedPage: {},
|
||||
localPage: {
|
||||
title: '',
|
||||
subtitle: '',
|
||||
url: '',
|
||||
},
|
||||
componentsSorted: [],
|
||||
|
||||
dialog: false,
|
||||
valid: true,
|
||||
nameComponent: '',
|
||||
nameComponentRules: [
|
||||
(v) => !!v || 'Name is required',
|
||||
(v) => (v && v.length <= 20) || 'Name must be less than 20 characters',
|
||||
],
|
||||
descriptionComponent: '',
|
||||
descriptionComponentRules: [
|
||||
(v) => !!v || 'Name is required',
|
||||
(v) =>
|
||||
(v && v.length <= 50) || 'Description must be less than 50 characters',
|
||||
],
|
||||
lazy: false,
|
||||
}),
|
||||
|
||||
watch: {
|
||||
downloadedPage: function () {
|
||||
this.componentsSorted = this.downloadedPage.components
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (!this.isValidSlug) this.$router.push('/')
|
||||
if (this.isNewPage) return
|
||||
if (this.isSlugNumber) this.getPage(this.pageSlug)
|
||||
},
|
||||
|
||||
computed: {
|
||||
pageSlug() {
|
||||
return this.$route.params.page
|
||||
},
|
||||
isNewPage() {
|
||||
return this.pageSlug.toLowerCase() === 'new'
|
||||
},
|
||||
pageDownloaded() {
|
||||
return Object.keys(this.downloadedPage).length > 0
|
||||
},
|
||||
isSlugNumber() {
|
||||
return !isNaN(this.pageSlug.split(['-'], 1))
|
||||
},
|
||||
isValidSlug() {
|
||||
return this.isNewPage || this.isSlugNumber
|
||||
},
|
||||
pageHasChanges() {
|
||||
return (
|
||||
this.downloadedPage.title !== this.localPage.title ||
|
||||
this.downloadedPage.subtitle !== this.localPage.subtitle ||
|
||||
this.downloadedPage.slug !== this.localPage.slug
|
||||
)
|
||||
},
|
||||
componentsSortedIds() {
|
||||
return this.componentsSorted.length > 0
|
||||
? this.componentsSorted.map(({ id }) => id)
|
||||
: []
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
reset() {
|
||||
this.localPage.title = ''
|
||||
this.localPage.subtitle = ''
|
||||
},
|
||||
async save() {
|
||||
const data = {
|
||||
id: this.downloadedPage.id,
|
||||
title: this.localPage.title,
|
||||
subtitle: this.localPage.subtitle,
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.$axios.post('/pages', data)
|
||||
this.$router.push('/manager/pages')
|
||||
} catch (error) {
|
||||
this.$notifier.showMessage({
|
||||
content:
|
||||
`${
|
||||
this.$route.params.page.toLowerCase() === 'new'
|
||||
? 'Creating'
|
||||
: 'Editing'
|
||||
} page: ` + error.message,
|
||||
color: 'error',
|
||||
icon: 'icon-message',
|
||||
})
|
||||
}
|
||||
},
|
||||
async getPage(pageUrl) {
|
||||
if (!this.isSlugNumber) return
|
||||
|
||||
try {
|
||||
const response = await this.$axios.get(`/pages/${pageUrl}`)
|
||||
|
||||
this.downloadedPage = response.data
|
||||
this.localPage.title = response.data.title
|
||||
this.localPage.subtitle = response.data.subtitle
|
||||
this.localPage.slug = response.data.slug
|
||||
} catch (error) {
|
||||
console.log('TCL: getPage -> error', error)
|
||||
}
|
||||
},
|
||||
|
||||
async saveComponentsOrder() {
|
||||
const data = {
|
||||
components_ids: this.componentsSortedIds,
|
||||
model: 'Page',
|
||||
model_id: this.downloadedPage.id,
|
||||
}
|
||||
|
||||
//todo add debounce
|
||||
try {
|
||||
await this.$axios.post('/components/sort', data)
|
||||
} catch (error) {
|
||||
console.log('TCL: saveComponentsOrder -> error', error)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
135
pages/manager/pages/index.vue
Normal file
135
pages/manager/pages/index.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<div>
|
||||
<page-header v-if="pages">PAGES ({{pages.length}})</page-header>
|
||||
|
||||
<v-sheet class="mx-auto pa-4" elevation="1" tile>
|
||||
<v-data-table :search="search" :headers="headers" :items="pages" :items-per-page="10">
|
||||
<template v-slot:top>
|
||||
<v-toolbar flat>
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
class="hidden-sm-and-down"
|
||||
hide-details
|
||||
flat
|
||||
append-icon="mdi-filter"
|
||||
placeholder="Filter..."
|
||||
light
|
||||
outlined
|
||||
dense
|
||||
></v-text-field>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn depressed to="/manager/pages/new">
|
||||
<v-icon small>icon-add</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
</template>
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn
|
||||
color="info"
|
||||
class="mr-2"
|
||||
small
|
||||
dark
|
||||
tile
|
||||
depressed
|
||||
nuxt
|
||||
:to="localePath(`/pages/${item.slug}`)"
|
||||
target="_blank"
|
||||
>
|
||||
<v-icon small>mdi-open-in-new</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
color="warning"
|
||||
class="mr-2"
|
||||
small
|
||||
dark
|
||||
tile
|
||||
depressed
|
||||
nuxt
|
||||
:to="localePath(`/manager/pages/${item.slug}`)"
|
||||
>
|
||||
<v-icon small>mdi-file-edit</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-dialog v-model="dialog" persistent max-width="500">
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn color="error" v-on="on" small dark tile depressed>
|
||||
<v-icon small>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-card-title class="headline">Do you want to delete this page?</v-card-title>
|
||||
<v-card-text>Deleting this page you will also delete all the components attached and won't be possible to restore it anymore.</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn color="info" @click="dialog = false" tile depressed>back</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="error" @click="deletePage(item.id)" tile depressed>Yes, Delete</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-sheet>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PageHeader from '~/components/UI/PageHeader/PageHeader'
|
||||
|
||||
export default {
|
||||
layout: `${process.env.CUSTOMER}Admin`,
|
||||
components: {
|
||||
PageHeader
|
||||
},
|
||||
data: () => ({
|
||||
dialog: false,
|
||||
search: '',
|
||||
pages: [],
|
||||
headers: [
|
||||
{ text: 'Title', sortable: true, value: 'title' },
|
||||
{ text: 'Created', value: 'created_at', sortable: true },
|
||||
{ text: 'Updated', value: 'updated_at', sortable: true },
|
||||
{ text: 'Actions', value: 'actions', sortable: false }
|
||||
]
|
||||
}),
|
||||
|
||||
watch: {
|
||||
dialog(val) {
|
||||
val || this.close()
|
||||
}
|
||||
},
|
||||
|
||||
async asyncData({ $axios }) {
|
||||
const response = await $axios.get('/pages')
|
||||
return { pages: response.data }
|
||||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.dialog = false
|
||||
},
|
||||
|
||||
save() {
|
||||
this.close()
|
||||
},
|
||||
|
||||
edit(item) {},
|
||||
|
||||
async deletePage(itemId) {
|
||||
try {
|
||||
const response = await this.$axios.delete(`/pages/${itemId}`)
|
||||
this.pages = this.pages.filter(page => page.id !== itemId)
|
||||
this.dialog = false
|
||||
|
||||
this.$notifier.showMessage({
|
||||
content: `Page deleted`,
|
||||
color: 'success',
|
||||
icon: 'mdi-delete'
|
||||
})
|
||||
} catch (error) {
|
||||
console.log('TCL: deletePage -> error', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
45
pages/pages/_page.vue
Normal file
45
pages/pages/_page.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<section>
|
||||
<div v-for="componentAttached in downloadedPage.components" :key="componentAttached.id" tile>
|
||||
<page-component :componentProp="componentAttached" />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PageComponent from '@/components/PageComponent/PageComponent'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PageComponent
|
||||
},
|
||||
|
||||
computed: {
|
||||
pageSlug() {
|
||||
return this.$route.params.page
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
downloadedPage: {},
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getPage(this.pageSlug)
|
||||
},
|
||||
methods: {
|
||||
async getPage(pageUrl) {
|
||||
try {
|
||||
const response = await this.$axios.get(`/pages/${pageUrl}`)
|
||||
this.downloadedPage = response.data
|
||||
} catch (error) {
|
||||
console.log('TCL: getPage -> error', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
35
pages/sso.vue
Normal file
35
pages/sso.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div>GGZ</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
layout: `${process.env.CUSTOMER}Default`,
|
||||
components: {},
|
||||
async mounted () {
|
||||
const responsehash = this.$route.hash.split('&', 3);
|
||||
const token = responsehash[0].replace('#access_token=', '');
|
||||
|
||||
try {
|
||||
await this.$auth.loginWith('local', {
|
||||
data: {
|
||||
email: 'zzz@zzz.nl',
|
||||
password: 'zzzzzzzzz',
|
||||
token: token
|
||||
},
|
||||
})
|
||||
|
||||
this.$router.push('/manager')
|
||||
} catch (error) {
|
||||
this.errors = error.response.data.errors
|
||||
this.$notifier.showMessage({
|
||||
content: error.response.data.message,
|
||||
color: 'error',
|
||||
icon: 'icon-message',
|
||||
})
|
||||
this.$router.push('/login')
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user