diff --git a/backend/controllers/adminController.js b/backend/controllers/adminController.js
index b7fe107..bc54541 100755
--- a/backend/controllers/adminController.js
+++ b/backend/controllers/adminController.js
@@ -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.updateFalukantRegion = this.updateFalukantRegion.bind(this);
this.deleteFalukantRegion = this.deleteFalukantRegion.bind(this);
this.updateFalukantRegionMap = this.updateFalukantRegionMap.bind(this);
this.getRegionDistances = this.getRegionDistances.bind(this);
@@ -623,6 +624,19 @@ class AdminController {
}
}
+ async updateFalukantRegion(req, res) {
+ try {
+ const { userid: userId } = req.headers;
+ const region = await AdminService.updateFalukantRegion(userId, req.params.id, req.body || {});
+ res.status(200).json(region);
+ } catch (error) {
+ console.log(error);
+ const status = error.message === 'noaccess' ? 403
+ : (error.message === 'regionNotFound' ? 404 : 400);
+ res.status(status).json({ error: error.message });
+ }
+ }
+
async deleteFalukantRegion(req, res) {
try {
const { userid: userId } = req.headers;
diff --git a/backend/routers/adminRouter.js b/backend/routers/adminRouter.js
index bd367c1..6bb4426 100755
--- a/backend/routers/adminRouter.js
+++ b/backend/routers/adminRouter.js
@@ -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.put('/falukant/regions/:id', authenticate, adminController.updateFalukantRegion);
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);
diff --git a/backend/services/adminService.js b/backend/services/adminService.js
index 2b29612..437356a 100755
--- a/backend/services/adminService.js
+++ b/backend/services/adminService.js
@@ -934,6 +934,30 @@ class AdminService {
return region;
}
+ async updateFalukantRegion(userId, regionId, { name } = {}) {
+ if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
+ throw new Error('noaccess');
+ }
+
+ const regionName = String(name || '').trim();
+ if (!regionName) {
+ throw new Error('missingName');
+ }
+
+ const region = await RegionData.findByPk(regionId);
+ if (!region) {
+ throw new Error('regionNotFound');
+ }
+
+ region.name = regionName;
+ await region.save();
+
+ return RegionData.findByPk(region.id, {
+ attributes: ['id', 'name', 'map', 'regionTypeId', 'parentId'],
+ include: [{ model: RegionType, as: 'regionType', attributes: ['id', 'labelTr', 'parentId'] }],
+ });
+ }
+
async deleteFalukantRegion(userId, regionId) {
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
throw new Error('noaccess');
diff --git a/frontend/src/i18n/locales/de/admin.json b/frontend/src/i18n/locales/de/admin.json
index d092456..fe0f325 100755
--- a/frontend/src/i18n/locales/de/admin.json
+++ b/frontend/src/i18n/locales/de/admin.json
@@ -267,6 +267,20 @@
"selectRegionHint": "Wähle eine Region aus der Liste oder direkt auf der Karte aus.",
"childRegions": "Direkte Unterregionen",
"noChildRegions": "Keine direkten Unterregionen zugeordnet.",
+ "rename": {
+ "label": "Regionsname",
+ "save": "Umbenennen",
+ "saving": "Benenne um…",
+ "success": "Region wurde umbenannt.",
+ "missingName": "Bitte einen Regionsnamen eingeben.",
+ "error": "Die Region konnte nicht umbenannt werden."
+ },
+ "hierarchy": {
+ "title": "Unterregionen-Hierarchie",
+ "summary": "{count} Unterregionen werden hervorgehoben.",
+ "direct": "Direkt: {count}",
+ "deeper": "Tiefer: {count}"
+ },
"unknownType": "Unbekannter Typ",
"coordsCompact": "X {x}, Y {y} · B {w} × H {h}",
"noCoords": "Keine Koordinaten gesetzt",
diff --git a/frontend/src/i18n/locales/en/admin.json b/frontend/src/i18n/locales/en/admin.json
index 492d848..b76b698 100755
--- a/frontend/src/i18n/locales/en/admin.json
+++ b/frontend/src/i18n/locales/en/admin.json
@@ -322,6 +322,20 @@
"selectRegionHint": "Select a region from the list or directly on the map.",
"childRegions": "Direct child regions",
"noChildRegions": "No direct child regions assigned.",
+ "rename": {
+ "label": "Region name",
+ "save": "Rename",
+ "saving": "Renaming…",
+ "success": "Region renamed.",
+ "missingName": "Enter a region name.",
+ "error": "Could not rename the region."
+ },
+ "hierarchy": {
+ "title": "Child-region hierarchy",
+ "summary": "{count} child regions highlighted.",
+ "direct": "Direct: {count}",
+ "deeper": "Deeper: {count}"
+ },
"unknownType": "Unknown type",
"coordsCompact": "X {x}, Y {y} · W {w} × H {h}",
"noCoords": "No coordinates set",
diff --git a/frontend/src/views/admin/falukant/MapRegionsView.vue b/frontend/src/views/admin/falukant/MapRegionsView.vue
index 7da45ba..4c34fc6 100755
--- a/frontend/src/views/admin/falukant/MapRegionsView.vue
+++ b/frontend/src/views/admin/falukant/MapRegionsView.vue
@@ -136,6 +136,28 @@
{{ $t('admin.falukant.map.unsaved') }}
+
+
+ {{ $t('admin.falukant.map.hierarchy.summary', { count: descendantRegions.length }) }}
+ {{ $t('admin.falukant.map.hierarchy.direct', { count: directChildRegions.length }) }}
+ {{ $t('admin.falukant.map.hierarchy.deeper', { count: deeperDescendantRegions.length }) }}
+
{{ $t('admin.falukant.map.childRegions') }}
@@ -339,6 +361,9 @@ export default {
savingRegion: false,
savingAllRegions: false,
deletingRegion: false,
+ renameName: '',
+ renameError: null,
+ renamingRegion: false,
};
},
computed: {
@@ -369,6 +394,40 @@ export default {
.filter((region) => String(region.parentId) === String(this.selectedRegion.id))
.sort((a, b) => a.name.localeCompare(b.name));
},
+ descendantDepthById() {
+ if (!this.selectedRegion) return new Map();
+ const childrenByParentId = new Map();
+ this.regions.forEach((region) => {
+ const parentId = String(region.parentId);
+ const children = childrenByParentId.get(parentId) || [];
+ children.push(region);
+ childrenByParentId.set(parentId, children);
+ });
+ const depths = new Map();
+ const visit = (parentId, depth, visited) => {
+ (childrenByParentId.get(String(parentId)) || []).forEach((child) => {
+ if (visited.has(String(child.id))) return;
+ depths.set(String(child.id), depth);
+ const nextVisited = new Set(visited);
+ nextVisited.add(String(child.id));
+ visit(child.id, depth + 1, nextVisited);
+ });
+ };
+ visit(this.selectedRegion.id, 1, new Set([String(this.selectedRegion.id)]));
+ return depths;
+ },
+ descendantRegions() {
+ return this.regions
+ .filter((region) => this.descendantDepthById.has(String(region.id)))
+ .sort((a, b) => a.name.localeCompare(b.name));
+ },
+ deeperDescendantRegions() {
+ return this.descendantRegions.filter((region) => this.descendantDepthById.get(String(region.id)) > 1);
+ },
+ canRenameSelectedRegion() {
+ return Boolean(this.selectedRegion && String(this.renameName || '').trim()
+ && String(this.renameName).trim() !== this.selectedRegion.name && !this.renamingRegion);
+ },
},
watch: {
'newRegion.regionTypeId': function () {
@@ -505,6 +564,8 @@ export default {
regionClasses(region) {
return {
selected: this.selectedRegion && this.selectedRegion.id === region.id,
+ 'direct-descendant': this.descendantDepthById.get(String(region.id)) === 1,
+ 'deep-descendant': this.descendantDepthById.get(String(region.id)) > 1,
dirty: this.dirtyRegionIds.includes(region.id),
source: this.newConnection.sourceRegionId === region.id,
target: this.newConnection.targetRegionId === region.id,
@@ -513,6 +574,8 @@ export default {
regionListClasses(region) {
return {
selected: this.selectedRegion && this.selectedRegion.id === region.id,
+ 'direct-descendant': this.descendantDepthById.get(String(region.id)) === 1,
+ 'deep-descendant': this.descendantDepthById.get(String(region.id)) > 1,
dirty: this.dirtyRegionIds.includes(region.id),
source: this.newConnection.sourceRegionId === region.id,
target: this.newConnection.targetRegionId === region.id,
@@ -529,9 +592,32 @@ export default {
}
this.selectedRegion = region;
+ this.renameName = region.name;
+ this.renameError = null;
this.selectedRegionDirty = this.dirtyRegionIds.includes(region.id);
this.drawingRect = null;
},
+ async renameSelectedRegion() {
+ if (!this.canRenameSelectedRegion) return;
+ const region = this.selectedRegion;
+ const name = String(this.renameName).trim();
+ this.renamingRegion = true;
+ this.renameError = null;
+ try {
+ const { data } = await apiClient.put(`/api/admin/falukant/regions/${region.id}`, { name });
+ const index = this.regions.findIndex((item) => item.id === region.id);
+ if (index !== -1) this.regions.splice(index, 1, { ...this.regions[index], ...data });
+ this.selectedRegion = this.regions.find((item) => item.id === region.id) || data;
+ this.renameName = this.selectedRegion.name;
+ showSuccess(this, this.$t('admin.falukant.map.rename.success'));
+ } catch (error) {
+ console.error('Error renaming Falukant region:', error);
+ const errorKey = error?.response?.data?.error;
+ this.renameError = this.$t(`admin.falukant.map.rename.${errorKey === 'missingName' ? 'missingName' : 'error'}`);
+ } finally {
+ this.renamingRegion = false;
+ }
+ },
onMouseDown(event) {
// Zeichnen von Rechtecken nur im "Positionen"-Tab
if (this.activeTab !== 'regions') return;
@@ -801,6 +887,16 @@ export default {
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.75);
}
+.region-rect.direct-descendant {
+ border-color: rgba(0, 128, 128, 0.95);
+ background-color: rgba(0, 148, 136, 0.28);
+}
+
+.region-rect.deep-descendant {
+ border-color: rgba(60, 166, 158, 0.9);
+ background-color: rgba(102, 196, 187, 0.2);
+}
+
.region-rect.drawing {
border-style: dashed;
}
@@ -854,6 +950,21 @@ export default {
box-shadow: inset 3px 0 0 #1976d2;
}
+.region-list li.direct-descendant .region-list-entry {
+ background: #d8f0ed;
+ box-shadow: inset 3px 0 0 #008c84;
+}
+
+.region-list li.deep-descendant .region-list-entry {
+ background: #ecf8f6;
+ box-shadow: inset 3px 0 0 #76bdb6;
+}
+
+.region-list li.selected .region-list-entry {
+ background-color: #dcecff;
+ box-shadow: inset 3px 0 0 #1976d2;
+}
+
.region-list-name {
min-width: 0;
overflow: hidden;
@@ -952,6 +1063,72 @@ export default {
color: #6b5a39;
}
+.rename-region {
+ margin-top: 12px;
+}
+
+.rename-region label {
+ display: block;
+ margin-bottom: 4px;
+ color: #41556a;
+ font-size: 0.78rem;
+ font-weight: 700;
+}
+
+.rename-region-controls {
+ display: flex;
+ gap: 6px;
+}
+
+.rename-region-controls input {
+ min-width: 0;
+ flex: 1;
+}
+
+.rename-error {
+ margin: 5px 0 0;
+}
+
+.hierarchy-summary {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 5px 10px;
+ margin-top: 12px;
+ padding: 7px 8px;
+ border: 1px solid #cfe5e2;
+ background: #f2faf9;
+ color: #315c59;
+ font-size: 0.78rem;
+}
+
+.hierarchy-summary > :first-child {
+ flex-basis: 100%;
+ font-weight: 700;
+}
+
+.hierarchy-legend {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.hierarchy-legend i {
+ display: inline-block;
+ width: 10px;
+ height: 10px;
+ border: 1px solid;
+}
+
+.legend-direct {
+ border-color: #008c84 !important;
+ background: #4eb5ac;
+}
+
+.legend-deep {
+ border-color: #76bdb6 !important;
+ background: #b9e1dc;
+}
+
.child-regions {
margin-top: 12px;
border: 1px solid #dfe7ee;