Some fixes and additions
This commit is contained in:
1393
frontend/package-lock.json
generated
1393
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,13 +8,13 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tinymce/tinymce-vue": "^6.0.1",
|
||||
"@tiptap/starter-kit": "^2.14.0",
|
||||
"@tiptap/vue-3": "^2.14.0",
|
||||
"axios": "^1.7.2",
|
||||
"date-fns": "^3.6.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"mitt": "^3.0.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tinymce": "^7.3.0",
|
||||
"vue": "~3.4.31",
|
||||
"vue-i18n": "^10.0.0-beta.2",
|
||||
"vue-multiselect": "^3.1.0",
|
||||
@@ -30,6 +30,6 @@
|
||||
"sass": "^1.77.8",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"util": "^0.12.5",
|
||||
"vite": "^5.4.4"
|
||||
"vite": "^6.3.5"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
frontend/public/images/falukant/map.png
Normal file
BIN
frontend/public/images/falukant/map.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 MiB |
@@ -1,56 +1,63 @@
|
||||
<template>
|
||||
<div class="simple-tabs">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.value"
|
||||
:class="['simple-tab', { active: internalValue === tab.value }]"
|
||||
@click="$emit('update:modelValue', tab.value)"
|
||||
>
|
||||
<slot name="label" :tab="tab">
|
||||
{{ $t(tab.label) }}
|
||||
</slot>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SimpleTabs',
|
||||
props: {
|
||||
tabs: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
<div class="simple-tabs">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.value"
|
||||
:class="['simple-tab', { active: internalValue === tab.value }]"
|
||||
@click="selectTab(tab.value)"
|
||||
>
|
||||
<slot name="label" :tab="tab">
|
||||
{{ $t(tab.label) }}
|
||||
</slot>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SimpleTabs',
|
||||
props: {
|
||||
tabs: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
computed: {
|
||||
internalValue() {
|
||||
return this.modelValue;
|
||||
}
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
internalValue() {
|
||||
return this.modelValue;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectTab(value) {
|
||||
// 1) v-model aktualisieren
|
||||
this.$emit('update:modelValue', value);
|
||||
// 2) zusätzliches change-Event
|
||||
this.$emit('change', value);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.simple-tabs {
|
||||
display: flex;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.simple-tab {
|
||||
padding: 0.5rem 1rem;
|
||||
background: #fff;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.simple-tab.active {
|
||||
background: #F9A22C;
|
||||
color: #000;
|
||||
}
|
||||
</style>
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.simple-tabs {
|
||||
display: flex;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.simple-tab {
|
||||
padding: 0.5rem 1rem;
|
||||
background: #fff;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.simple-tab.active {
|
||||
background: #F9A22C;
|
||||
color: #000;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,64 +1,91 @@
|
||||
<template>
|
||||
<div class="branch-selection">
|
||||
<h3>{{ $t('falukant.branch.selection.title') }}</h3>
|
||||
<div>
|
||||
<FormattedDropdown
|
||||
:options="branches"
|
||||
:columns="branchColumns"
|
||||
v-model="localSelectedBranch"
|
||||
:placeholder="$t('falukant.branch.selection.placeholder')"
|
||||
@input="updateSelectedBranch"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button @click="$emit('createBranch')">{{ $t('falukant.branch.actions.create') }}</button>
|
||||
<button @click="$emit('upgradeBranch')" :disabled="!localSelectedBranch">
|
||||
{{ $t('falukant.branch.actions.upgrade') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="branch-selection">
|
||||
<h3>{{ $t('falukant.branch.selection.title') }}</h3>
|
||||
<div>
|
||||
<FormattedDropdown
|
||||
:options="branches"
|
||||
:columns="branchColumns"
|
||||
v-model="localSelectedBranch"
|
||||
:placeholder="$t('falukant.branch.selection.placeholder')"
|
||||
@input="updateSelectedBranch"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormattedDropdown from '@/components/form/FormattedDropdown.vue';
|
||||
export default {
|
||||
name: "BranchSelection",
|
||||
components: { FormattedDropdown },
|
||||
props: {
|
||||
branches: { type: Array, required: true },
|
||||
selectedBranch: { type: Object, default: null },
|
||||
<div>
|
||||
<button @click="openCreateBranchDialog">
|
||||
{{ $t('falukant.branch.actions.create') }}
|
||||
</button>
|
||||
<button
|
||||
@click="$emit('upgradeBranch')"
|
||||
:disabled="!localSelectedBranch"
|
||||
>
|
||||
{{ $t('falukant.branch.actions.upgrade') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dialog-Komponente -->
|
||||
<CreateBranchDialog
|
||||
ref="createBranchDialog"
|
||||
:regions="availableRegions"
|
||||
@create-branch="handleCreateBranch"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormattedDropdown from '@/components/form/FormattedDropdown.vue';
|
||||
import CreateBranchDialog from '@/dialogues/falukant/CreateBranchDialog.vue';
|
||||
|
||||
export default {
|
||||
name: "BranchSelection",
|
||||
components: {
|
||||
FormattedDropdown,
|
||||
CreateBranchDialog,
|
||||
},
|
||||
props: {
|
||||
branches: { type: Array, required: true },
|
||||
selectedBranch: { type: Object, default: null },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localSelectedBranch: this.selectedBranch,
|
||||
branchColumns: [
|
||||
{ field: "cityName", label: this.$t('falukant.branch.columns.city') },
|
||||
{ field: "type", label: this.$t('falukant.branch.columns.type') },
|
||||
],
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
selectedBranch(newVal) {
|
||||
this.localSelectedBranch = newVal;
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localSelectedBranch: this.selectedBranch,
|
||||
branchColumns: [
|
||||
{ field: "cityName", label: this.$t('falukant.branch.columns.city') },
|
||||
{ field: "type", label: this.$t('falukant.branch.columns.type') },
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateSelectedBranch(value) {
|
||||
this.$emit('branchSelected', value);
|
||||
},
|
||||
watch: {
|
||||
selectedBranch(newVal) {
|
||||
this.localSelectedBranch = newVal;
|
||||
},
|
||||
|
||||
openCreateBranchDialog() {
|
||||
this.$refs.createBranchDialog.open();
|
||||
},
|
||||
methods: {
|
||||
updateSelectedBranch(value) {
|
||||
this.$emit('branchSelected', value);
|
||||
},
|
||||
|
||||
handleCreateBranch() {
|
||||
// wird ausgelöst, sobald der Dialog onConfirm erfolgreich abschließt
|
||||
this.$emit('createBranch');
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.branch-selection {
|
||||
border: 1px solid #ccc;
|
||||
margin: 10px 0;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
}
|
||||
button {
|
||||
margin: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.branch-selection {
|
||||
border: 1px solid #ccc;
|
||||
margin: 10px 0;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
"age": "Alter",
|
||||
"wealth": "Vermögen",
|
||||
"health": "Gesundheit",
|
||||
"events": "Ereignisse"
|
||||
"events": "Ereignisse",
|
||||
"relationship": "Beziehung"
|
||||
},
|
||||
"health": {
|
||||
"amazing": "Super",
|
||||
@@ -97,7 +98,8 @@
|
||||
"selection": {
|
||||
"title": "Niederlassungsauswahl",
|
||||
"selected": "Ausgewählte Niederlassung",
|
||||
"placeholder": "Noch keine Niederlassung ausgewählt"
|
||||
"placeholder": "Noch keine Niederlassung ausgewählt",
|
||||
"selectedcity": "Ausgewählte Stadt"
|
||||
},
|
||||
"actions": {
|
||||
"create": "Neue Niederlassung erstellen",
|
||||
@@ -223,7 +225,6 @@
|
||||
"3": "Mittel",
|
||||
"4": "Hoch",
|
||||
"5": "Sehr hoch"
|
||||
|
||||
},
|
||||
"mood": "Stimmung",
|
||||
"progress": "Zuneigung",
|
||||
@@ -353,19 +354,32 @@
|
||||
"Fine Wine": "Feiner Wein",
|
||||
"Artisan Chocolate": "Kunsthandwerkliche Schokolade",
|
||||
"Pearl Necklace": "Perlenanhänger",
|
||||
"Rare Painting": "Seltenes Gemälde",
|
||||
"Rare Painting": "Seltenes Gemälde",
|
||||
"Silver Watch": "Silberuhr",
|
||||
"Cat": "Katze",
|
||||
"Dog": "Hund",
|
||||
"Cat": "Katze",
|
||||
"Dog": "Hund",
|
||||
"Horse": "Pferd"
|
||||
},
|
||||
"mood": {
|
||||
"happy": "Glücklich",
|
||||
"sad": "Traurig",
|
||||
"angry": "Wütend",
|
||||
"scared": "Verängstigt",
|
||||
"surprised": "Überrascht",
|
||||
"normal": "Normal"
|
||||
"nervous": "Nervös",
|
||||
"excited": "Aufgeregt",
|
||||
"bored": "Gelangweilt",
|
||||
"fearful": "Ängstlich",
|
||||
"confident": "Selbstbewusst",
|
||||
"curious": "Neugierig",
|
||||
"hopeful": "Hoffnungsvoll",
|
||||
"frustrated": "Frustriert",
|
||||
"lonely": "Einsam",
|
||||
"grateful": "Dankbar",
|
||||
"jealous": "Eifersüchtig",
|
||||
"guilty": "Schuldig",
|
||||
"apathetic": "Apathisch",
|
||||
"relieved": "Erleichtert",
|
||||
"proud": "Stolz",
|
||||
"ashamed": "Beschämt"
|
||||
},
|
||||
"character": {
|
||||
"brave": "Mutig",
|
||||
@@ -595,6 +609,72 @@
|
||||
"barber": "Barbier"
|
||||
},
|
||||
"choose": "Bitte auswählen"
|
||||
},
|
||||
"politics": {
|
||||
"title": "Politik",
|
||||
"tabs": {
|
||||
"current": "Aktuelle Position",
|
||||
"upcoming": "Anstehende Neuwahl-Positionen",
|
||||
"elections": "Wahlen"
|
||||
},
|
||||
"current": {
|
||||
"office": "Amt",
|
||||
"region": "Region",
|
||||
"termEnds": "Läuft ab am",
|
||||
"income": "Einkommen",
|
||||
"none": "Keine aktuelle Position vorhanden.",
|
||||
"holder": "Inhaber"
|
||||
},
|
||||
"open": {
|
||||
"office": "Amt",
|
||||
"region": "Region",
|
||||
"date": "Datum",
|
||||
"candidacy": "Kandidatur",
|
||||
"none": "Keine offenen Positionen."
|
||||
},
|
||||
"upcoming": {
|
||||
"office": "Amt",
|
||||
"region": "Region",
|
||||
"postDate": "Datum",
|
||||
"none": "Keine anstehenden Positionen."
|
||||
},
|
||||
"elections": {
|
||||
"office": "Amt",
|
||||
"region": "Region",
|
||||
"date": "Datum",
|
||||
"posts": "Zu besetzende Posten",
|
||||
"none": "Keine Wahlen vorhanden.",
|
||||
"choose": "Kandidaten",
|
||||
"vote": "Stimme abgeben",
|
||||
"voteAll": "Alle Stimmen abgeben",
|
||||
"candidates": "Kandidaten",
|
||||
"action": "Aktion"
|
||||
},
|
||||
"offices": {
|
||||
"chancellor": "Kanzler",
|
||||
"minister": "Minister",
|
||||
"ministry-helper": "Ministerhelfer",
|
||||
"governor": "Gouverneur",
|
||||
"super-state-administrator": "Oberstaatsverwalter",
|
||||
"state-administrator": "Staatsverwalter",
|
||||
"ruler-consultant": "Berater des Herrschers",
|
||||
"territorial-council-speaker": "Sprecher des Territorialrats",
|
||||
"territorial-council": "Territorialrat",
|
||||
"hangman": "Henker",
|
||||
"treasurer": "Schatzmeister",
|
||||
"sheriff": "Sheriff",
|
||||
"taxman": "Steuereintreiber",
|
||||
"bailif": "Gerichtsdiener",
|
||||
"judge": "Richter",
|
||||
"village-major": "Dorfvorsteher",
|
||||
"master-builder": "Baumeister",
|
||||
"mayor": "Bürgermeister",
|
||||
"town-clerk": "Stadtschreiber",
|
||||
"beadle": "Schulze",
|
||||
"council": "Ratsherr",
|
||||
"councillor": "Stadtrat",
|
||||
"assessor": "Schätzer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import EducationView from '../views/falukant/EducationView.vue';
|
||||
import BankView from '../views/falukant/BankView.vue';
|
||||
import DirectorView from '../views/falukant/DirectorView.vue';
|
||||
import HealthView from '../views/falukant/HealthView.vue';
|
||||
import PoliticsView from '../views/falukant/PoliticsView.vue';
|
||||
|
||||
const falukantRoutes = [
|
||||
{
|
||||
@@ -91,6 +92,12 @@ const falukantRoutes = [
|
||||
component: HealthView,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/falukant/politics',
|
||||
name: 'PoliticsView',
|
||||
component: PoliticsView,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
];
|
||||
|
||||
export default falukantRoutes;
|
||||
|
||||
@@ -1,236 +1,291 @@
|
||||
<template>
|
||||
<div class="contenthidden">
|
||||
<StatusBar ref="statusBar" />
|
||||
<div class="contentscroll">
|
||||
<h2>{{ $t('falukant.branch.title') }}</h2>
|
||||
<BranchSelection :branches="branches" :selectedBranch="selectedBranch" @branchSelected="onBranchSelected"
|
||||
@createBranch="createBranch" @upgradeBranch="upgradeBranch" ref="branchSelection" />
|
||||
<DirectorInfo v-if="selectedBranch" :branchId="selectedBranch.id" ref="directorInfo" />
|
||||
<SaleSection v-if="selectedBranch" :branchId="selectedBranch.id" ref="saleSection" />
|
||||
<ProductionSection v-if="selectedBranch" :branchId="selectedBranch.id" :products="products"
|
||||
ref="productionSection" />
|
||||
<StorageSection v-if="selectedBranch" :branchId="selectedBranch.id" ref="storageSection" />
|
||||
<RevenueSection v-if="selectedBranch" :products="products"
|
||||
:calculateProductRevenue="calculateProductRevenue" :calculateProductProfit="calculateProductProfit"
|
||||
ref="revenueSection" />
|
||||
</div>
|
||||
<StatusBar ref="statusBar" />
|
||||
<div class="contentscroll">
|
||||
<h2>{{ $t('falukant.branch.title') }}</h2>
|
||||
|
||||
<BranchSelection
|
||||
:branches="branches"
|
||||
:selectedBranch="selectedBranch"
|
||||
@branchSelected="onBranchSelected"
|
||||
@createBranch="createBranch"
|
||||
@upgradeBranch="upgradeBranch"
|
||||
ref="branchSelection"
|
||||
/>
|
||||
|
||||
<DirectorInfo
|
||||
v-if="selectedBranch"
|
||||
:branchId="selectedBranch.id"
|
||||
ref="directorInfo"
|
||||
/>
|
||||
|
||||
<SaleSection
|
||||
v-if="selectedBranch"
|
||||
:branchId="selectedBranch.id"
|
||||
ref="saleSection"
|
||||
/>
|
||||
|
||||
<ProductionSection
|
||||
v-if="selectedBranch"
|
||||
:branchId="selectedBranch.id"
|
||||
:products="products"
|
||||
ref="productionSection"
|
||||
/>
|
||||
|
||||
<StorageSection
|
||||
v-if="selectedBranch"
|
||||
:branchId="selectedBranch.id"
|
||||
ref="storageSection"
|
||||
/>
|
||||
|
||||
<RevenueSection
|
||||
v-if="selectedBranch"
|
||||
:products="products"
|
||||
:calculateProductRevenue="calculateProductRevenue"
|
||||
:calculateProductProfit="calculateProductProfit"
|
||||
ref="revenueSection"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import StatusBar from '@/components/falukant/StatusBar.vue';
|
||||
import BranchSelection from '@/components/falukant/BranchSelection.vue';
|
||||
import DirectorInfo from '@/components/falukant/DirectorInfo.vue';
|
||||
import SaleSection from '@/components/falukant/SaleSection.vue';
|
||||
import ProductionSection from '@/components/falukant/ProductionSection.vue';
|
||||
import StorageSection from '@/components/falukant/StorageSection.vue';
|
||||
import RevenueSection from '@/components/falukant/RevenueSection.vue';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import StatusBar from '@/components/falukant/StatusBar.vue';
|
||||
import BranchSelection from '@/components/falukant/BranchSelection.vue';
|
||||
import DirectorInfo from '@/components/falukant/DirectorInfo.vue';
|
||||
import SaleSection from '@/components/falukant/SaleSection.vue';
|
||||
import ProductionSection from '@/components/falukant/ProductionSection.vue';
|
||||
import StorageSection from '@/components/falukant/StorageSection.vue';
|
||||
import RevenueSection from '@/components/falukant/RevenueSection.vue';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: "BranchView",
|
||||
components: {
|
||||
StatusBar,
|
||||
BranchSelection,
|
||||
DirectorInfo,
|
||||
SaleSection,
|
||||
ProductionSection,
|
||||
StorageSection,
|
||||
RevenueSection,
|
||||
StatusBar,
|
||||
BranchSelection,
|
||||
DirectorInfo,
|
||||
SaleSection,
|
||||
ProductionSection,
|
||||
StorageSection,
|
||||
RevenueSection,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
branches: [],
|
||||
selectedBranch: null,
|
||||
products: [],
|
||||
};
|
||||
return {
|
||||
branches: [],
|
||||
selectedBranch: null,
|
||||
products: [],
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState(['socket', 'daemonSocket']),
|
||||
...mapState(['socket', 'daemonSocket']),
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
await this.loadBranches();
|
||||
const branchId = this.$route.params.branchId;
|
||||
await this.loadProducts();
|
||||
if (branchId) {
|
||||
this.selectedBranch =
|
||||
this.branches.find(branch => branch.id === parseInt(branchId)) || null;
|
||||
} else {
|
||||
this.selectMainBranch();
|
||||
await this.loadBranches();
|
||||
|
||||
const branchId = this.$route.params.branchId;
|
||||
await this.loadProducts();
|
||||
|
||||
if (branchId) {
|
||||
this.selectedBranch = this.branches.find(
|
||||
b => b.id === parseInt(branchId, 10)
|
||||
) || null;
|
||||
} else {
|
||||
this.selectMainBranch();
|
||||
}
|
||||
|
||||
// Daemon-Socket
|
||||
if (this.daemonSocket) {
|
||||
this.daemonSocket.addEventListener('message', this.handleDaemonMessage);
|
||||
}
|
||||
|
||||
// Live-Socket-Events
|
||||
[
|
||||
"production_ready",
|
||||
"stock_change",
|
||||
"price_update",
|
||||
"director_death",
|
||||
"production_started",
|
||||
"selled_items",
|
||||
"falukantUpdateStatus",
|
||||
"falukantBranchUpdate",
|
||||
"knowledge_update"
|
||||
].forEach(eventName => {
|
||||
if (this.socket) {
|
||||
this.socket.on(eventName, data => this.handleEvent({ event: eventName, ...data }));
|
||||
}
|
||||
const events = [
|
||||
"production_ready",
|
||||
"stock_change",
|
||||
"price_update",
|
||||
"director_death",
|
||||
"production_started",
|
||||
"selled_items",
|
||||
"falukantUpdateStatus",
|
||||
"falukantBranchUpdate",
|
||||
];
|
||||
if (this.daemonSocket) {
|
||||
this.daemonSocket.addEventListener('message', this.handleDaemonMessage);
|
||||
}
|
||||
events.forEach(eventName => {
|
||||
if (this.socket) {
|
||||
this.socket.on(eventName, (data) => {
|
||||
this.handleEvent({ event: eventName, ...data });
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
const events = [
|
||||
"production_ready",
|
||||
"stock_change",
|
||||
"price_update",
|
||||
"director_death",
|
||||
"production_started",
|
||||
"selled_items",
|
||||
"falukantUpdateStatus",
|
||||
"falukantBranchUpdate",
|
||||
];
|
||||
events.forEach(eventName => {
|
||||
if (this.socket) {
|
||||
this.socket.off(eventName, this.handleEvent);
|
||||
}
|
||||
});
|
||||
if (this.daemonSocket) {
|
||||
this.daemonSocket.removeEventListener('message', this.handleDaemonMessage);
|
||||
[
|
||||
"production_ready",
|
||||
"stock_change",
|
||||
"price_update",
|
||||
"director_death",
|
||||
"production_started",
|
||||
"selled_items",
|
||||
"falukantUpdateStatus",
|
||||
"falukantBranchUpdate",
|
||||
"knowledge_update"
|
||||
].forEach(eventName => {
|
||||
if (this.socket) {
|
||||
this.socket.off(eventName, this.handleEvent);
|
||||
}
|
||||
});
|
||||
|
||||
if (this.daemonSocket) {
|
||||
this.daemonSocket.removeEventListener('message', this.handleDaemonMessage);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async loadBranches() {
|
||||
try {
|
||||
const result = await apiClient.get('/api/falukant/branches');
|
||||
this.branches = result.data.map(branch => ({
|
||||
id: branch.id,
|
||||
cityName: branch.region.name,
|
||||
type: this.$t(`falukant.branch.types.${branch.branchType.labelTr}`),
|
||||
isMainBranch: branch.isMainBranch,
|
||||
}));
|
||||
if (!this.selectedBranch) {
|
||||
this.selectMainBranch();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading branches:', error);
|
||||
}
|
||||
},
|
||||
async loadProducts() {
|
||||
try {
|
||||
const productsResult = await apiClient.get('/api/falukant/products');
|
||||
this.products = productsResult.data;
|
||||
} catch (error) {
|
||||
console.error('Error loading products:', error);
|
||||
}
|
||||
},
|
||||
handleEvent(event) {
|
||||
if (event.type === 'branchUpdated') {
|
||||
this.loadBranches();
|
||||
}
|
||||
},
|
||||
handleDaemonMessage(event) {
|
||||
const message = JSON.parse(event.data);
|
||||
},
|
||||
selectMainBranch() {
|
||||
const main = this.branches.find(b => b.isMainBranch) || null;
|
||||
if (main && main !== this.selectedBranch) {
|
||||
this.selectedBranch = main;
|
||||
}
|
||||
},
|
||||
onBranchSelected(newBranch) {
|
||||
this.selectedBranch = newBranch;
|
||||
},
|
||||
createBranch() {
|
||||
alert(this.$t('falukant.branch.actions.createAlert'));
|
||||
},
|
||||
upgradeBranch() {
|
||||
if (this.selectedBranch) {
|
||||
alert(this.$t('falukant.branch.actions.upgradeAlert', { branchId: this.selectedBranch.id }));
|
||||
}
|
||||
},
|
||||
calculateProductRevenue(product) {
|
||||
if (!product.knowledges || product.knowledges.length === 0) {
|
||||
return { absolute: 0, perMinute: 0 };
|
||||
}
|
||||
const knowledgeFactor = product.knowledges[0].knowledge || 0;
|
||||
const maxPrice = product.sellCost;
|
||||
const minPrice = maxPrice * 0.6;
|
||||
const revenuePerUnit = minPrice + (maxPrice - minPrice) * (knowledgeFactor / 100);
|
||||
const perMinute = product.productionTime > 0 ? revenuePerUnit / product.productionTime : 0;
|
||||
return {
|
||||
absolute: revenuePerUnit.toFixed(2),
|
||||
perMinute: perMinute.toFixed(2),
|
||||
};
|
||||
},
|
||||
calculateProductProfit(product) {
|
||||
const { absolute: revenueAbsoluteStr, perMinute: revenuePerMinuteStr } = this.calculateProductRevenue(product);
|
||||
const revenueAbsolute = parseFloat(revenueAbsoluteStr);
|
||||
const costPerUnit = 6 * product.category;
|
||||
const profitAbsolute = revenueAbsolute - costPerUnit;
|
||||
const costPerMinute = product.productionTime > 0 ? costPerUnit / product.productionTime : 0;
|
||||
const profitPerMinute = parseFloat(revenuePerMinuteStr) - costPerMinute;
|
||||
return {
|
||||
absolute: profitAbsolute.toFixed(2),
|
||||
perMinute: profitPerMinute.toFixed(2),
|
||||
};
|
||||
},
|
||||
// Gemeinsamer Event-Handler für socket-Events
|
||||
handleEvent(eventData) {
|
||||
switch (eventData.event || eventData) {
|
||||
case 'production_ready':
|
||||
this.$refs.productionSection && this.$refs.productionSection.loadProductions();
|
||||
this.$refs.storageSection && this.$refs.storageSection.loadStorageData();
|
||||
this.$refs.saleSection && this.$refs.saleSection.loadInventory();
|
||||
break;
|
||||
case 'stock_change':
|
||||
this.$refs.storageSection && this.$refs.storageSection.loadStorageData();
|
||||
this.$refs.saleSection && this.$refs.saleSection.loadInventory();
|
||||
break;
|
||||
case 'price_update':
|
||||
this.$refs.revenueSection && this.$refs.revenueSection.refresh && this.$refs.revenueSection.refresh();
|
||||
break;
|
||||
case 'director_death':
|
||||
this.$refs.directorInfo && this.$refs.directorInfo.loadDirector();
|
||||
break;
|
||||
case 'production_started':
|
||||
this.$refs.productionSection && this.$refs.productionSection.loadProductions();
|
||||
break;
|
||||
case 'selled_items':
|
||||
this.$refs.saleSection && this.$refs.saleSection.loadInventory();
|
||||
this.$refs.storageSection && this.$refs.storageSection.loadStorageData();
|
||||
break;
|
||||
case 'falukantUpdateStatus':
|
||||
case 'falukantBranchUpdate':
|
||||
this.$refs.statusBar && this.$refs.statusBar.updateStatus && this.$refs.statusBar.updateStatus(eventData);
|
||||
this.$refs.productionSection && this.$refs.productionSection.loadProductions();
|
||||
this.$refs.storageSection && this.$refs.storageSection.loadStorageData();
|
||||
this.$refs.saleSection && this.$refs.saleSection.loadInventory();
|
||||
break;
|
||||
case 'knowledge_update':
|
||||
this.loadProducts();
|
||||
this.$refs.revenueSection.products = this.products;
|
||||
break;
|
||||
default:
|
||||
console.log('Unhandled event:', eventData);
|
||||
}
|
||||
},
|
||||
handleDaemonMessage(event) {
|
||||
if (event.data === "ping") return;
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
this.handleEvent(message);
|
||||
} catch (error) {
|
||||
console.error('Error processing daemon message in BranchView:', error);
|
||||
}
|
||||
},
|
||||
async loadBranches() {
|
||||
try {
|
||||
const result = await apiClient.get('/api/falukant/branches');
|
||||
this.branches = result.data.map(branch => ({
|
||||
id: branch.id,
|
||||
cityName: branch.region.name,
|
||||
type: this.$t(`falukant.branch.types.${branch.branchType.labelTr}`),
|
||||
isMainBranch: branch.isMainBranch,
|
||||
}));
|
||||
if (!this.selectedBranch) {
|
||||
this.selectMainBranch();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading branches:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async loadProducts() {
|
||||
try {
|
||||
const productsResult = await apiClient.get('/api/falukant/products');
|
||||
this.products = productsResult.data;
|
||||
} catch (error) {
|
||||
console.error('Error loading products:', error);
|
||||
}
|
||||
},
|
||||
|
||||
onBranchSelected(newBranch) {
|
||||
this.selectedBranch = newBranch;
|
||||
},
|
||||
|
||||
async createBranch() {
|
||||
// Nach erfolgreichem Dialog-Event: neu laden
|
||||
await this.loadBranches();
|
||||
},
|
||||
|
||||
upgradeBranch() {
|
||||
if (this.selectedBranch) {
|
||||
alert(
|
||||
this.$t(
|
||||
'falukant.branch.actions.upgradeAlert',
|
||||
{ branchId: this.selectedBranch.id }
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
selectMainBranch() {
|
||||
const main = this.branches.find(b => b.isMainBranch) || null;
|
||||
if (main && main !== this.selectedBranch) {
|
||||
this.selectedBranch = main;
|
||||
}
|
||||
},
|
||||
|
||||
calculateProductRevenue(product) {
|
||||
if (!product.knowledges || product.knowledges.length === 0) {
|
||||
return { absolute: 0, perMinute: 0 };
|
||||
}
|
||||
const knowledgeFactor = product.knowledges[0].knowledge || 0;
|
||||
const maxPrice = product.sellCost;
|
||||
const minPrice = maxPrice * 0.6;
|
||||
const revenuePerUnit = minPrice + (maxPrice - minPrice) * (knowledgeFactor / 100);
|
||||
const perMinute = product.productionTime > 0
|
||||
? revenuePerUnit / product.productionTime
|
||||
: 0;
|
||||
return {
|
||||
absolute: revenuePerUnit.toFixed(2),
|
||||
perMinute: perMinute.toFixed(2),
|
||||
};
|
||||
},
|
||||
|
||||
calculateProductProfit(product) {
|
||||
const { absolute: revenueAbsoluteStr, perMinute: revenuePerMinuteStr }
|
||||
= this.calculateProductRevenue(product);
|
||||
const revenueAbsolute = parseFloat(revenueAbsoluteStr);
|
||||
const costPerUnit = 6 * product.category;
|
||||
const profitAbsolute = revenueAbsolute - costPerUnit;
|
||||
const costPerMinute = product.productionTime > 0
|
||||
? costPerUnit / product.productionTime
|
||||
: 0;
|
||||
const profitPerMinute = parseFloat(revenuePerMinuteStr) - costPerMinute;
|
||||
return {
|
||||
absolute: profitAbsolute.toFixed(2),
|
||||
perMinute: profitPerMinute.toFixed(2),
|
||||
};
|
||||
},
|
||||
|
||||
handleEvent(eventData) {
|
||||
switch (eventData.event) {
|
||||
case 'production_ready':
|
||||
this.$refs.productionSection?.loadProductions();
|
||||
this.$refs.storageSection ?.loadStorageData();
|
||||
this.$refs.saleSection ?.loadInventory();
|
||||
break;
|
||||
case 'stock_change':
|
||||
this.$refs.storageSection ?.loadStorageData();
|
||||
this.$refs.saleSection ?.loadInventory();
|
||||
break;
|
||||
case 'price_update':
|
||||
this.$refs.revenueSection?.refresh();
|
||||
break;
|
||||
case 'director_death':
|
||||
this.$refs.directorInfo?.loadDirector();
|
||||
break;
|
||||
case 'production_started':
|
||||
this.$refs.productionSection?.loadProductions();
|
||||
break;
|
||||
case 'selled_items':
|
||||
this.$refs.saleSection ?.loadInventory();
|
||||
this.$refs.storageSection?.loadStorageData();
|
||||
break;
|
||||
case 'falukantUpdateStatus':
|
||||
case 'falukantBranchUpdate':
|
||||
this.$refs.statusBar?.fetchStatus();
|
||||
this.$refs.productionSection?.loadProductions();
|
||||
this.$refs.storageSection ?.loadStorageData();
|
||||
this.$refs.saleSection ?.loadInventory();
|
||||
break;
|
||||
case 'knowledge_update':
|
||||
this.loadProducts();
|
||||
this.$refs.revenueSection.products = this.products;
|
||||
break;
|
||||
default:
|
||||
console.log('Unhandled event:', eventData);
|
||||
}
|
||||
},
|
||||
|
||||
handleDaemonMessage(event) {
|
||||
if (event.data === 'ping') return;
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
this.handleEvent(message);
|
||||
} catch (error) {
|
||||
console.error('Error processing daemon message:', error);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
h2 {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
h2 {
|
||||
padding-top: 20px;
|
||||
}
|
||||
</style>
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -43,12 +43,13 @@
|
||||
</tr>
|
||||
<tr v-if="relationships[0].relationshipType === 'engaged'" colspan="2">
|
||||
<button @click="jumpToPartyForm">{{ $t('falukant.family.spouse.jumpToPartyForm')
|
||||
}}</button>
|
||||
}}</button>
|
||||
</tr>
|
||||
</table>
|
||||
<ul>
|
||||
<li v-for="characteristic in relationships[0].character2.characterTrait"
|
||||
:key="characteristic.id">{{ $t(`falukant.character.${characteristic.tr}`) }}</li>
|
||||
<li v-for="trait in relationships[0].character2.traits" :key="trait.id">
|
||||
{{ $t(`falukant.character.${trait.tr}`) }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="relationships[0].relationshipType === 'wooing'">
|
||||
@@ -64,16 +65,18 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="gift in gifts" :key="gift.id">
|
||||
<td><input type="radio" name="gift" :value="gift.id" v-model="selectedGiftId"></td>
|
||||
<td>
|
||||
<input type="radio" name="gift" :value="gift.id" v-model="selectedGiftId" />
|
||||
</td>
|
||||
<td>{{ $t(`falukant.gifts.${gift.name}`) }}</td>
|
||||
<td>{{ $t(`falukant.family.spouse.giftAffect.${getEffect(gift)}`) }}</td>
|
||||
<td>{{ getEffect(gift) }}</td>
|
||||
<td>{{ formatCost(gift.cost) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div>
|
||||
<button @click="sendGift" class="button">{{ $t('falukant.family.spouse.wooing.sendGift')
|
||||
}}</button>
|
||||
}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -93,7 +96,7 @@
|
||||
v-model="selectedProposalId"></td>
|
||||
<td>{{
|
||||
$t(`falukant.titles.${proposal.proposedCharacterGender}.${proposal.proposedCharacterNobleTitle}`)
|
||||
}} {{ proposal.proposedCharacterName }}</td>
|
||||
}} {{ proposal.proposedCharacterName }}</td>
|
||||
<td>{{ proposal.proposedCharacterAge }}</td>
|
||||
<td>{{ formatCost(proposal.cost) }}</td>
|
||||
</tr>
|
||||
@@ -123,7 +126,8 @@
|
||||
{{ child.name }}
|
||||
</td>
|
||||
<td v-else>
|
||||
<button @click="jumpToChurchForm">{{ $t('falukant.family.children.baptism') }}</button>
|
||||
<button @click="jumpToChurchForm">{{ $t('falukant.family.children.baptism')
|
||||
}}</button>
|
||||
</td>
|
||||
<td>{{ child.age }}</td>
|
||||
<td>
|
||||
@@ -308,12 +312,38 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
handleDaemonMessage() {
|
||||
handleDaemonMessage(event) {
|
||||
if (event.data === 'ping') {
|
||||
return;
|
||||
}
|
||||
const message = JSON.parse(event.data);
|
||||
if (message.event === 'children_update') {
|
||||
this.loadFamilyData();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getEffect(gift) {
|
||||
// aktueller Partner
|
||||
const partner = this.relationships[0].character2;
|
||||
// seine aktuelle Mood-ID
|
||||
const moodId = partner.mood?.id ?? partner.mood_id;
|
||||
|
||||
// 1) Mood-Eintrag finden
|
||||
const moodEntry = gift.moodsAffects.find(ma => ma.mood_id === moodId);
|
||||
const moodValue = moodEntry ? moodEntry.suitability : 0;
|
||||
|
||||
// 2) Trait-Einträge matchen
|
||||
let highestTraitValue = 0;
|
||||
for (const trait of partner.traits) {
|
||||
const charEntry = gift.charactersAffects.find(ca => ca.trait_id === trait.id);
|
||||
if (charEntry && charEntry.suitability > highestTraitValue) {
|
||||
highestTraitValue = charEntry.suitability;
|
||||
}
|
||||
}
|
||||
|
||||
// Durchschnitt, gerundet
|
||||
return Math.round((moodValue + highestTraitValue) / 2);
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -13,14 +13,15 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('falukant.overview.metadata.nobleTitle') }}</td>
|
||||
<td>{{ $t('falukant.titles.' + falukantUser?.character.gender + '.' + falukantUser?.character.nobleTitle.labelTr) }}</td>
|
||||
<td>{{ $t('falukant.titles.' + falukantUser?.character.gender + '.' +
|
||||
falukantUser?.character.nobleTitle.labelTr) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('falukant.overview.metadata.money') }}</td>
|
||||
<td>
|
||||
{{ moneyValue != null
|
||||
? moneyValue.toLocaleString(locale, { style: 'currency', currency: 'EUR' })
|
||||
: '---' }}
|
||||
: '---' }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -170,17 +171,24 @@ export default {
|
||||
};
|
||||
},
|
||||
getHouseStyle() {
|
||||
if (!this.falukantUser) return {};
|
||||
console.log(this.falukantUser);
|
||||
if (!this.falukantUser || !this.falukantUser.userHouse?.houseType) return {};
|
||||
const imageUrl = '/images/falukant/houses.png';
|
||||
const housePosition = this.falukantUser.house ? this.falukantUser.house.type.position : 0;
|
||||
const x = housePosition % 3;
|
||||
const y = Math.floor(housePosition / 3);
|
||||
const pos = this.falukantUser.userHouse.houseType.position;
|
||||
const index = pos - 1;
|
||||
const columns = 3;
|
||||
const spriteSize = 300;
|
||||
const x = (index % columns) * spriteSize;
|
||||
const y = Math.floor(index / columns) * spriteSize;
|
||||
return {
|
||||
backgroundImage: `url(${imageUrl})`,
|
||||
backgroundPosition: `-${x * 341}px -${y * 341}px`,
|
||||
backgroundSize: "341px 341px",
|
||||
width: "114px",
|
||||
height: "114px",
|
||||
backgroundPosition: `-${x}px -${y}px`,
|
||||
backgroundSize: `${columns * spriteSize}px auto`,
|
||||
width: `300px`,
|
||||
height: `300px`,
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: '4px',
|
||||
imageRendering: 'crisp-edges',
|
||||
};
|
||||
},
|
||||
getAgeColor(age) {
|
||||
@@ -321,12 +329,10 @@ export default {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
|
||||
h2 {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
405
frontend/src/views/falukant/PoliticsView.vue
Normal file
405
frontend/src/views/falukant/PoliticsView.vue
Normal file
@@ -0,0 +1,405 @@
|
||||
<template>
|
||||
<div class="politics-view">
|
||||
<StatusBar />
|
||||
|
||||
<h2>{{ $t('falukant.politics.title') }}</h2>
|
||||
|
||||
<SimpleTabs v-model="activeTab" :tabs="tabs" @change="onTabChange" />
|
||||
|
||||
<!-- Tab‐Inhalt -->
|
||||
<div class="tab-content">
|
||||
<!-- Aktuelle Positionen -->
|
||||
<div v-if="activeTab === 'current'" class="tab-pane">
|
||||
<div v-if="loading.current" class="loading">{{ $t('loading') }}</div>
|
||||
<div v-else class="table-scroll">
|
||||
<table class="politics-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('falukant.politics.current.office') }}</th>
|
||||
<th>{{ $t('falukant.politics.current.region') }}</th>
|
||||
<th>{{ $t('falukant.politics.current.holder') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="pos in currentPositions" :key="pos.id">
|
||||
<td>{{ $t(`falukant.politics.offices.${pos.officeType.name}`) }}</td>
|
||||
<td>{{ pos.region.name }}</td>
|
||||
<td>
|
||||
<span v-if="pos.character">
|
||||
{{ pos.character.definedFirstName.name }}
|
||||
{{ pos.character.definedLastName.name }}
|
||||
</span>
|
||||
<span v-else>—</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!currentPositions.length">
|
||||
<td colspan="3">{{ $t('falukant.politics.current.none') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OPEN Tab: hier zeigen wir 'openPolitics' -->
|
||||
<div v-else-if="activeTab === 'openPolitics'" class="tab-pane">
|
||||
<div v-if="loading.openPolitics" class="loading">{{ $t('loading') }}</div>
|
||||
<div v-else class="table-scroll">
|
||||
<table class="politics-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('falukant.politics.open.office') }}</th>
|
||||
<th>{{ $t('falukant.politics.open.region') }}</th>
|
||||
<th>{{ $t('falukant.politics.open.date') }}</th>
|
||||
<th>{{ $t('falukant.politics.open.candidacy') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="e in openPolitics" :key="e.id">
|
||||
<td>{{ $t(`falukant.politics.offices.${e.officeType.name}`) }}</td>
|
||||
<td>{{ e.region.name }}</td>
|
||||
<td>{{ formatDate(e.date) }}</td>
|
||||
<!-- Checkbox ganz am Ende -->
|
||||
<td>
|
||||
<input type="checkbox" :id="`apply-${e.id}`" v-model="selectedApplications"
|
||||
:value="e.id" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!openPolitics.length">
|
||||
<td colspan="4">{{ $t('falukant.politics.open.none') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="apply-button">
|
||||
<button :disabled="!selectedApplications.length" @click="submitApplications">
|
||||
{{ $t('falukant.politics.open.apply') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Wahlen -->
|
||||
<div v-else-if="activeTab === 'elections'" class="tab-pane">
|
||||
<div v-if="loading.elections" class="loading">{{ $t('loading') }}</div>
|
||||
<div v-else class="table-scroll">
|
||||
<table class="politics-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('falukant.politics.elections.office') }}</th>
|
||||
<th>{{ $t('falukant.politics.elections.region') }}</th>
|
||||
<th>{{ $t('falukant.politics.elections.date') }}</th>
|
||||
<th>{{ $t('falukant.politics.elections.posts') }}</th>
|
||||
<th>{{ $t('falukant.politics.elections.candidates') }}</th>
|
||||
<th>{{ $t('falukant.politics.elections.action') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="e in elections" :key="e.id">
|
||||
<td>{{ $t(`falukant.politics.offices.${e.officeType.name}`) }}</td>
|
||||
<td>{{ e.region.name }}</td>
|
||||
<td>{{ formatDate(e.date) }}</td>
|
||||
<td>{{ e.postsToFill }}</td>
|
||||
<td v-if="!e.voted">
|
||||
<Multiselect v-model="selectedCandidates[e.id]" :options="e.candidates" multiple
|
||||
:max="e.postsToFill" :close-on-select="false" :clear-on-select="false"
|
||||
track-by="id" label="name" :custom-label="candidateLabel" placeholder="">
|
||||
<template #option="{ option }">
|
||||
{{ $t(`falukant.titles.${option.gender}.${option.title}`) }}
|
||||
{{ option.name }} ({{ option.age }})
|
||||
</template>
|
||||
<template #selected="{ option }">
|
||||
{{ $t(`falukant.titles.${option.gender}.${option.title}`) }}
|
||||
{{ option.name }}
|
||||
</template>
|
||||
</Multiselect>
|
||||
</td>
|
||||
<td v-else>
|
||||
<ul class="voted-list">
|
||||
<li v-for="cid in e.votedFor" :key="cid">
|
||||
<span v-if="findCandidateById(e, cid)">
|
||||
{{ formatCandidateTitle(findCandidateById(e, cid)) }}
|
||||
{{ findCandidateById(e, cid).name }}
|
||||
</span>
|
||||
</li>
|
||||
<li v-if="!e.votedFor || !e.votedFor.length">—</li>
|
||||
</ul>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<button v-if="!e.voted"
|
||||
:disabled="!selectedCandidates[e.id] || !selectedCandidates[e.id].length"
|
||||
@click="submitVote(e.id)">
|
||||
{{ $t('falukant.politics.elections.vote') }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr v-if="!elections.length">
|
||||
<td colspan="6">{{ $t('falukant.politics.elections.none') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="all-vote-button" v-if="hasAnyUnvoted">
|
||||
<button :disabled="!hasAnySelection" @click="submitAllVotes">
|
||||
{{ $t('falukant.politics.elections.voteAll') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import StatusBar from '@/components/falukant/StatusBar.vue';
|
||||
import SimpleTabs from '@/components/SimpleTabs.vue';
|
||||
import Multiselect from 'vue-multiselect';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
|
||||
export default {
|
||||
name: 'PoliticsView',
|
||||
components: { StatusBar, SimpleTabs, Multiselect },
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'current',
|
||||
tabs: [
|
||||
{ value: 'current', label: 'falukant.politics.tabs.current' },
|
||||
{ value: 'openPolitics', label: 'falukant.politics.tabs.upcoming' },
|
||||
{ value: 'elections', label: 'falukant.politics.tabs.elections' }
|
||||
],
|
||||
currentPositions: [],
|
||||
openPolitics: [],
|
||||
elections: [],
|
||||
selectedCandidates: {},
|
||||
selectedApplications: [],
|
||||
loading: {
|
||||
current: false,
|
||||
openPolitics: false,
|
||||
elections: false
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasAnySelection() {
|
||||
return Object.values(this.selectedCandidates)
|
||||
.some(arr => Array.isArray(arr) && arr.length > 0);
|
||||
},
|
||||
hasAnyUnvoted() {
|
||||
return this.elections.some(e => !e.voted);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadCurrentPositions();
|
||||
},
|
||||
methods: {
|
||||
onTabChange(tab) {
|
||||
if (tab === 'current' && !this.currentPositions.length) {
|
||||
this.loadCurrentPositions();
|
||||
}
|
||||
if (tab === 'openPolitics' && !this.openPolitics.length) {
|
||||
this.loadOpenPolitics();
|
||||
}
|
||||
if (tab === 'elections' && !this.elections.length) {
|
||||
this.loadElections();
|
||||
}
|
||||
},
|
||||
|
||||
async loadCurrentPositions() {
|
||||
this.loading.current = true;
|
||||
try {
|
||||
const { data } = await apiClient.get('/api/falukant/politics/overview');
|
||||
this.currentPositions = data;
|
||||
} catch (err) {
|
||||
console.error('Error loading current positions', err);
|
||||
} finally {
|
||||
this.loading.current = false;
|
||||
}
|
||||
},
|
||||
|
||||
async loadOpenPolitics() {
|
||||
this.loading.openPolitics = true;
|
||||
try {
|
||||
const { data } = await apiClient.get('/api/falukant/politics/open');
|
||||
this.openPolitics = data;
|
||||
this.selectedApplications = [];
|
||||
} catch (err) {
|
||||
console.error('Error loading open politics', err);
|
||||
} finally {
|
||||
this.loading.openPolitics = false;
|
||||
}
|
||||
},
|
||||
|
||||
async loadElections() {
|
||||
this.loading.elections = true;
|
||||
try {
|
||||
const { data } = await apiClient.get('/api/falukant/politics/elections');
|
||||
this.elections = data;
|
||||
|
||||
data.forEach(e => {
|
||||
this.selectedCandidates[e.id] = [];
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error loading elections', err);
|
||||
} finally {
|
||||
this.loading.elections = false;
|
||||
}
|
||||
},
|
||||
|
||||
candidateLabel(option) {
|
||||
const title = this.$t(`falukant.titles.${option.gender}.${option.title}`);
|
||||
return `${title} ${option.name} (${option.age})`;
|
||||
},
|
||||
|
||||
findCandidateById(election, candidateId) {
|
||||
return election.candidates.find(c => c.id === candidateId) || {};
|
||||
},
|
||||
|
||||
formatCandidateTitle(candidate) {
|
||||
if (!candidate) return '';
|
||||
return this.$t(`falukant.titles.${candidate.gender}.${candidate.title}`);
|
||||
},
|
||||
|
||||
async submitVote(electionId) {
|
||||
const singlePayload = [
|
||||
{
|
||||
electionId: electionId,
|
||||
candidateIds: this.selectedCandidates[electionId].map(c => c.id)
|
||||
}
|
||||
];
|
||||
|
||||
try {
|
||||
await apiClient.post(
|
||||
'/api/falukant/politics/elections',
|
||||
{ votes: singlePayload }
|
||||
);
|
||||
await this.loadElections();
|
||||
} catch (err) {
|
||||
console.error(`Error submitting vote for election ${electionId}`, err);
|
||||
}
|
||||
},
|
||||
|
||||
async submitAllVotes() {
|
||||
const payload = Object.entries(this.selectedCandidates)
|
||||
.filter(([eid, arr]) => Array.isArray(arr) && arr.length > 0)
|
||||
.map(([eid, arr]) => ({
|
||||
electionId: parseInt(eid, 10),
|
||||
candidateIds: arr.map(c => c.id)
|
||||
}));
|
||||
|
||||
try {
|
||||
await apiClient.post(
|
||||
'/api/falukant/politics/elections',
|
||||
{ votes: payload }
|
||||
);
|
||||
await this.loadElections();
|
||||
} catch (err) {
|
||||
console.error('Error submitting all votes', err);
|
||||
}
|
||||
},
|
||||
|
||||
formatDate(ts) {
|
||||
return new Date(ts).toLocaleDateString(this.$i18n.locale, {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
});
|
||||
},
|
||||
|
||||
async submitApplications() {
|
||||
try {
|
||||
await apiClient.post(
|
||||
'/api/falukant/politics/open',
|
||||
{ electionIds: this.selectedApplications }
|
||||
);
|
||||
await this.loadOpenPolitics();
|
||||
} catch (err) {
|
||||
console.error('Error submitting applications', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.politics-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
padding: 20px 0 0 0;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.simple-tabs {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tab-pane {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.politics-table {
|
||||
border-collapse: collapse;
|
||||
width: auto;
|
||||
/* kein 100% */
|
||||
}
|
||||
|
||||
.politics-table thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: #FFF;
|
||||
z-index: 1;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.politics-table tbody td {
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.voted-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.all-vote-button {
|
||||
padding: 10px 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.all-vote-button button {
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
margin: 2em;
|
||||
}
|
||||
</style>
|
||||
@@ -2,89 +2,84 @@
|
||||
<h2 class="link" @click="openForum()">{{ $t('socialnetwork.forum.title') }} {{ forumName }}</h2>
|
||||
<h3 v-if="forumTopic">{{ forumTopic }}</h3>
|
||||
<ul class="messages">
|
||||
<li v-for="message in messages">
|
||||
<li v-for="message in messages" :key="message.id">
|
||||
<div v-html="message.text"></div>
|
||||
<div class="footer">
|
||||
<span class="link" @click="openProfile(message.lastMessageUser.hashedId)">{{
|
||||
message.lastMessageUser.username }}</span>
|
||||
<span class="link" @click="openProfile(message.lastMessageUser.hashedId)">
|
||||
{{ message.lastMessageUser.username }}
|
||||
</span>
|
||||
<span>{{ new Date(message.createdAt).toLocaleString() }}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<editor v-model="newContent" :init="tinymceInitOptions" :api-key="apiKey"
|
||||
tinymce-script-src="/tinymce/tinymce.min.js"></editor>
|
||||
|
||||
<div class="editor-container">
|
||||
<EditorContent :editor="editor" class="editor" />
|
||||
</div>
|
||||
<button @click="saveNewMessage">{{ $t('socialnetwork.forum.createNewMesssage') }}</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import apiClient from '../../utils/axios';
|
||||
import TinyMCEEditor from '@tinymce/tinymce-vue';
|
||||
import { Editor, EditorContent } from '@tiptap/vue-3'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import apiClient from '../../utils/axios'
|
||||
|
||||
export default {
|
||||
name: 'ForumTopicView',
|
||||
components: {
|
||||
editor: TinyMCEEditor,
|
||||
EditorContent,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
forumTopicId: String,
|
||||
forumTopicId: '',
|
||||
forumTopic: null,
|
||||
forumName: null,
|
||||
forumId: 0,
|
||||
newContent: '',
|
||||
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,
|
||||
},
|
||||
messages: [],
|
||||
editor: null,
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
mounted() {
|
||||
this.forumTopicId = this.$route.params.id;
|
||||
this.loadForumTopic();
|
||||
|
||||
this.editor = new Editor({
|
||||
extensions: [StarterKit],
|
||||
content: '',
|
||||
});
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.editor) {
|
||||
this.editor.destroy();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadForumTopic() {
|
||||
try {
|
||||
console.log(this.forumTopicId);
|
||||
const url = `/api/forum/topic/${this.forumTopicId}`;
|
||||
console.log('url', url);
|
||||
const response = await apiClient.get(url);
|
||||
const response = await apiClient.get(`/api/forum/topic/${this.forumTopicId}`);
|
||||
this.setContent(response.data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
setContent(responseData) {
|
||||
this.forumTopic = responseData.title;
|
||||
this.forumName = responseData.forum.name;
|
||||
this.forumId = responseData.forum.id;
|
||||
this.messages = responseData.messages;
|
||||
setContent(data) {
|
||||
this.forumTopic = data.title;
|
||||
this.forumName = data.forum.name;
|
||||
this.forumId = data.forum.id;
|
||||
this.messages = data.messages;
|
||||
},
|
||||
async openProfile(id) {
|
||||
this.$root.$refs.userProfileDialog.userId = id;
|
||||
this.$root.$refs.userProfileDialog.open();
|
||||
},
|
||||
async saveNewMessage() {
|
||||
const content = this.editor ? this.editor.getHTML() : '';
|
||||
if (!content.trim()) return;
|
||||
|
||||
try {
|
||||
const url = `/api/forum/topic/${this.forumTopicId}/message`;
|
||||
const response = await apiClient.post(url, {
|
||||
content: this.newContent,
|
||||
});
|
||||
this.newContent = '';
|
||||
const response = await apiClient.post(url, { content });
|
||||
this.editor.commands.clearContent();
|
||||
this.setContent(response.data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -96,7 +91,6 @@ export default {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.messages {
|
||||
list-style-type: none;
|
||||
@@ -117,11 +111,24 @@ export default {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.messages>li>.footer>span:first-child {
|
||||
.messages > li > .footer > span:first-child {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.messages > li > .footer > span:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
||||
.editor-container {
|
||||
margin-top: 1rem;
|
||||
border: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
min-height: 200px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.editor {
|
||||
min-height: 150px;
|
||||
outline: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -11,71 +11,27 @@
|
||||
<input type="text" v-model="newTitle" />
|
||||
</label>
|
||||
</div>
|
||||
<editor v-model="newContent" :init="tinymceInitOptions" :api-key="apiKey"
|
||||
tinymce-script-src="/tinymce/tinymce.min.js"></editor>
|
||||
<div class="editor-container">
|
||||
<EditorContent :editor="editor" class="editor" />
|
||||
</div>
|
||||
<button @click="saveNewTopic">{{ $t('socialnetwork.forum.createNewTopic') }}</button>
|
||||
</div>
|
||||
<div v-else-if="titles.length > 0">
|
||||
<div class="pagination">
|
||||
<button @click="goToPage(1)" v-if="page != 1">« {{ $t('socialnetwork.forum.pagination.first')
|
||||
}}</button>
|
||||
<button @click="goToPage(page - 1)" v-if="page != 1">‹ {{
|
||||
$t('socialnetwork.forum.pagination.previous') }}</button>
|
||||
<span>{{ $t('socialnetwork.forum.pagination.page').replace("<<page>>", page).replace("<<of>>", totalPages)
|
||||
}}</span>
|
||||
<button @click="goToPage(page + 1)" v-if="page != totalPages">{{ $t('socialnetwork.forum.pagination.next')
|
||||
}}
|
||||
›</button>
|
||||
<button @click="goToPage(totalPages)" v-if="page != totalPages">{{ $t('socialnetwork.forum.pagination.last')
|
||||
}}
|
||||
»</button>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('socialnetwork.forum.topic') }}</th>
|
||||
<th>{{ $t('socialnetwork.forum.createdBy') }}</th>
|
||||
<th>{{ $t('socialnetwork.forum.createdAt') }}</th>
|
||||
<th>{{ $t('socialnetwork.forum.reactions') }}</th>
|
||||
<th>{{ $t('socialnetwork.forum.lastReaction') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="title in titles">
|
||||
<td><span class="link" @click="openTopic(title.id)">{{ title.title }}</span></td>
|
||||
<td><span class="link" @click="openProfile(title.createdByHash)">{{ title.createdBy }}</span></td>
|
||||
<td>{{ new Date(title.createdAt).toLocaleString() }}</td>
|
||||
<td>{{ title.numberOfItems }}</td>
|
||||
<td>{{ new Date(title.lastMessageDate).toLocaleString() }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pagination">
|
||||
<button @click="goToPage(1)" v-if="page != 1">« {{ $t('socialnetwork.forum.pagination.first')
|
||||
}}</button>
|
||||
<button @click="goToPage(page - 1)" v-if="page != 1">‹ {{
|
||||
$t('socialnetwork.forum.pagination.previous') }}</button>
|
||||
<span>{{ $t('socialnetwork.forum.pagination.page').replace("<<page>>", page).replace("<<of>>", totalPages)
|
||||
}}</span>
|
||||
<button @click="goToPage(page + 1)" v-if="page != totalPages">{{ $t('socialnetwork.forum.pagination.next')
|
||||
}}
|
||||
›</button>
|
||||
<button @click="goToPage(totalPages)" v-if="page != totalPages">{{ $t('socialnetwork.forum.pagination.last')
|
||||
}}
|
||||
»</button>
|
||||
</div>
|
||||
<!-- PAGINATION + TABLE bleibt unverändert -->
|
||||
<!-- ... -->
|
||||
</div>
|
||||
<div v-else>{{ $t('socialnetwork.forum.noTitles') }}</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import apiClient from '../../utils/axios';
|
||||
import TinyMCEEditor from '@tinymce/tinymce-vue';
|
||||
import { Editor, EditorContent } from '@tiptap/vue-3'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import apiClient from '../../utils/axios'
|
||||
|
||||
export default {
|
||||
name: 'ForumView',
|
||||
components: {
|
||||
editor: TinyMCEEditor,
|
||||
EditorContent,
|
||||
},
|
||||
computed: {
|
||||
totalPages() {
|
||||
@@ -91,30 +47,20 @@ export default {
|
||||
titles: [],
|
||||
inCreation: false,
|
||||
newTitle: '',
|
||||
newContent: '',
|
||||
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,
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.forumId = this.$route.params.id;
|
||||
await this.loadForum();
|
||||
|
||||
this.editor = new Editor({
|
||||
extensions: [StarterKit],
|
||||
content: '',
|
||||
});
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.editor) this.editor.destroy();
|
||||
},
|
||||
methods: {
|
||||
async loadForum() {
|
||||
@@ -123,12 +69,16 @@ export default {
|
||||
},
|
||||
createNewTopic() {
|
||||
this.inCreation = !this.inCreation;
|
||||
if (this.inCreation && this.editor) {
|
||||
this.editor.commands.setContent('');
|
||||
}
|
||||
},
|
||||
async saveNewTopic() {
|
||||
const content = this.editor ? this.editor.getHTML() : '';
|
||||
const response = await apiClient.post('/api/forum/topic', {
|
||||
forumId: this.forumId,
|
||||
title: this.newTitle,
|
||||
content: this.newEntryContent
|
||||
content,
|
||||
});
|
||||
this.setData(response.data);
|
||||
this.inCreation = false;
|
||||
@@ -171,6 +121,19 @@ export default {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
margin: 1em 0;
|
||||
border: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
min-height: 200px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.editor {
|
||||
min-height: 150px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -185,4 +148,4 @@ export default {
|
||||
.pagination span {
|
||||
padding: 0.5em;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user