feat(ClubSettings): add member data quality requirements configuration
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 38s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 38s
- 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.
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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';
|
||||
@@ -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',
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -604,6 +604,9 @@
|
||||
"dataIssuePhone": "電話番号がありません",
|
||||
"dataIssueEmail": "メールアドレスがありません",
|
||||
"dataIssueAddress": "住所がありません",
|
||||
"dataIssueStreet": "番地がありません",
|
||||
"dataIssuePostalCode": "郵便番号がありません",
|
||||
"dataIssueCity": "市区町村がありません",
|
||||
"dataIssueGender": "性別が未設定です",
|
||||
"dataIssueTrainingGroup": "練習グループがありません",
|
||||
"openTasks": "Open tasks",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -604,6 +604,9 @@
|
||||
"dataIssuePhone": "ไม่มีหมายเลขโทรศัพท์",
|
||||
"dataIssueEmail": "ไม่มีอีเมล",
|
||||
"dataIssueAddress": "ไม่มีที่อยู่",
|
||||
"dataIssueStreet": "ไม่มีถนน",
|
||||
"dataIssuePostalCode": "ไม่มีรหัสไปรษณีย์",
|
||||
"dataIssueCity": "ไม่มีเมือง",
|
||||
"dataIssueGender": "เพศยังไม่ชัดเจน",
|
||||
"dataIssueTrainingGroup": "ไม่มีกลุ่มฝึกซ้อม",
|
||||
"openTasks": "Open tasks",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -604,6 +604,9 @@
|
||||
"dataIssuePhone": "缺少电话号码",
|
||||
"dataIssueEmail": "缺少电子邮箱地址",
|
||||
"dataIssueAddress": "缺少地址",
|
||||
"dataIssueStreet": "缺少街道",
|
||||
"dataIssuePostalCode": "缺少邮政编码",
|
||||
"dataIssueCity": "缺少城市",
|
||||
"dataIssueGender": "性别未明确",
|
||||
"dataIssueTrainingGroup": "缺少训练组",
|
||||
"openTasks": "Open tasks",
|
||||
|
||||
@@ -69,6 +69,33 @@
|
||||
<p v-if="autoFetchRankings" class="hint">{{ $t('clubSettings.rankingsUsesAssociationNumber') }}</p>
|
||||
</section>
|
||||
|
||||
<section v-if="currentClub && !loading" class="card">
|
||||
<h2>{{ $t('clubSettings.memberDataQuality') }}</h2>
|
||||
<p class="hint">{{ $t('clubSettings.memberDataQualityHint') }}</p>
|
||||
<div class="quality-options">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" v-model="memberDataQualityRequirements.requireStreet" />
|
||||
{{ $t('clubSettings.requireStreet') }}
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" v-model="memberDataQualityRequirements.requirePostalCode" />
|
||||
{{ $t('clubSettings.requirePostalCode') }}
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" v-model="memberDataQualityRequirements.requireCity" />
|
||||
{{ $t('clubSettings.requireCity') }}
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" v-model="memberDataQualityRequirements.requirePhone" />
|
||||
{{ $t('clubSettings.requirePhone') }}
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" v-model="memberDataQualityRequirements.requireEmail" />
|
||||
{{ $t('clubSettings.requireEmail') }}
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="currentClub && !loading" class="card actions-card">
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary" @click="save">{{ $t('clubSettings.save') }}</button>
|
||||
@@ -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 {
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -195,20 +195,20 @@
|
||||
<div class="new-member-form">
|
||||
<label><span>{{ $t('members.firstName') }}:</span> <input type="text" v-model="newFirstname"></label>
|
||||
<label><span>{{ $t('members.lastName') }}:</span> <input type="text" v-model="newLastname"></label>
|
||||
<label :class="{ 'member-field-warning': editorHasIssue('address') }">
|
||||
<label :class="{ 'member-field-warning': editorHasIssue('street') }">
|
||||
<span>{{ $t('members.street') }}:</span>
|
||||
<input type="text" v-model="newStreet" :class="{ 'input-warning': editorHasIssue('address') }">
|
||||
<small v-if="editorHasIssue('address')" class="member-editor-field-hint">{{ $t('members.editorRecommendedEntry') }}</small>
|
||||
<input type="text" v-model="newStreet" :class="{ 'input-warning': editorHasIssue('street') }">
|
||||
<small v-if="editorHasIssue('street')" class="member-editor-field-hint">{{ $t('members.editorRecommendedEntry') }}</small>
|
||||
</label>
|
||||
<label :class="{ 'member-field-warning': editorHasIssue('address') }">
|
||||
<label :class="{ 'member-field-warning': editorHasIssue('postal-code') }">
|
||||
<span>{{ $t('members.postalCode') }}:</span>
|
||||
<input type="text" v-model="newPostalCode" maxlength="10" :class="{ 'input-warning': editorHasIssue('address') }">
|
||||
<small v-if="editorHasIssue('address')" class="member-editor-field-hint">{{ $t('members.editorRecommendedEntry') }}</small>
|
||||
<input type="text" v-model="newPostalCode" maxlength="10" :class="{ 'input-warning': editorHasIssue('postal-code') }">
|
||||
<small v-if="editorHasIssue('postal-code')" class="member-editor-field-hint">{{ $t('members.editorRecommendedEntry') }}</small>
|
||||
</label>
|
||||
<label :class="{ 'member-field-warning': editorHasIssue('address') }">
|
||||
<label :class="{ 'member-field-warning': editorHasIssue('city') }">
|
||||
<span>{{ $t('members.city') }}:</span>
|
||||
<input type="text" v-model="newCity" :class="{ 'input-warning': editorHasIssue('address') }">
|
||||
<small v-if="editorHasIssue('address')" class="member-editor-field-hint">{{ $t('members.editorRecommendedEntry') }}</small>
|
||||
<input type="text" v-model="newCity" :class="{ 'input-warning': editorHasIssue('city') }">
|
||||
<small v-if="editorHasIssue('city')" class="member-editor-field-hint">{{ $t('members.editorRecommendedEntry') }}</small>
|
||||
</label>
|
||||
<label :class="{ 'member-field-warning': editorHasIssue('birthdate') }">
|
||||
<span>{{ $t('members.birthdate') }}:</span>
|
||||
@@ -612,6 +612,15 @@ import MemberOrdersDialog from '../components/MemberOrdersDialog.vue';
|
||||
import MembersOverviewSection from '../components/members/MembersOverviewSection.vue';
|
||||
import { debounce } from '../utils/debounce.js';
|
||||
import { buildInfoConfig, buildConfirmConfig, safeErrorMessage, sanitizeText } from '../utils/dialogUtils.js';
|
||||
|
||||
const defaultMemberDataQualityRequirements = () => ({
|
||||
requireStreet: true,
|
||||
requirePostalCode: true,
|
||||
requireCity: true,
|
||||
requirePhone: true,
|
||||
requireEmail: true,
|
||||
});
|
||||
|
||||
export default {
|
||||
name: 'MembersView',
|
||||
components: {
|
||||
@@ -844,18 +853,25 @@ export default {
|
||||
|
||||
editorDataQualityIssues() {
|
||||
const issues = [];
|
||||
const requirements = this.memberDataQualityRequirements;
|
||||
|
||||
if (!this.newBirthdate) {
|
||||
issues.push({ key: 'birthdate', label: this.$t('members.dataIssueBirthdate') });
|
||||
}
|
||||
if (!this.hasEditorPhone) {
|
||||
if (requirements.requirePhone && !this.hasEditorPhone) {
|
||||
issues.push({ key: 'phone', label: this.$t('members.dataIssuePhone') });
|
||||
}
|
||||
if (!this.hasEditorEmail) {
|
||||
if (requirements.requireEmail && !this.hasEditorEmail) {
|
||||
issues.push({ key: 'email', label: this.$t('members.dataIssueEmail') });
|
||||
}
|
||||
if (!this.hasEditorAddress) {
|
||||
issues.push({ key: 'address', label: this.$t('members.dataIssueAddress') });
|
||||
if (requirements.requireStreet && !this.hasTextValue(this.newStreet)) {
|
||||
issues.push({ key: 'street', label: this.$t('members.dataIssueStreet') });
|
||||
}
|
||||
if (requirements.requirePostalCode && !this.hasTextValue(this.newPostalCode)) {
|
||||
issues.push({ key: 'postal-code', label: this.$t('members.dataIssuePostalCode') });
|
||||
}
|
||||
if (requirements.requireCity && !this.hasTextValue(this.newCity)) {
|
||||
issues.push({ key: 'city', label: this.$t('members.dataIssueCity') });
|
||||
}
|
||||
if (!this.newGender || this.newGender === 'unknown') {
|
||||
issues.push({ key: 'gender', label: this.$t('members.dataIssueGender') });
|
||||
@@ -1011,7 +1027,8 @@ export default {
|
||||
isLoadingMembers: false,
|
||||
membersLoadError: '',
|
||||
selectedMemberPreview: null,
|
||||
selectedPreviewTrainingGroups: []
|
||||
selectedPreviewTrainingGroups: [],
|
||||
memberDataQualityRequirements: defaultMemberDataQualityRequirements()
|
||||
}
|
||||
},
|
||||
created() {
|
||||
@@ -1022,6 +1039,7 @@ export default {
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadTrainingGroups();
|
||||
await this.loadMemberDataQualityRequirements();
|
||||
await this.init();
|
||||
},
|
||||
methods: {
|
||||
@@ -1061,6 +1079,42 @@ export default {
|
||||
async init() {
|
||||
await this.loadMembers();
|
||||
},
|
||||
async loadMemberDataQualityRequirements() {
|
||||
if (!this.currentClub) {
|
||||
this.memberDataQualityRequirements = defaultMemberDataQualityRequirements();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await apiClient.get(`/clubs/${this.currentClub}`);
|
||||
this.memberDataQualityRequirements = this.normalizeMemberDataQualityRequirements(response.data?.memberDataQualityRequirements);
|
||||
} catch (error) {
|
||||
console.error('[loadMemberDataQualityRequirements] error:', error);
|
||||
this.memberDataQualityRequirements = defaultMemberDataQualityRequirements();
|
||||
}
|
||||
},
|
||||
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 (error) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
hasTextValue(value) {
|
||||
return value !== null && value !== undefined && String(value).trim() !== '';
|
||||
},
|
||||
async loadMembers() {
|
||||
this.isLoadingMembers = true;
|
||||
this.membersLoadError = '';
|
||||
@@ -2281,7 +2335,7 @@ export default {
|
||||
if (!member) return [];
|
||||
|
||||
const issues = [];
|
||||
const compactAddress = this.getCompactAddress(member);
|
||||
const requirements = this.memberDataQualityRequirements;
|
||||
const hasPhone = this.getFormattedPhoneNumbers(member) !== '–';
|
||||
const hasEmail = this.getFormattedEmails(member) !== '–';
|
||||
const hasTrainingGroup = this.getMemberTrainingGroups(member).length > 0;
|
||||
@@ -2289,14 +2343,20 @@ export default {
|
||||
if (!member.birthDate) {
|
||||
issues.push({ key: 'birthdate', label: this.$t('members.dataIssueBirthdate') });
|
||||
}
|
||||
if (!hasPhone) {
|
||||
if (requirements.requirePhone && !hasPhone) {
|
||||
issues.push({ key: 'phone', label: this.$t('members.dataIssuePhone') });
|
||||
}
|
||||
if (!hasEmail) {
|
||||
if (requirements.requireEmail && !hasEmail) {
|
||||
issues.push({ key: 'email', label: this.$t('members.dataIssueEmail') });
|
||||
}
|
||||
if (!compactAddress) {
|
||||
issues.push({ key: 'address', label: this.$t('members.dataIssueAddress') });
|
||||
if (requirements.requireStreet && !this.hasTextValue(member.street)) {
|
||||
issues.push({ key: 'street', label: this.$t('members.dataIssueStreet') });
|
||||
}
|
||||
if (requirements.requirePostalCode && !this.hasTextValue(member.postalCode)) {
|
||||
issues.push({ key: 'postal-code', label: this.$t('members.dataIssuePostalCode') });
|
||||
}
|
||||
if (requirements.requireCity && !this.hasTextValue(member.city)) {
|
||||
issues.push({ key: 'city', label: this.$t('members.dataIssueCity') });
|
||||
}
|
||||
if (!member.gender || member.gender === 'unknown') {
|
||||
issues.push({ key: 'gender', label: this.$t('members.dataIssueGender') });
|
||||
|
||||
Reference in New Issue
Block a user