- 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:
568
pages/manager/accounts/_user.vue
Normal file
568
pages/manager/accounts/_user.vue
Normal file
@@ -0,0 +1,568 @@
|
||||
<template>
|
||||
<v-form ref="form" v-model="valid" lazy-validation>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<div class="d-flex justify-space-between">
|
||||
<h2 class="ml-6 txt--text">
|
||||
{{
|
||||
localUser.fullName && !isNewUser
|
||||
? localUser.fullName
|
||||
: $t('user.create')
|
||||
}}
|
||||
</h2>
|
||||
<current-date-time showIcon class="mr-6" />
|
||||
</div>
|
||||
|
||||
<accordion-card :title="$t('user.profile.title') | capitalize">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="3">
|
||||
<v-subheader class="txt--text font-weight-black">{{
|
||||
$t('user.profile.photo')
|
||||
}}</v-subheader>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-avatar
|
||||
size="150"
|
||||
class="mr-4 secondary has-outline"
|
||||
v-if="showDropzone"
|
||||
>
|
||||
<vue-dropzone
|
||||
ref="imgDropZone"
|
||||
id="customdropzone"
|
||||
:options="dropzoneOptions"
|
||||
@vdropzone-complete="afterComplete"
|
||||
class="secondary"
|
||||
/>
|
||||
</v-avatar>
|
||||
|
||||
<v-badge
|
||||
:style="{ cursor: 'pointer' }"
|
||||
offset-x="10"
|
||||
offset-y="15"
|
||||
class="pl-sm-3"
|
||||
>
|
||||
<template v-slot:badge>
|
||||
<v-icon>icon-close</v-icon>
|
||||
</template>
|
||||
|
||||
<v-avatar
|
||||
size="150"
|
||||
class="primary has-outline"
|
||||
@click="showDropzone = true"
|
||||
>
|
||||
<v-img :src="profilePic || noImage" contain />
|
||||
</v-avatar>
|
||||
</v-badge>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="3"></v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="3">
|
||||
<v-subheader class="txt--text font-weight-black">{{
|
||||
$t('general.name')
|
||||
}}</v-subheader>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field
|
||||
class="pl-sm-3"
|
||||
outlined
|
||||
type="text"
|
||||
required
|
||||
v-model="localUser.first_name"
|
||||
:rules="firstNameRules"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="3"></v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="3">
|
||||
<v-subheader class="txt--text font-weight-black">{{
|
||||
$t('general.surname')
|
||||
}}</v-subheader>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field
|
||||
class="pl-sm-3"
|
||||
outlined
|
||||
type="text"
|
||||
required
|
||||
v-model="localUser.last_name"
|
||||
:rules="lastNameRules"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="3"></v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="3">
|
||||
<v-subheader class="txt--text font-weight-black">{{
|
||||
$t('general.email') | capitalize
|
||||
}}</v-subheader>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field
|
||||
class="pl-sm-3"
|
||||
outlined
|
||||
v-model="localUser.email"
|
||||
:rules="emailRules"
|
||||
type="email"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="3"></v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="3">
|
||||
<v-subheader class="txt--text font-weight-black"
|
||||
>Nieuw wachtwoord</v-subheader
|
||||
>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field
|
||||
class="pl-sm-3"
|
||||
outlined
|
||||
type="password"
|
||||
placeholder=" "
|
||||
v-model="localUser.password"
|
||||
:rules="passwordRules"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="3"></v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="3">
|
||||
<v-subheader class="txt--text font-weight-black"
|
||||
>Herhaal nieuw wachtwoord
|
||||
</v-subheader>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field
|
||||
class="pl-sm-3"
|
||||
outlined
|
||||
type="password"
|
||||
placeholder=" "
|
||||
v-model="localUser.password_confirmation"
|
||||
:rules="
|
||||
passwordConfirmationRules.concat(passwordConfirmationRule)
|
||||
"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="3"></v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="$store.getters.isAdmin">
|
||||
<v-col cols="12" sm="12" md="3">
|
||||
<v-subheader class="txt--text font-weight-black">{{
|
||||
$t('general.roles')
|
||||
}}</v-subheader>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<div class="d-flex">
|
||||
<v-switch
|
||||
inset
|
||||
class="toggle pl-sm-3"
|
||||
v-model="selected"
|
||||
:label="role.name"
|
||||
:value="role.id"
|
||||
v-for="role in roles"
|
||||
:key="role.id"
|
||||
/>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="3"></v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="localUser.isMemberEditor">
|
||||
<v-col cols="12" sm="12" md="3">
|
||||
<v-subheader class="txt--text font-weight-black"
|
||||
>Leden Editor</v-subheader
|
||||
>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<div>
|
||||
<v-icon color="info"> icon-checkmark </v-icon>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="3"></v-col>
|
||||
</v-row>
|
||||
</accordion-card>
|
||||
|
||||
<!-- <accordion-card :title="$t('user.profile.notification')"></accordion-card> -->
|
||||
|
||||
<v-footer fixed style="z-index: 4" height="90" color="primary">
|
||||
<v-btn class="ma-4" text @click="$router.back()">
|
||||
<v-icon class="mx-2">icon-arrow-left</v-icon>
|
||||
</v-btn>
|
||||
<div>
|
||||
<v-btn
|
||||
class="ma-2 white--text"
|
||||
:color="$vuetify.theme.dark ? 'secondary' : 'txt'"
|
||||
depressed
|
||||
@click="save(true)"
|
||||
v-if="canSave"
|
||||
rounded
|
||||
>{{ $t('general.save') }}</v-btn
|
||||
>
|
||||
<v-btn
|
||||
class="ma-2 white--text"
|
||||
color="accent"
|
||||
depressed
|
||||
@click="save(false)"
|
||||
v-if="canSave"
|
||||
rounded
|
||||
>{{ $t('general.save_and_close') }}</v-btn
|
||||
>
|
||||
</div>
|
||||
|
||||
<v-spacer />
|
||||
<v-dialog v-model="dialog" persistent max-width="500">
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn class="ma-4" text v-on="on">
|
||||
<v-icon class="mx-2">icon-remove</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-card-title class="headline"
|
||||
>Do you want to delete {{ localUser.fullName }}</v-card-title
|
||||
>
|
||||
<v-card-text>
|
||||
Once deleted won't be possible to restore his/her data anymore.
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn color="info" @click="dialog = false" rounded depressed
|
||||
>back</v-btn
|
||||
>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="error" @click="deleteUser()" rounded depressed
|
||||
>Yes, Delete</v-btn
|
||||
>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-footer>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Util from '@/util'
|
||||
import PageHeader from '~/components/UI/PageHeader/PageHeader'
|
||||
import accordionCard from '@/components/UI/AccordionCard/AccordionCard'
|
||||
import currentDateTime from '@/components/CurrentDateTime/CurrentDateTime'
|
||||
import vueDropzone from 'vue2-dropzone'
|
||||
import 'vue2-dropzone/dist/vue2Dropzone.min.css'
|
||||
|
||||
export default {
|
||||
layout: `${process.env.CUSTOMER}Admin`,
|
||||
// middleware: 'onlySuperAdmins',
|
||||
components: {
|
||||
PageHeader,
|
||||
accordionCard,
|
||||
currentDateTime,
|
||||
vueDropzone,
|
||||
},
|
||||
data: () => ({
|
||||
valid: true,
|
||||
name: '',
|
||||
dialog: false,
|
||||
roles: [],
|
||||
selected: [],
|
||||
downloadedUser: {},
|
||||
localUser: {},
|
||||
loading: false,
|
||||
showDropzone: false,
|
||||
dropzoneOptions: {
|
||||
url: 'https://httpbin.org/post',
|
||||
maxFilesize: 1.2, // MB
|
||||
maxFiles: 1,
|
||||
thumbnailWidth: 150,
|
||||
thumbnailHeight: 150,
|
||||
addRemoveLinks: true,
|
||||
acceptedFiles: '.jpg, .jpeg, .png',
|
||||
dictDefaultMessage: `<span class="txt--text">Upload afbeelding</span>`,
|
||||
},
|
||||
images: [],
|
||||
img: {
|
||||
name: '',
|
||||
url: '',
|
||||
file: null,
|
||||
},
|
||||
firstNameRules: [
|
||||
(v) => !!v || 'First name is required',
|
||||
(v) => (v && v.length <= 30) || 'Name must be less than 30 characters',
|
||||
],
|
||||
lastNameRules: [
|
||||
(v) => !!v || 'Last name is required',
|
||||
(v) => (v && v.length <= 30) || 'Name must be less than 30 characters',
|
||||
],
|
||||
emailRules: [
|
||||
(v) => !!v || 'E-mail is required',
|
||||
(v) =>
|
||||
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
|
||||
v
|
||||
) || 'Invalid Email',
|
||||
],
|
||||
passwordRules: [
|
||||
(v) => !v || v.length <= 30 || 'password can be max 30 characters',
|
||||
(v) =>
|
||||
!v || v.length >= 8 || 'Het wachtwoord moet minimaal 8 tekens bevatten',
|
||||
],
|
||||
passwordConfirmationRules: [
|
||||
(v) =>
|
||||
!v ||
|
||||
v.length <= 30 ||
|
||||
'password confirmation can be max 30 characters',
|
||||
(v) =>
|
||||
!v ||
|
||||
v.length >= 8 ||
|
||||
'Het bevestigingswachtwoord moet minimaal 8 tekens bevatten',
|
||||
// (v) => (v && v === this.localUser.password) || `password and password confirmation don't match`,
|
||||
],
|
||||
}),
|
||||
|
||||
computed: {
|
||||
userSlug() {
|
||||
return this.$route.params.user
|
||||
},
|
||||
isUserLoggedProfilePage() {
|
||||
return +this.userSlug === +this.$auth.user.id
|
||||
},
|
||||
isNewUser() {
|
||||
return this.userSlug.toLowerCase() === 'new'
|
||||
},
|
||||
userDownloaded() {
|
||||
return Object.keys(this.downloadedUser).length > 0
|
||||
},
|
||||
isSlugNumber() {
|
||||
return !isNaN(this.userSlug)
|
||||
},
|
||||
isValidSlug() {
|
||||
return this.isNewUser || this.isSlugNumber
|
||||
},
|
||||
remoteRolesIds() {
|
||||
return this.downloadedUser.roles.map(({ id }) => id)
|
||||
},
|
||||
hasRolesChanges() {
|
||||
return !Util.arraysMatch(this.selected, this.remoteRolesIds)
|
||||
},
|
||||
userHasChanges() {
|
||||
return (
|
||||
!Util.areEqualInputs(this.downloadedUser, this.localUser) ||
|
||||
this.hasRolesChanges ||
|
||||
this.img.url
|
||||
)
|
||||
},
|
||||
passwordConfirmationRule() {
|
||||
return () =>
|
||||
this.localUser.password === this.localUser.password_confirmation ||
|
||||
'Password must match'
|
||||
},
|
||||
profilePic() {
|
||||
if (this.img.url) return this.img.url
|
||||
if (this.localUser.image) return this.localUser.image.full
|
||||
return null
|
||||
},
|
||||
noImage() {
|
||||
return require(`@/assets/img/no_image.png`)
|
||||
},
|
||||
canSave() {
|
||||
if (this.isNewUser) return true
|
||||
return (
|
||||
this.isSlugNumber &&
|
||||
this.userDownloaded &&
|
||||
this.userHasChanges &&
|
||||
this.valid
|
||||
)
|
||||
},
|
||||
},
|
||||
async asyncData({ $axios, store, $auth, params }) {
|
||||
// If a normal user tries to edit another profile, gets disconnected
|
||||
if (!store.getters.isSuperAdmin) {
|
||||
if (+params.user !== +store.getters.loggedInUser.id) {
|
||||
$auth.logout()
|
||||
}
|
||||
}
|
||||
|
||||
const response = await $axios.get('/roles')
|
||||
return { roles: response.data }
|
||||
},
|
||||
mounted() {
|
||||
if (!this.isValidSlug) this.$router.push('/')
|
||||
if (this.isNewUser) return
|
||||
if (this.isSlugNumber) this.getUser(this.userSlug)
|
||||
},
|
||||
methods: {
|
||||
validate() {
|
||||
this.$refs.form.validate()
|
||||
},
|
||||
reset() {
|
||||
this.$refs.form.reset()
|
||||
},
|
||||
resetValidation() {
|
||||
this.$refs.form.resetValidation()
|
||||
},
|
||||
|
||||
async getUser(userId) {
|
||||
if (!this.isSlugNumber) return
|
||||
|
||||
try {
|
||||
const response = await this.$axios.get(`/admin/users/${userId}`)
|
||||
|
||||
this.downloadedUser = response.data
|
||||
this.localUser = { ...response.data }
|
||||
this.selected = response.data.roles.map(({ id }) => id)
|
||||
} catch (error) {
|
||||
console.log('TCL: getUser -> error', error)
|
||||
}
|
||||
},
|
||||
|
||||
async save(stay = false) {
|
||||
const formData = new FormData()
|
||||
|
||||
if (!this.isNewUser) formData.append('id', this.downloadedUser.id)
|
||||
formData.append('first_name', this.localUser.first_name)
|
||||
formData.append('last_name', this.localUser.last_name)
|
||||
formData.append('email', this.localUser.email)
|
||||
|
||||
if (this.localUser.password) {
|
||||
formData.append('password', this.localUser.password)
|
||||
}
|
||||
if (this.localUser.password_confirmation) {
|
||||
formData.append(
|
||||
'password_confirmation',
|
||||
this.localUser.password_confirmation
|
||||
)
|
||||
}
|
||||
|
||||
if (this.selected.length > 0) {
|
||||
for (let i = 0; i < this.selected.length; i++) {
|
||||
formData.append('roles[]', this.selected[i])
|
||||
}
|
||||
}
|
||||
|
||||
if (this.img.file) formData.append('image', this.img.file)
|
||||
else formData.delete('image')
|
||||
|
||||
try {
|
||||
// console.log([...formData])
|
||||
const response = await this.$axios.post('/admin/users', formData)
|
||||
|
||||
const user = response.data
|
||||
|
||||
this.downloadedUser = response.data
|
||||
this.localUser = { ...response.data }
|
||||
|
||||
this.img.name = ''
|
||||
this.img.url = ''
|
||||
this.img.file = null
|
||||
|
||||
// If i'm editing myself, update mydata
|
||||
if (user.id === this.$auth.user.id) {
|
||||
this.$auth.setUser(user)
|
||||
}
|
||||
|
||||
this.$notifier.showMessage({
|
||||
content: `User updated`,
|
||||
color: 'success',
|
||||
icon: 'icon-checkmark',
|
||||
})
|
||||
|
||||
if (!stay) this.$router.push('/manager/accounts')
|
||||
} catch (error) {
|
||||
this.$notifier.showMessage({
|
||||
content:
|
||||
`${
|
||||
this.$route.params.user.toLowerCase() === 'new'
|
||||
? 'Creating'
|
||||
: 'Editing'
|
||||
} user: ` + error.message,
|
||||
color: 'error',
|
||||
icon: 'icon-message',
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
async deleteUser(userId = this.downloadedUser.id) {
|
||||
if (!userId) {
|
||||
this.$notifier.showMessage({
|
||||
content: `No User to delete selected`,
|
||||
color: 'error',
|
||||
icon: 'icon-message',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.$axios.delete(`/manager/accounts/${userId}`)
|
||||
|
||||
this.$notifier.showMessage({
|
||||
content: `User deleted`,
|
||||
color: 'success',
|
||||
icon: 'mdi-delete',
|
||||
})
|
||||
this.$router.push('/manager/accounts')
|
||||
} catch (error) {
|
||||
this.$notifier.showMessage({
|
||||
content: `Error deleting user: ${error}`,
|
||||
color: 'error',
|
||||
icon: 'icon-message',
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
async afterComplete(upload) {
|
||||
const file = upload
|
||||
|
||||
if (file === undefined) return
|
||||
if (file.name.lastIndexOf('.') <= 0) return
|
||||
|
||||
if (file.size > 2000000) {
|
||||
this.$notifier.showMessage({
|
||||
content: `Exceeded Max filesize`,
|
||||
color: 'error',
|
||||
icon: 'mdi-alert-circle',
|
||||
})
|
||||
this.img.file = null
|
||||
return
|
||||
}
|
||||
|
||||
this.isLoading = true
|
||||
|
||||
try {
|
||||
const fr = new FileReader()
|
||||
fr.readAsDataURL(file)
|
||||
fr.addEventListener('load', () => {
|
||||
this.img.name = file.name
|
||||
this.img.url = fr.result
|
||||
this.img.file = file // Image file ready to be sent to server
|
||||
})
|
||||
|
||||
this.loading = false
|
||||
this.$refs.imgDropZone.removeFile(upload)
|
||||
this.showDropzone = false
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.has-outline {
|
||||
box-shadow: 0 0 0 3px #eef7f9;
|
||||
}
|
||||
.v-badge .v-badge_wrapper .v-badge__badge {
|
||||
background-color: var(--v-primary-base) !important;
|
||||
}
|
||||
.icon-close {
|
||||
color: var(--v-tertiary-base) !important;
|
||||
}
|
||||
.v-text-field .v-input__control {
|
||||
padding-left: 14px !important;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user