Initial Nuxt frontend import
Some checks failed
continuous-integration/drone/push Build is failing

- 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:
Joris Slagter
2025-12-02 17:48:48 +01:00
parent 0f691e83e3
commit 791aebc346
290 changed files with 113801 additions and 0 deletions

81
pages/auth/_auth.vue Normal file
View 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
View 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>

View 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>

View 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
View 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>

View 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>

View 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
View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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
View 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
View 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>