Add connection review page with optional message on connection requests

- New GET route and template for reviewing individual connection requests
- Optional personal message field when sending connection requests
- Message visible to recipient on review page and connection list
- Dashboard "Review" button links to per-connection review page
- QR scan flow shows message prompt before sending request
- Fix accepted connections query: include both sent and received
- Fix url_for('uploaded_file') -> url_for('static', 'uploads/')
- Fix request.json -> request.is_json to avoid 415 on form POST
- Remove card from DOM on approve/reject instead of page reload

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-28 08:28:21 +00:00
parent 3c26f1310e
commit 009b7d896b
9 changed files with 579 additions and 64 deletions
+58 -21
View File
@@ -16,7 +16,13 @@
<div class="result-icon" id="result-icon"></div>
<h2 id="result-title"></h2>
<p id="result-message"></p>
<button id="scan-again" class="btn btn-primary"><i data-lucide="scan-line"></i> {{ 'scan_again'|t }}</button>
<div id="result-actions" class="result-actions hidden">
<textarea id="message-input" class="message-input" placeholder="{{ 'optional_message'|t }}" rows="2" maxlength="500"></textarea>
<div class="result-buttons">
<button id="send-request-btn" class="btn btn-primary"><i data-lucide="send"></i> {{ 'send_request'|t }}</button>
<button id="scan-again" class="btn btn-outline">{{ 'scan_again'|t }}</button>
</div>
</div>
</div>
</div>
@@ -32,13 +38,14 @@ const myId = {{ session.user_id }};
const myEventId = {{ session.event_id }};
let html5QrCode;
let scanning = true;
let pendingScannedId = null;
async function sendConnectionRequest(scannedId) {
async function sendConnectionRequest(scannedId, message) {
try {
const response = await fetch('/attendee/scan-request', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ scanned_id: scannedId })
body: JSON.stringify({ scanned_id: scannedId, message: message })
});
const data = await response.json();
@@ -46,17 +53,19 @@ async function sendConnectionRequest(scannedId) {
const icon = document.getElementById('result-icon');
const title = document.getElementById('result-title');
const message = document.getElementById('result-message');
const resultActions = document.getElementById('result-actions');
icon.className = 'result-icon';
icon.innerHTML = '<i data-lucide="clock"></i>';
resultActions.classList.add('hidden');
if (data.success) {
document.getElementById('qr-reader').style.display = 'none';
resultDiv.classList.remove('hidden');
icon.classList.add('pending');
icon.innerHTML = '<i data-lucide="clock"></i>';
icon.classList.add('success');
icon.innerHTML = '<i data-lucide="check-circle"></i>';
title.textContent = '{{ "request_sent"|t }}';
message.textContent = '{{ "request_sent_message"|t }}';
document.getElementById('message-input').value = '';
} else {
document.getElementById('qr-reader').style.display = 'none';
resultDiv.classList.remove('hidden');
@@ -68,6 +77,7 @@ async function sendConnectionRequest(scannedId) {
lucide.createIcons();
scanning = false;
pendingScannedId = null;
} catch (error) {
console.error('Connection error:', error);
showToast('Error sending connection request', 'error');
@@ -77,7 +87,6 @@ async function sendConnectionRequest(scannedId) {
function onScanSuccess(decodedText) {
if (!scanning) return;
// Expected format: "NETEVENT:{event_id}:{attendee_id}"
const parts = decodedText.split(':');
if (parts.length === 3 && parts[0] === 'NETEVENT') {
const eventId = parseInt(parts[1]);
@@ -88,16 +97,54 @@ function onScanSuccess(decodedText) {
showToast('{{ "cannot_scan_own_qr"|t }}', 'warning');
return;
}
sendConnectionRequest(attendeeId);
pendingScannedId = attendeeId;
showMessagePrompt();
} else {
showToast('{{ "qr_different_event"|t }}', 'error');
}
} else {
// QR format not recognized
showToast('{{ "unrecognized_qr"|t }}', 'error');
}
}
function showMessagePrompt() {
const resultDiv = document.getElementById('scan-result');
const icon = document.getElementById('result-icon');
const title = document.getElementById('result-title');
const msg = document.getElementById('result-message');
const resultActions = document.getElementById('result-actions');
document.getElementById('qr-reader').style.display = 'none';
resultDiv.classList.remove('hidden');
resultActions.classList.remove('hidden');
icon.className = 'result-icon pending';
icon.innerHTML = '<i data-lucide="message-circle"></i>';
title.textContent = '{{ "scanned_successfully"|t }}';
msg.textContent = '{{ "scan_add_message_hint"|t }}';
document.getElementById('message-input').value = '';
document.getElementById('message-input').focus();
lucide.createIcons();
scanning = false;
}
document.getElementById('send-request-btn').addEventListener('click', function() {
if (pendingScannedId) {
const message = document.getElementById('message-input').value.trim();
sendConnectionRequest(pendingScannedId, message || null);
}
});
document.getElementById('scan-again').addEventListener('click', resetScanner);
function resetScanner() {
document.getElementById('qr-reader').style.display = 'block';
document.getElementById('scan-result').classList.add('hidden');
document.getElementById('message-input').value = '';
scanning = true;
pendingScannedId = null;
}
function startScanner() {
html5QrCode = new Html5Qrcode("qr-reader");
@@ -108,23 +155,13 @@ function startScanner() {
qrbox: { width: 250, height: 250 }
},
onScanSuccess,
(errorMessage) => {
// Ignore scan errors
}
(errorMessage) => {}
).catch(err => {
console.error('Camera error:', err);
showToast('{{ "camera_permission_error"|t }}', 'error');
});
}
function resetScanner() {
document.getElementById('qr-reader').style.display = 'block';
document.getElementById('scan-result').classList.add('hidden');
scanning = true;
}
document.getElementById('scan-again').addEventListener('click', resetScanner);
window.addEventListener('DOMContentLoaded', startScanner);
</script>
{% endblock %}
{% endblock %}