From fd0b3020bf5c1d3f3cbdbf8895a545e1f935a758 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Wed, 22 Jul 2026 15:25:00 +0200 Subject: [PATCH] feat: implement load request ID for improved data loading consistency across views --- docs/club-outstanding-plan.md | 84 +++++++++++++++----- frontend/src/views/ClubAccountsView.vue | 25 ++++-- frontend/src/views/ClubCommunicationView.vue | 14 +++- frontend/src/views/ClubInvoicesView.vue | 14 +++- frontend/src/views/ClubTasksView.vue | 14 +++- 5 files changed, 113 insertions(+), 38 deletions(-) diff --git a/docs/club-outstanding-plan.md b/docs/club-outstanding-plan.md index a9b15f51..494aeb51 100755 --- a/docs/club-outstanding-plan.md +++ b/docs/club-outstanding-plan.md @@ -11,11 +11,17 @@ Die vorhandenen Club-Module sollen nicht mehr nur "vorhanden", sondern im Alltag ## Aktueller Fokus -- Kommunikation -- Historie -- Archiv -- Restliche Club-UI-Absicherung in den Kernviews -- Danach erst das `mein-tt.de`-Produkt inhaltlich weiter ausbauen +Die Reihenfolge folgt Abhaengigkeiten und Betriebsrisiko: Erst muessen die +vorhandenen Kernarbeitsbereiche bei Clubwechsel, Rechten und Fehlern stabil +sein. Danach wird der reale Kommunikationsversand abgesichert. Erst auf dieser +Basis lohnt sich der Ausbau von Nachvollziehbarkeit (Historie und Archiv) und +der letzte UX-Feinschliff. + +1. Club-UI-Absicherung in den Kernviews +2. Kommunikation produktiv absichern +3. Historie und Archiv vertiefen +4. Restliche Club-UX und Dashboard verdichten +5. Erst danach das `mein-tt.de`-Produkt inhaltlich ausbauen ## Arbeitsreihenfolge @@ -48,47 +54,66 @@ Fertig, wenn: - Beim Clubwechsel keine Formularwerte aus dem vorherigen Club sichtbar bleiben. - Read-only-Nutzer die Bereiche verstehen, ohne in kaputte Aktionen zu laufen. +Abnahmefolge je View: +1. Mit Schreibrechten Liste laden, Datensatz oeffnen, bearbeiten, Auswahl loeschen und neu anlegen. +2. Waerend ein Detail offen ist den Club wechseln und pruefen, dass keine alten Werte bleiben. +3. Mit Leserechten laden und pruefen, dass Orientierung, Navigation und Hinweise funktionieren. +4. Leere Liste, Reload und einen API-Fehler pruefen. + +Prioritaet innerhalb der Phase: +1. `ClubTasksView.vue` und `ClubCommunicationView.vue` (taegliche, zustandsreiche Arbeitsbereiche). +2. `ClubAccountsView.vue` und `ClubInvoicesView.vue` (finanzielle Folgerisiken). + ### Phase 2: Kommunikation produktiv absichern Ziel: - Nachrichtenfluss nicht nur funktional, sondern praxisnah robust machen. Arbeitspakete: -- SMTP real testen, inklusive Zustellprotokoll und Fehlerfaelle. -- Optional Reply-To pro Verein oder Kommunikationsvorlage sauber ergaenzen. -- Dokumentanhaenge fuer Nachrichten und Vorlagen einfuehren. -- Serienvorlagen und wiederkehrende Nachrichtentypen vorbereiten. +1. SMTP real testen, inklusive Zustellprotokoll, Konfigurationsfehlern und Fehlerfaellen. +2. Ergebnis und notwendige Betriebsparameter pro Verein dokumentieren. +3. Optional Reply-To pro Verein oder Kommunikationsvorlage sauber ergaenzen. +4. Dokumentanhaenge fuer Nachrichten und Vorlagen einfuehren. +5. Serienvorlagen und wiederkehrende Nachrichtentypen vorbereiten. Fertig, wenn: - Eine Testzustellung je Verein reproduzierbar gelingt. - Fehlende SMTP-Konfiguration klar und frueh sichtbar wird. - Nachrichten mit Anhaengen und Vorlagen ohne Sonderlogik im Alltag einsetzbar sind. +Hinweis zur Abhaengigkeit: +- Anhaenge und Serienvorlagen erst nach erfolgreichem SMTP-Smoke-Test umsetzen. +- Der SMTP-Test braucht eine reale, freigegebene Versandkonfiguration und mindestens eine Testadresse. + ### Phase 3: Historie und Archiv vertiefen Ziel: - Vergaengliche Vorgange muessen spaeter besser auffindbar und nachvollziehbar sein. Arbeitspakete: -- Historie nach Modulen und Vorgangstypen filtern. -- Historie exportierbar machen. -- Historie mit Zielobjekten und Querverweisen versehen. -- Archiv um weitere Entitaeten erweitern. -- Archivsuche und Schnellfilter verbessern. +1. Historie nach Modulen und Vorgangstypen filtern. +2. Historie mit Zielobjekten und Querverweisen versehen. +3. Archivsuche und Schnellfilter verbessern. +4. Archiv um weitere Entitaeten erweitern. +5. Historie exportierbar machen. Fertig, wenn: - Vorstand oder Verwaltung einen Vorgang aus Historie oder Archiv ohne Umweg wiederfinden kann. - Wichtige Clubobjekte nicht nur archiviert, sondern auch wieder auffindbar und verlinkt sind. +Hinweis zur Abhaengigkeit: +- Export erst nach Festlegung von Filtern, Zielobjekten und darzustellenden Feldern bauen; sonst wird ein instabiles Datenformat festgeschrieben. + ### Phase 4: Restliche Club-UX verdichten Ziel: - Das Dashboard und die Detailmodule sollen gleiche Sprache sprechen. Arbeitspakete: -- Dashboard-Schnellzugriffe weiter auf Tagesgeschaeft trimmen. -- Verlinkungen zwischen Dashboard, Mitgliedern, Zahlungen, Kommunikation und Archiv schaerfen. -- Kleine Inkonsistenzen in Statusworten, Akzentfarben und Listenlabels bereinigen. +1. Verlinkungen zwischen Dashboard, Mitgliedern, Zahlungen, Kommunikation und Archiv schaerfen. +2. Dashboard-Schnellzugriffe mit den tatsaechlich haeufigsten Tagesaktionen abgleichen und verdichten. +3. Kleine Inkonsistenzen in Statusworten, Akzentfarben und Listenlabels bereinigen. +4. Einen abschliessenden bereichsuebergreifenden Smoke-Check fuer Navigation, Rechte und leere Zustaende ausfuehren. Fertig, wenn: - Der Einstieg immer zur naechsten sinnvollen Aktion fuehrt. @@ -112,11 +137,26 @@ Arbeitspakete: ## Konkrete naechste Tickets -- SMTP-Test fuer Kommunikation mit realer Zieladresse und dokumentiertem Ergebnis. -- Dokumentanhaenge fuer Kommunikation und Vorlagen. -- Historie: Filter und Export. -- Archiv: weitere Objektklassen und Suche. -- Club-UI-Smoke-Check fuer Aufgaben, Kommunikation, Konten und Rechnungen. +### Jetzt + +1. Club-UI-Smoke-Check und Kantenfallkorrekturen fuer Aufgaben und Kommunikation. +2. Club-UI-Smoke-Check und Kantenfallkorrekturen fuer Konten und Rechnungen. +3. Einheitliche Read-only-, Lade- und Fehlerzustaende in den vier Kernviews. + +### Danach + +4. SMTP-Test fuer Kommunikation mit realer Zieladresse und dokumentiertem Ergebnis. +5. Sichtbare Behandlung fehlender oder fehlerhafter SMTP-Konfiguration nachziehen. +6. Reply-To je Verein/Vorlage entscheiden und bei Bedarf umsetzen. +7. Dokumentanhaenge fuer Kommunikation und Vorlagen. +8. Serienvorlagen und wiederkehrende Nachrichtentypen. + +### Anschliessend + +9. Historie: Filter nach Modul und Vorgangstyp sowie Querverweise auf Zielobjekte. +10. Archiv: Suche, Schnellfilter und danach weitere Objektklassen. +11. Historie: Export auf Basis der stabilen Filter- und Objektstruktur. +12. Dashboard-Deep-Links, Schnellzugriffe und Statuskonsistenz abschliessen. ## Abhaengigkeiten diff --git a/frontend/src/views/ClubAccountsView.vue b/frontend/src/views/ClubAccountsView.vue index 15893524..1489bcb7 100755 --- a/frontend/src/views/ClubAccountsView.vue +++ b/frontend/src/views/ClubAccountsView.vue @@ -483,6 +483,7 @@ export default { data() { return { loading: false, + loadRequestId: 0, saving: false, transactionSaving: false, loadError: '', @@ -562,6 +563,7 @@ export default { this.clearAccountsState(); return; } + this.clearAccountsState(); await this.loadAccounts(); }, }, @@ -619,6 +621,8 @@ export default { }, methods: { clearAccountsState() { + this.loadRequestId += 1; + this.loading = false; this.accounts = []; this.transactions = []; this.paymentClaims = []; @@ -732,28 +736,34 @@ export default { this.selectedTransactionId = null; this.transactionForm = createEmptyTransactionForm(accountId); }, - async loadPaymentClaims() { - if (!this.currentClub) { + async loadPaymentClaims(clubId = this.currentClub, requestId = this.loadRequestId) { + if (!clubId) { this.paymentClaims = []; return; } try { - const response = await apiClient.get(`/club-payment-claims/${this.currentClub}`); + const response = await apiClient.get(`/club-payment-claims/${clubId}`); + if (requestId !== this.loadRequestId || clubId !== this.currentClub) return; this.paymentClaims = Array.isArray(response.data?.claims) ? response.data.claims : []; } catch (_error) { + if (requestId !== this.loadRequestId || clubId !== this.currentClub) return; this.paymentClaims = []; } }, async loadAccounts() { - if (!this.currentClub) return; + const clubId = this.currentClub; + if (!clubId) return; + const requestId = ++this.loadRequestId; this.loading = true; this.loadError = ''; try { - const response = await apiClient.get(`/club-accounts/${this.currentClub}`); + const response = await apiClient.get(`/club-accounts/${clubId}`); + if (requestId !== this.loadRequestId || clubId !== this.currentClub) return; const entries = Array.isArray(response.data?.accounts) ? response.data.accounts : []; this.accounts = entries.map(normalizeAccount); this.transactions = Array.isArray(response.data?.transactions) ? response.data.transactions.map(normalizeTransaction) : []; - await this.loadPaymentClaims(); + await this.loadPaymentClaims(clubId, requestId); + if (requestId !== this.loadRequestId || clubId !== this.currentClub) return; this.applyRouteQuery(); if (this.selectedAccountId && !this.selectedAccount) { this.resetForm(); @@ -767,9 +777,10 @@ export default { this.resetForm(); } } catch (error) { + if (requestId !== this.loadRequestId || clubId !== this.currentClub) return; this.loadError = safeErrorMessage(error, 'Konten konnten nicht geladen werden.'); } finally { - this.loading = false; + if (requestId === this.loadRequestId) this.loading = false; } }, async submitAccount() { diff --git a/frontend/src/views/ClubCommunicationView.vue b/frontend/src/views/ClubCommunicationView.vue index 8b870af1..0d3fae9c 100755 --- a/frontend/src/views/ClubCommunicationView.vue +++ b/frontend/src/views/ClubCommunicationView.vue @@ -670,6 +670,7 @@ export default { data() { return { loading: false, + loadRequestId: 0, saving: false, messageSaving: false, groupSaving: false, @@ -756,6 +757,7 @@ export default { this.clearCommunicationState(); return; } + this.clearCommunicationState(); await this.loadCommunication(); }, }, @@ -803,6 +805,8 @@ export default { }, methods: { clearCommunicationState() { + this.loadRequestId += 1; + this.loading = false; this.threads = []; this.groups = []; this.members = []; @@ -925,11 +929,14 @@ export default { return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`; }, async loadCommunication() { - if (!this.currentClub) return; + const clubId = this.currentClub; + if (!clubId) return; + const requestId = ++this.loadRequestId; this.loading = true; this.loadError = ''; try { - const response = await apiClient.get(`/club-communication/${this.currentClub}`); + const response = await apiClient.get(`/club-communication/${clubId}`); + if (requestId !== this.loadRequestId || clubId !== this.currentClub) return; this.threads = Array.isArray(response.data?.threads) ? response.data.threads.map(normalizeThread) : []; this.groups = Array.isArray(response.data?.groups) ? response.data.groups.map(normalizeGroup) : []; this.members = Array.isArray(response.data?.members) ? response.data.members.map(normalizeMember) : []; @@ -955,9 +962,10 @@ export default { this.resetTemplateForm(); } } catch (error) { + if (requestId !== this.loadRequestId || clubId !== this.currentClub) return; this.loadError = safeErrorMessage(error, 'Kommunikation konnte nicht geladen werden.'); } finally { - this.loading = false; + if (requestId === this.loadRequestId) this.loading = false; } }, selectThread(thread) { diff --git a/frontend/src/views/ClubInvoicesView.vue b/frontend/src/views/ClubInvoicesView.vue index 893de628..ac7c602f 100755 --- a/frontend/src/views/ClubInvoicesView.vue +++ b/frontend/src/views/ClubInvoicesView.vue @@ -508,6 +508,7 @@ export default { data() { return { loading: false, + loadRequestId: 0, saving: false, partySaving: false, loadError: '', @@ -619,6 +620,7 @@ export default { this.clearInvoiceState(); return; } + this.clearInvoiceState(); await this.loadInvoices(); }, }, @@ -682,6 +684,8 @@ export default { }, methods: { clearInvoiceState() { + this.loadRequestId += 1; + this.loading = false; this.invoices = []; this.parties = []; this.accounts = []; @@ -751,11 +755,14 @@ export default { }).format(amount); }, async loadInvoices() { - if (!this.currentClub) return; + const clubId = this.currentClub; + if (!clubId) return; + const requestId = ++this.loadRequestId; this.loading = true; this.loadError = ''; try { - const response = await apiClient.get(`/club-invoices/${this.currentClub}`); + const response = await apiClient.get(`/club-invoices/${clubId}`); + if (requestId !== this.loadRequestId || clubId !== this.currentClub) return; this.invoices = Array.isArray(response.data?.invoices) ? response.data.invoices.map(normalizeInvoice) : []; this.parties = Array.isArray(response.data?.parties) ? response.data.parties.map(normalizeParty) : []; this.accounts = Array.isArray(response.data?.accounts) ? response.data.accounts : []; @@ -784,9 +791,10 @@ export default { } this.applyWorkflowQueryPrefill(); } catch (error) { + if (requestId !== this.loadRequestId || clubId !== this.currentClub) return; this.loadError = safeErrorMessage(error, 'Rechnungen konnten nicht geladen werden.'); } finally { - this.loading = false; + if (requestId === this.loadRequestId) this.loading = false; } }, selectInvoice(invoice) { diff --git a/frontend/src/views/ClubTasksView.vue b/frontend/src/views/ClubTasksView.vue index c92c25a6..4008a004 100755 --- a/frontend/src/views/ClubTasksView.vue +++ b/frontend/src/views/ClubTasksView.vue @@ -444,6 +444,7 @@ export default { data() { return { loading: false, + loadRequestId: 0, saving: false, materializing: false, loadError: '', @@ -516,6 +517,7 @@ export default { this.clearTaskState(); return; } + this.clearTaskState(); await this.loadTasks(); }, }, @@ -544,6 +546,8 @@ export default { }, methods: { clearTaskState() { + this.loadRequestId += 1; + this.loading = false; this.tasks = []; this.taskDefinitions = []; this.taskSuggestions = []; @@ -646,11 +650,14 @@ export default { this.form = createEmptyTaskForm(); }, async loadTasks() { - if (!this.currentClub) return; + const clubId = this.currentClub; + if (!clubId) return; + const requestId = ++this.loadRequestId; this.loading = true; this.loadError = ''; try { - const response = await apiClient.get(`/club-tasks/${this.currentClub}`); + const response = await apiClient.get(`/club-tasks/${clubId}`); + if (requestId !== this.loadRequestId || clubId !== this.currentClub) return; const entries = Array.isArray(response.data?.tasks) ? response.data.tasks : Array.isArray(response.data) @@ -675,9 +682,10 @@ export default { this.resetForm(); } } catch (error) { + if (requestId !== this.loadRequestId || clubId !== this.currentClub) return; this.loadError = safeErrorMessage(error, 'Aufgaben konnten nicht geladen werden.'); } finally { - this.loading = false; + if (requestId === this.loadRequestId) this.loading = false; } }, async submitTask() {