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