From d77ee69daa3cb8f8f71c4a9f987402975b1490c6 Mon Sep 17 00:00:00 2001 From: Joerg Date: Fri, 16 Jan 2026 08:53:23 +0100 Subject: [PATCH] Add award detail page and fix award progress calculation ## Frontend - Create award detail page at /awards/[id] - Show all entities with worked/confirmed status - Add filtering (all, worked, confirmed, unworked) - Add sorting (name, status) - Display summary cards (total, confirmed, worked, needed) - Show entity details (callsign, band, mode, date) ## Backend Fixes - Fix award progress calculation for filtered awards - Add normalizeAwardRules to handle "filtered" type awards - Fix satellite filter to check satName field instead of satellite - Add case-insensitive contains matching - Apply normalization to both progress and entity breakdown functions This fixes the 0/0 issue for DXCC CW, WAS, VUCC, and satellite awards. Co-Authored-By: Claude Sonnet 4.5 --- src/backend/services/awards.service.js | 39 +- .../src/routes/awards/[id]/+page.svelte | 408 ++++++++++++++++++ 2 files changed, 443 insertions(+), 4 deletions(-) create mode 100644 src/frontend/src/routes/awards/[id]/+page.svelte diff --git a/src/backend/services/awards.service.js b/src/backend/services/awards.service.js index aeab064..bf4d675 100644 --- a/src/backend/services/awards.service.js +++ b/src/backend/services/awards.service.js @@ -60,13 +60,33 @@ export async function getAllAwards() { })); } +/** + * Normalize award rules to a consistent format + */ +function normalizeAwardRules(rules) { + // Handle "filtered" type awards (like DXCC CW) + if (rules.type === 'filtered' && rules.baseRule) { + return { + type: 'entity', + entityType: rules.baseRule.entityType, + target: rules.baseRule.target, + filters: rules.filters, + }; + } + + return rules; +} + /** * Calculate award progress for a user * @param {number} userId - User ID * @param {Object} award - Award definition */ export async function calculateAwardProgress(userId, award) { - const { rules } = award; + let { rules } = award; + + // Normalize rules to handle different formats + rules = normalizeAwardRules(rules); // Get all QSOs for user const allQSOs = await db @@ -149,7 +169,15 @@ function applyFilters(qsos, filters) { * Check if a QSO matches a filter */ function matchesFilter(qso, filter) { - const value = qso[filter.field]; + let value; + + // Special handling for satellite field + if (filter.field === 'satellite') { + // Check if it's a satellite QSO (has satName) + value = qso.satName && qso.satName.length > 0; + } else { + value = qso[filter.field]; + } switch (filter.operator) { case 'eq': @@ -161,7 +189,7 @@ function matchesFilter(qso, filter) { case 'nin': return Array.isArray(filter.value) && !filter.value.includes(value); case 'contains': - return value && typeof value === 'string' && value.includes(filter.value); + return value && typeof value === 'string' && value.toLowerCase().includes(filter.value.toLowerCase()); default: return true; } @@ -204,7 +232,10 @@ export async function getAwardEntityBreakdown(userId, awardId) { throw new Error('Award not found'); } - const { rules } = award; + let { rules } = award; + + // Normalize rules to handle different formats + rules = normalizeAwardRules(rules); // Get all QSOs for user const allQSOs = await db diff --git a/src/frontend/src/routes/awards/[id]/+page.svelte b/src/frontend/src/routes/awards/[id]/+page.svelte new file mode 100644 index 0000000..be671b3 --- /dev/null +++ b/src/frontend/src/routes/awards/[id]/+page.svelte @@ -0,0 +1,408 @@ + + +
+ {#if loading} +
Loading award details...
+ {:else if error} +
+

Failed to load award: {error}

+ ← Back to Awards +
+ {:else if award} +
+

{award.name}

+

{award.description}

+ ← Back to Awards +
+ +
+
+ + +
+ +
+ + +
+
+ +
+
+ Total: + {entities.length} +
+
+ Confirmed: + {entities.filter((e) => e.confirmed).length} +
+
+ Worked: + {entities.filter((e) => e.worked).length} +
+
+ Needed: + {entities.filter((e) => !e.worked).length} +
+
+ +
+ {#if getFilteredEntities().length === 0} +
No entities match the current filter.
+ {:else} + {#each getFilteredEntities() as entity (entity.entity)} +
+
+
{entity.entity || 'Unknown'}
+
+ {#if entity.callsign} + {entity.callsign} + {/if} + {#if entity.band} + {entity.band} + {/if} + {#if entity.mode} + {entity.mode} + {/if} + {#if entity.qsoDate} + {entity.qsoDate} + {/if} +
+
+
+ + {getStatusText(entity)} + +
+
+ {/each} + {/if} +
+ {/if} +
+ +