Commit 0e8eb105 authored by schrieveslaach's avatar schrieveslaach

WIP

parent dbdef21c
Pipeline #148452428 passed with stages
in 1 minute and 41 seconds
......@@ -7,13 +7,10 @@ module.exports = {
browser: true,
},
extends: [
// https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
// consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
'plugin:vue/essential',
// https://github.com/standard/standard/blob/master/docs/RULES-en.md
'standard'
'standard',
'@nextcloud'
],
// required to lint *.vue files
plugins: [
'vue'
],
......
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
presets: [
'@vue/cli-plugin-babel/preset',
],
};
This diff is collapsed.
<template>
<Content app-name="spgverein">
<app-navigation>
<ul id="app-spgverein-navigation">
<app-navigation-item
title="Bestände"
icon="icon-folder"
:allow-collapse="false"
:loading="isLoadingClubList">
<template>
<app-navigation-item v-for="club in clubs"
:key="club"
:title="club"
:icon="club === selectedClub ? 'icon-category-enabled' : null"
:loading="club === selectedClub && isLoadingMembers"
@click="selectedClub = club">
</app-navigation-item>
<!-- TODO: without the following tag there is nothing in the navigation bar -->
<app-navigation-item title="AppNavigationItemChild2" style="display: none" />
</template>
</app-navigation-item>
</ul>
</app-navigation>
<app-content>
<div class="wait-for-data" v-if="isLoadingMembers">
<font-awesome-icon icon="spinner" size="lg" spin />
Vereinsdaten werden geladen…
</div>
<div v-else-if="!hasMembers" style="width: 100%; margin-bottom: 75px">
Keine Vereinsdaten vorhanden
</div>
<members v-else v-bind:members="membersByFilter" v-bind:cities="cities" :club="selectedClub"></members>
</app-content>
<footer>
<a class="button" :href="exportUrl" download>
<font-awesome-icon icon="file-excel"/>
Export
</a>
<a class="button" @click="showPrintAllMembers()">
<font-awesome-icon icon="print"/>
Etiketten aller Mitglieder drucken
</a>
</footer>
<labels-modal :club="selectedClub" :cities="cities" v-if="printAllLabels" @close="closePrintAllMembers()" />
</Content>
<Content app-name="spgverein">
<AppNavigation>
<ul id="app-spgverein-navigation">
<AppNavigationItem
title="Bestände"
icon="icon-folder"
:allow-collapse="false"
:loading="isLoadingClubList">
<template>
<AppNavigationItem v-for="club in clubs"
:key="club"
:title="club"
:icon="club === selectedClub ? 'icon-category-enabled' : null"
:loading="club === selectedClub && isLoadingMembers"
@click="selectedClub = club" />
<!-- TODO: without the following tag there is nothing in the navigation bar -->
<AppNavigationItem title="AppNavigationItemChild2" style="display: none" />
</template>
</AppNavigationItem>
</ul>
</AppNavigation>
<AppContent :class="{ 'wait-for-data': isLoadingMembers }">
<div v-if="isReady" class="action-buttons">
<button @click="exportAsOdt">
Export
</button>
<button @click="showPrinting = !showPrinting">
Etiketten drucken
</button>
</div>
<Members v-if="hasMembers" />
<div v-else-if="isReady"
class="no-data">
Keine Vereinsdaten vorhanden
</div>
</AppContent>
<Labels v-if="isReady && showPrinting"
v-show="showPrinting"
title="Etiketten drucken"
@close="showPrinting=false" />
</Content>
</template>
<style scoped>
.club-selection {
text-align: right;
margin: 1rem;
.wait-for-data::after {
z-index: 2;
content: '';
height: 28px;
width: 28px;
margin: -16px 0 0 -16px;
position: absolute;
top: 50%;
left: 50%;
border-radius: 100%;
-webkit-animation: rotate 0.8s infinite linear;
animation: rotate 0.8s infinite linear;
-webkit-transform-origin: center;
-ms-transform-origin: center;
transform-origin: center;
border: 2px solid var(--color-loading-light);
border-top-color: var(--color-loading-dark);
}
.club-selection h3 {
display: inline;
.no-data {
text-align: center;
padding-top: 1rem;
}
footer {
background: var(--color-main-background);
position: fixed;
bottom: 0;
height: 40px;
z-index: 1000;
left: 0;
right: 0;
}
footer > a {
float: right;
}
.wait-for-data {
margin: auto;
width: 50%;
padding: 25px;
text-align: center;
.action-buttons {
position: absolute;
right: 0;
}
</style>
......@@ -87,137 +82,75 @@ import AppContent from '@nextcloud/vue/dist/Components/AppContent';
import AppNavigation from '@nextcloud/vue/dist/Components/AppNavigation';
import AppNavigationItem from '@nextcloud/vue/dist/Components/AppNavigationItem';
import Content from '@nextcloud/vue/dist/Components/Content';
import Labels from './labels.vue';
import Members from './members.vue';
import LabelsModal from './labels-modal.vue';
import { mapGetters, mapActions } from 'vuex';
export default {
data () {
return {
clubs: null,
cities: null,
members: null,
selectedClub: '',
nameFilter: null,
printAllLabels: false
};
},
computed: {
isLoadingClubList () {
return this.clubs == null;
},
isLoadingMembers () {
return this.cities == null || this.members == null;
},
hasMembers () {
return this.members != null && this.members.length > 0;
},
membersByFilter () {
if (this.nameFilter == null) {
return this.members;
}
const filter = this.nameFilter.toLowerCase();
return this.members.filter(member => {
return member.fullnames.filter(name => name.toLowerCase().indexOf(filter) !== -1).length > 0;
});
},
exportUrl () {
return generateUrl(`/apps/spgverein/files/${this.selectedClub}.ods`);
}
},
components: {
AppContent,
AppNavigation,
AppNavigationItem,
Content,
Members,
LabelsModal
},
methods: {
showPrintAllMembers () {
this.printAllLabels = true;
},
closePrintAllMembers () {
this.printAllLabels = false;
},
filter (nameFilter) {
this.nameFilter = nameFilter;
},
cleanSearch () {
this.nameFilter = null;
},
fetchMembers () {
fetch(generateUrl(`/apps/spgverein/members/${this.selectedClub}`))
.then(response => response.json())
.then(members => {
const regex = /(.*)\s+((\d+)\s*([a-z])?)/;
return members.sort((m1, m2) => {
let cmp = m1.city.localeCompare(m2.city);
if (cmp === 0) {
const str1 = m1.street.match(regex);
const str2 = m2.street.match(regex);
cmp = str1[1].localeCompare(str2[1]);
if (cmp === 0) {
const a = parseInt(str1[3]);
const b = parseInt(str2[3]);
if (a < b) { cmp = -1; } else if (a > b) { cmp = 1; } else { cmp = 0; }
}
}
return cmp;
});
})
.then(members => {
this.members = members;
});
}
},
created () {
fetch(generateUrl('/apps/spgverein/clubs'))
.then(response => response.json())
.then(clubs => clubs.sort())
.then(clubs => {
this.clubs = clubs;
if (this.clubs.length === 0) {
this.cities = [];
this.members = [];
}
});
},
mounted () {
// TODO OC.Search = new OCA.Search(this.filter, this.cleanSearch);
this.fetchMembers();
},
watch: {
clubs (newClubs) {
this.selectedClub = newClubs[0];
},
selectedClub (newSelectedClub) {
this.cities = null;
fetch(generateUrl(`/apps/spgverein/cities/${newSelectedClub}`))
.then(response => response.json())
.then(cities => {
this.cities = cities;
})
.then(() => this.fetchMembers());
}
}
components: {
AppContent,
AppNavigation,
AppNavigationItem,
Content,
Labels,
Members,
},
data() {
return {
selectedClub: '',
printAllLabels: false,
showPrinting: false,
};
},
computed: {
...mapGetters(['clubs', 'members', 'cities']),
isReady() {
return !this.isLoadingClubList && !this.isLoadingMembers;
},
isLoadingClubList() {
return this.clubs == null;
},
isLoadingMembers() {
return this.members == null;
},
hasMembers() {
return this.members != null && this.members.length > 0;
},
exportUrl() {
return generateUrl(`/apps/spgverein/files/${this.selectedClub}.ods`);
},
},
watch: {
clubs(newClubs) {
this.selectedClub = newClubs[0];
},
selectedClub(club) {
if (club != null) {
this.openClub(club);
}
},
},
mounted() {
this.fetchClubs();
OC.Search = new OCA.Search(this.filterByName, this.clearNameFilter);
},
methods: {
...mapActions(['fetchClubs', 'openClub', 'filterByName', 'clearNameFilter']),
exportAsOdt() {
window.open(this.exportUrl);
},
},
};
</script>
<template>
<modal @close="close()">
<template slot="header">
Etiketten<span v-if="cities.length === 1">{{ cities[0] }}</span>
</template>
<div class="label-parameters">
<div>
<label for="group-by-checkbox" style="word-wrap:break-word">
<input id="group-by-checkbox" type="checkbox" @input="groupMembersInput"
:checked="groupMembers" style="vertical-align: middle;">
Gruppieren über Mitgliedsnummer
</label>
<label for="resigned-members-checkbox" style="word-wrap:break-word">
<input id="resigned-members-checkbox" type="checkbox" @input="resignedMembersInput"
:checked="resignedMembers" style="vertical-align: middle;">
Ausgetretene Mitglieder
</label>
</div>
<div>
<label for="addressline-input" class="text-box-label">
Adresszeile
</label>
<input placeholder="Adresszeile" id="addressline-input" @input="addressLineInput"
:value="addressLine" class="text-box-input">
</div>
<div>
<div style="width: 49%; float: left;">
<label for="label-format-select" class="text-box-label">
Etikettenformat
</label>
<select id="label-format-select" v-model="selectedLabelFormat" class="text-box-input">
<option :key="item" v-for="(item, index) in labelFormats" :value="item.id" :selected="index === 0">
{{item.id}} ({{item.size}}, {{item.rows}}&#215;{{item.columns}})
</option>
</select>
</div>
<div style="width: 49%; float: right;">
<label for="label-offset" class="text-box-label">
Anfang leere Etiketten
</label>
<input type="number" min="0" v-model="labelOffset" :max="maxLabelOffset" step="1"
id="label-offset" class="text-box-input">
</div>
</div>
</div>
<object :data="labelsUrl" type="application/pdf" class="labels">
<embed :src="labelsUrl" type="application/pdf"/>
</object>
<p>
<action-link icon="icon-download" :href="labelsUrl" target="_blank">
Download
</action-link>
</p>
</modal>
</template>
<style scoped>
.labels {
padding-top: 25px;
min-width: 600px;
min-height: 250px;
}
.label-parameters {
display: flex;
flex-direction: column;
}
.text-box-label {
display: block;
}
.text-box-input {
width: 100%;
}
</style>
<script>
import { generateUrl } from '@nextcloud/router';
import debounce from 'debounce';
import ActionLink from '@nextcloud/vue/dist/Components/ActionLink';
import Modal from '@nextcloud/vue/dist/Components/Modal';
export default {
data () {
return {
addressLine: '',
groupMembers: false,
resignedMembers: false,
labelFormatData: {},
selectedLabelFormat: null,
labelOffset: 0
};
},
components: {
ActionLink,
Modal
},
mounted () {
if (localStorage.addressLine) {
this.addressLine = localStorage.addressLine;
}
if (localStorage.groupMembers) {
this.groupMembers = localStorage.groupMembers;
}
fetch(generateUrl('/apps/spgverein/labels/formats'))
.then(response => response.json())
.then(formats => {
this.labelFormatData = formats;
if (localStorage.selectedLabelFormat != null) {
this.selectedLabelFormat = localStorage.selectedLabelFormat;
}
});
},
computed: {
maxLabelOffset () {
if (this.labelFormatData == null || this.selectedLabelFormat == null) {
return 0;
}
const format = this.labelFormatData[this.selectedLabelFormat];
return format.columns * format.rows - 1;
},
labelFormats () {
return Object.keys(this.labelFormatData)
.map(id => ({ id, ...this.labelFormatData[id] }));
},
labelsUrl () {
const params = {};
if (this.addressLine.length > 0) {
params.addressLine = this.addressLine;
}
if (this.groupMembers) {
params.groupMembers = this.groupMembers;
}
if (this.resignedMembers) {
params.resignedMembers = this.resignedMembers;
}
if (this.selectedLabelFormat != null) {
params.format = this.selectedLabelFormat;
}
if (this.labelOffset > 0) {
params.offset = this.labelOffset;
}
if (this.cities.length > 0) {
params.cities = this.cities
.map(v => encodeURIComponent(v))
.join(',');
}
const query = Object.keys(params)
.map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))
.join('&');
return generateUrl(`/apps/spgverein/labels/${this.club}?${query}`);
}
},
props: {
cities: {
type: Array
},
club: {
type: String
}
},
methods: {
close () {
this.$emit('close');
},
addressLineInput: debounce(function (e) {
this.addressLine = e.target.value;
}, 750),
groupMembersInput: debounce(function (e) {
const checked = e.target.checked;
this.groupMembers = checked != null && checked;
}, 750),
resignedMembersInput: debounce(function (e) {
const checked = e.target.checked;
this.resignedMembers = checked != null && checked;
}, 750)
},
watch: {
addressLine (newAddressLine) {
localStorage.addressLine = newAddressLine;
},
groupMembers (newGroupMembers) {
localStorage.groupMembers = newGroupMembers;
},
resignedMembers (newResignedMembers) {
localStorage.resignedMembers = newResignedMembers;
},
selectedLabelFormat (newSelectedLabelFormat) {
localStorage.selectedLabelFormat = newSelectedLabelFormat;
}
}
};
</script>
<template>
<AppSidebar title="Etiketten drucken" @close="close">
<template #primary-actions>
<input id="group-by-checkbox"
type="checkbox"
:checked="groupMembers"
class="checkbox link-checkbox"
@input="groupMembersInput">
<label for="group-by-checkbox" class="link-checkbox-label">
Gruppieren über Mitgliedsnummer
</label>
<input id="resigned-members-checkbox"
type="checkbox"
:checked="resignedMembers"
class="checkbox link-checkbox"
@input="resignedMembersInput">
<label for="resigned-members-checkbox" class="link-checkbox-label">
Ausgetretene Mitglieder
</label>
</template>
<div class="label-parameters">
<div>
<label for="addressline-input" class="text-box-label">
Adresszeile
</label>
<input id="addressline-input"
placeholder="Adresszeile"
:value="addressLine"
class="text-box-input"
@input="addressLineInput">
</div>
<div>
<div style="width: 49%; float: left;">
<label for="label-format-select" class="text-box-label">
Etikettenformat
</label>
<select id="label-format-select" v-model="selectedLabelFormat" class="text-box-input">
<option v-for="(item, index) in labelFormats"
:key="item"
:value="item.id"
:selected="index === 0">
{{ item.id }} ({{ item.size }}, {{ item.rows }}&#215;{{ item.columns }})
</option>
</select>
</div>
<div style="width: 49%; float: right;">
<label for="label-offset" class="text-box-label">
Anfang leere Etiketten
</label>
<input id="label-offset"
v-model="labelOffset"
type="number"
min="0"
:max="maxLabelOffset"
step="1"
class="text-box-input">
</div>
</div>
</div>
<object :data="labelsUrl" type="application/pdf" class="labels-preview">
<embed :src="labelsUrl" type="application/pdf">
</object>
<p>
<ActionLink icon="icon-download" :href="labelsUrl" target="_blank">
Download
</ActionLink>
</p>
</AppSidebar>
</template>
<style scoped>
.labels-preview {
margin-top: 25px;
margin-left: 10px;
margin-right: 10px;
min-height: 250px;
width: 280px;
}
.label-parameters {
display: flex;
flex-direction: column;
padding-top: 1.5rem;
padding-right: 0.3rem;
padding-left: 0.3rem;
}
.text-box-label {
display: block;
}
.text-box-input {