815 lines
26 KiB
Vue
Executable File
815 lines
26 KiB
Vue
Executable File
<template>
|
||
<div class="falukant-map-admin">
|
||
<div class="admin-header">
|
||
<h1>{{ $t('admin.falukant.map.title') }}</h1>
|
||
<p>{{ $t('admin.falukant.map.description') }}</p>
|
||
</div>
|
||
|
||
<div class="map-layout">
|
||
<div
|
||
class="map-container"
|
||
@mousedown="onMouseDown"
|
||
@mousemove="onMouseMove"
|
||
@mouseup="onMouseUp"
|
||
@mouseleave="onMouseUp"
|
||
>
|
||
<img
|
||
ref="mapImage"
|
||
src="/images/falukant/map.png"
|
||
class="map"
|
||
@load="onMapLoaded"
|
||
@dragstart.prevent
|
||
/>
|
||
|
||
<!-- vorhandene Regionen-Rechtecke -->
|
||
<template v-for="region in regions" :key="region.id">
|
||
<div
|
||
v-if="region.map"
|
||
class="region-rect"
|
||
:class="regionClasses(region)"
|
||
:style="rectStyle(region.map)"
|
||
@click.stop="selectRegion(region)"
|
||
:title="region.name"
|
||
></div>
|
||
</template>
|
||
|
||
<!-- aktuell gezeichneter Bereich -->
|
||
<div
|
||
v-if="drawingRect"
|
||
class="region-rect drawing"
|
||
:style="rectStyle(drawingRect)"
|
||
></div>
|
||
</div>
|
||
|
||
<div 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>
|
||
</li>
|
||
</ul>
|
||
|
||
<SimpleTabs v-model="activeTab" :tabs="tabs" />
|
||
|
||
<div v-if="activeTab === 'regions'">
|
||
<div class="create-region">
|
||
<h3>{{ $t('admin.falukant.map.createRegion.title') }}</h3>
|
||
<table class="create-region-table">
|
||
<tbody>
|
||
<tr>
|
||
<td class="label-cell">{{ $t('admin.falukant.map.createRegion.type') }}</td>
|
||
<td class="field-cell">
|
||
<select v-model.number="newRegion.regionTypeId">
|
||
<option :value="null" disabled>{{ $t('admin.falukant.map.createRegion.selectType') }}</option>
|
||
<option v-for="t in regionTypes" :key="`rt-${t.id}`" :value="t.id">
|
||
{{ regionTypeLabel(t.labelTr) }}
|
||
</option>
|
||
</select>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="label-cell">{{ $t('admin.falukant.map.createRegion.parent') }}</td>
|
||
<td class="field-cell">
|
||
<select v-model.number="newRegion.parentId" :disabled="!parentRegionEnabled">
|
||
<option :value="null" :disabled="parentRegionRequired">
|
||
{{ parentRegionEnabled ? $t('admin.falukant.map.createRegion.selectParent') : $t('admin.falukant.map.createRegion.noParent') }}
|
||
</option>
|
||
<option v-for="r in availableParentRegions" :key="`pr-${r.id}`" :value="r.id">
|
||
{{ r.name }}
|
||
</option>
|
||
</select>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="label-cell">{{ $t('admin.falukant.map.createRegion.name') }}</td>
|
||
<td class="field-cell">
|
||
<input type="text" v-model.trim="newRegion.name" />
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td colspan="2" class="create-region-actions-cell">
|
||
<button
|
||
class="btn btn-primary"
|
||
:disabled="creatingRegion || !canCreateRegion"
|
||
@click="createRegion"
|
||
>
|
||
{{ creatingRegion ? $t('admin.falukant.map.createRegion.creating') : $t('admin.falukant.map.createRegion.create') }}
|
||
</button>
|
||
<span v-if="createRegionError" class="error-text">
|
||
{{ createRegionError }}
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</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>
|
||
</div>
|
||
|
||
<div v-else-if="activeTab === 'distances'" class="connections">
|
||
<h3>{{ $t('admin.falukant.map.connectionsTitle') }}</h3>
|
||
<div class="connection-form">
|
||
<table class="connection-form-table">
|
||
<tbody>
|
||
<tr>
|
||
<td class="label-cell">
|
||
{{ $t('admin.falukant.map.source') }}
|
||
</td>
|
||
<td class="field-cell">
|
||
<div class="field-row">
|
||
<select v-model.number="newConnection.sourceRegionId">
|
||
<option :value="null" disabled>{{ $t('admin.falukant.map.selectSource') }}</option>
|
||
<option v-for="r in regions" :key="`src-${r.id}`" :value="r.id">
|
||
{{ r.name }}
|
||
</option>
|
||
</select>
|
||
<button
|
||
type="button"
|
||
class="btn mini icon"
|
||
@click="pickMode = 'source'"
|
||
:title="$t('admin.falukant.map.pickOnMap')"
|
||
>
|
||
⊕
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="label-cell">
|
||
{{ $t('admin.falukant.map.target') }}
|
||
</td>
|
||
<td class="field-cell">
|
||
<div class="field-row">
|
||
<select v-model.number="newConnection.targetRegionId">
|
||
<option :value="null" disabled>{{ $t('admin.falukant.map.selectTarget') }}</option>
|
||
<option v-for="r in regions" :key="`tgt-${r.id}`" :value="r.id">
|
||
{{ r.name }}
|
||
</option>
|
||
</select>
|
||
<button
|
||
type="button"
|
||
class="btn mini icon"
|
||
@click="pickMode = 'target'"
|
||
:title="$t('admin.falukant.map.pickOnMap')"
|
||
>
|
||
⊕
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="label-cell">
|
||
{{ $t('admin.falukant.map.mode') }}
|
||
</td>
|
||
<td class="field-cell">
|
||
<select v-model="newConnection.transportMode">
|
||
<option value="land">{{ $t('admin.falukant.map.modeLand') }}</option>
|
||
<option value="water">{{ $t('admin.falukant.map.modeWater') }}</option>
|
||
<option value="air">{{ $t('admin.falukant.map.modeAir') }}</option>
|
||
</select>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="label-cell">
|
||
{{ $t('admin.falukant.map.distance') }}
|
||
</td>
|
||
<td class="field-cell">
|
||
<input
|
||
type="number"
|
||
min="0.1"
|
||
step="0.1"
|
||
v-model.number="newConnection.distance"
|
||
/>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td colspan="2" class="connection-actions-cell">
|
||
<button
|
||
class="btn btn-primary"
|
||
:disabled="!newConnection.sourceRegionId || !newConnection.targetRegionId || !newConnection.distance"
|
||
@click="saveConnection"
|
||
>
|
||
{{ $t('admin.falukant.map.saveConnection') }}
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<table v-if="connections && connections.length" class="connections-table">
|
||
<thead>
|
||
<tr>
|
||
<th>{{ $t('admin.falukant.map.source') }}</th>
|
||
<th>{{ $t('admin.falukant.map.target') }}</th>
|
||
<th>{{ $t('admin.falukant.map.mode') }}</th>
|
||
<th>{{ $t('admin.falukant.map.distance') }}</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="c in connections" :key="c.id">
|
||
<td>{{ regionName(c.sourceRegionId) }}</td>
|
||
<td>{{ regionName(c.targetRegionId) }}</td>
|
||
<td>{{ modeLabel(c.transportMode) }}</td>
|
||
<td>{{ c.distance }}</td>
|
||
<td>
|
||
<button class="btn btn-secondary" @click="deleteConnection(c.id)">
|
||
{{ $t('common.delete') }}
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import apiClient from '@/utils/axios.js';
|
||
import SimpleTabs from '@/components/SimpleTabs.vue';
|
||
import { confirmAction, showError } from '@/utils/feedback.js';
|
||
|
||
export default {
|
||
name: 'AdminFalukantMapRegionsView',
|
||
components: { SimpleTabs },
|
||
data() {
|
||
return {
|
||
regions: [],
|
||
regionTypes: [],
|
||
selectedRegion: null,
|
||
selectedRegionDirty: false,
|
||
dirtyRegionIds: [],
|
||
drawingRect: null,
|
||
startX: null,
|
||
startY: null,
|
||
currentX: 0,
|
||
currentY: 0,
|
||
connections: [],
|
||
newConnection: {
|
||
sourceRegionId: null,
|
||
targetRegionId: null,
|
||
transportMode: 'land',
|
||
distance: 1,
|
||
},
|
||
activeTab: 'regions',
|
||
tabs: [
|
||
{ value: 'regions', label: 'admin.falukant.map.tabs.regions' },
|
||
{ value: 'distances', label: 'admin.falukant.map.tabs.distances' },
|
||
],
|
||
pickMode: null,
|
||
newRegion: {
|
||
regionTypeId: null,
|
||
parentId: null,
|
||
name: '',
|
||
},
|
||
creatingRegion: false,
|
||
createRegionError: null,
|
||
};
|
||
},
|
||
computed: {
|
||
selectedRegionType() {
|
||
if (!this.newRegion.regionTypeId) return null;
|
||
return this.regionTypes.find((t) => t.id === this.newRegion.regionTypeId) || null;
|
||
},
|
||
parentRegionEnabled() {
|
||
return Boolean(this.selectedRegionType && this.selectedRegionType.parentId != null);
|
||
},
|
||
parentRegionRequired() {
|
||
return this.parentRegionEnabled;
|
||
},
|
||
availableParentRegions() {
|
||
if (!this.parentRegionEnabled) return [];
|
||
const parentTypeId = this.selectedRegionType.parentId;
|
||
return this.regions.filter((r) => r.regionTypeId === parentTypeId);
|
||
},
|
||
canCreateRegion() {
|
||
if (!this.newRegion.regionTypeId) return false;
|
||
if (!this.newRegion.name || !String(this.newRegion.name).trim()) return false;
|
||
if (this.parentRegionRequired && !this.newRegion.parentId) return false;
|
||
return true;
|
||
},
|
||
},
|
||
watch: {
|
||
'newRegion.regionTypeId': function () {
|
||
// Wenn der Typ keinen Parent erlaubt, Parent zurücksetzen
|
||
if (!this.parentRegionEnabled) {
|
||
this.newRegion.parentId = null;
|
||
} else if (this.newRegion.parentId) {
|
||
// Falls der gewählte Parent nicht mehr passt (z.B. Typ gewechselt), resetten
|
||
const ok = this.availableParentRegions.some((r) => r.id === this.newRegion.parentId);
|
||
if (!ok) this.newRegion.parentId = null;
|
||
}
|
||
},
|
||
},
|
||
async mounted() {
|
||
await this.loadRegionTypes();
|
||
await this.loadRegions();
|
||
await this.loadConnections();
|
||
},
|
||
methods: {
|
||
onMapLoaded() {
|
||
const bounds = this.$refs.mapImage.getBoundingClientRect();
|
||
this.mapWidth = bounds.width;
|
||
this.mapHeight = bounds.height;
|
||
},
|
||
modeLabel(mode) {
|
||
if (!mode) return '';
|
||
const key = String(mode);
|
||
const tKey = `falukant.branch.transport.modes.${key}`;
|
||
const translated = this.$t(tKey);
|
||
if (!translated || translated === tKey) return mode;
|
||
return translated;
|
||
},
|
||
regionTypeLabel(labelTr) {
|
||
if (!labelTr) return '';
|
||
const raw = String(labelTr);
|
||
const keys = [
|
||
`falukant.politics.regionLevels.${raw}`,
|
||
`falukant.regionType.${raw}`,
|
||
];
|
||
for (const k of keys) {
|
||
const tr = this.$t(k);
|
||
if (tr && tr !== k) return tr;
|
||
}
|
||
return raw;
|
||
},
|
||
async loadRegions() {
|
||
try {
|
||
const { data } = await apiClient.get('/api/admin/falukant/regions/all');
|
||
// Sicherstellen, dass map-Objekte existieren oder null sind
|
||
this.regions = (data || []).map(r => ({
|
||
...r,
|
||
map: r.map || null,
|
||
}));
|
||
} catch (error) {
|
||
console.error('Error loading Falukant regions:', error);
|
||
}
|
||
},
|
||
async loadRegionTypes() {
|
||
try {
|
||
const { data } = await apiClient.get('/api/admin/falukant/region-types');
|
||
this.regionTypes = data || [];
|
||
} catch (error) {
|
||
console.error('Error loading Falukant region types:', error);
|
||
this.regionTypes = [];
|
||
}
|
||
},
|
||
async createRegion() {
|
||
if (this.creatingRegion || !this.canCreateRegion) return;
|
||
this.creatingRegion = true;
|
||
this.createRegionError = null;
|
||
try {
|
||
const payload = {
|
||
name: this.newRegion.name,
|
||
regionTypeId: this.newRegion.regionTypeId,
|
||
parentId: this.parentRegionEnabled ? this.newRegion.parentId : null,
|
||
};
|
||
const { data } = await apiClient.post('/api/admin/falukant/regions', payload);
|
||
|
||
await this.loadRegions();
|
||
if (data && data.id) {
|
||
const created = this.regions.find((r) => r.id === data.id) || null;
|
||
if (created) {
|
||
this.activeTab = 'regions';
|
||
this.selectRegion(created);
|
||
}
|
||
}
|
||
|
||
// Form zurücksetzen (Typ beibehalten, Name leeren)
|
||
this.newRegion.name = '';
|
||
if (this.parentRegionEnabled) {
|
||
this.newRegion.parentId = null;
|
||
}
|
||
} catch (error) {
|
||
console.error('Error creating region:', error);
|
||
const errKey = error?.response?.data?.error;
|
||
this.createRegionError = errKey ? String(errKey) : this.$t('admin.falukant.map.createRegion.error');
|
||
} finally {
|
||
this.creatingRegion = false;
|
||
}
|
||
},
|
||
async loadConnections() {
|
||
try {
|
||
const { data } = await apiClient.get('/api/admin/falukant/region-distances');
|
||
this.connections = data || [];
|
||
} catch (error) {
|
||
console.error('Error loading region distances:', error);
|
||
this.connections = [];
|
||
}
|
||
},
|
||
rectStyle(rect) {
|
||
if (!rect || !this.mapWidth || !this.mapHeight) return {};
|
||
|
||
// Unterstützt alte absolute Koordinaten (Pixel) und neue relative (0–1)
|
||
const toPxX = (v) => (v >= 0 && v <= 1 ? v * this.mapWidth : v);
|
||
const toPxY = (v) => (v >= 0 && v <= 1 ? v * this.mapHeight : v);
|
||
|
||
const x = toPxX(rect.x);
|
||
const y = toPxY(rect.y);
|
||
const w = toPxX(rect.w);
|
||
const h = toPxY(rect.h);
|
||
|
||
return {
|
||
top: `${y}px`,
|
||
left: `${x}px`,
|
||
width: `${w}px`,
|
||
height: `${h}px`,
|
||
};
|
||
},
|
||
regionClasses(region) {
|
||
return {
|
||
selected: this.selectedRegion && this.selectedRegion.id === region.id,
|
||
dirty: this.dirtyRegionIds.includes(region.id),
|
||
source: this.newConnection.sourceRegionId === region.id,
|
||
target: this.newConnection.targetRegionId === region.id,
|
||
};
|
||
},
|
||
regionListClasses(region) {
|
||
return {
|
||
selected: this.selectedRegion && this.selectedRegion.id === region.id,
|
||
dirty: this.dirtyRegionIds.includes(region.id),
|
||
source: this.newConnection.sourceRegionId === region.id,
|
||
target: this.newConnection.targetRegionId === region.id,
|
||
};
|
||
},
|
||
selectRegion(region) {
|
||
// Wenn wir im "auf Karte wählen"-Modus sind, setze Quelle/Ziel für die Verbindung
|
||
if (this.pickMode === 'source') {
|
||
this.newConnection.sourceRegionId = region.id;
|
||
this.pickMode = null;
|
||
} else if (this.pickMode === 'target') {
|
||
this.newConnection.targetRegionId = region.id;
|
||
this.pickMode = null;
|
||
}
|
||
|
||
this.selectedRegion = region;
|
||
this.selectedRegionDirty = this.dirtyRegionIds.includes(region.id);
|
||
this.drawingRect = null;
|
||
},
|
||
onMouseDown(event) {
|
||
// Zeichnen von Rechtecken nur im "Positionen"-Tab
|
||
if (this.activeTab !== 'regions') return;
|
||
if (!this.selectedRegion) return;
|
||
const bounds = this.$refs.mapImage.getBoundingClientRect();
|
||
this.startX = event.clientX - bounds.left;
|
||
this.startY = event.clientY - bounds.top;
|
||
this.currentX = this.startX;
|
||
this.currentY = this.startY;
|
||
this.updateDrawingRect();
|
||
event.preventDefault();
|
||
},
|
||
onMouseMove(event) {
|
||
// Zeichnen nur im "Positionen"-Tab
|
||
if (this.activeTab !== 'regions') return;
|
||
if (this.startX === null || this.startY === null) return;
|
||
const bounds = this.$refs.mapImage.getBoundingClientRect();
|
||
this.currentX = event.clientX - bounds.left;
|
||
this.currentY = event.clientY - bounds.top;
|
||
this.updateDrawingRect();
|
||
},
|
||
onMouseUp() {
|
||
// Im Entfernungen-Tab niemals Koordinaten verändern
|
||
if (this.activeTab !== 'regions') {
|
||
this.startX = null;
|
||
this.startY = null;
|
||
this.drawingRect = null;
|
||
return;
|
||
}
|
||
if (!this.selectedRegion || !this.drawingRect) {
|
||
this.startX = null;
|
||
this.startY = null;
|
||
return;
|
||
}
|
||
// Nur übernehmen, wenn tatsächlich ein Rechteck "aufgezogen" wurde
|
||
if (this.drawingRect.w < 3 || this.drawingRect.h < 3) {
|
||
this.drawingRect = null;
|
||
this.startX = null;
|
||
this.startY = null;
|
||
return;
|
||
}
|
||
// Übernehme gezeichnetes Rechteck in ausgewählte Region
|
||
const bounds = this.$refs.mapImage.getBoundingClientRect();
|
||
const updatedMap = {
|
||
x: this.drawingRect.x / bounds.width,
|
||
y: this.drawingRect.y / bounds.height,
|
||
w: this.drawingRect.w / bounds.width,
|
||
h: this.drawingRect.h / bounds.height,
|
||
};
|
||
this.selectedRegion.map = updatedMap;
|
||
this.markRegionDirty(this.selectedRegion.id);
|
||
this.startX = null;
|
||
this.startY = null;
|
||
this.drawingRect = null;
|
||
},
|
||
updateDrawingRect() {
|
||
if (this.startX === null || this.startY === null) return;
|
||
const x = Math.min(this.startX, this.currentX);
|
||
const y = Math.min(this.startY, this.currentY);
|
||
const w = Math.abs(this.currentX - this.startX);
|
||
const h = Math.abs(this.currentY - this.startY);
|
||
this.drawingRect = {
|
||
x: Math.round(x),
|
||
y: Math.round(y),
|
||
w: Math.round(w),
|
||
h: Math.round(h),
|
||
};
|
||
},
|
||
markRegionDirty(id) {
|
||
if (!this.dirtyRegionIds.includes(id)) {
|
||
this.dirtyRegionIds.push(id);
|
||
}
|
||
if (this.selectedRegion && this.selectedRegion.id === id) {
|
||
this.selectedRegionDirty = true;
|
||
}
|
||
},
|
||
async saveRegion(region) {
|
||
if (!region || !region.map) return;
|
||
await apiClient.put(`/api/admin/falukant/regions/${region.id}/map`, {
|
||
map: region.map,
|
||
});
|
||
// lokal als nicht mehr dirty markieren
|
||
this.dirtyRegionIds = this.dirtyRegionIds.filter(id => id !== region.id);
|
||
if (this.selectedRegion && this.selectedRegion.id === region.id) {
|
||
this.selectedRegionDirty = false;
|
||
}
|
||
},
|
||
async saveSelectedRegion() {
|
||
if (!this.selectedRegion || !this.selectedRegion.map) return;
|
||
try {
|
||
await this.saveRegion(this.selectedRegion);
|
||
// Liste aktualisieren, damit andere Einträge auch aktualisierte Daten sehen
|
||
await this.loadRegions();
|
||
const refreshed = this.regions.find(r => r.id === this.selectedRegion.id);
|
||
if (refreshed) {
|
||
this.selectedRegion = refreshed;
|
||
}
|
||
} catch (error) {
|
||
console.error('Error saving region map:', error);
|
||
}
|
||
},
|
||
async saveAllRegions() {
|
||
if (!this.dirtyRegionIds.length) return;
|
||
try {
|
||
const dirtyIds = [...this.dirtyRegionIds];
|
||
for (const id of dirtyIds) {
|
||
const region = this.regions.find(r => r.id === id && r.map);
|
||
if (!region) continue;
|
||
await this.saveRegion(region);
|
||
}
|
||
await this.loadRegions();
|
||
if (this.selectedRegion) {
|
||
const refreshed = this.regions.find(r => r.id === this.selectedRegion.id);
|
||
if (refreshed) {
|
||
this.selectedRegion = refreshed;
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Error saving all region maps:', error);
|
||
}
|
||
},
|
||
async saveConnection() {
|
||
try {
|
||
await apiClient.post('/api/admin/falukant/region-distances', {
|
||
sourceRegionId: this.newConnection.sourceRegionId,
|
||
targetRegionId: this.newConnection.targetRegionId,
|
||
transportMode: this.newConnection.transportMode,
|
||
distance: this.newConnection.distance,
|
||
});
|
||
await this.loadConnections();
|
||
} catch (error) {
|
||
console.error('Error saving region distance:', error);
|
||
showError(this, this.$t('admin.falukant.map.errorSaveConnection'));
|
||
}
|
||
},
|
||
async deleteConnection(id) {
|
||
const confirmed = await confirmAction(this, {
|
||
title: this.$t('admin.falukant.map.connectionsTitle'),
|
||
message: this.$t('admin.falukant.map.confirmDeleteConnection')
|
||
});
|
||
if (!confirmed) return;
|
||
try {
|
||
await apiClient.delete(`/api/admin/falukant/region-distances/${id}`);
|
||
await this.loadConnections();
|
||
} catch (error) {
|
||
console.error('Error deleting region distance:', error);
|
||
showError(this, this.$t('admin.falukant.map.errorDeleteConnection'));
|
||
}
|
||
},
|
||
regionName(id) {
|
||
const r = this.regions.find(x => x.id === id);
|
||
return r ? r.name : `#${id}`;
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.falukant-map-admin {
|
||
padding: 10px;
|
||
}
|
||
|
||
.map-layout {
|
||
display: flex;
|
||
gap: 20px;
|
||
}
|
||
|
||
.map-container {
|
||
position: relative;
|
||
display: inline-block;
|
||
}
|
||
|
||
.map {
|
||
max-width: 800px;
|
||
border: 1px solid #ccc;
|
||
}
|
||
|
||
.region-rect {
|
||
position: absolute;
|
||
border: 2px solid rgba(0, 128, 255, 0.7);
|
||
background-color: rgba(0, 128, 255, 0.2);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.region-rect.selected {
|
||
border-color: rgba(255, 128, 0, 0.9);
|
||
background-color: rgba(255, 128, 0, 0.25);
|
||
}
|
||
|
||
.region-rect.drawing {
|
||
border-style: dashed;
|
||
}
|
||
|
||
.sidebar {
|
||
min-width: 260px;
|
||
max-width: 320px;
|
||
}
|
||
|
||
.region-list {
|
||
list-style: none;
|
||
padding: 0;
|
||
margin: 0 0 1rem 0;
|
||
max-height: 400px;
|
||
overflow: auto;
|
||
border: 1px solid #ccc;
|
||
}
|
||
|
||
.region-list li {
|
||
padding: 4px 8px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.region-list li:nth-child(odd) {
|
||
background-color: #f8f8f8;
|
||
}
|
||
|
||
.region-list li.selected {
|
||
background-color: #e0f0ff;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.region-rect.source,
|
||
.region-rect.target {
|
||
border-color: rgba(0, 160, 0, 0.9);
|
||
background-color: rgba(0, 200, 0, 0.35);
|
||
}
|
||
|
||
.region-list li.source,
|
||
.region-list li.target {
|
||
background-color: #d2f5d2;
|
||
}
|
||
|
||
.region-list li.dirty {
|
||
font-style: italic;
|
||
}
|
||
|
||
.coords {
|
||
font-size: 0.8rem;
|
||
color: #555;
|
||
}
|
||
|
||
.coords.missing {
|
||
color: #b00;
|
||
}
|
||
|
||
.details {
|
||
margin-top: 1rem;
|
||
}
|
||
|
||
.create-region {
|
||
margin-top: 0.5rem;
|
||
padding: 0.5rem;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.create-region-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
}
|
||
|
||
.create-region-actions-cell {
|
||
text-align: right;
|
||
}
|
||
|
||
.error-text {
|
||
margin-left: 8px;
|
||
color: #b00;
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
.hint {
|
||
font-size: 0.85rem;
|
||
color: #555;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.connection-form-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.connection-form-table th,
|
||
.connection-form-table td {
|
||
padding: 2px 4px;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.label-cell {
|
||
width: 1%;
|
||
white-space: nowrap;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.field-cell {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 2px;
|
||
}
|
||
|
||
.field-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.connection-actions-cell {
|
||
text-align: right;
|
||
}
|
||
|
||
.btn.mini {
|
||
padding: 2px 6px;
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.btn.mini.icon {
|
||
}
|
||
</style>
|