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

@@ -6,13 +6,18 @@
</div>
<div class="map-layout">
<div
class="map-container"
@mousedown="onMouseDown"
@mousemove="onMouseMove"
@mouseup="onMouseUp"
@mouseleave="onMouseUp"
>
<section class="map-workspace" :aria-label="$t('admin.falukant.map.mapArea')">
<div class="map-instruction">
<span class="instruction-marker" aria-hidden="true">1</span>
{{ $t('admin.falukant.map.hintDraw') }}
</div>
<div
class="map-container"
@mousedown="onMouseDown"
@mousemove="onMouseMove"
@mouseup="onMouseUp"
@mouseleave="onMouseUp"
>
<img
ref="mapImage"
src="/images/falukant/map.png"
@@ -39,24 +44,26 @@
class="region-rect drawing"
:style="rectStyle(drawingRect)"
></div>
</div>
</div>
</section>
<div class="sidebar">
<aside class="sidebar">
<h2>{{ $t('admin.falukant.map.regionList') }}</h2>
<ul class="region-list">
<li
v-for="region in regions"
:key="region.id"
:class="regionListClasses(region)"
@click="selectRegion(region)"
>
{{ region.name }}
<span v-if="region.map" class="coords">
({{ region.map.x }},{{ region.map.y }} {{ region.map.w }}×{{ region.map.h }})
</span>
<span v-else class="coords missing">
{{ $t('admin.falukant.map.noCoords') }}
</span>
<button type="button" class="region-list-entry" @click="selectRegion(region)">
<span class="region-list-name">{{ region.name }}</span>
<span class="type-badge">{{ regionTypeName(region) }}</span>
<span v-if="region.map" class="coords">
{{ $t('admin.falukant.map.coordsCompact', { x: region.map.x, y: region.map.y, w: region.map.w, h: region.map.h }) }}
</span>
<span v-else class="coords missing">{{ $t('admin.falukant.map.noCoords') }}</span>
<span v-if="dirtyRegionIds.includes(region.id)" class="unsaved-badge">{{ $t('admin.falukant.map.unsaved') }}</span>
</button>
</li>
</ul>
@@ -115,34 +122,42 @@
</table>
</div>
<div v-if="selectedRegion" class="details">
<h3>{{ selectedRegion.name }}</h3>
<p v-if="selectedRegion.map">
{{ $t('admin.falukant.map.currentRect') }}:
{{ selectedRegion.map.x }},{{ selectedRegion.map.y }}
{{ selectedRegion.map.w }}×{{ selectedRegion.map.h }}
</p>
<p v-else>
{{ $t('admin.falukant.map.noCoords') }}
</p>
<p class="hint">
{{ $t('admin.falukant.map.hintDraw') }}
</p>
<button
class="btn btn-primary"
:disabled="!selectedRegionDirty"
@click="saveSelectedRegion"
>
{{ $t('common.save') }}
</button>
<button
class="btn btn-secondary"
:disabled="!dirtyRegionIds.length"
@click="saveAllRegions"
>
{{ $t('admin.falukant.map.saveAll') }}
</button>
</div>
<section v-if="selectedRegion" class="details" :aria-label="$t('admin.falukant.map.selectedRegion')">
<div class="detail-heading">
<div>
<p class="eyebrow">{{ $t('admin.falukant.map.selectedRegion') }}</p>
<h3>{{ selectedRegion.name }}</h3>
</div>
<span class="type-badge">{{ regionTypeName(selectedRegion) }}</span>
</div>
<div class="selection-state">
<span :class="['position-status', { missing: !selectedRegion.map }]">
{{ selectedRegion.map ? $t('admin.falukant.map.positionSet') : $t('admin.falukant.map.noCoords') }}
</span>
<span v-if="selectedRegionDirty" class="unsaved-badge">{{ $t('admin.falukant.map.unsaved') }}</span>
</div>
<dl v-if="selectedRegion.map" class="coordinates-grid">
<div><dt>{{ $t('admin.falukant.map.coordinateX') }}</dt><dd>{{ selectedRegion.map.x }}</dd></div>
<div><dt>{{ $t('admin.falukant.map.coordinateY') }}</dt><dd>{{ selectedRegion.map.y }}</dd></div>
<div><dt>{{ $t('admin.falukant.map.coordinateWidth') }}</dt><dd>{{ selectedRegion.map.w }}</dd></div>
<div><dt>{{ $t('admin.falukant.map.coordinateHeight') }}</dt><dd>{{ selectedRegion.map.h }}</dd></div>
</dl>
<div class="detail-actions">
<button class="btn btn-primary" :disabled="!selectedRegionDirty || savingRegion" @click="saveSelectedRegion">
{{ savingRegion ? $t('admin.falukant.map.saving') : $t('common.save') }}
</button>
<button class="btn btn-secondary" :disabled="!dirtyRegionIds.length || savingAllRegions" @click="saveAllRegions">
{{ savingAllRegions ? $t('admin.falukant.map.saving') : $t('admin.falukant.map.saveAll') }}
</button>
</div>
<div class="danger-zone">
<span>{{ $t('admin.falukant.map.deleteHint') }}</span>
<button class="btn btn-danger" :disabled="deletingRegion" @click="deleteSelectedRegion">
{{ deletingRegion ? $t('admin.falukant.map.deleting') : $t('admin.falukant.map.deleteRegion') }}
</button>
</div>
</section>
<p v-else class="empty-selection">{{ $t('admin.falukant.map.selectRegionHint') }}</p>
</div>
<div v-else-if="activeTab === 'distances'" class="connections">
@@ -261,7 +276,7 @@
</tbody>
</table>
</div>
</div>
</aside>
</div>
</div>
</template>
@@ -269,7 +284,7 @@
<script>
import apiClient from '@/utils/axios.js';
import SimpleTabs from '@/components/SimpleTabs.vue';
import { confirmAction, showError } from '@/utils/feedback.js';
import { confirmAction, showError, showSuccess } from '@/utils/feedback.js';
export default {
name: 'AdminFalukantMapRegionsView',
@@ -306,6 +321,9 @@ export default {
},
creatingRegion: false,
createRegionError: null,
savingRegion: false,
savingAllRegions: false,
deletingRegion: false,
};
},
computed: {
@@ -375,6 +393,10 @@ export default {
}
return raw;
},
regionTypeName(region) {
return this.regionTypeLabel(region?.regionType?.labelTr)
|| this.$t('admin.falukant.map.unknownType');
},
async loadRegions() {
try {
const { data } = await apiClient.get('/api/admin/falukant/regions/all');
@@ -422,6 +444,7 @@ export default {
if (this.parentRegionEnabled) {
this.newRegion.parentId = null;
}
showSuccess(this, this.$t('admin.falukant.map.createRegion.success'));
} catch (error) {
console.error('Error creating region:', error);
const errKey = error?.response?.data?.error;
@@ -577,6 +600,7 @@ export default {
},
async saveSelectedRegion() {
if (!this.selectedRegion || !this.selectedRegion.map) return;
this.savingRegion = true;
try {
await this.saveRegion(this.selectedRegion);
// Liste aktualisieren, damit andere Einträge auch aktualisierte Daten sehen
@@ -585,12 +609,17 @@ export default {
if (refreshed) {
this.selectedRegion = refreshed;
}
showSuccess(this, this.$t('admin.falukant.map.saveSuccess'));
} catch (error) {
console.error('Error saving region map:', error);
showError(this, this.$t('admin.falukant.map.errorSave'));
} finally {
this.savingRegion = false;
}
},
async saveAllRegions() {
if (!this.dirtyRegionIds.length) return;
this.savingAllRegions = true;
try {
const dirtyIds = [...this.dirtyRegionIds];
for (const id of dirtyIds) {
@@ -605,8 +634,45 @@ export default {
this.selectedRegion = refreshed;
}
}
showSuccess(this, this.$t('admin.falukant.map.saveAllSuccess'));
} catch (error) {
console.error('Error saving all region maps:', error);
showError(this, this.$t('admin.falukant.map.errorSave'));
} finally {
this.savingAllRegions = false;
}
},
async deleteSelectedRegion() {
const region = this.selectedRegion;
if (!region || this.deletingRegion) return;
const confirmed = await confirmAction(this, {
title: this.$t('admin.falukant.map.deleteRegion'),
message: this.$t('admin.falukant.map.confirmDeleteRegion', { name: region.name }),
});
if (!confirmed) return;
this.deletingRegion = true;
try {
await apiClient.delete(`/api/admin/falukant/regions/${region.id}`);
await Promise.all([this.loadRegions(), this.loadConnections()]);
this.selectedRegion = null;
this.selectedRegionDirty = false;
this.dirtyRegionIds = [];
this.drawingRect = null;
showSuccess(this, this.$t('admin.falukant.map.deleteSuccess'));
} catch (error) {
console.error('Error deleting Falukant region:', error);
const errorKey = error?.response?.data?.error;
const messages = {
regionHasChildren: 'deleteBlockedChildren',
regionHasDistances: 'deleteBlockedDistances',
regionInUse: 'deleteBlockedInUse',
regionNotFound: 'deleteBlockedNotFound',
};
showError(this, this.$t(`admin.falukant.map.${messages[errorKey] || 'deleteError'}`));
} finally {
this.deletingRegion = false;
}
},
async saveConnection() {
@@ -647,22 +713,58 @@ export default {
<style scoped>
.falukant-map-admin {
padding: 10px;
padding: 12px;
color: #24313d;
}
.map-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(280px, 360px);
gap: 16px;
align-items: start;
}
.map-workspace {
min-width: 0;
}
.map-instruction {
display: flex;
gap: 20px;
gap: 8px;
align-items: center;
margin-bottom: 8px;
padding: 8px 10px;
border-left: 3px solid #1976d2;
background: #f4f8fc;
color: #41556a;
font-size: 0.85rem;
}
.instruction-marker {
display: inline-grid;
place-items: center;
flex: 0 0 auto;
width: 19px;
height: 19px;
border-radius: 50%;
background: #1976d2;
color: #fff;
font-weight: 700;
font-size: 0.72rem;
}
.map-container {
position: relative;
display: inline-block;
max-width: 100%;
line-height: 0;
}
.map {
display: block;
width: 100%;
max-width: 800px;
border: 1px solid #ccc;
border: 1px solid #bac6d0;
}
.region-rect {
@@ -673,8 +775,9 @@ export default {
}
.region-rect.selected {
border-color: rgba(255, 128, 0, 0.9);
background-color: rgba(255, 128, 0, 0.25);
border-color: rgba(20, 97, 173, 1);
background-color: rgba(25, 118, 210, 0.28);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.75);
}
.region-rect.drawing {
@@ -682,8 +785,10 @@ export default {
}
.sidebar {
min-width: 260px;
max-width: 320px;
min-width: 0;
padding: 12px;
border: 1px solid #d7dee5;
background: #fbfcfd;
}
.region-list {
@@ -692,24 +797,48 @@ export default {
margin: 0 0 1rem 0;
max-height: 400px;
overflow: auto;
border: 1px solid #ccc;
border: 1px solid #cbd5df;
background: #fff;
}
.region-list li {
padding: 4px 8px;
border-bottom: 1px solid #edf0f3;
}
.region-list li:last-child {
border-bottom: 0;
}
.region-list-entry {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 3px 8px;
width: 100%;
padding: 8px;
border: 0;
background: transparent;
color: inherit;
text-align: left;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
}
.region-list li:nth-child(odd) {
background-color: #f8f8f8;
.region-list-entry:hover,
.region-list-entry:focus-visible {
background: #eef6ff;
outline: none;
}
.region-list li.selected {
background-color: #e0f0ff;
font-weight: bold;
.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;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 700;
}
.region-rect.source,
@@ -724,10 +853,11 @@ export default {
}
.region-list li.dirty {
font-style: italic;
font-style: normal;
}
.coords {
grid-column: 1 / -1;
font-size: 0.8rem;
color: #555;
}
@@ -737,7 +867,129 @@ export default {
}
.details {
margin-top: 1rem;
margin-top: 12px;
padding: 12px;
border: 1px solid #b9d5ef;
border-left: 3px solid #1976d2;
background: #fff;
}
.detail-heading,
.selection-state,
.detail-actions,
.danger-zone {
display: flex;
gap: 8px;
align-items: center;
justify-content: space-between;
}
.detail-heading h3 {
margin: 0;
font-size: 1.1rem;
}
.eyebrow {
margin: 0 0 2px;
color: #657786;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.type-badge,
.unsaved-badge,
.position-status {
display: inline-block;
width: fit-content;
padding: 2px 6px;
border-radius: 3px;
font-size: 0.72rem;
font-weight: 700;
line-height: 1.3;
}
.type-badge {
background: #e8eef4;
color: #34495d;
}
.unsaved-badge {
background: #fff1ce;
color: #815700;
}
.position-status {
margin-top: 10px;
background: #eaf4ff;
color: #1765a9;
}
.position-status.missing {
background: #f4f0e8;
color: #6b5a39;
}
.coordinates-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 1px;
margin: 12px 0;
background: #dfe6ec;
}
.coordinates-grid > div {
min-width: 0;
padding: 6px;
background: #f8fafb;
}
.coordinates-grid dt {
color: #687986;
font-size: 0.68rem;
font-weight: 700;
}
.coordinates-grid dd {
margin: 2px 0 0;
overflow: hidden;
text-overflow: ellipsis;
font-family: monospace;
font-size: 0.78rem;
}
.detail-actions {
justify-content: flex-start;
}
.danger-zone {
margin-top: 12px;
padding-top: 10px;
border-top: 1px solid #e6d8d8;
color: #7a4a4a;
font-size: 0.78rem;
}
.btn-danger {
margin-left: auto;
border-color: #c84646;
background: #fff;
color: #a52e2e;
}
.btn-danger:hover:not(:disabled),
.btn-danger:focus-visible {
background: #c84646;
color: #fff;
}
.empty-selection {
margin: 12px 0 0;
padding: 10px;
border: 1px dashed #c5d0da;
color: #627482;
font-size: 0.85rem;
}
.create-region {
@@ -811,4 +1063,37 @@ export default {
.btn.mini.icon {
}
@media (max-width: 900px) {
.map-layout {
grid-template-columns: 1fr;
}
.sidebar {
max-width: none;
}
.region-list {
max-height: 260px;
}
}
@media (max-width: 480px) {
.falukant-map-admin {
padding: 8px;
}
.coordinates-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.danger-zone {
align-items: flex-start;
flex-direction: column;
}
.btn-danger {
margin-left: 0;
}
}
</style>