Commit 266ab58b authored by schrieveslaach's avatar schrieveslaach

WIP

parent dbdef21c
Pipeline #144539271 passed with stages
in 1 minute and 36 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',
],
};
......@@ -1069,6 +1069,21 @@
"core-js": "^3.6.4"
}
},
"@nextcloud/eslint-config": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@nextcloud/eslint-config/-/eslint-config-2.0.0.tgz",
"integrity": "sha512-rpBCwFm4/UpJUhGf38CHbOGzoQikvht90JqqbI0GtbpP2Ty1F8Pvr/3ntg+OVeu6utkJL1hybtD9pQswiZfWCg==",
"dev": true
},
"@nextcloud/eslint-plugin": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@nextcloud/eslint-plugin/-/eslint-plugin-1.4.0.tgz",
"integrity": "sha512-w3k04Rj1lBHO4MNhiO4e4WPnijsqTYJhBJ3v+8bYlBi83L5OG+oqu7UHq4ETeDrHVC8QLweu/8vx6iGah00img==",
"dev": true,
"requires": {
"requireindex": "^1.2.0"
}
},
"@nextcloud/event-bus": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@nextcloud/event-bus/-/event-bus-1.1.4.tgz",
......@@ -9808,6 +9823,12 @@
"integrity": "sha1-0LMp7MfMD2Fkn2IhW+aa9UqomJs=",
"dev": true
},
"requireindex": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz",
"integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==",
"dev": true
},
"requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npm.taobao.org/requires-port/download/requires-port-1.0.0.tgz",
......
<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="!isLoadingClubList && !hasMembers">
Keine Vereinsdaten vorhanden
</div>
<Members v-else
:members="members"
:cities="cities" />
</AppContent>
<AppSidebar v-show="showPrinting" />
</Content>
</template>
<style scoped>
.club-selection {
text-align: right;
margin: 1rem;
}
.club-selection h3 {
display: inline;
}
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;
.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);
}
</style>
......@@ -86,138 +60,119 @@ import { generateUrl } from '@nextcloud/router';
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 AppSidebar from '@nextcloud/vue/dist/Components/AppSidebar';
import Content from '@nextcloud/vue/dist/Components/Content';
import Members from './members.vue';
import LabelsModal from './labels-modal.vue';
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,
AppSidebar,
Content,
Members,
},
data() {
return {
clubs: null,
cities: null,
members: null,
selectedClub: '',
printAllLabels: false,
showPrinting: false,
};
},
computed: {
isLoadingClubList() {
return this.clubs == null;
},
isLoadingMembers() {
return this.cities == null || 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(newSelectedClub) {
this.cities = null;
fetch(generateUrl(`/apps/spgverein/cities/${newSelectedClub}`))
.then(response => response.json())
.then(cities => {
this.cities = cities;
})
.then(() => this.fetchMembers());
},
},
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() {
this.fetchMembers();
},
methods: {
showPrintAllMembers() {
this.printAllLabels = true;
},
closePrintAllMembers() {
this.printAllLabels = false;
},
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;
});
},
},
};
</script>
This diff is collapsed.
import { library } from '@fortawesome/fontawesome-svg-core';
import { faDownload, faFile, faFileExcel, faFilePdf, faPrint, faTimes, faSpinner } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import Vue from 'vue';
import App from './app.vue';
library.add(faDownload);
library.add(faFile);
library.add(faFileExcel);
library.add(faFilePdf);
library.add(faPrint);
library.add(faSpinner);
library.add(faTimes);
Vue.component('font-awesome-icon', FontAwesomeIcon);
Vue.component('app', App);
export default new Vue({
el: '#app',
render: h => h(App)
el: '#content',
render: h => h(App),
});
<template>
<tr :id="member.id">
<td v-for="name in member.fullnames" :key="name">
{{ name }}
</td>
<td>{{ member.street }}</td>
<td>{{ member.zipcode }}</td>
<td>{{ member.city }}</td>
<tr :id="member.id">
<td v-for="name in member.fullnames" :key="name">
{{ name }}
</td>
<td>{{ member.street }}</td>
<td>{{ member.zipcode }}</td>
<td>{{ member.city }}</td>
<td>
<ul>
<li>Geburtsdatum: {{ member.birth }}</li>
<li>Eintrittsdatum: {{ member.admissionDate }}</li>
<li v-if="member.resignationDate">Austrittsdatum: {{ member.resignationDate }}</li>
</ul>
</td>
<td>
<ul>
<li>Geburtsdatum: {{ member.birth }}</li>
<li>Eintrittsdatum: {{ member.admissionDate }}</li>
<li v-if="member.resignationDate">
Austrittsdatum: {{ member.resignationDate }}
</li>
</ul>
</td>
<td>
<template v-if="Object.keys(member.files).length > 0">
<a class="attachment-link" :href="getFileUrl(file)" v-for="file in member.files" :key="file">
<font-awesome-icon icon="file"/>
{{ file }}
</a>
</template>
<template v-else>
<span>&nbsp;</span>
</template>
</td>
</tr>
<td>
<template v-if="Object.keys(member.files).length > 0">
<a v-for="file in member.files"
:key="file"
class="attachment-link"
:href="getFileUrl(file)">
<font-awesome-icon icon="file" />
{{ file }}
</a>
</template>
<template v-else>
<span>&nbsp;</span>
</template>
</td>
</tr>
</template>
<style scoped>
......@@ -43,23 +48,23 @@
import { generateUrl } from '@nextcloud/router';
export default {
data () {
return {};
},
props: {
member: {
type: Object
},
club: {
type: String
}
},
props: {
member: {
type: Object,
},
club: {
type: String,
},
},
data() {
return {};
},
methods: {
getFileUrl (file) {
return generateUrl(`/apps/spgverein/files/${this.club}/${this.member.id}/${file}`);
}
}
methods: {
getFileUrl(file) {
return generateUrl(`/apps/spgverein/files/${this.club}/${this.member.id}/${file}`);
},
},
};
</script>
<template>
<div style="width: 100%">
<template v-for="city in cities">
<section :key="city" class="city" v-if="getMembersOf(city).length > 0">
<div class="city-header">
<h2> {{ city }} </h2>
<a class="button" v-on:click="openLabelsDialog(city)">
<font-awesome-icon icon="print"/>
</a>
</div>
<table>
<colgroup>
<col style="width:25%">
<col style="width:20%">
<col style="width:5%">
<col style="width:20%">
<col style="width:30%">
</colgroup>
<thead>
<tr>
<th>Name</th>
<th>Straße</th>
<th>Postleitzahl</th>
<th>Ort</th>
<th>Datümer</th>
<th>Anhänge</th>
</tr>
</thead>
<tbody>
<member :club="club" v-bind:member="member" v-for="member in getMembersOf(city)" :key="member.id"/>
</tbody>
</table>
</section>
</template>
<labels-modal :club="club" :cities="[selectedCityForLabels]" v-if="showModal" @close="closeLabelsDialog" />
</div>
<div style="width: 100%">
<div class="city-filter">
<Multiselect
v-model="selectedCities"
style="width: 100%"
:options="cities"
:multiple="true"
placeholder="Ort" />
</div>
<table>
<colgroup>
<col style="width:20%">
<col style="width:15%">
<col style="width:5%">
<col style="width:25%">
<col style="width:30%">
<col style="width:15%">
</colgroup>
<thead>
<tr>
<th>Name</th>
<th>Straße</th>
<th>Postleitzahl</th>
<th>Ort</th>
<th>Datümer</th>
<th>Anhänge</th>
</tr>
</thead>
<tbody>
<Member v-for="member in filteredMembers"
:key="member.id"
:club="club"
:member="member" />
</tbody>
</table>
<LabelsModal v-if="showModal"
:club="club"
:cities="[selectedCityForLabels]"
@close="closeLabelsDialog" />
</div>
</template>
<style scoped>
.city-header {
display: grid;
grid-template-columns: 1fr 1fr;
}
@media only screen and (max-width: 760px), (min-device-width: 768px) and (max-device-width: 1024px) {
.city-header h2 {
min-width: 250px;
}
}
.city-header a {
justify-self: end;
}
.city {
padding: 1rem;
.city-filter {
margin-top: 5rem;
}
.city + .city {
padding-top: 2rem;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 1rem;
}
tr:nth-of-type(even) {
......@@ -150,62 +134,85 @@ td, th {
<script>
import Member from './member.vue';
import LabelsModal from './labels-modal.vue';
import Multiselect from '@nextcloud/vue/dist/Components/Multiselect';
export default {
data () {
return {
selectedCityForLabels: null
};
},
components: {
Member,
LabelsModal
},