From 5fa34637ba62381fe0892575937fdc8d6dc2e27c Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Wed, 15 Apr 2026 22:15:04 +0200 Subject: [PATCH] feat(ClubSettings): add member data quality requirements configuration - Introduced new settings for member data quality requirements in club settings, allowing configuration of required fields such as street, postal code, city, phone, and email. - Updated the backend to handle the new memberDataQualityRequirements field in club settings. - Enhanced the frontend to display and manage these requirements in the ClubSettings view, improving user experience and data integrity. - Added localization support for new terms related to member data quality across multiple languages. --- .codex | 0 backend/controllers/clubsController.js | 11 ++- ...ber_data_quality_requirements_to_clubs.sql | 6 ++ backend/models/Club.js | 6 ++ backend/services/clubService.js | 27 ++++- frontend/src/i18n/locales/de-CH.json | 3 + frontend/src/i18n/locales/de.json | 12 ++- frontend/src/i18n/locales/en-AU.json | 3 + frontend/src/i18n/locales/en-GB.json | 3 + frontend/src/i18n/locales/en-US.json | 3 + frontend/src/i18n/locales/es.json | 3 + frontend/src/i18n/locales/fil.json | 3 + frontend/src/i18n/locales/fr.json | 3 + frontend/src/i18n/locales/it.json | 3 + frontend/src/i18n/locales/ja.json | 3 + frontend/src/i18n/locales/pl.json | 3 + frontend/src/i18n/locales/th.json | 3 + frontend/src/i18n/locales/tl.json | 3 + frontend/src/i18n/locales/zh.json | 3 + frontend/src/views/ClubSettings.vue | 62 +++++++++++- frontend/src/views/MembersView.vue | 98 +++++++++++++++---- 21 files changed, 237 insertions(+), 24 deletions(-) create mode 100644 .codex create mode 100644 backend/migrations/20260415_add_member_data_quality_requirements_to_clubs.sql diff --git a/.codex b/.codex new file mode 100644 index 00000000..e69de29b diff --git a/backend/controllers/clubsController.js b/backend/controllers/clubsController.js index 7fc7e5c3..8b721fd4 100644 --- a/backend/controllers/clubsController.js +++ b/backend/controllers/clubsController.js @@ -60,12 +60,19 @@ export const updateClubSettings = async (req, res) => { try { const { authcode: token } = req.headers; const { clubid } = req.params; - const { greetingText, associationMemberNumber, myTischtennisFedNickname, autoFetchRankings } = req.body; + const { + greetingText, + associationMemberNumber, + myTischtennisFedNickname, + autoFetchRankings, + memberDataQualityRequirements + } = req.body; const updated = await ClubService.updateClubSettings(token, clubid, { greetingText, associationMemberNumber, myTischtennisFedNickname, - autoFetchRankings + autoFetchRankings, + memberDataQualityRequirements }); res.status(200).json(updated); } catch (error) { diff --git a/backend/migrations/20260415_add_member_data_quality_requirements_to_clubs.sql b/backend/migrations/20260415_add_member_data_quality_requirements_to_clubs.sql new file mode 100644 index 00000000..03fdfadd --- /dev/null +++ b/backend/migrations/20260415_add_member_data_quality_requirements_to_clubs.sql @@ -0,0 +1,6 @@ +-- Migration: Add per-club member data quality requirements. +-- Controls which optional contact/address fields count as required on /members. + +ALTER TABLE clubs + ADD COLUMN IF NOT EXISTS member_data_quality_requirements JSON NULL + COMMENT 'Configures which member fields are required for data quality checks'; diff --git a/backend/models/Club.js b/backend/models/Club.js index 1fe14861..ad9654a8 100644 --- a/backend/models/Club.js +++ b/backend/models/Club.js @@ -29,6 +29,12 @@ const Club = sequelize.define('Club', { defaultValue: false, field: 'auto_fetch_rankings', comment: 'Enable automatic TTR/QTTR rankings fetch for this club' + }, + memberDataQualityRequirements: { + type: DataTypes.JSON, + allowNull: true, + field: 'member_data_quality_requirements', + comment: 'Configures which member fields are required for data quality checks' } }, { tableName: 'clubs', diff --git a/backend/services/clubService.js b/backend/services/clubService.js index af315b45..a5239df0 100644 --- a/backend/services/clubService.js +++ b/backend/services/clubService.js @@ -71,7 +71,8 @@ class ClubService { greetingText, associationMemberNumber, myTischtennisFedNickname, - autoFetchRankings + autoFetchRankings, + memberDataQualityRequirements }) { await checkAccess(userToken, clubId); const club = await Club.findByPk(clubId); @@ -81,9 +82,33 @@ class ClubService { const updates = { greetingText, associationMemberNumber }; if (myTischtennisFedNickname !== undefined) updates.myTischtennisFedNickname = myTischtennisFedNickname || null; if (autoFetchRankings !== undefined) updates.autoFetchRankings = !!autoFetchRankings; + if (memberDataQualityRequirements !== undefined) { + updates.memberDataQualityRequirements = this.normalizeMemberDataQualityRequirements(memberDataQualityRequirements); + } return await club.update(updates); } + normalizeMemberDataQualityRequirements(settings) { + const defaults = { + requireStreet: true, + requirePostalCode: true, + requireCity: true, + requirePhone: true, + requireEmail: true + }; + + if (!settings || typeof settings !== 'object' || Array.isArray(settings)) { + return defaults; + } + + return Object.fromEntries( + Object.entries(defaults).map(([key, defaultValue]) => [ + key, + typeof settings[key] === 'boolean' ? settings[key] : defaultValue + ]) + ); + } + async approveUserClubAccess(userToken, clubId, toApproveUserId) { await checkAccess(userToken, clubId); const toApproveUserClub = await UserClub.findOne({ diff --git a/frontend/src/i18n/locales/de-CH.json b/frontend/src/i18n/locales/de-CH.json index af04507e..b090e9a1 100644 --- a/frontend/src/i18n/locales/de-CH.json +++ b/frontend/src/i18n/locales/de-CH.json @@ -644,6 +644,9 @@ "dataIssuePhone": "Telefon fehlt", "dataIssueEmail": "E-Mail fehlt", "dataIssueAddress": "Adresse fehlt", + "dataIssueStreet": "Strasse fehlt", + "dataIssuePostalCode": "PLZ fehlt", + "dataIssueCity": "Ort fehlt", "dataIssueGender": "Geschlecht ungeklärt", "dataIssueTrainingGroup": "Trainingsgruppe fehlt", "openTasks": "Offene Aufgaben", diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 19d1f5e6..dc950bd7 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -392,6 +392,9 @@ "dataIssuePhone": "Telefon fehlt", "dataIssueEmail": "E-Mail fehlt", "dataIssueAddress": "Adresse fehlt", + "dataIssueStreet": "Straße fehlt", + "dataIssuePostalCode": "PLZ fehlt", + "dataIssueCity": "Ort fehlt", "dataIssueGender": "Geschlecht ungeklärt", "dataIssueTrainingGroup": "Trainingsgruppe fehlt", "openTasks": "Offene Aufgaben", @@ -1201,7 +1204,14 @@ "autoFetchRankings": "Ranglisten automatisch abrufen", "myTischtennisFedNickname": "Verbandskürzel", "myTischtennisFedNicknamePlaceholder": "z. B. HeTTV", - "rankingsUsesAssociationNumber": "Die Vereinsnummer für den Ranglisten-Abruf entspricht der Verbands-Mitgliedsnummer oben." + "rankingsUsesAssociationNumber": "Die Vereinsnummer für den Ranglisten-Abruf entspricht der Verbands-Mitgliedsnummer oben.", + "memberDataQuality": "Datenqualität Mitglieder", + "memberDataQualityHint": "Diese Felder zählen auf der Mitgliederseite als nötig. Alle Felder bleiben weiterhin eingebbar.", + "requireStreet": "Straße nötig", + "requirePostalCode": "PLZ nötig", + "requireCity": "Ort nötig", + "requirePhone": "Telefonnummer nötig", + "requireEmail": "E-Mail-Adresse nötig" }, "predefinedActivities": { "title": "Vordefinierte Aktivitäten", diff --git a/frontend/src/i18n/locales/en-AU.json b/frontend/src/i18n/locales/en-AU.json index ffef64e6..e36d172b 100644 --- a/frontend/src/i18n/locales/en-AU.json +++ b/frontend/src/i18n/locales/en-AU.json @@ -641,6 +641,9 @@ "dataIssuePhone": "Phone missing", "dataIssueEmail": "Email missing", "dataIssueAddress": "Address missing", + "dataIssueStreet": "Street missing", + "dataIssuePostalCode": "Postcode missing", + "dataIssueCity": "Suburb/city missing", "dataIssueGender": "Gender unclear", "dataIssueTrainingGroup": "Training group missing", "openTasks": "Open tasks", diff --git a/frontend/src/i18n/locales/en-GB.json b/frontend/src/i18n/locales/en-GB.json index 0ab43dc9..6be339c7 100644 --- a/frontend/src/i18n/locales/en-GB.json +++ b/frontend/src/i18n/locales/en-GB.json @@ -916,6 +916,9 @@ "dataIssuePhone": "Phone missing", "dataIssueEmail": "Email missing", "dataIssueAddress": "Address missing", + "dataIssueStreet": "Street missing", + "dataIssuePostalCode": "Postcode missing", + "dataIssueCity": "Town/city missing", "dataIssueGender": "Gender unclear", "dataIssueTrainingGroup": "Training group missing", "openTasks": "Open tasks", diff --git a/frontend/src/i18n/locales/en-US.json b/frontend/src/i18n/locales/en-US.json index a4e27c2f..3a1bec0d 100644 --- a/frontend/src/i18n/locales/en-US.json +++ b/frontend/src/i18n/locales/en-US.json @@ -641,6 +641,9 @@ "dataIssuePhone": "Phone missing", "dataIssueEmail": "Email missing", "dataIssueAddress": "Address missing", + "dataIssueStreet": "Street missing", + "dataIssuePostalCode": "Postal code missing", + "dataIssueCity": "City missing", "dataIssueGender": "Gender unclear", "dataIssueTrainingGroup": "Training group missing", "openTasks": "Open tasks", diff --git a/frontend/src/i18n/locales/es.json b/frontend/src/i18n/locales/es.json index 76a304cf..894d6ac7 100644 --- a/frontend/src/i18n/locales/es.json +++ b/frontend/src/i18n/locales/es.json @@ -604,6 +604,9 @@ "dataIssuePhone": "Falta el teléfono", "dataIssueEmail": "Falta el correo", "dataIssueAddress": "Falta la dirección", + "dataIssueStreet": "Falta la calle", + "dataIssuePostalCode": "Falta el código postal", + "dataIssueCity": "Falta la ciudad", "dataIssueGender": "Sexo no definido", "dataIssueTrainingGroup": "Falta el grupo de entrenamiento", "openTasks": "Tareas abiertas", diff --git a/frontend/src/i18n/locales/fil.json b/frontend/src/i18n/locales/fil.json index 91fe076f..1a035e3e 100644 --- a/frontend/src/i18n/locales/fil.json +++ b/frontend/src/i18n/locales/fil.json @@ -604,6 +604,9 @@ "dataIssuePhone": "Kulang ang telepono", "dataIssueEmail": "Kulang ang email", "dataIssueAddress": "Kulang ang address", + "dataIssueStreet": "Kulang ang kalye", + "dataIssuePostalCode": "Kulang ang postal code", + "dataIssueCity": "Kulang ang lungsod", "dataIssueGender": "Hindi malinaw ang kasarian", "dataIssueTrainingGroup": "Kulang ang grupo ng pagsasanay", "openTasks": "Open tasks", diff --git a/frontend/src/i18n/locales/fr.json b/frontend/src/i18n/locales/fr.json index 5b8c81b4..12a4f370 100644 --- a/frontend/src/i18n/locales/fr.json +++ b/frontend/src/i18n/locales/fr.json @@ -604,6 +604,9 @@ "dataIssuePhone": "Téléphone manquant", "dataIssueEmail": "E-mail manquant", "dataIssueAddress": "Adresse manquante", + "dataIssueStreet": "Rue manquante", + "dataIssuePostalCode": "Code postal manquant", + "dataIssueCity": "Ville manquante", "dataIssueGender": "Sexe non défini", "dataIssueTrainingGroup": "Groupe d'entraînement manquant", "openTasks": "Tâches ouvertes", diff --git a/frontend/src/i18n/locales/it.json b/frontend/src/i18n/locales/it.json index 6f0afa1b..dfe54861 100644 --- a/frontend/src/i18n/locales/it.json +++ b/frontend/src/i18n/locales/it.json @@ -604,6 +604,9 @@ "dataIssuePhone": "Telefono mancante", "dataIssueEmail": "E-mail mancante", "dataIssueAddress": "Indirizzo mancante", + "dataIssueStreet": "Via mancante", + "dataIssuePostalCode": "CAP mancante", + "dataIssueCity": "Città mancante", "dataIssueGender": "Genere non definito", "dataIssueTrainingGroup": "Gruppo di allenamento mancante", "openTasks": "Attività aperte", diff --git a/frontend/src/i18n/locales/ja.json b/frontend/src/i18n/locales/ja.json index e7eb60e9..c19c8406 100644 --- a/frontend/src/i18n/locales/ja.json +++ b/frontend/src/i18n/locales/ja.json @@ -604,6 +604,9 @@ "dataIssuePhone": "電話番号がありません", "dataIssueEmail": "メールアドレスがありません", "dataIssueAddress": "住所がありません", + "dataIssueStreet": "番地がありません", + "dataIssuePostalCode": "郵便番号がありません", + "dataIssueCity": "市区町村がありません", "dataIssueGender": "性別が未設定です", "dataIssueTrainingGroup": "練習グループがありません", "openTasks": "Open tasks", diff --git a/frontend/src/i18n/locales/pl.json b/frontend/src/i18n/locales/pl.json index c62d3368..cb564932 100644 --- a/frontend/src/i18n/locales/pl.json +++ b/frontend/src/i18n/locales/pl.json @@ -604,6 +604,9 @@ "dataIssuePhone": "Brak telefonu", "dataIssueEmail": "Brak e-maila", "dataIssueAddress": "Brak adresu", + "dataIssueStreet": "Brak ulicy", + "dataIssuePostalCode": "Brak kodu pocztowego", + "dataIssueCity": "Brak miasta", "dataIssueGender": "Płeć nieokreślona", "dataIssueTrainingGroup": "Brak grupy treningowej", "openTasks": "Otwarte zadania", diff --git a/frontend/src/i18n/locales/th.json b/frontend/src/i18n/locales/th.json index 56d5f42f..81ca3fe2 100644 --- a/frontend/src/i18n/locales/th.json +++ b/frontend/src/i18n/locales/th.json @@ -604,6 +604,9 @@ "dataIssuePhone": "ไม่มีหมายเลขโทรศัพท์", "dataIssueEmail": "ไม่มีอีเมล", "dataIssueAddress": "ไม่มีที่อยู่", + "dataIssueStreet": "ไม่มีถนน", + "dataIssuePostalCode": "ไม่มีรหัสไปรษณีย์", + "dataIssueCity": "ไม่มีเมือง", "dataIssueGender": "เพศยังไม่ชัดเจน", "dataIssueTrainingGroup": "ไม่มีกลุ่มฝึกซ้อม", "openTasks": "Open tasks", diff --git a/frontend/src/i18n/locales/tl.json b/frontend/src/i18n/locales/tl.json index 51a9f6a6..04e6fbaf 100644 --- a/frontend/src/i18n/locales/tl.json +++ b/frontend/src/i18n/locales/tl.json @@ -604,6 +604,9 @@ "dataIssuePhone": "Kulang ang telepono", "dataIssueEmail": "Kulang ang email", "dataIssueAddress": "Kulang ang address", + "dataIssueStreet": "Kulang ang kalye", + "dataIssuePostalCode": "Kulang ang postal code", + "dataIssueCity": "Kulang ang lungsod", "dataIssueGender": "Hindi malinaw ang kasarian", "dataIssueTrainingGroup": "Kulang ang grupo ng pagsasanay", "openTasks": "Open tasks", diff --git a/frontend/src/i18n/locales/zh.json b/frontend/src/i18n/locales/zh.json index 242cfda1..d724c7a4 100644 --- a/frontend/src/i18n/locales/zh.json +++ b/frontend/src/i18n/locales/zh.json @@ -604,6 +604,9 @@ "dataIssuePhone": "缺少电话号码", "dataIssueEmail": "缺少电子邮箱地址", "dataIssueAddress": "缺少地址", + "dataIssueStreet": "缺少街道", + "dataIssuePostalCode": "缺少邮政编码", + "dataIssueCity": "缺少城市", "dataIssueGender": "性别未明确", "dataIssueTrainingGroup": "缺少训练组", "openTasks": "Open tasks", diff --git a/frontend/src/views/ClubSettings.vue b/frontend/src/views/ClubSettings.vue index 97429a0c..79c7424d 100644 --- a/frontend/src/views/ClubSettings.vue +++ b/frontend/src/views/ClubSettings.vue @@ -69,6 +69,33 @@

{{ $t('clubSettings.rankingsUsesAssociationNumber') }}

+
+

{{ $t('clubSettings.memberDataQuality') }}

+

{{ $t('clubSettings.memberDataQualityHint') }}

+
+ + + + + +
+
+
@@ -98,6 +125,14 @@ import apiClient from '../apiClient'; import TrainingGroupsTab from '../components/TrainingGroupsTab.vue'; import TrainingTimesTab from '../components/TrainingTimesTab.vue'; +const defaultMemberDataQualityRequirements = () => ({ + requireStreet: true, + requirePostalCode: true, + requireCity: true, + requirePhone: true, + requireEmail: true, +}); + export default { name: 'ClubSettings', components: { @@ -111,6 +146,7 @@ export default { associationMemberNumber: '', myTischtennisFedNickname: '', autoFetchRankings: false, + memberDataQualityRequirements: defaultMemberDataQualityRequirements(), saved: false, loading: false, loadError: null, @@ -134,6 +170,7 @@ export default { this.associationMemberNumber = ''; this.myTischtennisFedNickname = ''; this.autoFetchRankings = false; + this.memberDataQualityRequirements = defaultMemberDataQualityRequirements(); this.loadError = null; return; } @@ -146,16 +183,38 @@ export default { this.associationMemberNumber = club?.associationMemberNumber ?? ''; this.myTischtennisFedNickname = club?.myTischtennisFedNickname ?? ''; this.autoFetchRankings = !!club?.autoFetchRankings; + this.memberDataQualityRequirements = this.normalizeMemberDataQualityRequirements(club?.memberDataQualityRequirements); } catch (e) { this.loadError = this.$t('clubSettings.loadFailed'); this.greeting = ''; this.associationMemberNumber = ''; this.myTischtennisFedNickname = ''; this.autoFetchRankings = false; + this.memberDataQualityRequirements = defaultMemberDataQualityRequirements(); } finally { this.loading = false; } }, + normalizeMemberDataQualityRequirements(settings) { + const defaults = defaultMemberDataQualityRequirements(); + const parsed = typeof settings === 'string' ? this.parseJsonSetting(settings) : settings; + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + return defaults; + } + return Object.fromEntries( + Object.keys(defaults).map(key => [ + key, + typeof parsed[key] === 'boolean' ? parsed[key] : defaults[key], + ]) + ); + }, + parseJsonSetting(value) { + try { + return JSON.parse(value); + } catch (e) { + return null; + } + }, async save() { if (!this.currentClub) { alert(this.$t('clubSettings.noClubSelected')); @@ -167,6 +226,7 @@ export default { associationMemberNumber: this.associationMemberNumber, myTischtennisFedNickname: this.myTischtennisFedNickname || null, autoFetchRankings: this.autoFetchRankings, + memberDataQualityRequirements: this.normalizeMemberDataQualityRequirements(this.memberDataQualityRequirements), }); this.saved = true; setTimeout(() => (this.saved = false), 1500); @@ -204,6 +264,7 @@ export default { .text-input { width: 100%; border: 1px solid #ddd; border-radius: 6px; padding: 8px; font-size: 14px; } .rankings-row { margin-bottom: 12px; } .rankings-fields { margin-top: 12px; } +.quality-options { display: grid; gap: 10px; margin-top: 12px; } .field-group label { display: block; margin-bottom: 4px; font-weight: 500; color: #333; } .checkbox-label { display: flex; align-items: center; gap: 8px; cursor: pointer; } .checkbox-label input[type="checkbox"] { width: auto; } @@ -246,4 +307,3 @@ export default { } - diff --git a/frontend/src/views/MembersView.vue b/frontend/src/views/MembersView.vue index b6de2d10..109035ec 100644 --- a/frontend/src/views/MembersView.vue +++ b/frontend/src/views/MembersView.vue @@ -195,20 +195,20 @@
-