From b422c204637374ad3f35c52c7ba922c06c728a78 Mon Sep 17 00:00:00 2001 From: Joerg Date: Mon, 19 Jan 2026 07:39:33 +0100 Subject: [PATCH] Filters --- src/backend/index.js | 5 +++ src/backend/services/dcl.service.js | 20 ++++++++++- src/backend/services/lotw.service.js | 31 +++++++++++++++- src/frontend/src/routes/qsos/+page.svelte | 43 +++++++++++++++++++++-- 4 files changed, 95 insertions(+), 4 deletions(-) diff --git a/src/backend/index.js b/src/backend/index.js index fad1b15..bca86c4 100644 --- a/src/backend/index.js +++ b/src/backend/index.js @@ -450,6 +450,7 @@ const app = new Elysia() * GET /api/qsos * Get user's QSOs (requires authentication) * Supports pagination: ?page=1&limit=100 + * Supports filters: band, mode, confirmed, confirmationType (all, lotw, dcl, both, none), search */ .get('/api/qsos', async ({ user, query, set }) => { if (!user) { @@ -462,6 +463,10 @@ const app = new Elysia() if (query.band) filters.band = query.band; if (query.mode) filters.mode = query.mode; if (query.confirmed) filters.confirmed = query.confirmed === 'true'; + if (query.confirmationType && query.confirmationType !== 'all') { + filters.confirmationType = query.confirmationType; + } + if (query.search) filters.search = query.search; // Pagination options const options = { diff --git a/src/backend/services/dcl.service.js b/src/backend/services/dcl.service.js index ce76ac7..d06b729 100644 --- a/src/backend/services/dcl.service.js +++ b/src/backend/services/dcl.service.js @@ -148,6 +148,7 @@ function convertQSODatabaseFormat(adifQSO, userId) { mode: normalizeMode(adifQSO.mode), freq: adifQSO.freq ? parseInt(adifQSO.freq) : null, freqRx: adifQSO.freq_rx ? parseInt(adifQSO.freq_rx) : null, + // DCL may or may not include DXCC fields - use them if available entity: adifQSO.country || adifQSO.dxcc_country || '', entityId: adifQSO.dxcc ? parseInt(adifQSO.dxcc) : null, grid: adifQSO.gridsquare || '', @@ -252,7 +253,6 @@ export async function syncQSOs(userId, dclApiKey, sinceDate = null, jobId = null if (dataChanged) { // Update existing QSO with changed DCL confirmation and DOK data - // Only update DOK/grid fields if DCL actually sent values (non-empty) const updateData = { dclQslRdate: dbQSO.dclQslRdate, dclQslRstatus: dbQSO.dclQslRstatus, @@ -268,6 +268,24 @@ export async function syncQSOs(userId, dclApiKey, sinceDate = null, jobId = null updateData.gridSource = dbQSO.gridSource; } + // DXCC priority: LoTW > DCL + // Only update entity fields from DCL if: + // 1. QSO is NOT LoTW confirmed, AND + // 2. DCL actually sent entity data, AND + // 3. Current entity is missing + const hasLoTWConfirmation = existingQSO.lotwQslRstatus === 'Y'; + const hasDCLData = dbQSO.entity || dbQSO.entityId; + const missingEntity = !existingQSO.entity || existingQSO.entity === ''; + + if (!hasLoTWConfirmation && hasDCLData && missingEntity) { + // Fill in entity data from DCL (only if DCL provides it) + if (dbQSO.entity) updateData.entity = dbQSO.entity; + if (dbQSO.entityId) updateData.entityId = dbQSO.entityId; + if (dbQSO.continent) updateData.continent = dbQSO.continent; + if (dbQSO.cqZone) updateData.cqZone = dbQSO.cqZone; + if (dbQSO.ituZone) updateData.ituZone = dbQSO.ituZone; + } + await db .update(qsos) .set(updateData) diff --git a/src/backend/services/lotw.service.js b/src/backend/services/lotw.service.js index 3318c1c..2251871 100644 --- a/src/backend/services/lotw.service.js +++ b/src/backend/services/lotw.service.js @@ -1,6 +1,6 @@ import { db, logger } from '../config.js'; import { qsos } from '../db/schema/index.js'; -import { max, sql, eq, and, desc } from 'drizzle-orm'; +import { max, sql, eq, and, or, desc, like } from 'drizzle-orm'; import { updateJobProgress } from './job-queue.service.js'; import { parseADIF, normalizeBand, normalizeMode } from '../utils/adif-parser.js'; @@ -328,6 +328,35 @@ export async function getUserQSOs(userId, filters = {}, options = {}) { if (filters.mode) conditions.push(eq(qsos.mode, filters.mode)); if (filters.confirmed) conditions.push(eq(qsos.lotwQslRstatus, 'Y')); + // Confirmation type filter: lotw, dcl, both, none + if (filters.confirmationType) { + if (filters.confirmationType === 'lotw') { + conditions.push(eq(qsos.lotwQslRstatus, 'Y')); + } else if (filters.confirmationType === 'dcl') { + conditions.push(eq(qsos.dclQslRstatus, 'Y')); + } else if (filters.confirmationType === 'both') { + conditions.push(and( + eq(qsos.lotwQslRstatus, 'Y'), + eq(qsos.dclQslRstatus, 'Y') + )); + } else if (filters.confirmationType === 'none') { + conditions.push(and( + sql`${qsos.lotwQslRstatus} IS NULL OR ${qsos.lotwQslRstatus} != 'Y'`, + sql`${qsos.dclQslRstatus} IS NULL OR ${qsos.dclQslRstatus} != 'Y'` + )); + } + } + + // Search filter: callsign, entity, or grid + if (filters.search) { + const searchTerm = `%${filters.search}%`; + conditions.push(or( + like(qsos.callsign, searchTerm), + like(qsos.entity, searchTerm), + like(qsos.grid, searchTerm) + )); + } + const allResults = await db.select().from(qsos).where(and(...conditions)); const totalCount = allResults.length; diff --git a/src/frontend/src/routes/qsos/+page.svelte b/src/frontend/src/routes/qsos/+page.svelte index 72591ba..19e0bb1 100644 --- a/src/frontend/src/routes/qsos/+page.svelte +++ b/src/frontend/src/routes/qsos/+page.svelte @@ -35,7 +35,9 @@ let filters = { band: '', - mode: '' + mode: '', + confirmationType: 'all', + search: '' }; // Load QSOs on mount @@ -65,6 +67,10 @@ const activeFilters = {}; if (filters.band) activeFilters.band = filters.band; if (filters.mode) activeFilters.mode = filters.mode; + if (filters.confirmationType && filters.confirmationType !== 'all') { + activeFilters.confirmationType = filters.confirmationType; + } + if (filters.search) activeFilters.search = filters.search; activeFilters.page = currentPage; activeFilters.limit = pageSize; @@ -261,7 +267,9 @@ currentPage = 1; filters = { band: '', - mode: '' + mode: '', + confirmationType: 'all', + search: '' }; loadQSOs(); } @@ -534,6 +542,14 @@

Filters

+ e.key === 'Enter' && applyFilters()} + /> + + +
@@ -778,6 +802,21 @@ font-size: 0.9rem; } + .search-input { + flex: 1; + min-width: 200px; + padding: 0.5rem; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 0.9rem; + } + + .search-input:focus { + outline: none; + border-color: #4a90e2; + box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2); + } + .checkbox-label { display: flex; align-items: center;