feat(admin): implement region renaming functionality and update UI components
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -136,6 +136,28 @@
|
||||
</span>
|
||||
<span v-if="selectedRegionDirty" class="unsaved-badge">{{ $t('admin.falukant.map.unsaved') }}</span>
|
||||
</div>
|
||||
<form class="rename-region" @submit.prevent="renameSelectedRegion">
|
||||
<label for="selected-region-name">{{ $t('admin.falukant.map.rename.label') }}</label>
|
||||
<div class="rename-region-controls">
|
||||
<input
|
||||
id="selected-region-name"
|
||||
v-model="renameName"
|
||||
type="text"
|
||||
required
|
||||
:disabled="renamingRegion"
|
||||
:aria-describedby="renameError ? 'selected-region-name-error' : null"
|
||||
/>
|
||||
<button class="btn btn-secondary" type="submit" :disabled="!canRenameSelectedRegion">
|
||||
{{ renamingRegion ? $t('admin.falukant.map.rename.saving') : $t('admin.falukant.map.rename.save') }}
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="renameError" id="selected-region-name-error" class="error-text rename-error" role="alert">{{ renameError }}</p>
|
||||
</form>
|
||||
<section class="hierarchy-summary" :aria-label="$t('admin.falukant.map.hierarchy.title')">
|
||||
<span>{{ $t('admin.falukant.map.hierarchy.summary', { count: descendantRegions.length }) }}</span>
|
||||
<span class="hierarchy-legend"><i class="legend-direct"></i>{{ $t('admin.falukant.map.hierarchy.direct', { count: directChildRegions.length }) }}</span>
|
||||
<span class="hierarchy-legend"><i class="legend-deep"></i>{{ $t('admin.falukant.map.hierarchy.deeper', { count: deeperDescendantRegions.length }) }}</span>
|
||||
</section>
|
||||
<section class="child-regions" :aria-label="$t('admin.falukant.map.childRegions')">
|
||||
<div class="child-regions-heading">
|
||||
<h4>{{ $t('admin.falukant.map.childRegions') }}</h4>
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user