feat(admin): implement delete functionality for Falukant regions and update UI components

This commit is contained in:
Torsten Schulz (local)
2026-08-14 09:54:09 +02:00
parent 3f80cc40ff
commit e6f6fc984a
7 changed files with 522 additions and 68 deletions

View File

@@ -57,6 +57,7 @@ class AdminController {
this.getFalukantAllRegions = this.getFalukantAllRegions.bind(this);
this.getFalukantRegionTypes = this.getFalukantRegionTypes.bind(this);
this.createFalukantRegion = this.createFalukantRegion.bind(this);
this.deleteFalukantRegion = this.deleteFalukantRegion.bind(this);
this.updateFalukantRegionMap = this.updateFalukantRegionMap.bind(this);
this.getRegionDistances = this.getRegionDistances.bind(this);
this.upsertRegionDistance = this.upsertRegionDistance.bind(this);
@@ -622,6 +623,19 @@ class AdminController {
}
}
async deleteFalukantRegion(req, res) {
try {
const { userid: userId } = req.headers;
const result = await AdminService.deleteFalukantRegion(userId, req.params.id);
res.status(200).json(result);
} catch (error) {
console.log(error);
const status = error.message === 'noaccess' ? 403
: (['regionNotFound'].includes(error.message) ? 404 : 400);
res.status(status).json({ error: error.message });
}
}
async updateFalukantRegionMap(req, res) {
try {
const { userid: userId } = req.headers;

View File

@@ -0,0 +1,63 @@
'use strict';
/**
* Older generated SRS data contained the number word "Baynte" as the
* currency phrase "20 Peso". Baynte itself means twenty, so the expected
* answer must be the number without a currency unit.
*/
module.exports = {
async up(queryInterface) {
await queryInterface.sequelize.query(`
WITH affected AS (
SELECT
id,
user_id,
course_id,
lesson_id,
direction,
CASE
WHEN LOWER(TRIM(learning)) = 'baynte' THEN 'baynte'
ELSE '20'
END AS corrected_learning,
CASE
WHEN LOWER(TRIM(reference)) = 'baynte' THEN 'baynte'
ELSE '20'
END AS corrected_reference
FROM community.vocab_srs_item
WHERE (LOWER(TRIM(learning)) = 'baynte' AND LOWER(TRIM(reference)) IN ('20 peso', '20 pesos'))
OR (LOWER(TRIM(reference)) = 'baynte' AND LOWER(TRIM(learning)) IN ('20 peso', '20 pesos'))
),
keyed AS (
SELECT
affected.*,
ENCODE(DIGEST(CONCAT_WS(
'|',
course_id::text,
COALESCE(lesson_id::text, 'course'),
UPPER(direction),
corrected_learning,
corrected_reference
), 'sha1'), 'hex') AS corrected_item_key
FROM affected
)
UPDATE community.vocab_srs_item item
SET learning = keyed.corrected_learning,
reference = keyed.corrected_reference,
item_key = keyed.corrected_item_key,
updated_at = NOW()
FROM keyed
WHERE item.id = keyed.id
AND NOT EXISTS (
SELECT 1
FROM community.vocab_srs_item duplicate
WHERE duplicate.user_id = keyed.user_id
AND duplicate.item_key = keyed.corrected_item_key
AND duplicate.id <> keyed.id
);
`);
},
async down() {
// The old answer was semantically incorrect and must not be restored.
},
};

View File

@@ -65,6 +65,7 @@ router.get('/falukant/region-types', authenticate, adminController.getFalukantRe
router.get('/falukant/regions', authenticate, adminController.getFalukantRegions);
router.get('/falukant/regions/all', authenticate, adminController.getFalukantAllRegions);
router.post('/falukant/regions', authenticate, adminController.createFalukantRegion);
router.delete('/falukant/regions/:id', authenticate, adminController.deleteFalukantRegion);
router.put('/falukant/regions/:id/map', authenticate, adminController.updateFalukantRegionMap);
router.get('/falukant/region-distances', authenticate, adminController.getRegionDistances);
router.post('/falukant/region-distances', authenticate, adminController.upsertRegionDistance);

View File

@@ -934,6 +934,45 @@ class AdminService {
return region;
}
async deleteFalukantRegion(userId, regionId) {
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
throw new Error('noaccess');
}
const region = await RegionData.findByPk(regionId);
if (!region) {
throw new Error('regionNotFound');
}
const [childCount, distanceCount, userCount, characterCount, weatherCount] = await Promise.all([
RegionData.count({ where: { parentId: region.id } }),
RegionDistance.count({
where: {
[Op.or]: [
{ sourceRegionId: region.id },
{ targetRegionId: region.id },
],
},
}),
FalukantUser.count({ where: { mainBranchRegionId: region.id } }),
FalukantCharacter.count({ where: { regionId: region.id } }),
Weather.count({ where: { regionId: region.id } }),
]);
if (childCount > 0) {
throw new Error('regionHasChildren');
}
if (distanceCount > 0) {
throw new Error('regionHasDistances');
}
if (userCount > 0 || characterCount > 0 || weatherCount > 0) {
throw new Error('regionInUse');
}
await region.destroy();
return { success: true };
}
async getRegionDistances(userId) {
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
throw new Error('noaccess');