feat(admin): implement delete functionality for Falukant regions and update UI components
This commit is contained in:
@@ -57,6 +57,7 @@ class AdminController {
|
|||||||
this.getFalukantAllRegions = this.getFalukantAllRegions.bind(this);
|
this.getFalukantAllRegions = this.getFalukantAllRegions.bind(this);
|
||||||
this.getFalukantRegionTypes = this.getFalukantRegionTypes.bind(this);
|
this.getFalukantRegionTypes = this.getFalukantRegionTypes.bind(this);
|
||||||
this.createFalukantRegion = this.createFalukantRegion.bind(this);
|
this.createFalukantRegion = this.createFalukantRegion.bind(this);
|
||||||
|
this.deleteFalukantRegion = this.deleteFalukantRegion.bind(this);
|
||||||
this.updateFalukantRegionMap = this.updateFalukantRegionMap.bind(this);
|
this.updateFalukantRegionMap = this.updateFalukantRegionMap.bind(this);
|
||||||
this.getRegionDistances = this.getRegionDistances.bind(this);
|
this.getRegionDistances = this.getRegionDistances.bind(this);
|
||||||
this.upsertRegionDistance = this.upsertRegionDistance.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) {
|
async updateFalukantRegionMap(req, res) {
|
||||||
try {
|
try {
|
||||||
const { userid: userId } = req.headers;
|
const { userid: userId } = req.headers;
|
||||||
|
|||||||
@@ -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.
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -65,6 +65,7 @@ router.get('/falukant/region-types', authenticate, adminController.getFalukantRe
|
|||||||
router.get('/falukant/regions', authenticate, adminController.getFalukantRegions);
|
router.get('/falukant/regions', authenticate, adminController.getFalukantRegions);
|
||||||
router.get('/falukant/regions/all', authenticate, adminController.getFalukantAllRegions);
|
router.get('/falukant/regions/all', authenticate, adminController.getFalukantAllRegions);
|
||||||
router.post('/falukant/regions', authenticate, adminController.createFalukantRegion);
|
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.put('/falukant/regions/:id/map', authenticate, adminController.updateFalukantRegionMap);
|
||||||
router.get('/falukant/region-distances', authenticate, adminController.getRegionDistances);
|
router.get('/falukant/region-distances', authenticate, adminController.getRegionDistances);
|
||||||
router.post('/falukant/region-distances', authenticate, adminController.upsertRegionDistance);
|
router.post('/falukant/region-distances', authenticate, adminController.upsertRegionDistance);
|
||||||
|
|||||||
@@ -934,6 +934,45 @@ class AdminService {
|
|||||||
return region;
|
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) {
|
async getRegionDistances(userId) {
|
||||||
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
|
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
|
||||||
throw new Error('noaccess');
|
throw new Error('noaccess');
|
||||||
|
|||||||
@@ -262,10 +262,35 @@
|
|||||||
"distances": "Entfernungen"
|
"distances": "Entfernungen"
|
||||||
},
|
},
|
||||||
"regionList": "Regionen",
|
"regionList": "Regionen",
|
||||||
|
"mapArea": "Karten-Arbeitsbereich",
|
||||||
|
"selectedRegion": "Ausgewählte Region",
|
||||||
|
"selectRegionHint": "Wähle eine Region aus der Liste oder direkt auf der Karte aus.",
|
||||||
|
"unknownType": "Unbekannter Typ",
|
||||||
|
"coordsCompact": "X {x}, Y {y} · B {w} × H {h}",
|
||||||
"noCoords": "Keine Koordinaten gesetzt",
|
"noCoords": "Keine Koordinaten gesetzt",
|
||||||
|
"positionSet": "Position gesetzt",
|
||||||
|
"coordinateX": "X-Position",
|
||||||
|
"coordinateY": "Y-Position",
|
||||||
|
"coordinateWidth": "Breite",
|
||||||
|
"coordinateHeight": "Höhe",
|
||||||
"currentRect": "Aktuelles Rechteck",
|
"currentRect": "Aktuelles Rechteck",
|
||||||
"hintDraw": "Wähle eine Region und ziehe mit der Maus ein Rechteck auf der Karte, um die Position festzulegen.",
|
"hintDraw": "Wähle eine Region und ziehe mit der Maus ein Rechteck auf der Karte, um die Position festzulegen.",
|
||||||
"saveAll": "Alle geänderten Regionen speichern",
|
"saveAll": "Alle geänderten Regionen speichern",
|
||||||
|
"saving": "Speichere…",
|
||||||
|
"saveSuccess": "Region wurde gespeichert.",
|
||||||
|
"saveAllSuccess": "Alle Änderungen wurden gespeichert.",
|
||||||
|
"errorSave": "Die Region konnte nicht gespeichert werden.",
|
||||||
|
"unsaved": "Nicht gespeichert",
|
||||||
|
"deleteRegion": "Region löschen",
|
||||||
|
"deleting": "Lösche…",
|
||||||
|
"deleteHint": "Das Löschen ist nur möglich, wenn keine Unterregionen oder Entfernungen vorhanden sind.",
|
||||||
|
"confirmDeleteRegion": "Region „{name}“ wirklich löschen? Dieser Vorgang kann nicht rückgängig gemacht werden.",
|
||||||
|
"deleteSuccess": "Region wurde gelöscht.",
|
||||||
|
"deleteError": "Die Region konnte nicht gelöscht werden.",
|
||||||
|
"deleteBlockedChildren": "Diese Region kann nicht gelöscht werden, weil sie Unterregionen enthält.",
|
||||||
|
"deleteBlockedDistances": "Diese Region kann nicht gelöscht werden, weil sie in Entfernungsverbindungen verwendet wird.",
|
||||||
|
"deleteBlockedInUse": "Diese Region kann nicht gelöscht werden, weil sie bereits von Spiel- oder Wetterdaten verwendet wird.",
|
||||||
|
"deleteBlockedNotFound": "Die Region wurde bereits gelöscht oder existiert nicht mehr.",
|
||||||
"createRegion": {
|
"createRegion": {
|
||||||
"title": "Neue Region anlegen",
|
"title": "Neue Region anlegen",
|
||||||
"type": "Regionstyp",
|
"type": "Regionstyp",
|
||||||
@@ -276,7 +301,8 @@
|
|||||||
"name": "Name",
|
"name": "Name",
|
||||||
"create": "Region anlegen",
|
"create": "Region anlegen",
|
||||||
"creating": "Lege an…",
|
"creating": "Lege an…",
|
||||||
"error": "Region konnte nicht angelegt werden."
|
"error": "Region konnte nicht angelegt werden.",
|
||||||
|
"success": "Region wurde angelegt."
|
||||||
},
|
},
|
||||||
"connectionsTitle": "Verbindungen (region_distance)",
|
"connectionsTitle": "Verbindungen (region_distance)",
|
||||||
"source": "Von",
|
"source": "Von",
|
||||||
|
|||||||
@@ -317,10 +317,35 @@
|
|||||||
"distances": "Distances"
|
"distances": "Distances"
|
||||||
},
|
},
|
||||||
"regionList": "Regions",
|
"regionList": "Regions",
|
||||||
|
"mapArea": "Map workspace",
|
||||||
|
"selectedRegion": "Selected region",
|
||||||
|
"selectRegionHint": "Select a region from the list or directly on the map.",
|
||||||
|
"unknownType": "Unknown type",
|
||||||
|
"coordsCompact": "X {x}, Y {y} · W {w} × H {h}",
|
||||||
"noCoords": "No coordinates set",
|
"noCoords": "No coordinates set",
|
||||||
|
"positionSet": "Position set",
|
||||||
|
"coordinateX": "X position",
|
||||||
|
"coordinateY": "Y position",
|
||||||
|
"coordinateWidth": "Width",
|
||||||
|
"coordinateHeight": "Height",
|
||||||
"currentRect": "Current rectangle",
|
"currentRect": "Current rectangle",
|
||||||
"hintDraw": "Select a region and drag a rectangle on the map to set its position.",
|
"hintDraw": "Select a region and drag a rectangle on the map to set its position.",
|
||||||
"saveAll": "Save all changed regions",
|
"saveAll": "Save all changed regions",
|
||||||
|
"saving": "Saving…",
|
||||||
|
"saveSuccess": "Region saved.",
|
||||||
|
"saveAllSuccess": "All changes saved.",
|
||||||
|
"errorSave": "Could not save the region.",
|
||||||
|
"unsaved": "Unsaved",
|
||||||
|
"deleteRegion": "Delete region",
|
||||||
|
"deleting": "Deleting…",
|
||||||
|
"deleteHint": "Deletion is available only when the region has no child regions or distance connections.",
|
||||||
|
"confirmDeleteRegion": "Delete region “{name}”? This cannot be undone.",
|
||||||
|
"deleteSuccess": "Region deleted.",
|
||||||
|
"deleteError": "Could not delete the region.",
|
||||||
|
"deleteBlockedChildren": "This region cannot be deleted because it has child regions.",
|
||||||
|
"deleteBlockedDistances": "This region cannot be deleted because it is used by distance connections.",
|
||||||
|
"deleteBlockedInUse": "This region cannot be deleted because it is already used by game or weather data.",
|
||||||
|
"deleteBlockedNotFound": "This region has already been deleted or no longer exists.",
|
||||||
"createRegion": {
|
"createRegion": {
|
||||||
"title": "Create new region",
|
"title": "Create new region",
|
||||||
"type": "Region type",
|
"type": "Region type",
|
||||||
@@ -331,7 +356,8 @@
|
|||||||
"name": "Name",
|
"name": "Name",
|
||||||
"create": "Create region",
|
"create": "Create region",
|
||||||
"creating": "Creating…",
|
"creating": "Creating…",
|
||||||
"error": "Could not create region."
|
"error": "Could not create region.",
|
||||||
|
"success": "Region created."
|
||||||
},
|
},
|
||||||
"connectionsTitle": "Connections (region_distance)",
|
"connectionsTitle": "Connections (region_distance)",
|
||||||
"source": "From",
|
"source": "From",
|
||||||
|
|||||||
@@ -6,13 +6,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="map-layout">
|
<div class="map-layout">
|
||||||
<div
|
<section class="map-workspace" :aria-label="$t('admin.falukant.map.mapArea')">
|
||||||
class="map-container"
|
<div class="map-instruction">
|
||||||
@mousedown="onMouseDown"
|
<span class="instruction-marker" aria-hidden="true">1</span>
|
||||||
@mousemove="onMouseMove"
|
{{ $t('admin.falukant.map.hintDraw') }}
|
||||||
@mouseup="onMouseUp"
|
</div>
|
||||||
@mouseleave="onMouseUp"
|
<div
|
||||||
>
|
class="map-container"
|
||||||
|
@mousedown="onMouseDown"
|
||||||
|
@mousemove="onMouseMove"
|
||||||
|
@mouseup="onMouseUp"
|
||||||
|
@mouseleave="onMouseUp"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
ref="mapImage"
|
ref="mapImage"
|
||||||
src="/images/falukant/map.png"
|
src="/images/falukant/map.png"
|
||||||
@@ -39,24 +44,26 @@
|
|||||||
class="region-rect drawing"
|
class="region-rect drawing"
|
||||||
:style="rectStyle(drawingRect)"
|
:style="rectStyle(drawingRect)"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div class="sidebar">
|
<aside class="sidebar">
|
||||||
<h2>{{ $t('admin.falukant.map.regionList') }}</h2>
|
<h2>{{ $t('admin.falukant.map.regionList') }}</h2>
|
||||||
<ul class="region-list">
|
<ul class="region-list">
|
||||||
<li
|
<li
|
||||||
v-for="region in regions"
|
v-for="region in regions"
|
||||||
:key="region.id"
|
:key="region.id"
|
||||||
:class="regionListClasses(region)"
|
:class="regionListClasses(region)"
|
||||||
@click="selectRegion(region)"
|
|
||||||
>
|
>
|
||||||
{{ region.name }}
|
<button type="button" class="region-list-entry" @click="selectRegion(region)">
|
||||||
<span v-if="region.map" class="coords">
|
<span class="region-list-name">{{ region.name }}</span>
|
||||||
({{ region.map.x }},{{ region.map.y }} {{ region.map.w }}×{{ region.map.h }})
|
<span class="type-badge">{{ regionTypeName(region) }}</span>
|
||||||
</span>
|
<span v-if="region.map" class="coords">
|
||||||
<span v-else class="coords missing">
|
{{ $t('admin.falukant.map.coordsCompact', { x: region.map.x, y: region.map.y, w: region.map.w, h: region.map.h }) }}
|
||||||
{{ $t('admin.falukant.map.noCoords') }}
|
</span>
|
||||||
</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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@@ -115,34 +122,42 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="selectedRegion" class="details">
|
<section v-if="selectedRegion" class="details" :aria-label="$t('admin.falukant.map.selectedRegion')">
|
||||||
<h3>{{ selectedRegion.name }}</h3>
|
<div class="detail-heading">
|
||||||
<p v-if="selectedRegion.map">
|
<div>
|
||||||
{{ $t('admin.falukant.map.currentRect') }}:
|
<p class="eyebrow">{{ $t('admin.falukant.map.selectedRegion') }}</p>
|
||||||
{{ selectedRegion.map.x }},{{ selectedRegion.map.y }}
|
<h3>{{ selectedRegion.name }}</h3>
|
||||||
{{ selectedRegion.map.w }}×{{ selectedRegion.map.h }}
|
</div>
|
||||||
</p>
|
<span class="type-badge">{{ regionTypeName(selectedRegion) }}</span>
|
||||||
<p v-else>
|
</div>
|
||||||
{{ $t('admin.falukant.map.noCoords') }}
|
<div class="selection-state">
|
||||||
</p>
|
<span :class="['position-status', { missing: !selectedRegion.map }]">
|
||||||
<p class="hint">
|
{{ selectedRegion.map ? $t('admin.falukant.map.positionSet') : $t('admin.falukant.map.noCoords') }}
|
||||||
{{ $t('admin.falukant.map.hintDraw') }}
|
</span>
|
||||||
</p>
|
<span v-if="selectedRegionDirty" class="unsaved-badge">{{ $t('admin.falukant.map.unsaved') }}</span>
|
||||||
<button
|
</div>
|
||||||
class="btn btn-primary"
|
<dl v-if="selectedRegion.map" class="coordinates-grid">
|
||||||
:disabled="!selectedRegionDirty"
|
<div><dt>{{ $t('admin.falukant.map.coordinateX') }}</dt><dd>{{ selectedRegion.map.x }}</dd></div>
|
||||||
@click="saveSelectedRegion"
|
<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>
|
||||||
{{ $t('common.save') }}
|
<div><dt>{{ $t('admin.falukant.map.coordinateHeight') }}</dt><dd>{{ selectedRegion.map.h }}</dd></div>
|
||||||
</button>
|
</dl>
|
||||||
<button
|
<div class="detail-actions">
|
||||||
class="btn btn-secondary"
|
<button class="btn btn-primary" :disabled="!selectedRegionDirty || savingRegion" @click="saveSelectedRegion">
|
||||||
:disabled="!dirtyRegionIds.length"
|
{{ savingRegion ? $t('admin.falukant.map.saving') : $t('common.save') }}
|
||||||
@click="saveAllRegions"
|
</button>
|
||||||
>
|
<button class="btn btn-secondary" :disabled="!dirtyRegionIds.length || savingAllRegions" @click="saveAllRegions">
|
||||||
{{ $t('admin.falukant.map.saveAll') }}
|
{{ savingAllRegions ? $t('admin.falukant.map.saving') : $t('admin.falukant.map.saveAll') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div v-else-if="activeTab === 'distances'" class="connections">
|
<div v-else-if="activeTab === 'distances'" class="connections">
|
||||||
@@ -261,7 +276,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -269,7 +284,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import apiClient from '@/utils/axios.js';
|
import apiClient from '@/utils/axios.js';
|
||||||
import SimpleTabs from '@/components/SimpleTabs.vue';
|
import SimpleTabs from '@/components/SimpleTabs.vue';
|
||||||
import { confirmAction, showError } from '@/utils/feedback.js';
|
import { confirmAction, showError, showSuccess } from '@/utils/feedback.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AdminFalukantMapRegionsView',
|
name: 'AdminFalukantMapRegionsView',
|
||||||
@@ -306,6 +321,9 @@ export default {
|
|||||||
},
|
},
|
||||||
creatingRegion: false,
|
creatingRegion: false,
|
||||||
createRegionError: null,
|
createRegionError: null,
|
||||||
|
savingRegion: false,
|
||||||
|
savingAllRegions: false,
|
||||||
|
deletingRegion: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -375,6 +393,10 @@ export default {
|
|||||||
}
|
}
|
||||||
return raw;
|
return raw;
|
||||||
},
|
},
|
||||||
|
regionTypeName(region) {
|
||||||
|
return this.regionTypeLabel(region?.regionType?.labelTr)
|
||||||
|
|| this.$t('admin.falukant.map.unknownType');
|
||||||
|
},
|
||||||
async loadRegions() {
|
async loadRegions() {
|
||||||
try {
|
try {
|
||||||
const { data } = await apiClient.get('/api/admin/falukant/regions/all');
|
const { data } = await apiClient.get('/api/admin/falukant/regions/all');
|
||||||
@@ -422,6 +444,7 @@ export default {
|
|||||||
if (this.parentRegionEnabled) {
|
if (this.parentRegionEnabled) {
|
||||||
this.newRegion.parentId = null;
|
this.newRegion.parentId = null;
|
||||||
}
|
}
|
||||||
|
showSuccess(this, this.$t('admin.falukant.map.createRegion.success'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating region:', error);
|
console.error('Error creating region:', error);
|
||||||
const errKey = error?.response?.data?.error;
|
const errKey = error?.response?.data?.error;
|
||||||
@@ -577,6 +600,7 @@ export default {
|
|||||||
},
|
},
|
||||||
async saveSelectedRegion() {
|
async saveSelectedRegion() {
|
||||||
if (!this.selectedRegion || !this.selectedRegion.map) return;
|
if (!this.selectedRegion || !this.selectedRegion.map) return;
|
||||||
|
this.savingRegion = true;
|
||||||
try {
|
try {
|
||||||
await this.saveRegion(this.selectedRegion);
|
await this.saveRegion(this.selectedRegion);
|
||||||
// Liste aktualisieren, damit andere Einträge auch aktualisierte Daten sehen
|
// Liste aktualisieren, damit andere Einträge auch aktualisierte Daten sehen
|
||||||
@@ -585,12 +609,17 @@ export default {
|
|||||||
if (refreshed) {
|
if (refreshed) {
|
||||||
this.selectedRegion = refreshed;
|
this.selectedRegion = refreshed;
|
||||||
}
|
}
|
||||||
|
showSuccess(this, this.$t('admin.falukant.map.saveSuccess'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving region map:', error);
|
console.error('Error saving region map:', error);
|
||||||
|
showError(this, this.$t('admin.falukant.map.errorSave'));
|
||||||
|
} finally {
|
||||||
|
this.savingRegion = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async saveAllRegions() {
|
async saveAllRegions() {
|
||||||
if (!this.dirtyRegionIds.length) return;
|
if (!this.dirtyRegionIds.length) return;
|
||||||
|
this.savingAllRegions = true;
|
||||||
try {
|
try {
|
||||||
const dirtyIds = [...this.dirtyRegionIds];
|
const dirtyIds = [...this.dirtyRegionIds];
|
||||||
for (const id of dirtyIds) {
|
for (const id of dirtyIds) {
|
||||||
@@ -605,8 +634,45 @@ export default {
|
|||||||
this.selectedRegion = refreshed;
|
this.selectedRegion = refreshed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
showSuccess(this, this.$t('admin.falukant.map.saveAllSuccess'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving all region maps:', 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() {
|
async saveConnection() {
|
||||||
@@ -647,22 +713,58 @@ export default {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.falukant-map-admin {
|
.falukant-map-admin {
|
||||||
padding: 10px;
|
padding: 12px;
|
||||||
|
color: #24313d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-layout {
|
.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;
|
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 {
|
.map-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
max-width: 100%;
|
||||||
|
line-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map {
|
.map {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #bac6d0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.region-rect {
|
.region-rect {
|
||||||
@@ -673,8 +775,9 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.region-rect.selected {
|
.region-rect.selected {
|
||||||
border-color: rgba(255, 128, 0, 0.9);
|
border-color: rgba(20, 97, 173, 1);
|
||||||
background-color: rgba(255, 128, 0, 0.25);
|
background-color: rgba(25, 118, 210, 0.28);
|
||||||
|
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.75);
|
||||||
}
|
}
|
||||||
|
|
||||||
.region-rect.drawing {
|
.region-rect.drawing {
|
||||||
@@ -682,8 +785,10 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
min-width: 260px;
|
min-width: 0;
|
||||||
max-width: 320px;
|
padding: 12px;
|
||||||
|
border: 1px solid #d7dee5;
|
||||||
|
background: #fbfcfd;
|
||||||
}
|
}
|
||||||
|
|
||||||
.region-list {
|
.region-list {
|
||||||
@@ -692,24 +797,48 @@ export default {
|
|||||||
margin: 0 0 1rem 0;
|
margin: 0 0 1rem 0;
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #cbd5df;
|
||||||
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.region-list li {
|
.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;
|
cursor: pointer;
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.region-list li:nth-child(odd) {
|
.region-list-entry:hover,
|
||||||
background-color: #f8f8f8;
|
.region-list-entry:focus-visible {
|
||||||
|
background: #eef6ff;
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.region-list li.selected {
|
.region-list li.selected .region-list-entry {
|
||||||
background-color: #e0f0ff;
|
background-color: #dcecff;
|
||||||
font-weight: bold;
|
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,
|
.region-rect.source,
|
||||||
@@ -724,10 +853,11 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.region-list li.dirty {
|
.region-list li.dirty {
|
||||||
font-style: italic;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coords {
|
.coords {
|
||||||
|
grid-column: 1 / -1;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: #555;
|
color: #555;
|
||||||
}
|
}
|
||||||
@@ -737,7 +867,129 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.details {
|
.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 {
|
.create-region {
|
||||||
@@ -811,4 +1063,37 @@ export default {
|
|||||||
|
|
||||||
.btn.mini.icon {
|
.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>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user