From 9dc8c8b678a948b551e762bf52f71e0b1cfbcf3d Mon Sep 17 00:00:00 2001 From: Joerg Date: Mon, 19 Jan 2026 13:08:19 +0100 Subject: [PATCH] fix: use qsoId for fetching QSO details in award page modal The award page was filtering QSOs by callsign/date/band/mode, which could return the wrong QSO when multiple QSOs with the same callsign exist on the same band/mode combination. Changes: - Backend: Add qsoId field to award entity breakdown responses - Backend: Add GET /api/qsos/:id endpoint to fetch QSO by ID - Backend: Implement getQSOById() function in lotw.service.js - Frontend: Update openQSODetailModal() to fetch by qsoId instead of filtering - Frontend: Include qsoId in QSO entry objects for modal click handler Co-Authored-By: Claude Sonnet 4.5 --- src/backend/index.js | 39 +++++++++++++++++++ src/backend/services/awards.service.js | 8 ++++ src/backend/services/lotw.service.js | 15 +++++++ .../src/routes/awards/[id]/+page.svelte | 18 +++------ 4 files changed, 68 insertions(+), 12 deletions(-) diff --git a/src/backend/index.js b/src/backend/index.js index bca86c4..21fcfa8 100644 --- a/src/backend/index.js +++ b/src/backend/index.js @@ -13,6 +13,7 @@ import { getUserQSOs, getQSOStats, deleteQSOs, + getQSOById, } from './services/lotw.service.js'; import { enqueueJob, @@ -489,6 +490,44 @@ const app = new Elysia() } }) + /** + * GET /api/qsos/:id + * Get a single QSO by ID (requires authentication) + */ + .get('/api/qsos/:id', async ({ user, params, set }) => { + if (!user) { + set.status = 401; + return { success: false, error: 'Unauthorized' }; + } + + try { + const qsoId = parseInt(params.id); + if (isNaN(qsoId)) { + set.status = 400; + return { success: false, error: 'Invalid QSO ID' }; + } + + const qso = await getQSOById(user.id, qsoId); + + if (!qso) { + set.status = 404; + return { success: false, error: 'QSO not found' }; + } + + return { + success: true, + qso, + }; + } catch (error) { + logger.error('Failed to fetch QSO by ID', { error: error.message, userId: user?.id, qsoId: params.id }); + set.status = 500; + return { + success: false, + error: 'Failed to fetch QSO', + }; + } + }) + /** * GET /api/qsos/stats * Get QSO statistics (requires authentication) diff --git a/src/backend/services/awards.service.js b/src/backend/services/awards.service.js index cd20d1b..eec8881 100644 --- a/src/backend/services/awards.service.js +++ b/src/backend/services/awards.service.js @@ -210,6 +210,7 @@ async function calculateDOKAwardProgress(userId, award, options = {}) { // Initialize combination if not exists if (!dokCombinations.has(combinationKey)) { dokCombinations.set(combinationKey, { + qsoId: qso.id, entity: dok, entityId: null, entityName: dok, @@ -335,6 +336,7 @@ async function calculatePointsAwardProgress(userId, award, options = {}) { if (!combinationMap.has(combinationKey)) { combinationMap.set(combinationKey, { + qsoId: qso.id, callsign, band, mode, @@ -373,6 +375,7 @@ async function calculatePointsAwardProgress(userId, award, options = {}) { if (!stationMap.has(callsign)) { stationMap.set(callsign, { + qsoId: qso.id, callsign, points, worked: true, @@ -410,6 +413,7 @@ async function calculatePointsAwardProgress(userId, award, options = {}) { if (qso.lotwQslRstatus === 'Y') { totalPoints += points; stationDetails.push({ + qsoId: qso.id, callsign, points, worked: true, @@ -446,6 +450,7 @@ async function calculatePointsAwardProgress(userId, award, options = {}) { const entities = stationDetails.map((detail) => { if (countMode === 'perBandMode') { return { + qsoId: detail.qsoId, entity: `${detail.callsign}/${detail.band}/${detail.mode}`, entityId: null, entityName: `${detail.callsign} (${detail.band}/${detail.mode})`, @@ -460,6 +465,7 @@ async function calculatePointsAwardProgress(userId, award, options = {}) { }; } else if (countMode === 'perStation') { return { + qsoId: detail.qsoId, entity: detail.callsign, entityId: null, entityName: detail.callsign, @@ -474,6 +480,7 @@ async function calculatePointsAwardProgress(userId, award, options = {}) { }; } else { return { + qsoId: detail.qsoId, entity: `${detail.callsign}-${detail.qsoDate}`, entityId: null, entityName: `${detail.callsign} on ${detail.qsoDate}`, @@ -673,6 +680,7 @@ export async function getAwardEntityBreakdown(userId, awardId) { } entityMap.set(entity, { + qsoId: qso.id, entity, entityId: qso.entityId, entityName: displayName, diff --git a/src/backend/services/lotw.service.js b/src/backend/services/lotw.service.js index e7eb59d..dbac8d3 100644 --- a/src/backend/services/lotw.service.js +++ b/src/backend/services/lotw.service.js @@ -451,3 +451,18 @@ export async function deleteQSOs(userId) { return result; } +/** + * Get a single QSO by ID for a specific user + * @param {number} userId - User ID + * @param {number} qsoId - QSO ID + * @returns {Object|null} QSO object or null if not found + */ +export async function getQSOById(userId, qsoId) { + const result = await db + .select() + .from(qsos) + .where(and(eq(qsos.userId, userId), eq(qsos.id, qsoId))); + + return result.length > 0 ? result[0] : null; +} + diff --git a/src/frontend/src/routes/awards/[id]/+page.svelte b/src/frontend/src/routes/awards/[id]/+page.svelte index c49a71e..f80f9f3 100644 --- a/src/frontend/src/routes/awards/[id]/+page.svelte +++ b/src/frontend/src/routes/awards/[id]/+page.svelte @@ -83,6 +83,7 @@ // Add QSO info to this band entityData.bands.get(entity.band).push({ + qsoId: entity.qsoId, callsign: entity.callsign, mode: entity.mode, band: entity.band, @@ -131,6 +132,7 @@ } entityData.bands.get(entity.band).push({ + qsoId: entity.qsoId, callsign: entity.callsign, mode: entity.mode, band: entity.band, @@ -169,16 +171,8 @@ selectedQSO = null; try { - // Fetch full QSO details using filters - const params = new URLSearchParams({ - callsign: qso.callsign, - qsoDate: qso.qsoDate, - band: qso.band, - mode: qso.mode, - limit: '1' - }); - - const response = await fetch(`/api/qsos?${params}`, { + // Fetch full QSO details by ID + const response = await fetch(`/api/qsos/${qso.qsoId}`, { headers: { 'Authorization': `Bearer ${$auth.token}`, }, @@ -194,8 +188,8 @@ throw new Error(data.error || 'Failed to fetch QSO details'); } - if (data.qsos && data.qsos.length > 0) { - selectedQSO = data.qsos[0]; + if (data.qso) { + selectedQSO = data.qso; } else { throw new Error('QSO not found'); }