Compare commits
2 Commits
287d1fe972
...
322ccafcae
| Author | SHA1 | Date | |
|---|---|---|---|
|
322ccafcae
|
|||
|
c982dcd0fe
|
13
award-definitions/dld.json
Normal file
13
award-definitions/dld.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "dld",
|
||||
"name": "DLD",
|
||||
"description": "Deutschland Diplom - Confirm 100 unique DOKs on different bands/modes",
|
||||
"caption": "Contact and confirm stations with 100 unique DOKs (DARC Ortsverband Kennung) on different band/mode combinations. Each unique DOK on a unique band/mode counts as one point. Only DCL-confirmed QSOs with valid DOK information count toward this award.",
|
||||
"category": "darc",
|
||||
"rules": {
|
||||
"type": "dok",
|
||||
"target": 100,
|
||||
"confirmationType": "dcl",
|
||||
"displayField": "darcDok"
|
||||
}
|
||||
}
|
||||
@@ -131,7 +131,8 @@ Defines the database structure using Drizzle ORM schema builder.
|
||||
- Award progress calculation
|
||||
- Entity breakdown by band/mode
|
||||
- Confirmation status tracking (LoTW, DCL)
|
||||
- DXCC, WAS, VUCC award support
|
||||
- DXCC, WAS, VUCC, DLD award support
|
||||
- DOK-based award calculation with DCL confirmation
|
||||
|
||||
**Job Queue Service** (`src/backend/services/job-queue.service.js`)
|
||||
- Background job management
|
||||
@@ -194,7 +195,9 @@ award/
|
||||
│ ├── dxcc-cw.json # DXCC CW-specific award
|
||||
│ ├── was.json # WAS award configurations
|
||||
│ ├── vucc-sat.json # VUCC Satellite award
|
||||
│ └── sat-rs44.json # Satellite RS-44 award
|
||||
│ ├── sat-rs44.json # Satellite RS-44 award
|
||||
│ ├── special-stations.json # Special event stations award
|
||||
│ └── dld.json # DLD (Deutschland Diplom) award
|
||||
│
|
||||
├── drizzle/ # Database migrations
|
||||
│ └── 0000_init.sql # Initial schema
|
||||
@@ -288,8 +291,12 @@ award/
|
||||
county: text
|
||||
satName: text (satellite name)
|
||||
satMode: text (satellite mode)
|
||||
myDarcDok: text (user's DOK - DARC Ortsverband Kennung)
|
||||
darcDok: text (QSO partner's DOK)
|
||||
lotwQslRdate: text (LoTW confirmation date)
|
||||
lotwQslRstatus: text ('Y', 'N', '?')
|
||||
dclQslRdate: text (DCL confirmation date)
|
||||
dclQslRstatus: text ('Y', 'N', '?')
|
||||
lotwSyncedAt: timestamp
|
||||
createdAt: timestamp
|
||||
}
|
||||
@@ -361,6 +368,7 @@ Each award is defined with the following structure:
|
||||
| `dxcc` | DXCC entities (countries) | `entityId` |
|
||||
| `state` | US States, Canadian provinces | `state` |
|
||||
| `grid` | Maidenhead grid squares | `grid` |
|
||||
| `dok` | DARC Ortsverband Kennung (German local clubs) | `darcDok` |
|
||||
|
||||
### Filter Operators
|
||||
|
||||
@@ -380,6 +388,7 @@ Each award is defined with the following structure:
|
||||
- **`was`**: Worked All States awards
|
||||
- **`vucc`**: VHF/UHF Century Club awards
|
||||
- **`satellite`**: Satellite-specific awards
|
||||
- **`darc`**: DARC (German Amateur Radio Club) awards
|
||||
|
||||
---
|
||||
|
||||
@@ -603,6 +612,55 @@ confirmed = COUNT(DISTINCT callsign
|
||||
|
||||
---
|
||||
|
||||
### 6. DLD (Deutschland Diplom)
|
||||
|
||||
**File**: `award-definitions/dld.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "dld",
|
||||
"name": "DLD",
|
||||
"description": "Deutschland Diplom - Confirm 100 unique DOKs on different bands/modes",
|
||||
"caption": "Contact and confirm stations with 100 unique DOKs (DARC Ortsverband Kennung) on different band/mode combinations. Each unique DOK on a unique band/mode counts as one point. Only DCL-confirmed QSOs with valid DOK information count toward this award.",
|
||||
"category": "darc",
|
||||
"rules": {
|
||||
"type": "dok",
|
||||
"target": 100,
|
||||
"confirmationType": "dcl",
|
||||
"displayField": "darcDok"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Explanation:**
|
||||
- Counts unique (DOK, band, mode) combinations
|
||||
- Only QSOs with valid `darcDok` values are counted
|
||||
- Only DCL-confirmed QSOs (`dclQslRstatus = 'Y'`) count toward the award
|
||||
- Target: 100 unique DOKs on different band/mode combinations
|
||||
- Each unique DOK on a unique band/mode combination counts as one point
|
||||
- Example: Working DOK F03 on 20m CW and 40m SSB counts as 2 points
|
||||
|
||||
**Progress Calculation:**
|
||||
```javascript
|
||||
worked = COUNT(DISTINCT darcDok
|
||||
WHERE darcDok IS NOT NULL)
|
||||
confirmed = COUNT(DISTINCT darcDok
|
||||
WHERE darcDok IS NOT NULL AND dclQslRstatus = 'Y')
|
||||
```
|
||||
|
||||
**Example DOKs:**
|
||||
- `"F03"` - Ortsverband Frankfurt am Main
|
||||
- `"P30"` - Ortsverband München
|
||||
- `"G20"` - Ortsverband Köln
|
||||
- DOKs consist of a letter (district) and two digits (local club)
|
||||
|
||||
**Confirmation:**
|
||||
- Only DCL (DARC Community Logbook) confirmations count
|
||||
- LoTW confirmations do not count toward this award
|
||||
- This is a DARC-specific award using DARC's confirmation system
|
||||
|
||||
---
|
||||
|
||||
### Advanced Filter Examples
|
||||
|
||||
#### Multiple Conditions (AND)
|
||||
@@ -776,6 +834,7 @@ The award system is designed for extensibility. Planned enhancements:
|
||||
- CQ Zones
|
||||
- ITU Zones
|
||||
- Counties
|
||||
- DOK (German DARC Ortsverband Kennung) ✓ **Implemented**
|
||||
|
||||
2. **Advanced Filters**:
|
||||
- Date ranges
|
||||
@@ -812,7 +871,9 @@ When adding new awards or modifying the award system:
|
||||
## Resources
|
||||
|
||||
- [ARRL LoTW](https://lotw.arrl.org/)
|
||||
- [DARC Community Logbook (DCL)](https://dcl.darc.de/)
|
||||
- [ADIF Specification](https://adif.org/)
|
||||
- [DXCC List](https://www.arrl.org/dxcc)
|
||||
- [VUCC Program](https://www.arrl.org/vucc)
|
||||
- [WAS Award](https://www.arrl.org/was)
|
||||
- [DLD Award](https://www.darc.de/der-club/referate/conteste/dld/)
|
||||
|
||||
@@ -26,6 +26,7 @@ function loadAwardDefinitions() {
|
||||
'vucc-sat.json',
|
||||
'sat-rs44.json',
|
||||
'special-stations.json',
|
||||
'dld.json',
|
||||
];
|
||||
|
||||
for (const file of files) {
|
||||
@@ -108,6 +109,11 @@ export async function calculateAwardProgress(userId, award, options = {}) {
|
||||
hasFilters: !!rules.filters,
|
||||
});
|
||||
|
||||
// Handle DOK-based awards (DLD)
|
||||
if (rules.type === 'dok') {
|
||||
return calculateDOKAwardProgress(userId, award, { includeDetails });
|
||||
}
|
||||
|
||||
// Handle point-based awards
|
||||
if (rules.type === 'points') {
|
||||
return calculatePointsAwardProgress(userId, award, { includeDetails });
|
||||
@@ -156,6 +162,111 @@ export async function calculateAwardProgress(userId, award, options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate progress for DOK-based awards (DLD)
|
||||
* Counts unique (DOK, band, mode) combinations with DCL confirmation
|
||||
* @param {number} userId - User ID
|
||||
* @param {Object} award - Award definition
|
||||
* @param {Object} options - Options
|
||||
* @param {boolean} options.includeDetails - Include detailed entity breakdown
|
||||
*/
|
||||
async function calculateDOKAwardProgress(userId, award, options = {}) {
|
||||
const { includeDetails = false } = options;
|
||||
const { rules } = award;
|
||||
const { target, displayField } = rules;
|
||||
|
||||
logger.debug('Calculating DOK-based award progress', { userId, awardId: award.id, target });
|
||||
|
||||
// Get all QSOs for user
|
||||
const allQSOs = await db
|
||||
.select()
|
||||
.from(qsos)
|
||||
.where(eq(qsos.userId, userId));
|
||||
|
||||
logger.debug('Total QSOs for user', { count: allQSOs.length });
|
||||
|
||||
// Track unique (DOK, band, mode) combinations
|
||||
const dokCombinations = new Map(); // Key: "DOK/band/mode" -> detail object
|
||||
|
||||
for (const qso of allQSOs) {
|
||||
const dok = qso.darcDok;
|
||||
if (!dok) continue; // Skip QSOs without DOK
|
||||
|
||||
const band = qso.band || 'Unknown';
|
||||
const mode = qso.mode || 'Unknown';
|
||||
const combinationKey = `${dok}/${band}/${mode}`;
|
||||
|
||||
// Initialize combination if not exists
|
||||
if (!dokCombinations.has(combinationKey)) {
|
||||
dokCombinations.set(combinationKey, {
|
||||
entity: dok,
|
||||
entityId: null,
|
||||
entityName: dok,
|
||||
band,
|
||||
mode,
|
||||
worked: false,
|
||||
confirmed: false,
|
||||
qsoDate: qso.qsoDate,
|
||||
dclQslRdate: null,
|
||||
});
|
||||
}
|
||||
|
||||
const detail = dokCombinations.get(combinationKey);
|
||||
detail.worked = true;
|
||||
|
||||
// Check for DCL confirmation
|
||||
if (qso.dclQslRstatus === 'Y') {
|
||||
if (!detail.confirmed) {
|
||||
detail.confirmed = true;
|
||||
detail.dclQslRdate = qso.dclQslRdate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const workedDOKs = new Set();
|
||||
const confirmedDOKs = new Set();
|
||||
|
||||
for (const [key, detail] of dokCombinations) {
|
||||
const dok = detail.entity;
|
||||
workedDOKs.add(dok);
|
||||
if (detail.confirmed) {
|
||||
confirmedDOKs.add(dok);
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug('DOK award progress', {
|
||||
workedDOKs: workedDOKs.size,
|
||||
confirmedDOKs: confirmedDOKs.size,
|
||||
target,
|
||||
});
|
||||
|
||||
// Base result
|
||||
const result = {
|
||||
worked: workedDOKs.size,
|
||||
confirmed: confirmedDOKs.size,
|
||||
target: target || 0,
|
||||
percentage: target ? Math.round((confirmedDOKs.size / target) * 100) : 0,
|
||||
workedEntities: Array.from(workedDOKs),
|
||||
confirmedEntities: Array.from(confirmedDOKs),
|
||||
};
|
||||
|
||||
// Add details if requested
|
||||
if (includeDetails) {
|
||||
result.award = {
|
||||
id: award.id,
|
||||
name: award.name,
|
||||
description: award.description,
|
||||
caption: award.caption,
|
||||
target: target || 0,
|
||||
};
|
||||
result.entities = Array.from(dokCombinations.values());
|
||||
result.total = result.entities.length;
|
||||
result.confirmed = result.entities.filter((e) => e.confirmed).length;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate progress for point-based awards
|
||||
* countMode determines how points are counted:
|
||||
@@ -510,6 +621,11 @@ export async function getAwardEntityBreakdown(userId, awardId) {
|
||||
};
|
||||
}
|
||||
|
||||
// Handle DOK-based awards - use the dedicated function
|
||||
if (rules.type === 'dok') {
|
||||
return await calculateDOKAwardProgress(userId, award, { includeDetails: true });
|
||||
}
|
||||
|
||||
// Handle point-based awards - use the unified function
|
||||
if (rules.type === 'points') {
|
||||
return await calculatePointsAwardProgress(userId, award, { includeDetails: true });
|
||||
|
||||
Reference in New Issue
Block a user