big improvements yay!

This commit is contained in:
2025-11-14 22:18:59 +01:00
parent 9b0eec1388
commit f5aa3d6993
6 changed files with 231 additions and 67 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>de.champonthis</groupId> <groupId>de.champonthis</groupId>
<artifactId>abi</artifactId> <artifactId>abi</artifactId>
<version>0.2.8</version> <version>0.3.0</version>
<name>abi</name> <name>abi</name>
<parent> <parent>
@@ -1,6 +1,7 @@
package de.champonthis.abi.buisinesslogic; package de.champonthis.abi.buisinesslogic;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@@ -37,6 +38,14 @@ public class ContactDataManager {
contactDataRepository.save(contactData); contactDataRepository.save(contactData);
} }
public List<ContactData> findPendingContactData(Long reportedBy) {
return contactDataRepository.findByReportedBy(reportedBy).stream()
.filter(cd -> !cd.getContact().equals(reportedBy))
.filter(cd -> findByContactAndReportedBy(cd.getContact(), cd.getContact()) == null)
.sorted((cd1, cd2) -> cd1.getContact().compareTo(cd2.getContact()))
.toList();
}
public void sendInviteMail(Contact contact, ContactData contactData, Contact reportedBy) { public void sendInviteMail(Contact contact, ContactData contactData, Contact reportedBy) {
if (StringUtils.isNoneEmpty(contactData.getEmail()) && StringUtils.isNoneEmpty(contactData.getInviteToken())) { if (StringUtils.isNoneEmpty(contactData.getEmail()) && StringUtils.isNoneEmpty(contactData.getInviteToken())) {
Map<String, Object> variables = new HashMap<>(); Map<String, Object> variables = new HashMap<>();
@@ -1,10 +1,16 @@
package de.champonthis.abi.controller; package de.champonthis.abi.controller;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
@@ -12,6 +18,7 @@ import org.springframework.web.server.ResponseStatusException;
import de.champonthis.abi.buisinesslogic.ContactDataManager; import de.champonthis.abi.buisinesslogic.ContactDataManager;
import de.champonthis.abi.buisinesslogic.ContactManager; import de.champonthis.abi.buisinesslogic.ContactManager;
import de.champonthis.abi.controller.request.ContactDataRequest; import de.champonthis.abi.controller.request.ContactDataRequest;
import de.champonthis.abi.controller.response.ContactDataResponse;
import de.champonthis.abi.entity.Contact; import de.champonthis.abi.entity.Contact;
import de.champonthis.abi.entity.ContactData; import de.champonthis.abi.entity.ContactData;
import jakarta.validation.Valid; import jakarta.validation.Valid;
@@ -58,13 +65,14 @@ public class ContactDataController {
contactData.setEmail(request.getEmail().toLowerCase()); contactData.setEmail(request.getEmail().toLowerCase());
contactData.setPhone(request.getPhone()); contactData.setPhone(request.getPhone());
if (!contact.getId().equals(reportedBy.getId()) if (!contact.getId().equals(reportedBy.getId()) && StringUtils.isEmpty(contact.getToken())) {
&& (contactData.getInviteToken() == null || StringUtils.isEmpty(contactData.getInviteToken()))) { if (contactData.getInviteToken() == null || StringUtils.isEmpty(contactData.getInviteToken())) {
String inviteToken = ContactManager.generateToken(); String inviteToken = ContactManager.generateToken();
while (contactDataManager.findByinviteToken(inviteToken) != null) { while (contactDataManager.findByinviteToken(inviteToken) != null) {
inviteToken = ContactManager.generateToken(); inviteToken = ContactManager.generateToken();
}
contactData.setInviteToken(inviteToken);
} }
contactData.setInviteToken(inviteToken);
if (contactDataManager.findByContactAndEmail(contact.getId(), contactData.getEmail()) == null) { if (contactDataManager.findByContactAndEmail(contact.getId(), contactData.getEmail()) == null) {
contactDataManager.sendInviteMail(contact, contactData, reportedBy); contactDataManager.sendInviteMail(contact, contactData, reportedBy);
@@ -80,4 +88,29 @@ public class ContactDataController {
return ResponseEntity.status(status.value()).body(contactData); return ResponseEntity.status(status.value()).body(contactData);
} }
@GetMapping("/pending")
public List<ContactDataResponse> getPending(
@RequestHeader(name = HttpHeaders.AUTHORIZATION, required = false) String authToken) {
if (StringUtils.isEmpty(authToken)) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
}
Contact contact = contactManager.findByToken(authToken);
if (contact == null) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
}
return contactDataManager.findPendingContactData(contact.getId()).stream()
.map(cd -> {
Contact contactEntity = contactManager.findById(cd.getContact());
String contactName = contactEntity != null ? contactEntity.getName() : "Unbekannt";
return new ContactDataResponse(
contactName,
cd.getEmail(),
cd.getPhone());
})
.collect(Collectors.toList());
}
} }
@@ -0,0 +1,12 @@
package de.champonthis.abi.controller.response;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class ContactDataResponse {
private String contactName;
private String email;
private String phone;
}
@@ -1,5 +1,6 @@
package de.champonthis.abi.repository; package de.champonthis.abi.repository;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
@@ -15,4 +16,6 @@ public interface ContactDataRepository extends JpaRepository<ContactData, Long>
Optional<ContactData> findByContactAndEmail(Long contact, String email); Optional<ContactData> findByContactAndEmail(Long contact, String email);
Optional<ContactData> findByinviteToken(String inviteToken); Optional<ContactData> findByinviteToken(String inviteToken);
List<ContactData> findByReportedBy(Long reportedBy);
} }
+167 -60
View File
@@ -201,27 +201,54 @@
</div> </div>
</form> </form>
<div class="alert py-2" :class="'alert-' + stepMsgType" x-text="stepMsg" x-show="stepMsg"></div> <div class="alert py-2" :class="'alert-' + stepMsgType" x-text="stepMsg" x-show="stepMsg"></div>
<div id="knownContactDataDiv" x-show="showKnownContactData" x-transition> <ul class="list-group mb-3" id="enteredContacts" x-show="enteredContacts && enteredContacts.length > 0">
<p x-text="foundKnownContactName"></p> <template x-for="(c, index) in enteredContacts || []" :key="c.name + '_' + index">
<form @submit.prevent="submitKnownContactData" class="mb-3"> <li class="list-group-item">
<div class="mb-3"> <!-- Normal view -->
<label class="form-label">E-Mail:</label> <div x-show="editingContactIndex !== index"
<input type="email" x-model="knownContactEmail" placeholder="name@example.com" :class="editingContactIndex === index ? 'd-none' : 'd-flex justify-content-between align-items-center'">
required autocomplete="off" class="form-control" autofocus /> <div>
</div> <strong x-text="c.name"></strong><br>
<div class="mb-3"> <small class="text-muted">
<label class="form-label">Handynummer (optional):</label> <span x-text="c.email"></span>
<input type="text" x-model="knownContactPhone" placeholder="+49123456789" <span x-show="c.phone" x-text="', ' + c.phone"></span>
autocomplete="off" class="form-control" /> </small>
</div> </div>
<button type="submit" class="btn btn-outline-success flex-fill" <button class="btn btn-sm btn-outline-primary" @click="startEdit(index)" :disabled="working">
:class="{'disabled': working}" autofocus>Speichern</button> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
</form> <path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708L4.707 14.707a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168L12.146.146zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293L12.793 5.5z"/>
</div> </svg>
<ul class="list-group mb-3" id="enteredContacts"> </button>
<template x-for="c in enteredContacts" :key="c.name"> </div>
<li class="list-group-item"
x-text="`${c.name} (${c.email}${c.phone ? ', ' + c.phone : ''})`"></li> <!-- Edit view - fully inline -->
<div x-show="editingContactIndex === index"
:class="editingContactIndex === index ? 'row g-2 align-items-start' : 'd-none'">
<div class="col-12">
<strong x-text="c.name"></strong>
</div>
<div class="col-12 col-md">
<input type="email" class="form-control form-control-sm" x-model="editingContactData.email" placeholder="E-Mail" required>
</div>
<div class="col-12 col-md">
<input type="text" class="form-control form-control-sm" x-model="editingContactData.phone" placeholder="Telefon (optional)">
</div>
<div class="col-12 col-md-auto">
<div class="d-flex gap-1">
<button class="btn btn-sm btn-outline-danger" @click="cancelEdit()" :disabled="working">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
<path d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8 2.146 2.854Z"/>
</svg>
</button>
<button class="btn btn-sm btn-success" @click="saveContactData(index)" :disabled="working">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
<path d="M10.97 4.97a.235.235 0 0 0-.02.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05z"/>
</svg>
</button>
</div>
</div>
</div>
</li>
</template> </template>
</ul> </ul>
<div class="d-flex gap-2 mt-3 justify-content-center"> <div class="d-flex gap-2 mt-3 justify-content-center">
@@ -250,16 +277,22 @@
</svg> Schritt 4: Bist du beim 20-Jahrestreffen am 13.11.2027 in Gevelsberg dabei? </svg> Schritt 4: Bist du beim 20-Jahrestreffen am 13.11.2027 in Gevelsberg dabei?
</h2> </h2>
<div class="alert py-2" :class="'alert-' + stepMsgType" x-text="stepMsg" x-show="stepMsg"></div> <div class="alert py-2" :class="'alert-' + stepMsgType" x-text="stepMsg" x-show="stepMsg"></div>
<p x-show="!stepMsg"> <p x-show="!stepMsg && !committed">
Hast du Lust, am Samstag, den 13.11.2027, beim Nachtreffen in Gevelsberg dabei zu sein? Hast du Lust, am Samstag, den 13.11.2027, beim Nachtreffen in Gevelsberg dabei zu sein?
Genauere Infos und Details gibt's dann später hier und per Mail. Genauere Infos und Details gibt's dann später hier und per Mail.
</p> </p>
<p x-show="!stepMsg && committed === 'YES'">
Du hast bereits <strong>zugesagt</strong>. Genauere Infos und Details gibt's dann später hier und per Mail. Falls du deine Meinung ändern möchtest, kannst du hier nochmal wählen.
</p>
<p x-show="!stepMsg && committed === 'NO'">
Du hast bereits <strong>abgesagt</strong>. Falls du deine Meinung ändern möchtest, kannst du hier nochmal wählen.
</p>
<div class="d-flex gap-2 mb-3"> <div class="d-flex gap-2 mb-3">
<button x-show="!stepMsg" id="commitYes" class="btn flex-fill" <button x-show="!stepMsg" id="commitYes" class="btn flex-fill"
:class="{'btn-success': committed != 'NO', 'btn-outline-dark' : committed == 'NO', 'disabled': working}" :class="{'btn-success': committed != 'NO', 'btn-outline-secondary' : committed == 'NO', 'disabled': working}"
@click="submitCommit('YES')" autofocus>Ja, ich bin dabei!</button> @click="submitCommit('YES')" autofocus>Ja, ich bin dabei!</button>
<button x-show="!stepMsg" id="commitNo" class="btn flex-fill" <button x-show="!stepMsg" id="commitNo" class="btn flex-fill"
:class="{'btn-danger' : committed != 'YES','btn-outline-dark' : committed == 'YES', 'disabled': working}" :class="{'btn-danger' : committed != 'YES','btn-outline-secondary' : committed == 'YES', 'disabled': working}"
@click="submitCommit('NO')">Nein, @click="submitCommit('NO')">Nein,
ich ich
kann leider nicht</button> kann leider nicht</button>
@@ -416,11 +449,9 @@
email: '[[${email}]]', email: '[[${email}]]',
phone: '[[${phone}]]', phone: '[[${phone}]]',
knownContactName: '', knownContactName: '',
knownContactEmail: '',
knownContactPhone: '',
showKnownContactData: false,
enteredContacts: [], enteredContacts: [],
foundKnownContactName: null, editingContactIndex: -1,
editingContactData: { email: '', phone: '' },
committed: '[[${committed}]]', committed: '[[${committed}]]',
init() { init() {
@@ -443,6 +474,36 @@
this.stepMsg = ''; this.stepMsg = '';
this.stepMsgType = ''; this.stepMsgType = '';
this.step = step; this.step = step;
if (!this.enteredContacts) {
this.enteredContacts = [];
}
if (step === 3 && this.userToken) {
this.loadPendingContacts();
}
},
async loadPendingContacts() {
try {
const resp = await axios.get('/api/v1/contact/data/pending', {
headers: { 'Authorization': this.userToken },
validateStatus: () => true
});
if (resp.status === 200) {
this.enteredContacts = (this.enteredContacts || []).filter((c) => c.isNew);
resp.data.forEach(contact => {
this.enteredContacts.push({
name: contact.contactName,
email: contact.email,
phone: contact.phone || ''
});
});
}
} catch (error) {
console.error('Fehler beim Laden der Kontakte:', error);
}
}, },
async findSelf() { async findSelf() {
@@ -535,36 +596,48 @@
async findKnownContact() { async findKnownContact() {
this.stepMsg = ''; this.stepMsg = '';
this.stepMsgType = ''; this.stepMsgType = '';
this.showKnownContactData = false;
this.knownContactEmail = '';
this.knownContactPhone = '';
this.foundKnownContactName = null;
this.working = true; this.working = true;
try { try {
const resp = await axios.get('/api/v1/contact', { const resp = await axios.get('/api/v1/contact', {
params: { name: this.knownContactName.trim() }, params: { name: this.knownContactName.trim() },
headers: this.userToken ? { 'Authorization': this.userToken } : {},
validateStatus: () => true validateStatus: () => true
}); });
if (resp.status === 200) { if (resp.status === 200) {
this.foundKnownContactName = resp.data.name.trim(); const contactName = resp.data.name.trim();
if (this.foundKnownContactName == this.yourName) { if (contactName == this.yourName || contactName == this.userName) {
this.stepMsg = 'Das bist du selbst - deine Daten hast du ja schon eingetragen!'; this.stepMsg = 'Witzig, das bist du selbst - deine Daten hast du ja schon eingetragen!';
this.stepMsgType = 'warning'; this.stepMsgType = 'warning';
this.showKnownContactData = false; this.knownContactName = '';
this.knownContactName = ''
} else { } else {
this.knownContactName = this.foundKnownContactName; const existingIndex = this.enteredContacts.findIndex(c => c.name === contactName);
this.stepMsg = 'Super, trag bitte die Kontaktdaten ein!'; if (existingIndex !== -1) {
this.stepMsgType = 'success'; this.startEdit(existingIndex);
this.showKnownContactData = true; this.stepMsg = 'Kontakt bereits vorhanden - du kannst die Daten hier bearbeiten.';
this.stepMsgType = 'info';
this.knownContactName = '';
} else {
this.enteredContacts.unshift({
name: contactName,
email: '',
phone: '',
isNew: true // Mark as new for cancel behavior
});
this.startEdit(0);
this.stepMsg = 'Super, trag bitte die Kontaktdaten ein!';
this.stepMsgType = 'success';
this.knownContactName = '';
}
} }
} else if (resp.status === 404) { } else if (resp.status === 404) {
this.stepMsg = 'Diesen Namen gibt es leider nicht in unserer Liste.'; this.stepMsg = 'Diesen Namen gibt es leider nicht in unserer Liste.';
this.stepMsgType = 'warning'; this.stepMsgType = 'warning';
} else if (resp.status === 208) { } else if (resp.status === 208) {
this.stepMsg = 'Für diese Person haben wir schon Kontaktdaten. Danke trotzdem fürs Mitmachen!'; this.stepMsg = 'Für diese Person haben wir schon Kontaktdaten. Danke trotzdem fürs Mitmachen!';
this.stepMsgType = 'info'; this.stepMsgType = 'info';
this.showKnownContactData = false;
} else { } else {
this.stepMsg = 'Da ist was schiefgelaufen. Versuch es bitte nochmal!'; this.stepMsg = 'Da ist was schiefgelaufen. Versuch es bitte nochmal!';
this.stepMsgType = 'danger'; this.stepMsgType = 'danger';
@@ -576,34 +649,68 @@
this.working = false; this.working = false;
}, },
async submitKnownContactData() { startEdit(index) {
this.stepMsg = ''; this.editingContactIndex = index;
this.stepMsgType = ''; this.editingContactData = {
email: this.enteredContacts[index].email,
phone: this.enteredContacts[index].phone || ''
};
// Focus the email input of the specific contact being edited
setTimeout(() => {
// Find all email inputs and select the one that's currently visible (not d-none)
const emailInputs = document.querySelectorAll('.list-group-item input[type="email"]');
for (let input of emailInputs) {
const parentDiv = input.closest('div[x-show]');
if (parentDiv && !parentDiv.classList.contains('d-none')) {
input.focus();
input.select();
break;
}
}
}, 100);
},
cancelEdit() {
// If this is a new contact that hasn't been saved, remove it from the list
if (this.editingContactIndex >= 0 && this.enteredContacts[this.editingContactIndex].isNew) {
this.enteredContacts.splice(this.editingContactIndex, 1);
}
this.editingContactIndex = -1;
this.editingContactData = { email: '', phone: '' };
},
async saveContactData(index) {
this.working = true; this.working = true;
try { try {
const contact = this.enteredContacts[index];
const resp = await axios.post('/api/v1/contact/data', { const resp = await axios.post('/api/v1/contact/data', {
name: this.foundKnownContactName, name: contact.name,
reportedBy: this.userName, reportedBy: this.userName,
email: this.knownContactEmail.trim(), email: this.editingContactData.email.trim(),
phone: this.knownContactPhone.trim() phone: this.editingContactData.phone.trim()
}, { validateStatus: () => true }); }, {
headers: this.userToken ? { 'Authorization': this.userToken } : {},
validateStatus: () => true
});
if (resp.status === 200 || resp.status === 201) { if (resp.status === 200 || resp.status === 201) {
this.enteredContacts = this.enteredContacts.filter((c) => c.name !== this.foundKnownContactName); // Update the contact in our local list
this.enteredContacts.push({ this.enteredContacts[index].email = this.editingContactData.email.trim();
name: this.foundKnownContactName, this.enteredContacts[index].phone = this.editingContactData.phone.trim();
email: this.knownContactEmail.trim(), // Remove the isNew flag since it's now saved
phone: this.knownContactPhone.trim() delete this.enteredContacts[index].isNew;
});
this.stepMsg = 'Danke, die Kontaktdaten sind gespeichert! Du kannst noch mehr eintragen oder einfach weitermachen.'; this.stepMsg = 'Kontakt erfolgreich gespeichert!';
this.stepMsgType = 'success'; this.stepMsgType = 'success';
this.showKnownContactData = false; this.editingContactIndex = -1;
this.knownContactName = ''; this.editingContactData = { email: '', phone: '' };
this.foundKnownContactName = null;
} else if (resp.status === 400) { } else if (resp.status === 400) {
this.stepMsg = 'Bitte prüfe die Angaben nochmal da stimmt was nicht.'; this.stepMsg = 'Bitte prüfe die Angaben nochmal da stimmt was nicht.';
this.stepMsgType = 'warning'; this.stepMsgType = 'warning';
} else { } else {
this.stepMsg = 'Da ist was schiefgelaufen. Versuch es bitte nochmal!'; this.stepMsg = 'Fehler beim Speichern des Kontakts.';
this.stepMsgType = 'danger'; this.stepMsgType = 'danger';
} }
} catch { } catch {