Some fixes and additions
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
<p>{{ contact.message }}</p>
|
||||
</div>
|
||||
<div class="editor-container">
|
||||
<Editor v-model="answer" :init="tinymceInitOptions" :api-key="apiKey" />
|
||||
<EditorContent :editor="editor" class="editor" />
|
||||
</div>
|
||||
</DialogWidget>
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onBeforeUnmount } from 'vue'
|
||||
import Editor from '@tinymce/tinymce-vue'
|
||||
import { Editor, EditorContent } from '@tiptap/vue-3'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import apiClient from '@/utils/axios.js'
|
||||
import DialogWidget from '@/components/DialogWidget.vue'
|
||||
|
||||
@@ -28,29 +28,15 @@ export default {
|
||||
name: 'AnswerContact',
|
||||
components: {
|
||||
DialogWidget,
|
||||
Editor,
|
||||
EditorContent,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
apiKey: import.meta.env.VITE_TINYMCE_API_KEY,
|
||||
dialog: null,
|
||||
errorDialog: null,
|
||||
contact: null,
|
||||
answer: '',
|
||||
errorMessage: '',
|
||||
tinymceInitOptions: {
|
||||
height: 300,
|
||||
menubar: false,
|
||||
plugins: [
|
||||
'advlist autolink lists link image charmap print preview anchor',
|
||||
'searchreplace visualblocks code fullscreen',
|
||||
'insertdatetime media table paste code help wordcount'
|
||||
],
|
||||
toolbar:
|
||||
'undo redo cut copy paste | bold italic forecolor fontfamily fontsize | \
|
||||
alignleft aligncenter alignright alignjustify | \
|
||||
bullist numlist outdent indent | removeformat | help'
|
||||
},
|
||||
editor: null,
|
||||
buttons: [
|
||||
{ text: 'OK', action: this.sendAnswer },
|
||||
{ text: 'Cancel', action: this.closeDialog }
|
||||
@@ -64,24 +50,25 @@ export default {
|
||||
open(contactData) {
|
||||
this.contact = contactData;
|
||||
this.dialog.open();
|
||||
this.answer = '';
|
||||
if (this.editor) this.editor.commands.setContent('');
|
||||
},
|
||||
closeDialog() {
|
||||
this.dialog.close();
|
||||
this.answer = '';
|
||||
if (this.editor) this.editor.commands.clearContent();
|
||||
},
|
||||
closeErrorDialog() {
|
||||
this.errorDialog.close();
|
||||
},
|
||||
async sendAnswer() {
|
||||
const answer = this.editor ? this.editor.getHTML() : '';
|
||||
try {
|
||||
await apiClient.post('/api/admin/contacts/answer', {
|
||||
id: this.contact.id,
|
||||
answer: this.answer,
|
||||
answer,
|
||||
});
|
||||
this.dialog.close();
|
||||
this.$emit('refresh');
|
||||
this.answer = '';
|
||||
if (this.editor) this.editor.commands.clearContent();
|
||||
} catch (error) {
|
||||
const errorText = error.response?.data?.error || 'An unexpected error occurred.';
|
||||
this.errorMessage = errorText;
|
||||
@@ -92,9 +79,16 @@ export default {
|
||||
mounted() {
|
||||
this.dialog = this.$refs.dialog;
|
||||
this.errorDialog = this.$refs.errorDialog;
|
||||
|
||||
this.editor = new Editor({
|
||||
extensions: [StarterKit],
|
||||
content: '',
|
||||
});
|
||||
},
|
||||
beforeUnmount() {
|
||||
// Aufräumarbeiten falls nötig
|
||||
if (this.editor) {
|
||||
this.editor.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -106,5 +100,13 @@ export default {
|
||||
|
||||
.editor-container {
|
||||
margin-top: 20px;
|
||||
border: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.editor {
|
||||
min-height: 150px;
|
||||
outline: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
342
frontend/src/dialogues/falukant/CreateBranchDialog.vue
Normal file
342
frontend/src/dialogues/falukant/CreateBranchDialog.vue
Normal file
@@ -0,0 +1,342 @@
|
||||
<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>
|
||||
|
||||
@@ -51,8 +51,8 @@ export default {
|
||||
selectedProposal: null,
|
||||
products: [],
|
||||
buttons: [
|
||||
{ text: 'Einstellen', action: this.hireDirector },
|
||||
{ text: 'Abbrechen', action: 'close' },
|
||||
{ text: this.$t('falukant.newdirector.hire'), action: this.hireDirector },
|
||||
{ text: this.$t('Cancel'), action: 'close' },
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
@@ -55,8 +55,7 @@
|
||||
<img :src="imagePreview" alt="Image Preview"
|
||||
style="max-width: 100px; max-height: 100px;" />
|
||||
</div>
|
||||
<editor v-model="newEntryContent" :init="tinymceInitOptions" :api-key="apiKey"
|
||||
tinymce-script-src="/tinymce/tinymce.min.js"></editor>
|
||||
<EditorContent :editor="editor" class="editor" />
|
||||
</div>
|
||||
<button @click="submitGuestbookEntry">{{ $t('socialnetwork.profile.guestbook.submit')
|
||||
}}</button>
|
||||
@@ -95,14 +94,15 @@
|
||||
import DialogWidget from '@/components/DialogWidget.vue';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import FolderItem from '../../components/FolderItem.vue';
|
||||
import TinyMCEEditor from '@tinymce/tinymce-vue';
|
||||
import { Editor, EditorContent } from '@tiptap/vue-3'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
|
||||
export default {
|
||||
name: 'UserProfileDialog',
|
||||
components: {
|
||||
DialogWidget,
|
||||
FolderItem,
|
||||
editor: TinyMCEEditor,
|
||||
EditorContent,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -126,27 +126,20 @@ export default {
|
||||
{ name: 'guestbook', label: this.$t('socialnetwork.profile.tab.guestbook') }
|
||||
],
|
||||
apiKey: import.meta.env.VITE_TINYMCE_API_KEY,
|
||||
tinymceInitOptions: {
|
||||
script_url: '/tinymce/tinymce.min.js',
|
||||
height: 300,
|
||||
menubar: true,
|
||||
plugins: [
|
||||
'lists', 'link',
|
||||
'searchreplace', 'visualblocks', 'code',
|
||||
'insertdatetime', 'table'
|
||||
],
|
||||
toolbar:
|
||||
'undo redo cut copy paste | bold italic forecolor backcolor fontfamily fontsize| \
|
||||
alignleft aligncenter alignright alignjustify | \
|
||||
bullist numlist outdent indent | removeformat | link visualblocks code',
|
||||
contextmenu: 'link image table',
|
||||
menubar: 'edit format table',
|
||||
promotion: false,
|
||||
},
|
||||
editor: null,
|
||||
hasSendFriendshipRequest: false,
|
||||
friendshipState: 'none',
|
||||
};
|
||||
},
|
||||
mounted: async function () {
|
||||
this.editor = new Editor({
|
||||
extensions: [StarterKit],
|
||||
content: '',
|
||||
});
|
||||
},
|
||||
beforeUnmount: function () {
|
||||
if (this.editor) this.editor.destroy();
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.$refs.dialog.open();
|
||||
|
||||
Reference in New Issue
Block a user