feat: add award detail view with QSO count per slot and mode filter
- Award detail page now shows QSO counts per (entity, band, mode) slot - Click count to open modal with all QSOs for that slot - Click QSO in list to view full details - Add mode filter: "Mixed Mode" aggregates by band, specific modes show (band, mode) columns - Backend groups by slot and collects all confirmed QSOs in qsos array - Frontend displays clickable count links (removed blue bubbles) Backend changes: - calculateDOKAwardProgress(): groups by (DOK, band, mode), collects qsos array - calculatePointsAwardProgress(): updated for all count modes with qsos array - getAwardEntityBreakdown(): groups by (entity, band, mode) slots Frontend changes: - Add mode filter dropdown with "Mixed Mode" default - Update grouping logic to handle mixed mode vs specific mode - Replace count badges with simple clickable links - Add QSO list modal showing all QSOs per slot - Add Mode column to QSO list (useful in mixed mode) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -199,7 +199,7 @@ async function calculateDOKAwardProgress(userId, award, options = {}) {
|
||||
}
|
||||
|
||||
// Track unique (DOK, band, mode) combinations
|
||||
const dokCombinations = new Map(); // Key: "DOK/band/mode" -> detail object
|
||||
const dokCombinations = new Map(); // Key: "DOK/band/mode" -> detail object with qsos array
|
||||
|
||||
for (const qso of filteredQSOs) {
|
||||
const dok = qso.darcDok;
|
||||
@@ -212,29 +212,35 @@ 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,
|
||||
band,
|
||||
mode,
|
||||
callsign: qso.callsign,
|
||||
worked: false,
|
||||
confirmed: false,
|
||||
qsoDate: qso.qsoDate,
|
||||
dclQslRdate: null,
|
||||
qsos: [], // Array of confirmed QSOs for this slot
|
||||
});
|
||||
}
|
||||
|
||||
const detail = dokCombinations.get(combinationKey);
|
||||
detail.worked = true;
|
||||
|
||||
// Check for DCL confirmation
|
||||
// Check for DCL confirmation and add to qsos array
|
||||
if (qso.dclQslRstatus === 'Y') {
|
||||
if (!detail.confirmed) {
|
||||
detail.confirmed = true;
|
||||
detail.dclQslRdate = qso.dclQslRdate;
|
||||
}
|
||||
// Add this confirmed QSO to the qsos array
|
||||
detail.qsos.push({
|
||||
qsoId: qso.id,
|
||||
callsign: qso.callsign,
|
||||
mode: qso.mode,
|
||||
qsoDate: qso.qsoDate,
|
||||
timeOn: qso.timeOn,
|
||||
band: qso.band,
|
||||
confirmed: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,15 +345,13 @@ async function calculatePointsAwardProgress(userId, award, options = {}) {
|
||||
|
||||
if (!combinationMap.has(combinationKey)) {
|
||||
combinationMap.set(combinationKey, {
|
||||
qsoId: qso.id,
|
||||
callsign,
|
||||
band,
|
||||
mode,
|
||||
points,
|
||||
worked: true,
|
||||
confirmed: false,
|
||||
qsoDate: qso.qsoDate,
|
||||
lotwQslRdate: null,
|
||||
qsos: [], // Array of confirmed QSOs for this slot
|
||||
});
|
||||
}
|
||||
|
||||
@@ -355,8 +359,17 @@ async function calculatePointsAwardProgress(userId, award, options = {}) {
|
||||
const detail = combinationMap.get(combinationKey);
|
||||
if (!detail.confirmed) {
|
||||
detail.confirmed = true;
|
||||
detail.lotwQslRdate = qso.lotwQslRdate;
|
||||
}
|
||||
// Add this confirmed QSO to the qsos array
|
||||
detail.qsos.push({
|
||||
qsoId: qso.id,
|
||||
callsign: qso.callsign,
|
||||
mode: qso.mode,
|
||||
qsoDate: qso.qsoDate,
|
||||
timeOn: qso.timeOn,
|
||||
band: qso.band,
|
||||
confirmed: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,15 +391,11 @@ async function calculatePointsAwardProgress(userId, award, options = {}) {
|
||||
|
||||
if (!stationMap.has(callsign)) {
|
||||
stationMap.set(callsign, {
|
||||
qsoId: qso.id,
|
||||
callsign,
|
||||
points,
|
||||
worked: true,
|
||||
confirmed: false,
|
||||
qsoDate: qso.qsoDate,
|
||||
band: qso.band,
|
||||
mode: qso.mode,
|
||||
lotwQslRdate: null,
|
||||
qsos: [], // Array of confirmed QSOs for this station
|
||||
});
|
||||
}
|
||||
|
||||
@@ -394,8 +403,17 @@ async function calculatePointsAwardProgress(userId, award, options = {}) {
|
||||
const detail = stationMap.get(callsign);
|
||||
if (!detail.confirmed) {
|
||||
detail.confirmed = true;
|
||||
detail.lotwQslRdate = qso.lotwQslRdate;
|
||||
}
|
||||
// Add this confirmed QSO to the qsos array
|
||||
detail.qsos.push({
|
||||
qsoId: qso.id,
|
||||
callsign: qso.callsign,
|
||||
mode: qso.mode,
|
||||
qsoDate: qso.qsoDate,
|
||||
timeOn: qso.timeOn,
|
||||
band: qso.band,
|
||||
confirmed: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,6 +433,7 @@ async function calculatePointsAwardProgress(userId, award, options = {}) {
|
||||
|
||||
if (qso.lotwQslRstatus === 'Y') {
|
||||
totalPoints += points;
|
||||
// For perQso mode, each QSO is its own slot with a qsos array containing just itself
|
||||
stationDetails.push({
|
||||
qsoId: qso.id,
|
||||
callsign,
|
||||
@@ -424,7 +443,15 @@ async function calculatePointsAwardProgress(userId, award, options = {}) {
|
||||
qsoDate: qso.qsoDate,
|
||||
band: qso.band,
|
||||
mode: qso.mode,
|
||||
lotwQslRdate: qso.lotwQslRdate,
|
||||
qsos: [{
|
||||
qsoId: qso.id,
|
||||
callsign: qso.callsign,
|
||||
mode: qso.mode,
|
||||
qsoDate: qso.qsoDate,
|
||||
timeOn: qso.timeOn,
|
||||
band: qso.band,
|
||||
confirmed: true,
|
||||
}],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -465,6 +492,7 @@ async function calculatePointsAwardProgress(userId, award, options = {}) {
|
||||
mode: detail.mode,
|
||||
callsign: detail.callsign,
|
||||
lotwQslRdate: detail.lotwQslRdate,
|
||||
qsos: detail.qsos || [], // All confirmed QSOs for this slot
|
||||
};
|
||||
} else if (countMode === 'perStation') {
|
||||
return {
|
||||
@@ -480,6 +508,7 @@ async function calculatePointsAwardProgress(userId, award, options = {}) {
|
||||
mode: detail.mode,
|
||||
callsign: detail.callsign,
|
||||
lotwQslRdate: detail.lotwQslRdate,
|
||||
qsos: detail.qsos || [], // All confirmed QSOs for this station
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
@@ -495,6 +524,7 @@ async function calculatePointsAwardProgress(userId, award, options = {}) {
|
||||
mode: detail.mode,
|
||||
callsign: detail.callsign,
|
||||
lotwQslRdate: detail.lotwQslRdate,
|
||||
qsos: detail.qsos || [], // All confirmed QSOs for this slot (just this one QSO)
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -675,48 +705,62 @@ export async function getAwardEntityBreakdown(userId, awardId) {
|
||||
// Apply filters
|
||||
const filteredQSOs = applyFilters(allQSOs, rules.filters);
|
||||
|
||||
// Group by entity
|
||||
const entityMap = new Map();
|
||||
// Group by (entity, band, mode) slot for entity awards
|
||||
// This allows showing multiple QSOs per entity on different bands/modes
|
||||
const slotMap = new Map(); // Key: "entity/band/mode" -> slot object
|
||||
|
||||
for (const qso of filteredQSOs) {
|
||||
const entity = getEntityValue(qso, rules.entityType);
|
||||
|
||||
if (!entity) continue;
|
||||
|
||||
if (!entityMap.has(entity)) {
|
||||
// Determine what to display as the entity name
|
||||
let displayName = String(entity);
|
||||
if (rules.displayField) {
|
||||
let rawValue = qso[rules.displayField];
|
||||
if (rules.displayField === 'grid' && rawValue && rawValue.length > 4) {
|
||||
rawValue = rawValue.substring(0, 4);
|
||||
}
|
||||
displayName = String(rawValue || entity);
|
||||
} else {
|
||||
displayName = qso.entity || qso.state || qso.grid || qso.callsign || String(entity);
|
||||
}
|
||||
const band = qso.band || 'Unknown';
|
||||
const mode = qso.mode || 'Unknown';
|
||||
const slotKey = `${entity}/${band}/${mode}`;
|
||||
|
||||
entityMap.set(entity, {
|
||||
qsoId: qso.id,
|
||||
// Determine what to display as the entity name (only on first create)
|
||||
let displayName = String(entity);
|
||||
if (rules.displayField) {
|
||||
let rawValue = qso[rules.displayField];
|
||||
if (rules.displayField === 'grid' && rawValue && rawValue.length > 4) {
|
||||
rawValue = rawValue.substring(0, 4);
|
||||
}
|
||||
displayName = String(rawValue || entity);
|
||||
} else {
|
||||
displayName = qso.entity || qso.state || qso.grid || qso.callsign || String(entity);
|
||||
}
|
||||
|
||||
if (!slotMap.has(slotKey)) {
|
||||
slotMap.set(slotKey, {
|
||||
entity,
|
||||
entityId: qso.entityId,
|
||||
entityName: displayName,
|
||||
band,
|
||||
mode,
|
||||
worked: false,
|
||||
confirmed: false,
|
||||
qsoDate: qso.qsoDate,
|
||||
band: qso.band,
|
||||
mode: qso.mode,
|
||||
callsign: qso.callsign,
|
||||
satName: qso.satName,
|
||||
qsos: [], // Array of confirmed QSOs for this slot
|
||||
});
|
||||
}
|
||||
|
||||
const entityData = entityMap.get(entity);
|
||||
entityData.worked = true;
|
||||
const slotData = slotMap.get(slotKey);
|
||||
slotData.worked = true;
|
||||
|
||||
// Check for LoTW confirmation and add to qsos array
|
||||
if (qso.lotwQslRstatus === 'Y') {
|
||||
entityData.confirmed = true;
|
||||
entityData.lotwQslRdate = qso.lotwQslRdate;
|
||||
if (!slotData.confirmed) {
|
||||
slotData.confirmed = true;
|
||||
}
|
||||
// Add this confirmed QSO to the qsos array
|
||||
slotData.qsos.push({
|
||||
qsoId: qso.id,
|
||||
callsign: qso.callsign,
|
||||
mode: qso.mode,
|
||||
qsoDate: qso.qsoDate,
|
||||
timeOn: qso.timeOn,
|
||||
band: qso.band,
|
||||
confirmed: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -728,8 +772,8 @@ export async function getAwardEntityBreakdown(userId, awardId) {
|
||||
caption: award.caption,
|
||||
target: rules.target || 0,
|
||||
},
|
||||
entities: Array.from(entityMap.values()),
|
||||
total: entityMap.size,
|
||||
confirmed: Array.from(entityMap.values()).filter((e) => e.confirmed).length,
|
||||
entities: Array.from(slotMap.values()),
|
||||
total: slotMap.size,
|
||||
confirmed: Array.from(slotMap.values()).filter((e) => e.confirmed).length,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user