fix: include band field in QSO entries for award detail modal
The QSO entries on the award detail page were missing the band field, causing the openQSODetailModal() function to fail with "QSO not found" when trying to fetch full QSO details. The band field is now included in the QSO entry objects so the filter parameters match correctly. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,11 @@
|
||||
let groupedData = [];
|
||||
let bands = [];
|
||||
|
||||
// QSO detail modal state
|
||||
let selectedQSO = null;
|
||||
let showQSODetailModal = false;
|
||||
let loadingQSO = false;
|
||||
|
||||
onMount(async () => {
|
||||
await loadAwardData();
|
||||
});
|
||||
@@ -80,6 +85,7 @@
|
||||
entityData.bands.get(entity.band).push({
|
||||
callsign: entity.callsign,
|
||||
mode: entity.mode,
|
||||
band: entity.band,
|
||||
confirmed: entity.confirmed,
|
||||
qsoDate: entity.qsoDate,
|
||||
});
|
||||
@@ -127,6 +133,7 @@
|
||||
entityData.bands.get(entity.band).push({
|
||||
callsign: entity.callsign,
|
||||
mode: entity.mode,
|
||||
band: entity.band,
|
||||
confirmed: entity.confirmed,
|
||||
qsoDate: entity.qsoDate,
|
||||
});
|
||||
@@ -154,6 +161,79 @@
|
||||
$: if (entities.length > 0) {
|
||||
applyFilter();
|
||||
}
|
||||
|
||||
// QSO Detail Modal Functions
|
||||
async function openQSODetailModal(qso) {
|
||||
loadingQSO = true;
|
||||
showQSODetailModal = true;
|
||||
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}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${$auth.token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch QSO details');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.error || 'Failed to fetch QSO details');
|
||||
}
|
||||
|
||||
if (data.qsos && data.qsos.length > 0) {
|
||||
selectedQSO = data.qsos[0];
|
||||
} else {
|
||||
throw new Error('QSO not found');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load QSO details:', err);
|
||||
alert('Failed to load QSO details: ' + err.message);
|
||||
showQSODetailModal = false;
|
||||
} finally {
|
||||
loadingQSO = false;
|
||||
}
|
||||
}
|
||||
|
||||
function closeQSODetailModal() {
|
||||
selectedQSO = null;
|
||||
showQSODetailModal = false;
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
// ADIF format: YYYYMMDD
|
||||
const year = dateStr.substring(0, 4);
|
||||
const month = dateStr.substring(4, 6);
|
||||
const day = dateStr.substring(6, 8);
|
||||
return `${day}/${month}/${year}`;
|
||||
}
|
||||
|
||||
function formatTime(timeStr) {
|
||||
if (!timeStr) return '-';
|
||||
// ADIF format: HHMMSS or HHMM
|
||||
return timeStr.substring(0, 2) + ':' + timeStr.substring(2, 4);
|
||||
}
|
||||
|
||||
function getConfirmationStatus(status) {
|
||||
if (status === 'Y') return { label: 'Confirmed', class: 'confirmed' };
|
||||
if (status === 'N') return { label: 'Not Confirmed', class: 'not-confirmed' };
|
||||
if (status === '?') return { label: 'Unknown', class: 'unknown' };
|
||||
return { label: 'No Data', class: 'no-data' };
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
@@ -247,7 +327,13 @@
|
||||
{#if qsos.length > 0}
|
||||
<div class="qso-list">
|
||||
{#each qsos as qso}
|
||||
<div class="qso-entry {qso.confirmed ? 'qso-confirmed' : 'qso-worked'}">
|
||||
<div
|
||||
class="qso-entry {qso.confirmed ? 'qso-confirmed' : 'qso-worked'}"
|
||||
on:click={() => openQSODetailModal(qso)}
|
||||
on:keydown={(e) => e.key === 'Enter' && openQSODetailModal(qso)}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<span class="callsign">{qso.callsign}</span>
|
||||
<span class="mode">{qso.mode}</span>
|
||||
</div>
|
||||
@@ -267,6 +353,188 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- QSO Detail Modal -->
|
||||
{#if showQSODetailModal && selectedQSO}
|
||||
<div class="modal-backdrop" on:click={closeQSODetailModal} on:keydown={(e) => e.key === 'Escape' && closeQSODetailModal()} role="dialog" aria-modal="true">
|
||||
<div class="modal-content" on:click|stopPropagation>
|
||||
<div class="modal-header">
|
||||
<h2>QSO Details</h2>
|
||||
<button class="modal-close" on:click={closeQSODetailModal} aria-label="Close modal">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
{#if loadingQSO}
|
||||
<div class="loading-modal">Loading QSO details...</div>
|
||||
{:else}
|
||||
<div class="qso-detail-section">
|
||||
<h3>Basic Information</h3>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Callsign:</span>
|
||||
<span class="detail-value">{selectedQSO.callsign}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Date:</span>
|
||||
<span class="detail-value">{formatDate(selectedQSO.qsoDate)}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Time:</span>
|
||||
<span class="detail-value">{formatTime(selectedQSO.timeOn)}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Band:</span>
|
||||
<span class="detail-value">{selectedQSO.band || '-'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Mode:</span>
|
||||
<span class="detail-value">{selectedQSO.mode || '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="qso-detail-section">
|
||||
<h3>Location</h3>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Entity:</span>
|
||||
<span class="detail-value">{selectedQSO.entity || '-'}</span>
|
||||
</div>
|
||||
{#if selectedQSO.entityId}
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">DXCC ID:</span>
|
||||
<span class="detail-value">{selectedQSO.entityId}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Grid Square:</span>
|
||||
<span class="detail-value">{selectedQSO.grid || '-'}</span>
|
||||
</div>
|
||||
{#if selectedQSO.gridSource}
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Grid Source:</span>
|
||||
<span class="detail-value">{selectedQSO.gridSource}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Continent:</span>
|
||||
<span class="detail-value">{selectedQSO.continent || '-'}</span>
|
||||
</div>
|
||||
{#if selectedQSO.cqZone}
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">CQ Zone:</span>
|
||||
<span class="detail-value">{selectedQSO.cqZone}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if selectedQSO.ituZone}
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">ITU Zone:</span>
|
||||
<span class="detail-value">{selectedQSO.ituZone}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if selectedQSO.state}
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">State:</span>
|
||||
<span class="detail-value">{selectedQSO.state}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if selectedQSO.county}
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">County:</span>
|
||||
<span class="detail-value">{selectedQSO.county}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if selectedQSO.satName}
|
||||
<div class="qso-detail-section">
|
||||
<h3>Satellite</h3>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Satellite:</span>
|
||||
<span class="detail-value">{selectedQSO.satName}</span>
|
||||
</div>
|
||||
{#if selectedQSO.satMode}
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Mode:</span>
|
||||
<span class="detail-value">{selectedQSO.satMode}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="qso-detail-section">
|
||||
<h3>DOK Information</h3>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Partner's DOK:</span>
|
||||
<span class="detail-value">{selectedQSO.darcDok || '-'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">My DOK:</span>
|
||||
<span class="detail-value">{selectedQSO.myDarcDok || '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="qso-detail-section">
|
||||
<h3>Confirmation Status</h3>
|
||||
<div class="confirmation-details">
|
||||
<div class="confirmation-service">
|
||||
<h4>LoTW</h4>
|
||||
<div class="confirmation-status-item">
|
||||
<span class="detail-label">Status:</span>
|
||||
<span class="status-badge {getConfirmationStatus(selectedQSO.lotwQslRstatus).class}">
|
||||
{getConfirmationStatus(selectedQSO.lotwQslRstatus).label}
|
||||
</span>
|
||||
</div>
|
||||
{#if selectedQSO.lotwQslRdate}
|
||||
<div class="confirmation-status-item">
|
||||
<span class="detail-label">Confirmed:</span>
|
||||
<span class="detail-value">{formatDate(selectedQSO.lotwQslRdate)}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if selectedQSO.lotwSyncedAt}
|
||||
<div class="confirmation-status-item">
|
||||
<span class="detail-label">Last Synced:</span>
|
||||
<span class="detail-value">{new Date(selectedQSO.lotwSyncedAt).toLocaleString()}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="confirmation-service">
|
||||
<h4>DCL</h4>
|
||||
<div class="confirmation-status-item">
|
||||
<span class="detail-label">Status:</span>
|
||||
<span class="status-badge {getConfirmationStatus(selectedQSO.dclQslRstatus).class}">
|
||||
{getConfirmationStatus(selectedQSO.dclQslRstatus).label}
|
||||
</span>
|
||||
</div>
|
||||
{#if selectedQSO.dclQslRdate}
|
||||
<div class="confirmation-status-item">
|
||||
<span class="detail-label">Confirmed:</span>
|
||||
<span class="detail-value">{formatDate(selectedQSO.dclQslRdate)}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="qso-detail-section meta-info">
|
||||
<span class="meta-label">QSO ID:</span>
|
||||
<span class="meta-value">{selectedQSO.id}</span>
|
||||
{#if selectedQSO.createdAt}
|
||||
<span class="meta-label">Created:</span>
|
||||
<span class="meta-value">{new Date(selectedQSO.createdAt).toLocaleString()}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
@@ -498,4 +766,224 @@
|
||||
.btn:hover {
|
||||
background-color: #357abd;
|
||||
}
|
||||
|
||||
/* QSO Detail Modal Styles */
|
||||
.qso-entry {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.qso-entry:hover {
|
||||
background-color: #f0f7ff;
|
||||
}
|
||||
|
||||
.qso-entry:focus {
|
||||
outline: 2px solid #4a90e2;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
/* Modal Backdrop */
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Modal Content */
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
max-width: 700px;
|
||||
width: 100%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Modal Header */
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 2rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #666;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
background-color: #f0f0f0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Modal Body */
|
||||
.modal-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
.loading-modal {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Detail Sections */
|
||||
.qso-detail-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.qso-detail-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.qso-detail-section h3 {
|
||||
font-size: 1.1rem;
|
||||
color: #4a90e2;
|
||||
margin: 0 0 1rem 0;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
}
|
||||
|
||||
/* Detail Grid */
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 0.75rem;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 0.95rem;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Confirmation Details */
|
||||
.confirmation-details {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.confirmation-service h4 {
|
||||
font-size: 1rem;
|
||||
color: #333;
|
||||
margin: 0 0 1rem 0;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.confirmation-status-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
/* Status Badges */
|
||||
.status-badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-badge.confirmed {
|
||||
background-color: #4a90e2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-badge.not-confirmed,
|
||||
.status-badge.no-data {
|
||||
background-color: #e0e0e0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.status-badge.unknown {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
/* Meta Info */
|
||||
.meta-info {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
padding: 1rem;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
color: #666;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
color: #333;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* Scrollbar Styling for Modal */
|
||||
.modal-content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.modal-content::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
.modal-content::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.modal-content::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user