Files
yourpart3/frontend/src/dialogues/falukant/CreateBranchDialog.vue

376 lines
9.8 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"
@mousedown="onMouseDown"
@mousemove="onMouseMove"
@mouseup="onMouseUp"
@mouseleave="onMouseUp"
>
<img
ref="mapImage"
src="/images/falukant/map.png"
class="map"
@load="onMapLoaded"
@dragstart.prevent
/>
<div
v-for="city in cities"
:key="city.name"
class="city-region"
:class="city.branches.length > 0 ? 'has-branch' : 'clickable'"
:style="cityRegionStyle(city.map)"
@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 form-field">
{{ $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>
<span class="form-hint">Wähle zuerst die Region und dann den Niederlassungstyp.</span>
</label>
</div>
<div v-else class="form-hint">Wähle auf der Karte eine freie Region aus.</div>
</div>
</div>
</div>
</DialogWidget>
</template>
<script>
import DialogWidget from '@/components/DialogWidget.vue';
import apiClient from '@/utils/axios.js';
import { showApiError, showError, showSuccess } from '@/utils/feedback.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,
mapWidth: 0,
mapHeight: 0,
};
},
computed: {
dialogButtons() {
return [
{ text: this.$t('Cancel'), action: this.close },
{ text: this.$t('falukant.branch.actions.create'), action: this.onConfirm, disabled: !this.selectedRegion || !this.selectedType },
];
},
},
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: {
onMapLoaded() {
const bounds = this.$refs.mapImage.getBoundingClientRect();
this.mapWidth = bounds.width;
this.mapHeight = bounds.height;
},
open() {
this.$refs.dialog.open();
},
close() {
this.$refs.dialog.close();
},
async onConfirm() {
if (!this.selectedRegion || !this.selectedType) {
showError(this, 'Bitte zuerst Region und Typ auswählen.');
return;
}
try {
await apiClient.post('/api/falukant/branches', {
cityId: this.selectedRegion.id,
branchTypeId: this.selectedType,
});
this.$emit('create-branch');
showSuccess(this, 'Niederlassung erfolgreich erstellt.');
this.close();
} catch (e) {
if (e?.response?.status === 412 && e?.response?.data?.error === 'insufficientFunds') {
showError(this, this.$t('falukant.branch.actions.insufficientFunds'));
} else {
console.error('Error creating branch', e);
showApiError(this, e, this.$t('falukant.error.generic') || 'Fehler beim Erstellen der Niederlassung.');
}
}
},
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),
};
},
cityRegionStyle(map) {
if (!map || !this.mapWidth || !this.mapHeight) return {};
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(map.x);
const y = toPxY(map.y);
const w = toPxX(map.w);
const h = toPxY(map.h);
return {
top: `${y}px`,
left: `${x}px`,
width: `${w}px`,
height: `${h}px`,
};
},
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>