feat: add archived documents section and filtering options to club archive view
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 52s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 52s
This commit is contained in:
@@ -38,6 +38,15 @@
|
||||
<span class="stat-label">Historische Forderungen</span>
|
||||
<strong class="stat-value">{{ archive.summary?.archivedClaims ?? 0 }}</strong>
|
||||
</article>
|
||||
<article class="card stat-card">
|
||||
<span class="stat-label">Archivierte Dokumente</span>
|
||||
<strong class="stat-value">{{ archive.summary?.archivedDocuments ?? 0 }}</strong>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="archive-section card archive-filters">
|
||||
<label><span>Bereich</span><select v-model="filters.entity"><option value="">Alle Bereiche</option><option value="members">Mitglieder</option><option value="requests">Anfragen</option><option value="tasks">Aufgaben</option><option value="claims">Forderungen</option><option value="documents">Dokumente</option></select></label>
|
||||
<label><span>Suche</span><input v-model.trim="filters.search" type="search" placeholder="Name, Titel, Typ oder Status" /></label>
|
||||
</section>
|
||||
|
||||
<section class="archive-notes card" v-if="archive.notes?.length">
|
||||
@@ -47,7 +56,7 @@
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="archive-section card">
|
||||
<section v-if="showEntity('members')" class="archive-section card">
|
||||
<div class="section-header">
|
||||
<div>
|
||||
<h3>Ehemalige Mitglieder</h3>
|
||||
@@ -58,7 +67,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p v-if="archive.inactiveMembers.length === 0" class="state-banner">Keine inaktiven Mitglieder im Archiv.</p>
|
||||
<p v-if="filteredArchive.inactiveMembers.length === 0" class="state-banner">Keine passenden inaktiven Mitglieder im Archiv.</p>
|
||||
<div v-else class="table-wrap">
|
||||
<table class="archive-table">
|
||||
<thead>
|
||||
@@ -71,7 +80,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="member in archive.inactiveMembers" :key="`member-${member.id}`">
|
||||
<tr v-for="member in filteredArchive.inactiveMembers" :key="`member-${member.id}`">
|
||||
<td>{{ member.displayName }}</td>
|
||||
<td>{{ member.email || '–' }}</td>
|
||||
<td>{{ member.city || '–' }}</td>
|
||||
@@ -85,7 +94,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="archive-section card">
|
||||
<section v-if="showEntity('requests')" class="archive-section card">
|
||||
<div class="section-header">
|
||||
<div>
|
||||
<h3>Archivierte Anfragen</h3>
|
||||
@@ -96,7 +105,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p v-if="archive.archivedRequests.length === 0" class="state-banner">Keine archivierten Anfragen vorhanden.</p>
|
||||
<p v-if="filteredArchive.archivedRequests.length === 0" class="state-banner">Keine passenden archivierten Anfragen vorhanden.</p>
|
||||
<div v-else class="table-wrap">
|
||||
<table class="archive-table">
|
||||
<thead>
|
||||
@@ -109,7 +118,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="request in archive.archivedRequests" :key="`request-${request.id}`">
|
||||
<tr v-for="request in filteredArchive.archivedRequests" :key="`request-${request.id}`">
|
||||
<td>{{ displayRequestType(request.requestType) }}</td>
|
||||
<td>{{ request.subject || '–' }}</td>
|
||||
<td>{{ request.personName || request.email || '–' }}</td>
|
||||
@@ -123,7 +132,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="archive-section card">
|
||||
<section v-if="showEntity('tasks')" class="archive-section card">
|
||||
<div class="section-header">
|
||||
<div>
|
||||
<h3>Archivierte Aufgaben</h3>
|
||||
@@ -134,7 +143,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p v-if="archive.archivedTasks.length === 0" class="state-banner">Keine archivierten Aufgaben vorhanden.</p>
|
||||
<p v-if="filteredArchive.archivedTasks.length === 0" class="state-banner">Keine passenden archivierten Aufgaben vorhanden.</p>
|
||||
<div v-else class="table-wrap">
|
||||
<table class="archive-table">
|
||||
<thead>
|
||||
@@ -147,7 +156,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="task in archive.archivedTasks" :key="`task-${task.id}`">
|
||||
<tr v-for="task in filteredArchive.archivedTasks" :key="`task-${task.id}`">
|
||||
<td>{{ task.title }}</td>
|
||||
<td>{{ task.taskType || '–' }}</td>
|
||||
<td>{{ displayPriority(task.priority) }}</td>
|
||||
@@ -161,7 +170,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="archive-section card">
|
||||
<section v-if="showEntity('claims')" class="archive-section card">
|
||||
<div class="section-header">
|
||||
<div>
|
||||
<h3>Historische Forderungen</h3>
|
||||
@@ -169,7 +178,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="archive.archivedClaims.length === 0" class="state-banner">Keine historischen Forderungen vorhanden.</p>
|
||||
<p v-if="filteredArchive.archivedClaims.length === 0" class="state-banner">Keine passenden historischen Forderungen vorhanden.</p>
|
||||
<div v-else class="table-wrap">
|
||||
<table class="archive-table">
|
||||
<thead>
|
||||
@@ -182,7 +191,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="claim in archive.archivedClaims" :key="`claim-${claim.id}`">
|
||||
<tr v-for="claim in filteredArchive.archivedClaims" :key="`claim-${claim.id}`">
|
||||
<td>{{ claim.memberName || '–' }}</td>
|
||||
<td>{{ claim.claimType || '–' }}</td>
|
||||
<td>{{ displayClaimStatus(claim.status) }}</td>
|
||||
@@ -193,6 +202,12 @@
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="showEntity('documents')" class="archive-section card">
|
||||
<div class="section-header"><div><h3>Archivierte Dokumente</h3><p>Abgelegte oder ersetzte Vereinsdokumente.</p></div></div>
|
||||
<p v-if="filteredArchive.archivedDocuments.length === 0" class="state-banner">Keine passenden archivierten Dokumente vorhanden.</p>
|
||||
<div v-else class="table-wrap"><table class="archive-table"><thead><tr><th>Titel</th><th>Typ</th><th>Status</th><th>Zuletzt geändert</th></tr></thead><tbody><tr v-for="document in filteredArchive.archivedDocuments" :key="`document-${document.id}`"><td>{{ document.title }}</td><td>{{ document.documentType }}</td><td>{{ document.status }}</td><td>{{ formatDateTime(document.archivedAt || document.updatedAt) }}</td></tr></tbody></table></div>
|
||||
</section>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -209,11 +224,13 @@ function createEmptyArchive() {
|
||||
archivedRequests: 0,
|
||||
archivedTasks: 0,
|
||||
archivedClaims: 0,
|
||||
archivedDocuments: 0,
|
||||
},
|
||||
inactiveMembers: [],
|
||||
archivedRequests: [],
|
||||
archivedTasks: [],
|
||||
archivedClaims: [],
|
||||
archivedDocuments: [],
|
||||
notes: [],
|
||||
};
|
||||
}
|
||||
@@ -225,10 +242,22 @@ export default {
|
||||
loading: false,
|
||||
loadError: '',
|
||||
archive: createEmptyArchive(),
|
||||
filters: { entity: '', search: '' },
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['currentClub']),
|
||||
filteredArchive() {
|
||||
const needle = this.filters.search.toLowerCase();
|
||||
const filter = (entries, fields) => entries.filter((entry) => !needle || fields.map((field) => entry[field]).join(' ').toLowerCase().includes(needle));
|
||||
return {
|
||||
inactiveMembers: filter(this.archive.inactiveMembers, ['displayName', 'email', 'city']),
|
||||
archivedRequests: filter(this.archive.archivedRequests, ['subject', 'personName', 'email', 'requestType', 'status']),
|
||||
archivedTasks: filter(this.archive.archivedTasks, ['title', 'taskType', 'priority', 'status']),
|
||||
archivedClaims: filter(this.archive.archivedClaims, ['memberName', 'claimType', 'status']),
|
||||
archivedDocuments: filter(this.archive.archivedDocuments || [], ['title', 'documentType', 'status']),
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentClub: {
|
||||
@@ -243,6 +272,7 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showEntity(entity) { return !this.filters.entity || this.filters.entity === entity; },
|
||||
async loadArchive() {
|
||||
if (!this.currentClub) return;
|
||||
this.loading = true;
|
||||
|
||||
@@ -8,9 +8,10 @@
|
||||
Letzte Vorgänge aus Anfragen und Aufgaben für den Vereinsalltag in einer gemeinsamen Übersicht.
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" class="btn-secondary" @click="loadHistory" :disabled="loading">
|
||||
{{ loading ? 'Lädt…' : 'Neu laden' }}
|
||||
</button>
|
||||
<div class="header-actions">
|
||||
<button type="button" class="btn-secondary" @click="exportHistory" :disabled="filteredEntries.length === 0">CSV exportieren</button>
|
||||
<button type="button" class="btn-secondary" @click="loadHistory" :disabled="loading">{{ loading ? 'Lädt…' : 'Neu laden' }}</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section v-if="!currentClub" class="card empty-state">
|
||||
@@ -61,6 +62,8 @@
|
||||
<span>Suche</span>
|
||||
<input v-model.trim="filters.search" type="text" placeholder="Titel, Person oder Beschreibung" />
|
||||
</label>
|
||||
<label><span>Von</span><input v-model="filters.from" type="date" /></label>
|
||||
<label><span>Bis</span><input v-model="filters.to" type="date" /></label>
|
||||
</div>
|
||||
|
||||
<div class="history-note-box">
|
||||
@@ -208,6 +211,8 @@ export default {
|
||||
filters: {
|
||||
source: '',
|
||||
search: '',
|
||||
from: '',
|
||||
to: '',
|
||||
},
|
||||
};
|
||||
},
|
||||
@@ -381,6 +386,9 @@ export default {
|
||||
if (this.filters.source && entry.source !== this.filters.source) {
|
||||
return false;
|
||||
}
|
||||
const occurredAt = new Date(entry.occurredAt);
|
||||
if (this.filters.from && occurredAt < new Date(`${this.filters.from}T00:00:00`)) return false;
|
||||
if (this.filters.to && occurredAt > new Date(`${this.filters.to}T23:59:59`)) return false;
|
||||
if (!search) {
|
||||
return true;
|
||||
}
|
||||
@@ -482,6 +490,16 @@ export default {
|
||||
}
|
||||
this.$router.push(entry.to);
|
||||
},
|
||||
exportHistory() {
|
||||
const rows = [['Zeitpunkt', 'Quelle', 'Titel', 'Beschreibung', 'Person', 'Status'], ...this.filteredEntries.map((entry) => [entry.occurredAt, entry.sourceLabel, entry.title, entry.description, entry.personLabel, entry.statusLabel])];
|
||||
const csv = rows.map((row) => row.map((value) => `"${String(value || '').replace(/"/g, '""')}"`).join(';')).join('\n');
|
||||
const url = URL.createObjectURL(new Blob([`\uFEFF${csv}`], { type: 'text/csv;charset=utf-8' }));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `vereinshistorie-${new Date().toISOString().slice(0, 10)}.csv`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
},
|
||||
formatDateTime(value) {
|
||||
if (!value) return '–';
|
||||
const date = new Date(value);
|
||||
|
||||
Reference in New Issue
Block a user