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 <noreply@anthropic.com>
This commit is contained in:
2026-01-16 08:53:23 +01:00
parent 884bdb436d
commit d77ee69daa
2 changed files with 443 additions and 4 deletions

View File

@@ -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