Files
nuxt-frontend/components/Members/EmployeesMembers.vue
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

544 lines
16 KiB
Vue

<template>
<accordion-card title="Werknemers">
<v-data-table
:headers="headers"
:items="summaries"
:options="options"
sort-by="year"
sort-desc
hide-default-footer
item-key="accreditation"
flat
v-if="hasSummaries"
>
<template v-slot:item.profiel="{ 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-menu
offset-y
v-if="
(editing && $store.getters.isSuperAdminOrAdmin) ||
(editing && $store.getters.isOnlyMemberEditor && !item.approved_at)
"
>
<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="editing"
v-model="dialogDelete"
v-if="!$store.getters.isOnlyMemberEditor"
>
<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_employeesmembers_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="dialogEmployees"
max-width="75%"
v-if="editing && $store.getters['members/isSuperAdminAdminOrDelegated']"
>
<template v-slot:activator="{ on, attrs }">
<v-btn
v-if="editing && $store.getters.isSuperAdminOrAdmin"
class="my-10 cta-secondary"
block
depressed
min-height="60px"
:disabled="isCreateMode"
v-on="on"
>
<v-icon x-small class="mx-4">icon-add</v-icon>
Voeg jaartal toe</v-btn
>
</template>
<v-form ref="form" v-model="valid" lazy-validation>
<v-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"
>Jaar</v-subheader
>
</v-col>
<v-col cols="12" sm="12" md="6">
<v-select
:items="yearsComputed"
v-model.number="editedItem.year"
:outlined="editing"
:solo="!editing"
:disabled="!editing || !$store.getters.isSuperAdminOrAdmin"
:flat="!editing"
v-if="isCreatedModeSummary"
:rules="rules.year"
required
error
/>
<v-text-field
v-model.number="editedItem.year"
:outlined="editing"
:solo="!editing"
disabled
:flat="!editing"
v-else
:rules="rules.year"
required
error
/>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="12" md="6">
<v-subheader class="txt--text font-weight-black"
>Werkelijk aantal medewerkers per 31-12-{{
editedItem.year - 1 || '__'
}}</v-subheader
>
</v-col>
<v-col cols="12" sm="12" md="6">
<v-text-field
v-model.number="editedItem.real_number_last_year"
:outlined="editing"
:solo="!editing"
:disabled="!editing"
:flat="!editing"
type="number"
:rules="rules.real_number_last_year"
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"
>Schatting aantal medewerkers per 31-12-{{
editedItem.year || '__'
}}</v-subheader
>
</v-col>
<v-col cols="12" sm="12" md="6">
<v-text-field
v-model="editedItem.estimated_number_next_year"
:outlined="editing"
:solo="!editing"
:disabled="!editing"
:flat="!editing"
type="number"
:rules="rules.estimated_number_next_year"
required
error
></v-text-field>
</v-col>
</v-row>
<v-row
v-if="
isCreatedModeSummary &&
editing &&
$store.getters.isSuperAdminOrAdmin
"
>
<v-col cols="12" sm="12" md="6">
<v-subheader class="txt--text font-weight-black"
>Pas toe op alle leden
</v-subheader>
</v-col>
<v-col class="checkbox" cols="12" sm="12" md="1">
<v-checkbox
v-model="editedItem.toAll"
:disabled="!editing"
></v-checkbox>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="12" md="12">
<small> * Personeel in loondienst </small>
</v-col>
<v-col cols="12" sm="12" md="12">
<small
>Stuur de onderbouwing van het werknemersaantal van vorig
jaar (bijvoorbeeld een pagina uit een jaarverslag) naar
<a href="mailto:info@ggzecademy.nl"
>info@ggzecademy.nl</a
></small
>
</v-col>
</v-row>
</v-container>
</v-card-text>
<v-divider />
<v-card-actions v-if="editing">
<v-btn
class="ma-2 white--text"
color="info"
depressed
rounded
:disabled="loading"
v-if="isFormFilled"
@click="validate() && save()"
>{{
!isCreatedModeSummary && $store.getters.isSuperAdminOrAdmin
? 'Goedkeuren'
: $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-form>
</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'
export default {
components: {
accordionCard,
},
props: {
editMode: {
type: Boolean,
default: false,
},
isCreateMode: {
type: Boolean,
default: true,
},
},
data() {
return {
rules: {
year: [(v) => !!v || 'Het jaar is vereist.'],
real_number_last_year: [
(v) => +v >= 0 || 'Het werkelijk aantal werknemers is vereist.',
],
estimated_number_next_year: [
(v) => +v >= 0 || 'De schatting aantal werknemers is vereist.',
],
},
options: {
itemsPerPage: -1,
},
headers: [
{ text: 'jaar', value: 'year' },
{ text: 'opgave medewerkers', value: 'real_number_last_year' },
{
text: 'schatting medewerkers komende jaar',
value: 'estimated_number_next_year',
},
{ text: 'ledencontrole', value: 'profiel', sortable: false },
{ text: '', value: 'actions' },
],
employersEditMode: false,
dialogEmployees: false,
dialogDelete: false,
editedIndex: -1,
editedItem: {},
loading: false,
valid: true,
}
},
computed: {
editing() {
return (
this.$store.getters.isOnlyMemberEditor ||
(this.editMode && this.$store.getters.isSuperAdminOrAdmin)
)
},
local() {
return this.$store.state.members.local
},
isCreatedModeSummary() {
return this.editedIndex === -1
},
formTitle() {
return this.isCreatedModeSummary ? 'Nieuw' : 'Bewerken'
},
summaries() {
if (!this.local || !Array.isArray(this.local.summaries)) return []
// Years older than the current are made hidden for Users delegated to edit members
if (this.$store.getters.isOnlyMemberEditor) {
const yearValid = new Date().getFullYear()
const summary = this.local.summaries.find(
(summary) => summary.year === yearValid
)
return summary ? [summary] : []
}
return this.local.summaries
},
hasSummaries() {
if (!this.summaries) return false
return this.summaries.length > 0
},
years() {
const year = new Date().getFullYear()
return Array.from({ length: 51 }, (value, index) => 2010 + index)
},
yearsTaken() {
if (!this.hasSummaries) return []
return this.summaries.map(({ year }) => year)
},
yearsComputed() {
return this.years.filter((y) => !this.yearsTaken.includes(y))
},
isFormFilled() {
if (this.editedItem['year'] === null) return false
if (this.editedItem['year'] === undefined) return false
if (this.editedItem['year'] === '') return false
if (this.editedItem['real_number_last_year'] === null) return false
if (this.editedItem['real_number_last_year'] === undefined) return false
if (this.editedItem['real_number_last_year'] === '') return false
if (this.editedItem['estimated_number_next_year'] === null) return false
if (this.editedItem['estimated_number_next_year'] === undefined)
return false
if (this.editedItem['estimated_number_next_year'] === '') return false
return true
},
defaultItem() {
return {
year: this.yearsTaken.includes(new Date().getFullYear())
? this.yearsComputed[0]
: new Date().getFullYear(),
real_number_last_year: 0,
estimated_number_next_year: 0,
}
},
},
watch: {
dialogEmployees(val) {
val || this.close()
if (Object.entries(this.editedItem).length === 0) {
this.editedItem = Object.assign({}, this.defaultItem)
}
},
},
methods: {
close() {
this.dialogEmployees = false
this.$nextTick(() => {
this.editedItem = Object.assign({}, this.defaultItem)
this.editedIndex = -1
})
},
editItem(item) {
if (!this.editing) return
this.setSummaryToEdit(item)
},
setSummaryToEdit(item) {
this.editedIndex = this.summaries.indexOf(item)
this.editedItem = Object.assign({}, item)
this.$forceUpdate()
this.dialogEmployees = true
},
async deleteItem(item) {
if (!item.id) {
this.$notifier.showMessage({
content: `No summary to delete selected`,
color: 'error',
icon: 'icon-message',
})
}
this.$nextTick(() => this.$nuxt.$loading.start())
try {
await this.$store.dispatch('members/deleteSummary', item)
this.dialogDelete = false
this.dialogEmployees = false
this.$nuxt.$loading.finish()
} catch (error) {
console.log('deleteItem -> error', error)
this.$nuxt.$loading.finish()
}
this.$forceUpdate()
},
validate() {
return this.$refs.form.validate()
},
async save() {
this.loading = true
this.$nextTick(() => this.$nuxt.$loading.start())
this.dialogEmployees = false
if (this.isCreatedModeSummary) {
try {
await this.$store.dispatch('members/storeSummary', this.editedItem)
this.loading = false
} catch (error) {
console.log('save -> error', error)
this.loading = false
}
} else {
// Edit mode
try {
await this.$store.dispatch('members/storeSummary', this.editedItem)
this.dialogEmployees = false
this.loading = false
} catch (error) {
console.log('save -> error', error)
this.loading = false
}
}
this.close()
this.$nuxt.$loading.finish()
},
},
}
</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);
/* 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;
}
</style>