Refactor feedback handling across components: Replace alert and confirm calls with centralized feedback functions for improved user experience. Update various components to utilize showError, showSuccess, and confirmAction for consistent messaging and confirmation dialogs. Enhance UI responsiveness and maintainability by streamlining feedback logic.
This commit is contained in:
@@ -343,6 +343,33 @@ Aktueller Stand:
|
||||
- breite Tabellen auf kleinen Screens per horizontalem Scroll-Fallback abgesichert
|
||||
- globale Touch-Ziele fuer Buttons leicht vergroessert und letzte Shell-Kanten geglaettet
|
||||
|
||||
### Phase U6: Vereinfachung und Restentruempelung
|
||||
|
||||
Ergebnis:
|
||||
|
||||
- die letzten spuerbaren Bedienhuerden aus Altmustern, Scrolllogik und funktionslastigen Ansichten werden systematisch entfernt
|
||||
|
||||
Arbeit:
|
||||
|
||||
- verbliebene `alert`-/`confirm`-Fluesse auf das zentrale Feedbacksystem umstellen
|
||||
- verschachtelte Scrollcontainer in Falukant, Admin und Minigames entfernen oder entkoppeln
|
||||
- tabellenlastige Kernansichten auf klarere Aufgabenreihenfolge pruefen
|
||||
- Debug-/Altinteraktionen aus grossen Kernviews reduzieren, wenn sie Bedienbarkeit oder Folgepflege stoeren
|
||||
|
||||
Aktueller Stand:
|
||||
|
||||
- `U6.1` abgeschlossen
|
||||
- `U6.2` abgeschlossen
|
||||
- `U6.3` abgeschlossen
|
||||
- `U6.4` abgeschlossen
|
||||
- aus der Review nach U5 als eigener Nachlauf identifiziert
|
||||
- Fokus bewusst nicht mehr auf Redesign, sondern auf Reibungsabbau in realen Nutzungswegen
|
||||
- priorisierte Teilpakete:
|
||||
- `U6.1 Feedback vereinheitlichen`
|
||||
- `U6.2 Scroll- und Layoutfallen entfernen`
|
||||
- `U6.3 Tabellen- und Arbeitsflaechen vereinfachen`
|
||||
- `U6.4 Interaktionsaltlasten reduzieren`
|
||||
|
||||
## Konkreter Arbeitskatalog
|
||||
|
||||
### 1. Shell und Navigation
|
||||
@@ -378,6 +405,13 @@ Aktueller Stand:
|
||||
- breite Inhalte auf kleine Screens pruefen
|
||||
- Dialoge und Tabellen fuer Touch pruefen
|
||||
|
||||
### 6. Vereinfachungsreview
|
||||
|
||||
- Restbestände an `alert`, `confirm` und lokalen Sonderdialogen abbauen
|
||||
- komplexe Tabellenbereiche in Aufgabenfolge statt nur Datenanzeige gliedern
|
||||
- verschachtelte Scrollbereiche konsequent entfernen
|
||||
- Debug-/Sonderlogik in Kerninteraktionen auf Bedienrelevanz pruefen
|
||||
|
||||
## Definition of Done
|
||||
|
||||
Das Bedienbarkeitsprojekt gilt als abgeschlossen, wenn:
|
||||
@@ -386,6 +420,7 @@ Das Bedienbarkeitsprojekt gilt als abgeschlossen, wenn:
|
||||
- P1- und P2-Probleme aus dem Audit abgearbeitet sind
|
||||
- Navigation, Formulare, Dialoge und Feedback nach gemeinsamen Regeln funktionieren
|
||||
- Kernaufgaben auf Desktop und kleinem Viewport ohne strukturelle Reibung moeglich sind
|
||||
- verbleibende Altinteraktionen in Kernpfaden keine zusaetzliche Bedienlogik mehr erzwingen
|
||||
- Restpunkte nur noch P3/P4-Feinschliff sind
|
||||
|
||||
## Empfohlene Reihenfolge
|
||||
@@ -395,7 +430,8 @@ Das Bedienbarkeitsprojekt gilt als abgeschlossen, wenn:
|
||||
3. Formulare und Abschlusslogik
|
||||
4. Falukant, Vokabeltrainer, Admin, Minigames
|
||||
5. Mobile Endabnahme
|
||||
6. Vereinfachungsnachlauf ueber Feedback, Scrolllogik und tabellenlastige Restbereiche
|
||||
|
||||
## Naechster konkreter Schritt
|
||||
|
||||
Der erste sinnvolle Umsetzungsschritt ist nicht sofort Code, sondern ein kurzer UX-Audit-Durchgang ueber die wichtigsten Aufgabenfluesse. Daraus entsteht ein priorisierter Problemkatalog, auf dessen Basis die Bedienbarkeitsarbeit strukturiert umgesetzt wird.
|
||||
Der naechste sinnvolle Umsetzungsschritt ist `U6.1 Feedback vereinheitlichen`: alle verbliebenen `alert`-/`confirm`-Fluesse in Kernpfaden auf das zentrale Feedback- und Bestätigungssystem ziehen und dabei zugleich die groebsten Altinteraktionen in Falukant, Kalender, Vokabeln und Admin bereinigen.
|
||||
|
||||
@@ -175,24 +175,11 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import { createApp } from 'vue';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { EventBus } from '@/utils/eventBus.js';
|
||||
|
||||
import RandomChatDialog from '../dialogues/chat/RandomChatDialog.vue';
|
||||
import MultiChatDialog from '../dialogues/chat/MultiChatDialog.vue';
|
||||
|
||||
// Wichtig: die zentrale Instanzen importieren
|
||||
import store from '@/store';
|
||||
import router from '@/router';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
export default {
|
||||
name: 'AppNavigation',
|
||||
components: {
|
||||
RandomChatDialog,
|
||||
MultiChatDialog
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
forumList: [],
|
||||
@@ -292,7 +279,8 @@ export default {
|
||||
this.pinnedSubKey = this.pinnedSubKey === key ? null : key;
|
||||
},
|
||||
|
||||
collapseMenus() {
|
||||
collapseMenus(options = {}) {
|
||||
const { blurActiveElement = true } = options;
|
||||
this.expandedMainKey = null;
|
||||
this.expandedSubKey = null;
|
||||
this.pinnedMainKey = null;
|
||||
@@ -305,11 +293,13 @@ export default {
|
||||
this.suppressHover = false;
|
||||
this.hoverReleaseTimer = null;
|
||||
}, 180);
|
||||
if (blurActiveElement) {
|
||||
this.$nextTick(() => {
|
||||
if (document.activeElement && typeof document.activeElement.blur === 'function') {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
handleDocumentClick(event) {
|
||||
@@ -317,7 +307,7 @@ export default {
|
||||
if (!root || root.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
this.collapseMenus();
|
||||
this.collapseMenus({ blurActiveElement: false });
|
||||
},
|
||||
|
||||
handleDocumentKeydown(event) {
|
||||
@@ -435,10 +425,21 @@ export default {
|
||||
},
|
||||
|
||||
openChat(userId) {
|
||||
console.log('openChat:', userId);
|
||||
// Datei erstellen und ans body anhängen
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
const dialogRef = this.$root.$refs.multiChatDialog;
|
||||
const friend = this.friendsList.find((entry) => entry.id === userId);
|
||||
if (!dialogRef || typeof dialogRef.open !== 'function') {
|
||||
this.openProfile(userId);
|
||||
return;
|
||||
}
|
||||
dialogRef.open();
|
||||
if (!friend?.username) {
|
||||
return;
|
||||
}
|
||||
window.setTimeout(() => {
|
||||
if (dialogRef.usersInRoom?.some((user) => user.name === friend.username)) {
|
||||
dialogRef.selectedTargetUser = friend.username;
|
||||
}
|
||||
}, 250);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -179,6 +179,7 @@
|
||||
<script>
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import NewDirectorDialog from '@/dialogues/falukant/NewDirectorDialog.vue';
|
||||
import { showError, showInfo, showSuccess } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: "DirectorInfo",
|
||||
@@ -307,11 +308,11 @@ export default {
|
||||
},
|
||||
|
||||
fireDirector() {
|
||||
alert(this.$t('falukant.branch.director.fireAlert'));
|
||||
showInfo(this, this.$t('falukant.branch.director.fireAlert'));
|
||||
},
|
||||
|
||||
teachDirector() {
|
||||
alert(this.$t('falukant.branch.director.teachAlert'));
|
||||
showInfo(this, this.$t('falukant.branch.director.teachAlert'));
|
||||
},
|
||||
|
||||
vehicleTypeOptions() {
|
||||
@@ -440,11 +441,11 @@ export default {
|
||||
cost: 0.1,
|
||||
costLabel: '',
|
||||
};
|
||||
alert(this.$t('falukant.branch.director.emptyTransport.success'));
|
||||
showSuccess(this, this.$t('falukant.branch.director.emptyTransport.success'));
|
||||
this.$emit('transportCreated');
|
||||
} catch (error) {
|
||||
console.error('Error creating empty transport:', error);
|
||||
alert(this.$t('falukant.branch.director.emptyTransport.error'));
|
||||
showError(this, this.$t('falukant.branch.director.emptyTransport.error'));
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
|
||||
<script>
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { showApiError } from '@/utils/feedback.js';
|
||||
export default {
|
||||
name: "ProductionSection",
|
||||
props: {
|
||||
@@ -166,7 +167,7 @@
|
||||
});
|
||||
this.loadProductions();
|
||||
} catch (error) {
|
||||
alert(this.$t(`falukant.branch.production.error${error.response.data.error}`));
|
||||
showApiError(this, error, 'tr:error.network');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,6 +173,7 @@
|
||||
|
||||
<script>
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { showError, showSuccess } from '@/utils/feedback.js';
|
||||
export default {
|
||||
name: "SaleSection",
|
||||
props: {
|
||||
@@ -334,13 +335,13 @@
|
||||
quantity: quantityToSell,
|
||||
quality: item.quality,
|
||||
}).catch(() => {
|
||||
alert(this.$t('falukant.branch.sale.sellError'));
|
||||
showError(this, this.$t('falukant.branch.sale.sellError'));
|
||||
});
|
||||
},
|
||||
sellAll() {
|
||||
apiClient.post(`/api/falukant/sell/all`, { branchId: this.branchId })
|
||||
.catch(() => {
|
||||
alert(this.$t('falukant.branch.sale.sellAllError'));
|
||||
showError(this, this.$t('falukant.branch.sale.sellAllError'));
|
||||
});
|
||||
},
|
||||
inventoryOptions() {
|
||||
@@ -502,11 +503,11 @@
|
||||
});
|
||||
await this.loadInventory();
|
||||
await this.loadTransports();
|
||||
alert(this.$t('falukant.branch.sale.transportStarted'));
|
||||
showSuccess(this, this.$t('falukant.branch.sale.transportStarted'));
|
||||
this.$emit('transportCreated');
|
||||
} catch (error) {
|
||||
console.error('Error creating transport:', error);
|
||||
alert(this.$t('falukant.branch.sale.transportError'));
|
||||
showError(this, this.$t('falukant.branch.sale.transportError'));
|
||||
}
|
||||
},
|
||||
async loadTransports() {
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
|
||||
<script>
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { showError } from '@/utils/feedback.js';
|
||||
export default {
|
||||
name: "StorageSection",
|
||||
props: { branchId: { type: Number, required: true } },
|
||||
@@ -164,12 +165,12 @@
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert('Error buying storage for one part of the order');
|
||||
showError(this, 'Fehler beim Kaufen eines Teils der Lagerkapazität.');
|
||||
}
|
||||
remainingAmount -= toBuy;
|
||||
}
|
||||
if (remainingAmount > 0) {
|
||||
alert(this.$t('falukant.branch.storage.notEnoughAvailable'));
|
||||
showError(this, this.$t('falukant.branch.storage.notEnoughAvailable'));
|
||||
}
|
||||
this.loadStorageData();
|
||||
},
|
||||
@@ -185,7 +186,7 @@
|
||||
.then(() => this.loadStorageData())
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
alert('Error selling storage');
|
||||
showError(this, 'Fehler beim Verkaufen der Lagerkapazität.');
|
||||
});
|
||||
},
|
||||
getCostOfType(labelTr) {
|
||||
|
||||
@@ -239,6 +239,7 @@ import DialogWidget from '@/components/DialogWidget.vue';
|
||||
import { fetchPublicRooms, fetchRoomCreateOptions, fetchOwnRooms } from '@/api/chatApi.js';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { getChatWsUrl, getChatWsCandidates, getChatWsProtocols } from '@/services/chatWs.js';
|
||||
import { confirmAction } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: 'MultiChat',
|
||||
@@ -561,7 +562,10 @@ export default {
|
||||
},
|
||||
async deleteOwnedRoom(room) {
|
||||
const title = room?.title || '';
|
||||
const confirmed = window.confirm(this.$t('chat.multichat.createRoom.ownedRooms.confirmDelete', { room: title }));
|
||||
const confirmed = await confirmAction(this, {
|
||||
title: this.$t('chat.multichat.createRoom.ownedRooms.title'),
|
||||
message: this.$t('chat.multichat.createRoom.ownedRooms.confirmDelete', { room: title })
|
||||
});
|
||||
if (!confirmed) return;
|
||||
if (!this.transportConnected) {
|
||||
this.messages.push({
|
||||
|
||||
@@ -41,6 +41,7 @@ import DialogWidget from '@/components/DialogWidget.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { EventBus } from '@/utils/eventBus.js';
|
||||
import { showError } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: 'CreateFolderDialog',
|
||||
@@ -93,7 +94,7 @@ export default {
|
||||
},
|
||||
async createFolder() {
|
||||
if (!this.folderTitle || !this.selectedVisibility.length) {
|
||||
alert(this.$t('socialnetwork.gallery.errors.missing_fields'));
|
||||
showError(this, this.$t('socialnetwork.gallery.errors.missing_fields'));
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
|
||||
@@ -97,6 +97,7 @@ import FolderItem from '../../components/FolderItem.vue';
|
||||
import { Editor, EditorContent } from '@tiptap/vue-3'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import DOMPurify from 'dompurify';
|
||||
import { showError } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: 'UserProfileDialog',
|
||||
@@ -265,7 +266,10 @@ export default {
|
||||
}
|
||||
},
|
||||
async submitGuestbookEntry() {
|
||||
if (!this.newEntryContent) return alert(this.$t('socialnetwork.guestbook.emptyContent'));
|
||||
if (!this.newEntryContent) {
|
||||
showError(this, this.$t('socialnetwork.guestbook.emptyContent'));
|
||||
return;
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append('htmlContent', this.newEntryContent);
|
||||
formData.append('recipientName', this.userProfile.username);
|
||||
|
||||
@@ -42,12 +42,10 @@ export default {
|
||||
this.$refs.dialog.close();
|
||||
},
|
||||
confirmYes() {
|
||||
console.log('ja');
|
||||
this.resolve(true);
|
||||
this.close();
|
||||
},
|
||||
confirmNo() {
|
||||
console.log('nein');
|
||||
this.resolve(false);
|
||||
this.close();
|
||||
},
|
||||
|
||||
@@ -52,6 +52,11 @@ export function showApiError(context, error, fallback = 'tr:error.network') {
|
||||
showError(context, fallback, fallback);
|
||||
}
|
||||
|
||||
export async function confirmAction(context, options = {}) {
|
||||
const refs = getRootRefs(context);
|
||||
return refs.chooseDialog?.open?.(options) ?? false;
|
||||
}
|
||||
|
||||
export default {
|
||||
install(app) {
|
||||
const getAppContext = () => app._instance?.proxy;
|
||||
@@ -61,7 +66,8 @@ export default {
|
||||
showSuccess: (...args) => showSuccess(getAppContext(), ...args),
|
||||
showInfo: (...args) => showInfo(getAppContext(), ...args),
|
||||
showError: (...args) => showError(getAppContext(), ...args),
|
||||
showApiError: (...args) => showApiError(getAppContext(), ...args)
|
||||
showApiError: (...args) => showApiError(getAppContext(), ...args),
|
||||
confirmAction: (...args) => confirmAction(getAppContext(), ...args)
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<div class="contenthidden">
|
||||
<div class="contentscroll">
|
||||
<div class="admin-minigames-view">
|
||||
<div class="admin-header">
|
||||
<h1>{{ $t('admin.match3.title') }}</h1>
|
||||
<p>Verwalte Minigames, Level und Konfigurationen</p>
|
||||
@@ -14,7 +13,7 @@
|
||||
<div>
|
||||
<span class="workflow-hero__eyebrow">Arbeitsfluss</span>
|
||||
<h2>{{ $t('admin.match3.title') }}</h2>
|
||||
<p>Erst Level waehlen, dann Spielfeld und Ziele anpassen und erst am Ende speichern.</p>
|
||||
<p>Erst Level wählen, dann Spielfeld und Ziele anpassen und erst am Ende speichern.</p>
|
||||
</div>
|
||||
<div class="workflow-hero__meta">
|
||||
<span class="workflow-pill">{{ currentModeLabel }}</span>
|
||||
@@ -26,12 +25,12 @@
|
||||
<article class="workflow-card surface-card">
|
||||
<span class="workflow-card__step">1</span>
|
||||
<h3>Level waehlen</h3>
|
||||
<p>Bestehendes Level oeffnen oder sofort mit einer neuen Vorlage starten.</p>
|
||||
<p>Bestehendes Level öffnen oder sofort mit einer neuen Vorlage starten.</p>
|
||||
</article>
|
||||
<article class="workflow-card surface-card">
|
||||
<span class="workflow-card__step">2</span>
|
||||
<h3>Spielfeld bauen</h3>
|
||||
<p>Groesse, Zuege, Kacheln und Layout zuerst festziehen, bevor Ziele folgen.</p>
|
||||
<p>Größe, Züge, Kacheln und Layout zuerst festziehen, bevor Ziele folgen.</p>
|
||||
</article>
|
||||
<article class="workflow-card surface-card">
|
||||
<span class="workflow-card__step">3</span>
|
||||
@@ -79,7 +78,7 @@
|
||||
<article class="admin-summary-card surface-card">
|
||||
<span class="admin-summary-card__label">Spielfeld</span>
|
||||
<strong>{{ levelForm.boardWidth }} x {{ levelForm.boardHeight }}</strong>
|
||||
<p>{{ levelForm.moveLimit }} Zuege, {{ levelForm.tileTypes.length }} aktive Tile-Typen.</p>
|
||||
<p>{{ levelForm.moveLimit }} Züge, {{ levelForm.tileTypes.length }} aktive Tile-Typen.</p>
|
||||
</article>
|
||||
<article class="admin-summary-card surface-card">
|
||||
<span class="admin-summary-card__label">Objectives</span>
|
||||
@@ -602,13 +601,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SimpleTabs from '../../components/SimpleTabs.vue';
|
||||
import apiClient from '../../utils/axios.js';
|
||||
import { showError, showSuccess } from '@/utils/feedback.js';
|
||||
import { confirmAction, showError, showSuccess } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: 'AdminMinigamesView',
|
||||
@@ -1016,15 +1014,20 @@ export default {
|
||||
},
|
||||
|
||||
async deleteLevel(levelId) {
|
||||
if (confirm('Möchtest du dieses Level wirklich löschen?')) {
|
||||
const confirmed = await confirmAction(this, {
|
||||
title: 'Level löschen',
|
||||
message: 'Möchtest du dieses Level wirklich löschen?'
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await apiClient.delete(`/api/admin/minigames/match3/levels/${levelId}`);
|
||||
this.loadLevels();
|
||||
showSuccess(this, 'Level wurde geloescht.');
|
||||
showSuccess(this, 'Level wurde gelöscht.');
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen des Levels:', error);
|
||||
showError(this, 'Fehler beim Loeschen des Levels');
|
||||
}
|
||||
showError(this, 'Fehler beim Löschen des Levels');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1047,14 +1050,20 @@ export default {
|
||||
},
|
||||
|
||||
removeObjective(index) {
|
||||
if (confirm('Möchtest du dieses Objective wirklich löschen?')) {
|
||||
confirmAction(this, {
|
||||
title: 'Ziel löschen',
|
||||
message: 'Möchtest du dieses Objective wirklich löschen?'
|
||||
}).then((confirmed) => {
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
this.levelForm.objectives.splice(index, 1);
|
||||
|
||||
// Aktualisiere die Reihenfolge
|
||||
this.levelForm.objectives.forEach((objective, idx) => {
|
||||
objective.order = idx + 1;
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async loadObjectivesForLevel(levelId) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<div class="contenthidden">
|
||||
<div class="contentscroll">
|
||||
<div class="services-status-view">
|
||||
<div class="admin-header">
|
||||
<h1>{{ $t('admin.servicesStatus.title') }}</h1>
|
||||
<p>{{ $t('admin.servicesStatus.description') }}</p>
|
||||
@@ -95,7 +94,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- WebSocket Log Dialog -->
|
||||
<WebSocketLogDialog ref="webSocketLogDialog" />
|
||||
@@ -481,4 +479,3 @@ export default {
|
||||
color: #c62828;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<div class="contenthidden">
|
||||
<div class="contentscroll">
|
||||
<div class="admin-taxi-tools-view">
|
||||
<div class="admin-header">
|
||||
<h1>{{ $t('admin.taxiTools.title') }}</h1>
|
||||
<p>{{ $t('admin.taxiTools.description') }}</p>
|
||||
@@ -367,7 +366,6 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Message Dialog -->
|
||||
<MessageDialog ref="messageDialog" />
|
||||
@@ -378,6 +376,7 @@
|
||||
import SimpleTabs from '../../components/SimpleTabs.vue';
|
||||
import MessageDialog from '../../dialogues/standard/MessageDialog.vue';
|
||||
import apiClient from '../../utils/axios.js';
|
||||
import { confirmAction, showError, showSuccess } from '@/utils/feedback.js';
|
||||
|
||||
// Matrix: erlaubte Haus-Tür-Richtungen je Tile-Typ und Ecke
|
||||
// Richtungen: bottom, right, top, left
|
||||
@@ -1216,10 +1215,10 @@ export default {
|
||||
|
||||
// Erfolgsmeldung anzeigen
|
||||
const message = isUpdate ? 'admin.taxiTools.mapEditor.updateSuccess' : 'admin.taxiTools.mapEditor.createSuccess';
|
||||
this.$refs.messageDialog.open(`tr:${message}`);
|
||||
showSuccess(this, `tr:${message}`);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Speichern der Map:', error);
|
||||
alert('Fehler beim Speichern der Map');
|
||||
showError(this, 'Fehler beim Speichern der Map');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1247,16 +1246,20 @@ export default {
|
||||
},
|
||||
|
||||
async deleteMap(mapId) {
|
||||
if (confirm('Möchtest du diese Map wirklich löschen?')) {
|
||||
const confirmed = await confirmAction(this, {
|
||||
title: 'Map löschen',
|
||||
message: 'Möchtest du diese Map wirklich löschen?'
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await apiClient.delete(`/api/taxi-maps/maps/${mapId}`);
|
||||
this.loadMaps();
|
||||
|
||||
// Erfolgsmeldung anzeigen
|
||||
this.$refs.messageDialog.open('tr:admin.taxiTools.mapEditor.deleteSuccess');
|
||||
showSuccess(this, 'tr:admin.taxiTools.mapEditor.deleteSuccess');
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen der Map:', error);
|
||||
}
|
||||
showError(this, 'Fehler beim Löschen der Map');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<div class="contenthidden">
|
||||
<div class="contentscroll">
|
||||
<div class="create-npc-page">
|
||||
<div class="create-npc-view">
|
||||
<h1>{{ $t('admin.falukant.createNPC.title') }}</h1>
|
||||
|
||||
@@ -104,7 +103,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<div class="contenthidden">
|
||||
<div class="contentscroll falukant-map-admin">
|
||||
<div class="falukant-map-admin">
|
||||
<div class="admin-header">
|
||||
<h1>{{ $t('admin.falukant.map.title') }}</h1>
|
||||
<p>{{ $t('admin.falukant.map.description') }}</p>
|
||||
@@ -213,12 +212,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import SimpleTabs from '@/components/SimpleTabs.vue';
|
||||
import { confirmAction, showError } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: 'AdminFalukantMapRegionsView',
|
||||
@@ -469,17 +468,21 @@ export default {
|
||||
await this.loadConnections();
|
||||
} catch (error) {
|
||||
console.error('Error saving region distance:', error);
|
||||
alert(this.$t('admin.falukant.map.errorSaveConnection'));
|
||||
showError(this, this.$t('admin.falukant.map.errorSaveConnection'));
|
||||
}
|
||||
},
|
||||
async deleteConnection(id) {
|
||||
if (!confirm(this.$t('admin.falukant.map.confirmDeleteConnection'))) return;
|
||||
const confirmed = await confirmAction(this, {
|
||||
title: this.$t('admin.falukant.map.connectionsTitle'),
|
||||
message: this.$t('admin.falukant.map.confirmDeleteConnection')
|
||||
});
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
await apiClient.delete(`/api/admin/falukant/region-distances/${id}`);
|
||||
await this.loadConnections();
|
||||
} catch (error) {
|
||||
console.error('Error deleting region distance:', error);
|
||||
alert(this.$t('admin.falukant.map.errorDeleteConnection'));
|
||||
showError(this, this.$t('admin.falukant.map.errorDeleteConnection'));
|
||||
}
|
||||
},
|
||||
regionName(id) {
|
||||
@@ -635,5 +638,3 @@ export default {
|
||||
.btn.mini.icon {
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
<script>
|
||||
import { createBlog, updateBlog, getBlog, createPost, shareBlog } from '@/api/blogApi.js';
|
||||
import RichTextEditor from './components/RichTextEditor.vue';
|
||||
import { showError } from '@/utils/feedback.js';
|
||||
export default {
|
||||
name: 'BlogEditorView',
|
||||
components: { RichTextEditor },
|
||||
@@ -107,7 +108,7 @@ export default {
|
||||
async save() {
|
||||
if (this.form.visibility === 'logged_in') {
|
||||
if (this.form.ageMin != null && this.form.ageMax != null && this.form.ageMin > this.form.ageMax) {
|
||||
alert('Ungültiger Altersbereich');
|
||||
showError(this, 'Ungültiger Altersbereich');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="contenthidden">
|
||||
<div class="bank-view">
|
||||
<StatusBar />
|
||||
<div class="contentscroll">
|
||||
<div class="bank-content">
|
||||
|
||||
<h2>{{ $t('falukant.bank.title') }}</h2>
|
||||
<SimpleTabs v-model="activeTab" :tabs="tabs" />
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<div class="contenthidden">
|
||||
<div class="falukant-branch-view">
|
||||
<StatusBar ref="statusBar" />
|
||||
<div class="contentscroll">
|
||||
<div class="falukant-branch">
|
||||
<section class="branch-hero surface-card">
|
||||
<div>
|
||||
@@ -317,7 +316,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -332,6 +330,7 @@ import RevenueSection from '@/components/falukant/RevenueSection.vue';
|
||||
import BuyVehicleDialog from '@/dialogues/falukant/BuyVehicleDialog.vue';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { mapState } from 'vuex';
|
||||
import { showError, showSuccess, showApiError } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: "BranchView",
|
||||
@@ -657,7 +656,7 @@ export default {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error upgrading branch:', error);
|
||||
alert(this.$t('falukant.branch.actions.upgradeAlert', { branchId: this.selectedBranch.id }));
|
||||
showError(this, this.$t('falukant.branch.actions.upgradeAlert', { branchId: this.selectedBranch.id }));
|
||||
}
|
||||
},
|
||||
|
||||
@@ -956,7 +955,7 @@ export default {
|
||||
|
||||
async sendVehicles() {
|
||||
if (!this.sendVehicleDialog.targetBranchId) {
|
||||
alert(this.$t('falukant.branch.transport.selectTargetError'));
|
||||
showError(this, this.$t('falukant.branch.transport.selectTargetError'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -975,7 +974,7 @@ export default {
|
||||
} else if (this.sendVehicleDialog.vehicleTypeId) {
|
||||
payload.vehicleTypeId = this.sendVehicleDialog.vehicleTypeId;
|
||||
} else {
|
||||
alert(this.$t('falukant.branch.transport.noVehiclesSelected'));
|
||||
showError(this, this.$t('falukant.branch.transport.noVehiclesSelected'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -984,7 +983,7 @@ export default {
|
||||
this.sendVehicleDialog.success = true;
|
||||
} catch (error) {
|
||||
console.error('Error sending vehicles:', error);
|
||||
alert(this.$t('falukant.branch.transport.sendError'));
|
||||
showApiError(this, error, this.$t('falukant.branch.transport.sendError'));
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1043,12 +1042,12 @@ export default {
|
||||
});
|
||||
await this.loadVehicles();
|
||||
this.closeRepairAllVehiclesDialog();
|
||||
alert(this.$t('falukant.branch.transport.repairAllSuccess'));
|
||||
showSuccess(this, this.$t('falukant.branch.transport.repairAllSuccess'));
|
||||
this.$refs.statusBar?.fetchStatus();
|
||||
} catch (error) {
|
||||
console.error('Error repairing all vehicles:', error);
|
||||
const errorMessage = error.response?.data?.message || this.$t('falukant.branch.transport.repairAllError');
|
||||
alert(errorMessage);
|
||||
showError(this, errorMessage);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1105,12 +1104,12 @@ export default {
|
||||
await apiClient.post(`/api/falukant/vehicles/${this.repairVehicleDialog.vehicle.id}/repair`);
|
||||
await this.loadVehicles();
|
||||
this.closeRepairVehicleDialog();
|
||||
alert(this.$t('falukant.branch.transport.repairSuccess'));
|
||||
showSuccess(this, this.$t('falukant.branch.transport.repairSuccess'));
|
||||
this.$refs.statusBar?.fetchStatus();
|
||||
} catch (error) {
|
||||
console.error('Error repairing vehicle:', error);
|
||||
const errorMessage = error.response?.data?.message || this.$t('falukant.branch.transport.repairError');
|
||||
alert(errorMessage);
|
||||
showError(this, errorMessage);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="contenthidden">
|
||||
<div class="church-view">
|
||||
<StatusBar />
|
||||
<div class="contentscroll">
|
||||
<div class="church-content">
|
||||
<h2>{{ $t('falukant.church.title') }}</h2>
|
||||
<SimpleTabs v-model="activeTab" :tabs="tabs" />
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="contenthidden">
|
||||
<div class="education-view">
|
||||
<StatusBar />
|
||||
<div class="contentscroll">
|
||||
<div class="education-content">
|
||||
<h2>{{ $t('falukant.education.title') }}</h2>
|
||||
<SimpleTabs v-model="activeTab" :tabs="tabs" />
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="contenthidden">
|
||||
<div class="family-view">
|
||||
<StatusBar />
|
||||
<div class="contentscroll family-layout">
|
||||
<div class="family-layout">
|
||||
<div class="family-content">
|
||||
<section class="family-hero surface-card">
|
||||
<div>
|
||||
@@ -189,7 +189,6 @@
|
||||
<p>{{ $t('falukant.family.lovers.none') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<ChildDetailsDialog ref="childDetailsDialog" />
|
||||
@@ -204,6 +203,7 @@ import ChildDetailsDialog from '@/dialogues/falukant/ChildDetailsDialog.vue'
|
||||
import Character3D from '@/components/Character3D.vue'
|
||||
|
||||
import apiClient from '@/utils/axios.js'
|
||||
import { confirmAction } from '@/utils/feedback.js'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
const WOOING_PROGRESS_TARGET = 70
|
||||
@@ -350,7 +350,10 @@ export default {
|
||||
},
|
||||
|
||||
async cancelWooing() {
|
||||
const confirmed = window.confirm(this.$t('falukant.family.spouse.wooing.cancelConfirm'));
|
||||
const confirmed = await confirmAction(this, {
|
||||
title: 'Werbung abbrechen',
|
||||
message: this.$t('falukant.family.spouse.wooing.cancelConfirm')
|
||||
});
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
await apiClient.post('/api/falukant/family/cancel-wooing');
|
||||
|
||||
@@ -4,40 +4,33 @@
|
||||
<h2>{{ $t('falukant.house.title') }}</h2>
|
||||
<div class="existing-house">
|
||||
<div :style="houseType ? houseStyle(houseType.position, 341) : {}" class="house"></div>
|
||||
<div class="status-panel">
|
||||
<div class="status-panel surface-card">
|
||||
<h3>{{ $t('falukant.house.statusreport') }}</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('falukant.house.element') }}</th>
|
||||
<th>{{ $t('falukant.house.state') }}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(value, key) in status" :key="key">
|
||||
<td>{{ $t(`falukant.house.status.${key}`) }}</td>
|
||||
<td>{{ conditionLabel(value) }}</td>
|
||||
<td>
|
||||
<div class="status-cards">
|
||||
<article v-for="(value, key) in status" :key="key" class="status-card">
|
||||
<div>
|
||||
<span class="status-card__label">{{ $t(`falukant.house.status.${key}`) }}</span>
|
||||
<strong>{{ conditionLabel(value) }}</strong>
|
||||
</div>
|
||||
<button v-if="value < 100" @click="renovate(key)">
|
||||
{{ $t('falukant.house.renovate') }} ({{ getRenovationCost(key, value) }})
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('falukant.house.worth') }}</td>
|
||||
<td>{{ getWorth() }} {{ currency }}</td>
|
||||
<td>
|
||||
</article>
|
||||
<article class="status-card status-card--summary">
|
||||
<div>
|
||||
<span class="status-card__label">{{ $t('falukant.house.worth') }}</span>
|
||||
<strong>{{ getWorth() }} {{ currency }}</strong>
|
||||
</div>
|
||||
<div class="status-card__actions">
|
||||
<button @click="renovateAll" :disabled="allRenovated">
|
||||
{{ $t('falukant.house.renovateAll') }} ({{ getAllRenovationCost() }})
|
||||
</button>
|
||||
<button @click="sellHouse">
|
||||
<button class="button-secondary" @click="sellHouse">
|
||||
{{ $t('falukant.house.sell') }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -49,16 +42,15 @@
|
||||
class="house-preview"></div>
|
||||
<div class="house-info">
|
||||
<h4>{{ $t(`falukant.house.type.${house.houseType.labelTr}`) }}</h4>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr v-for="(val, prop) in house" :key="prop"
|
||||
v-if="['roofCondition', 'wallCondition', 'floorCondition', 'windowCondition'].includes(prop)">
|
||||
<td>{{ $t(`falukant.house.status.${prop}`) }}</td>
|
||||
<td>{{ conditionLabel(val) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div>
|
||||
<div class="buyable-house-stats">
|
||||
<div v-for="(val, prop) in house" :key="prop"
|
||||
v-if="['roofCondition', 'wallCondition', 'floorCondition', 'windowCondition'].includes(prop)"
|
||||
class="buyable-house-stat">
|
||||
<span>{{ $t(`falukant.house.status.${prop}`) }}</span>
|
||||
<strong>{{ conditionLabel(val) }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="buyable-house-price">
|
||||
{{ $t('falukant.house.price') }}: {{ buyCost(house) }}
|
||||
</div>
|
||||
<button @click="buyHouse(house.id)">
|
||||
@@ -263,6 +255,7 @@ h2 {
|
||||
|
||||
.status-panel {
|
||||
flex: 1;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.buyable-houses {
|
||||
@@ -284,8 +277,12 @@ h2 {
|
||||
.house-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 18px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: rgba(255, 255, 255, 0.68);
|
||||
}
|
||||
|
||||
.house-preview {
|
||||
@@ -301,20 +298,73 @@ h2 {
|
||||
/* center sprite */
|
||||
}
|
||||
|
||||
table {
|
||||
.house-info {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
table th,
|
||||
table td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
.status-cards,
|
||||
.buyable-house-stats {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.status-card,
|
||||
.buyable-house-stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: rgba(255, 255, 255, 0.68);
|
||||
}
|
||||
|
||||
.status-card__label,
|
||||
.buyable-house-stat span {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.status-card--summary {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.status-card__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.buyable-house-price {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.existing-house {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.house {
|
||||
width: min(341px, 100%);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.status-card,
|
||||
.buyable-house-stat {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="contenthidden">
|
||||
<div class="nobility-view">
|
||||
<StatusBar />
|
||||
<div class="contentscroll">
|
||||
<div class="nobility-content">
|
||||
<h2>{{ $t('falukant.nobility.title') }}</h2>
|
||||
<SimpleTabs v-model="activeTab" :tabs="tabs" />
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div>
|
||||
<span class="falukant-kicker">Falukant</span>
|
||||
<h2>{{ $t('falukant.overview.title') }}</h2>
|
||||
<p>Dein Stand in Wirtschaft, Familie und Besitz in einer verdichteten Uebersicht.</p>
|
||||
<p>Dein Stand in Wirtschaft, Familie und Besitz in einer verdichteten Übersicht.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<article class="summary-card surface-card">
|
||||
<span class="summary-card__label">Niederlassungen</span>
|
||||
<strong>{{ branchCount }}</strong>
|
||||
<p>Direkter Zugriff auf deine wichtigsten Geschaeftsstandorte.</p>
|
||||
<p>Direkter Zugriff auf deine wichtigsten Geschäftsstandorte.</p>
|
||||
</article>
|
||||
<article class="summary-card surface-card">
|
||||
<span class="summary-card__label">Produktionen aktiv</span>
|
||||
@@ -37,7 +37,7 @@
|
||||
<article class="summary-card surface-card">
|
||||
<span class="summary-card__label">Lagerpositionen</span>
|
||||
<strong>{{ stockEntryCount }}</strong>
|
||||
<p>Verdichteter Blick auf Warenbestand ueber alle Regionen.</p>
|
||||
<p>Verdichteter Blick auf Warenbestand über alle Regionen.</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
@@ -82,93 +82,77 @@
|
||||
|
||||
<!-- Normale Übersicht wenn Charakter vorhanden -->
|
||||
<div v-if="falukantUser?.character" class="overviewcontainer">
|
||||
<div>
|
||||
<section class="overview-panel surface-card">
|
||||
<h3>{{ $t('falukant.overview.metadata.title') }}</h3>
|
||||
<table>
|
||||
<tr>
|
||||
<td>{{ $t('falukant.overview.metadata.name') }}</td>
|
||||
<td>{{ falukantUser?.character?.definedFirstName?.name }} {{
|
||||
falukantUser?.character?.definedLastName?.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('falukant.overview.metadata.nobleTitle') }}</td>
|
||||
<td>{{ $t('falukant.titles.' + falukantUser?.character?.gender + '.' +
|
||||
falukantUser?.character?.nobleTitle?.labelTr) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('falukant.overview.metadata.money') }}</td>
|
||||
<td>
|
||||
<div class="detail-list">
|
||||
<div class="detail-list__item">
|
||||
<span>{{ $t('falukant.overview.metadata.name') }}</span>
|
||||
<strong>{{ falukantUser?.character?.definedFirstName?.name }} {{ falukantUser?.character?.definedLastName?.name }}</strong>
|
||||
</div>
|
||||
<div class="detail-list__item">
|
||||
<span>{{ $t('falukant.overview.metadata.nobleTitle') }}</span>
|
||||
<strong>{{ $t('falukant.titles.' + falukantUser?.character?.gender + '.' + falukantUser?.character?.nobleTitle?.labelTr) }}</strong>
|
||||
</div>
|
||||
<div class="detail-list__item">
|
||||
<span>{{ $t('falukant.overview.metadata.money') }}</span>
|
||||
<strong>
|
||||
{{ moneyValue != null
|
||||
? moneyValue.toLocaleString(locale, { style: 'currency', currency: 'EUR' })
|
||||
: '---' }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('falukant.overview.metadata.age') }}</td>
|
||||
<td>{{ falukantUser?.character?.age }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('falukant.overview.metadata.mainbranch') }}</td>
|
||||
<td>{{ falukantUser?.mainBranchRegion?.name }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<div class="detail-list__item">
|
||||
<span>{{ $t('falukant.overview.metadata.age') }}</span>
|
||||
<strong>{{ falukantUser?.character?.age }}</strong>
|
||||
</div>
|
||||
<div class="detail-list__item">
|
||||
<span>{{ $t('falukant.overview.metadata.mainbranch') }}</span>
|
||||
<strong>{{ falukantUser?.mainBranchRegion?.name }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="overview-panel surface-card">
|
||||
<h3>{{ $t('falukant.overview.productions.title') }}</h3>
|
||||
<table v-if="productions.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('falukant.branch.sale.region') }}</th>
|
||||
<th>{{ $t('falukant.branch.production.product') }}</th>
|
||||
<th>{{ $t('falukant.branch.production.quantity') }}</th>
|
||||
<th>{{ $t('falukant.branch.production.ending') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(production, index) in productions" :key="index">
|
||||
<td>{{ production.cityName }}</td>
|
||||
<td>{{ $t(`falukant.product.${production.productName}`) }}</td>
|
||||
<td>{{ production.quantity }}</td>
|
||||
<td>{{ formatDate(production.endTimestamp) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-if="productions.length > 0" class="overview-card-list">
|
||||
<article v-for="(production, index) in productions" :key="index" class="overview-entry-card">
|
||||
<strong>{{ $t(`falukant.product.${production.productName}`) }}</strong>
|
||||
<div class="overview-entry-card__meta">
|
||||
<span>{{ $t('falukant.branch.sale.region') }}: {{ production.cityName }}</span>
|
||||
<span>{{ $t('falukant.branch.production.quantity') }}: {{ production.quantity }}</span>
|
||||
<span>{{ $t('falukant.branch.production.ending') }}: {{ formatDate(production.endTimestamp) }}</span>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<p v-else>{{ $t('falukant.branch.production.noProductions') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
</section>
|
||||
<section class="overview-panel surface-card">
|
||||
<h3>{{ $t('falukant.overview.stock.title') }}</h3>
|
||||
<table v-if="allStock.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('falukant.branch.sale.region') }}</th>
|
||||
<th>{{ $t('falukant.branch.sale.product') }}</th>
|
||||
<th>{{ $t('falukant.branch.sale.quantity') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(item, index) in allStock" :key="index">
|
||||
<td>{{ item.regionName }}</td>
|
||||
<td>{{ $t(`falukant.product.${item.productLabelTr}`) }}</td>
|
||||
<td>{{ item.quantity }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-if="allStock.length > 0" class="overview-card-list">
|
||||
<article v-for="(item, index) in allStock" :key="index" class="overview-entry-card">
|
||||
<strong>{{ $t(`falukant.product.${item.productLabelTr}`) }}</strong>
|
||||
<div class="overview-entry-card__meta">
|
||||
<span>{{ $t('falukant.branch.sale.region') }}: {{ item.regionName }}</span>
|
||||
<span>{{ $t('falukant.branch.sale.quantity') }}: {{ item.quantity }}</span>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<p v-else>{{ $t('falukant.branch.sale.noInventory') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
</section>
|
||||
<section class="overview-panel surface-card">
|
||||
<h3>{{ $t('falukant.overview.branches.title') }}</h3>
|
||||
<table>
|
||||
<tr v-for="branch in falukantUser?.branches" :key="branch.id">
|
||||
<td>
|
||||
<span @click="openBranch(branch.id)" class="link">{{ branch.region.name }}</span>
|
||||
</td>
|
||||
<td>
|
||||
{{ $t(`falukant.overview.branches.level.${branch.branchType.labelTr}`) }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="overview-card-list">
|
||||
<article v-for="branch in falukantUser?.branches" :key="branch.id" class="overview-entry-card overview-entry-card--action">
|
||||
<div>
|
||||
<strong>{{ branch.region.name }}</strong>
|
||||
<div class="overview-entry-card__meta">
|
||||
<span>{{ $t(`falukant.overview.branches.level.${branch.branchType.labelTr}`) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="button-secondary" @click="openBranch(branch.id)">Öffnen</button>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -287,7 +271,7 @@ export default {
|
||||
return [
|
||||
{
|
||||
kicker: 'Routine',
|
||||
title: 'Niederlassung oeffnen',
|
||||
title: 'Niederlassung öffnen',
|
||||
description: 'Die schnellste Route zu Produktion, Lager, Verkauf und Transport.',
|
||||
cta: 'Zu den Betrieben',
|
||||
route: 'BranchView',
|
||||
@@ -303,8 +287,8 @@ export default {
|
||||
{
|
||||
kicker: 'Charakter',
|
||||
title: 'Familie und Nachfolge',
|
||||
description: 'Wichtige persoenliche Entscheidungen und Haushaltsstatus gesammelt.',
|
||||
cta: 'Familie oeffnen',
|
||||
description: 'Wichtige persönliche Entscheidungen und Haushaltsstatus gesammelt.',
|
||||
cta: 'Familie öffnen',
|
||||
route: 'FalukantFamily',
|
||||
secondary: true,
|
||||
},
|
||||
@@ -499,7 +483,7 @@ export default {
|
||||
await this.fetchAllStock();
|
||||
await this.fetchProductions();
|
||||
}
|
||||
showSuccess(this, 'Erbe wurde uebernommen.');
|
||||
showSuccess(this, 'Erbe wurde übernommen.');
|
||||
} catch (error) {
|
||||
console.error('Error selecting heir:', error);
|
||||
showError(this, this.$t('falukant.overview.heirSelection.error'));
|
||||
@@ -597,12 +581,54 @@ export default {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.overviewcontainer>div {
|
||||
.overview-panel {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.detail-list,
|
||||
.overview-card-list {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.detail-list__item,
|
||||
.overview-entry-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid var(--color-border);
|
||||
padding: 16px;
|
||||
border-radius: var(--radius-lg);
|
||||
background: rgba(255, 253, 249, 0.82);
|
||||
box-shadow: var(--shadow-soft);
|
||||
background: rgba(255, 255, 255, 0.66);
|
||||
}
|
||||
|
||||
.detail-list__item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.detail-list__item span,
|
||||
.overview-entry-card__meta {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.overview-entry-card {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.overview-entry-card__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 16px;
|
||||
margin-top: 6px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.overview-entry-card--action {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.imagecontainer {
|
||||
|
||||
@@ -2,7 +2,13 @@
|
||||
<div class="politics-view">
|
||||
<StatusBar />
|
||||
|
||||
<section class="politics-hero surface-card">
|
||||
<div>
|
||||
<span class="politics-kicker">Falukant</span>
|
||||
<h2>{{ $t('falukant.politics.title') }}</h2>
|
||||
<p>Ämter, Kandidaturen und Wahlen als klare Aufgabenfläche statt als reine Verwaltungstabelle.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<SimpleTabs v-model="activeTab" :tabs="tabs" @change="onTabChange" />
|
||||
|
||||
@@ -11,71 +17,53 @@
|
||||
<!-- 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>
|
||||
<th>{{ $t('falukant.politics.current.benefit') }}</th>
|
||||
<th>{{ $t('falukant.politics.current.termEnds') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="pos in currentPositions" :key="pos.id" :class="{ 'own-position': isOwnPosition(pos) }">
|
||||
<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 }}
|
||||
<div v-else-if="currentPositions.length" class="politics-card-list">
|
||||
<article v-for="pos in currentPositions" :key="pos.id" class="politics-card" :class="{ 'own-position': isOwnPosition(pos) }">
|
||||
<div class="politics-card__header">
|
||||
<strong>{{ $t(`falukant.politics.offices.${pos.officeType.name}`) }}</strong>
|
||||
<span>{{ pos.region.name }}</span>
|
||||
</div>
|
||||
<div class="politics-card__meta">
|
||||
<span>
|
||||
{{ $t('falukant.politics.current.holder') }}:
|
||||
<template v-if="pos.character">
|
||||
{{ pos.character.definedFirstName.name }} {{ pos.character.definedLastName.name }}
|
||||
</template>
|
||||
<template v-else>—</template>
|
||||
</span>
|
||||
<span v-else>—</span>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="pos.benefit && pos.benefit.length">
|
||||
<span>
|
||||
{{ $t('falukant.politics.current.benefit') }}:
|
||||
<template v-if="pos.benefit && pos.benefit.length">
|
||||
<span v-if="pos.benefit.includes('*')">{{ $t('falukant.politics.current.benefit_all') }}</span>
|
||||
<span v-else>{{ pos.benefit.join(', ') }}</span>
|
||||
</template>
|
||||
<template v-else>—</template>
|
||||
</span>
|
||||
<span v-else>—</span>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="pos.termEnds">
|
||||
{{ formatDate(pos.termEnds) }}
|
||||
<span>
|
||||
{{ $t('falukant.politics.current.termEnds') }}:
|
||||
<template v-if="pos.termEnds">{{ formatDate(pos.termEnds) }}</template>
|
||||
<template v-else>—</template>
|
||||
</span>
|
||||
<span v-else>—</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!currentPositions.length">
|
||||
<td colspan="4">{{ $t('falukant.politics.current.none') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<p v-else class="loading">{{ $t('falukant.politics.current.none') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- OPEN Tab: hier zeigen wir 'openPolitics' -->
|
||||
<div v-else-if="activeTab === 'openPolitics'" class="tab-pane">
|
||||
<p class="politics-age-requirement">{{ $t('falukant.politics.open.ageRequirement') }}</p>
|
||||
<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.candidacyWithAge') }}</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 :title="e.canApplyByAge === false ? $t('falukant.politics.open.minAgeHint') : null">
|
||||
<div v-else-if="openPolitics.length" class="politics-card-list">
|
||||
<article v-for="e in openPolitics" :key="e.id" class="politics-card">
|
||||
<div class="politics-card__header">
|
||||
<strong>{{ $t(`falukant.politics.offices.${e.officeType.name}`) }}</strong>
|
||||
<span>{{ e.region.name }}</span>
|
||||
</div>
|
||||
<div class="politics-card__meta">
|
||||
<span>{{ $t('falukant.politics.open.date') }}: {{ formatDate(e.date) }}</span>
|
||||
</div>
|
||||
<label class="politics-card__checkbox" :title="e.canApplyByAge === false ? $t('falukant.politics.open.minAgeHint') : null">
|
||||
<input
|
||||
type="checkbox"
|
||||
:id="`apply-${e.id}`"
|
||||
@@ -83,14 +71,11 @@
|
||||
:value="e.id"
|
||||
:disabled="e.alreadyApplied || e.canApplyByAge === false"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!openPolitics.length">
|
||||
<td colspan="4">{{ $t('falukant.politics.open.none') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<span>Für diese Kandidatur vormerken</span>
|
||||
</label>
|
||||
</article>
|
||||
</div>
|
||||
<p v-else class="loading">{{ $t('falukant.politics.open.none') }}</p>
|
||||
|
||||
<div class="apply-button">
|
||||
<button :disabled="!selectedApplications.length" @click="submitApplications">
|
||||
@@ -102,25 +87,17 @@
|
||||
<!-- 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">
|
||||
<div v-else-if="elections.length" class="politics-card-list">
|
||||
<article v-for="e in elections" :key="e.id" class="politics-card politics-card--election">
|
||||
<div class="politics-card__header">
|
||||
<strong>{{ $t(`falukant.politics.offices.${e.officeType.name}`) }}</strong>
|
||||
<span>{{ e.region.name }}</span>
|
||||
</div>
|
||||
<div class="politics-card__meta">
|
||||
<span>{{ $t('falukant.politics.elections.date') }}: {{ formatDate(e.date) }}</span>
|
||||
<span>{{ $t('falukant.politics.elections.posts') }}: {{ e.postsToFill }}</span>
|
||||
</div>
|
||||
<div v-if="!e.voted" class="politics-card__vote">
|
||||
<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="">
|
||||
@@ -133,9 +110,13 @@
|
||||
{{ option.name }}
|
||||
</template>
|
||||
</Multiselect>
|
||||
</td>
|
||||
<td v-else>
|
||||
<ul class="voted-list">
|
||||
<button
|
||||
:disabled="!selectedCandidates[e.id] || !selectedCandidates[e.id].length"
|
||||
@click="submitVote(e.id)">
|
||||
{{ $t('falukant.politics.elections.vote') }}
|
||||
</button>
|
||||
</div>
|
||||
<ul v-else class="voted-list">
|
||||
<li v-for="cid in e.votedFor" :key="cid">
|
||||
<span v-if="findCandidateById(e, cid)">
|
||||
{{ formatCandidateTitle(findCandidateById(e, cid)) }}
|
||||
@@ -144,23 +125,9 @@
|
||||
</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>
|
||||
</article>
|
||||
</div>
|
||||
<p v-else class="loading">{{ $t('falukant.politics.elections.none') }}</p>
|
||||
|
||||
<div class="all-vote-button" v-if="hasAnyUnvoted">
|
||||
<button :disabled="!hasAnySelection" @click="submitAllVotes">
|
||||
@@ -177,6 +144,9 @@ import StatusBar from '@/components/falukant/StatusBar.vue';
|
||||
import SimpleTabs from '@/components/SimpleTabs.vue';
|
||||
import Multiselect from 'vue-multiselect';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { showApiError, showSuccess } from '@/utils/feedback.js';
|
||||
|
||||
const debugLog = () => {};
|
||||
|
||||
export default {
|
||||
name: 'PoliticsView',
|
||||
@@ -232,10 +202,10 @@ export default {
|
||||
this.loading.current = true;
|
||||
try {
|
||||
const { data } = await apiClient.get('/api/falukant/politics/overview');
|
||||
console.log('[PoliticsView] loadCurrentPositions - API response:', data);
|
||||
console.log('[PoliticsView] loadCurrentPositions - ownCharacterId at load time:', this.ownCharacterId);
|
||||
debugLog('[PoliticsView] loadCurrentPositions - API response:', data);
|
||||
debugLog('[PoliticsView] loadCurrentPositions - ownCharacterId at load time:', this.ownCharacterId);
|
||||
this.currentPositions = data;
|
||||
console.log('[PoliticsView] loadCurrentPositions - Loaded', data.length, 'positions');
|
||||
debugLog('[PoliticsView] loadCurrentPositions - Loaded', data.length, 'positions');
|
||||
} catch (err) {
|
||||
console.error('[PoliticsView] Error loading current positions', err);
|
||||
} finally {
|
||||
@@ -304,8 +274,10 @@ export default {
|
||||
{ votes: singlePayload }
|
||||
);
|
||||
await this.loadElections();
|
||||
showSuccess(this, 'Stimme erfolgreich abgegeben.');
|
||||
} catch (err) {
|
||||
console.error(`Error submitting vote for election ${electionId}`, err);
|
||||
showApiError(this, err, 'Fehler beim Abgeben der Stimme');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -323,8 +295,10 @@ export default {
|
||||
{ votes: payload }
|
||||
);
|
||||
await this.loadElections();
|
||||
showSuccess(this, 'Alle Stimmen erfolgreich abgegeben.');
|
||||
} catch (err) {
|
||||
console.error('Error submitting all votes', err);
|
||||
showApiError(this, err, 'Fehler beim Abgeben der Stimmen');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -339,18 +313,8 @@ export default {
|
||||
async loadOwnCharacterId() {
|
||||
try {
|
||||
const { data } = await apiClient.get('/api/falukant/info');
|
||||
console.log('[PoliticsView] loadOwnCharacterId - API response:', data);
|
||||
console.log('[PoliticsView] loadOwnCharacterId - data.character:', data.character);
|
||||
console.log('[PoliticsView] loadOwnCharacterId - data.character?.id:', data.character?.id);
|
||||
if (data.character && data.character.id) {
|
||||
this.ownCharacterId = data.character.id;
|
||||
console.log('[PoliticsView] loadOwnCharacterId - Set ownCharacterId to:', this.ownCharacterId);
|
||||
} else {
|
||||
console.warn('[PoliticsView] loadOwnCharacterId - No character ID found in response', {
|
||||
hasCharacter: !!data.character,
|
||||
characterKeys: data.character ? Object.keys(data.character) : null,
|
||||
characterId: data.character?.id
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[PoliticsView] Error loading own character ID', err);
|
||||
@@ -358,20 +322,10 @@ export default {
|
||||
},
|
||||
|
||||
isOwnPosition(pos) {
|
||||
console.log('[PoliticsView] isOwnPosition - Checking position:', {
|
||||
posId: pos.id,
|
||||
posCharacter: pos.character,
|
||||
posCharacterId: pos.character?.id,
|
||||
ownCharacterId: this.ownCharacterId,
|
||||
match: pos.character?.id === this.ownCharacterId
|
||||
});
|
||||
if (!this.ownCharacterId || !pos.character) {
|
||||
console.log('[PoliticsView] isOwnPosition - Returning false (missing ownCharacterId or pos.character)');
|
||||
return false;
|
||||
}
|
||||
const isMatch = pos.character.id === this.ownCharacterId;
|
||||
console.log('[PoliticsView] isOwnPosition - Result:', isMatch);
|
||||
return isMatch;
|
||||
return pos.character.id === this.ownCharacterId;
|
||||
},
|
||||
|
||||
async submitApplications() {
|
||||
@@ -388,12 +342,14 @@ export default {
|
||||
this.selectedApplications = this.openPolitics
|
||||
.filter(e => e.alreadyApplied || appliedIds.includes(e.id))
|
||||
.map(e => e.id);
|
||||
showSuccess(this, 'Kandidatur erfolgreich vorgemerkt.');
|
||||
} catch (err) {
|
||||
console.error('Error submitting applications', err);
|
||||
const msg = err?.response?.data?.error === 'too_young'
|
||||
? this.$t('falukant.politics.too_young')
|
||||
: (err?.response?.data?.error || err?.message || this.$t('falukant.politics.applyError'));
|
||||
this.$root.$refs?.messageDialog?.open?.(msg, this.$t('falukant.politics.title'));
|
||||
if (err?.response?.data?.error === 'too_young') {
|
||||
showApiError(this, err, this.$t('falukant.politics.too_young'));
|
||||
return;
|
||||
}
|
||||
showApiError(this, err, this.$t('falukant.politics.applyError'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -402,73 +358,99 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
.politics-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
max-width: var(--content-max-width);
|
||||
margin: 0 auto;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
.politics-hero {
|
||||
padding: 24px 26px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.politics-kicker {
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(120, 195, 138, 0.14);
|
||||
color: #42634e;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.politics-hero p {
|
||||
margin: 0;
|
||||
padding: 20px 0 0 0;
|
||||
flex: 0 0 auto;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.simple-tabs {
|
||||
flex: 0 0 auto;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tab-pane {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.politics-age-requirement {
|
||||
flex: 0 0 auto;
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 0.95em;
|
||||
color: #555;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.table-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ddd;
|
||||
.politics-card-list {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.politics-table {
|
||||
border-collapse: collapse;
|
||||
width: auto;
|
||||
/* kein 100% */
|
||||
.politics-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 18px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.politics-table thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: #FFF;
|
||||
z-index: 1;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
text-align: left;
|
||||
.politics-card.own-position {
|
||||
border-color: rgba(120, 195, 138, 0.45);
|
||||
background: rgba(236, 248, 238, 0.92);
|
||||
}
|
||||
|
||||
.politics-table tbody td {
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
.politics-card__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.politics-table tbody tr.own-position {
|
||||
background-color: #e0e0e0;
|
||||
font-weight: bold;
|
||||
.politics-card__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 16px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.politics-card__checkbox {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.politics-card__vote {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
@@ -491,6 +473,13 @@ h2 {
|
||||
.all-vote-button button {
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
margin: 2em;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.politics-card__header,
|
||||
.politics-card__meta {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<div class="contenthidden">
|
||||
<div class="contentscroll">
|
||||
<div class="taxi-game-view">
|
||||
<!-- Spiel-Titel -->
|
||||
<div class="game-title">
|
||||
<h1>{{ $t('minigames.taxi.title') }}</h1>
|
||||
@@ -248,8 +247,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -347,6 +347,7 @@
|
||||
|
||||
<script>
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { showError } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: 'CalendarView',
|
||||
@@ -784,7 +785,7 @@ export default {
|
||||
this.closeEventDialog();
|
||||
} catch (error) {
|
||||
console.error('Error saving event:', error);
|
||||
alert(this.$t('personal.calendar.form.saveError') || 'Error saving event');
|
||||
showError(this, this.$t('personal.calendar.form.saveError') || 'Error saving event');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
@@ -803,7 +804,7 @@ export default {
|
||||
this.closeEventDialog();
|
||||
} catch (error) {
|
||||
console.error('Error deleting event:', error);
|
||||
alert(this.$t('personal.calendar.form.deleteError') || 'Error deleting event');
|
||||
showError(this, this.$t('personal.calendar.form.deleteError') || 'Error deleting event');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
|
||||
@@ -160,13 +160,9 @@ export default {
|
||||
},
|
||||
setupSocketListener() {
|
||||
if (this.socket) {
|
||||
console.log("Setting up friendshipChanged listener");
|
||||
this.socket.on("friendshipChanged", (data) => {
|
||||
console.log("Friendship changed:", data);
|
||||
this.socket.on("friendshipChanged", () => {
|
||||
this.fetchFriendships();
|
||||
});
|
||||
} else {
|
||||
console.error("Socket not initialized");
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -47,22 +47,20 @@
|
||||
<h3>{{ $t('socialnetwork.usersearch.results_title') }}</h3>
|
||||
<span class="results-count">{{ searchResults.length }} Treffer</span>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t("socialnetwork.usersearch.result.nick") }}</th>
|
||||
<th>{{ $t("socialnetwork.usersearch.result.gender") }}</th>
|
||||
<th>{{ $t("socialnetwork.usersearch.result.age") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="result in searchResults" :key="result.id">
|
||||
<td><span @click.prevent="openUserProfile(result.id)" :class="'clickable g-' + result.gender">{{ result.username }}</span></td>
|
||||
<td>{{ result.gender }}</td>
|
||||
<td>{{ result.age }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="result-cards">
|
||||
<article v-for="result in searchResults" :key="result.id" class="result-card">
|
||||
<div class="result-card__main">
|
||||
<span @click.prevent="openUserProfile(result.id)" :class="'clickable g-' + result.gender">{{ result.username }}</span>
|
||||
<div class="result-card__meta">
|
||||
<span>{{ $t("socialnetwork.usersearch.result.gender") }}: {{ result.gender }}</span>
|
||||
<span>{{ $t("socialnetwork.usersearch.result.age") }}: {{ result.age }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="button-secondary" @click="openUserProfile(result.id)">
|
||||
Profil öffnen
|
||||
</button>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
<div v-else class="no-results surface-card">
|
||||
{{ $t('socialnetwork.usersearch.no_results') }}
|
||||
@@ -211,26 +209,34 @@ label {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
margin: 0.5em 0;
|
||||
padding: 0;
|
||||
border-collapse: collapse;
|
||||
background: rgba(255, 255, 255, 0.62);
|
||||
.result-cards {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 16px 18px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
background: rgba(255, 255, 255, 0.66);
|
||||
}
|
||||
|
||||
thead {
|
||||
color: #42634e;
|
||||
.result-card__main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
tbody tr + tr td {
|
||||
border-top: 1px solid var(--color-border);
|
||||
.result-card__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px 16px;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
@@ -259,5 +265,10 @@ tbody tr + tr td {
|
||||
.age-range {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -141,6 +141,7 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { showApiError, showError } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: 'VocabCourseListView',
|
||||
@@ -273,7 +274,7 @@ export default {
|
||||
},
|
||||
async findCourseByCode() {
|
||||
if (!this.shareCode.trim()) {
|
||||
alert(this.$t('socialnetwork.vocab.courses.invalidCode'));
|
||||
showError(this, this.$t('socialnetwork.vocab.courses.invalidCode'));
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
@@ -286,7 +287,7 @@ export default {
|
||||
this.openCourse(course.id);
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Suchen des Kurses:', e);
|
||||
alert(e.response?.data?.error || this.$t('socialnetwork.vocab.courses.courseNotFound'));
|
||||
showApiError(this, e, this.$t('socialnetwork.vocab.courses.courseNotFound'));
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
@@ -332,7 +333,7 @@ export default {
|
||||
await this.loadAllCourses();
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Erstellen des Kurses:', e);
|
||||
alert(e.response?.data?.error || 'Fehler beim Erstellen des Kurses');
|
||||
showApiError(this, e, 'Fehler beim Erstellen des Kurses');
|
||||
}
|
||||
},
|
||||
async enroll(courseId) {
|
||||
@@ -342,7 +343,7 @@ export default {
|
||||
this.openCourse(courseId);
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Einschreiben:', e);
|
||||
alert(e.response?.data?.error || 'Fehler beim Einschreiben');
|
||||
showApiError(this, e, 'Fehler beim Einschreiben');
|
||||
}
|
||||
},
|
||||
openCourse(courseId) {
|
||||
|
||||
@@ -29,26 +29,14 @@
|
||||
{{ $t('socialnetwork.vocab.courses.continueCurrentLesson') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="lessons-header">
|
||||
<h3>{{ $t('socialnetwork.vocab.courses.lessons') }}</h3>
|
||||
<table class="lessons-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-number">#</th>
|
||||
<th class="col-title">{{ $t('socialnetwork.vocab.courses.title') }}</th>
|
||||
<th class="col-status">Status</th>
|
||||
<th class="col-actions">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="lesson in course.lessons" :key="lesson.id" class="lesson-row">
|
||||
<td class="lesson-number">{{ lesson.lessonNumber }}</td>
|
||||
<td class="lesson-title">
|
||||
<div class="lesson-title-content">
|
||||
<span class="title-label">{{ lesson.title }}</span>
|
||||
<span v-if="lesson.description" class="lesson-description">{{ lesson.description }}</span>
|
||||
<span class="lessons-count">{{ course.lessons.length }} Lektionen</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="lesson-status">
|
||||
<div class="lesson-cards">
|
||||
<article v-for="lesson in course.lessons" :key="lesson.id" class="lesson-card">
|
||||
<div class="lesson-card__header">
|
||||
<span class="lesson-number">#{{ lesson.lessonNumber }}</span>
|
||||
<div class="lesson-status-content">
|
||||
<span v-if="getLessonProgress(lesson.id)?.completed" class="badge completed">
|
||||
{{ $t('socialnetwork.vocab.courses.completed') }}
|
||||
@@ -60,8 +48,11 @@
|
||||
{{ $t('socialnetwork.vocab.courses.notStarted') }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="lesson-actions">
|
||||
</div>
|
||||
<div class="lesson-title-content">
|
||||
<span class="title-label">{{ lesson.title }}</span>
|
||||
<span v-if="lesson.description" class="lesson-description">{{ lesson.description }}</span>
|
||||
</div>
|
||||
<div class="lesson-actions-content">
|
||||
<button
|
||||
@click="openLesson(lesson.id)"
|
||||
@@ -74,10 +65,8 @@
|
||||
<button v-if="isOwner" @click="editLesson(lesson.id)" class="btn-edit">{{ $t('socialnetwork.vocab.courses.edit') }}</button>
|
||||
<button v-if="isOwner" @click="deleteLesson(lesson.id)" class="btn-delete">{{ $t('general.delete') }}</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p class="surface-card course-state">{{ $t('socialnetwork.vocab.courses.noLessons') }}</p>
|
||||
@@ -122,7 +111,7 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { showApiError, showSuccess } from '@/utils/feedback.js';
|
||||
import { confirmAction, showApiError, showSuccess } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: 'VocabCourseView',
|
||||
@@ -274,7 +263,11 @@ export default {
|
||||
}
|
||||
},
|
||||
async deleteLesson(lessonId) {
|
||||
if (!confirm(this.$t('socialnetwork.vocab.courses.confirmDelete'))) {
|
||||
const confirmed = await confirmAction(this, {
|
||||
title: 'Lektion löschen',
|
||||
message: this.$t('socialnetwork.vocab.courses.confirmDelete')
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -370,6 +363,20 @@ export default {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.lessons-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.lessons-count {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.84rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.course-state {
|
||||
padding: 18px;
|
||||
text-align: center;
|
||||
@@ -403,93 +410,40 @@ export default {
|
||||
border: 1px solid #5D4037;
|
||||
}
|
||||
|
||||
.lessons-table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.lessons-table th,
|
||||
.lessons-table td {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.lessons-table thead {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.lessons-table th {
|
||||
padding: 12px 15px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
font-size: 0.9em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.lessons-table th.col-number,
|
||||
.lessons-table td.lesson-number {
|
||||
width: 50px;
|
||||
min-width: 50px;
|
||||
max-width: 50px;
|
||||
overflow: visible;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.lessons-table th.col-number {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.lessons-table th.col-title,
|
||||
.lessons-table td.lesson-title {
|
||||
width: auto;
|
||||
min-width: 200px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.lessons-table th.col-status,
|
||||
.lessons-table td.lesson-status {
|
||||
width: 200px;
|
||||
min-width: 200px;
|
||||
max-width: 200px;
|
||||
overflow: visible;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.lessons-table th.col-actions,
|
||||
.lessons-table td.lesson-actions {
|
||||
width: 250px;
|
||||
min-width: 250px;
|
||||
max-width: 250px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.lessons-table tbody tr {
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.lessons-table tbody tr:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.lessons-table td {
|
||||
padding: 15px;
|
||||
vertical-align: top;
|
||||
display: table-cell;
|
||||
.lesson-cards {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.lesson-number {
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
font-size: 0.95em;
|
||||
text-align: center;
|
||||
display: block;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 48px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(248, 162, 43, 0.12);
|
||||
}
|
||||
|
||||
.lesson-title {
|
||||
display: block;
|
||||
.lesson-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
padding: 16px 18px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.lesson-card__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.lesson-title-content {
|
||||
@@ -510,10 +464,6 @@ export default {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.lesson-status {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.lesson-status-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -352,8 +352,8 @@
|
||||
|
||||
<!-- Fallback für unbekannte Typen -->
|
||||
<div v-else class="unknown-exercise">
|
||||
<p>Übungstyp: {{ getExerciseType(exercise) }}</p>
|
||||
<pre>{{ JSON.stringify(exercise, null, 2) }}</pre>
|
||||
<p>Dieser Übungstyp wird in der aktuellen Ansicht noch nicht interaktiv dargestellt.</p>
|
||||
<p class="unknown-exercise__type">Typ: {{ getExerciseType(exercise) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -418,6 +418,8 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
|
||||
const debugLog = () => {};
|
||||
|
||||
export default {
|
||||
name: 'VocabLessonView',
|
||||
props: {
|
||||
@@ -533,7 +535,7 @@ export default {
|
||||
// Normale Lektion: Verwende effectiveExercises (grammarExercises)
|
||||
const exercises = this.effectiveExercises;
|
||||
if (!exercises || !Array.isArray(exercises) || exercises.length === 0) {
|
||||
console.log('[importantVocab] Keine Übungen vorhanden');
|
||||
debugLog('[importantVocab] Keine Übungen vorhanden');
|
||||
return [];
|
||||
}
|
||||
return this._extractVocabFromExercises(exercises);
|
||||
@@ -576,7 +578,7 @@ export default {
|
||||
const isArrayLike = exercises.length !== undefined && typeof exercises.length === 'number';
|
||||
|
||||
if (!Array.isArray(exercises) && isArrayLike) {
|
||||
console.log('[_extractVocabFromExercises] exercises ist kein Array, aber array-like, konvertiere...');
|
||||
debugLog('[_extractVocabFromExercises] exercises ist kein Array, aber array-like, konvertiere...');
|
||||
// Versuche Array.from (funktioniert mit iterierbaren Objekten und Array-like Objekten)
|
||||
try {
|
||||
exercisesArray = Array.from(exercises);
|
||||
@@ -601,13 +603,13 @@ export default {
|
||||
|
||||
exercisesArray.forEach((exercise, idx) => {
|
||||
try {
|
||||
console.log(`[importantVocab] Verarbeite Übung ${idx + 1}:`, exercise.title);
|
||||
debugLog(`[importantVocab] Verarbeite Übung ${idx + 1}:`, exercise.title);
|
||||
// Extrahiere aus questionData
|
||||
const qData = this.getQuestionData(exercise);
|
||||
const aData = this.getAnswerData(exercise);
|
||||
|
||||
console.log(`[importantVocab] qData:`, qData);
|
||||
console.log(`[importantVocab] aData:`, aData);
|
||||
debugLog(`[importantVocab] qData:`, qData);
|
||||
debugLog(`[importantVocab] aData:`, aData);
|
||||
|
||||
if (qData && aData) {
|
||||
// Für Multiple Choice: Extrahiere Optionen und richtige Antwort
|
||||
@@ -616,12 +618,12 @@ export default {
|
||||
const correctIndex = aData.correctAnswer !== undefined ? aData.correctAnswer : (aData.correct || 0);
|
||||
const correctAnswer = options[correctIndex] || '';
|
||||
|
||||
console.log(`[importantVocab] Multiple Choice - options:`, options, `correctIndex:`, correctIndex, `correctAnswer:`, correctAnswer);
|
||||
debugLog(`[importantVocab] Multiple Choice - options:`, options, `correctIndex:`, correctIndex, `correctAnswer:`, correctAnswer);
|
||||
|
||||
if (correctAnswer) {
|
||||
// Versuche die Frage zu analysieren (z.B. "Wie sagt man 'X' auf Bisaya?" oder "Was bedeutet 'X'?")
|
||||
const question = qData.question || qData.text || '';
|
||||
console.log(`[importantVocab] Frage:`, question);
|
||||
debugLog(`[importantVocab] Frage:`, question);
|
||||
|
||||
// Pattern 1: "Wie sagt man 'X' auf Bisaya?" -> X ist Muttersprache (z.B. "Großmutter"), correctAnswer ist Bisaya (z.B. "Lola")
|
||||
let match = question.match(/Wie sagt man ['"]([^'"]+)['"]/i);
|
||||
@@ -629,11 +631,11 @@ export default {
|
||||
const nativeWord = match[1]; // Das Wort in der Muttersprache
|
||||
// Nur hinzufügen, wenn Muttersprache und Bisaya unterschiedlich sind (verhindert "ko" -> "ko")
|
||||
if (nativeWord && correctAnswer && nativeWord.trim() !== correctAnswer.trim()) {
|
||||
console.log(`[importantVocab] Pattern 1 gefunden - Muttersprache:`, nativeWord, `Bisaya:`, correctAnswer);
|
||||
debugLog(`[importantVocab] Pattern 1 gefunden - Muttersprache:`, nativeWord, `Bisaya:`, correctAnswer);
|
||||
// learning = Muttersprache (was man lernt), reference = Bisaya (Zielsprache)
|
||||
vocabMap.set(`${nativeWord}-${correctAnswer}`, { learning: nativeWord, reference: correctAnswer });
|
||||
} else {
|
||||
console.log(`[importantVocab] Pattern 1 übersprungen - Muttersprache und Bisaya sind gleich:`, nativeWord, correctAnswer);
|
||||
debugLog(`[importantVocab] Pattern 1 übersprungen - Muttersprache und Bisaya sind gleich:`, nativeWord, correctAnswer);
|
||||
}
|
||||
} else {
|
||||
// Pattern 2: "Was bedeutet 'X'?" -> X ist Bisaya, correctAnswer ist Muttersprache
|
||||
@@ -642,14 +644,14 @@ export default {
|
||||
const bisayaWord = match[1];
|
||||
// Nur hinzufügen, wenn Bisaya und Muttersprache unterschiedlich sind (verhindert "ko" -> "ko")
|
||||
if (bisayaWord && correctAnswer && bisayaWord.trim() !== correctAnswer.trim()) {
|
||||
console.log(`[importantVocab] Pattern 2 gefunden - Bisaya:`, bisayaWord, `Muttersprache:`, correctAnswer);
|
||||
debugLog(`[importantVocab] Pattern 2 gefunden - Bisaya:`, bisayaWord, `Muttersprache:`, correctAnswer);
|
||||
// learning = Muttersprache (was man lernt), reference = Bisaya (Zielsprache)
|
||||
vocabMap.set(`${correctAnswer}-${bisayaWord}`, { learning: correctAnswer, reference: bisayaWord });
|
||||
} else {
|
||||
console.log(`[importantVocab] Pattern 2 übersprungen - Bisaya und Muttersprache sind gleich:`, bisayaWord, correctAnswer);
|
||||
debugLog(`[importantVocab] Pattern 2 übersprungen - Bisaya und Muttersprache sind gleich:`, bisayaWord, correctAnswer);
|
||||
}
|
||||
} else {
|
||||
console.log(`[importantVocab] Kein Pattern gefunden, Überspringe diese Übung`);
|
||||
debugLog(`[importantVocab] Kein Pattern gefunden, Überspringe diese Übung`);
|
||||
// Überspringe, wenn wir die Richtung nicht erkennen können
|
||||
}
|
||||
}
|
||||
@@ -659,7 +661,7 @@ export default {
|
||||
// Für Gap Fill: Extrahiere richtige Antworten
|
||||
if (this.getExerciseType(exercise) === 'gap_fill') {
|
||||
const answers = aData.answers || (aData.correct ? (Array.isArray(aData.correct) ? aData.correct : [aData.correct]) : []);
|
||||
console.log(`[importantVocab] Gap Fill - answers:`, answers);
|
||||
debugLog(`[importantVocab] Gap Fill - answers:`, answers);
|
||||
if (answers.length > 0) {
|
||||
// Versuche aus dem Text Kontext zu extrahieren
|
||||
// Gap Fill hat normalerweise Format: "{gap} (Muttersprache) | {gap} (Muttersprache) | ..."
|
||||
@@ -668,7 +670,7 @@ export default {
|
||||
const matches = text.matchAll(/\(([^)]+)\)/g);
|
||||
const nativeWords = Array.from(matches, m => m[1]);
|
||||
|
||||
console.log(`[importantVocab] Gap Fill - text:`, text, `nativeWords:`, nativeWords);
|
||||
debugLog(`[importantVocab] Gap Fill - text:`, text, `nativeWords:`, nativeWords);
|
||||
|
||||
// Nur extrahieren, wenn Muttersprache-Hinweise (Klammern) vorhanden sind
|
||||
if (nativeWords.length > 0) {
|
||||
@@ -679,14 +681,14 @@ export default {
|
||||
// Die answer ist normalerweise Bisaya, nativeWord ist Muttersprache
|
||||
// Nur hinzufügen, wenn sie unterschiedlich sind (verhindert "ko" -> "ko")
|
||||
vocabMap.set(`${nativeWord}-${answer}`, { learning: nativeWord, reference: answer });
|
||||
console.log(`[importantVocab] Gap Fill extrahiert - Muttersprache:`, nativeWord, `Bisaya:`, answer);
|
||||
debugLog(`[importantVocab] Gap Fill extrahiert - Muttersprache:`, nativeWord, `Bisaya:`, answer);
|
||||
} else {
|
||||
console.log(`[importantVocab] Gap Fill übersprungen - keine Muttersprache oder gleich:`, nativeWord, answer);
|
||||
debugLog(`[importantVocab] Gap Fill übersprungen - keine Muttersprache oder gleich:`, nativeWord, answer);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log(`[importantVocab] Gap Fill übersprungen - keine Muttersprache-Hinweise (Klammern) gefunden`);
|
||||
debugLog(`[importantVocab] Gap Fill übersprungen - keine Muttersprache-Hinweise (Klammern) gefunden`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -697,17 +699,17 @@ export default {
|
||||
});
|
||||
|
||||
const result = Array.from(vocabMap.values());
|
||||
console.log(`[_extractVocabFromExercises] Ergebnis:`, result.length, 'Vokabeln');
|
||||
debugLog(`[_extractVocabFromExercises] Ergebnis:`, result.length, 'Vokabeln');
|
||||
return result;
|
||||
},
|
||||
async loadLesson() {
|
||||
// Verhindere mehrfaches Laden
|
||||
if (this.loading) {
|
||||
console.log('[VocabLessonView] loadLesson übersprungen - bereits am Laden');
|
||||
debugLog('[VocabLessonView] loadLesson übersprungen - bereits am Laden');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[VocabLessonView] loadLesson gestartet für lessonId:', this.lessonId);
|
||||
debugLog('[VocabLessonView] loadLesson gestartet für lessonId:', this.lessonId);
|
||||
this.loading = true;
|
||||
// Setze Antworten und Ergebnisse zurück
|
||||
this.exerciseAnswers = {};
|
||||
@@ -725,19 +727,19 @@ export default {
|
||||
try {
|
||||
const res = await apiClient.get(`/api/vocab/lessons/${this.lessonId}`);
|
||||
this.lesson = res.data;
|
||||
console.log('[VocabLessonView] Geladene Lektion:', this.lesson?.id, this.lesson?.title);
|
||||
debugLog('[VocabLessonView] Geladene Lektion:', this.lesson?.id, this.lesson?.title);
|
||||
// Initialisiere mit effectiveExercises (für Review: reviewVocabExercises, sonst: grammarExercises)
|
||||
this.$nextTick(async () => {
|
||||
const exercises = this.effectiveExercises;
|
||||
if (exercises && exercises.length > 0) {
|
||||
console.log('[VocabLessonView] Übungen für Kapitel-Prüfung:', exercises.length);
|
||||
debugLog('[VocabLessonView] Übungen für Kapitel-Prüfung:', exercises.length);
|
||||
this.initializeExercises(exercises);
|
||||
} else {
|
||||
console.log('[VocabLessonView] Lade Übungen separat...');
|
||||
debugLog('[VocabLessonView] Lade Übungen separat...');
|
||||
await this.loadGrammarExercises();
|
||||
}
|
||||
});
|
||||
console.log('[VocabLessonView] loadLesson abgeschlossen');
|
||||
debugLog('[VocabLessonView] loadLesson abgeschlossen');
|
||||
} catch (e) {
|
||||
console.error('[VocabLessonView] Fehler beim Laden der Lektion:', e);
|
||||
} finally {
|
||||
@@ -883,18 +885,18 @@ export default {
|
||||
async checkLessonCompletion() {
|
||||
// Verhindere mehrfache Ausführung
|
||||
if (this.isCheckingLessonCompletion || this.isNavigatingToNext) {
|
||||
console.log('[VocabLessonView] checkLessonCompletion übersprungen - bereits in Ausführung');
|
||||
debugLog('[VocabLessonView] checkLessonCompletion übersprungen - bereits in Ausführung');
|
||||
return;
|
||||
}
|
||||
|
||||
// Prüfe ob alle Übungen korrekt beantwortet wurden (effectiveExercises = Kapitel-Prüfung)
|
||||
const allExercises = this.effectiveExercises;
|
||||
if (!this.lesson || !allExercises || allExercises.length === 0) {
|
||||
console.log('[VocabLessonView] checkLessonCompletion übersprungen - keine Lektion/Übungen');
|
||||
debugLog('[VocabLessonView] checkLessonCompletion übersprungen - keine Lektion/Übungen');
|
||||
return;
|
||||
}
|
||||
if (allExercises.length === 0) {
|
||||
console.log('[VocabLessonView] checkLessonCompletion übersprungen - keine Übungen');
|
||||
debugLog('[VocabLessonView] checkLessonCompletion übersprungen - keine Übungen');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -904,11 +906,11 @@ export default {
|
||||
return result && result.correct;
|
||||
});
|
||||
|
||||
console.log('[VocabLessonView] checkLessonCompletion - allCompleted:', allCompleted, 'Übungen:', allExercises.length, 'Korrekt:', allExercises.filter(ex => this.exerciseResults[ex.id]?.correct).length);
|
||||
debugLog('[VocabLessonView] checkLessonCompletion - allCompleted:', allCompleted, 'Übungen:', allExercises.length, 'Korrekt:', allExercises.filter(ex => this.exerciseResults[ex.id]?.correct).length);
|
||||
|
||||
if (allCompleted && !this.isCheckingLessonCompletion) {
|
||||
this.isCheckingLessonCompletion = true;
|
||||
console.log('[VocabLessonView] Alle Übungen abgeschlossen - starte Fortschritts-Update');
|
||||
debugLog('[VocabLessonView] Alle Übungen abgeschlossen - starte Fortschritts-Update');
|
||||
|
||||
try {
|
||||
// Berechne Gesamt-Score
|
||||
@@ -916,7 +918,7 @@ export default {
|
||||
const correctExercises = allExercises.filter(ex => this.exerciseResults[ex.id]?.correct).length;
|
||||
const score = Math.round((correctExercises / totalExercises) * 100);
|
||||
|
||||
console.log('[VocabLessonView] Score berechnet:', score, '%');
|
||||
debugLog('[VocabLessonView] Score berechnet:', score, '%');
|
||||
|
||||
// Aktualisiere Fortschritt
|
||||
await apiClient.put(`/api/vocab/lessons/${this.lessonId}/progress`, {
|
||||
@@ -925,7 +927,7 @@ export default {
|
||||
timeSpentMinutes: 0 // TODO: Zeit tracken
|
||||
});
|
||||
|
||||
console.log('[VocabLessonView] Fortschritt aktualisiert - starte Navigation');
|
||||
debugLog('[VocabLessonView] Fortschritt aktualisiert - starte Navigation');
|
||||
|
||||
// Weiterleitung zur nächsten Lektion
|
||||
await this.navigateToNextLesson();
|
||||
@@ -938,7 +940,7 @@ export default {
|
||||
async navigateToNextLesson() {
|
||||
// Verhindere mehrfache Navigation
|
||||
if (this.isNavigatingToNext) {
|
||||
console.log('[VocabLessonView] Navigation bereits in Gang, überspringe...');
|
||||
debugLog('[VocabLessonView] Navigation bereits in Gang, überspringe...');
|
||||
return;
|
||||
}
|
||||
this.isNavigatingToNext = true;
|
||||
@@ -949,7 +951,7 @@ export default {
|
||||
const course = courseRes.data;
|
||||
|
||||
if (!course.lessons || course.lessons.length === 0) {
|
||||
console.log('[VocabLessonView] Keine Lektionen gefunden');
|
||||
debugLog('[VocabLessonView] Keine Lektionen gefunden');
|
||||
this.isNavigatingToNext = false;
|
||||
this.isCheckingLessonCompletion = false;
|
||||
return;
|
||||
@@ -961,7 +963,7 @@ export default {
|
||||
if (currentLessonIndex >= 0 && currentLessonIndex < course.lessons.length - 1) {
|
||||
// Nächste Lektion gefunden
|
||||
const nextLesson = course.lessons[currentLessonIndex + 1];
|
||||
console.log('[VocabLessonView] Nächste Lektion gefunden:', nextLesson.id);
|
||||
debugLog('[VocabLessonView] Nächste Lektion gefunden:', nextLesson.id);
|
||||
|
||||
// Zeige Erfolgs-Meldung und leite weiter
|
||||
// Verwende Dialog statt confirm
|
||||
@@ -969,7 +971,7 @@ export default {
|
||||
this.nextLessonId = nextLesson.id;
|
||||
} else {
|
||||
// Letzte Lektion - zeige Abschluss-Meldung
|
||||
console.log('[VocabLessonView] Letzte Lektion erreicht');
|
||||
debugLog('[VocabLessonView] Letzte Lektion erreicht');
|
||||
this.showCompletionDialog = true;
|
||||
this.isNavigatingToNext = false;
|
||||
this.isCheckingLessonCompletion = false;
|
||||
@@ -985,7 +987,7 @@ export default {
|
||||
},
|
||||
confirmNavigateToNextLesson() {
|
||||
if (this.nextLessonId) {
|
||||
console.log('[VocabLessonView] Navigiere zur nächsten Lektion:', this.nextLessonId);
|
||||
debugLog('[VocabLessonView] Navigiere zur nächsten Lektion:', this.nextLessonId);
|
||||
// Setze Flags zurück BEVOR die Navigation stattfindet
|
||||
this.isNavigatingToNext = false;
|
||||
this.isCheckingLessonCompletion = false;
|
||||
@@ -996,7 +998,7 @@ export default {
|
||||
}
|
||||
},
|
||||
cancelNavigateToNextLesson() {
|
||||
console.log('[VocabLessonView] Navigation abgebrochen');
|
||||
debugLog('[VocabLessonView] Navigation abgebrochen');
|
||||
this.isNavigatingToNext = false;
|
||||
this.isCheckingLessonCompletion = false;
|
||||
this.showNextLessonDialog = false;
|
||||
@@ -1011,13 +1013,13 @@ export default {
|
||||
},
|
||||
// Vokabeltrainer-Methoden
|
||||
startVocabTrainer() {
|
||||
console.log('[VocabLessonView] startVocabTrainer aufgerufen');
|
||||
debugLog('[VocabLessonView] startVocabTrainer aufgerufen');
|
||||
if (!this.importantVocab || this.importantVocab.length === 0) {
|
||||
console.log('[VocabLessonView] Keine Vokabeln vorhanden');
|
||||
debugLog('[VocabLessonView] Keine Vokabeln vorhanden');
|
||||
return;
|
||||
}
|
||||
console.log('[VocabLessonView] Vokabeln gefunden:', this.importantVocab.length);
|
||||
console.log('[VocabLessonView] Alte Vokabeln:', this.previousVocab?.length || 0);
|
||||
debugLog('[VocabLessonView] Vokabeln gefunden:', this.importantVocab.length);
|
||||
debugLog('[VocabLessonView] Alte Vokabeln:', this.previousVocab?.length || 0);
|
||||
this.vocabTrainerActive = true;
|
||||
this.vocabTrainerPool = [...this.importantVocab];
|
||||
this.vocabTrainerMode = 'multiple_choice';
|
||||
@@ -1030,8 +1032,8 @@ export default {
|
||||
this.vocabTrainerMixedAttempts = 0;
|
||||
// Bereite Mixed-Pool aus alten Vokabeln vor (ohne Duplikate aus aktueller Lektion)
|
||||
this.vocabTrainerMixedPool = this._buildMixedPool();
|
||||
console.log('[VocabLessonView] Mixed-Pool:', this.vocabTrainerMixedPool.length, 'Vokabeln');
|
||||
console.log('[VocabLessonView] Rufe nextVocabQuestion auf');
|
||||
debugLog('[VocabLessonView] Mixed-Pool:', this.vocabTrainerMixedPool.length, 'Vokabeln');
|
||||
debugLog('[VocabLessonView] Rufe nextVocabQuestion auf');
|
||||
this.$nextTick(() => {
|
||||
this.nextVocabQuestion();
|
||||
});
|
||||
@@ -1078,7 +1080,7 @@ export default {
|
||||
if (successRate >= 80) {
|
||||
// Wechsel zur Mixed-Phase (falls alte Vokabeln vorhanden)
|
||||
if (this.vocabTrainerMixedPool.length > 0) {
|
||||
console.log('[VocabLessonView] Wechsel zu Mixed-Phase mit', this.vocabTrainerMixedPool.length, 'alten Vokabeln');
|
||||
debugLog('[VocabLessonView] Wechsel zu Mixed-Phase mit', this.vocabTrainerMixedPool.length, 'alten Vokabeln');
|
||||
this.vocabTrainerPhase = 'mixed';
|
||||
this.vocabTrainerPool = [...this.vocabTrainerMixedPool];
|
||||
this.vocabTrainerMixedAttempts = 0;
|
||||
@@ -1100,7 +1102,7 @@ export default {
|
||||
} else if (this.vocabTrainerPhase === 'mixed') {
|
||||
// Phase 2: Gemischte Wiederholung - nach MIXED_LIMIT Versuchen → Wechsel zu Typing mit allen Vokabeln
|
||||
if (this.vocabTrainerMode === 'multiple_choice' && this.vocabTrainerTotalAttempts >= MIXED_LIMIT) {
|
||||
console.log('[VocabLessonView] Mixed-Phase abgeschlossen, wechsle zu Typing');
|
||||
debugLog('[VocabLessonView] Mixed-Phase abgeschlossen, wechsle zu Typing');
|
||||
this.vocabTrainerMode = 'typing';
|
||||
this.vocabTrainerAutoSwitchedToTyping = true;
|
||||
// Im Typing: Pool aus aktuellen + alten Vokabeln kombinieren
|
||||
@@ -1206,9 +1208,9 @@ export default {
|
||||
return arr;
|
||||
},
|
||||
nextVocabQuestion() {
|
||||
console.log('[VocabLessonView] nextVocabQuestion aufgerufen');
|
||||
debugLog('[VocabLessonView] nextVocabQuestion aufgerufen');
|
||||
if (!this.vocabTrainerPool || this.vocabTrainerPool.length === 0) {
|
||||
console.log('[VocabLessonView] Keine Vokabeln im Pool');
|
||||
debugLog('[VocabLessonView] Keine Vokabeln im Pool');
|
||||
this.currentVocabQuestion = null;
|
||||
return;
|
||||
}
|
||||
@@ -1227,7 +1229,7 @@ export default {
|
||||
key: this.getVocabKey(vocab)
|
||||
};
|
||||
|
||||
console.log('[VocabLessonView] Neue Frage erstellt:', this.currentVocabQuestion.prompt);
|
||||
debugLog('[VocabLessonView] Neue Frage erstellt:', this.currentVocabQuestion.prompt);
|
||||
|
||||
// Reset UI
|
||||
this.vocabTrainerAnswer = '';
|
||||
@@ -1236,16 +1238,16 @@ export default {
|
||||
|
||||
// Erstelle Choice-Optionen für Multiple Choice
|
||||
if (this.vocabTrainerMode === 'multiple_choice') {
|
||||
console.log('[VocabLessonView] Erstelle Choice-Optionen...');
|
||||
console.log('[VocabLessonView] Prompt:', this.currentVocabQuestion.prompt);
|
||||
console.log('[VocabLessonView] Answer:', this.currentVocabQuestion.answer);
|
||||
debugLog('[VocabLessonView] Erstelle Choice-Optionen...');
|
||||
debugLog('[VocabLessonView] Prompt:', this.currentVocabQuestion.prompt);
|
||||
debugLog('[VocabLessonView] Answer:', this.currentVocabQuestion.answer);
|
||||
// Wichtig: Der Prompt (die Frage) darf nicht in den Optionen erscheinen
|
||||
this.vocabTrainerChoiceOptions = this.buildChoiceOptions(
|
||||
this.currentVocabQuestion.answer,
|
||||
this.vocabTrainerPool,
|
||||
this.currentVocabQuestion.prompt // Exkludiere den Prompt
|
||||
);
|
||||
console.log('[VocabLessonView] Choice-Optionen erstellt:', this.vocabTrainerChoiceOptions);
|
||||
debugLog('[VocabLessonView] Choice-Optionen erstellt:', this.vocabTrainerChoiceOptions);
|
||||
}
|
||||
|
||||
// Fokussiere Eingabefeld im Typing-Modus
|
||||
@@ -1597,10 +1599,10 @@ export default {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.unknown-exercise pre {
|
||||
margin-top: 10px;
|
||||
font-size: 0.85em;
|
||||
overflow-x: auto;
|
||||
.unknown-exercise__type {
|
||||
margin-top: 8px;
|
||||
font-size: 0.9em;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<article class="summary-card surface-card">
|
||||
<span class="summary-card__label">Abonniert</span>
|
||||
<strong>{{ subscribedLanguages.length }}</strong>
|
||||
<p>Diese Bereiche sind eher fuer Lernen und Fortschritt statt Verwaltung gedacht.</p>
|
||||
<p>Diese Bereiche sind eher für Lernen und Fortschritt statt Verwaltung gedacht.</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
</article>
|
||||
<article class="task-card surface-card">
|
||||
<span class="task-card__eyebrow">Weiterlernen</span>
|
||||
<h3>Kurse und Kapitel oeffnen</h3>
|
||||
<h3>Kurse und Kapitel öffnen</h3>
|
||||
<p>Springe direkt in bestehende Lernpfade und arbeite mit vorhandenen Kursen weiter.</p>
|
||||
<button type="button" class="button-secondary" @click="goCourses">{{ $t('socialnetwork.vocab.courses.title') }}</button>
|
||||
</article>
|
||||
@@ -77,7 +77,7 @@
|
||||
<div class="language-section__header">
|
||||
<div>
|
||||
<h3>Abonnierte Sprachen</h3>
|
||||
<p>Gut fuer schnellen Wiedereinstieg ins Lernen ohne Verwaltungsaufwand.</p>
|
||||
<p>Gut für schnellen Wiedereinstieg ins Lernen ohne Verwaltungsaufwand.</p>
|
||||
</div>
|
||||
<span class="language-section__count">{{ subscribedLanguages.length }}</span>
|
||||
</div>
|
||||
@@ -86,7 +86,7 @@
|
||||
<button type="button" class="language-card__main" @click="openLanguage(l.id)">
|
||||
<div class="language-card__info">
|
||||
<span class="langname">{{ l.name }}</span>
|
||||
<span class="language-card__hint">Lernen, ueben und Fortschritt ansehen</span>
|
||||
<span class="language-card__hint">Lernen, üben und Fortschritt ansehen</span>
|
||||
</div>
|
||||
<span class="role">{{ $t('socialnetwork.vocab.subscribed') }}</span>
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user