feat: add import log showing synced QSOs

Add import log display that shows QSOs imported via LoTW/DCL sync.
Backend now tracks added/updated QSOs (callsign, date, band, mode)
and returns them in sync result. Frontend displays tables showing
new and updated QSOs after sync completes.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-17 19:39:15 +01:00
parent f7d62ed247
commit 3592dbb4fb
3 changed files with 166 additions and 0 deletions

View File

@@ -193,6 +193,8 @@ export async function syncQSOs(userId, dclApiKey, sinceDate = null, jobId = null
let addedCount = 0;
let updatedCount = 0;
const errors = [];
const addedQSOs = [];
const updatedQSOs = [];
for (let i = 0; i < adifQSOs.length; i++) {
const adifQSO = adifQSOs[i];
@@ -230,10 +232,24 @@ export async function syncQSOs(userId, dclApiKey, sinceDate = null, jobId = null
})
.where(eq(qsos.id, existing[0].id));
updatedCount++;
// Track updated QSO (CALL and DATE)
updatedQSOs.push({
callsign: dbQSO.callsign,
date: dbQSO.qsoDate,
band: dbQSO.band,
mode: dbQSO.mode,
});
} else {
// Insert new QSO
await db.insert(qsos).values(dbQSO);
addedCount++;
// Track added QSO (CALL and DATE)
addedQSOs.push({
callsign: dbQSO.callsign,
date: dbQSO.qsoDate,
band: dbQSO.band,
mode: dbQSO.mode,
});
}
// Update job progress every 10 QSOs
@@ -258,6 +274,8 @@ export async function syncQSOs(userId, dclApiKey, sinceDate = null, jobId = null
total: adifQSOs.length,
added: addedCount,
updated: updatedCount,
addedQSOs,
updatedQSOs,
confirmed: adifQSOs.filter(q => q.dcl_qsl_rcvd === 'Y').length,
errors: errors.length > 0 ? errors : undefined,
};

View File

@@ -223,6 +223,8 @@ export async function syncQSOs(userId, lotwUsername, lotwPassword, sinceDate = n
let addedCount = 0;
let updatedCount = 0;
const errors = [];
const addedQSOs = [];
const updatedQSOs = [];
for (let i = 0; i < adifQSOs.length; i++) {
const qsoData = adifQSOs[i];
@@ -254,9 +256,23 @@ export async function syncQSOs(userId, lotwUsername, lotwPassword, sinceDate = n
})
.where(eq(qsos.id, existing[0].id));
updatedCount++;
// Track updated QSO (CALL and DATE)
updatedQSOs.push({
callsign: dbQSO.callsign,
date: dbQSO.qsoDate,
band: dbQSO.band,
mode: dbQSO.mode,
});
} else {
await db.insert(qsos).values(dbQSO);
addedCount++;
// Track added QSO (CALL and DATE)
addedQSOs.push({
callsign: dbQSO.callsign,
date: dbQSO.qsoDate,
band: dbQSO.band,
mode: dbQSO.mode,
});
}
// Update job progress every 10 QSOs
@@ -279,6 +295,8 @@ export async function syncQSOs(userId, lotwUsername, lotwPassword, sinceDate = n
total: adifQSOs.length,
added: addedCount,
updated: updatedCount,
addedQSOs,
updatedQSOs,
errors: errors.length > 0 ? errors : undefined,
};
}

View File

@@ -306,6 +306,68 @@
{/if}
<button on:click={() => syncResult = null} class="btn-small">Dismiss</button>
</div>
{#if syncResult.success && (syncResult.addedQSOs?.length > 0 || syncResult.updatedQSOs?.length > 0)}
<div class="import-log">
<h3>Import Log</h3>
{#if syncResult.addedQSOs && syncResult.addedQSOs.length > 0}
<div class="log-section">
<h4>New QSOs ({syncResult.addedQSOs.length})</h4>
<div class="log-table-container">
<table class="log-table">
<thead>
<tr>
<th>Callsign</th>
<th>Date</th>
<th>Band</th>
<th>Mode</th>
</tr>
</thead>
<tbody>
{#each syncResult.addedQSOs as qso}
<tr>
<td class="callsign">{qso.callsign}</td>
<td>{formatDate(qso.date)}</td>
<td>{qso.band || '-'}</td>
<td>{qso.mode || '-'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
{/if}
{#if syncResult.updatedQSOs && syncResult.updatedQSOs.length > 0}
<div class="log-section">
<h4>Updated QSOs ({syncResult.updatedQSOs.length})</h4>
<div class="log-table-container">
<table class="log-table">
<thead>
<tr>
<th>Callsign</th>
<th>Date</th>
<th>Band</th>
<th>Mode</th>
</tr>
</thead>
<tbody>
{#each syncResult.updatedQSOs as qso}
<tr>
<td class="callsign">{qso.callsign}</td>
<td>{formatDate(qso.date)}</td>
<td>{qso.band || '-'}</td>
<td>{qso.mode || '-'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
{/if}
</div>
{/if}
{/if}
{#if showDeleteConfirm}
@@ -845,4 +907,72 @@
opacity: 0.5;
cursor: not-allowed;
}
.import-log {
background: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 1.5rem;
margin-top: 1rem;
}
.import-log h3 {
margin: 0 0 1rem 0;
color: #333;
font-size: 1.25rem;
}
.log-section {
margin-bottom: 1.5rem;
}
.log-section:last-child {
margin-bottom: 0;
}
.log-section h4 {
margin: 0 0 0.75rem 0;
color: #555;
font-size: 1rem;
font-weight: 600;
}
.log-table-container {
overflow-x: auto;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.log-table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
.log-table th,
.log-table td {
padding: 0.5rem 0.75rem;
text-align: left;
border-bottom: 1px solid #e0e0e0;
}
.log-table th {
background-color: #f8f9fa;
font-weight: 600;
color: #333;
font-size: 0.85rem;
}
.log-table tr:last-child td {
border-bottom: none;
}
.log-table tr:hover {
background-color: #f8f9fa;
}
.log-table .callsign {
font-weight: 600;
color: #4a90e2;
}
</style>