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 <noreply@anthropic.com>
This commit is contained in:
2026-01-19 13:08:19 +01:00
parent b332989844
commit 9dc8c8b678
4 changed files with 68 additions and 12 deletions

View File

@@ -13,6 +13,7 @@ import {
getUserQSOs, getUserQSOs,
getQSOStats, getQSOStats,
deleteQSOs, deleteQSOs,
getQSOById,
} from './services/lotw.service.js'; } from './services/lotw.service.js';
import { import {
enqueueJob, 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 /api/qsos/stats
* Get QSO statistics (requires authentication) * Get QSO statistics (requires authentication)

View File

@@ -210,6 +210,7 @@ async function calculateDOKAwardProgress(userId, award, options = {}) {
// Initialize combination if not exists // Initialize combination if not exists
if (!dokCombinations.has(combinationKey)) { if (!dokCombinations.has(combinationKey)) {
dokCombinations.set(combinationKey, { dokCombinations.set(combinationKey, {
qsoId: qso.id,
entity: dok, entity: dok,
entityId: null, entityId: null,
entityName: dok, entityName: dok,
@@ -335,6 +336,7 @@ async function calculatePointsAwardProgress(userId, award, options = {}) {
if (!combinationMap.has(combinationKey)) { if (!combinationMap.has(combinationKey)) {
combinationMap.set(combinationKey, { combinationMap.set(combinationKey, {
qsoId: qso.id,
callsign, callsign,
band, band,
mode, mode,
@@ -373,6 +375,7 @@ async function calculatePointsAwardProgress(userId, award, options = {}) {
if (!stationMap.has(callsign)) { if (!stationMap.has(callsign)) {
stationMap.set(callsign, { stationMap.set(callsign, {
qsoId: qso.id,
callsign, callsign,
points, points,
worked: true, worked: true,
@@ -410,6 +413,7 @@ async function calculatePointsAwardProgress(userId, award, options = {}) {
if (qso.lotwQslRstatus === 'Y') { if (qso.lotwQslRstatus === 'Y') {
totalPoints += points; totalPoints += points;
stationDetails.push({ stationDetails.push({
qsoId: qso.id,
callsign, callsign,
points, points,
worked: true, worked: true,
@@ -446,6 +450,7 @@ async function calculatePointsAwardProgress(userId, award, options = {}) {
const entities = stationDetails.map((detail) => { const entities = stationDetails.map((detail) => {
if (countMode === 'perBandMode') { if (countMode === 'perBandMode') {
return { return {
qsoId: detail.qsoId,
entity: `${detail.callsign}/${detail.band}/${detail.mode}`, entity: `${detail.callsign}/${detail.band}/${detail.mode}`,
entityId: null, entityId: null,
entityName: `${detail.callsign} (${detail.band}/${detail.mode})`, entityName: `${detail.callsign} (${detail.band}/${detail.mode})`,
@@ -460,6 +465,7 @@ async function calculatePointsAwardProgress(userId, award, options = {}) {
}; };
} else if (countMode === 'perStation') { } else if (countMode === 'perStation') {
return { return {
qsoId: detail.qsoId,
entity: detail.callsign, entity: detail.callsign,
entityId: null, entityId: null,
entityName: detail.callsign, entityName: detail.callsign,
@@ -474,6 +480,7 @@ async function calculatePointsAwardProgress(userId, award, options = {}) {
}; };
} else { } else {
return { return {
qsoId: detail.qsoId,
entity: `${detail.callsign}-${detail.qsoDate}`, entity: `${detail.callsign}-${detail.qsoDate}`,
entityId: null, entityId: null,
entityName: `${detail.callsign} on ${detail.qsoDate}`, entityName: `${detail.callsign} on ${detail.qsoDate}`,
@@ -673,6 +680,7 @@ export async function getAwardEntityBreakdown(userId, awardId) {
} }
entityMap.set(entity, { entityMap.set(entity, {
qsoId: qso.id,
entity, entity,
entityId: qso.entityId, entityId: qso.entityId,
entityName: displayName, entityName: displayName,

View File

@@ -451,3 +451,18 @@ export async function deleteQSOs(userId) {
return result; 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;
}

View File

@@ -83,6 +83,7 @@
// Add QSO info to this band // Add QSO info to this band
entityData.bands.get(entity.band).push({ entityData.bands.get(entity.band).push({
qsoId: entity.qsoId,
callsign: entity.callsign, callsign: entity.callsign,
mode: entity.mode, mode: entity.mode,
band: entity.band, band: entity.band,
@@ -131,6 +132,7 @@
} }
entityData.bands.get(entity.band).push({ entityData.bands.get(entity.band).push({
qsoId: entity.qsoId,
callsign: entity.callsign, callsign: entity.callsign,
mode: entity.mode, mode: entity.mode,
band: entity.band, band: entity.band,
@@ -169,16 +171,8 @@
selectedQSO = null; selectedQSO = null;
try { try {
// Fetch full QSO details using filters // Fetch full QSO details by ID
const params = new URLSearchParams({ const response = await fetch(`/api/qsos/${qso.qsoId}`, {
callsign: qso.callsign,
qsoDate: qso.qsoDate,
band: qso.band,
mode: qso.mode,
limit: '1'
});
const response = await fetch(`/api/qsos?${params}`, {
headers: { headers: {
'Authorization': `Bearer ${$auth.token}`, 'Authorization': `Bearer ${$auth.token}`,
}, },
@@ -194,8 +188,8 @@
throw new Error(data.error || 'Failed to fetch QSO details'); throw new Error(data.error || 'Failed to fetch QSO details');
} }
if (data.qsos && data.qsos.length > 0) { if (data.qso) {
selectedQSO = data.qsos[0]; selectedQSO = data.qso;
} else { } else {
throw new Error('QSO not found'); throw new Error('QSO not found');
} }