feat: implement award definition editor with safety validation
Add comprehensive admin-only UI for managing award definitions stored as JSON files. Backend changes: - Add awards-admin.service.js with file operations and validation - Add clearAwardCache() function to invalidate in-memory cache after changes - Add API routes: GET/POST/PUT/DELETE /api/admin/awards, POST /api/admin/awards/:id/test - Support testing unsaved awards by passing award definition directly Frontend changes: - Add awards list view at /admin/awards - Add create form at /admin/awards/create with safety checks for: - Impossible filter combinations (e.g., mode=CW AND mode=SSB) - Redundant filters and mode groups - Logical contradictions (e.g., satellite_only with HF-only bands) - Duplicate callsigns, empty mode groups, etc. - Add edit form at /admin/awards/[id] with same validation - Add FilterBuilder component for nested filter structures - Add TestAwardModal with deep validation and test calculation - Add Awards tab to admin dashboard Safety validation includes: - Schema validation (required fields, types, formats) - Business rule validation (valid rule types, operators, bands, modes) - Cross-field validation (filter contradictions, allowed_bands conflicts) - Edge case detection (complex filters, impossible targets) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -44,6 +44,14 @@ import {
|
||||
getAwardProgressDetails,
|
||||
getAwardEntityBreakdown,
|
||||
} from './services/awards.service.js';
|
||||
import {
|
||||
getAllAwardDefinitions,
|
||||
getAwardDefinition,
|
||||
createAwardDefinition,
|
||||
updateAwardDefinition,
|
||||
deleteAwardDefinition,
|
||||
testAwardCalculation,
|
||||
} from './services/awards-admin.service.js';
|
||||
import {
|
||||
getAutoSyncSettings,
|
||||
updateAutoSyncSettings,
|
||||
@@ -1445,6 +1453,220 @@ const app = new Elysia()
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* ================================================================
|
||||
* AWARD MANAGEMENT ROUTES (Admin Only)
|
||||
* ================================================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* GET /api/admin/awards
|
||||
* Get all award definitions (admin only)
|
||||
*/
|
||||
.get('/api/admin/awards', async ({ user, set }) => {
|
||||
if (!user || !user.isAdmin) {
|
||||
set.status = !user ? 401 : 403;
|
||||
return { success: false, error: !user ? 'Unauthorized' : 'Admin access required' };
|
||||
}
|
||||
|
||||
try {
|
||||
const awards = await getAllAwardDefinitions();
|
||||
return {
|
||||
success: true,
|
||||
awards,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Error fetching award definitions', { error: error.message, userId: user.id });
|
||||
set.status = 500;
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to fetch award definitions',
|
||||
};
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* GET /api/admin/awards/:id
|
||||
* Get a single award definition (admin only)
|
||||
*/
|
||||
.get('/api/admin/awards/:id', async ({ user, params, set }) => {
|
||||
if (!user || !user.isAdmin) {
|
||||
set.status = !user ? 401 : 403;
|
||||
return { success: false, error: !user ? 'Unauthorized' : 'Admin access required' };
|
||||
}
|
||||
|
||||
try {
|
||||
const award = await getAwardDefinition(params.id);
|
||||
|
||||
if (!award) {
|
||||
set.status = 404;
|
||||
return {
|
||||
success: false,
|
||||
error: 'Award not found',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
award,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Error fetching award definition', { error: error.message, userId: user.id });
|
||||
set.status = 500;
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to fetch award definition',
|
||||
};
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* POST /api/admin/awards
|
||||
* Create a new award definition (admin only)
|
||||
*/
|
||||
.post(
|
||||
'/api/admin/awards',
|
||||
async ({ user, body, set }) => {
|
||||
if (!user || !user.isAdmin) {
|
||||
set.status = !user ? 401 : 403;
|
||||
return { success: false, error: !user ? 'Unauthorized' : 'Admin access required' };
|
||||
}
|
||||
|
||||
try {
|
||||
const award = await createAwardDefinition(body);
|
||||
return {
|
||||
success: true,
|
||||
award,
|
||||
message: 'Award definition created successfully',
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Error creating award definition', { error: error.message, userId: user.id });
|
||||
set.status = 400;
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
id: t.String(),
|
||||
name: t.String(),
|
||||
description: t.String(),
|
||||
caption: t.String(),
|
||||
category: t.String(),
|
||||
rules: t.Any(),
|
||||
modeGroups: t.Optional(t.Any()),
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* PUT /api/admin/awards/:id
|
||||
* Update an award definition (admin only)
|
||||
*/
|
||||
.put(
|
||||
'/api/admin/awards/:id',
|
||||
async ({ user, params, body, set }) => {
|
||||
if (!user || !user.isAdmin) {
|
||||
set.status = !user ? 401 : 403;
|
||||
return { success: false, error: !user ? 'Unauthorized' : 'Admin access required' };
|
||||
}
|
||||
|
||||
try {
|
||||
const award = await updateAwardDefinition(params.id, body);
|
||||
return {
|
||||
success: true,
|
||||
award,
|
||||
message: 'Award definition updated successfully',
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Error updating award definition', { error: error.message, userId: user.id, awardId: params.id });
|
||||
set.status = 400;
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
id: t.Optional(t.String()),
|
||||
name: t.String(),
|
||||
description: t.String(),
|
||||
caption: t.String(),
|
||||
category: t.String(),
|
||||
rules: t.Any(),
|
||||
modeGroups: t.Optional(t.Any()),
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* DELETE /api/admin/awards/:id
|
||||
* Delete an award definition (admin only)
|
||||
*/
|
||||
.delete('/api/admin/awards/:id', async ({ user, params, set }) => {
|
||||
if (!user || !user.isAdmin) {
|
||||
set.status = !user ? 401 : 403;
|
||||
return { success: false, error: !user ? 'Unauthorized' : 'Admin access required' };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await deleteAwardDefinition(params.id);
|
||||
return {
|
||||
success: true,
|
||||
...result,
|
||||
message: 'Award definition deleted successfully',
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Error deleting award definition', { error: error.message, userId: user.id, awardId: params.id });
|
||||
set.status = 400;
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* POST /api/admin/awards/:id/test
|
||||
* Test award calculation (admin only)
|
||||
*/
|
||||
.post(
|
||||
'/api/admin/awards/:id/test',
|
||||
async ({ user, params, body, set }) => {
|
||||
if (!user || !user.isAdmin) {
|
||||
set.status = !user ? 401 : 403;
|
||||
return { success: false, error: !user ? 'Unauthorized' : 'Admin access required' };
|
||||
}
|
||||
|
||||
try {
|
||||
// Use provided userId or admin's own account
|
||||
const testUserId = body.userId || user.id;
|
||||
const awardDefinition = body.awardDefinition || null;
|
||||
const result = await testAwardCalculation(params.id, testUserId, awardDefinition);
|
||||
return {
|
||||
success: true,
|
||||
...result,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Error testing award calculation', { error: error.message, userId: user.id, awardId: params.id });
|
||||
set.status = 400;
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
userId: t.Optional(t.Integer()),
|
||||
awardDefinition: t.Optional(t.Any()),
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* ================================================================
|
||||
* AUTO-SYNC SETTINGS ROUTES
|
||||
|
||||
Reference in New Issue
Block a user