Implement debtors prison features across the application: Enhance FalukantController to include debtors prison logic in various service methods. Update FalukantService to manage debtors prison state and integrate it into user data retrieval. Modify frontend components, including DashboardWidget, StatusBar, and BankView, to display debtors prison status and warnings. Add localization for debtors prison messages in English, German, and Spanish, ensuring clarity in user notifications and actions.

This commit is contained in:
Torsten Schulz (local)
2026-03-23 11:59:59 +01:00
parent f2343098d2
commit 9b88a98a20
19 changed files with 1643 additions and 102 deletions

View File

@@ -108,7 +108,7 @@ export default {
},
setupSocketListeners() {
this.teardownSocketListeners();
const daemonEvents = ['falukantUpdateStatus', 'falukantUpdateFamily', 'falukantUpdateChurch', 'children_update', 'falukantUpdateProductionCertificate', 'stock_change', 'familychanged'];
const daemonEvents = ['falukantUpdateStatus', 'falukantUpdateFamily', 'falukantUpdateChurch', 'falukantUpdateDebt', 'children_update', 'falukantUpdateProductionCertificate', 'stock_change', 'familychanged'];
if (this.daemonSocket) {
this._daemonMessageHandler = (event) => {
if (event.data === 'ping') return;
@@ -129,6 +129,9 @@ export default {
this._churchSocketHandler = (data) => {
if (this.matchesCurrentUser(data)) this.queueFetchData();
};
this._debtSocketHandler = (data) => {
if (this.matchesCurrentUser(data)) this.queueFetchData();
};
this._childrenSocketHandler = (data) => {
if (this.matchesCurrentUser(data)) this.queueFetchData();
};
@@ -140,6 +143,7 @@ export default {
this.socket.on('falukantUpdateStatus', this._statusSocketHandler);
this.socket.on('falukantUpdateFamily', this._familySocketHandler);
this.socket.on('falukantUpdateChurch', this._churchSocketHandler);
this.socket.on('falukantUpdateDebt', this._debtSocketHandler);
this.socket.on('children_update', this._childrenSocketHandler);
this.socket.on('falukantUpdateProductionCertificate', this._productionCertificateSocketHandler);
this.socket.on('falukantBranchUpdate', this._branchSocketHandler);
@@ -154,6 +158,7 @@ export default {
if (this._statusSocketHandler) this.socket.off('falukantUpdateStatus', this._statusSocketHandler);
if (this._familySocketHandler) this.socket.off('falukantUpdateFamily', this._familySocketHandler);
if (this._churchSocketHandler) this.socket.off('falukantUpdateChurch', this._churchSocketHandler);
if (this._debtSocketHandler) this.socket.off('falukantUpdateDebt', this._debtSocketHandler);
if (this._childrenSocketHandler) this.socket.off('children_update', this._childrenSocketHandler);
if (this._productionCertificateSocketHandler) this.socket.off('falukantUpdateProductionCertificate', this._productionCertificateSocketHandler);
if (this._branchSocketHandler) this.socket.off('falukantBranchUpdate', this._branchSocketHandler);

View File

@@ -18,15 +18,19 @@
</div>
</div>
<div>
<button @click="openCreateBranchDialog">
<button
@click="openCreateBranchDialog"
:disabled="blocked"
>
{{ $t('falukant.branch.actions.create') }}
</button>
<button
@click="$emit('upgradeBranch')"
:disabled="!localSelectedBranch"
:disabled="!localSelectedBranch || blocked"
>
{{ $t('falukant.branch.actions.upgrade') }}
</button>
<span v-if="blocked && blockedReason" class="blocked-hint">{{ blockedReason }}</span>
</div>
</div>
@@ -51,6 +55,8 @@ export default {
props: {
branches: { type: Array, required: true },
selectedBranch: { type: Object, default: null },
blocked: { type: Boolean, default: false },
blockedReason: { type: String, default: '' },
},
data() {
return {
@@ -82,6 +88,7 @@ export default {
},
openCreateBranchDialog() {
if (this.blocked) return;
this.$refs.createBranchDialog.open();
},
@@ -131,4 +138,13 @@ button {
.weather-value {
text-transform: capitalize;
}
.blocked-hint {
display: inline-flex;
align-items: center;
margin-left: 8px;
color: #8b2f23;
font-size: 0.88rem;
font-weight: 600;
}
</style>

View File

@@ -36,6 +36,18 @@
/>
</template>
</div>
<div v-if="debtorsPrison.active" class="statusbar-warning" :class="{ 'is-prison': debtorsPrison.inDebtorsPrison }">
<strong>
{{ debtorsPrison.inDebtorsPrison
? $t('falukant.bank.debtorsPrison.titlePrison')
: $t('falukant.bank.debtorsPrison.titleWarning') }}
</strong>
<span>
{{ debtorsPrison.inDebtorsPrison
? $t('falukant.debtorsPrison.globalLocked')
: $t('falukant.debtorsPrison.globalWarning') }}
</span>
</div>
<MessagesDialog ref="msgs" />
</div>
</template>
@@ -60,6 +72,10 @@ export default {
{ key: "children", icon: "👶", value: null },
],
unreadCount: 0,
debtorsPrison: {
active: false,
inDebtorsPrison: false
},
pendingStatusRefresh: null,
};
},
@@ -146,6 +162,10 @@ export default {
const childCount = Number(response.data.childrenCount) || 0;
const unbaptisedCount = Number(response.data.unbaptisedChildrenCount) || 0;
this.unreadCount = Number(response.data.unreadNotifications) || 0;
this.debtorsPrison = response.data.debtorsPrison || {
active: false,
inDebtorsPrison: false
};
const childrenDisplay = `${childCount}${unbaptisedCount > 0 ? `(${unbaptisedCount})` : ''}`;
let healthStatus = '';
if (health > 90) {
@@ -177,6 +197,7 @@ export default {
this._statusSocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateStatus', ...data });
this._familySocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateFamily', ...data });
this._churchSocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateChurch', ...data });
this._debtSocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateDebt', ...data });
this._childrenSocketHandler = (data) => this.handleEvent({ event: 'children_update', ...data });
this._productionCertificateSocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateProductionCertificate', ...data });
this._stockSocketHandler = (data) => this.handleEvent({ event: 'stock_change', ...data });
@@ -185,6 +206,7 @@ export default {
this.socket.on('falukantUpdateStatus', this._statusSocketHandler);
this.socket.on('falukantUpdateFamily', this._familySocketHandler);
this.socket.on('falukantUpdateChurch', this._churchSocketHandler);
this.socket.on('falukantUpdateDebt', this._debtSocketHandler);
this.socket.on('children_update', this._childrenSocketHandler);
this.socket.on('falukantUpdateProductionCertificate', this._productionCertificateSocketHandler);
this.socket.on('stock_change', this._stockSocketHandler);
@@ -195,6 +217,7 @@ export default {
if (this._statusSocketHandler) this.socket.off('falukantUpdateStatus', this._statusSocketHandler);
if (this._familySocketHandler) this.socket.off('falukantUpdateFamily', this._familySocketHandler);
if (this._churchSocketHandler) this.socket.off('falukantUpdateChurch', this._churchSocketHandler);
if (this._debtSocketHandler) this.socket.off('falukantUpdateDebt', this._debtSocketHandler);
if (this._childrenSocketHandler) this.socket.off('children_update', this._childrenSocketHandler);
if (this._productionCertificateSocketHandler) this.socket.off('falukantUpdateProductionCertificate', this._productionCertificateSocketHandler);
if (this._stockSocketHandler) this.socket.off('stock_change', this._stockSocketHandler);
@@ -207,7 +230,7 @@ export default {
this._daemonHandler = (event) => {
try {
const data = JSON.parse(event.data);
if (['falukantUpdateStatus', 'falukantUpdateFamily', 'falukantUpdateChurch', 'children_update', 'falukantUpdateProductionCertificate', 'stock_change', 'familychanged'].includes(data.event)) {
if (['falukantUpdateStatus', 'falukantUpdateFamily', 'falukantUpdateChurch', 'falukantUpdateDebt', 'children_update', 'falukantUpdateProductionCertificate', 'stock_change', 'familychanged'].includes(data.event)) {
this.handleEvent(data);
}
} catch (_) {}
@@ -247,6 +270,7 @@ export default {
case 'falukantUpdateStatus':
case 'falukantUpdateFamily':
case 'falukantUpdateChurch':
case 'falukantUpdateDebt':
case 'children_update':
case 'falukantUpdateProductionCertificate':
case 'stock_change':
@@ -294,6 +318,25 @@ export default {
box-shadow: var(--shadow-soft);
}
.statusbar-warning {
flex: 1 1 100%;
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: var(--radius-md);
border: 1px solid rgba(180, 120, 40, 0.35);
background: rgba(255, 244, 223, 0.92);
color: #8a5411;
font-size: 0.92rem;
}
.statusbar-warning.is-prison {
border-color: rgba(146, 57, 40, 0.42);
background: rgba(255, 232, 225, 0.94);
color: #8b2f23;
}
.status-item {
text-align: center;
cursor: pointer;

View File

@@ -12,6 +12,14 @@
<dd>{{ falukantData.unreadNotificationsCount }}</dd>
<dt>{{ $t('falukant.statusbar.children') }}</dt>
<dd>{{ falukantData.childrenCount }}</dd>
<template v-if="falukantData.debtorsPrison?.active">
<dt>{{ $t('falukant.bank.debtorsPrison.titlePrison') }}</dt>
<dd class="falukant-debt" :class="{ 'falukant-debt--warning': !falukantData.debtorsPrison?.inDebtorsPrison }">
{{ falukantData.debtorsPrison?.inDebtorsPrison
? $t('falukant.bank.debtorsPrison.titlePrison')
: $t('falukant.bank.debtorsPrison.titleWarning') }}
</dd>
</template>
</dl>
<span v-else></span>
</template>
@@ -43,6 +51,7 @@ export default {
money: pick(raw, 'money', 'money'),
unreadNotificationsCount: pick(raw, 'unreadNotificationsCount', 'unread_notifications_count'),
childrenCount: pick(raw, 'childrenCount', 'children_count'),
debtorsPrison: pick(raw, 'debtorsPrison', 'debtors_prison'),
// keep all original keys as fallback for any other usage
...raw
};
@@ -173,4 +182,13 @@ export default {
color: #198754;
font-weight: 600;
}
.dashboard-widget__falukant dd.falukant-debt {
color: #8b2f23;
font-weight: 700;
}
.dashboard-widget__falukant dd.falukant-debt--warning {
color: #9a5a08;
}
</style>

View File

@@ -24,6 +24,11 @@
"children": "Kinder",
"children_unbaptised": "ungetaufte Kinder"
},
"debtorsPrison": {
"actionBlocked": "Im Schuldturm kannst du diese Aktion derzeit nicht ausführen.",
"globalWarning": "Dein Kreditverzug schränkt dein Handeln ein. Zwangsmaßnahmen können bald folgen.",
"globalLocked": "Du bist im Schuldturm. Fast alle aktiven Falukant-Handlungen sind derzeit gesperrt."
},
"messages": {
"title": "Nachrichten",
"tooltip": "Nachrichten",
@@ -220,6 +225,11 @@
},
"branch": {
"title": "Filiale",
"debtorsPrison": {
"branchLocked": "Im Schuldturm sind neue wirtschaftliche Schritte blockiert. Geschlossene oder gepfändete Standorte werden hier ebenfalls sichtbar.",
"branchRisk": "Dein Kreditverzug gefährdet Niederlassungen, Fahrzeuge und Lagerbestände.",
"selectionBlocked": "Neue Ausbauten sind im Schuldturm gesperrt."
},
"currentCertificate": "Derzeitiges Zertifikat",
"tabs": {
"director": "Direktor",
@@ -483,6 +493,10 @@
},
"family": {
"title": "Familie",
"debtorsPrison": {
"familyWarning": "Anhaltender Kreditverzug belastet Ehe, Haushalt und Liebschaften.",
"familyImpact": "Der Schuldturm schadet Ehe, Hausfrieden und der Stabilität von Liebschaften."
},
"spouse": {
"title": "Beziehung",
"name": "Name",
@@ -845,6 +859,10 @@
},
"house": {
"title": "Haus",
"debtorsPrison": {
"houseWarning": "Mit wachsendem Kreditverzug steigt das Risiko für Pfändung und erzwungenen Hausverlust.",
"houseRisk": "Dein Haus ist jetzt Teil der möglichen Zwangsverwertung."
},
"statusreport": "Zustand des Hauses",
"element": "Bereich",
"state": "Zustand",
@@ -1137,6 +1155,23 @@
"maxCredit": "Maximaler Kredit",
"availableCredit": "Verfügbarer Kredit"
},
"debtorsPrison": {
"titleWarning": "Kreditverzug",
"titlePrison": "Schuldturm",
"descriptionWarning": "Deine Kredite sind im Verzug. Wenn du weiter nicht bedienst, drohen Zwangsmaßnahmen.",
"descriptionPrison": "Du sitzt im Schuldturm. Neue Kredite sind gesperrt und dein Vermögen wird schrittweise verwertet.",
"daysOverdue": "Verzugstage",
"creditworthiness": "Kreditwürdigkeit",
"nextForcedAction": "Nächste Zwangsmaßnahme",
"creditBlocked": "Im Schuldturm kannst du keine neuen Kredite aufnehmen.",
"creditError": "Der Kredit konnte nicht aufgenommen werden.",
"actions": {
"reminder": "Erste Mahnung",
"final_warning": "Letzte Mahnung",
"debtors_prison": "Einweisung in den Schuldturm",
"asset_seizure": "Pfändung von Vermögen"
}
},
"credits": {
"title": "Kredite",
"none": "Derzeit hast Du keinen Kredit aufgenommen.",

View File

@@ -10,6 +10,11 @@
"windy": "Windy",
"clear": "Clear"
},
"debtorsPrison": {
"actionBlocked": "This action is blocked while you are in debtors' prison.",
"globalWarning": "Your credit delinquency is already restricting your actions. Forced measures may follow soon.",
"globalLocked": "You are in debtors' prison. Almost all active Falukant actions are currently blocked."
},
"messages": {
"title": "Messages",
"tooltip": "Messages",
@@ -184,6 +189,10 @@
},
"house": {
"title": "House",
"debtorsPrison": {
"houseWarning": "As delinquency grows, the risk of seizure and forced loss of the house increases.",
"houseRisk": "Your house is now part of the possible forced liquidation."
},
"statusreport": "House condition",
"element": "Element",
"state": "Condition",
@@ -265,6 +274,11 @@
"noProposals": "No director candidates available."
},
"branch": {
"debtorsPrison": {
"branchLocked": "While in debtors' prison, new economic steps are blocked. Closed or seized branches will also become visible here.",
"branchRisk": "Your delinquency puts branches, vehicles and stored goods at risk.",
"selectionBlocked": "New expansions are blocked while imprisoned for debt."
},
"currentCertificate": "Current certificate",
"selection": {
"title": "Branch Selection",
@@ -505,6 +519,10 @@
}
},
"family": {
"debtorsPrison": {
"familyWarning": "Ongoing debt delinquency puts strain on marriage, household and affairs.",
"familyImpact": "Debtors' prison damages marriage, household peace and the stability of affairs."
},
"children": {
"title": "Children",
"name": "Name",
@@ -736,6 +754,55 @@
"error": "The child could not be baptized."
}
},
"bank": {
"title": "Bank",
"account": {
"title": "Account",
"balance": "Balance",
"totalDebt": "Total debt",
"maxCredit": "Maximum credit",
"availableCredit": "Available credit"
},
"debtorsPrison": {
"titleWarning": "Credit delinquency",
"titlePrison": "Debtors' prison",
"descriptionWarning": "Your credits are overdue. If you continue to default, forced measures will follow.",
"descriptionPrison": "You are in debtors' prison. New credits are blocked and your assets will be liquidated step by step.",
"daysOverdue": "Days overdue",
"creditworthiness": "Creditworthiness",
"nextForcedAction": "Next forced action",
"creditBlocked": "You cannot take new credits while imprisoned for debt.",
"creditError": "The credit could not be taken.",
"actions": {
"reminder": "First reminder",
"final_warning": "Final warning",
"debtors_prison": "Commitment to debtors' prison",
"asset_seizure": "Asset seizure"
}
},
"credits": {
"title": "Credits",
"none": "You currently do not have any credits.",
"amount": "Amount",
"remaining": "Remaining",
"interestRate": "Interest rate",
"table": {
"name": "Name",
"amount": "Amount",
"reason": "Reason",
"date": "Date"
},
"payoff": {
"title": "Take a new credit",
"height": "Credit amount",
"remaining": "Remaining possible credit amount",
"fee": "Credit interest",
"feeHeight": "Installment (10 payments)",
"total": "Total",
"confirm": "Take credit"
}
}
},
"reputation": {
"title": "Reputation",
"overview": {

View File

@@ -24,6 +24,11 @@
"children": "Hijos",
"children_unbaptised": "hijos no bautizados"
},
"debtorsPrison": {
"actionBlocked": "Esta acción está bloqueada mientras estés en la prisión por deudas.",
"globalWarning": "Tu mora crediticia ya restringe tus acciones. Pronto pueden llegar medidas forzosas.",
"globalLocked": "Estás en la prisión por deudas. Casi todas las acciones activas de Falukant están actualmente bloqueadas."
},
"messages": {
"title": "Mensajes",
"tooltip": "Mensajes",
@@ -208,6 +213,11 @@
},
"branch": {
"title": "Sucursal",
"debtorsPrison": {
"branchLocked": "En la prisión por deudas se bloquean los nuevos pasos económicos. Las sucursales cerradas o embargadas también se reflejarán aquí.",
"branchRisk": "Tu mora pone en peligro sucursales, vehículos y mercancías almacenadas.",
"selectionBlocked": "Las nuevas ampliaciones están bloqueadas en la prisión por deudas."
},
"currentCertificate": "Certificado actual",
"tabs": {
"director": "Director",
@@ -467,6 +477,10 @@
},
"family": {
"title": "Familia",
"debtorsPrison": {
"familyWarning": "La mora continuada perjudica el matrimonio, el hogar y las relaciones.",
"familyImpact": "La prisión por deudas daña el matrimonio, la paz del hogar y la estabilidad de las relaciones."
},
"spouse": {
"title": "Relación",
"name": "Nombre",
@@ -811,6 +825,10 @@
},
"house": {
"title": "Casa",
"debtorsPrison": {
"houseWarning": "A medida que aumenta la mora, crece el riesgo de embargo y pérdida forzosa de la casa.",
"houseRisk": "Tu casa forma ahora parte de la posible liquidación forzosa."
},
"statusreport": "Estado de la casa",
"element": "Elemento",
"state": "Estado",
@@ -1070,6 +1088,23 @@
"maxCredit": "Crédito máximo",
"availableCredit": "Crédito disponible"
},
"debtorsPrison": {
"titleWarning": "Mora crediticia",
"titlePrison": "Prisión por deudas",
"descriptionWarning": "Tus créditos están en mora. Si sigues sin pagar, te amenazan medidas forzosas.",
"descriptionPrison": "Estás en la prisión por deudas. Los nuevos créditos están bloqueados y tu patrimonio se liquidará gradualmente.",
"daysOverdue": "Días de retraso",
"creditworthiness": "Solvencia crediticia",
"nextForcedAction": "Siguiente medida forzosa",
"creditBlocked": "No puedes solicitar nuevos créditos mientras estés en la prisión por deudas.",
"creditError": "No se pudo solicitar el crédito.",
"actions": {
"reminder": "Primer aviso",
"final_warning": "Último aviso",
"debtors_prison": "Ingreso en prisión por deudas",
"asset_seizure": "Embargo de bienes"
}
},
"credits": {
"title": "Créditos",
"none": "Actualmente no tienes ningún crédito.",

View File

@@ -8,6 +8,18 @@
<!-- OVERVIEW -->
<div v-if="activeTab === 'account'">
<div v-if="debtorsPrison.active" class="debt-status" :class="{ 'is-prison': debtorsPrison.inDebtorsPrison }">
<h3>{{ debtStatusTitle }}</h3>
<p>{{ debtStatusDescription }}</p>
<div class="debt-status__meta">
<span>{{ $t('falukant.bank.debtorsPrison.daysOverdue') }}: <strong>{{ debtorsPrison.daysOverdue }}</strong></span>
<span>{{ $t('falukant.bank.debtorsPrison.creditworthiness') }}: <strong>{{ debtorsPrison.creditworthiness }}</strong></span>
<span v-if="debtorsPrison.nextForcedAction">
{{ $t('falukant.bank.debtorsPrison.nextForcedAction') }}:
<strong>{{ $t(`falukant.bank.debtorsPrison.actions.${debtorsPrison.nextForcedAction}`) }}</strong>
</span>
</div>
</div>
<div class="account-section">
<table>
<tr>
@@ -26,6 +38,10 @@
<td>{{ $t('falukant.bank.account.availableCredit') }}</td>
<td>{{ formatCost(bankOverview.availableCredit) }}</td>
</tr>
<tr>
<td>{{ $t('falukant.bank.debtorsPrison.creditworthiness') }}</td>
<td>{{ bankOverview.creditworthiness }}</td>
</tr>
</table>
</div>
</div>
@@ -77,9 +93,12 @@
<p>
<strong>{{ $t('falukant.bank.credits.payoff.total') }}: {{ formatCost(creditCost()) }}</strong>
</p>
<button @click="confirmPayoff" class="button" :disabled="!selectedCredit">
<button @click="confirmPayoff" class="button" :disabled="!selectedCredit || isCreditBlocked">
{{ $t('falukant.bank.credits.payoff.confirm') }}
</button>
<p v-if="isCreditBlocked" class="payoff-hint payoff-hint--error">
{{ $t('falukant.bank.debtorsPrison.creditBlocked') }}
</p>
</div>
</div>
</div>
@@ -93,6 +112,7 @@ import StatusBar from '@/components/falukant/StatusBar.vue';
import SimpleTabs from '@/components/SimpleTabs.vue';
import apiClient from '@/utils/axios.js';
import { mapState } from 'vuex';
import { showError } from '@/utils/feedback.js';
export default {
name: 'BankView',
@@ -118,34 +138,114 @@ export default {
};
},
computed: {
...mapState(['socket'])
...mapState(['socket', 'daemonSocket', 'user']),
debtorsPrison() {
return this.bankOverview.debtorsPrison || {
active: false,
inDebtorsPrison: false,
daysOverdue: 0,
creditworthiness: 100,
nextForcedAction: null
};
},
isCreditBlocked() {
return this.debtorsPrison.inDebtorsPrison;
},
debtStatusTitle() {
return this.debtorsPrison.inDebtorsPrison
? this.$t('falukant.bank.debtorsPrison.titlePrison')
: this.$t('falukant.bank.debtorsPrison.titleWarning');
},
debtStatusDescription() {
return this.debtorsPrison.inDebtorsPrison
? this.$t('falukant.bank.debtorsPrison.descriptionPrison')
: this.$t('falukant.bank.debtorsPrison.descriptionWarning');
}
},
watch: {
socket(newVal, oldVal) {
if (oldVal) this.teardownSocketEvents();
if (newVal) this.setupSocketEvents();
},
daemonSocket(newVal, oldVal) {
if (oldVal) this.teardownSocketEvents();
if (newVal) this.setupSocketEvents();
}
},
async mounted() {
await this.loadBankOverview();
this.setupSocketEvents();
},
beforeUnmount() {
if (this.socket) {
this.socket.off('falukantUpdateStatus', this.loadBankOverview);
if (this._pendingRefresh) {
clearTimeout(this._pendingRefresh);
this._pendingRefresh = null;
}
this.teardownSocketEvents();
},
methods: {
matchesCurrentUser(eventData) {
if (eventData?.user_id == null) {
return true;
}
const currentIds = [this.user?.id, this.user?.hashedId]
.filter(Boolean)
.map((value) => String(value));
return currentIds.includes(String(eventData.user_id));
},
setupSocketEvents() {
this.teardownSocketEvents();
if (this.socket) {
this.socket.on('falukantUpdateStatus', (data) => {
this.handleEvent({ event: 'falukantUpdateStatus', ...data });
});
} else {
setTimeout(() => this.setupSocketEvents(), 1000);
this._statusSocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateStatus', ...data });
this._familySocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateFamily', ...data });
this._debtSocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateDebt', ...data });
this.socket.on('falukantUpdateStatus', this._statusSocketHandler);
this.socket.on('falukantUpdateFamily', this._familySocketHandler);
this.socket.on('falukantUpdateDebt', this._debtSocketHandler);
}
if (this.daemonSocket) {
this._daemonHandler = (event) => this.handleDaemonMessage(event);
this.daemonSocket.addEventListener('message', this._daemonHandler);
}
},
teardownSocketEvents() {
if (this.socket) {
if (this._statusSocketHandler) this.socket.off('falukantUpdateStatus', this._statusSocketHandler);
if (this._familySocketHandler) this.socket.off('falukantUpdateFamily', this._familySocketHandler);
if (this._debtSocketHandler) this.socket.off('falukantUpdateDebt', this._debtSocketHandler);
}
if (this.daemonSocket && this._daemonHandler) {
this.daemonSocket.removeEventListener('message', this._daemonHandler);
this._daemonHandler = null;
}
},
handleEvent(eventData) {
if (!this.matchesCurrentUser(eventData)) {
return;
}
switch (eventData.event) {
case 'falukantUpdateStatus':
this.loadBankOverview();
this.queueBankRefresh();
break;
case 'falukantUpdateDebt':
this.queueBankRefresh();
break;
case 'falukantUpdateFamily':
if (['monthly', 'lover_installment'].includes(eventData.reason)) {
this.queueBankRefresh();
}
break;
}
},
queueBankRefresh() {
if (this._pendingRefresh) {
clearTimeout(this._pendingRefresh);
}
this._pendingRefresh = setTimeout(() => {
this._pendingRefresh = null;
this.loadBankOverview();
}, 120);
},
async loadBankOverview() {
try {
const { data } = await apiClient.get('/api/falukant/bank/overview');
@@ -155,6 +255,7 @@ export default {
}
},
async confirmPayoff() {
if (this.isCreditBlocked) return;
try {
await apiClient.post('/api/falukant/bank/credits', {
height: this.selectedCredit
@@ -163,16 +264,17 @@ export default {
this.selectedCredit = null;
this.activeTab = 'credits';
} catch (err) {
console.error(err);
showError(err.response?.data?.error || this.$t('falukant.bank.debtorsPrison.creditError'));
}
},
handleDaemonMessage(msg) {
try {
if (['falukantUpdateStatus', 'moneyChange', 'creditChange'].includes(msg.event)) {
this.loadBankOverview();
const data = JSON.parse(msg.data);
if (['falukantUpdateStatus', 'falukantUpdateDebt'].includes(data.event)) {
this.handleEvent(data);
}
} catch (err) {
console.error(evt, err);
console.error(err);
}
},
feeRate() {
@@ -190,4 +292,38 @@ export default {
<style scoped lang="scss">
h2 { padding-top: 20px; }
.debt-status {
margin-bottom: 1rem;
padding: 1rem 1.1rem;
border-radius: var(--radius-md);
border: 1px solid rgba(180, 120, 40, 0.35);
background: linear-gradient(180deg, rgba(255, 244, 223, 0.95), rgba(255, 249, 238, 0.98));
}
.debt-status.is-prison {
border-color: rgba(146, 57, 40, 0.45);
background: linear-gradient(180deg, rgba(255, 232, 225, 0.96), rgba(255, 245, 241, 0.98));
}
.debt-status h3 {
margin-bottom: 0.35rem;
}
.debt-status__meta {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-top: 0.75rem;
font-size: 0.95rem;
}
.payoff-hint {
margin-top: 0.75rem;
}
.payoff-hint--error {
color: #8b2f23;
font-weight: 600;
}
</style>

View File

@@ -15,9 +15,28 @@
</div>
</section>
<section
v-if="debtorsPrison.active"
class="branch-debt-warning surface-card"
:class="{ 'is-prison': debtorsPrison.inDebtorsPrison }"
>
<strong>
{{ debtorsPrison.inDebtorsPrison
? $t('falukant.bank.debtorsPrison.titlePrison')
: $t('falukant.bank.debtorsPrison.titleWarning') }}
</strong>
<p>
{{ debtorsPrison.inDebtorsPrison
? $t('falukant.branch.debtorsPrison.branchLocked')
: $t('falukant.branch.debtorsPrison.branchRisk') }}
</p>
</section>
<BranchSelection
:branches="branches"
:selectedBranch="selectedBranch"
:blocked="debtorsPrison.inDebtorsPrison"
:blocked-reason="debtorsPrison.inDebtorsPrison ? $t('falukant.branch.debtorsPrison.selectionBlocked') : ''"
@branchSelected="onBranchSelected"
@createBranch="createBranch"
@upgradeBranch="upgradeBranch"
@@ -404,6 +423,10 @@ export default {
branchTaxesLoading: false,
branchTaxesError: null,
currentCertificate: null,
debtorsPrison: {
active: false,
inDebtorsPrison: false
},
pendingBranchRefresh: null,
};
},
@@ -462,6 +485,7 @@ export default {
// Live-Socket-Events (Backend Socket.io)
if (this.socket) {
this.socket.on('falukantUpdateStatus', (data) => this.handleEvent({ event: 'falukantUpdateStatus', ...data }));
this.socket.on('falukantUpdateDebt', (data) => this.handleEvent({ event: 'falukantUpdateDebt', ...data }));
this.socket.on('falukantUpdateProductionCertificate', (data) => this.handleEvent({ event: 'falukantUpdateProductionCertificate', ...data }));
this.socket.on('falukantBranchUpdate', (data) => this.handleEvent({ event: 'falukantBranchUpdate', ...data }));
this.socket.on('transport_arrived', (data) => this.handleEvent({ event: 'transport_arrived', ...data }));
@@ -482,6 +506,7 @@ export default {
}
if (this.socket) {
this.socket.off('falukantUpdateStatus');
this.socket.off('falukantUpdateDebt');
this.socket.off('falukantUpdateProductionCertificate');
this.socket.off('falukantBranchUpdate');
this.socket.off('transport_arrived');
@@ -558,6 +583,10 @@ export default {
try {
const result = await apiClient.get('/api/falukant/user');
this.currentCertificate = result.data?.certificate ?? null;
this.debtorsPrison = result.data?.debtorsPrison || {
active: false,
inDebtorsPrison: false
};
} catch (error) {
console.error('Error loading certificate:', error);
}
@@ -678,6 +707,10 @@ export default {
},
async createBranch() {
if (this.debtorsPrison.inDebtorsPrison) {
showError(this, this.$t('falukant.branch.debtorsPrison.selectionBlocked'));
return;
}
await this.loadBranches();
// Nach dem Anlegen eines neuen Branches automatisch den
// zuletzt/neu erstellten Branch auswählen.
@@ -694,6 +727,10 @@ export default {
async upgradeBranch() {
if (!this.selectedBranch) return;
if (this.debtorsPrison.inDebtorsPrison) {
showError(this, this.$t('falukant.branch.debtorsPrison.selectionBlocked'));
return;
}
try {
await apiClient.post('/api/falukant/branches/upgrade', {
branchId: this.selectedBranch.id,
@@ -851,6 +888,7 @@ export default {
this.$refs.productionSection?.loadStorage();
break;
case 'falukantUpdateStatus':
case 'falukantUpdateDebt':
case 'falukantUpdateProductionCertificate':
case 'falukantBranchUpdate':
this.queueBranchRefresh();
@@ -1184,6 +1222,23 @@ export default {
color: var(--color-text-secondary);
}
.branch-debt-warning {
margin-bottom: 16px;
padding: 16px 18px;
border: 1px solid rgba(180, 120, 40, 0.32);
background: linear-gradient(180deg, rgba(255, 244, 223, 0.95), rgba(255, 250, 239, 0.98));
}
.branch-debt-warning.is-prison {
border-color: rgba(146, 57, 40, 0.4);
background: linear-gradient(180deg, rgba(255, 232, 225, 0.96), rgba(255, 245, 241, 0.98));
}
.branch-debt-warning p {
margin: 6px 0 0;
color: var(--color-text-secondary);
}
.branch-hero__meta {
margin-top: 12px;
}

View File

@@ -11,6 +11,23 @@
</div>
</section>
<section
v-if="debtorsPrison.active"
class="family-debt-warning surface-card"
:class="{ 'is-prison': debtorsPrison.inDebtorsPrison }"
>
<strong>
{{ debtorsPrison.inDebtorsPrison
? $t('falukant.bank.debtorsPrison.titlePrison')
: $t('falukant.bank.debtorsPrison.titleWarning') }}
</strong>
<p>
{{ debtorsPrison.inDebtorsPrison
? $t('falukant.family.debtorsPrison.familyImpact')
: $t('falukant.family.debtorsPrison.familyWarning') }}
</p>
</section>
<div class="spouse-section">
<h3>{{ $t('falukant.family.spouse.title') }}</h3>
<div v-if="relationships.length > 0" class="relationship-container">
@@ -386,6 +403,10 @@ export default {
householdTension: null,
householdTensionScore: null,
householdTensionReasons: [],
debtorsPrison: {
active: false,
inDebtorsPrison: false
},
selectedChild: null,
pendingFamilyRefresh: null
}
@@ -426,11 +447,13 @@ export default {
if (this.socket) {
this._falukantUpdateStatusHandler = (data) => this.handleEvent({ event: 'falukantUpdateStatus', ...data });
this._falukantUpdateFamilyHandler = (data) => this.handleEvent({ event: 'falukantUpdateFamily', ...data });
this._falukantUpdateDebtHandler = (data) => this.handleEvent({ event: 'falukantUpdateDebt', ...data });
this._childrenUpdateHandler = (data) => this.handleEvent({ event: 'children_update', ...data });
this._familyChangedHandler = (data) => this.handleEvent({ event: 'familychanged', ...data });
this.socket.on('falukantUpdateStatus', this._falukantUpdateStatusHandler);
this.socket.on('falukantUpdateFamily', this._falukantUpdateFamilyHandler);
this.socket.on('falukantUpdateDebt', this._falukantUpdateDebtHandler);
this.socket.on('children_update', this._childrenUpdateHandler);
this.socket.on('familychanged', this._familyChangedHandler);
} else {
@@ -441,6 +464,7 @@ export default {
if (!this.socket) return;
if (this._falukantUpdateStatusHandler) this.socket.off('falukantUpdateStatus', this._falukantUpdateStatusHandler);
if (this._falukantUpdateFamilyHandler) this.socket.off('falukantUpdateFamily', this._falukantUpdateFamilyHandler);
if (this._falukantUpdateDebtHandler) this.socket.off('falukantUpdateDebt', this._falukantUpdateDebtHandler);
if (this._childrenUpdateHandler) this.socket.off('children_update', this._childrenUpdateHandler);
if (this._familyChangedHandler) this.socket.off('familychanged', this._familyChangedHandler);
},
@@ -454,6 +478,7 @@ export default {
if ([
'falukantUpdateStatus',
'falukantUpdateFamily',
'falukantUpdateDebt',
'children_update',
'falukantUpdateChurch',
'familychanged',
@@ -502,6 +527,7 @@ export default {
switch (eventData.event) {
case 'falukantUpdateStatus':
case 'falukantUpdateDebt':
case 'familychanged':
this.queueFamilyRefresh({ reloadCharacter: true });
break;
@@ -533,6 +559,10 @@ export default {
this.householdTension = response.data.householdTension;
this.householdTensionScore = response.data.householdTensionScore;
this.householdTensionReasons = response.data.householdTensionReasons || [];
this.debtorsPrison = response.data.debtorsPrison || {
active: false,
inDebtorsPrison: false
};
} catch (error) {
console.error('Error loading family data:', error);
}
@@ -874,6 +904,23 @@ export default {
color: var(--color-text-secondary);
}
.family-debt-warning {
margin-bottom: 16px;
padding: 16px 18px;
border: 1px solid rgba(180, 120, 40, 0.32);
background: linear-gradient(180deg, rgba(255, 244, 223, 0.95), rgba(255, 250, 239, 0.98));
}
.family-debt-warning.is-prison {
border-color: rgba(146, 57, 40, 0.4);
background: linear-gradient(180deg, rgba(255, 232, 225, 0.96), rgba(255, 245, 241, 0.98));
}
.family-debt-warning p {
margin: 6px 0 0;
color: var(--color-text-secondary);
}
.marriage-overview {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));

View File

@@ -2,6 +2,22 @@
<div class="house-view">
<StatusBar />
<h2>{{ $t('falukant.house.title') }}</h2>
<section
v-if="debtorsPrison.active"
class="house-debt-warning surface-card"
:class="{ 'is-prison': debtorsPrison.inDebtorsPrison }"
>
<strong>
{{ debtorsPrison.inDebtorsPrison
? $t('falukant.bank.debtorsPrison.titlePrison')
: $t('falukant.bank.debtorsPrison.titleWarning') }}
</strong>
<p>
{{ debtorsPrison.inDebtorsPrison
? $t('falukant.house.debtorsPrison.houseRisk')
: $t('falukant.house.debtorsPrison.houseWarning') }}
</p>
</section>
<div class="existing-house">
<div :style="houseType ? houseStyle(houseType.position, 341) : {}" class="house"></div>
<div class="status-panel surface-card">
@@ -127,7 +143,7 @@
<div class="buyable-house-price">
{{ $t('falukant.house.price') }}: {{ buyCost(house) }}
</div>
<button @click="buyHouse(house.id)">
<button @click="buyHouse(house.id)" :disabled="debtorsPrison.inDebtorsPrison">
{{ $t('falukant.house.buy') }}
</button>
</div>
@@ -161,11 +177,15 @@ export default {
servantPayLevel: 'normal',
servantPayOptions: ['low', 'normal', 'high'],
buyableHouses: [],
currency: '€'
currency: '€',
debtorsPrison: {
active: false,
inDebtorsPrison: false
}
};
},
computed: {
...mapState(['socket']),
...mapState(['socket', 'daemonSocket', 'user']),
allRenovated() {
return Object.values(this.status).every(v => v >= 100);
}
@@ -176,6 +196,10 @@ export default {
const userRes = await apiClient.get('/api/falukant/houses');
this.userHouse = userRes.data;
this.houseType = this.userHouse.houseType;
this.debtorsPrison = this.userHouse.debtorsPrison || {
active: false,
inDebtorsPrison: false
};
const { roofCondition, wallCondition, floorCondition, windowCondition } = this.userHouse;
this.status = { roofCondition, wallCondition, floorCondition, windowCondition };
this.servantSummary = this.userHouse.servantSummary || this.servantSummary;
@@ -327,14 +351,27 @@ export default {
handleDaemonMessage(evt) {
try {
const msg = JSON.parse(evt.data);
if (msg.event === 'houseupdated') this.loadData();
if (!this.matchesCurrentUser(msg)) return;
if (['houseupdated', 'falukantUpdateStatus', 'falukantUpdateDebt', 'falukantHouseUpdate'].includes(msg.event)) this.loadData();
} catch { }
},
matchesCurrentUser(eventData) {
if (eventData?.user_id == null) {
return true;
}
const currentIds = [this.user?.id, this.user?.hashedId]
.filter(Boolean)
.map((value) => String(value));
return currentIds.includes(String(eventData.user_id));
},
setupSocketEvents() {
if (this.socket) {
this.socket.on('falukantHouseUpdate', (data) => {
this.handleEvent({ event: 'falukantHouseUpdate', ...data });
});
this.socket.on('falukantUpdateDebt', (data) => {
this.handleEvent({ event: 'falukantUpdateDebt', ...data });
});
this.socket.on('falukantUpdateStatus', (data) => {
this.handleEvent({ event: 'falukantUpdateStatus', ...data });
});
@@ -345,6 +382,7 @@ export default {
handleEvent(eventData) {
switch (eventData.event) {
case 'falukantUpdateStatus':
case 'falukantUpdateDebt':
case 'falukantHouseUpdate':
this.loadData();
break;
@@ -354,10 +392,17 @@ export default {
async mounted() {
await this.loadData();
this.setupSocketEvents();
if (this.daemonSocket) {
this.daemonSocket.addEventListener('message', this.handleDaemonMessage);
}
},
beforeUnmount() {
if (this.daemonSocket) {
this.daemonSocket.removeEventListener('message', this.handleDaemonMessage);
}
if (this.socket) {
this.socket.off('falukantHouseUpdate', this.loadData);
this.socket.off('falukantUpdateDebt', this.loadData);
this.socket.off('falukantUpdateStatus', this.loadData);
}
}
@@ -387,6 +432,22 @@ h2 {
margin: 0 0 10px;
}
.house-debt-warning {
padding: 16px 18px;
border: 1px solid rgba(180, 120, 40, 0.32);
background: linear-gradient(180deg, rgba(255, 244, 223, 0.95), rgba(255, 250, 239, 0.98));
}
.house-debt-warning.is-prison {
border-color: rgba(146, 57, 40, 0.4);
background: linear-gradient(180deg, rgba(255, 232, 225, 0.96), rgba(255, 245, 241, 0.98));
}
.house-debt-warning p {
margin: 6px 0 0;
color: var(--color-text-secondary);
}
.existing-house {
display: flex;
gap: 20px;

View File

@@ -9,7 +9,24 @@
</div>
</section>
<div v-if="falukantUser?.character" class="imagecontainer">
<section
v-if="falukantUser?.debtorsPrison?.active"
class="falukant-debt-warning surface-card"
:class="{ 'is-prison': falukantUser?.debtorsPrison?.inDebtorsPrison }"
>
<strong>
{{ falukantUser?.debtorsPrison?.inDebtorsPrison
? $t('falukant.bank.debtorsPrison.titlePrison')
: $t('falukant.bank.debtorsPrison.titleWarning') }}
</strong>
<p>
{{ falukantUser?.debtorsPrison?.inDebtorsPrison
? $t('falukant.bank.debtorsPrison.descriptionPrison')
: $t('falukant.bank.debtorsPrison.descriptionWarning') }}
</p>
</section>
<div v-if="falukantUser?.character && !falukantUser?.debtorsPrison?.inDebtorsPrison" class="imagecontainer">
<div :style="getAvatarStyle" class="avatar"></div>
<div class="house-with-character">
<div :style="getHouseStyle" class="house"></div>
@@ -23,6 +40,16 @@
</div>
</div>
<div v-else-if="falukantUser?.character && falukantUser?.debtorsPrison?.inDebtorsPrison" class="imagecontainer imagecontainer--prison">
<div class="debtors-prison-visual" aria-hidden="true">
<div class="debtors-prison-visual__moon"></div>
<div class="debtors-prison-visual__tower"></div>
<div class="debtors-prison-visual__bars debtors-prison-visual__bars--left"></div>
<div class="debtors-prison-visual__bars debtors-prison-visual__bars--right"></div>
<div class="debtors-prison-visual__ground"></div>
</div>
</div>
<section v-if="falukantUser?.character" class="falukant-summary-grid">
<article class="summary-card surface-card">
<span class="summary-card__label">{{ $t('falukant.overview.metadata.certificate') }}</span>
@@ -44,6 +71,15 @@
<strong>{{ stockEntryCount }}</strong>
<p>Verdichteter Blick auf Warenbestand über alle Regionen.</p>
</article>
<article v-if="falukantUser?.debtorsPrison?.active" class="summary-card surface-card">
<span class="summary-card__label">{{ $t('falukant.bank.debtorsPrison.creditworthiness') }}</span>
<strong>{{ falukantUser.debtorsPrison.creditworthiness }}</strong>
<p>
{{ falukantUser.debtorsPrison.nextForcedAction
? $t(`falukant.bank.debtorsPrison.actions.${falukantUser.debtorsPrison.nextForcedAction}`)
: $t('falukant.bank.debtorsPrison.titleWarning') }}
</p>
</article>
</section>
<section v-if="falukantUser?.character" class="falukant-routine-grid">
@@ -357,6 +393,7 @@ export default {
this.socket.off("falukantUpdateStatus");
this.socket.off("falukantUpdateFamily");
this.socket.off("falukantUpdateChurch");
this.socket.off("falukantUpdateDebt");
this.socket.off("falukantUpdateProductionCertificate");
this.socket.off("children_update");
this.socket.off("falukantBranchUpdate");
@@ -376,6 +413,9 @@ export default {
this.socket.on("falukantUpdateChurch", (data) => {
this.handleEvent({ event: 'falukantUpdateChurch', ...data });
});
this.socket.on("falukantUpdateDebt", (data) => {
this.handleEvent({ event: 'falukantUpdateDebt', ...data });
});
this.socket.on("falukantUpdateProductionCertificate", (data) => {
this.handleEvent({ event: 'falukantUpdateProductionCertificate', ...data });
});
@@ -446,6 +486,7 @@ export default {
case 'falukantUpdateStatus':
case 'falukantUpdateFamily':
case 'falukantUpdateChurch':
case 'falukantUpdateDebt':
case 'falukantUpdateProductionCertificate':
case 'children_update':
case 'falukantBranchUpdate':
@@ -579,6 +620,23 @@ export default {
color: var(--color-text-secondary);
}
.falukant-debt-warning {
margin-bottom: 16px;
padding: 16px 18px;
border: 1px solid rgba(180, 120, 40, 0.32);
background: linear-gradient(180deg, rgba(255, 244, 223, 0.95), rgba(255, 250, 239, 0.98));
}
.falukant-debt-warning.is-prison {
border-color: rgba(146, 57, 40, 0.4);
background: linear-gradient(180deg, rgba(255, 232, 225, 0.96), rgba(255, 245, 241, 0.98));
}
.falukant-debt-warning p {
margin: 6px 0 0;
color: var(--color-text-secondary);
}
.falukant-summary-grid,
.falukant-routine-grid {
display: grid;
@@ -694,6 +752,101 @@ export default {
z-index: 0;
}
.imagecontainer--prison {
min-height: 320px;
}
.debtors-prison-visual {
position: relative;
width: min(100%, 540px);
height: 320px;
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
overflow: hidden;
background:
radial-gradient(circle at 18% 22%, rgba(255, 233, 167, 0.95) 0, rgba(255, 233, 167, 0.95) 10%, rgba(255, 233, 167, 0) 11%),
linear-gradient(180deg, #273149 0%, #31476b 42%, #6d5953 42%, #6d5953 100%);
box-shadow: var(--shadow-soft);
}
.debtors-prison-visual__moon {
position: absolute;
top: 42px;
left: 72px;
width: 38px;
height: 38px;
border-radius: 50%;
background: rgba(255, 241, 194, 0.9);
box-shadow: 0 0 24px rgba(255, 241, 194, 0.5);
}
.debtors-prison-visual__tower {
position: absolute;
left: 50%;
bottom: 54px;
width: 160px;
height: 190px;
transform: translateX(-50%);
border-radius: 18px 18px 10px 10px;
background:
linear-gradient(180deg, #8c8a86 0%, #6f6a64 100%);
box-shadow: inset 0 0 0 2px rgba(53, 49, 45, 0.18);
}
.debtors-prison-visual__tower::before {
content: '';
position: absolute;
top: -26px;
left: 18px;
width: 124px;
height: 34px;
border-radius: 10px 10px 0 0;
background:
repeating-linear-gradient(90deg, #7d786f 0, #7d786f 18px, #646057 18px, #646057 26px);
}
.debtors-prison-visual__tower::after {
content: '';
position: absolute;
left: 50%;
bottom: 0;
width: 42px;
height: 88px;
transform: translateX(-50%);
border-radius: 18px 18px 0 0;
background: #40362f;
box-shadow: inset 0 0 0 2px rgba(17, 13, 11, 0.22);
}
.debtors-prison-visual__bars {
position: absolute;
top: 116px;
width: 34px;
height: 54px;
border-radius: 8px;
background:
repeating-linear-gradient(90deg, rgba(33, 31, 29, 0.85) 0, rgba(33, 31, 29, 0.85) 5px, transparent 5px, transparent 11px);
}
.debtors-prison-visual__bars--left {
left: calc(50% - 54px);
}
.debtors-prison-visual__bars--right {
right: calc(50% - 54px);
}
.debtors-prison-visual__ground {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 64px;
background:
linear-gradient(180deg, rgba(72, 57, 51, 0.2), rgba(72, 57, 51, 0.42)),
repeating-linear-gradient(90deg, #756357 0, #756357 18px, #6d5a4f 18px, #6d5a4f 30px);
}
.avatar {
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);