Enhance member management by adding postal code and contact handling
Introduced a postal code field to the member model and implemented a new MemberContact model to manage multiple phone numbers and email addresses. Updated the member service and controller to handle contact data during member creation and updates. Enhanced the MembersView component to support input for multiple contacts, ensuring better organization and accessibility of member information.
This commit is contained in:
@@ -203,7 +203,40 @@ class PDFGenerator {
|
||||
addPhoneListRow(member) {
|
||||
const fullName = `${member.lastName}, ${member.firstName}`;
|
||||
const birthDate = member.birthDate ? new Date(member.birthDate).toLocaleDateString('de-DE') : '';
|
||||
const phoneNumber = member.phone || '';
|
||||
|
||||
// Sammle alle Telefonnummern aus contacts
|
||||
let phoneNumbers = [];
|
||||
if (member.contacts && Array.isArray(member.contacts)) {
|
||||
const phoneContacts = member.contacts
|
||||
.filter(c => c.type === 'phone' && c.value && String(c.value).trim() !== '')
|
||||
.sort((a, b) => {
|
||||
// Primäre Telefonnummer zuerst
|
||||
if (a.isPrimary && !b.isPrimary) return -1;
|
||||
if (!a.isPrimary && b.isPrimary) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
phoneContacts.forEach(contact => {
|
||||
let phoneText = contact.value;
|
||||
if (contact.isParent) {
|
||||
// Bei Elternteil-Nummern: Name des Elternteils + Name des Mitglieds
|
||||
if (contact.parentName) {
|
||||
phoneText += ` (${contact.parentName} von ${member.firstName} ${member.lastName})`;
|
||||
} else {
|
||||
phoneText += ` (Elternteil von ${member.firstName} ${member.lastName})`;
|
||||
}
|
||||
}
|
||||
// Bei eigenen Nummern wird nur die Nummer angezeigt (Name steht bereits in erster Spalte)
|
||||
phoneNumbers.push(phoneText);
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback auf altes phone-Feld für Rückwärtskompatibilität
|
||||
if (phoneNumbers.length === 0 && member.phone) {
|
||||
phoneNumbers.push(member.phone);
|
||||
}
|
||||
|
||||
const phoneNumber = phoneNumbers.join(', ') || '';
|
||||
|
||||
this.pdf.text(fullName, this.margin, this.yPos);
|
||||
this.pdf.text(birthDate, this.margin + 60, this.yPos);
|
||||
|
||||
@@ -44,10 +44,43 @@
|
||||
<label><span>Vorname:</span> <input type="text" v-model="newFirstname"></label>
|
||||
<label><span>Nachname:</span> <input type="text" v-model="newLastname"></label>
|
||||
<label><span>Straße:</span> <input type="text" v-model="newStreet"></label>
|
||||
<label><span>PLZ:</span> <input type="text" v-model="newPostalCode" maxlength="10"></label>
|
||||
<label><span>Ort:</span> <input type="text" v-model="newCity"></label>
|
||||
<label><span>Geburtsdatum:</span> <input type="date" v-model="newBirthdate"></label>
|
||||
<label><span>Telefon-Nr.:</span> <input type="text" v-model="newPhone"></label>
|
||||
<label><span>Email-Adresse:</span> <input type="email" v-model="newEmail"></label>
|
||||
|
||||
<!-- Telefonnummern -->
|
||||
<div class="contact-section">
|
||||
<label><span>Telefonnummern:</span></label>
|
||||
<div v-for="(phone, index) in memberContacts.phones" :key="'phone-' + index" class="contact-item">
|
||||
<input type="text" v-model="phone.value" placeholder="Telefonnummer" class="contact-input">
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" v-model="phone.isParent"> Elternteil
|
||||
</label>
|
||||
<input v-if="phone.isParent" type="text" v-model="phone.parentName" placeholder="Name (z.B. Mutter, Vater)" class="parent-name-input">
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" v-model="phone.isPrimary"> Primär
|
||||
</label>
|
||||
<button type="button" @click="removeContact('phone', index)" class="btn-remove-contact">✕</button>
|
||||
</div>
|
||||
<button type="button" @click="addContact('phone')" class="btn-add-contact">+ Telefonnummer hinzufügen</button>
|
||||
</div>
|
||||
|
||||
<!-- E-Mail-Adressen -->
|
||||
<div class="contact-section">
|
||||
<label><span>E-Mail-Adressen:</span></label>
|
||||
<div v-for="(email, index) in memberContacts.emails" :key="'email-' + index" class="contact-item">
|
||||
<input type="email" v-model="email.value" placeholder="E-Mail-Adresse" class="contact-input">
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" v-model="email.isParent"> Elternteil
|
||||
</label>
|
||||
<input v-if="email.isParent" type="text" v-model="email.parentName" placeholder="Name (z.B. Mutter, Vater)" class="parent-name-input">
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" v-model="email.isPrimary"> Primär
|
||||
</label>
|
||||
<button type="button" @click="removeContact('email', index)" class="btn-remove-contact">✕</button>
|
||||
</div>
|
||||
<button type="button" @click="addContact('email')" class="btn-add-contact">+ E-Mail-Adresse hinzufügen</button>
|
||||
</div>
|
||||
<label><span>Geschlecht:</span>
|
||||
<select v-model="newGender">
|
||||
<option value="unknown">Unbekannt</option>
|
||||
@@ -151,11 +184,11 @@
|
||||
</span>
|
||||
<span v-else class="no-rating">-</span>
|
||||
</td>
|
||||
<td>{{ member.street }}, {{ member.city }}</td>
|
||||
<td>{{ member.street }}{{ member.postalCode ? ', ' + member.postalCode : '' }}, {{ member.city }}</td>
|
||||
<td>{{ member.memberFormHandedOver ? '✓' : '' }}</td>
|
||||
<td>{{ getFormattedBirthdate(member.birthDate) }}</td>
|
||||
<td>{{ member.phone }}</td>
|
||||
<td>{{ member.email }}</td>
|
||||
<td>{{ getFormattedPhoneNumbers(member) }}</td>
|
||||
<td>{{ getFormattedEmails(member) }}</td>
|
||||
<td v-if="hasTestMembers">
|
||||
<span v-if="member.testMembership">{{ member.trainingParticipations || 0 }}</span>
|
||||
<span v-else>-</span>
|
||||
@@ -336,10 +369,15 @@ export default {
|
||||
newFirstname: '',
|
||||
newLastname: '',
|
||||
newStreet: '',
|
||||
newPostalCode: '',
|
||||
newCity: '',
|
||||
newBirthdate: '',
|
||||
newPhone: '',
|
||||
newEmail: '',
|
||||
memberContacts: {
|
||||
phones: [],
|
||||
emails: []
|
||||
},
|
||||
newGender: 'unknown',
|
||||
newActive: true,
|
||||
memberToEdit: null,
|
||||
@@ -553,6 +591,7 @@ export default {
|
||||
this.newFirstname = '';
|
||||
this.newLastname = '';
|
||||
this.newStreet = '';
|
||||
this.newPostalCode = '';
|
||||
this.newCity = '';
|
||||
this.newBirthdate = '';
|
||||
this.newPhone = '';
|
||||
@@ -564,6 +603,34 @@ export default {
|
||||
this.memberImage = null;
|
||||
this.memberImagePreview = null;
|
||||
this.newMemberFormHandedOver = false;
|
||||
this.memberContacts = {
|
||||
phones: [{ value: '', isParent: false, parentName: '', isPrimary: false }],
|
||||
emails: [{ value: '', isParent: false, parentName: '', isPrimary: false }]
|
||||
};
|
||||
},
|
||||
addContact(type) {
|
||||
if (type === 'phone') {
|
||||
this.memberContacts.phones.push({
|
||||
value: '',
|
||||
isParent: false,
|
||||
parentName: '',
|
||||
isPrimary: false
|
||||
});
|
||||
} else if (type === 'email') {
|
||||
this.memberContacts.emails.push({
|
||||
value: '',
|
||||
isParent: false,
|
||||
parentName: '',
|
||||
isPrimary: false
|
||||
});
|
||||
}
|
||||
},
|
||||
removeContact(type, index) {
|
||||
if (type === 'phone') {
|
||||
this.memberContacts.phones.splice(index, 1);
|
||||
} else if (type === 'email') {
|
||||
this.memberContacts.emails.splice(index, 1);
|
||||
}
|
||||
},
|
||||
onFileSelected(event) {
|
||||
const file = event.target.files[0];
|
||||
@@ -577,20 +644,47 @@ export default {
|
||||
}
|
||||
},
|
||||
async addNewMember() {
|
||||
// Prepare contacts array
|
||||
const contacts = [];
|
||||
this.memberContacts.phones.forEach(phone => {
|
||||
if (phone.value && phone.value.trim()) {
|
||||
contacts.push({
|
||||
type: 'phone',
|
||||
value: phone.value.trim(),
|
||||
isParent: phone.isParent || false,
|
||||
parentName: phone.isParent ? (phone.parentName || null) : null,
|
||||
isPrimary: phone.isPrimary || false
|
||||
});
|
||||
}
|
||||
});
|
||||
this.memberContacts.emails.forEach(email => {
|
||||
if (email.value && email.value.trim()) {
|
||||
contacts.push({
|
||||
type: 'email',
|
||||
value: email.value.trim(),
|
||||
isParent: email.isParent || false,
|
||||
parentName: email.isParent ? (email.parentName || null) : null,
|
||||
isPrimary: email.isPrimary || false
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const memberData = {
|
||||
firstname: this.newFirstname,
|
||||
lastname: this.newLastname,
|
||||
street: this.newStreet,
|
||||
city: this.newCity,
|
||||
postalCode: this.newPostalCode || null,
|
||||
birthdate: this.newBirthdate,
|
||||
phone: this.newPhone,
|
||||
email: this.newEmail,
|
||||
phone: this.newPhone, // Keep for backward compatibility
|
||||
email: this.newEmail, // Keep for backward compatibility
|
||||
gender: this.newGender,
|
||||
active: this.newActive,
|
||||
id: this.memberToEdit ? this.memberToEdit.id : null,
|
||||
testMembership: this.testMembership,
|
||||
picsInInternetAllowed: this.newPicsInInternetAllowed,
|
||||
memberFormHandedOver: this.newMemberFormHandedOver,
|
||||
contacts: contacts
|
||||
};
|
||||
|
||||
let response;
|
||||
@@ -627,6 +721,7 @@ export default {
|
||||
this.newFirstname = member.firstName;
|
||||
this.newLastname = member.lastName;
|
||||
this.newStreet = member.street;
|
||||
this.newPostalCode = member.postalCode || '';
|
||||
this.newCity = member.city;
|
||||
this.newPhone = member.phone;
|
||||
this.newEmail = member.email;
|
||||
@@ -636,6 +731,38 @@ export default {
|
||||
this.testMembership = member.testMembership;
|
||||
this.newPicsInInternetAllowed = member.picsInInternetAllowed;
|
||||
this.newMemberFormHandedOver = !!member.memberFormHandedOver;
|
||||
|
||||
// Load contacts
|
||||
if (member.contacts && Array.isArray(member.contacts)) {
|
||||
this.memberContacts.phones = member.contacts
|
||||
.filter(c => c.type === 'phone')
|
||||
.map(c => ({
|
||||
value: c.value || '',
|
||||
isParent: c.isParent || false,
|
||||
parentName: c.parentName || '',
|
||||
isPrimary: c.isPrimary || false
|
||||
}));
|
||||
this.memberContacts.emails = member.contacts
|
||||
.filter(c => c.type === 'email')
|
||||
.map(c => ({
|
||||
value: c.value || '',
|
||||
isParent: c.isParent || false,
|
||||
parentName: c.parentName || '',
|
||||
isPrimary: c.isPrimary || false
|
||||
}));
|
||||
|
||||
// If no contacts exist, add empty ones
|
||||
if (this.memberContacts.phones.length === 0) {
|
||||
this.memberContacts.phones.push({ value: '', isParent: false, parentName: '', isPrimary: false });
|
||||
}
|
||||
if (this.memberContacts.emails.length === 0) {
|
||||
this.memberContacts.emails.push({ value: '', isParent: false, parentName: '', isPrimary: false });
|
||||
}
|
||||
} else {
|
||||
// Fallback: use old phone/email fields
|
||||
this.memberContacts.phones = [{ value: member.phone || '', isParent: false, parentName: '', isPrimary: true }];
|
||||
this.memberContacts.emails = [{ value: member.email || '', isParent: false, parentName: '', isPrimary: true }];
|
||||
}
|
||||
try {
|
||||
const response = await apiClient.get(`/clubmembers/image/${member.id}`, {
|
||||
responseType: 'blob'
|
||||
@@ -787,6 +914,76 @@ export default {
|
||||
if (isNaN(date.getTime())) return '–';
|
||||
return `${String(date.getDate()).padStart(2, '0')}.${String(date.getMonth() + 1).padStart(2, '0')}.${date.getFullYear()}`;
|
||||
},
|
||||
getFormattedPhoneNumbers(member) {
|
||||
if (!member) return '–';
|
||||
|
||||
// Sammle alle Telefonnummern aus contacts
|
||||
let phoneNumbers = [];
|
||||
if (member.contacts && Array.isArray(member.contacts)) {
|
||||
const phoneContacts = member.contacts
|
||||
.filter(c => c.type === 'phone' && c.value && String(c.value).trim() !== '')
|
||||
.sort((a, b) => {
|
||||
// Primäre Telefonnummer zuerst
|
||||
if (a.isPrimary && !b.isPrimary) return -1;
|
||||
if (!a.isPrimary && b.isPrimary) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
phoneContacts.forEach(contact => {
|
||||
let phoneText = contact.value;
|
||||
if (contact.isParent) {
|
||||
if (contact.parentName) {
|
||||
phoneText += ` (${contact.parentName})`;
|
||||
} else {
|
||||
phoneText += ' (Elternteil)';
|
||||
}
|
||||
}
|
||||
phoneNumbers.push(phoneText);
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback auf altes phone-Feld für Rückwärtskompatibilität
|
||||
if (phoneNumbers.length === 0 && member.phone) {
|
||||
phoneNumbers.push(member.phone);
|
||||
}
|
||||
|
||||
return phoneNumbers.length > 0 ? phoneNumbers.join(', ') : '–';
|
||||
},
|
||||
getFormattedEmails(member) {
|
||||
if (!member) return '–';
|
||||
|
||||
// Sammle alle E-Mail-Adressen aus contacts
|
||||
let emails = [];
|
||||
if (member.contacts && Array.isArray(member.contacts)) {
|
||||
const emailContacts = member.contacts
|
||||
.filter(c => c.type === 'email' && c.value && String(c.value).trim() !== '')
|
||||
.sort((a, b) => {
|
||||
// Primäre E-Mail zuerst
|
||||
if (a.isPrimary && !b.isPrimary) return -1;
|
||||
if (!a.isPrimary && b.isPrimary) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
emailContacts.forEach(contact => {
|
||||
let emailText = contact.value;
|
||||
if (contact.isParent) {
|
||||
if (contact.parentName) {
|
||||
emailText += ` (${contact.parentName})`;
|
||||
} else {
|
||||
emailText += ' (Elternteil)';
|
||||
}
|
||||
}
|
||||
emails.push(emailText);
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback auf altes email-Feld für Rückwärtskompatibilität
|
||||
if (emails.length === 0 && member.email) {
|
||||
emails.push(member.email);
|
||||
}
|
||||
|
||||
return emails.length > 0 ? emails.join(', ') : '–';
|
||||
},
|
||||
labelGender(g) {
|
||||
const v = (g || 'unknown');
|
||||
if (v === 'male') return 'Männlich';
|
||||
@@ -1347,6 +1544,74 @@ table td {
|
||||
filter: grayscale(0);
|
||||
}
|
||||
|
||||
.contact-section {
|
||||
margin: 1rem 0;
|
||||
padding: 1rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.contact-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.contact-input {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
padding: 0.4rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.parent-name-input {
|
||||
flex: 0 0 150px;
|
||||
padding: 0.4rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.checkbox-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
white-space: nowrap;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.btn-add-contact {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.4rem 0.8rem;
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.btn-add-contact:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
|
||||
.btn-remove-contact {
|
||||
padding: 0.2rem 0.5rem;
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.btn-remove-contact:hover {
|
||||
background-color: #c82333;
|
||||
}
|
||||
|
||||
.warning-icon {
|
||||
margin-right: 0.25rem;
|
||||
font-size: 1.1em;
|
||||
|
||||
Reference in New Issue
Block a user