Files
yourpart3/frontend/src/dialogues/falukant/CreateBranchDialog.vue
2025-07-09 14:28:35 +02:00

342 lines
8.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<DialogWidget
ref="dialog"
name="create-branch"
:title="$t('falukant.branch.actions.create')"
icon="branch.png"
showClose
:buttons="dialogButtons"
@close="onClose"
>
<div class="create-branch-form">
<div class="map-wrapper">
<!-- linke Spalte: Karte + Regionen + Dev-Draw -->
<div class="map-container">
<img
ref="mapImage"
src="/images/falukant/map.png"
class="map"
@mousedown="onMouseDown"
@mousemove="onMouseMove"
@mouseup="onMouseUp"
@mouseleave="onMouseUp"
@dragstart.prevent
/>
<div
v-for="city in cities"
:key="city.name"
class="city-region"
:class="city.branches.length > 0 ? 'has-branch' : 'clickable'"
:style="{
top: city.map.y + 'px',
left: city.map.x + 'px',
width: city.map.w + 'px',
height: city.map.h + 'px'
}"
@click="city.branches.length === 0 && onCityClick(city)"
:title="city.name"
></div>
<div
v-if="devMode && rect"
class="dev-rect"
:style="{
top: rect.y + 'px',
left: rect.x + 'px',
width: rect.width + 'px',
height: rect.height + 'px'
}"
></div>
</div>
<!-- rechte Spalte: Dev-Info + Auswahl -->
<div class="sidebar">
<div v-if="devMode" class="dev-info">
<span class="dev-badge">DEV MODE</span>
<span v-if="rect" class="dev-label-outside">
{{ rect.x }},{{ rect.y }} {{ rect.width }}×{{ rect.height }}
</span>
</div>
<div v-if="selectedRegion" class="selected-region-wrapper">
<div class="selected-region">
{{ $t('falukant.branch.selection.selected') }}:
<strong>{{ selectedRegion.name }}</strong>
</div>
<label class="form-label">
{{ $t('falukant.branch.columns.type') }}
<select v-model="selectedType" class="form-control">
<option
v-for="type in branchTypes"
:key="type.id"
:value="type.id"
>
{{ $t(`falukant.branch.types.${type.labelTr}`) }}
({{ formatCost(computeBranchCost(type)) }})
</option>
</select>
</label>
</div>
</div>
</div>
</div>
</DialogWidget>
</template>
<script>
import DialogWidget from '@/components/DialogWidget.vue';
import apiClient from '@/utils/axios.js';
export default {
name: 'CreateBranchDialog',
components: { DialogWidget },
data() {
return {
cities: [],
branchTypes: [],
selectedRegion: null,
selectedType: null,
devMode: false,
rect: null,
startX: null,
startY: null,
currentX: 0,
currentY: 0,
};
},
computed: {
dialogButtons() {
return [
{ text: this.$t('Cancel'), action: this.close },
{ text: this.$t('falukant.branch.actions.create'), action: this.onConfirm },
];
},
},
async mounted() {
window.addEventListener('keydown', this.onKeyDown);
await Promise.all([
this.loadCities(),
this.loadBranchTypes(),
]);
this.selectedType = this.branchTypes.length ? this.branchTypes[0].id : null;
},
beforeDestroy() {
window.removeEventListener('keydown', this.onKeyDown);
},
methods: {
open() {
this.$refs.dialog.open();
},
close() {
this.$refs.dialog.close();
},
async onConfirm() {
if (!this.selectedRegion || !this.selectedType) return;
try {
await apiClient.post('/api/falukant/branches', {
cityId: this.selectedRegion.id,
branchTypeId: this.selectedType,
});
this.$emit('create-branch');
this.close();
} catch (e) {
}
},
onClose() {
this.close();
this.$emit('close');
},
onKeyDown(e) {
if (e.ctrlKey && e.altKey && e.code === 'KeyD') {
this.devMode = !this.devMode;
if (!this.devMode) this.rect = null;
}
},
onMouseDown(e) {
if (!this.devMode) return;
const bounds = this.$refs.mapImage.getBoundingClientRect();
this.startX = e.clientX - bounds.left;
this.startY = e.clientY - bounds.top;
this.currentX = this.startX;
this.currentY = this.startY;
this.updateRect();
e.preventDefault();
},
onMouseMove(e) {
if (!this.devMode || this.startX === null) return;
const bounds = this.$refs.mapImage.getBoundingClientRect();
this.currentX = e.clientX - bounds.left;
this.currentY = e.clientY - bounds.top;
this.updateRect();
},
onMouseUp() {
if (!this.devMode) return;
this.startX = null;
this.startY = null;
},
updateRect() {
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 width = Math.abs(this.currentX - this.startX);
const height = Math.abs(this.currentY - this.startY);
this.rect = {
x: Math.round(x),
y: Math.round(y),
width: Math.round(width),
height: Math.round(height),
};
},
async loadCities() {
const { data } = await apiClient.get('/api/falukant/cities');
this.cities = data;
},
onCityClick(city) {
this.selectedRegion = city;
},
async loadBranchTypes() {
const { data } = await apiClient.get('/api/falukant/branches/types');
this.branchTypes = data;
},
computeBranchCost(type) {
const total = this.cities.reduce((sum, city) => sum + city.branches.length, 0);
const factor = Math.pow(Math.max(total, 1), 1.2);
const raw = type.baseCost * factor;
return Math.round(raw * 100) / 100;
},
formatCost(value) {
return new Intl.NumberFormat(navigator.language, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(value);
},
},
};
</script>
<style scoped>
.create-branch-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.map-wrapper {
display: flex;
align-items: flex-start;
gap: 1rem;
}
.map-container {
position: relative;
width: fit-content;
}
.map {
max-width: 500px;
max-height: 400px;
user-select: none;
cursor: crosshair;
}
.city-region {
position: absolute;
}
.city-region.clickable {
cursor: pointer;
background: rgba(0, 0, 255, 0.2);
}
.city-region.has-branch {
cursor: default;
background: transparent;
border: 2px solid #00ff00;
}
.dev-rect {
position: absolute;
border: 2px dashed red;
pointer-events: none;
}
.sidebar {
display: flex;
flex-direction: column;
gap: 1rem;
min-width: 200px;
}
.dev-info {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.dev-badge {
background: rgba(255, 0, 0, 0.7);
color: white;
padding: 2px 6px;
font-size: 0.75rem;
border-radius: 3px;
pointer-events: none;
}
.dev-label-outside {
background: rgba(0, 0, 0, 0.7);
color: #fff;
font-size: 0.75rem;
padding: 2px 4px;
border-radius: 2px;
pointer-events: none;
}
.selected-region-wrapper {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.selected-region {
font-weight: 600;
}
.form-label {
font-weight: 600;
margin-bottom: 0.25rem;
}
.form-control {
padding: 0.5rem;
font-size: 1rem;
border: 1px solid #ccc;
border-radius: 4px;
}
</style>