Files
Joris Slagter 791aebc346
Some checks failed
continuous-integration/drone/push Build is failing
Initial Nuxt frontend import
- 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
2025-12-02 17:48:48 +01:00

521 lines
14 KiB
Vue

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