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
|
- breite Tabellen auf kleinen Screens per horizontalem Scroll-Fallback abgesichert
|
||||||
- globale Touch-Ziele fuer Buttons leicht vergroessert und letzte Shell-Kanten geglaettet
|
- 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
|
## Konkreter Arbeitskatalog
|
||||||
|
|
||||||
### 1. Shell und Navigation
|
### 1. Shell und Navigation
|
||||||
@@ -378,6 +405,13 @@ Aktueller Stand:
|
|||||||
- breite Inhalte auf kleine Screens pruefen
|
- breite Inhalte auf kleine Screens pruefen
|
||||||
- Dialoge und Tabellen fuer Touch 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
|
## Definition of Done
|
||||||
|
|
||||||
Das Bedienbarkeitsprojekt gilt als abgeschlossen, wenn:
|
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
|
- P1- und P2-Probleme aus dem Audit abgearbeitet sind
|
||||||
- Navigation, Formulare, Dialoge und Feedback nach gemeinsamen Regeln funktionieren
|
- Navigation, Formulare, Dialoge und Feedback nach gemeinsamen Regeln funktionieren
|
||||||
- Kernaufgaben auf Desktop und kleinem Viewport ohne strukturelle Reibung moeglich sind
|
- 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
|
- Restpunkte nur noch P3/P4-Feinschliff sind
|
||||||
|
|
||||||
## Empfohlene Reihenfolge
|
## Empfohlene Reihenfolge
|
||||||
@@ -395,7 +430,8 @@ Das Bedienbarkeitsprojekt gilt als abgeschlossen, wenn:
|
|||||||
3. Formulare und Abschlusslogik
|
3. Formulare und Abschlusslogik
|
||||||
4. Falukant, Vokabeltrainer, Admin, Minigames
|
4. Falukant, Vokabeltrainer, Admin, Minigames
|
||||||
5. Mobile Endabnahme
|
5. Mobile Endabnahme
|
||||||
|
6. Vereinfachungsnachlauf ueber Feedback, Scrolllogik und tabellenlastige Restbereiche
|
||||||
|
|
||||||
## Naechster konkreter Schritt
|
## 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>
|
<script>
|
||||||
import { mapGetters, mapActions } from 'vuex';
|
import { mapGetters, mapActions } from 'vuex';
|
||||||
import { createApp } from 'vue';
|
|
||||||
import apiClient from '@/utils/axios.js';
|
import apiClient from '@/utils/axios.js';
|
||||||
import { EventBus } from '@/utils/eventBus.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 {
|
export default {
|
||||||
name: 'AppNavigation',
|
name: 'AppNavigation',
|
||||||
components: {
|
|
||||||
RandomChatDialog,
|
|
||||||
MultiChatDialog
|
|
||||||
},
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
forumList: [],
|
forumList: [],
|
||||||
@@ -292,7 +279,8 @@ export default {
|
|||||||
this.pinnedSubKey = this.pinnedSubKey === key ? null : key;
|
this.pinnedSubKey = this.pinnedSubKey === key ? null : key;
|
||||||
},
|
},
|
||||||
|
|
||||||
collapseMenus() {
|
collapseMenus(options = {}) {
|
||||||
|
const { blurActiveElement = true } = options;
|
||||||
this.expandedMainKey = null;
|
this.expandedMainKey = null;
|
||||||
this.expandedSubKey = null;
|
this.expandedSubKey = null;
|
||||||
this.pinnedMainKey = null;
|
this.pinnedMainKey = null;
|
||||||
@@ -305,11 +293,13 @@ export default {
|
|||||||
this.suppressHover = false;
|
this.suppressHover = false;
|
||||||
this.hoverReleaseTimer = null;
|
this.hoverReleaseTimer = null;
|
||||||
}, 180);
|
}, 180);
|
||||||
this.$nextTick(() => {
|
if (blurActiveElement) {
|
||||||
if (document.activeElement && typeof document.activeElement.blur === 'function') {
|
this.$nextTick(() => {
|
||||||
document.activeElement.blur();
|
if (document.activeElement && typeof document.activeElement.blur === 'function') {
|
||||||
}
|
document.activeElement.blur();
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleDocumentClick(event) {
|
handleDocumentClick(event) {
|
||||||
@@ -317,7 +307,7 @@ export default {
|
|||||||
if (!root || root.contains(event.target)) {
|
if (!root || root.contains(event.target)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.collapseMenus();
|
this.collapseMenus({ blurActiveElement: false });
|
||||||
},
|
},
|
||||||
|
|
||||||
handleDocumentKeydown(event) {
|
handleDocumentKeydown(event) {
|
||||||
@@ -435,10 +425,21 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
openChat(userId) {
|
openChat(userId) {
|
||||||
console.log('openChat:', userId);
|
const dialogRef = this.$root.$refs.multiChatDialog;
|
||||||
// Datei erstellen und ans body anhängen
|
const friend = this.friendsList.find((entry) => entry.id === userId);
|
||||||
const container = document.createElement('div');
|
if (!dialogRef || typeof dialogRef.open !== 'function') {
|
||||||
document.body.appendChild(container);
|
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>
|
<script>
|
||||||
import apiClient from '@/utils/axios.js';
|
import apiClient from '@/utils/axios.js';
|
||||||
import NewDirectorDialog from '@/dialogues/falukant/NewDirectorDialog.vue';
|
import NewDirectorDialog from '@/dialogues/falukant/NewDirectorDialog.vue';
|
||||||
|
import { showError, showInfo, showSuccess } from '@/utils/feedback.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "DirectorInfo",
|
name: "DirectorInfo",
|
||||||
@@ -307,11 +308,11 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
fireDirector() {
|
fireDirector() {
|
||||||
alert(this.$t('falukant.branch.director.fireAlert'));
|
showInfo(this, this.$t('falukant.branch.director.fireAlert'));
|
||||||
},
|
},
|
||||||
|
|
||||||
teachDirector() {
|
teachDirector() {
|
||||||
alert(this.$t('falukant.branch.director.teachAlert'));
|
showInfo(this, this.$t('falukant.branch.director.teachAlert'));
|
||||||
},
|
},
|
||||||
|
|
||||||
vehicleTypeOptions() {
|
vehicleTypeOptions() {
|
||||||
@@ -440,11 +441,11 @@ export default {
|
|||||||
cost: 0.1,
|
cost: 0.1,
|
||||||
costLabel: '',
|
costLabel: '',
|
||||||
};
|
};
|
||||||
alert(this.$t('falukant.branch.director.emptyTransport.success'));
|
showSuccess(this, this.$t('falukant.branch.director.emptyTransport.success'));
|
||||||
this.$emit('transportCreated');
|
this.$emit('transportCreated');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating empty transport:', 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'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -575,4 +576,4 @@ export default {
|
|||||||
.transport-route > div {
|
.transport-route > div {
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -63,6 +63,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import apiClient from '@/utils/axios.js';
|
import apiClient from '@/utils/axios.js';
|
||||||
|
import { showApiError } from '@/utils/feedback.js';
|
||||||
export default {
|
export default {
|
||||||
name: "ProductionSection",
|
name: "ProductionSection",
|
||||||
props: {
|
props: {
|
||||||
@@ -166,7 +167,7 @@
|
|||||||
});
|
});
|
||||||
this.loadProductions();
|
this.loadProductions();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(this.$t(`falukant.branch.production.error${error.response.data.error}`));
|
showApiError(this, error, 'tr:error.network');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,4 +206,4 @@
|
|||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -173,6 +173,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import apiClient from '@/utils/axios.js';
|
import apiClient from '@/utils/axios.js';
|
||||||
|
import { showError, showSuccess } from '@/utils/feedback.js';
|
||||||
export default {
|
export default {
|
||||||
name: "SaleSection",
|
name: "SaleSection",
|
||||||
props: {
|
props: {
|
||||||
@@ -334,13 +335,13 @@
|
|||||||
quantity: quantityToSell,
|
quantity: quantityToSell,
|
||||||
quality: item.quality,
|
quality: item.quality,
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
alert(this.$t('falukant.branch.sale.sellError'));
|
showError(this, this.$t('falukant.branch.sale.sellError'));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
sellAll() {
|
sellAll() {
|
||||||
apiClient.post(`/api/falukant/sell/all`, { branchId: this.branchId })
|
apiClient.post(`/api/falukant/sell/all`, { branchId: this.branchId })
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
alert(this.$t('falukant.branch.sale.sellAllError'));
|
showError(this, this.$t('falukant.branch.sale.sellAllError'));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
inventoryOptions() {
|
inventoryOptions() {
|
||||||
@@ -502,11 +503,11 @@
|
|||||||
});
|
});
|
||||||
await this.loadInventory();
|
await this.loadInventory();
|
||||||
await this.loadTransports();
|
await this.loadTransports();
|
||||||
alert(this.$t('falukant.branch.sale.transportStarted'));
|
showSuccess(this, this.$t('falukant.branch.sale.transportStarted'));
|
||||||
this.$emit('transportCreated');
|
this.$emit('transportCreated');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating transport:', error);
|
console.error('Error creating transport:', error);
|
||||||
alert(this.$t('falukant.branch.sale.transportError'));
|
showError(this, this.$t('falukant.branch.sale.transportError'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async loadTransports() {
|
async loadTransports() {
|
||||||
@@ -596,4 +597,4 @@
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import apiClient from '@/utils/axios.js';
|
import apiClient from '@/utils/axios.js';
|
||||||
|
import { showError } from '@/utils/feedback.js';
|
||||||
export default {
|
export default {
|
||||||
name: "StorageSection",
|
name: "StorageSection",
|
||||||
props: { branchId: { type: Number, required: true } },
|
props: { branchId: { type: Number, required: true } },
|
||||||
@@ -164,12 +165,12 @@
|
|||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(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;
|
remainingAmount -= toBuy;
|
||||||
}
|
}
|
||||||
if (remainingAmount > 0) {
|
if (remainingAmount > 0) {
|
||||||
alert(this.$t('falukant.branch.storage.notEnoughAvailable'));
|
showError(this, this.$t('falukant.branch.storage.notEnoughAvailable'));
|
||||||
}
|
}
|
||||||
this.loadStorageData();
|
this.loadStorageData();
|
||||||
},
|
},
|
||||||
@@ -185,7 +186,7 @@
|
|||||||
.then(() => this.loadStorageData())
|
.then(() => this.loadStorageData())
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
alert('Error selling storage');
|
showError(this, 'Fehler beim Verkaufen der Lagerkapazität.');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getCostOfType(labelTr) {
|
getCostOfType(labelTr) {
|
||||||
@@ -216,4 +217,4 @@
|
|||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -239,6 +239,7 @@ import DialogWidget from '@/components/DialogWidget.vue';
|
|||||||
import { fetchPublicRooms, fetchRoomCreateOptions, fetchOwnRooms } from '@/api/chatApi.js';
|
import { fetchPublicRooms, fetchRoomCreateOptions, fetchOwnRooms } from '@/api/chatApi.js';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { getChatWsUrl, getChatWsCandidates, getChatWsProtocols } from '@/services/chatWs.js';
|
import { getChatWsUrl, getChatWsCandidates, getChatWsProtocols } from '@/services/chatWs.js';
|
||||||
|
import { confirmAction } from '@/utils/feedback.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MultiChat',
|
name: 'MultiChat',
|
||||||
@@ -561,7 +562,10 @@ export default {
|
|||||||
},
|
},
|
||||||
async deleteOwnedRoom(room) {
|
async deleteOwnedRoom(room) {
|
||||||
const title = room?.title || '';
|
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 (!confirmed) return;
|
||||||
if (!this.transportConnected) {
|
if (!this.transportConnected) {
|
||||||
this.messages.push({
|
this.messages.push({
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import DialogWidget from '@/components/DialogWidget.vue';
|
|||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import apiClient from '@/utils/axios.js';
|
import apiClient from '@/utils/axios.js';
|
||||||
import { EventBus } from '@/utils/eventBus.js';
|
import { EventBus } from '@/utils/eventBus.js';
|
||||||
|
import { showError } from '@/utils/feedback.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CreateFolderDialog',
|
name: 'CreateFolderDialog',
|
||||||
@@ -93,7 +94,7 @@ export default {
|
|||||||
},
|
},
|
||||||
async createFolder() {
|
async createFolder() {
|
||||||
if (!this.folderTitle || !this.selectedVisibility.length) {
|
if (!this.folderTitle || !this.selectedVisibility.length) {
|
||||||
alert(this.$t('socialnetwork.gallery.errors.missing_fields'));
|
showError(this, this.$t('socialnetwork.gallery.errors.missing_fields'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const payload = {
|
const payload = {
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ import FolderItem from '../../components/FolderItem.vue';
|
|||||||
import { Editor, EditorContent } from '@tiptap/vue-3'
|
import { Editor, EditorContent } from '@tiptap/vue-3'
|
||||||
import StarterKit from '@tiptap/starter-kit'
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
|
import { showError } from '@/utils/feedback.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'UserProfileDialog',
|
name: 'UserProfileDialog',
|
||||||
@@ -265,7 +266,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async submitGuestbookEntry() {
|
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();
|
const formData = new FormData();
|
||||||
formData.append('htmlContent', this.newEntryContent);
|
formData.append('htmlContent', this.newEntryContent);
|
||||||
formData.append('recipientName', this.userProfile.username);
|
formData.append('recipientName', this.userProfile.username);
|
||||||
|
|||||||
@@ -42,12 +42,10 @@ export default {
|
|||||||
this.$refs.dialog.close();
|
this.$refs.dialog.close();
|
||||||
},
|
},
|
||||||
confirmYes() {
|
confirmYes() {
|
||||||
console.log('ja');
|
|
||||||
this.resolve(true);
|
this.resolve(true);
|
||||||
this.close();
|
this.close();
|
||||||
},
|
},
|
||||||
confirmNo() {
|
confirmNo() {
|
||||||
console.log('nein');
|
|
||||||
this.resolve(false);
|
this.resolve(false);
|
||||||
this.close();
|
this.close();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -52,6 +52,11 @@ export function showApiError(context, error, fallback = 'tr:error.network') {
|
|||||||
showError(context, fallback, fallback);
|
showError(context, fallback, fallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function confirmAction(context, options = {}) {
|
||||||
|
const refs = getRootRefs(context);
|
||||||
|
return refs.chooseDialog?.open?.(options) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
install(app) {
|
install(app) {
|
||||||
const getAppContext = () => app._instance?.proxy;
|
const getAppContext = () => app._instance?.proxy;
|
||||||
@@ -61,7 +66,8 @@ export default {
|
|||||||
showSuccess: (...args) => showSuccess(getAppContext(), ...args),
|
showSuccess: (...args) => showSuccess(getAppContext(), ...args),
|
||||||
showInfo: (...args) => showInfo(getAppContext(), ...args),
|
showInfo: (...args) => showInfo(getAppContext(), ...args),
|
||||||
showError: (...args) => showError(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>
|
<template>
|
||||||
<div class="contenthidden">
|
<div class="admin-minigames-view">
|
||||||
<div class="contentscroll">
|
|
||||||
<div class="admin-header">
|
<div class="admin-header">
|
||||||
<h1>{{ $t('admin.match3.title') }}</h1>
|
<h1>{{ $t('admin.match3.title') }}</h1>
|
||||||
<p>Verwalte Minigames, Level und Konfigurationen</p>
|
<p>Verwalte Minigames, Level und Konfigurationen</p>
|
||||||
@@ -14,7 +13,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<span class="workflow-hero__eyebrow">Arbeitsfluss</span>
|
<span class="workflow-hero__eyebrow">Arbeitsfluss</span>
|
||||||
<h2>{{ $t('admin.match3.title') }}</h2>
|
<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>
|
||||||
<div class="workflow-hero__meta">
|
<div class="workflow-hero__meta">
|
||||||
<span class="workflow-pill">{{ currentModeLabel }}</span>
|
<span class="workflow-pill">{{ currentModeLabel }}</span>
|
||||||
@@ -26,12 +25,12 @@
|
|||||||
<article class="workflow-card surface-card">
|
<article class="workflow-card surface-card">
|
||||||
<span class="workflow-card__step">1</span>
|
<span class="workflow-card__step">1</span>
|
||||||
<h3>Level waehlen</h3>
|
<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>
|
||||||
<article class="workflow-card surface-card">
|
<article class="workflow-card surface-card">
|
||||||
<span class="workflow-card__step">2</span>
|
<span class="workflow-card__step">2</span>
|
||||||
<h3>Spielfeld bauen</h3>
|
<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>
|
||||||
<article class="workflow-card surface-card">
|
<article class="workflow-card surface-card">
|
||||||
<span class="workflow-card__step">3</span>
|
<span class="workflow-card__step">3</span>
|
||||||
@@ -79,7 +78,7 @@
|
|||||||
<article class="admin-summary-card surface-card">
|
<article class="admin-summary-card surface-card">
|
||||||
<span class="admin-summary-card__label">Spielfeld</span>
|
<span class="admin-summary-card__label">Spielfeld</span>
|
||||||
<strong>{{ levelForm.boardWidth }} x {{ levelForm.boardHeight }}</strong>
|
<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>
|
||||||
<article class="admin-summary-card surface-card">
|
<article class="admin-summary-card surface-card">
|
||||||
<span class="admin-summary-card__label">Objectives</span>
|
<span class="admin-summary-card__label">Objectives</span>
|
||||||
@@ -601,14 +600,13 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import SimpleTabs from '../../components/SimpleTabs.vue';
|
import SimpleTabs from '../../components/SimpleTabs.vue';
|
||||||
import apiClient from '../../utils/axios.js';
|
import apiClient from '../../utils/axios.js';
|
||||||
import { showError, showSuccess } from '@/utils/feedback.js';
|
import { confirmAction, showError, showSuccess } from '@/utils/feedback.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AdminMinigamesView',
|
name: 'AdminMinigamesView',
|
||||||
@@ -1016,15 +1014,20 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async deleteLevel(levelId) {
|
async deleteLevel(levelId) {
|
||||||
if (confirm('Möchtest du dieses Level wirklich löschen?')) {
|
const confirmed = await confirmAction(this, {
|
||||||
try {
|
title: 'Level löschen',
|
||||||
await apiClient.delete(`/api/admin/minigames/match3/levels/${levelId}`);
|
message: 'Möchtest du dieses Level wirklich löschen?'
|
||||||
this.loadLevels();
|
});
|
||||||
showSuccess(this, 'Level wurde geloescht.');
|
if (!confirmed) {
|
||||||
} catch (error) {
|
return;
|
||||||
console.error('Fehler beim Löschen des Levels:', error);
|
}
|
||||||
showError(this, 'Fehler beim Loeschen des Levels');
|
try {
|
||||||
}
|
await apiClient.delete(`/api/admin/minigames/match3/levels/${levelId}`);
|
||||||
|
this.loadLevels();
|
||||||
|
showSuccess(this, 'Level wurde gelöscht.');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Löschen des Levels:', error);
|
||||||
|
showError(this, 'Fehler beim Löschen des Levels');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1047,14 +1050,20 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
removeObjective(index) {
|
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);
|
this.levelForm.objectives.splice(index, 1);
|
||||||
|
|
||||||
// Aktualisiere die Reihenfolge
|
// Aktualisiere die Reihenfolge
|
||||||
this.levelForm.objectives.forEach((objective, idx) => {
|
this.levelForm.objectives.forEach((objective, idx) => {
|
||||||
objective.order = idx + 1;
|
objective.order = idx + 1;
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async loadObjectivesForLevel(levelId) {
|
async loadObjectivesForLevel(levelId) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="contenthidden">
|
<div class="services-status-view">
|
||||||
<div class="contentscroll">
|
|
||||||
<div class="admin-header">
|
<div class="admin-header">
|
||||||
<h1>{{ $t('admin.servicesStatus.title') }}</h1>
|
<h1>{{ $t('admin.servicesStatus.title') }}</h1>
|
||||||
<p>{{ $t('admin.servicesStatus.description') }}</p>
|
<p>{{ $t('admin.servicesStatus.description') }}</p>
|
||||||
@@ -95,8 +94,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- WebSocket Log Dialog -->
|
<!-- WebSocket Log Dialog -->
|
||||||
<WebSocketLogDialog ref="webSocketLogDialog" />
|
<WebSocketLogDialog ref="webSocketLogDialog" />
|
||||||
</div>
|
</div>
|
||||||
@@ -481,4 +479,3 @@ export default {
|
|||||||
color: #c62828;
|
color: #c62828;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="contenthidden">
|
<div class="admin-taxi-tools-view">
|
||||||
<div class="contentscroll">
|
|
||||||
<div class="admin-header">
|
<div class="admin-header">
|
||||||
<h1>{{ $t('admin.taxiTools.title') }}</h1>
|
<h1>{{ $t('admin.taxiTools.title') }}</h1>
|
||||||
<p>{{ $t('admin.taxiTools.description') }}</p>
|
<p>{{ $t('admin.taxiTools.description') }}</p>
|
||||||
@@ -367,8 +366,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Message Dialog -->
|
<!-- Message Dialog -->
|
||||||
<MessageDialog ref="messageDialog" />
|
<MessageDialog ref="messageDialog" />
|
||||||
</div>
|
</div>
|
||||||
@@ -378,6 +376,7 @@
|
|||||||
import SimpleTabs from '../../components/SimpleTabs.vue';
|
import SimpleTabs from '../../components/SimpleTabs.vue';
|
||||||
import MessageDialog from '../../dialogues/standard/MessageDialog.vue';
|
import MessageDialog from '../../dialogues/standard/MessageDialog.vue';
|
||||||
import apiClient from '../../utils/axios.js';
|
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
|
// Matrix: erlaubte Haus-Tür-Richtungen je Tile-Typ und Ecke
|
||||||
// Richtungen: bottom, right, top, left
|
// Richtungen: bottom, right, top, left
|
||||||
@@ -1216,10 +1215,10 @@ export default {
|
|||||||
|
|
||||||
// Erfolgsmeldung anzeigen
|
// Erfolgsmeldung anzeigen
|
||||||
const message = isUpdate ? 'admin.taxiTools.mapEditor.updateSuccess' : 'admin.taxiTools.mapEditor.createSuccess';
|
const message = isUpdate ? 'admin.taxiTools.mapEditor.updateSuccess' : 'admin.taxiTools.mapEditor.createSuccess';
|
||||||
this.$refs.messageDialog.open(`tr:${message}`);
|
showSuccess(this, `tr:${message}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Speichern der Map:', 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) {
|
async deleteMap(mapId) {
|
||||||
if (confirm('Möchtest du diese Map wirklich löschen?')) {
|
const confirmed = await confirmAction(this, {
|
||||||
try {
|
title: 'Map löschen',
|
||||||
await apiClient.delete(`/api/taxi-maps/maps/${mapId}`);
|
message: 'Möchtest du diese Map wirklich löschen?'
|
||||||
this.loadMaps();
|
});
|
||||||
|
if (!confirmed) {
|
||||||
// Erfolgsmeldung anzeigen
|
return;
|
||||||
this.$refs.messageDialog.open('tr:admin.taxiTools.mapEditor.deleteSuccess');
|
}
|
||||||
} catch (error) {
|
try {
|
||||||
console.error('Fehler beim Löschen der Map:', error);
|
await apiClient.delete(`/api/taxi-maps/maps/${mapId}`);
|
||||||
}
|
this.loadMaps();
|
||||||
|
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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1692,4 +1695,4 @@ export default {
|
|||||||
height: 40px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="contenthidden">
|
<div class="create-npc-page">
|
||||||
<div class="contentscroll">
|
|
||||||
<div class="create-npc-view">
|
<div class="create-npc-view">
|
||||||
<h1>{{ $t('admin.falukant.createNPC.title') }}</h1>
|
<h1>{{ $t('admin.falukant.createNPC.title') }}</h1>
|
||||||
|
|
||||||
@@ -102,7 +101,6 @@
|
|||||||
<div v-if="error" class="error-message">
|
<div v-if="error" class="error-message">
|
||||||
{{ error }}
|
{{ error }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="contenthidden">
|
<div class="falukant-map-admin">
|
||||||
<div class="contentscroll falukant-map-admin">
|
|
||||||
<div class="admin-header">
|
<div class="admin-header">
|
||||||
<h1>{{ $t('admin.falukant.map.title') }}</h1>
|
<h1>{{ $t('admin.falukant.map.title') }}</h1>
|
||||||
<p>{{ $t('admin.falukant.map.description') }}</p>
|
<p>{{ $t('admin.falukant.map.description') }}</p>
|
||||||
@@ -212,13 +211,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import apiClient from '@/utils/axios.js';
|
import apiClient from '@/utils/axios.js';
|
||||||
import SimpleTabs from '@/components/SimpleTabs.vue';
|
import SimpleTabs from '@/components/SimpleTabs.vue';
|
||||||
|
import { confirmAction, showError } from '@/utils/feedback.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AdminFalukantMapRegionsView',
|
name: 'AdminFalukantMapRegionsView',
|
||||||
@@ -469,17 +468,21 @@ export default {
|
|||||||
await this.loadConnections();
|
await this.loadConnections();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving region distance:', 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) {
|
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 {
|
try {
|
||||||
await apiClient.delete(`/api/admin/falukant/region-distances/${id}`);
|
await apiClient.delete(`/api/admin/falukant/region-distances/${id}`);
|
||||||
await this.loadConnections();
|
await this.loadConnections();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting region distance:', 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) {
|
regionName(id) {
|
||||||
@@ -635,5 +638,3 @@ export default {
|
|||||||
.btn.mini.icon {
|
.btn.mini.icon {
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { createBlog, updateBlog, getBlog, createPost, shareBlog } from '@/api/blogApi.js';
|
import { createBlog, updateBlog, getBlog, createPost, shareBlog } from '@/api/blogApi.js';
|
||||||
import RichTextEditor from './components/RichTextEditor.vue';
|
import RichTextEditor from './components/RichTextEditor.vue';
|
||||||
|
import { showError } from '@/utils/feedback.js';
|
||||||
export default {
|
export default {
|
||||||
name: 'BlogEditorView',
|
name: 'BlogEditorView',
|
||||||
components: { RichTextEditor },
|
components: { RichTextEditor },
|
||||||
@@ -107,7 +108,7 @@ export default {
|
|||||||
async save() {
|
async save() {
|
||||||
if (this.form.visibility === 'logged_in') {
|
if (this.form.visibility === 'logged_in') {
|
||||||
if (this.form.ageMin != null && this.form.ageMax != null && this.form.ageMin > this.form.ageMax) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="contenthidden">
|
<div class="bank-view">
|
||||||
<StatusBar />
|
<StatusBar />
|
||||||
<div class="contentscroll">
|
<div class="bank-content">
|
||||||
|
|
||||||
<h2>{{ $t('falukant.bank.title') }}</h2>
|
<h2>{{ $t('falukant.bank.title') }}</h2>
|
||||||
<SimpleTabs v-model="activeTab" :tabs="tabs" />
|
<SimpleTabs v-model="activeTab" :tabs="tabs" />
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="contenthidden">
|
<div class="falukant-branch-view">
|
||||||
<StatusBar ref="statusBar" />
|
<StatusBar ref="statusBar" />
|
||||||
<div class="contentscroll">
|
<div class="falukant-branch">
|
||||||
<div class="falukant-branch">
|
|
||||||
<section class="branch-hero surface-card">
|
<section class="branch-hero surface-card">
|
||||||
<div>
|
<div>
|
||||||
<span class="branch-kicker">Niederlassung</span>
|
<span class="branch-kicker">Niederlassung</span>
|
||||||
@@ -315,7 +314,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -332,6 +330,7 @@ import RevenueSection from '@/components/falukant/RevenueSection.vue';
|
|||||||
import BuyVehicleDialog from '@/dialogues/falukant/BuyVehicleDialog.vue';
|
import BuyVehicleDialog from '@/dialogues/falukant/BuyVehicleDialog.vue';
|
||||||
import apiClient from '@/utils/axios.js';
|
import apiClient from '@/utils/axios.js';
|
||||||
import { mapState } from 'vuex';
|
import { mapState } from 'vuex';
|
||||||
|
import { showError, showSuccess, showApiError } from '@/utils/feedback.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "BranchView",
|
name: "BranchView",
|
||||||
@@ -657,7 +656,7 @@ export default {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error upgrading branch:', 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() {
|
async sendVehicles() {
|
||||||
if (!this.sendVehicleDialog.targetBranchId) {
|
if (!this.sendVehicleDialog.targetBranchId) {
|
||||||
alert(this.$t('falukant.branch.transport.selectTargetError'));
|
showError(this, this.$t('falukant.branch.transport.selectTargetError'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -975,7 +974,7 @@ export default {
|
|||||||
} else if (this.sendVehicleDialog.vehicleTypeId) {
|
} else if (this.sendVehicleDialog.vehicleTypeId) {
|
||||||
payload.vehicleTypeId = this.sendVehicleDialog.vehicleTypeId;
|
payload.vehicleTypeId = this.sendVehicleDialog.vehicleTypeId;
|
||||||
} else {
|
} else {
|
||||||
alert(this.$t('falukant.branch.transport.noVehiclesSelected'));
|
showError(this, this.$t('falukant.branch.transport.noVehiclesSelected'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -984,7 +983,7 @@ export default {
|
|||||||
this.sendVehicleDialog.success = true;
|
this.sendVehicleDialog.success = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error sending vehicles:', 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();
|
await this.loadVehicles();
|
||||||
this.closeRepairAllVehiclesDialog();
|
this.closeRepairAllVehiclesDialog();
|
||||||
alert(this.$t('falukant.branch.transport.repairAllSuccess'));
|
showSuccess(this, this.$t('falukant.branch.transport.repairAllSuccess'));
|
||||||
this.$refs.statusBar?.fetchStatus();
|
this.$refs.statusBar?.fetchStatus();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error repairing all vehicles:', error);
|
console.error('Error repairing all vehicles:', error);
|
||||||
const errorMessage = error.response?.data?.message || this.$t('falukant.branch.transport.repairAllError');
|
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 apiClient.post(`/api/falukant/vehicles/${this.repairVehicleDialog.vehicle.id}/repair`);
|
||||||
await this.loadVehicles();
|
await this.loadVehicles();
|
||||||
this.closeRepairVehicleDialog();
|
this.closeRepairVehicleDialog();
|
||||||
alert(this.$t('falukant.branch.transport.repairSuccess'));
|
showSuccess(this, this.$t('falukant.branch.transport.repairSuccess'));
|
||||||
this.$refs.statusBar?.fetchStatus();
|
this.$refs.statusBar?.fetchStatus();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error repairing vehicle:', error);
|
console.error('Error repairing vehicle:', error);
|
||||||
const errorMessage = error.response?.data?.message || this.$t('falukant.branch.transport.repairError');
|
const errorMessage = error.response?.data?.message || this.$t('falukant.branch.transport.repairError');
|
||||||
alert(errorMessage);
|
showError(this, errorMessage);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="contenthidden">
|
<div class="church-view">
|
||||||
<StatusBar />
|
<StatusBar />
|
||||||
<div class="contentscroll">
|
<div class="church-content">
|
||||||
<h2>{{ $t('falukant.church.title') }}</h2>
|
<h2>{{ $t('falukant.church.title') }}</h2>
|
||||||
<SimpleTabs v-model="activeTab" :tabs="tabs" />
|
<SimpleTabs v-model="activeTab" :tabs="tabs" />
|
||||||
|
|
||||||
@@ -450,4 +450,4 @@ th {
|
|||||||
.reject-button:hover {
|
.reject-button:hover {
|
||||||
background-color: #c82333;
|
background-color: #c82333;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="contenthidden">
|
<div class="education-view">
|
||||||
<StatusBar />
|
<StatusBar />
|
||||||
<div class="contentscroll">
|
<div class="education-content">
|
||||||
<h2>{{ $t('falukant.education.title') }}</h2>
|
<h2>{{ $t('falukant.education.title') }}</h2>
|
||||||
<SimpleTabs v-model="activeTab" :tabs="tabs" />
|
<SimpleTabs v-model="activeTab" :tabs="tabs" />
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="contenthidden">
|
<div class="family-view">
|
||||||
<StatusBar />
|
<StatusBar />
|
||||||
<div class="contentscroll family-layout">
|
<div class="family-layout">
|
||||||
<div class="family-content">
|
<div class="family-content">
|
||||||
<section class="family-hero surface-card">
|
<section class="family-hero surface-card">
|
||||||
<div>
|
<div>
|
||||||
@@ -189,8 +189,7 @@
|
|||||||
<p>{{ $t('falukant.family.lovers.none') }}</p>
|
<p>{{ $t('falukant.family.lovers.none') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<ChildDetailsDialog ref="childDetailsDialog" />
|
<ChildDetailsDialog ref="childDetailsDialog" />
|
||||||
</div>
|
</div>
|
||||||
@@ -204,6 +203,7 @@ import ChildDetailsDialog from '@/dialogues/falukant/ChildDetailsDialog.vue'
|
|||||||
import Character3D from '@/components/Character3D.vue'
|
import Character3D from '@/components/Character3D.vue'
|
||||||
|
|
||||||
import apiClient from '@/utils/axios.js'
|
import apiClient from '@/utils/axios.js'
|
||||||
|
import { confirmAction } from '@/utils/feedback.js'
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
const WOOING_PROGRESS_TARGET = 70
|
const WOOING_PROGRESS_TARGET = 70
|
||||||
@@ -350,7 +350,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async cancelWooing() {
|
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;
|
if (!confirmed) return;
|
||||||
try {
|
try {
|
||||||
await apiClient.post('/api/falukant/family/cancel-wooing');
|
await apiClient.post('/api/falukant/family/cancel-wooing');
|
||||||
|
|||||||
@@ -4,40 +4,33 @@
|
|||||||
<h2>{{ $t('falukant.house.title') }}</h2>
|
<h2>{{ $t('falukant.house.title') }}</h2>
|
||||||
<div class="existing-house">
|
<div class="existing-house">
|
||||||
<div :style="houseType ? houseStyle(houseType.position, 341) : {}" class="house"></div>
|
<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>
|
<h3>{{ $t('falukant.house.statusreport') }}</h3>
|
||||||
<table>
|
<div class="status-cards">
|
||||||
<thead>
|
<article v-for="(value, key) in status" :key="key" class="status-card">
|
||||||
<tr>
|
<div>
|
||||||
<th>{{ $t('falukant.house.element') }}</th>
|
<span class="status-card__label">{{ $t(`falukant.house.status.${key}`) }}</span>
|
||||||
<th>{{ $t('falukant.house.state') }}</th>
|
<strong>{{ conditionLabel(value) }}</strong>
|
||||||
<th></th>
|
</div>
|
||||||
</tr>
|
<button v-if="value < 100" @click="renovate(key)">
|
||||||
</thead>
|
{{ $t('falukant.house.renovate') }} ({{ getRenovationCost(key, value) }})
|
||||||
<tbody>
|
</button>
|
||||||
<tr v-for="(value, key) in status" :key="key">
|
</article>
|
||||||
<td>{{ $t(`falukant.house.status.${key}`) }}</td>
|
<article class="status-card status-card--summary">
|
||||||
<td>{{ conditionLabel(value) }}</td>
|
<div>
|
||||||
<td>
|
<span class="status-card__label">{{ $t('falukant.house.worth') }}</span>
|
||||||
<button v-if="value < 100" @click="renovate(key)">
|
<strong>{{ getWorth() }} {{ currency }}</strong>
|
||||||
{{ $t('falukant.house.renovate') }} ({{ getRenovationCost(key, value) }})
|
</div>
|
||||||
</button>
|
<div class="status-card__actions">
|
||||||
</td>
|
<button @click="renovateAll" :disabled="allRenovated">
|
||||||
</tr>
|
{{ $t('falukant.house.renovateAll') }} ({{ getAllRenovationCost() }})
|
||||||
<tr>
|
</button>
|
||||||
<td>{{ $t('falukant.house.worth') }}</td>
|
<button class="button-secondary" @click="sellHouse">
|
||||||
<td>{{ getWorth() }} {{ currency }}</td>
|
{{ $t('falukant.house.sell') }}
|
||||||
<td>
|
</button>
|
||||||
<button @click="renovateAll" :disabled="allRenovated">
|
</div>
|
||||||
{{ $t('falukant.house.renovateAll') }} ({{ getAllRenovationCost() }})
|
</article>
|
||||||
</button>
|
</div>
|
||||||
<button @click="sellHouse">
|
|
||||||
{{ $t('falukant.house.sell') }}
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -49,16 +42,15 @@
|
|||||||
class="house-preview"></div>
|
class="house-preview"></div>
|
||||||
<div class="house-info">
|
<div class="house-info">
|
||||||
<h4>{{ $t(`falukant.house.type.${house.houseType.labelTr}`) }}</h4>
|
<h4>{{ $t(`falukant.house.type.${house.houseType.labelTr}`) }}</h4>
|
||||||
<table>
|
<div class="buyable-house-stats">
|
||||||
<tbody>
|
<div v-for="(val, prop) in house" :key="prop"
|
||||||
<tr v-for="(val, prop) in house" :key="prop"
|
v-if="['roofCondition', 'wallCondition', 'floorCondition', 'windowCondition'].includes(prop)"
|
||||||
v-if="['roofCondition', 'wallCondition', 'floorCondition', 'windowCondition'].includes(prop)">
|
class="buyable-house-stat">
|
||||||
<td>{{ $t(`falukant.house.status.${prop}`) }}</td>
|
<span>{{ $t(`falukant.house.status.${prop}`) }}</span>
|
||||||
<td>{{ conditionLabel(val) }}</td>
|
<strong>{{ conditionLabel(val) }}</strong>
|
||||||
</tr>
|
</div>
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
<div class="buyable-house-price">
|
||||||
<div>
|
|
||||||
{{ $t('falukant.house.price') }}: {{ buyCost(house) }}
|
{{ $t('falukant.house.price') }}: {{ buyCost(house) }}
|
||||||
</div>
|
</div>
|
||||||
<button @click="buyHouse(house.id)">
|
<button @click="buyHouse(house.id)">
|
||||||
@@ -263,6 +255,7 @@ h2 {
|
|||||||
|
|
||||||
.status-panel {
|
.status-panel {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
padding: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.buyable-houses {
|
.buyable-houses {
|
||||||
@@ -284,8 +277,12 @@ h2 {
|
|||||||
.house-item {
|
.house-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 5px;
|
gap: 12px;
|
||||||
align-items: center;
|
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 {
|
.house-preview {
|
||||||
@@ -301,20 +298,73 @@ h2 {
|
|||||||
/* center sprite */
|
/* center sprite */
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
.house-info {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
table th,
|
.status-cards,
|
||||||
table td {
|
.buyable-house-stats {
|
||||||
border: 1px solid #ddd;
|
display: grid;
|
||||||
padding: 8px;
|
gap: 12px;
|
||||||
text-align: left;
|
}
|
||||||
|
|
||||||
|
.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 {
|
button {
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
cursor: pointer;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="contenthidden">
|
<div class="nobility-view">
|
||||||
<StatusBar />
|
<StatusBar />
|
||||||
<div class="contentscroll">
|
<div class="nobility-content">
|
||||||
<h2>{{ $t('falukant.nobility.title') }}</h2>
|
<h2>{{ $t('falukant.nobility.title') }}</h2>
|
||||||
<SimpleTabs v-model="activeTab" :tabs="tabs" />
|
<SimpleTabs v-model="activeTab" :tabs="tabs" />
|
||||||
|
|
||||||
@@ -200,4 +200,4 @@
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<span class="falukant-kicker">Falukant</span>
|
<span class="falukant-kicker">Falukant</span>
|
||||||
<h2>{{ $t('falukant.overview.title') }}</h2>
|
<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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
<article class="summary-card surface-card">
|
<article class="summary-card surface-card">
|
||||||
<span class="summary-card__label">Niederlassungen</span>
|
<span class="summary-card__label">Niederlassungen</span>
|
||||||
<strong>{{ branchCount }}</strong>
|
<strong>{{ branchCount }}</strong>
|
||||||
<p>Direkter Zugriff auf deine wichtigsten Geschaeftsstandorte.</p>
|
<p>Direkter Zugriff auf deine wichtigsten Geschäftsstandorte.</p>
|
||||||
</article>
|
</article>
|
||||||
<article class="summary-card surface-card">
|
<article class="summary-card surface-card">
|
||||||
<span class="summary-card__label">Produktionen aktiv</span>
|
<span class="summary-card__label">Produktionen aktiv</span>
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
<article class="summary-card surface-card">
|
<article class="summary-card surface-card">
|
||||||
<span class="summary-card__label">Lagerpositionen</span>
|
<span class="summary-card__label">Lagerpositionen</span>
|
||||||
<strong>{{ stockEntryCount }}</strong>
|
<strong>{{ stockEntryCount }}</strong>
|
||||||
<p>Verdichteter Blick auf Warenbestand ueber alle Regionen.</p>
|
<p>Verdichteter Blick auf Warenbestand über alle Regionen.</p>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -82,92 +82,76 @@
|
|||||||
|
|
||||||
<!-- Normale Übersicht wenn Charakter vorhanden -->
|
<!-- Normale Übersicht wenn Charakter vorhanden -->
|
||||||
<div v-if="falukantUser?.character" class="overviewcontainer">
|
<div v-if="falukantUser?.character" class="overviewcontainer">
|
||||||
<div>
|
<section class="overview-panel surface-card">
|
||||||
<h3>{{ $t('falukant.overview.metadata.title') }}</h3>
|
<h3>{{ $t('falukant.overview.metadata.title') }}</h3>
|
||||||
<table>
|
<div class="detail-list">
|
||||||
<tr>
|
<div class="detail-list__item">
|
||||||
<td>{{ $t('falukant.overview.metadata.name') }}</td>
|
<span>{{ $t('falukant.overview.metadata.name') }}</span>
|
||||||
<td>{{ falukantUser?.character?.definedFirstName?.name }} {{
|
<strong>{{ falukantUser?.character?.definedFirstName?.name }} {{ falukantUser?.character?.definedLastName?.name }}</strong>
|
||||||
falukantUser?.character?.definedLastName?.name }}</td>
|
</div>
|
||||||
</tr>
|
<div class="detail-list__item">
|
||||||
<tr>
|
<span>{{ $t('falukant.overview.metadata.nobleTitle') }}</span>
|
||||||
<td>{{ $t('falukant.overview.metadata.nobleTitle') }}</td>
|
<strong>{{ $t('falukant.titles.' + falukantUser?.character?.gender + '.' + falukantUser?.character?.nobleTitle?.labelTr) }}</strong>
|
||||||
<td>{{ $t('falukant.titles.' + falukantUser?.character?.gender + '.' +
|
</div>
|
||||||
falukantUser?.character?.nobleTitle?.labelTr) }}</td>
|
<div class="detail-list__item">
|
||||||
</tr>
|
<span>{{ $t('falukant.overview.metadata.money') }}</span>
|
||||||
<tr>
|
<strong>
|
||||||
<td>{{ $t('falukant.overview.metadata.money') }}</td>
|
|
||||||
<td>
|
|
||||||
{{ moneyValue != null
|
{{ moneyValue != null
|
||||||
? moneyValue.toLocaleString(locale, { style: 'currency', currency: 'EUR' })
|
? moneyValue.toLocaleString(locale, { style: 'currency', currency: 'EUR' })
|
||||||
: '---' }}
|
: '---' }}
|
||||||
</td>
|
</strong>
|
||||||
</tr>
|
</div>
|
||||||
<tr>
|
<div class="detail-list__item">
|
||||||
<td>{{ $t('falukant.overview.metadata.age') }}</td>
|
<span>{{ $t('falukant.overview.metadata.age') }}</span>
|
||||||
<td>{{ falukantUser?.character?.age }}</td>
|
<strong>{{ falukantUser?.character?.age }}</strong>
|
||||||
</tr>
|
</div>
|
||||||
<tr>
|
<div class="detail-list__item">
|
||||||
<td>{{ $t('falukant.overview.metadata.mainbranch') }}</td>
|
<span>{{ $t('falukant.overview.metadata.mainbranch') }}</span>
|
||||||
<td>{{ falukantUser?.mainBranchRegion?.name }}</td>
|
<strong>{{ falukantUser?.mainBranchRegion?.name }}</strong>
|
||||||
</tr>
|
</div>
|
||||||
</table>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
<div>
|
<section class="overview-panel surface-card">
|
||||||
<h3>{{ $t('falukant.overview.productions.title') }}</h3>
|
<h3>{{ $t('falukant.overview.productions.title') }}</h3>
|
||||||
<table v-if="productions.length > 0">
|
<div v-if="productions.length > 0" class="overview-card-list">
|
||||||
<thead>
|
<article v-for="(production, index) in productions" :key="index" class="overview-entry-card">
|
||||||
<tr>
|
<strong>{{ $t(`falukant.product.${production.productName}`) }}</strong>
|
||||||
<th>{{ $t('falukant.branch.sale.region') }}</th>
|
<div class="overview-entry-card__meta">
|
||||||
<th>{{ $t('falukant.branch.production.product') }}</th>
|
<span>{{ $t('falukant.branch.sale.region') }}: {{ production.cityName }}</span>
|
||||||
<th>{{ $t('falukant.branch.production.quantity') }}</th>
|
<span>{{ $t('falukant.branch.production.quantity') }}: {{ production.quantity }}</span>
|
||||||
<th>{{ $t('falukant.branch.production.ending') }}</th>
|
<span>{{ $t('falukant.branch.production.ending') }}: {{ formatDate(production.endTimestamp) }}</span>
|
||||||
</tr>
|
</div>
|
||||||
</thead>
|
</article>
|
||||||
<tbody>
|
</div>
|
||||||
<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>
|
|
||||||
<p v-else>{{ $t('falukant.branch.production.noProductions') }}</p>
|
<p v-else>{{ $t('falukant.branch.production.noProductions') }}</p>
|
||||||
</div>
|
</section>
|
||||||
<div>
|
<section class="overview-panel surface-card">
|
||||||
<h3>{{ $t('falukant.overview.stock.title') }}</h3>
|
<h3>{{ $t('falukant.overview.stock.title') }}</h3>
|
||||||
<table v-if="allStock.length > 0">
|
<div v-if="allStock.length > 0" class="overview-card-list">
|
||||||
<thead>
|
<article v-for="(item, index) in allStock" :key="index" class="overview-entry-card">
|
||||||
<tr>
|
<strong>{{ $t(`falukant.product.${item.productLabelTr}`) }}</strong>
|
||||||
<th>{{ $t('falukant.branch.sale.region') }}</th>
|
<div class="overview-entry-card__meta">
|
||||||
<th>{{ $t('falukant.branch.sale.product') }}</th>
|
<span>{{ $t('falukant.branch.sale.region') }}: {{ item.regionName }}</span>
|
||||||
<th>{{ $t('falukant.branch.sale.quantity') }}</th>
|
<span>{{ $t('falukant.branch.sale.quantity') }}: {{ item.quantity }}</span>
|
||||||
</tr>
|
</div>
|
||||||
</thead>
|
</article>
|
||||||
<tbody>
|
</div>
|
||||||
<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>
|
|
||||||
<p v-else>{{ $t('falukant.branch.sale.noInventory') }}</p>
|
<p v-else>{{ $t('falukant.branch.sale.noInventory') }}</p>
|
||||||
</div>
|
</section>
|
||||||
<div>
|
<section class="overview-panel surface-card">
|
||||||
<h3>{{ $t('falukant.overview.branches.title') }}</h3>
|
<h3>{{ $t('falukant.overview.branches.title') }}</h3>
|
||||||
<table>
|
<div class="overview-card-list">
|
||||||
<tr v-for="branch in falukantUser?.branches" :key="branch.id">
|
<article v-for="branch in falukantUser?.branches" :key="branch.id" class="overview-entry-card overview-entry-card--action">
|
||||||
<td>
|
<div>
|
||||||
<span @click="openBranch(branch.id)" class="link">{{ branch.region.name }}</span>
|
<strong>{{ branch.region.name }}</strong>
|
||||||
</td>
|
<div class="overview-entry-card__meta">
|
||||||
<td>
|
<span>{{ $t(`falukant.overview.branches.level.${branch.branchType.labelTr}`) }}</span>
|
||||||
{{ $t(`falukant.overview.branches.level.${branch.branchType.labelTr}`) }}
|
</div>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
<button type="button" class="button-secondary" @click="openBranch(branch.id)">Öffnen</button>
|
||||||
</table>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -287,7 +271,7 @@ export default {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
kicker: 'Routine',
|
kicker: 'Routine',
|
||||||
title: 'Niederlassung oeffnen',
|
title: 'Niederlassung öffnen',
|
||||||
description: 'Die schnellste Route zu Produktion, Lager, Verkauf und Transport.',
|
description: 'Die schnellste Route zu Produktion, Lager, Verkauf und Transport.',
|
||||||
cta: 'Zu den Betrieben',
|
cta: 'Zu den Betrieben',
|
||||||
route: 'BranchView',
|
route: 'BranchView',
|
||||||
@@ -303,8 +287,8 @@ export default {
|
|||||||
{
|
{
|
||||||
kicker: 'Charakter',
|
kicker: 'Charakter',
|
||||||
title: 'Familie und Nachfolge',
|
title: 'Familie und Nachfolge',
|
||||||
description: 'Wichtige persoenliche Entscheidungen und Haushaltsstatus gesammelt.',
|
description: 'Wichtige persönliche Entscheidungen und Haushaltsstatus gesammelt.',
|
||||||
cta: 'Familie oeffnen',
|
cta: 'Familie öffnen',
|
||||||
route: 'FalukantFamily',
|
route: 'FalukantFamily',
|
||||||
secondary: true,
|
secondary: true,
|
||||||
},
|
},
|
||||||
@@ -499,7 +483,7 @@ export default {
|
|||||||
await this.fetchAllStock();
|
await this.fetchAllStock();
|
||||||
await this.fetchProductions();
|
await this.fetchProductions();
|
||||||
}
|
}
|
||||||
showSuccess(this, 'Erbe wurde uebernommen.');
|
showSuccess(this, 'Erbe wurde übernommen.');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error selecting heir:', error);
|
console.error('Error selecting heir:', error);
|
||||||
showError(this, this.$t('falukant.overview.heirSelection.error'));
|
showError(this, this.$t('falukant.overview.heirSelection.error'));
|
||||||
@@ -597,12 +581,54 @@ export default {
|
|||||||
gap: 12px;
|
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);
|
border: 1px solid var(--color-border);
|
||||||
padding: 16px;
|
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
background: rgba(255, 253, 249, 0.82);
|
background: rgba(255, 255, 255, 0.66);
|
||||||
box-shadow: var(--shadow-soft);
|
}
|
||||||
|
|
||||||
|
.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 {
|
.imagecontainer {
|
||||||
|
|||||||
@@ -2,7 +2,13 @@
|
|||||||
<div class="politics-view">
|
<div class="politics-view">
|
||||||
<StatusBar />
|
<StatusBar />
|
||||||
|
|
||||||
<h2>{{ $t('falukant.politics.title') }}</h2>
|
<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" />
|
<SimpleTabs v-model="activeTab" :tabs="tabs" @change="onTabChange" />
|
||||||
|
|
||||||
@@ -11,86 +17,65 @@
|
|||||||
<!-- Aktuelle Positionen -->
|
<!-- Aktuelle Positionen -->
|
||||||
<div v-if="activeTab === 'current'" class="tab-pane">
|
<div v-if="activeTab === 'current'" class="tab-pane">
|
||||||
<div v-if="loading.current" class="loading">{{ $t('loading') }}</div>
|
<div v-if="loading.current" class="loading">{{ $t('loading') }}</div>
|
||||||
<div v-else class="table-scroll">
|
<div v-else-if="currentPositions.length" class="politics-card-list">
|
||||||
<table class="politics-table">
|
<article v-for="pos in currentPositions" :key="pos.id" class="politics-card" :class="{ 'own-position': isOwnPosition(pos) }">
|
||||||
<thead>
|
<div class="politics-card__header">
|
||||||
<tr>
|
<strong>{{ $t(`falukant.politics.offices.${pos.officeType.name}`) }}</strong>
|
||||||
<th>{{ $t('falukant.politics.current.office') }}</th>
|
<span>{{ pos.region.name }}</span>
|
||||||
<th>{{ $t('falukant.politics.current.region') }}</th>
|
</div>
|
||||||
<th>{{ $t('falukant.politics.current.holder') }}</th>
|
<div class="politics-card__meta">
|
||||||
<th>{{ $t('falukant.politics.current.benefit') }}</th>
|
<span>
|
||||||
<th>{{ $t('falukant.politics.current.termEnds') }}</th>
|
{{ $t('falukant.politics.current.holder') }}:
|
||||||
</tr>
|
<template v-if="pos.character">
|
||||||
</thead>
|
{{ pos.character.definedFirstName.name }} {{ pos.character.definedLastName.name }}
|
||||||
<tbody>
|
</template>
|
||||||
<tr v-for="pos in currentPositions" :key="pos.id" :class="{ 'own-position': isOwnPosition(pos) }">
|
<template v-else>—</template>
|
||||||
<td>{{ $t(`falukant.politics.offices.${pos.officeType.name}`) }}</td>
|
</span>
|
||||||
<td>{{ pos.region.name }}</td>
|
<span>
|
||||||
<td>
|
{{ $t('falukant.politics.current.benefit') }}:
|
||||||
<span v-if="pos.character">
|
<template v-if="pos.benefit && pos.benefit.length">
|
||||||
{{ pos.character.definedFirstName.name }}
|
<span v-if="pos.benefit.includes('*')">{{ $t('falukant.politics.current.benefit_all') }}</span>
|
||||||
{{ pos.character.definedLastName.name }}
|
<span v-else>{{ pos.benefit.join(', ') }}</span>
|
||||||
</span>
|
</template>
|
||||||
<span v-else>—</span>
|
<template v-else>—</template>
|
||||||
</td>
|
</span>
|
||||||
<td>
|
<span>
|
||||||
<span v-if="pos.benefit && pos.benefit.length">
|
{{ $t('falukant.politics.current.termEnds') }}:
|
||||||
<span v-if="pos.benefit.includes('*')">{{ $t('falukant.politics.current.benefit_all') }}</span>
|
<template v-if="pos.termEnds">{{ formatDate(pos.termEnds) }}</template>
|
||||||
<span v-else>{{ pos.benefit.join(', ') }}</span>
|
<template v-else>—</template>
|
||||||
</span>
|
</span>
|
||||||
<span v-else>—</span>
|
</div>
|
||||||
</td>
|
</article>
|
||||||
<td>
|
|
||||||
<span v-if="pos.termEnds">
|
|
||||||
{{ formatDate(pos.termEnds) }}
|
|
||||||
</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>
|
</div>
|
||||||
|
<p v-else class="loading">{{ $t('falukant.politics.current.none') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- OPEN Tab: hier zeigen wir 'openPolitics' -->
|
<!-- OPEN Tab: hier zeigen wir 'openPolitics' -->
|
||||||
<div v-else-if="activeTab === 'openPolitics'" class="tab-pane">
|
<div v-else-if="activeTab === 'openPolitics'" class="tab-pane">
|
||||||
<p class="politics-age-requirement">{{ $t('falukant.politics.open.ageRequirement') }}</p>
|
<p class="politics-age-requirement">{{ $t('falukant.politics.open.ageRequirement') }}</p>
|
||||||
<div v-if="loading.openPolitics" class="loading">{{ $t('loading') }}</div>
|
<div v-if="loading.openPolitics" class="loading">{{ $t('loading') }}</div>
|
||||||
<div v-else class="table-scroll">
|
<div v-else-if="openPolitics.length" class="politics-card-list">
|
||||||
<table class="politics-table">
|
<article v-for="e in openPolitics" :key="e.id" class="politics-card">
|
||||||
<thead>
|
<div class="politics-card__header">
|
||||||
<tr>
|
<strong>{{ $t(`falukant.politics.offices.${e.officeType.name}`) }}</strong>
|
||||||
<th>{{ $t('falukant.politics.open.office') }}</th>
|
<span>{{ e.region.name }}</span>
|
||||||
<th>{{ $t('falukant.politics.open.region') }}</th>
|
</div>
|
||||||
<th>{{ $t('falukant.politics.open.date') }}</th>
|
<div class="politics-card__meta">
|
||||||
<th>{{ $t('falukant.politics.open.candidacyWithAge') }}</th>
|
<span>{{ $t('falukant.politics.open.date') }}: {{ formatDate(e.date) }}</span>
|
||||||
</tr>
|
</div>
|
||||||
</thead>
|
<label class="politics-card__checkbox" :title="e.canApplyByAge === false ? $t('falukant.politics.open.minAgeHint') : null">
|
||||||
<tbody>
|
<input
|
||||||
<tr v-for="e in openPolitics" :key="e.id">
|
type="checkbox"
|
||||||
<td>{{ $t(`falukant.politics.offices.${e.officeType.name}`) }}</td>
|
:id="`apply-${e.id}`"
|
||||||
<td>{{ e.region.name }}</td>
|
v-model="selectedApplications"
|
||||||
<td>{{ formatDate(e.date) }}</td>
|
:value="e.id"
|
||||||
<!-- Checkbox ganz am Ende -->
|
:disabled="e.alreadyApplied || e.canApplyByAge === false"
|
||||||
<td :title="e.canApplyByAge === false ? $t('falukant.politics.open.minAgeHint') : null">
|
/>
|
||||||
<input
|
<span>Für diese Kandidatur vormerken</span>
|
||||||
type="checkbox"
|
</label>
|
||||||
:id="`apply-${e.id}`"
|
</article>
|
||||||
v-model="selectedApplications"
|
|
||||||
: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>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p v-else class="loading">{{ $t('falukant.politics.open.none') }}</p>
|
||||||
|
|
||||||
<div class="apply-button">
|
<div class="apply-button">
|
||||||
<button :disabled="!selectedApplications.length" @click="submitApplications">
|
<button :disabled="!selectedApplications.length" @click="submitApplications">
|
||||||
@@ -102,65 +87,47 @@
|
|||||||
<!-- Wahlen -->
|
<!-- Wahlen -->
|
||||||
<div v-else-if="activeTab === 'elections'" class="tab-pane">
|
<div v-else-if="activeTab === 'elections'" class="tab-pane">
|
||||||
<div v-if="loading.elections" class="loading">{{ $t('loading') }}</div>
|
<div v-if="loading.elections" class="loading">{{ $t('loading') }}</div>
|
||||||
<div v-else class="table-scroll">
|
<div v-else-if="elections.length" class="politics-card-list">
|
||||||
<table class="politics-table">
|
<article v-for="e in elections" :key="e.id" class="politics-card politics-card--election">
|
||||||
<thead>
|
<div class="politics-card__header">
|
||||||
<tr>
|
<strong>{{ $t(`falukant.politics.offices.${e.officeType.name}`) }}</strong>
|
||||||
<th>{{ $t('falukant.politics.elections.office') }}</th>
|
<span>{{ e.region.name }}</span>
|
||||||
<th>{{ $t('falukant.politics.elections.region') }}</th>
|
</div>
|
||||||
<th>{{ $t('falukant.politics.elections.date') }}</th>
|
<div class="politics-card__meta">
|
||||||
<th>{{ $t('falukant.politics.elections.posts') }}</th>
|
<span>{{ $t('falukant.politics.elections.date') }}: {{ formatDate(e.date) }}</span>
|
||||||
<th>{{ $t('falukant.politics.elections.candidates') }}</th>
|
<span>{{ $t('falukant.politics.elections.posts') }}: {{ e.postsToFill }}</span>
|
||||||
<th>{{ $t('falukant.politics.elections.action') }}</th>
|
</div>
|
||||||
</tr>
|
<div v-if="!e.voted" class="politics-card__vote">
|
||||||
</thead>
|
<Multiselect v-model="selectedCandidates[e.id]" :options="e.candidates" multiple
|
||||||
<tbody>
|
:max="e.postsToFill" :close-on-select="false" :clear-on-select="false"
|
||||||
<tr v-for="e in elections" :key="e.id">
|
track-by="id" label="name" :custom-label="candidateLabel" placeholder="">
|
||||||
<td>{{ $t(`falukant.politics.offices.${e.officeType.name}`) }}</td>
|
<template #option="{ option }">
|
||||||
<td>{{ e.region.name }}</td>
|
{{ $t(`falukant.titles.${option.gender}.${option.title}`) }}
|
||||||
<td>{{ formatDate(e.date) }}</td>
|
{{ option.name }} ({{ option.age }})
|
||||||
<td>{{ e.postsToFill }}</td>
|
</template>
|
||||||
<td v-if="!e.voted">
|
<template #selected="{ option }">
|
||||||
<Multiselect v-model="selectedCandidates[e.id]" :options="e.candidates" multiple
|
{{ $t(`falukant.titles.${option.gender}.${option.title}`) }}
|
||||||
:max="e.postsToFill" :close-on-select="false" :clear-on-select="false"
|
{{ option.name }}
|
||||||
track-by="id" label="name" :custom-label="candidateLabel" placeholder="">
|
</template>
|
||||||
<template #option="{ option }">
|
</Multiselect>
|
||||||
{{ $t(`falukant.titles.${option.gender}.${option.title}`) }}
|
<button
|
||||||
{{ option.name }} ({{ option.age }})
|
:disabled="!selectedCandidates[e.id] || !selectedCandidates[e.id].length"
|
||||||
</template>
|
@click="submitVote(e.id)">
|
||||||
<template #selected="{ option }">
|
{{ $t('falukant.politics.elections.vote') }}
|
||||||
{{ $t(`falukant.titles.${option.gender}.${option.title}`) }}
|
</button>
|
||||||
{{ option.name }}
|
</div>
|
||||||
</template>
|
<ul v-else class="voted-list">
|
||||||
</Multiselect>
|
<li v-for="cid in e.votedFor" :key="cid">
|
||||||
</td>
|
<span v-if="findCandidateById(e, cid)">
|
||||||
<td v-else>
|
{{ formatCandidateTitle(findCandidateById(e, cid)) }}
|
||||||
<ul class="voted-list">
|
{{ findCandidateById(e, cid).name }}
|
||||||
<li v-for="cid in e.votedFor" :key="cid">
|
</span>
|
||||||
<span v-if="findCandidateById(e, cid)">
|
</li>
|
||||||
{{ formatCandidateTitle(findCandidateById(e, cid)) }}
|
<li v-if="!e.votedFor || !e.votedFor.length">—</li>
|
||||||
{{ findCandidateById(e, cid).name }}
|
</ul>
|
||||||
</span>
|
</article>
|
||||||
</li>
|
|
||||||
<li v-if="!e.votedFor || !e.votedFor.length">—</li>
|
|
||||||
</ul>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<button v-if="!e.voted"
|
|
||||||
:disabled="!selectedCandidates[e.id] || !selectedCandidates[e.id].length"
|
|
||||||
@click="submitVote(e.id)">
|
|
||||||
{{ $t('falukant.politics.elections.vote') }}
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr v-if="!elections.length">
|
|
||||||
<td colspan="6">{{ $t('falukant.politics.elections.none') }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p v-else class="loading">{{ $t('falukant.politics.elections.none') }}</p>
|
||||||
|
|
||||||
<div class="all-vote-button" v-if="hasAnyUnvoted">
|
<div class="all-vote-button" v-if="hasAnyUnvoted">
|
||||||
<button :disabled="!hasAnySelection" @click="submitAllVotes">
|
<button :disabled="!hasAnySelection" @click="submitAllVotes">
|
||||||
@@ -177,6 +144,9 @@ import StatusBar from '@/components/falukant/StatusBar.vue';
|
|||||||
import SimpleTabs from '@/components/SimpleTabs.vue';
|
import SimpleTabs from '@/components/SimpleTabs.vue';
|
||||||
import Multiselect from 'vue-multiselect';
|
import Multiselect from 'vue-multiselect';
|
||||||
import apiClient from '@/utils/axios.js';
|
import apiClient from '@/utils/axios.js';
|
||||||
|
import { showApiError, showSuccess } from '@/utils/feedback.js';
|
||||||
|
|
||||||
|
const debugLog = () => {};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'PoliticsView',
|
name: 'PoliticsView',
|
||||||
@@ -232,10 +202,10 @@ export default {
|
|||||||
this.loading.current = true;
|
this.loading.current = true;
|
||||||
try {
|
try {
|
||||||
const { data } = await apiClient.get('/api/falukant/politics/overview');
|
const { data } = await apiClient.get('/api/falukant/politics/overview');
|
||||||
console.log('[PoliticsView] loadCurrentPositions - API response:', data);
|
debugLog('[PoliticsView] loadCurrentPositions - API response:', data);
|
||||||
console.log('[PoliticsView] loadCurrentPositions - ownCharacterId at load time:', this.ownCharacterId);
|
debugLog('[PoliticsView] loadCurrentPositions - ownCharacterId at load time:', this.ownCharacterId);
|
||||||
this.currentPositions = data;
|
this.currentPositions = data;
|
||||||
console.log('[PoliticsView] loadCurrentPositions - Loaded', data.length, 'positions');
|
debugLog('[PoliticsView] loadCurrentPositions - Loaded', data.length, 'positions');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[PoliticsView] Error loading current positions', err);
|
console.error('[PoliticsView] Error loading current positions', err);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -304,8 +274,10 @@ export default {
|
|||||||
{ votes: singlePayload }
|
{ votes: singlePayload }
|
||||||
);
|
);
|
||||||
await this.loadElections();
|
await this.loadElections();
|
||||||
|
showSuccess(this, 'Stimme erfolgreich abgegeben.');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Error submitting vote for election ${electionId}`, 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 }
|
{ votes: payload }
|
||||||
);
|
);
|
||||||
await this.loadElections();
|
await this.loadElections();
|
||||||
|
showSuccess(this, 'Alle Stimmen erfolgreich abgegeben.');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error submitting all votes', err);
|
console.error('Error submitting all votes', err);
|
||||||
|
showApiError(this, err, 'Fehler beim Abgeben der Stimmen');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -339,18 +313,8 @@ export default {
|
|||||||
async loadOwnCharacterId() {
|
async loadOwnCharacterId() {
|
||||||
try {
|
try {
|
||||||
const { data } = await apiClient.get('/api/falukant/info');
|
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) {
|
if (data.character && data.character.id) {
|
||||||
this.ownCharacterId = 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) {
|
} catch (err) {
|
||||||
console.error('[PoliticsView] Error loading own character ID', err);
|
console.error('[PoliticsView] Error loading own character ID', err);
|
||||||
@@ -358,20 +322,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
isOwnPosition(pos) {
|
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) {
|
if (!this.ownCharacterId || !pos.character) {
|
||||||
console.log('[PoliticsView] isOwnPosition - Returning false (missing ownCharacterId or pos.character)');
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const isMatch = pos.character.id === this.ownCharacterId;
|
return pos.character.id === this.ownCharacterId;
|
||||||
console.log('[PoliticsView] isOwnPosition - Result:', isMatch);
|
|
||||||
return isMatch;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async submitApplications() {
|
async submitApplications() {
|
||||||
@@ -388,12 +342,14 @@ export default {
|
|||||||
this.selectedApplications = this.openPolitics
|
this.selectedApplications = this.openPolitics
|
||||||
.filter(e => e.alreadyApplied || appliedIds.includes(e.id))
|
.filter(e => e.alreadyApplied || appliedIds.includes(e.id))
|
||||||
.map(e => e.id);
|
.map(e => e.id);
|
||||||
|
showSuccess(this, 'Kandidatur erfolgreich vorgemerkt.');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error submitting applications', err);
|
console.error('Error submitting applications', err);
|
||||||
const msg = err?.response?.data?.error === 'too_young'
|
if (err?.response?.data?.error === 'too_young') {
|
||||||
? this.$t('falukant.politics.too_young')
|
showApiError(this, err, this.$t('falukant.politics.too_young'));
|
||||||
: (err?.response?.data?.error || err?.message || this.$t('falukant.politics.applyError'));
|
return;
|
||||||
this.$root.$refs?.messageDialog?.open?.(msg, this.$t('falukant.politics.title'));
|
}
|
||||||
|
showApiError(this, err, this.$t('falukant.politics.applyError'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -402,73 +358,99 @@ export default {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.politics-view {
|
.politics-view {
|
||||||
display: flex;
|
max-width: var(--content-max-width);
|
||||||
flex-direction: column;
|
margin: 0 auto;
|
||||||
height: 100%;
|
padding-bottom: 24px;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
margin: 0;
|
||||||
padding: 20px 0 0 0;
|
color: var(--color-text-secondary);
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.simple-tabs {
|
.simple-tabs {
|
||||||
flex: 0 0 auto;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-content {
|
.tab-content {
|
||||||
flex: 1 1 auto;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-pane {
|
.tab-pane {
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.politics-age-requirement {
|
.politics-age-requirement {
|
||||||
flex: 0 0 auto;
|
|
||||||
margin: 0 0 10px 0;
|
margin: 0 0 10px 0;
|
||||||
font-size: 0.95em;
|
font-size: 0.95em;
|
||||||
color: #555;
|
color: var(--color-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-scroll {
|
.politics-card-list {
|
||||||
flex: 1;
|
display: grid;
|
||||||
overflow-y: auto;
|
gap: 12px;
|
||||||
border: 1px solid #ddd;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.politics-table {
|
.politics-card {
|
||||||
border-collapse: collapse;
|
display: flex;
|
||||||
width: auto;
|
flex-direction: column;
|
||||||
/* kein 100% */
|
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 {
|
.politics-card.own-position {
|
||||||
position: sticky;
|
border-color: rgba(120, 195, 138, 0.45);
|
||||||
top: 0;
|
background: rgba(236, 248, 238, 0.92);
|
||||||
background: #FFF;
|
|
||||||
z-index: 1;
|
|
||||||
padding: 8px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
text-align: left;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.politics-table tbody td {
|
.politics-card__header {
|
||||||
padding: 8px;
|
display: flex;
|
||||||
border: 1px solid #ddd;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.politics-table tbody tr.own-position {
|
.politics-card__meta {
|
||||||
background-color: #e0e0e0;
|
display: flex;
|
||||||
font-weight: bold;
|
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 {
|
.loading {
|
||||||
@@ -491,6 +473,13 @@ h2 {
|
|||||||
.all-vote-button button {
|
.all-vote-button button {
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin: 2em;
|
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
@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>
|
<template>
|
||||||
<div class="contenthidden">
|
<div class="taxi-game-view">
|
||||||
<div class="contentscroll">
|
|
||||||
<!-- Spiel-Titel -->
|
<!-- Spiel-Titel -->
|
||||||
<div class="game-title">
|
<div class="game-title">
|
||||||
<h1>{{ $t('minigames.taxi.title') }}</h1>
|
<h1>{{ $t('minigames.taxi.title') }}</h1>
|
||||||
@@ -247,8 +246,6 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -347,6 +347,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import apiClient from '@/utils/axios.js';
|
import apiClient from '@/utils/axios.js';
|
||||||
|
import { showError } from '@/utils/feedback.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CalendarView',
|
name: 'CalendarView',
|
||||||
@@ -784,7 +785,7 @@ export default {
|
|||||||
this.closeEventDialog();
|
this.closeEventDialog();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving event:', 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 {
|
} finally {
|
||||||
this.saving = false;
|
this.saving = false;
|
||||||
}
|
}
|
||||||
@@ -803,7 +804,7 @@ export default {
|
|||||||
this.closeEventDialog();
|
this.closeEventDialog();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting event:', 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 {
|
} finally {
|
||||||
this.saving = false;
|
this.saving = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,13 +160,9 @@ export default {
|
|||||||
},
|
},
|
||||||
setupSocketListener() {
|
setupSocketListener() {
|
||||||
if (this.socket) {
|
if (this.socket) {
|
||||||
console.log("Setting up friendshipChanged listener");
|
this.socket.on("friendshipChanged", () => {
|
||||||
this.socket.on("friendshipChanged", (data) => {
|
|
||||||
console.log("Friendship changed:", data);
|
|
||||||
this.fetchFriendships();
|
this.fetchFriendships();
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
console.error("Socket not initialized");
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -47,22 +47,20 @@
|
|||||||
<h3>{{ $t('socialnetwork.usersearch.results_title') }}</h3>
|
<h3>{{ $t('socialnetwork.usersearch.results_title') }}</h3>
|
||||||
<span class="results-count">{{ searchResults.length }} Treffer</span>
|
<span class="results-count">{{ searchResults.length }} Treffer</span>
|
||||||
</div>
|
</div>
|
||||||
<table>
|
<div class="result-cards">
|
||||||
<thead>
|
<article v-for="result in searchResults" :key="result.id" class="result-card">
|
||||||
<tr>
|
<div class="result-card__main">
|
||||||
<th>{{ $t("socialnetwork.usersearch.result.nick") }}</th>
|
<span @click.prevent="openUserProfile(result.id)" :class="'clickable g-' + result.gender">{{ result.username }}</span>
|
||||||
<th>{{ $t("socialnetwork.usersearch.result.gender") }}</th>
|
<div class="result-card__meta">
|
||||||
<th>{{ $t("socialnetwork.usersearch.result.age") }}</th>
|
<span>{{ $t("socialnetwork.usersearch.result.gender") }}: {{ result.gender }}</span>
|
||||||
</tr>
|
<span>{{ $t("socialnetwork.usersearch.result.age") }}: {{ result.age }}</span>
|
||||||
</thead>
|
</div>
|
||||||
<tbody>
|
</div>
|
||||||
<tr v-for="result in searchResults" :key="result.id">
|
<button type="button" class="button-secondary" @click="openUserProfile(result.id)">
|
||||||
<td><span @click.prevent="openUserProfile(result.id)" :class="'clickable g-' + result.gender">{{ result.username }}</span></td>
|
Profil öffnen
|
||||||
<td>{{ result.gender }}</td>
|
</button>
|
||||||
<td>{{ result.age }}</td>
|
</article>
|
||||||
</tr>
|
</div>
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</section>
|
</section>
|
||||||
<div v-else class="no-results surface-card">
|
<div v-else class="no-results surface-card">
|
||||||
{{ $t('socialnetwork.usersearch.no_results') }}
|
{{ $t('socialnetwork.usersearch.no_results') }}
|
||||||
@@ -211,26 +209,34 @@ label {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
.result-cards {
|
||||||
width: 100%;
|
display: grid;
|
||||||
margin: 0.5em 0;
|
gap: 12px;
|
||||||
padding: 0;
|
}
|
||||||
border-collapse: collapse;
|
|
||||||
background: rgba(255, 255, 255, 0.62);
|
.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);
|
border-radius: var(--radius-lg);
|
||||||
overflow: hidden;
|
background: rgba(255, 255, 255, 0.66);
|
||||||
}
|
}
|
||||||
|
|
||||||
thead {
|
.result-card__main {
|
||||||
color: #42634e;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
th, td {
|
.result-card__meta {
|
||||||
padding: 12px 14px;
|
display: flex;
|
||||||
}
|
flex-wrap: wrap;
|
||||||
|
gap: 10px 16px;
|
||||||
tbody tr + tr td {
|
color: var(--color-text-secondary);
|
||||||
border-top: 1px solid var(--color-border);
|
font-size: 0.92rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clickable {
|
.clickable {
|
||||||
@@ -259,5 +265,10 @@ tbody tr + tr td {
|
|||||||
.age-range {
|
.age-range {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.result-card {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -141,6 +141,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import apiClient from '@/utils/axios.js';
|
import apiClient from '@/utils/axios.js';
|
||||||
|
import { showApiError, showError } from '@/utils/feedback.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'VocabCourseListView',
|
name: 'VocabCourseListView',
|
||||||
@@ -273,7 +274,7 @@ export default {
|
|||||||
},
|
},
|
||||||
async findCourseByCode() {
|
async findCourseByCode() {
|
||||||
if (!this.shareCode.trim()) {
|
if (!this.shareCode.trim()) {
|
||||||
alert(this.$t('socialnetwork.vocab.courses.invalidCode'));
|
showError(this, this.$t('socialnetwork.vocab.courses.invalidCode'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
@@ -286,7 +287,7 @@ export default {
|
|||||||
this.openCourse(course.id);
|
this.openCourse(course.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Fehler beim Suchen des Kurses:', 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 {
|
} finally {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
@@ -332,7 +333,7 @@ export default {
|
|||||||
await this.loadAllCourses();
|
await this.loadAllCourses();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Fehler beim Erstellen des Kurses:', 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) {
|
async enroll(courseId) {
|
||||||
@@ -342,7 +343,7 @@ export default {
|
|||||||
this.openCourse(courseId);
|
this.openCourse(courseId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Fehler beim Einschreiben:', e);
|
console.error('Fehler beim Einschreiben:', e);
|
||||||
alert(e.response?.data?.error || 'Fehler beim Einschreiben');
|
showApiError(this, e, 'Fehler beim Einschreiben');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
openCourse(courseId) {
|
openCourse(courseId) {
|
||||||
|
|||||||
@@ -29,55 +29,44 @@
|
|||||||
{{ $t('socialnetwork.vocab.courses.continueCurrentLesson') }}
|
{{ $t('socialnetwork.vocab.courses.continueCurrentLesson') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<h3>{{ $t('socialnetwork.vocab.courses.lessons') }}</h3>
|
<div class="lessons-header">
|
||||||
<table class="lessons-table">
|
<h3>{{ $t('socialnetwork.vocab.courses.lessons') }}</h3>
|
||||||
<thead>
|
<span class="lessons-count">{{ course.lessons.length }} Lektionen</span>
|
||||||
<tr>
|
</div>
|
||||||
<th class="col-number">#</th>
|
<div class="lesson-cards">
|
||||||
<th class="col-title">{{ $t('socialnetwork.vocab.courses.title') }}</th>
|
<article v-for="lesson in course.lessons" :key="lesson.id" class="lesson-card">
|
||||||
<th class="col-status">Status</th>
|
<div class="lesson-card__header">
|
||||||
<th class="col-actions">Aktionen</th>
|
<span class="lesson-number">#{{ lesson.lessonNumber }}</span>
|
||||||
</tr>
|
<div class="lesson-status-content">
|
||||||
</thead>
|
<span v-if="getLessonProgress(lesson.id)?.completed" class="badge completed">
|
||||||
<tbody>
|
{{ $t('socialnetwork.vocab.courses.completed') }}
|
||||||
<tr v-for="lesson in course.lessons" :key="lesson.id" class="lesson-row">
|
</span>
|
||||||
<td class="lesson-number">{{ lesson.lessonNumber }}</td>
|
<span v-else-if="getLessonProgress(lesson.id)?.score" class="score">
|
||||||
<td class="lesson-title">
|
{{ $t('socialnetwork.vocab.courses.score') }}: {{ getLessonProgress(lesson.id).score }}%
|
||||||
<div class="lesson-title-content">
|
</span>
|
||||||
<span class="title-label">{{ lesson.title }}</span>
|
<span v-else class="status-new">
|
||||||
<span v-if="lesson.description" class="lesson-description">{{ lesson.description }}</span>
|
{{ $t('socialnetwork.vocab.courses.notStarted') }}
|
||||||
</div>
|
</span>
|
||||||
</td>
|
</div>
|
||||||
<td class="lesson-status">
|
</div>
|
||||||
<div class="lesson-status-content">
|
<div class="lesson-title-content">
|
||||||
<span v-if="getLessonProgress(lesson.id)?.completed" class="badge completed">
|
<span class="title-label">{{ lesson.title }}</span>
|
||||||
{{ $t('socialnetwork.vocab.courses.completed') }}
|
<span v-if="lesson.description" class="lesson-description">{{ lesson.description }}</span>
|
||||||
</span>
|
</div>
|
||||||
<span v-else-if="getLessonProgress(lesson.id)?.score" class="score">
|
<div class="lesson-actions-content">
|
||||||
{{ $t('socialnetwork.vocab.courses.score') }}: {{ getLessonProgress(lesson.id).score }}%
|
<button
|
||||||
</span>
|
@click="openLesson(lesson.id)"
|
||||||
<span v-else class="status-new">
|
class="btn-start"
|
||||||
{{ $t('socialnetwork.vocab.courses.notStarted') }}
|
:disabled="!canStartLesson(lesson)"
|
||||||
</span>
|
:title="!canStartLesson(lesson) ? $t('socialnetwork.vocab.courses.previousLessonRequired') : ''"
|
||||||
</div>
|
>
|
||||||
</td>
|
{{ getLessonProgress(lesson.id)?.completed ? $t('socialnetwork.vocab.courses.review') : $t('socialnetwork.vocab.courses.start') }}
|
||||||
<td class="lesson-actions">
|
</button>
|
||||||
<div class="lesson-actions-content">
|
<button v-if="isOwner" @click="editLesson(lesson.id)" class="btn-edit">{{ $t('socialnetwork.vocab.courses.edit') }}</button>
|
||||||
<button
|
<button v-if="isOwner" @click="deleteLesson(lesson.id)" class="btn-delete">{{ $t('general.delete') }}</button>
|
||||||
@click="openLesson(lesson.id)"
|
</div>
|
||||||
class="btn-start"
|
</article>
|
||||||
:disabled="!canStartLesson(lesson)"
|
</div>
|
||||||
:title="!canStartLesson(lesson) ? $t('socialnetwork.vocab.courses.previousLessonRequired') : ''"
|
|
||||||
>
|
|
||||||
{{ getLessonProgress(lesson.id)?.completed ? $t('socialnetwork.vocab.courses.review') : $t('socialnetwork.vocab.courses.start') }}
|
|
||||||
</button>
|
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<p class="surface-card course-state">{{ $t('socialnetwork.vocab.courses.noLessons') }}</p>
|
<p class="surface-card course-state">{{ $t('socialnetwork.vocab.courses.noLessons') }}</p>
|
||||||
@@ -122,7 +111,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import apiClient from '@/utils/axios.js';
|
import apiClient from '@/utils/axios.js';
|
||||||
import { showApiError, showSuccess } from '@/utils/feedback.js';
|
import { confirmAction, showApiError, showSuccess } from '@/utils/feedback.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'VocabCourseView',
|
name: 'VocabCourseView',
|
||||||
@@ -274,7 +263,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async deleteLesson(lessonId) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -370,6 +363,20 @@ export default {
|
|||||||
padding: 20px;
|
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 {
|
.course-state {
|
||||||
padding: 18px;
|
padding: 18px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -403,93 +410,40 @@ export default {
|
|||||||
border: 1px solid #5D4037;
|
border: 1px solid #5D4037;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lessons-table {
|
.lesson-cards {
|
||||||
width: 100%;
|
display: grid;
|
||||||
border-collapse: separate;
|
gap: 12px;
|
||||||
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-number {
|
.lesson-number {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #666;
|
color: #666;
|
||||||
font-size: 0.95em;
|
font-size: 0.95em;
|
||||||
text-align: center;
|
display: inline-flex;
|
||||||
display: block;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 48px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(248, 162, 43, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.lesson-title {
|
.lesson-card {
|
||||||
display: block;
|
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 {
|
.lesson-title-content {
|
||||||
@@ -510,10 +464,6 @@ export default {
|
|||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lesson-status {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lesson-status-content {
|
.lesson-status-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -352,8 +352,8 @@
|
|||||||
|
|
||||||
<!-- Fallback für unbekannte Typen -->
|
<!-- Fallback für unbekannte Typen -->
|
||||||
<div v-else class="unknown-exercise">
|
<div v-else class="unknown-exercise">
|
||||||
<p>Übungstyp: {{ getExerciseType(exercise) }}</p>
|
<p>Dieser Übungstyp wird in der aktuellen Ansicht noch nicht interaktiv dargestellt.</p>
|
||||||
<pre>{{ JSON.stringify(exercise, null, 2) }}</pre>
|
<p class="unknown-exercise__type">Typ: {{ getExerciseType(exercise) }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -418,6 +418,8 @@
|
|||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import apiClient from '@/utils/axios.js';
|
import apiClient from '@/utils/axios.js';
|
||||||
|
|
||||||
|
const debugLog = () => {};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'VocabLessonView',
|
name: 'VocabLessonView',
|
||||||
props: {
|
props: {
|
||||||
@@ -533,7 +535,7 @@ export default {
|
|||||||
// Normale Lektion: Verwende effectiveExercises (grammarExercises)
|
// Normale Lektion: Verwende effectiveExercises (grammarExercises)
|
||||||
const exercises = this.effectiveExercises;
|
const exercises = this.effectiveExercises;
|
||||||
if (!exercises || !Array.isArray(exercises) || exercises.length === 0) {
|
if (!exercises || !Array.isArray(exercises) || exercises.length === 0) {
|
||||||
console.log('[importantVocab] Keine Übungen vorhanden');
|
debugLog('[importantVocab] Keine Übungen vorhanden');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return this._extractVocabFromExercises(exercises);
|
return this._extractVocabFromExercises(exercises);
|
||||||
@@ -576,7 +578,7 @@ export default {
|
|||||||
const isArrayLike = exercises.length !== undefined && typeof exercises.length === 'number';
|
const isArrayLike = exercises.length !== undefined && typeof exercises.length === 'number';
|
||||||
|
|
||||||
if (!Array.isArray(exercises) && isArrayLike) {
|
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)
|
// Versuche Array.from (funktioniert mit iterierbaren Objekten und Array-like Objekten)
|
||||||
try {
|
try {
|
||||||
exercisesArray = Array.from(exercises);
|
exercisesArray = Array.from(exercises);
|
||||||
@@ -601,13 +603,13 @@ export default {
|
|||||||
|
|
||||||
exercisesArray.forEach((exercise, idx) => {
|
exercisesArray.forEach((exercise, idx) => {
|
||||||
try {
|
try {
|
||||||
console.log(`[importantVocab] Verarbeite Übung ${idx + 1}:`, exercise.title);
|
debugLog(`[importantVocab] Verarbeite Übung ${idx + 1}:`, exercise.title);
|
||||||
// Extrahiere aus questionData
|
// Extrahiere aus questionData
|
||||||
const qData = this.getQuestionData(exercise);
|
const qData = this.getQuestionData(exercise);
|
||||||
const aData = this.getAnswerData(exercise);
|
const aData = this.getAnswerData(exercise);
|
||||||
|
|
||||||
console.log(`[importantVocab] qData:`, qData);
|
debugLog(`[importantVocab] qData:`, qData);
|
||||||
console.log(`[importantVocab] aData:`, aData);
|
debugLog(`[importantVocab] aData:`, aData);
|
||||||
|
|
||||||
if (qData && aData) {
|
if (qData && aData) {
|
||||||
// Für Multiple Choice: Extrahiere Optionen und richtige Antwort
|
// 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 correctIndex = aData.correctAnswer !== undefined ? aData.correctAnswer : (aData.correct || 0);
|
||||||
const correctAnswer = options[correctIndex] || '';
|
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) {
|
if (correctAnswer) {
|
||||||
// Versuche die Frage zu analysieren (z.B. "Wie sagt man 'X' auf Bisaya?" oder "Was bedeutet 'X'?")
|
// Versuche die Frage zu analysieren (z.B. "Wie sagt man 'X' auf Bisaya?" oder "Was bedeutet 'X'?")
|
||||||
const question = qData.question || qData.text || '';
|
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")
|
// 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);
|
let match = question.match(/Wie sagt man ['"]([^'"]+)['"]/i);
|
||||||
@@ -629,11 +631,11 @@ export default {
|
|||||||
const nativeWord = match[1]; // Das Wort in der Muttersprache
|
const nativeWord = match[1]; // Das Wort in der Muttersprache
|
||||||
// Nur hinzufügen, wenn Muttersprache und Bisaya unterschiedlich sind (verhindert "ko" -> "ko")
|
// Nur hinzufügen, wenn Muttersprache und Bisaya unterschiedlich sind (verhindert "ko" -> "ko")
|
||||||
if (nativeWord && correctAnswer && nativeWord.trim() !== correctAnswer.trim()) {
|
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)
|
// learning = Muttersprache (was man lernt), reference = Bisaya (Zielsprache)
|
||||||
vocabMap.set(`${nativeWord}-${correctAnswer}`, { learning: nativeWord, reference: correctAnswer });
|
vocabMap.set(`${nativeWord}-${correctAnswer}`, { learning: nativeWord, reference: correctAnswer });
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
// Pattern 2: "Was bedeutet 'X'?" -> X ist Bisaya, correctAnswer ist Muttersprache
|
// Pattern 2: "Was bedeutet 'X'?" -> X ist Bisaya, correctAnswer ist Muttersprache
|
||||||
@@ -642,14 +644,14 @@ export default {
|
|||||||
const bisayaWord = match[1];
|
const bisayaWord = match[1];
|
||||||
// Nur hinzufügen, wenn Bisaya und Muttersprache unterschiedlich sind (verhindert "ko" -> "ko")
|
// Nur hinzufügen, wenn Bisaya und Muttersprache unterschiedlich sind (verhindert "ko" -> "ko")
|
||||||
if (bisayaWord && correctAnswer && bisayaWord.trim() !== correctAnswer.trim()) {
|
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)
|
// learning = Muttersprache (was man lernt), reference = Bisaya (Zielsprache)
|
||||||
vocabMap.set(`${correctAnswer}-${bisayaWord}`, { learning: correctAnswer, reference: bisayaWord });
|
vocabMap.set(`${correctAnswer}-${bisayaWord}`, { learning: correctAnswer, reference: bisayaWord });
|
||||||
} else {
|
} 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 {
|
} 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
|
// Überspringe, wenn wir die Richtung nicht erkennen können
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -659,7 +661,7 @@ export default {
|
|||||||
// Für Gap Fill: Extrahiere richtige Antworten
|
// Für Gap Fill: Extrahiere richtige Antworten
|
||||||
if (this.getExerciseType(exercise) === 'gap_fill') {
|
if (this.getExerciseType(exercise) === 'gap_fill') {
|
||||||
const answers = aData.answers || (aData.correct ? (Array.isArray(aData.correct) ? aData.correct : [aData.correct]) : []);
|
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) {
|
if (answers.length > 0) {
|
||||||
// Versuche aus dem Text Kontext zu extrahieren
|
// Versuche aus dem Text Kontext zu extrahieren
|
||||||
// Gap Fill hat normalerweise Format: "{gap} (Muttersprache) | {gap} (Muttersprache) | ..."
|
// Gap Fill hat normalerweise Format: "{gap} (Muttersprache) | {gap} (Muttersprache) | ..."
|
||||||
@@ -668,7 +670,7 @@ export default {
|
|||||||
const matches = text.matchAll(/\(([^)]+)\)/g);
|
const matches = text.matchAll(/\(([^)]+)\)/g);
|
||||||
const nativeWords = Array.from(matches, m => m[1]);
|
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
|
// Nur extrahieren, wenn Muttersprache-Hinweise (Klammern) vorhanden sind
|
||||||
if (nativeWords.length > 0) {
|
if (nativeWords.length > 0) {
|
||||||
@@ -679,14 +681,14 @@ export default {
|
|||||||
// Die answer ist normalerweise Bisaya, nativeWord ist Muttersprache
|
// Die answer ist normalerweise Bisaya, nativeWord ist Muttersprache
|
||||||
// Nur hinzufügen, wenn sie unterschiedlich sind (verhindert "ko" -> "ko")
|
// Nur hinzufügen, wenn sie unterschiedlich sind (verhindert "ko" -> "ko")
|
||||||
vocabMap.set(`${nativeWord}-${answer}`, { learning: nativeWord, reference: answer });
|
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 {
|
} 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 {
|
} 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());
|
const result = Array.from(vocabMap.values());
|
||||||
console.log(`[_extractVocabFromExercises] Ergebnis:`, result.length, 'Vokabeln');
|
debugLog(`[_extractVocabFromExercises] Ergebnis:`, result.length, 'Vokabeln');
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
async loadLesson() {
|
async loadLesson() {
|
||||||
// Verhindere mehrfaches Laden
|
// Verhindere mehrfaches Laden
|
||||||
if (this.loading) {
|
if (this.loading) {
|
||||||
console.log('[VocabLessonView] loadLesson übersprungen - bereits am Laden');
|
debugLog('[VocabLessonView] loadLesson übersprungen - bereits am Laden');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[VocabLessonView] loadLesson gestartet für lessonId:', this.lessonId);
|
debugLog('[VocabLessonView] loadLesson gestartet für lessonId:', this.lessonId);
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
// Setze Antworten und Ergebnisse zurück
|
// Setze Antworten und Ergebnisse zurück
|
||||||
this.exerciseAnswers = {};
|
this.exerciseAnswers = {};
|
||||||
@@ -725,19 +727,19 @@ export default {
|
|||||||
try {
|
try {
|
||||||
const res = await apiClient.get(`/api/vocab/lessons/${this.lessonId}`);
|
const res = await apiClient.get(`/api/vocab/lessons/${this.lessonId}`);
|
||||||
this.lesson = res.data;
|
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)
|
// Initialisiere mit effectiveExercises (für Review: reviewVocabExercises, sonst: grammarExercises)
|
||||||
this.$nextTick(async () => {
|
this.$nextTick(async () => {
|
||||||
const exercises = this.effectiveExercises;
|
const exercises = this.effectiveExercises;
|
||||||
if (exercises && exercises.length > 0) {
|
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);
|
this.initializeExercises(exercises);
|
||||||
} else {
|
} else {
|
||||||
console.log('[VocabLessonView] Lade Übungen separat...');
|
debugLog('[VocabLessonView] Lade Übungen separat...');
|
||||||
await this.loadGrammarExercises();
|
await this.loadGrammarExercises();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
console.log('[VocabLessonView] loadLesson abgeschlossen');
|
debugLog('[VocabLessonView] loadLesson abgeschlossen');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[VocabLessonView] Fehler beim Laden der Lektion:', e);
|
console.error('[VocabLessonView] Fehler beim Laden der Lektion:', e);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -883,18 +885,18 @@ export default {
|
|||||||
async checkLessonCompletion() {
|
async checkLessonCompletion() {
|
||||||
// Verhindere mehrfache Ausführung
|
// Verhindere mehrfache Ausführung
|
||||||
if (this.isCheckingLessonCompletion || this.isNavigatingToNext) {
|
if (this.isCheckingLessonCompletion || this.isNavigatingToNext) {
|
||||||
console.log('[VocabLessonView] checkLessonCompletion übersprungen - bereits in Ausführung');
|
debugLog('[VocabLessonView] checkLessonCompletion übersprungen - bereits in Ausführung');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prüfe ob alle Übungen korrekt beantwortet wurden (effectiveExercises = Kapitel-Prüfung)
|
// Prüfe ob alle Übungen korrekt beantwortet wurden (effectiveExercises = Kapitel-Prüfung)
|
||||||
const allExercises = this.effectiveExercises;
|
const allExercises = this.effectiveExercises;
|
||||||
if (!this.lesson || !allExercises || allExercises.length === 0) {
|
if (!this.lesson || !allExercises || allExercises.length === 0) {
|
||||||
console.log('[VocabLessonView] checkLessonCompletion übersprungen - keine Lektion/Übungen');
|
debugLog('[VocabLessonView] checkLessonCompletion übersprungen - keine Lektion/Übungen');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (allExercises.length === 0) {
|
if (allExercises.length === 0) {
|
||||||
console.log('[VocabLessonView] checkLessonCompletion übersprungen - keine Übungen');
|
debugLog('[VocabLessonView] checkLessonCompletion übersprungen - keine Übungen');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -904,11 +906,11 @@ export default {
|
|||||||
return result && result.correct;
|
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) {
|
if (allCompleted && !this.isCheckingLessonCompletion) {
|
||||||
this.isCheckingLessonCompletion = true;
|
this.isCheckingLessonCompletion = true;
|
||||||
console.log('[VocabLessonView] Alle Übungen abgeschlossen - starte Fortschritts-Update');
|
debugLog('[VocabLessonView] Alle Übungen abgeschlossen - starte Fortschritts-Update');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Berechne Gesamt-Score
|
// Berechne Gesamt-Score
|
||||||
@@ -916,7 +918,7 @@ export default {
|
|||||||
const correctExercises = allExercises.filter(ex => this.exerciseResults[ex.id]?.correct).length;
|
const correctExercises = allExercises.filter(ex => this.exerciseResults[ex.id]?.correct).length;
|
||||||
const score = Math.round((correctExercises / totalExercises) * 100);
|
const score = Math.round((correctExercises / totalExercises) * 100);
|
||||||
|
|
||||||
console.log('[VocabLessonView] Score berechnet:', score, '%');
|
debugLog('[VocabLessonView] Score berechnet:', score, '%');
|
||||||
|
|
||||||
// Aktualisiere Fortschritt
|
// Aktualisiere Fortschritt
|
||||||
await apiClient.put(`/api/vocab/lessons/${this.lessonId}/progress`, {
|
await apiClient.put(`/api/vocab/lessons/${this.lessonId}/progress`, {
|
||||||
@@ -925,7 +927,7 @@ export default {
|
|||||||
timeSpentMinutes: 0 // TODO: Zeit tracken
|
timeSpentMinutes: 0 // TODO: Zeit tracken
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('[VocabLessonView] Fortschritt aktualisiert - starte Navigation');
|
debugLog('[VocabLessonView] Fortschritt aktualisiert - starte Navigation');
|
||||||
|
|
||||||
// Weiterleitung zur nächsten Lektion
|
// Weiterleitung zur nächsten Lektion
|
||||||
await this.navigateToNextLesson();
|
await this.navigateToNextLesson();
|
||||||
@@ -938,7 +940,7 @@ export default {
|
|||||||
async navigateToNextLesson() {
|
async navigateToNextLesson() {
|
||||||
// Verhindere mehrfache Navigation
|
// Verhindere mehrfache Navigation
|
||||||
if (this.isNavigatingToNext) {
|
if (this.isNavigatingToNext) {
|
||||||
console.log('[VocabLessonView] Navigation bereits in Gang, überspringe...');
|
debugLog('[VocabLessonView] Navigation bereits in Gang, überspringe...');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.isNavigatingToNext = true;
|
this.isNavigatingToNext = true;
|
||||||
@@ -949,7 +951,7 @@ export default {
|
|||||||
const course = courseRes.data;
|
const course = courseRes.data;
|
||||||
|
|
||||||
if (!course.lessons || course.lessons.length === 0) {
|
if (!course.lessons || course.lessons.length === 0) {
|
||||||
console.log('[VocabLessonView] Keine Lektionen gefunden');
|
debugLog('[VocabLessonView] Keine Lektionen gefunden');
|
||||||
this.isNavigatingToNext = false;
|
this.isNavigatingToNext = false;
|
||||||
this.isCheckingLessonCompletion = false;
|
this.isCheckingLessonCompletion = false;
|
||||||
return;
|
return;
|
||||||
@@ -961,7 +963,7 @@ export default {
|
|||||||
if (currentLessonIndex >= 0 && currentLessonIndex < course.lessons.length - 1) {
|
if (currentLessonIndex >= 0 && currentLessonIndex < course.lessons.length - 1) {
|
||||||
// Nächste Lektion gefunden
|
// Nächste Lektion gefunden
|
||||||
const nextLesson = course.lessons[currentLessonIndex + 1];
|
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
|
// Zeige Erfolgs-Meldung und leite weiter
|
||||||
// Verwende Dialog statt confirm
|
// Verwende Dialog statt confirm
|
||||||
@@ -969,7 +971,7 @@ export default {
|
|||||||
this.nextLessonId = nextLesson.id;
|
this.nextLessonId = nextLesson.id;
|
||||||
} else {
|
} else {
|
||||||
// Letzte Lektion - zeige Abschluss-Meldung
|
// Letzte Lektion - zeige Abschluss-Meldung
|
||||||
console.log('[VocabLessonView] Letzte Lektion erreicht');
|
debugLog('[VocabLessonView] Letzte Lektion erreicht');
|
||||||
this.showCompletionDialog = true;
|
this.showCompletionDialog = true;
|
||||||
this.isNavigatingToNext = false;
|
this.isNavigatingToNext = false;
|
||||||
this.isCheckingLessonCompletion = false;
|
this.isCheckingLessonCompletion = false;
|
||||||
@@ -985,7 +987,7 @@ export default {
|
|||||||
},
|
},
|
||||||
confirmNavigateToNextLesson() {
|
confirmNavigateToNextLesson() {
|
||||||
if (this.nextLessonId) {
|
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
|
// Setze Flags zurück BEVOR die Navigation stattfindet
|
||||||
this.isNavigatingToNext = false;
|
this.isNavigatingToNext = false;
|
||||||
this.isCheckingLessonCompletion = false;
|
this.isCheckingLessonCompletion = false;
|
||||||
@@ -996,7 +998,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
cancelNavigateToNextLesson() {
|
cancelNavigateToNextLesson() {
|
||||||
console.log('[VocabLessonView] Navigation abgebrochen');
|
debugLog('[VocabLessonView] Navigation abgebrochen');
|
||||||
this.isNavigatingToNext = false;
|
this.isNavigatingToNext = false;
|
||||||
this.isCheckingLessonCompletion = false;
|
this.isCheckingLessonCompletion = false;
|
||||||
this.showNextLessonDialog = false;
|
this.showNextLessonDialog = false;
|
||||||
@@ -1011,13 +1013,13 @@ export default {
|
|||||||
},
|
},
|
||||||
// Vokabeltrainer-Methoden
|
// Vokabeltrainer-Methoden
|
||||||
startVocabTrainer() {
|
startVocabTrainer() {
|
||||||
console.log('[VocabLessonView] startVocabTrainer aufgerufen');
|
debugLog('[VocabLessonView] startVocabTrainer aufgerufen');
|
||||||
if (!this.importantVocab || this.importantVocab.length === 0) {
|
if (!this.importantVocab || this.importantVocab.length === 0) {
|
||||||
console.log('[VocabLessonView] Keine Vokabeln vorhanden');
|
debugLog('[VocabLessonView] Keine Vokabeln vorhanden');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log('[VocabLessonView] Vokabeln gefunden:', this.importantVocab.length);
|
debugLog('[VocabLessonView] Vokabeln gefunden:', this.importantVocab.length);
|
||||||
console.log('[VocabLessonView] Alte Vokabeln:', this.previousVocab?.length || 0);
|
debugLog('[VocabLessonView] Alte Vokabeln:', this.previousVocab?.length || 0);
|
||||||
this.vocabTrainerActive = true;
|
this.vocabTrainerActive = true;
|
||||||
this.vocabTrainerPool = [...this.importantVocab];
|
this.vocabTrainerPool = [...this.importantVocab];
|
||||||
this.vocabTrainerMode = 'multiple_choice';
|
this.vocabTrainerMode = 'multiple_choice';
|
||||||
@@ -1030,8 +1032,8 @@ export default {
|
|||||||
this.vocabTrainerMixedAttempts = 0;
|
this.vocabTrainerMixedAttempts = 0;
|
||||||
// Bereite Mixed-Pool aus alten Vokabeln vor (ohne Duplikate aus aktueller Lektion)
|
// Bereite Mixed-Pool aus alten Vokabeln vor (ohne Duplikate aus aktueller Lektion)
|
||||||
this.vocabTrainerMixedPool = this._buildMixedPool();
|
this.vocabTrainerMixedPool = this._buildMixedPool();
|
||||||
console.log('[VocabLessonView] Mixed-Pool:', this.vocabTrainerMixedPool.length, 'Vokabeln');
|
debugLog('[VocabLessonView] Mixed-Pool:', this.vocabTrainerMixedPool.length, 'Vokabeln');
|
||||||
console.log('[VocabLessonView] Rufe nextVocabQuestion auf');
|
debugLog('[VocabLessonView] Rufe nextVocabQuestion auf');
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.nextVocabQuestion();
|
this.nextVocabQuestion();
|
||||||
});
|
});
|
||||||
@@ -1078,7 +1080,7 @@ export default {
|
|||||||
if (successRate >= 80) {
|
if (successRate >= 80) {
|
||||||
// Wechsel zur Mixed-Phase (falls alte Vokabeln vorhanden)
|
// Wechsel zur Mixed-Phase (falls alte Vokabeln vorhanden)
|
||||||
if (this.vocabTrainerMixedPool.length > 0) {
|
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.vocabTrainerPhase = 'mixed';
|
||||||
this.vocabTrainerPool = [...this.vocabTrainerMixedPool];
|
this.vocabTrainerPool = [...this.vocabTrainerMixedPool];
|
||||||
this.vocabTrainerMixedAttempts = 0;
|
this.vocabTrainerMixedAttempts = 0;
|
||||||
@@ -1100,7 +1102,7 @@ export default {
|
|||||||
} else if (this.vocabTrainerPhase === 'mixed') {
|
} else if (this.vocabTrainerPhase === 'mixed') {
|
||||||
// Phase 2: Gemischte Wiederholung - nach MIXED_LIMIT Versuchen → Wechsel zu Typing mit allen Vokabeln
|
// Phase 2: Gemischte Wiederholung - nach MIXED_LIMIT Versuchen → Wechsel zu Typing mit allen Vokabeln
|
||||||
if (this.vocabTrainerMode === 'multiple_choice' && this.vocabTrainerTotalAttempts >= MIXED_LIMIT) {
|
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.vocabTrainerMode = 'typing';
|
||||||
this.vocabTrainerAutoSwitchedToTyping = true;
|
this.vocabTrainerAutoSwitchedToTyping = true;
|
||||||
// Im Typing: Pool aus aktuellen + alten Vokabeln kombinieren
|
// Im Typing: Pool aus aktuellen + alten Vokabeln kombinieren
|
||||||
@@ -1206,9 +1208,9 @@ export default {
|
|||||||
return arr;
|
return arr;
|
||||||
},
|
},
|
||||||
nextVocabQuestion() {
|
nextVocabQuestion() {
|
||||||
console.log('[VocabLessonView] nextVocabQuestion aufgerufen');
|
debugLog('[VocabLessonView] nextVocabQuestion aufgerufen');
|
||||||
if (!this.vocabTrainerPool || this.vocabTrainerPool.length === 0) {
|
if (!this.vocabTrainerPool || this.vocabTrainerPool.length === 0) {
|
||||||
console.log('[VocabLessonView] Keine Vokabeln im Pool');
|
debugLog('[VocabLessonView] Keine Vokabeln im Pool');
|
||||||
this.currentVocabQuestion = null;
|
this.currentVocabQuestion = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1227,7 +1229,7 @@ export default {
|
|||||||
key: this.getVocabKey(vocab)
|
key: this.getVocabKey(vocab)
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('[VocabLessonView] Neue Frage erstellt:', this.currentVocabQuestion.prompt);
|
debugLog('[VocabLessonView] Neue Frage erstellt:', this.currentVocabQuestion.prompt);
|
||||||
|
|
||||||
// Reset UI
|
// Reset UI
|
||||||
this.vocabTrainerAnswer = '';
|
this.vocabTrainerAnswer = '';
|
||||||
@@ -1236,16 +1238,16 @@ export default {
|
|||||||
|
|
||||||
// Erstelle Choice-Optionen für Multiple Choice
|
// Erstelle Choice-Optionen für Multiple Choice
|
||||||
if (this.vocabTrainerMode === 'multiple_choice') {
|
if (this.vocabTrainerMode === 'multiple_choice') {
|
||||||
console.log('[VocabLessonView] Erstelle Choice-Optionen...');
|
debugLog('[VocabLessonView] Erstelle Choice-Optionen...');
|
||||||
console.log('[VocabLessonView] Prompt:', this.currentVocabQuestion.prompt);
|
debugLog('[VocabLessonView] Prompt:', this.currentVocabQuestion.prompt);
|
||||||
console.log('[VocabLessonView] Answer:', this.currentVocabQuestion.answer);
|
debugLog('[VocabLessonView] Answer:', this.currentVocabQuestion.answer);
|
||||||
// Wichtig: Der Prompt (die Frage) darf nicht in den Optionen erscheinen
|
// Wichtig: Der Prompt (die Frage) darf nicht in den Optionen erscheinen
|
||||||
this.vocabTrainerChoiceOptions = this.buildChoiceOptions(
|
this.vocabTrainerChoiceOptions = this.buildChoiceOptions(
|
||||||
this.currentVocabQuestion.answer,
|
this.currentVocabQuestion.answer,
|
||||||
this.vocabTrainerPool,
|
this.vocabTrainerPool,
|
||||||
this.currentVocabQuestion.prompt // Exkludiere den Prompt
|
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
|
// Fokussiere Eingabefeld im Typing-Modus
|
||||||
@@ -1597,10 +1599,10 @@ export default {
|
|||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.unknown-exercise pre {
|
.unknown-exercise__type {
|
||||||
margin-top: 10px;
|
margin-top: 8px;
|
||||||
font-size: 0.85em;
|
font-size: 0.9em;
|
||||||
overflow-x: auto;
|
color: var(--color-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tabs */
|
/* Tabs */
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
<article class="summary-card surface-card">
|
<article class="summary-card surface-card">
|
||||||
<span class="summary-card__label">Abonniert</span>
|
<span class="summary-card__label">Abonniert</span>
|
||||||
<strong>{{ subscribedLanguages.length }}</strong>
|
<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>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
</article>
|
</article>
|
||||||
<article class="task-card surface-card">
|
<article class="task-card surface-card">
|
||||||
<span class="task-card__eyebrow">Weiterlernen</span>
|
<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>
|
<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>
|
<button type="button" class="button-secondary" @click="goCourses">{{ $t('socialnetwork.vocab.courses.title') }}</button>
|
||||||
</article>
|
</article>
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
<div class="language-section__header">
|
<div class="language-section__header">
|
||||||
<div>
|
<div>
|
||||||
<h3>Abonnierte Sprachen</h3>
|
<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>
|
</div>
|
||||||
<span class="language-section__count">{{ subscribedLanguages.length }}</span>
|
<span class="language-section__count">{{ subscribedLanguages.length }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -86,7 +86,7 @@
|
|||||||
<button type="button" class="language-card__main" @click="openLanguage(l.id)">
|
<button type="button" class="language-card__main" @click="openLanguage(l.id)">
|
||||||
<div class="language-card__info">
|
<div class="language-card__info">
|
||||||
<span class="langname">{{ l.name }}</span>
|
<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>
|
</div>
|
||||||
<span class="role">{{ $t('socialnetwork.vocab.subscribed') }}</span>
|
<span class="role">{{ $t('socialnetwork.vocab.subscribed') }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user