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>
<groupId>de.champonthis</groupId>
<artifactId>abi</artifactId>
<version>0.2.8</version>
<version>0.3.0</version>
<name>abi</name>
<parent>
@@ -1,6 +1,7 @@
package de.champonthis.abi.buisinesslogic;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
@@ -37,6 +38,14 @@ public class ContactDataManager {
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) {
if (StringUtils.isNoneEmpty(contactData.getEmail()) && StringUtils.isNoneEmpty(contactData.getInviteToken())) {
Map<String, Object> variables = new HashMap<>();
@@ -1,10 +1,16 @@
package de.champonthis.abi.controller;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
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.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
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.ContactManager;
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.ContactData;
import jakarta.validation.Valid;
@@ -58,13 +65,14 @@ public class ContactDataController {
contactData.setEmail(request.getEmail().toLowerCase());
contactData.setPhone(request.getPhone());
if (!contact.getId().equals(reportedBy.getId())
&& (contactData.getInviteToken() == null || StringUtils.isEmpty(contactData.getInviteToken()))) {
if (!contact.getId().equals(reportedBy.getId()) && StringUtils.isEmpty(contact.getToken())) {
if (contactData.getInviteToken() == null || StringUtils.isEmpty(contactData.getInviteToken())) {
String inviteToken = ContactManager.generateToken();
while (contactDataManager.findByinviteToken(inviteToken) != null) {
inviteToken = ContactManager.generateToken();
}
contactData.setInviteToken(inviteToken);
}
if (contactDataManager.findByContactAndEmail(contact.getId(), contactData.getEmail()) == null) {
contactDataManager.sendInviteMail(contact, contactData, reportedBy);
@@ -80,4 +88,29 @@ public class ContactDataController {
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;
import java.util.List;
import java.util.Optional;
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> findByinviteToken(String inviteToken);
List<ContactData> findByReportedBy(Long reportedBy);
}
+162 -55
View File
@@ -201,27 +201,54 @@
</div>
</form>
<div class="alert py-2" :class="'alert-' + stepMsgType" x-text="stepMsg" x-show="stepMsg"></div>
<div id="knownContactDataDiv" x-show="showKnownContactData" x-transition>
<p x-text="foundKnownContactName"></p>
<form @submit.prevent="submitKnownContactData" class="mb-3">
<div class="mb-3">
<label class="form-label">E-Mail:</label>
<input type="email" x-model="knownContactEmail" placeholder="name@example.com"
required autocomplete="off" class="form-control" autofocus />
<ul class="list-group mb-3" id="enteredContacts" x-show="enteredContacts && enteredContacts.length > 0">
<template x-for="(c, index) in enteredContacts || []" :key="c.name + '_' + index">
<li class="list-group-item">
<!-- Normal view -->
<div x-show="editingContactIndex !== index"
:class="editingContactIndex === index ? 'd-none' : 'd-flex justify-content-between align-items-center'">
<div>
<strong x-text="c.name"></strong><br>
<small class="text-muted">
<span x-text="c.email"></span>
<span x-show="c.phone" x-text="', ' + c.phone"></span>
</small>
</div>
<div class="mb-3">
<label class="form-label">Handynummer (optional):</label>
<input type="text" x-model="knownContactPhone" placeholder="+49123456789"
autocomplete="off" class="form-control" />
<button class="btn btn-sm btn-outline-primary" @click="startEdit(index)" :disabled="working">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<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"/>
</svg>
</button>
</div>
<button type="submit" class="btn btn-outline-success flex-fill"
:class="{'disabled': working}" autofocus>Speichern</button>
</form>
<!-- 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>
<ul class="list-group mb-3" id="enteredContacts">
<template x-for="c in enteredContacts" :key="c.name">
<li class="list-group-item"
x-text="`${c.name} (${c.email}${c.phone ? ', ' + c.phone : ''})`"></li>
<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>
</ul>
<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?
</h2>
<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?
Genauere Infos und Details gibt's dann später hier und per Mail.
</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">
<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>
<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,
ich
kann leider nicht</button>
@@ -416,11 +449,9 @@
email: '[[${email}]]',
phone: '[[${phone}]]',
knownContactName: '',
knownContactEmail: '',
knownContactPhone: '',
showKnownContactData: false,
enteredContacts: [],
foundKnownContactName: null,
editingContactIndex: -1,
editingContactData: { email: '', phone: '' },
committed: '[[${committed}]]',
init() {
@@ -443,6 +474,36 @@
this.stepMsg = '';
this.stepMsgType = '';
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() {
@@ -535,36 +596,48 @@
async findKnownContact() {
this.stepMsg = '';
this.stepMsgType = '';
this.showKnownContactData = false;
this.knownContactEmail = '';
this.knownContactPhone = '';
this.foundKnownContactName = null;
this.working = true;
try {
const resp = await axios.get('/api/v1/contact', {
params: { name: this.knownContactName.trim() },
headers: this.userToken ? { 'Authorization': this.userToken } : {},
validateStatus: () => true
});
if (resp.status === 200) {
this.foundKnownContactName = resp.data.name.trim();
if (this.foundKnownContactName == this.yourName) {
this.stepMsg = 'Das bist du selbst - deine Daten hast du ja schon eingetragen!';
const contactName = resp.data.name.trim();
if (contactName == this.yourName || contactName == this.userName) {
this.stepMsg = 'Witzig, das bist du selbst - deine Daten hast du ja schon eingetragen!';
this.stepMsgType = 'warning';
this.showKnownContactData = false;
this.knownContactName = ''
this.knownContactName = '';
} else {
this.knownContactName = this.foundKnownContactName;
const existingIndex = this.enteredContacts.findIndex(c => c.name === contactName);
if (existingIndex !== -1) {
this.startEdit(existingIndex);
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.showKnownContactData = true;
this.knownContactName = '';
}
}
} else if (resp.status === 404) {
this.stepMsg = 'Diesen Namen gibt es leider nicht in unserer Liste.';
this.stepMsgType = 'warning';
} else if (resp.status === 208) {
this.stepMsg = 'Für diese Person haben wir schon Kontaktdaten. Danke trotzdem fürs Mitmachen!';
this.stepMsgType = 'info';
this.showKnownContactData = false;
} else {
this.stepMsg = 'Da ist was schiefgelaufen. Versuch es bitte nochmal!';
this.stepMsgType = 'danger';
@@ -576,34 +649,68 @@
this.working = false;
},
async submitKnownContactData() {
this.stepMsg = '';
this.stepMsgType = '';
startEdit(index) {
this.editingContactIndex = index;
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;
try {
const contact = this.enteredContacts[index];
const resp = await axios.post('/api/v1/contact/data', {
name: this.foundKnownContactName,
name: contact.name,
reportedBy: this.userName,
email: this.knownContactEmail.trim(),
phone: this.knownContactPhone.trim()
}, { validateStatus: () => true });
if (resp.status === 200 || resp.status === 201) {
this.enteredContacts = this.enteredContacts.filter((c) => c.name !== this.foundKnownContactName);
this.enteredContacts.push({
name: this.foundKnownContactName,
email: this.knownContactEmail.trim(),
phone: this.knownContactPhone.trim()
email: this.editingContactData.email.trim(),
phone: this.editingContactData.phone.trim()
}, {
headers: this.userToken ? { 'Authorization': this.userToken } : {},
validateStatus: () => true
});
this.stepMsg = 'Danke, die Kontaktdaten sind gespeichert! Du kannst noch mehr eintragen oder einfach weitermachen.';
if (resp.status === 200 || resp.status === 201) {
// Update the contact in our local list
this.enteredContacts[index].email = this.editingContactData.email.trim();
this.enteredContacts[index].phone = this.editingContactData.phone.trim();
// Remove the isNew flag since it's now saved
delete this.enteredContacts[index].isNew;
this.stepMsg = 'Kontakt erfolgreich gespeichert!';
this.stepMsgType = 'success';
this.showKnownContactData = false;
this.knownContactName = '';
this.foundKnownContactName = null;
this.editingContactIndex = -1;
this.editingContactData = { email: '', phone: '' };
} else if (resp.status === 400) {
this.stepMsg = 'Bitte prüfe die Angaben nochmal da stimmt was nicht.';
this.stepMsgType = 'warning';
} else {
this.stepMsg = 'Da ist was schiefgelaufen. Versuch es bitte nochmal!';
this.stepMsg = 'Fehler beim Speichern des Kontakts.';
this.stepMsgType = 'danger';
}
} catch {