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

View File

@@ -0,0 +1,685 @@
<template>
<accordion-card title="Contactpersonen">
<v-data-table
:headers="headers"
:items="contacts"
:options="options"
hide-default-footer
item-key="contact"
flat
v-if="hasContacts"
>
<template v-slot:item.contact_person="{ item }">
{{ determineNameFullByContactPersonItem(item) }}
</template>
<template v-slot:item.ledencontrole="{ item }">
<v-icon
:color="!item.approved_at ? 'accent' : 'success'"
>
{{
!item.approved_at
? 'mdi-alert-circle-outline'
: 'icon-checkmark'
}}
</v-icon>
</template>
<template v-slot:item.actions="{ item }">
<v-btn
class="mx-4 white--text view"
style="height: 100%"
:color="$vuetify.theme.dark ? 'info' : 'txt'"
rounded
depressed
small
@click="viewItem(item)"
v-if="$store.getters.hasCharges"
>{{ $t('general.view') }}</v-btn
>
<v-menu
offset-y
v-if="
editMode && $store.getters['members/isSuperAdminAdminOrDelegated']
"
>
<template v-slot:activator="{ on }">
<v-hover v-slot:default="{ hover }">
<v-btn
:color="hover ? 'info' : ''"
:outlined="hover"
depressed
fab
small
v-on="on"
>
<v-icon>icon-options</v-icon>
</v-btn>
</v-hover>
</template>
<v-list width="200">
<v-list-item @click="editItem(item)">
<v-list-item-icon class="mr-1">
<v-icon small>icon-edit</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-dialog max-width="740" persistent v-model="dialogDelete">
<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.delete_contactsmembers_confirmation')
}}</v-card-title>
<v-card-actions>
<div class="ma-4">
<v-btn
@click="deleteItem(item)"
class="mx-2"
color="accent"
depressed
rounded
>{{ $t('general.delete') }}</v-btn
>
<v-btn
@click="dialogDelete = false"
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>
<v-dialog
v-model="dialogContacts"
:persistent="editModeComputed"
max-width="75%"
v-if="$store.getters['members/isSuperAdminAdminOrDelegated']"
>
<template v-slot:activator="{ on, attrs }">
<v-btn
v-if="editMode"
class="my-10 cta-secondary"
block
depressed
min-height="60px"
:disabled="isCreateMode"
v-on="on"
@click="contactEditMode = true"
>
<v-icon x-small class="mx-4">icon-add</v-icon>Nieuw contactperson
toevoegen
</v-btn>
</template>
<v-card ref="card">
<v-card-title>
<span class="headline">{{ formTitle }}</span>
</v-card-title>
<v-card-text>
<v-container>
<v-row>
<v-col cols="12" sm="12" md="6">
<v-subheader class="txt--text font-weight-black"
>Rol</v-subheader
>
</v-col>
<v-col cols="12" sm="12" md="6">
<v-select
:items="roles"
v-model="editedItem.function"
:outlined="editModeComputed"
:solo="!editModeComputed"
:disabled="!editModeComputed"
:flat="!editModeComputed"
/>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="12" md="6">
<v-subheader class="txt--text font-weight-black"
>Locatie</v-subheader
>
</v-col>
<v-col cols="12" sm="12" md="6">
<!-- :items="formatAddresses(addresses)" -->
<v-select
:items="addresses"
item-text="indicating"
item-value="id"
v-model="editedItem.address_id"
:outlined="editModeComputed"
:solo="!editModeComputed"
:disabled="!editModeComputed"
:flat="!editModeComputed"
>
<template v-slot:item="{ item, attrs, on }">
{{
`
${item.indicating || ''}
${item.house_number + ',' || ''}
${item.postal + ',' || ''}
${item.city + ',' || ''}
${item.country || ''}
`
}}
</template>
</v-select>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="12" md="6">
<v-subheader class="txt--text font-weight-black"
>Aanhef</v-subheader
>
</v-col>
<v-col cols="12" sm="12" md="6">
<v-select
:items="salutations"
v-model="editedItem.salutation"
:outlined="editModeComputed"
:solo="!editModeComputed"
:disabled="!editModeComputed"
:flat="!editModeComputed"
/>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="12" md="6">
<v-subheader class="txt--text font-weight-black"
>Voorletters</v-subheader
>
</v-col>
<v-col cols="12" sm="12" md="6">
<v-text-field
:outlined="editModeComputed"
:solo="!editModeComputed"
:disabled="!editModeComputed"
:flat="!editModeComputed"
v-model="editedItem.initials"
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="12" md="6">
<v-subheader class="txt--text font-weight-black"
>Voornaam</v-subheader
>
</v-col>
<v-col cols="12" sm="12" md="6">
<v-text-field
:outlined="editModeComputed"
:solo="!editModeComputed"
:disabled="!editModeComputed"
:flat="!editModeComputed"
v-model="editedItem.firstname"
:rules="rules.firstname"
required
error
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="12" md="6">
<v-subheader class="txt--text font-weight-black"
>Tussenvoegsel</v-subheader
>
</v-col>
<v-col cols="12" sm="12" md="6">
<v-text-field
:outlined="editModeComputed"
:solo="!editModeComputed"
:disabled="!editModeComputed"
:flat="!editModeComputed"
v-model="editedItem.middlename"
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="12" md="6">
<v-subheader class="txt--text font-weight-black"
>Achternaam</v-subheader
>
</v-col>
<v-col cols="12" sm="12" md="6">
<v-text-field
:outlined="editModeComputed"
:solo="!editModeComputed"
:disabled="!editModeComputed"
:flat="!editModeComputed"
v-model="editedItem.lastname"
:rules="rules.lastname"
required
error
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="12" md="6">
<v-subheader class="txt--text font-weight-black"
>Functie</v-subheader
>
</v-col>
<v-col cols="12" sm="12" md="6">
<v-text-field
:outlined="editModeComputed"
:solo="!editModeComputed"
:disabled="!editModeComputed"
:flat="!editModeComputed"
v-model="editedItem.job_title"
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="12" md="6">
<v-subheader class="txt--text font-weight-black"
>Mailadres</v-subheader
>
</v-col>
<v-col cols="12" sm="12" md="6">
<v-text-field
:outlined="editModeComputed"
:solo="!editModeComputed"
:disabled="!editModeComputed"
:flat="!editModeComputed"
v-model="editedItem.email"
type="email"
required
error
:rules="[rules.email.required]"
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="12" md="6">
<v-subheader class="txt--text font-weight-black"
>Mailadres 2</v-subheader
>
</v-col>
<v-col cols="12" sm="12" md="6">
<v-text-field
:outlined="editModeComputed"
:solo="!editModeComputed"
:disabled="!editModeComputed"
:flat="!editModeComputed"
v-model="editedItem.email2"
type="email"
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="12" md="6">
<v-subheader class="txt--text font-weight-black"
>Mailadres 3</v-subheader
>
</v-col>
<v-col cols="12" sm="12" md="6">
<v-text-field
:outlined="editModeComputed"
:solo="!editModeComputed"
:disabled="!editModeComputed"
:flat="!editModeComputed"
v-model="editedItem.email3"
type="email"
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="12" md="6">
<v-subheader class="txt--text font-weight-black"
>Telefoonnummer</v-subheader
>
</v-col>
<v-col cols="12" sm="12" md="6">
<v-text-field
:outlined="editModeComputed"
:solo="!editModeComputed"
:disabled="!editModeComputed"
:flat="!editModeComputed"
v-model="editedItem.phone"
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="12" md="6">
<v-subheader class="txt--text font-weight-black"
>Mobiel</v-subheader
>
</v-col>
<v-col cols="12" sm="12" md="6">
<v-text-field
:outlined="editModeComputed"
:solo="!editModeComputed"
:disabled="!editModeComputed"
:flat="!editModeComputed"
v-model="editedItem.mobile"
></v-text-field>
</v-col>
</v-row>
</v-container>
</v-card-text>
<v-divider />
<v-card-actions v-if="editModeComputed">
<v-btn
class="ma-2 white--text"
color="info"
depressed
rounded
:disabled="loading"
@click="save"
v-if="
$store.getters.isAdmin ||
$store.getters.isOperator ||
isUserDelegated
"
>{{ $t('general.save') }}</v-btn
>
<v-spacer />
<v-btn
class="ma-2 white--text"
color="txt"
depressed
text
:disabled="loading"
@click="close"
>{{ $t('general.cancel') }}</v-btn
>
</v-card-actions>
</v-card>
</v-dialog>
<p v-if="isCreateMode" class="text-center">
Bij het aanmaken van een nieuw lid kiest u eerst voor 'Tussentijds
Opslaan' om deze functie te activeren.
</p>
</accordion-card>
</template>
<script>
import accordionCard from '@/components/UI/AccordionCard/AccordionCard'
import Name from '@/util/Name'
export default {
components: {
accordionCard,
},
props: {
editMode: {
type: Boolean,
default: false,
},
isCreateMode: {
type: Boolean,
default: true,
},
},
data() {
return {
options: {
itemsPerPage: -1,
},
headers: [
{ text: 'rol', value: 'function' },
{ text: 'contactpersoon', value: 'contact_person' },
{ text: 'ledencontrole', value: 'ledencontrole', sortable: false },
{ text: '', value: 'actions' },
],
roles: [
'Bestuurder',
'Klankbordgroeplid cc',
'Klankbordgroeplid cp',
'Algemene vergadering cp',
'Algemene vergadering cc',
'Vertegenwoordiger',
],
salutations: ['Mevrouw', 'De heer'],
contactEditMode: false,
dialogContacts: false,
dialogDelete: false,
editedIndex: -1,
editedItem: {},
defaultItem: {},
loading: false,
rules: {
firstname: [(v) => !!v || 'De voornaam is verplicht.'],
lastname: [(v) => !!v || 'De achternaam is verplicht.'],
email: {
required: (value) => !!value || 'Het mailadres is verplicht.',
},
},
}
},
computed: {
local() {
return this.$store.state.members.local
},
addresses() {
if (!this.local || !Array.isArray(this.local.addresses)) return []
return this.local.addresses
},
hasAddresses() {
if (!this.addresses) return false
return this.addresses.length > 0
},
contacts() {
if (!this.local || !Array.isArray(this.local.contacts)) return []
return this.local.contacts
},
hasContacts() {
if (!this.contacts) return false
return this.contacts.length > 0
},
formTitle() {
return this.editedIndex === -1 ? 'new' : 'edit'
},
isCreatedModeContact() {
return this.editedIndex === -1
},
editModeComputed() {
return this.contactEditMode && this.editMode
},
isUserDelegated() {
return this.local.user_id === this.$store.getters.loggedInUser.id
},
},
watch: {
dialogContacts(val) {
val || this.close()
},
},
methods: {
close() {
this.dialogContacts = false
this.$nextTick(() => {
this.editedItem = Object.assign({}, this.defaultItem)
this.editedIndex = -1
this.$refs.card.$el.scrollIntoView(true)
})
},
viewItem(item) {
this.contactEditMode = false
this.editedIndex = this.contacts.indexOf(item)
this.editedItem = Object.assign({}, item)
this.$forceUpdate()
this.dialogContacts = true
},
editItem(item) {
if (!this.editMode) return
this.setItemToEdit(item)
this.dialogContacts = true
},
setItemToEdit(item) {
this.contactEditMode = true
this.editedIndex = this.contacts.indexOf(item)
this.editedItem = Object.assign({}, item)
this.$forceUpdate()
},
async save() {
this.$nextTick(() => {
this.$refs.card.$el.scrollIntoView(true)
})
this.loading = true
this.$nextTick(() => this.$nuxt.$loading.start())
this.dialogContacts = false
if (this.isCreatedModeContact) {
try {
await this.$store.dispatch('members/storeContact', this.editedItem)
this.loading = false
} catch (error) {
console.log('save -> error', error)
this.loading = false
}
} else {
// Edit mode
try {
await this.$store.dispatch('members/storeContact', this.editedItem)
this.loading = false
this.dialogContacts = false
} catch (error) {
console.log('save -> error', error)
this.loading = false
}
}
this.close()
this.$nuxt.$loading.finish()
},
async deleteItem(item) {
if (!item.id) {
this.$notifier.showMessage({
content: `No contact to delete selected`,
color: 'error',
icon: 'icon-message',
})
}
this.$nextTick(() => this.$nuxt.$loading.start())
try {
await this.$store.dispatch('members/deleteContact', item)
this.dialogDelete = false
this.dialogContacts = false
this.$nuxt.$loading.finish()
} catch (error) {
console.log('deleteItem -> error', error)
this.$nuxt.$loading.finish()
}
},
determineNameFullByContactPersonItem(item) {
return Name.fromContactPersonItem(item).nameFull
},
formatAddresses(addresses) {
return addresses.map((x) => this.formatAddress(x)).filter((x) => x)
},
formatAddress(address) {
const addressParts = [
address.indicating || '',
address.house_number || '',
address.postal || '',
address.city || '',
address.country || '',
]
return addressParts
.filter((x) => x)
.map((x) => x.trim())
.join(', ')
.trimRight()
},
},
}
</script>
<style scoped>
.v-card >>> table,
.v-card >>> .v-data-table-header tr,
.v-card >>> .v-data-footer {
background-color: var(--v-primary-base);
}
.v-card >>> .v-data-table-header th span {
color: var(--v-tertiary-base);
}
.v-card >>> .text-start,
.v-card >>> .v-icon,
.v-dialog__content >>> .v-subheader,
.v-dialog__content >>> i {
color: var(--v-txt-base);
font-weight: bold;
}
.v-card >>> .v-subheader {
padding: 0px !important;
}
.v-dialog__content >>> .v-dialog {
border: 4px solid var(--v-secAccent-base);
max-height: 68% !important;
/* box-shadow: inset 0px 0px 2px 2px var(--v-secAccent-base),
inset 6px 6px 14px -14px var(--v-secAccent-base) !important; */
}
.v-dialog .v-card {
background: var(--v-secondary-base);
}
.v-dialog__content >>> .v-input__slot {
background: var(--v-primary-base);
border: 1px solid var(--v-lines-base);
}
.v-menu__content >>> .v-date-picker-table {
height: 285px !important;
}
table tr button.view {
opacity: 0;
}
table tr:hover button.view {
opacity: 1;
}
</style>