Filters
This commit is contained in:
@@ -450,6 +450,7 @@ const app = new Elysia()
|
|||||||
* GET /api/qsos
|
* GET /api/qsos
|
||||||
* Get user's QSOs (requires authentication)
|
* Get user's QSOs (requires authentication)
|
||||||
* Supports pagination: ?page=1&limit=100
|
* 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 }) => {
|
.get('/api/qsos', async ({ user, query, set }) => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@@ -462,6 +463,10 @@ const app = new Elysia()
|
|||||||
if (query.band) filters.band = query.band;
|
if (query.band) filters.band = query.band;
|
||||||
if (query.mode) filters.mode = query.mode;
|
if (query.mode) filters.mode = query.mode;
|
||||||
if (query.confirmed) filters.confirmed = query.confirmed === 'true';
|
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
|
// Pagination options
|
||||||
const options = {
|
const options = {
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ function convertQSODatabaseFormat(adifQSO, userId) {
|
|||||||
mode: normalizeMode(adifQSO.mode),
|
mode: normalizeMode(adifQSO.mode),
|
||||||
freq: adifQSO.freq ? parseInt(adifQSO.freq) : null,
|
freq: adifQSO.freq ? parseInt(adifQSO.freq) : null,
|
||||||
freqRx: adifQSO.freq_rx ? parseInt(adifQSO.freq_rx) : 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 || '',
|
entity: adifQSO.country || adifQSO.dxcc_country || '',
|
||||||
entityId: adifQSO.dxcc ? parseInt(adifQSO.dxcc) : null,
|
entityId: adifQSO.dxcc ? parseInt(adifQSO.dxcc) : null,
|
||||||
grid: adifQSO.gridsquare || '',
|
grid: adifQSO.gridsquare || '',
|
||||||
@@ -252,7 +253,6 @@ export async function syncQSOs(userId, dclApiKey, sinceDate = null, jobId = null
|
|||||||
|
|
||||||
if (dataChanged) {
|
if (dataChanged) {
|
||||||
// Update existing QSO with changed DCL confirmation and DOK data
|
// Update existing QSO with changed DCL confirmation and DOK data
|
||||||
// Only update DOK/grid fields if DCL actually sent values (non-empty)
|
|
||||||
const updateData = {
|
const updateData = {
|
||||||
dclQslRdate: dbQSO.dclQslRdate,
|
dclQslRdate: dbQSO.dclQslRdate,
|
||||||
dclQslRstatus: dbQSO.dclQslRstatus,
|
dclQslRstatus: dbQSO.dclQslRstatus,
|
||||||
@@ -268,6 +268,24 @@ export async function syncQSOs(userId, dclApiKey, sinceDate = null, jobId = null
|
|||||||
updateData.gridSource = dbQSO.gridSource;
|
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
|
await db
|
||||||
.update(qsos)
|
.update(qsos)
|
||||||
.set(updateData)
|
.set(updateData)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { db, logger } from '../config.js';
|
import { db, logger } from '../config.js';
|
||||||
import { qsos } from '../db/schema/index.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 { updateJobProgress } from './job-queue.service.js';
|
||||||
import { parseADIF, normalizeBand, normalizeMode } from '../utils/adif-parser.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.mode) conditions.push(eq(qsos.mode, filters.mode));
|
||||||
if (filters.confirmed) conditions.push(eq(qsos.lotwQslRstatus, 'Y'));
|
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 allResults = await db.select().from(qsos).where(and(...conditions));
|
||||||
const totalCount = allResults.length;
|
const totalCount = allResults.length;
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,9 @@
|
|||||||
|
|
||||||
let filters = {
|
let filters = {
|
||||||
band: '',
|
band: '',
|
||||||
mode: ''
|
mode: '',
|
||||||
|
confirmationType: 'all',
|
||||||
|
search: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load QSOs on mount
|
// Load QSOs on mount
|
||||||
@@ -65,6 +67,10 @@
|
|||||||
const activeFilters = {};
|
const activeFilters = {};
|
||||||
if (filters.band) activeFilters.band = filters.band;
|
if (filters.band) activeFilters.band = filters.band;
|
||||||
if (filters.mode) activeFilters.mode = filters.mode;
|
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.page = currentPage;
|
||||||
activeFilters.limit = pageSize;
|
activeFilters.limit = pageSize;
|
||||||
@@ -261,7 +267,9 @@
|
|||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
filters = {
|
filters = {
|
||||||
band: '',
|
band: '',
|
||||||
mode: ''
|
mode: '',
|
||||||
|
confirmationType: 'all',
|
||||||
|
search: ''
|
||||||
};
|
};
|
||||||
loadQSOs();
|
loadQSOs();
|
||||||
}
|
}
|
||||||
@@ -534,6 +542,14 @@
|
|||||||
<div class="filters">
|
<div class="filters">
|
||||||
<h3>Filters</h3>
|
<h3>Filters</h3>
|
||||||
<div class="filter-row">
|
<div class="filter-row">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={filters.search}
|
||||||
|
placeholder="Search callsign, entity, grid..."
|
||||||
|
class="search-input"
|
||||||
|
on:keydown={(e) => e.key === 'Enter' && applyFilters()}
|
||||||
|
/>
|
||||||
|
|
||||||
<select bind:value={filters.band} on:change={applyFilters}>
|
<select bind:value={filters.band} on:change={applyFilters}>
|
||||||
<option value="">All Bands</option>
|
<option value="">All Bands</option>
|
||||||
{#each bands as band}
|
{#each bands as band}
|
||||||
@@ -548,6 +564,14 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select bind:value={filters.confirmationType} on:change={applyFilters} class="confirmation-filter">
|
||||||
|
<option value="all">All Confirmation</option>
|
||||||
|
<option value="lotw">LoTW Only</option>
|
||||||
|
<option value="dcl">DCL Only</option>
|
||||||
|
<option value="both">Both Confirmed</option>
|
||||||
|
<option value="none">Not Confirmed</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
<button class="btn btn-secondary" on:click={clearFilters}>Clear</button>
|
<button class="btn btn-secondary" on:click={clearFilters}>Clear</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -778,6 +802,21 @@
|
|||||||
font-size: 0.9rem;
|
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 {
|
.checkbox-label {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user