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:
@@ -29,30 +29,30 @@ class FalukantController {
|
||||
// Dashboard widget: originaler Endpoint (siehe Commit 62d8cd7)
|
||||
this.getDashboardWidget = this._wrapWithUser((userId) => this.service.getDashboardWidget(userId));
|
||||
this.getBranches = this._wrapWithUser((userId) => this.service.getBranches(userId));
|
||||
this.createBranch = this._wrapWithUser((userId, req) => this.service.createBranch(userId, req.body.cityId, req.body.branchTypeId));
|
||||
this.createBranch = this._wrapWithUser((userId, req) => this.service.createBranch(userId, req.body.cityId, req.body.branchTypeId), { blockInDebtorsPrison: true });
|
||||
this.getBranchTypes = this._wrapWithUser((userId) => this.service.getBranchTypes(userId));
|
||||
this.getBranch = this._wrapWithUser((userId, req) => this.service.getBranch(userId, req.params.branch));
|
||||
this.upgradeBranch = this._wrapWithUser((userId, req) => this.service.upgradeBranch(userId, req.body.branchId));
|
||||
this.upgradeBranch = this._wrapWithUser((userId, req) => this.service.upgradeBranch(userId, req.body.branchId), { blockInDebtorsPrison: true });
|
||||
this.createProduction = this._wrapWithUser((userId, req) => {
|
||||
const { branchId, productId, quantity } = req.body;
|
||||
return this.service.createProduction(userId, branchId, productId, quantity);
|
||||
}, { successStatus: 201 });
|
||||
}, { successStatus: 201, blockInDebtorsPrison: true });
|
||||
this.getProduction = this._wrapWithUser((userId, req) => this.service.getProduction(userId, req.params.branchId));
|
||||
this.getStock = this._wrapWithUser((userId, req) => this.service.getStock(userId, req.params.branchId || null));
|
||||
this.createStock = this._wrapWithUser((userId, req) => {
|
||||
const { branchId, stockTypeId, stockSize } = req.body;
|
||||
return this.service.createStock(userId, branchId, stockTypeId, stockSize);
|
||||
}, { successStatus: 201 });
|
||||
}, { successStatus: 201, blockInDebtorsPrison: true });
|
||||
this.getProducts = this._wrapWithUser((userId) => this.service.getProducts(userId));
|
||||
this.getInventory = this._wrapWithUser((userId, req) => this.service.getInventory(userId, req.params.branchId));
|
||||
this.sellProduct = this._wrapWithUser((userId, req) => {
|
||||
const { branchId, productId, quality, quantity } = req.body;
|
||||
return this.service.sellProduct(userId, branchId, productId, quality, quantity);
|
||||
}, { successStatus: 201 });
|
||||
}, { successStatus: 201, blockInDebtorsPrison: true });
|
||||
this.sellAllProducts = this._wrapWithUser((userId, req) => {
|
||||
const { branchId } = req.body;
|
||||
return this.service.sellAllProducts(userId, branchId);
|
||||
}, { successStatus: 201 });
|
||||
}, { successStatus: 201, blockInDebtorsPrison: true });
|
||||
this.moneyHistory = this._wrapWithUser((userId, req) => {
|
||||
let { page, filter } = req.body;
|
||||
if (!page) page = 1;
|
||||
@@ -66,11 +66,11 @@ class FalukantController {
|
||||
this.buyStorage = this._wrapWithUser((userId, req) => {
|
||||
const { branchId, amount, stockTypeId } = req.body;
|
||||
return this.service.buyStorage(userId, branchId, amount, stockTypeId);
|
||||
}, { successStatus: 201 });
|
||||
}, { successStatus: 201, blockInDebtorsPrison: true });
|
||||
this.sellStorage = this._wrapWithUser((userId, req) => {
|
||||
const { branchId, amount, stockTypeId } = req.body;
|
||||
return this.service.sellStorage(userId, branchId, amount, stockTypeId);
|
||||
}, { successStatus: 202 });
|
||||
}, { successStatus: 202, blockInDebtorsPrison: true });
|
||||
|
||||
this.getStockTypes = this._wrapSimple(() => this.service.getStockTypes());
|
||||
this.getStockOverview = this._wrapSimple(() => this.service.getStockOverview());
|
||||
@@ -80,18 +80,18 @@ class FalukantController {
|
||||
console.log('🔍 getDirectorProposals called with userId:', userId, 'branchId:', req.body.branchId);
|
||||
return this.service.getDirectorProposals(userId, req.body.branchId);
|
||||
});
|
||||
this.convertProposalToDirector = this._wrapWithUser((userId, req) => this.service.convertProposalToDirector(userId, req.body.proposalId));
|
||||
this.convertProposalToDirector = this._wrapWithUser((userId, req) => this.service.convertProposalToDirector(userId, req.body.proposalId), { blockInDebtorsPrison: true });
|
||||
this.getDirectorForBranch = this._wrapWithUser((userId, req) => this.service.getDirectorForBranch(userId, req.params.branchId));
|
||||
this.getAllDirectors = this._wrapWithUser((userId) => this.service.getAllDirectors(userId));
|
||||
this.updateDirector = this._wrapWithUser((userId, req) => {
|
||||
const { directorId, income } = req.body;
|
||||
return this.service.updateDirector(userId, directorId, income);
|
||||
});
|
||||
}, { blockInDebtorsPrison: true });
|
||||
|
||||
this.setSetting = this._wrapWithUser((userId, req) => {
|
||||
const { branchId, directorId, settingKey, value } = req.body;
|
||||
return this.service.setSetting(userId, branchId, directorId, settingKey, value);
|
||||
});
|
||||
}, { blockInDebtorsPrison: true });
|
||||
|
||||
this.getFamily = this._wrapWithUser(async (userId) => {
|
||||
const result = await this.service.getFamily(userId);
|
||||
@@ -99,9 +99,9 @@ class FalukantController {
|
||||
return result;
|
||||
});
|
||||
this.getPotentialHeirs = this._wrapWithUser((userId) => this.service.getPotentialHeirs(userId));
|
||||
this.selectHeir = this._wrapWithUser((userId, req) => this.service.selectHeir(userId, req.body.heirId));
|
||||
this.setHeir = this._wrapWithUser((userId, req) => this.service.setHeir(userId, req.body.childCharacterId));
|
||||
this.acceptMarriageProposal = this._wrapWithUser((userId, req) => this.service.acceptMarriageProposal(userId, req.body.proposalId));
|
||||
this.selectHeir = this._wrapWithUser((userId, req) => this.service.selectHeir(userId, req.body.heirId), { blockInDebtorsPrison: true });
|
||||
this.setHeir = this._wrapWithUser((userId, req) => this.service.setHeir(userId, req.body.childCharacterId), { blockInDebtorsPrison: true });
|
||||
this.acceptMarriageProposal = this._wrapWithUser((userId, req) => this.service.acceptMarriageProposal(userId, req.body.proposalId), { blockInDebtorsPrison: true });
|
||||
this.cancelWooing = this._wrapWithUser(async (userId) => {
|
||||
try {
|
||||
return await this.service.cancelWooing(userId);
|
||||
@@ -111,25 +111,25 @@ class FalukantController {
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}, { successStatus: 202 });
|
||||
}, { successStatus: 202, blockInDebtorsPrison: true });
|
||||
this.getGifts = this._wrapWithUser((userId) => {
|
||||
console.log('🔍 getGifts called with userId:', userId);
|
||||
return this.service.getGifts(userId);
|
||||
});
|
||||
this.setLoverMaintenance = this._wrapWithUser((userId, req) =>
|
||||
this.service.setLoverMaintenance(userId, req.params.relationshipId, req.body?.maintenanceLevel));
|
||||
this.service.setLoverMaintenance(userId, req.params.relationshipId, req.body?.maintenanceLevel), { blockInDebtorsPrison: true });
|
||||
this.createLoverRelationship = this._wrapWithUser((userId, req) =>
|
||||
this.service.createLoverRelationship(userId, req.body?.targetCharacterId, req.body?.loverRole), { successStatus: 201 });
|
||||
this.service.createLoverRelationship(userId, req.body?.targetCharacterId, req.body?.loverRole), { successStatus: 201, blockInDebtorsPrison: true });
|
||||
this.spendTimeWithSpouse = this._wrapWithUser((userId) =>
|
||||
this.service.spendTimeWithSpouse(userId));
|
||||
this.service.spendTimeWithSpouse(userId), { blockInDebtorsPrison: true });
|
||||
this.giftToSpouse = this._wrapWithUser((userId, req) =>
|
||||
this.service.giftToSpouse(userId, req.body?.giftLevel));
|
||||
this.service.giftToSpouse(userId, req.body?.giftLevel), { blockInDebtorsPrison: true });
|
||||
this.reconcileMarriage = this._wrapWithUser((userId) =>
|
||||
this.service.reconcileMarriage(userId));
|
||||
this.service.reconcileMarriage(userId), { blockInDebtorsPrison: true });
|
||||
this.acknowledgeLover = this._wrapWithUser((userId, req) =>
|
||||
this.service.acknowledgeLover(userId, req.params.relationshipId));
|
||||
this.service.acknowledgeLover(userId, req.params.relationshipId), { blockInDebtorsPrison: true });
|
||||
this.endLoverRelationship = this._wrapWithUser((userId, req) =>
|
||||
this.service.endLoverRelationship(userId, req.params.relationshipId));
|
||||
this.service.endLoverRelationship(userId, req.params.relationshipId), { blockInDebtorsPrison: true });
|
||||
this.getChildren = this._wrapWithUser((userId) => this.service.getChildren(userId));
|
||||
this.sendGift = this._wrapWithUser(async (userId, req) => {
|
||||
try {
|
||||
@@ -140,59 +140,59 @@ class FalukantController {
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}, { blockInDebtorsPrison: true });
|
||||
|
||||
this.getTitlesOfNobility = this._wrapWithUser((userId) => this.service.getTitlesOfNobility(userId));
|
||||
this.getReputationActions = this._wrapWithUser((userId) => this.service.getReputationActions(userId));
|
||||
this.executeReputationAction = this._wrapWithUser((userId, req) =>
|
||||
this.service.executeReputationAction(userId, req.body?.actionTypeId), { successStatus: 201 });
|
||||
this.service.executeReputationAction(userId, req.body?.actionTypeId), { successStatus: 201, blockInDebtorsPrison: true });
|
||||
this.getHouseTypes = this._wrapWithUser((userId) => this.service.getHouseTypes(userId));
|
||||
this.getMoodAffect = this._wrapWithUser((userId) => this.service.getMoodAffect(userId));
|
||||
this.getCharacterAffect = this._wrapWithUser((userId) => this.service.getCharacterAffect(userId));
|
||||
this.getUserHouse = this._wrapWithUser((userId) => this.service.getUserHouse(userId));
|
||||
this.getBuyableHouses = this._wrapWithUser((userId) => this.service.getBuyableHouses(userId));
|
||||
this.buyUserHouse = this._wrapWithUser((userId, req) => this.service.buyUserHouse(userId, req.body.houseId), { successStatus: 201 });
|
||||
this.hireServants = this._wrapWithUser((userId, req) => this.service.hireServants(userId, req.body?.amount), { successStatus: 201 });
|
||||
this.dismissServants = this._wrapWithUser((userId, req) => this.service.dismissServants(userId, req.body?.amount));
|
||||
this.setServantPayLevel = this._wrapWithUser((userId, req) => this.service.setServantPayLevel(userId, req.body?.payLevel));
|
||||
this.tidyHousehold = this._wrapWithUser((userId) => this.service.tidyHousehold(userId));
|
||||
this.buyUserHouse = this._wrapWithUser((userId, req) => this.service.buyUserHouse(userId, req.body.houseId), { successStatus: 201, blockInDebtorsPrison: true });
|
||||
this.hireServants = this._wrapWithUser((userId, req) => this.service.hireServants(userId, req.body?.amount), { successStatus: 201, blockInDebtorsPrison: true });
|
||||
this.dismissServants = this._wrapWithUser((userId, req) => this.service.dismissServants(userId, req.body?.amount), { blockInDebtorsPrison: true });
|
||||
this.setServantPayLevel = this._wrapWithUser((userId, req) => this.service.setServantPayLevel(userId, req.body?.payLevel), { blockInDebtorsPrison: true });
|
||||
this.tidyHousehold = this._wrapWithUser((userId) => this.service.tidyHousehold(userId), { blockInDebtorsPrison: true });
|
||||
|
||||
this.getPartyTypes = this._wrapWithUser((userId) => this.service.getPartyTypes(userId));
|
||||
this.createParty = this._wrapWithUser((userId, req) => {
|
||||
const { partyTypeId, musicId, banquetteId, nobilityIds, servantRatio } = req.body;
|
||||
return this.service.createParty(userId, partyTypeId, musicId, banquetteId, nobilityIds, servantRatio);
|
||||
}, { successStatus: 201 });
|
||||
}, { successStatus: 201, blockInDebtorsPrison: true });
|
||||
this.getParties = this._wrapWithUser((userId) => this.service.getParties(userId));
|
||||
|
||||
this.getNotBaptisedChildren = this._wrapWithUser((userId) => this.service.getNotBaptisedChildren(userId));
|
||||
this.baptise = this._wrapWithUser((userId, req) => {
|
||||
const { characterId: childId, firstName } = req.body;
|
||||
return this.service.baptise(userId, childId, firstName);
|
||||
});
|
||||
}, { blockInDebtorsPrison: true });
|
||||
this.getChurchOverview = this._wrapWithUser((userId) => this.service.getChurchOverview(userId));
|
||||
this.getAvailableChurchPositions = this._wrapWithUser((userId) => this.service.getAvailableChurchPositions(userId));
|
||||
this.getSupervisedApplications = this._wrapWithUser((userId) => this.service.getSupervisedApplications(userId));
|
||||
this.applyForChurchPosition = this._wrapWithUser((userId, req) => {
|
||||
const { officeTypeId, regionId } = req.body;
|
||||
return this.service.applyForChurchPosition(userId, officeTypeId, regionId);
|
||||
});
|
||||
}, { blockInDebtorsPrison: true });
|
||||
this.decideOnChurchApplication = this._wrapWithUser((userId, req) => {
|
||||
const { applicationId, decision } = req.body;
|
||||
return this.service.decideOnChurchApplication(userId, applicationId, decision);
|
||||
});
|
||||
}, { blockInDebtorsPrison: true });
|
||||
|
||||
this.getEducation = this._wrapWithUser((userId) => this.service.getEducation(userId));
|
||||
this.sendToSchool = this._wrapWithUser((userId, req) => {
|
||||
const { item, student, studentId } = req.body;
|
||||
return this.service.sendToSchool(userId, item, student, studentId);
|
||||
});
|
||||
}, { blockInDebtorsPrison: true });
|
||||
|
||||
this.getBankOverview = this._wrapWithUser((userId) => this.service.getBankOverview(userId));
|
||||
this.getBankCredits = this._wrapWithUser((userId) => this.service.getBankCredits(userId));
|
||||
this.takeBankCredits = this._wrapWithUser((userId, req) => this.service.takeBankCredits(userId, req.body.height));
|
||||
this.takeBankCredits = this._wrapWithUser((userId, req) => this.service.takeBankCredits(userId, req.body.height), { blockInDebtorsPrison: true });
|
||||
|
||||
this.getNobility = this._wrapWithUser((userId) => this.service.getNobility(userId));
|
||||
this.advanceNobility = this._wrapWithUser((userId) => this.service.advanceNobility(userId));
|
||||
this.advanceNobility = this._wrapWithUser((userId) => this.service.advanceNobility(userId), { blockInDebtorsPrison: true });
|
||||
|
||||
this.getHealth = this._wrapWithUser((userId) => this.service.getHealth(userId));
|
||||
this.healthActivity = this._wrapWithUser(async (userId, req) => {
|
||||
@@ -204,13 +204,13 @@ class FalukantController {
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}, { blockInDebtorsPrison: true });
|
||||
|
||||
this.getPoliticsOverview = this._wrapWithUser((userId) => this.service.getPoliticsOverview(userId));
|
||||
this.getOpenPolitics = this._wrapWithUser((userId) => this.service.getOpenPolitics(userId));
|
||||
this.getElections = this._wrapWithUser((userId) => this.service.getElections(userId));
|
||||
this.vote = this._wrapWithUser((userId, req) => this.service.vote(userId, req.body.votes));
|
||||
this.applyForElections = this._wrapWithUser((userId, req) => this.service.applyForElections(userId, req.body.electionIds));
|
||||
this.vote = this._wrapWithUser((userId, req) => this.service.vote(userId, req.body.votes), { blockInDebtorsPrison: true });
|
||||
this.applyForElections = this._wrapWithUser((userId, req) => this.service.applyForElections(userId, req.body.electionIds), { blockInDebtorsPrison: true });
|
||||
|
||||
this.getRegions = this._wrapWithUser((userId) => this.service.getRegions(userId));
|
||||
this.getBranchTaxes = this._wrapWithUser((userId, req) => this.service.getBranchTaxes(userId, req.params.branchId));
|
||||
@@ -248,8 +248,8 @@ class FalukantController {
|
||||
})).filter(i => !Number.isNaN(i.productId) && !Number.isNaN(i.currentPrice));
|
||||
return this.service.getProductPricesInCitiesBatch(userId, valid, Number.isNaN(currentRegionId) ? null : currentRegionId);
|
||||
});
|
||||
this.renovate = this._wrapWithUser((userId, req) => this.service.renovate(userId, req.body.element));
|
||||
this.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(userId));
|
||||
this.renovate = this._wrapWithUser((userId, req) => this.service.renovate(userId, req.body.element), { blockInDebtorsPrison: true });
|
||||
this.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(userId), { blockInDebtorsPrison: true });
|
||||
|
||||
this.getUndergroundTypes = this._wrapWithUser((userId) => this.service.getUndergroundTypes(userId));
|
||||
this.getUndergroundActivities = this._wrapWithUser((userId) => this.service.getUndergroundActivities(userId));
|
||||
@@ -277,7 +277,7 @@ class FalukantController {
|
||||
throw { status: 400, message: 'goal is required for corrupt_politician' };
|
||||
}
|
||||
return this.service.createUndergroundActivity(userId, payload);
|
||||
}, { successStatus: 201 });
|
||||
}, { successStatus: 201, blockInDebtorsPrison: true });
|
||||
|
||||
this.getUndergroundAttacks = this._wrapWithUser((userId, req) => {
|
||||
const direction = (req.query.direction || '').toLowerCase();
|
||||
@@ -291,14 +291,14 @@ class FalukantController {
|
||||
this.getVehicleTypes = this._wrapWithUser((userId) => this.service.getVehicleTypes(userId));
|
||||
this.buyVehicles = this._wrapWithUser(
|
||||
(userId, req) => this.service.buyVehicles(userId, req.body),
|
||||
{ successStatus: 201 }
|
||||
{ successStatus: 201, blockInDebtorsPrison: true }
|
||||
);
|
||||
this.getVehicles = this._wrapWithUser(
|
||||
(userId, req) => this.service.getVehicles(userId, req.query.regionId)
|
||||
);
|
||||
this.createTransport = this._wrapWithUser(
|
||||
(userId, req) => this.service.createTransport(userId, req.body),
|
||||
{ successStatus: 201 }
|
||||
{ successStatus: 201, blockInDebtorsPrison: true }
|
||||
);
|
||||
this.getTransportRoute = this._wrapWithUser(
|
||||
(userId, req) => this.service.getTransportRoute(userId, req.query)
|
||||
@@ -308,23 +308,26 @@ class FalukantController {
|
||||
);
|
||||
this.repairVehicle = this._wrapWithUser(
|
||||
(userId, req) => this.service.repairVehicle(userId, req.params.vehicleId),
|
||||
{ successStatus: 200 }
|
||||
{ successStatus: 200, blockInDebtorsPrison: true }
|
||||
);
|
||||
this.repairAllVehicles = this._wrapWithUser(
|
||||
(userId, req) => this.service.repairAllVehicles(userId, req.body.vehicleIds),
|
||||
{ successStatus: 200 }
|
||||
{ successStatus: 200, blockInDebtorsPrison: true }
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
_wrapWithUser(fn, { successStatus = 200, postProcess } = {}) {
|
||||
_wrapWithUser(fn, { successStatus = 200, postProcess, blockInDebtorsPrison = false } = {}) {
|
||||
return async (req, res) => {
|
||||
try {
|
||||
const hashedUserId = extractHashedUserId(req);
|
||||
if (!hashedUserId) {
|
||||
return res.status(400).json({ error: 'Missing user identifier' });
|
||||
}
|
||||
if (blockInDebtorsPrison) {
|
||||
await this.service.assertActionAllowedOutsideDebtorsPrison(hashedUserId);
|
||||
}
|
||||
const result = await fn(hashedUserId, req, res);
|
||||
const toSend = postProcess ? postProcess(result) : result;
|
||||
res.status(successStatus).json(toSend);
|
||||
|
||||
83
backend/migrations/20260323010000-expand-debtors-prism.cjs
Normal file
83
backend/migrations/20260323010000-expand-debtors-prism.cjs
Normal file
@@ -0,0 +1,83 @@
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
const table = { schema: 'falukant_data', tableName: 'debtors_prism' };
|
||||
|
||||
await queryInterface.addColumn(table, 'status', {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'delinquent'
|
||||
});
|
||||
|
||||
await queryInterface.addColumn(table, 'entered_at', {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true
|
||||
});
|
||||
|
||||
await queryInterface.addColumn(table, 'released_at', {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true
|
||||
});
|
||||
|
||||
await queryInterface.addColumn(table, 'debt_at_entry', {
|
||||
type: Sequelize.DECIMAL(14, 2),
|
||||
allowNull: true
|
||||
});
|
||||
|
||||
await queryInterface.addColumn(table, 'remaining_debt', {
|
||||
type: Sequelize.DECIMAL(14, 2),
|
||||
allowNull: true
|
||||
});
|
||||
|
||||
await queryInterface.addColumn(table, 'days_overdue', {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
});
|
||||
|
||||
await queryInterface.addColumn(table, 'reason', {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: true
|
||||
});
|
||||
|
||||
await queryInterface.addColumn(table, 'creditworthiness_penalty', {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
});
|
||||
|
||||
await queryInterface.addColumn(table, 'next_forced_action', {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: true
|
||||
});
|
||||
|
||||
await queryInterface.addColumn(table, 'assets_seized_json', {
|
||||
type: Sequelize.JSONB,
|
||||
allowNull: true
|
||||
});
|
||||
|
||||
await queryInterface.addColumn(table, 'public_known', {
|
||||
type: Sequelize.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
});
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
const table = { schema: 'falukant_data', tableName: 'debtors_prism' };
|
||||
|
||||
await queryInterface.removeColumn(table, 'public_known');
|
||||
await queryInterface.removeColumn(table, 'assets_seized_json');
|
||||
await queryInterface.removeColumn(table, 'next_forced_action');
|
||||
await queryInterface.removeColumn(table, 'creditworthiness_penalty');
|
||||
await queryInterface.removeColumn(table, 'reason');
|
||||
await queryInterface.removeColumn(table, 'days_overdue');
|
||||
await queryInterface.removeColumn(table, 'remaining_debt');
|
||||
await queryInterface.removeColumn(table, 'debt_at_entry');
|
||||
await queryInterface.removeColumn(table, 'released_at');
|
||||
await queryInterface.removeColumn(table, 'entered_at');
|
||||
await queryInterface.removeColumn(table, 'status');
|
||||
}
|
||||
};
|
||||
@@ -7,7 +7,57 @@ DebtorsPrism.init({
|
||||
// Verknüpfung auf FalukantCharacter
|
||||
characterId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false}}, {
|
||||
allowNull: false
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'delinquent'
|
||||
},
|
||||
enteredAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
},
|
||||
releasedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
},
|
||||
debtAtEntry: {
|
||||
type: DataTypes.DECIMAL(14, 2),
|
||||
allowNull: true
|
||||
},
|
||||
remainingDebt: {
|
||||
type: DataTypes.DECIMAL(14, 2),
|
||||
allowNull: true
|
||||
},
|
||||
daysOverdue: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
reason: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
creditworthinessPenalty: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
nextForcedAction: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
assetsSeizedJson: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true
|
||||
},
|
||||
publicKnown: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'DebtorsPrism',
|
||||
tableName: 'debtors_prism',
|
||||
|
||||
@@ -48,6 +48,7 @@ import ChildRelation from '../models/falukant/data/child_relation.js';
|
||||
import Learning from '../models/falukant/data/learning.js';
|
||||
import LearnRecipient from '../models/falukant/type/learn_recipient.js';
|
||||
import Credit from '../models/falukant/data/credit.js';
|
||||
import DebtorsPrism from '../models/falukant/data/debtors_prism.js';
|
||||
import TitleRequirement from '../models/falukant/type/title_requirement.js';
|
||||
import HealthActivity from '../models/falukant/log/health_activity.js';
|
||||
import Election from '../models/falukant/data/election.js';
|
||||
@@ -808,6 +809,7 @@ class FalukantService extends BaseService {
|
||||
}
|
||||
if (userHouse) user.setDataValue('userHouse', userHouse);
|
||||
}
|
||||
user.setDataValue('debtorsPrison', await this.getDebtorsPrisonStateForUser(user));
|
||||
return user;
|
||||
}
|
||||
|
||||
@@ -865,6 +867,7 @@ class FalukantService extends BaseService {
|
||||
}
|
||||
if (userHouse) u.setDataValue('userHouse', userHouse);
|
||||
if (u.character?.birthdate) u.character.setDataValue('age', calcAge(u.character.birthdate));
|
||||
u.setDataValue('debtorsPrison', await this.getDebtorsPrisonStateForUser(u));
|
||||
return u;
|
||||
}
|
||||
|
||||
@@ -1026,6 +1029,8 @@ class FalukantService extends BaseService {
|
||||
falukantUser.setDataValue('unreadNotifications', 0);
|
||||
}
|
||||
|
||||
falukantUser.setDataValue('debtorsPrison', await this.getDebtorsPrisonStateForUser(falukantUser));
|
||||
|
||||
return falukantUser;
|
||||
}
|
||||
|
||||
@@ -3278,6 +3283,7 @@ class FalukantService extends BaseService {
|
||||
householdTension: householdTension.label,
|
||||
householdTensionScore: householdTension.score,
|
||||
householdTensionReasons: householdTension.reasons,
|
||||
debtorsPrison: await this.getDebtorsPrisonStateForUser(user),
|
||||
lovers,
|
||||
deathPartners: relationships.filter(r => r.relationshipType === 'widowed'),
|
||||
children: children.map(({ _createdAt, ...rest }) => rest),
|
||||
@@ -4385,6 +4391,7 @@ class FalukantService extends BaseService {
|
||||
|
||||
const plainHouse = userHouse.get({ plain: true });
|
||||
plainHouse.servantSummary = this.buildServantSummary(plainHouse, falukantUser.character);
|
||||
plainHouse.debtorsPrison = await this.getDebtorsPrisonStateForUser(falukantUser);
|
||||
return plainHouse;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
@@ -5121,6 +5128,115 @@ class FalukantService extends BaseService {
|
||||
return true;
|
||||
}
|
||||
|
||||
deriveDebtNextForcedAction(status, daysOverdue) {
|
||||
if (status === 'imprisoned') return 'asset_seizure';
|
||||
if (daysOverdue >= 3) return 'debtors_prison';
|
||||
if (daysOverdue === 2) return 'final_warning';
|
||||
if (daysOverdue === 1) return 'reminder';
|
||||
return null;
|
||||
}
|
||||
|
||||
calculateCreditworthinessFromDebtState(record) {
|
||||
if (!record) return 100;
|
||||
|
||||
const penalty = Number(record.creditworthinessPenalty || 0);
|
||||
const daysOverdue = Number(record.daysOverdue || 0);
|
||||
|
||||
if (record.status === 'imprisoned') {
|
||||
return Math.max(0, 20 - penalty);
|
||||
}
|
||||
if (record.status === 'delinquent') {
|
||||
return Math.max(15, 100 - (daysOverdue * 20) - penalty);
|
||||
}
|
||||
return Math.max(0, 80 - penalty);
|
||||
}
|
||||
|
||||
serializeDebtorsPrisonRecord(record, totalDebt = 0) {
|
||||
if (!record) {
|
||||
return {
|
||||
active: false,
|
||||
inDebtorsPrison: false,
|
||||
status: null,
|
||||
daysOverdue: 0,
|
||||
debtAtEntry: null,
|
||||
remainingDebt: Number(totalDebt || 0),
|
||||
reason: null,
|
||||
creditworthinessPenalty: 0,
|
||||
creditworthiness: 100,
|
||||
nextForcedAction: null,
|
||||
publicKnown: false,
|
||||
enteredAt: null,
|
||||
releasedAt: null,
|
||||
assetsSeized: null
|
||||
};
|
||||
}
|
||||
|
||||
const remainingDebt = Number(record.remainingDebt ?? totalDebt ?? 0);
|
||||
const debtState = {
|
||||
active: record.status !== 'released' && !record.releasedAt,
|
||||
inDebtorsPrison: record.status === 'imprisoned' && !record.releasedAt,
|
||||
status: record.status || null,
|
||||
daysOverdue: Number(record.daysOverdue || 0),
|
||||
debtAtEntry: record.debtAtEntry != null ? Number(record.debtAtEntry) : null,
|
||||
remainingDebt,
|
||||
reason: record.reason || null,
|
||||
creditworthinessPenalty: Number(record.creditworthinessPenalty || 0),
|
||||
nextForcedAction: record.nextForcedAction || this.deriveDebtNextForcedAction(record.status, Number(record.daysOverdue || 0)),
|
||||
publicKnown: !!record.publicKnown,
|
||||
enteredAt: record.enteredAt || null,
|
||||
releasedAt: record.releasedAt || null,
|
||||
assetsSeized: record.assetsSeizedJson || null
|
||||
};
|
||||
debtState.creditworthiness = this.calculateCreditworthinessFromDebtState(debtState);
|
||||
return debtState;
|
||||
}
|
||||
|
||||
async getDebtorsPrisonStateForUser(falukantUser) {
|
||||
if (!falukantUser?.id) {
|
||||
return this.serializeDebtorsPrisonRecord(null);
|
||||
}
|
||||
|
||||
const character = falukantUser.character || await FalukantCharacter.findOne({
|
||||
where: { userId: falukantUser.id },
|
||||
attributes: ['id']
|
||||
});
|
||||
|
||||
if (!character?.id) {
|
||||
return this.serializeDebtorsPrisonRecord(null);
|
||||
}
|
||||
|
||||
const records = await DebtorsPrism.findAll({
|
||||
where: { characterId: character.id },
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
|
||||
const activeRecord = records.find((record) => record.status !== 'released' && !record.releasedAt)
|
||||
|| records[0]
|
||||
|| null;
|
||||
|
||||
const totalDebt = await Credit.sum('remaining_amount', {
|
||||
where: { falukant_user_id: falukantUser.id }
|
||||
}) || 0;
|
||||
|
||||
return this.serializeDebtorsPrisonRecord(activeRecord, totalDebt);
|
||||
}
|
||||
|
||||
async assertActionAllowedOutsideDebtorsPrison(hashedUserId) {
|
||||
const falukantUser = await getFalukantUserOrFail(hashedUserId);
|
||||
const debtorsPrison = await this.getDebtorsPrisonStateForUser(falukantUser);
|
||||
|
||||
if (debtorsPrison.inDebtorsPrison) {
|
||||
throw {
|
||||
status: 423,
|
||||
message: 'Aktion im Schuldturm gesperrt',
|
||||
code: 'falukant.debtorsPrison.actionBlocked',
|
||||
debtorsPrison
|
||||
};
|
||||
}
|
||||
|
||||
return { falukantUser, debtorsPrison };
|
||||
}
|
||||
|
||||
async getBankOverview(hashedUserId) {
|
||||
const falukantUser = await getFalukantUserOrFail(hashedUserId);
|
||||
if (!falukantUser) throw new Error('User not found');
|
||||
@@ -5149,9 +5265,14 @@ class FalukantService extends BaseService {
|
||||
const branchCount = await Branch.count({ where: { falukantUserId: falukantUser.id } });
|
||||
const branchValue = branchCount * 1000;
|
||||
|
||||
const debtorsPrison = await this.getDebtorsPrisonStateForUser(falukantUser);
|
||||
|
||||
// 5) Maximaler Kredit und verfügbare Linie
|
||||
const maxCredit = Math.floor(houseValue + branchValue);
|
||||
const availableCredit = maxCredit - totalDebt;
|
||||
const availableCreditRaw = maxCredit - totalDebt;
|
||||
const availableCredit = debtorsPrison.inDebtorsPrison
|
||||
? 0
|
||||
: Math.max(0, availableCreditRaw);
|
||||
|
||||
// 6) aktive Kredite laden
|
||||
const activeCredits = await Credit.findAll({
|
||||
@@ -5165,7 +5286,12 @@ class FalukantService extends BaseService {
|
||||
maxCredit,
|
||||
availableCredit,
|
||||
activeCredits,
|
||||
fee: 7
|
||||
fee: 7,
|
||||
inDebtorsPrison: debtorsPrison.inDebtorsPrison,
|
||||
daysOverdue: debtorsPrison.daysOverdue,
|
||||
nextForcedAction: debtorsPrison.nextForcedAction,
|
||||
creditworthiness: debtorsPrison.creditworthiness,
|
||||
debtorsPrison
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5182,6 +5308,9 @@ class FalukantService extends BaseService {
|
||||
const falukantUser = await getFalukantUserOrFail(hashedUserId);
|
||||
if (!falukantUser) throw new Error('User not found');
|
||||
const financialData = await this.getBankOverview(hashedUserId);
|
||||
if (financialData.inDebtorsPrison) {
|
||||
throw new Error('debtorPrisonBlocksCredit');
|
||||
}
|
||||
if (financialData.availableCredit < height) {
|
||||
throw new Error('Not enough credit');
|
||||
}
|
||||
@@ -6454,7 +6583,7 @@ ORDER BY r.id`,
|
||||
const characterName = titleLabelTr ? [titleLabelTr, firstName, lastName].filter(Boolean).join(' ') : nameWithoutTitle;
|
||||
const age = character.birthdate ? calcAge(character.birthdate) : null;
|
||||
|
||||
const [unreadNotificationsCount, childrenCount] = await Promise.all([
|
||||
const [unreadNotificationsCount, childrenCount, debtorsPrison] = await Promise.all([
|
||||
Notification.count({ where: { userId: falukantUser.id, shown: false } }),
|
||||
ChildRelation.count({
|
||||
where: {
|
||||
@@ -6463,7 +6592,8 @@ ORDER BY r.id`,
|
||||
{ motherCharacterId: character.id }
|
||||
]
|
||||
}
|
||||
})
|
||||
}),
|
||||
this.getDebtorsPrisonStateForUser(falukantUser)
|
||||
]);
|
||||
|
||||
return {
|
||||
@@ -6474,7 +6604,8 @@ ORDER BY r.id`,
|
||||
age,
|
||||
money: Number(falukantUser.money ?? 0),
|
||||
unreadNotificationsCount,
|
||||
childrenCount
|
||||
childrenCount,
|
||||
debtorsPrison
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
32
backend/sql/expand_debtors_prism.sql
Normal file
32
backend/sql/expand_debtors_prism.sql
Normal file
@@ -0,0 +1,32 @@
|
||||
ALTER TABLE falukant_data.debtors_prism
|
||||
ADD COLUMN IF NOT EXISTS status varchar(255) NOT NULL DEFAULT 'delinquent';
|
||||
|
||||
ALTER TABLE falukant_data.debtors_prism
|
||||
ADD COLUMN IF NOT EXISTS entered_at timestamp with time zone NULL;
|
||||
|
||||
ALTER TABLE falukant_data.debtors_prism
|
||||
ADD COLUMN IF NOT EXISTS released_at timestamp with time zone NULL;
|
||||
|
||||
ALTER TABLE falukant_data.debtors_prism
|
||||
ADD COLUMN IF NOT EXISTS debt_at_entry numeric(14,2) NULL;
|
||||
|
||||
ALTER TABLE falukant_data.debtors_prism
|
||||
ADD COLUMN IF NOT EXISTS remaining_debt numeric(14,2) NULL;
|
||||
|
||||
ALTER TABLE falukant_data.debtors_prism
|
||||
ADD COLUMN IF NOT EXISTS days_overdue integer NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE falukant_data.debtors_prism
|
||||
ADD COLUMN IF NOT EXISTS reason varchar(255) NULL;
|
||||
|
||||
ALTER TABLE falukant_data.debtors_prism
|
||||
ADD COLUMN IF NOT EXISTS creditworthiness_penalty integer NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE falukant_data.debtors_prism
|
||||
ADD COLUMN IF NOT EXISTS next_forced_action varchar(255) NULL;
|
||||
|
||||
ALTER TABLE falukant_data.debtors_prism
|
||||
ADD COLUMN IF NOT EXISTS assets_seized_json jsonb NULL;
|
||||
|
||||
ALTER TABLE falukant_data.debtors_prism
|
||||
ADD COLUMN IF NOT EXISTS public_known boolean NOT NULL DEFAULT false;
|
||||
447
docs/FALUKANT_DEBTORS_PRISON_DAEMON_SPEC.md
Normal file
447
docs/FALUKANT_DEBTORS_PRISON_DAEMON_SPEC.md
Normal file
@@ -0,0 +1,447 @@
|
||||
# Falukant: Schuldturm und Pfändung - Daemon-Spezifikation
|
||||
|
||||
Dieses Dokument beschreibt die Umsetzung des **Schuldturm-Systems** im externen Daemon.
|
||||
|
||||
Wichtig:
|
||||
|
||||
- Die projektseitigen DB-Felder, API-Erweiterungen, UI-Warnungen und Aktionssperren sind bereits umgesetzt.
|
||||
- Der Daemon ist die führende Quelle für:
|
||||
- Verzugstage
|
||||
- Eintritt in den Schuldturm
|
||||
- Pfändung und Verwertung
|
||||
- soziale Folgen
|
||||
- Freilassung
|
||||
|
||||
## 1. Bereits vorhandene Datenbasis
|
||||
|
||||
Bereits im Projekt vorhanden:
|
||||
|
||||
- `falukant_data.credit`
|
||||
- `falukant_data.debtors_prism`
|
||||
- `falukant_data.user_house`
|
||||
- inkl. `household_tension_score`
|
||||
- inkl. `household_tension_reasons_json`
|
||||
- Familien-/Liebschaftsdaten in:
|
||||
- `falukant_data.relationship`
|
||||
- `falukant_data.relationship_state`
|
||||
- `falukant_data.child_relation`
|
||||
|
||||
Bereits erweitert:
|
||||
|
||||
- `debtors_prism.status`
|
||||
- `debtors_prism.entered_at`
|
||||
- `debtors_prism.released_at`
|
||||
- `debtors_prism.debt_at_entry`
|
||||
- `debtors_prism.remaining_debt`
|
||||
- `debtors_prism.days_overdue`
|
||||
- `debtors_prism.reason`
|
||||
- `debtors_prism.creditworthiness_penalty`
|
||||
- `debtors_prism.next_forced_action`
|
||||
- `debtors_prism.assets_seized_json`
|
||||
- `debtors_prism.public_known`
|
||||
|
||||
Es sind für den Daemon derzeit keine weiteren DB-Änderungen nötig.
|
||||
|
||||
## 2. Grundregel
|
||||
|
||||
Ein Charakter kommt in den Schuldturm, wenn:
|
||||
|
||||
- mindestens ein aktiver Kredit offen ist
|
||||
- fällige Kreditbedienung ausbleibt
|
||||
- und `days_overdue >= 3`
|
||||
|
||||
Der Daemon prüft dies im Daily-Tick.
|
||||
|
||||
## 3. Zustände
|
||||
|
||||
`debtors_prism.status` verwendet mindestens:
|
||||
|
||||
- `delinquent`
|
||||
- `imprisoned`
|
||||
- `released`
|
||||
|
||||
Bedeutung:
|
||||
|
||||
- `delinquent`: Kreditverzug, aber noch nicht im Schuldturm
|
||||
- `imprisoned`: im Schuldturm, Verwertung läuft
|
||||
- `released`: historischer abgeschlossener Fall
|
||||
|
||||
## 4. Daily-Tick
|
||||
|
||||
Der Daily-Tick prüft pro Falukant-Nutzer:
|
||||
|
||||
1. aktive Kredite
|
||||
2. verbleibende Schuld
|
||||
3. geleistete Bedienung seit letztem Tick
|
||||
4. neue Verzugstage
|
||||
5. Schuldturm-Eintritt
|
||||
6. laufende soziale Folgen
|
||||
7. Verwertungsschritt
|
||||
|
||||
### 4.1 Verzugstage
|
||||
|
||||
Regel:
|
||||
|
||||
- wenn offene Schuld vorhanden und fällige Bedienung ausbleibt:
|
||||
- `days_overdue += 1`
|
||||
- wenn Kreditpflicht erfüllt wurde:
|
||||
- `days_overdue = 0`
|
||||
- falls nicht im Schuldturm
|
||||
|
||||
Wenn noch kein aktiver `debtors_prism`-Eintrag existiert:
|
||||
|
||||
- bei erstem Verzug `debtors_prism` anlegen mit
|
||||
- `status = 'delinquent'`
|
||||
- `days_overdue = 1`
|
||||
- `remaining_debt = aktuelle offene Schuld`
|
||||
- `next_forced_action = 'reminder'`
|
||||
|
||||
### 4.2 Warnstufen
|
||||
|
||||
Bei Verzug:
|
||||
|
||||
- Tag 1:
|
||||
- `next_forced_action = 'reminder'`
|
||||
- Event `falukantUpdateDebt` mit `reason: 'delinquency'`
|
||||
- Tag 2:
|
||||
- `next_forced_action = 'final_warning'`
|
||||
- Event `falukantUpdateDebt` mit `reason: 'delinquency'`
|
||||
- Tag 3:
|
||||
- Schuldturm-Eintritt
|
||||
|
||||
Für Warnstufen senden:
|
||||
|
||||
- `falukantUpdateDebt`
|
||||
- zusätzlich `falukantUpdateStatus`
|
||||
|
||||
## 5. Eintritt in den Schuldturm
|
||||
|
||||
Bei `days_overdue >= 3`:
|
||||
|
||||
- `status = 'imprisoned'`
|
||||
- `entered_at = now()`
|
||||
- `released_at = null`
|
||||
- `debt_at_entry = aktuelle offene Schuld`
|
||||
- `remaining_debt = aktuelle offene Schuld`
|
||||
- `reason = 'credit_default'`
|
||||
- `creditworthiness_penalty += 45`
|
||||
- `next_forced_action = 'asset_seizure'`
|
||||
- `public_known = true`
|
||||
|
||||
### 5.1 Sofortfolgen bei Eintritt
|
||||
|
||||
Einmalig anwenden:
|
||||
|
||||
- Reputation deutlich senken
|
||||
- Empfehlung: `-12`
|
||||
- `marriage_satisfaction` senken
|
||||
- Empfehlung: `-10`
|
||||
- `household_tension_score` erhöhen
|
||||
- Empfehlung: `+15`
|
||||
- `household_tension_reasons_json` um `debtorsPrison` ergänzen
|
||||
|
||||
Zusätzlich:
|
||||
|
||||
- aktive Liebhaber/Mätressen sichtbar destabilisieren
|
||||
- mindestens `affection -= 4`
|
||||
- Kreditaufnahme und aktive Falukant-Aktionen bleiben projektseitig bereits gesperrt
|
||||
|
||||
Bei Eintritt senden:
|
||||
|
||||
- `falukantUpdateDebt`
|
||||
- `reason: 'debtors_prison_entered'`
|
||||
- `falukantUpdateStatus`
|
||||
- `falukantUpdateFamily`
|
||||
- `reason: 'daily'`
|
||||
- `falukantHouseUpdate`
|
||||
- `falukantBranchUpdate`
|
||||
|
||||
## 6. Verwertung / Pfändung
|
||||
|
||||
Die Verwertung läuft nicht alles auf einmal, sondern schrittweise pro Tick.
|
||||
|
||||
Reihenfolge:
|
||||
|
||||
1. freies Geld
|
||||
2. Fahrzeuge
|
||||
3. Waren / Lagerbestände
|
||||
4. Haus
|
||||
5. Niederlassungen
|
||||
|
||||
Ziel:
|
||||
|
||||
- `remaining_debt` schrittweise senken
|
||||
- Fortschritt im UI sichtbar machen
|
||||
|
||||
### 6.1 Geld
|
||||
|
||||
Wenn `falukant_user.money > 0`:
|
||||
|
||||
- direkt zur Schuld tilgen
|
||||
- `remaining_debt -= eingezogener_betrag`
|
||||
|
||||
Events:
|
||||
|
||||
- `falukantUpdateDebt`
|
||||
- `reason: 'asset_seizure'`
|
||||
- `falukantUpdateStatus`
|
||||
|
||||
### 6.2 Fahrzeuge
|
||||
|
||||
Verkaufe zuerst:
|
||||
|
||||
- freie Fahrzeuge
|
||||
- dann weniger wertvolle Typen
|
||||
- keine Fahrzeuge in aktiven Transporten im selben Tick anfassen, falls technisch problematisch
|
||||
|
||||
Erlös:
|
||||
|
||||
- Empfehlung: `vehicle_type.cost * condition_factor * 0.55`
|
||||
|
||||
Zusätzlich in `assets_seized_json` protokollieren:
|
||||
|
||||
- Typ
|
||||
- Anzahl
|
||||
- Erlös
|
||||
|
||||
Events:
|
||||
|
||||
- `falukantUpdateDebt`
|
||||
- `reason: 'vehicle_liquidation'`
|
||||
- `falukantUpdateStatus`
|
||||
|
||||
### 6.3 Waren / Lager
|
||||
|
||||
Verwertbare Güter:
|
||||
|
||||
- Lagerbestände
|
||||
- Inventar
|
||||
- handelbare Waren
|
||||
|
||||
Erlös:
|
||||
|
||||
- Empfehlung: Marktwert mit Abschlag von `35% bis 50%`
|
||||
|
||||
Events:
|
||||
|
||||
- `falukantUpdateDebt`
|
||||
- `reason: 'asset_seizure'`
|
||||
- `falukantUpdateStatus`
|
||||
- `falukantBranchUpdate`
|
||||
|
||||
### 6.4 Haus
|
||||
|
||||
Wenn Restschuld nach Geld/Fahrzeugen/Waren weiter hoch ist:
|
||||
|
||||
- Haus pfänden
|
||||
- Spieler auf niedrigeres Haus oder Minimalhaus zurücksetzen
|
||||
- Dienerschaft reduzieren
|
||||
- `household_order` senken
|
||||
|
||||
Events:
|
||||
|
||||
- `falukantUpdateDebt`
|
||||
- `reason: 'house_seizure'`
|
||||
- `falukantHouseUpdate`
|
||||
- `falukantUpdateStatus`
|
||||
- `falukantUpdateFamily`
|
||||
- `reason: 'daily'`
|
||||
|
||||
### 6.5 Niederlassungen
|
||||
|
||||
Wenn weiter nicht gedeckt:
|
||||
|
||||
- Niederlassungen schließen
|
||||
- zuerst niedrige Stufe / niedriger Wert
|
||||
- Hauptniederlassung nur als letzter Schritt
|
||||
|
||||
Events:
|
||||
|
||||
- `falukantUpdateDebt`
|
||||
- `reason: 'branch_closure'`
|
||||
- `falukantBranchUpdate`
|
||||
- `falukantUpdateStatus`
|
||||
|
||||
## 7. Laufende soziale Folgen im Schuldturm
|
||||
|
||||
Solange `status = 'imprisoned'`:
|
||||
|
||||
- täglicher Reputationsmalus
|
||||
- Empfehlung: `-2`
|
||||
- zusätzliche `creditworthiness_penalty += 1` pro Tag
|
||||
- `marriage_satisfaction -= 1`
|
||||
- `household_tension_score += 2`
|
||||
|
||||
Wenn aktive Liebschaften bestehen:
|
||||
|
||||
- `affection -= 2`
|
||||
- bei niedriger Zuneigung oder hoher Sichtbarkeit kann Beziehung enden
|
||||
|
||||
Empfohlene Absprungregel:
|
||||
|
||||
- wenn `affection <= 30` oder `months_underfunded >= 2`
|
||||
- Chance auf Beziehungsende prüfen
|
||||
- bei repräsentativen Beziehungen zusätzlich höhere Absprungchance, wenn `public_known = true`
|
||||
|
||||
Events bei sozialen Folgewirkungen:
|
||||
|
||||
- `falukantUpdateFamily`
|
||||
- `reason: 'daily'`
|
||||
- zusätzlich `falukantUpdateStatus`
|
||||
|
||||
## 8. Kreditwürdigkeit
|
||||
|
||||
Die UI rechnet bereits aus `creditworthiness_penalty` und Status einen sichtbaren Wert.
|
||||
|
||||
Der Daemon muss pflegen:
|
||||
|
||||
- `creditworthiness_penalty`
|
||||
- `status`
|
||||
- `days_overdue`
|
||||
|
||||
Empfehlung:
|
||||
|
||||
- Eintritt Schuldturm: `+45`
|
||||
- pro weiterem Hafttag: `+1`
|
||||
- Hauspfändung: zusätzlich `+10`
|
||||
- Niederlassungsschließung: zusätzlich `+8`
|
||||
|
||||
## 9. Freilassung
|
||||
|
||||
Freilassung, wenn:
|
||||
|
||||
- keine relevante Restschuld mehr offen ist
|
||||
oder
|
||||
- ein definierter Restwert unterschritten wird, falls ihr einen Bagatellgrenzwert wollt
|
||||
|
||||
Dann:
|
||||
|
||||
- `status = 'released'`
|
||||
- `released_at = now()`
|
||||
- `next_forced_action = null`
|
||||
- `days_overdue = 0`
|
||||
- `remaining_debt = 0`
|
||||
|
||||
Events:
|
||||
|
||||
- `falukantUpdateDebt`
|
||||
- `reason: 'debtors_prison_released'`
|
||||
- `falukantUpdateStatus`
|
||||
- `falukantUpdateFamily`
|
||||
- `reason: 'daily'`
|
||||
- `falukantHouseUpdate`
|
||||
- `falukantBranchUpdate`
|
||||
|
||||
Keine automatische vollständige soziale Heilung:
|
||||
|
||||
- Reputation bleibt reduziert
|
||||
- Kreditwürdigkeit bleibt reduziert
|
||||
- Familie/Haus bleiben in Folgezuständen
|
||||
|
||||
## 10. Event-Kommunikation zur UI
|
||||
|
||||
Der Daemon sendet als Primärevent:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "falukantUpdateDebt",
|
||||
"user_id": 123,
|
||||
"reason": "delinquency"
|
||||
}
|
||||
```
|
||||
|
||||
Mögliche `reason`:
|
||||
|
||||
- `delinquency`
|
||||
- `debtors_prison_entered`
|
||||
- `asset_seizure`
|
||||
- `vehicle_liquidation`
|
||||
- `house_seizure`
|
||||
- `branch_closure`
|
||||
- `debtors_prison_released`
|
||||
|
||||
### 10.1 Begleitevents
|
||||
|
||||
Je nach Folge zusätzlich:
|
||||
|
||||
- `falukantUpdateStatus`
|
||||
- `falukantHouseUpdate`
|
||||
- `falukantBranchUpdate`
|
||||
- `falukantUpdateFamily`
|
||||
|
||||
### 10.2 Empfohlene Minimalregeln
|
||||
|
||||
- `delinquency`:
|
||||
- `falukantUpdateDebt`
|
||||
- `falukantUpdateStatus`
|
||||
- `debtors_prison_entered`:
|
||||
- `falukantUpdateDebt`
|
||||
- `falukantUpdateStatus`
|
||||
- `falukantUpdateFamily`
|
||||
- `falukantHouseUpdate`
|
||||
- `falukantBranchUpdate`
|
||||
- `asset_seizure`:
|
||||
- `falukantUpdateDebt`
|
||||
- `falukantUpdateStatus`
|
||||
- optional `falukantBranchUpdate`
|
||||
- `vehicle_liquidation`:
|
||||
- `falukantUpdateDebt`
|
||||
- `falukantUpdateStatus`
|
||||
- `house_seizure`:
|
||||
- `falukantUpdateDebt`
|
||||
- `falukantUpdateStatus`
|
||||
- `falukantHouseUpdate`
|
||||
- `falukantUpdateFamily`
|
||||
- `branch_closure`:
|
||||
- `falukantUpdateDebt`
|
||||
- `falukantUpdateStatus`
|
||||
- `falukantBranchUpdate`
|
||||
- `debtors_prison_released`:
|
||||
- `falukantUpdateDebt`
|
||||
- `falukantUpdateStatus`
|
||||
- `falukantUpdateFamily`
|
||||
- `falukantHouseUpdate`
|
||||
- `falukantBranchUpdate`
|
||||
|
||||
## 11. Idempotenz
|
||||
|
||||
Der Worker muss idempotent arbeiten.
|
||||
|
||||
Wichtig:
|
||||
|
||||
- Eintritt in den Schuldturm nicht mehrfach für denselben aktiven Fall auslösen
|
||||
- Verwertungsschritte nur einmal je Asset anwenden
|
||||
- `released` nicht erneut freisetzen
|
||||
|
||||
Empfehlung:
|
||||
|
||||
- pro Tick Transaktion
|
||||
- pro Nutzer eine klare Reihenfolge
|
||||
- Änderungen in `assets_seized_json` protokollieren
|
||||
|
||||
## 12. Mindestumsetzung für Version 1
|
||||
|
||||
Pflicht:
|
||||
|
||||
1. Verzugstage pflegen
|
||||
2. Eintritt nach 3 Tagen
|
||||
3. Status und Penalty schreiben
|
||||
4. Geld zuerst einziehen
|
||||
5. danach Fahrzeuge
|
||||
6. Events senden
|
||||
|
||||
Danach:
|
||||
|
||||
7. Hauspfändung
|
||||
8. Niederlassungsschließung
|
||||
9. volle Familienfolgen
|
||||
|
||||
## 13. Hinweis an den Daemon
|
||||
|
||||
Die projektseitigen Grundlagen sind bereits umgesetzt:
|
||||
|
||||
- `debtors_prism` ist erweitert
|
||||
- Bank-/Haus-/Familien-/Übersichts-UI reagiert auf den Status
|
||||
- aktive Falukant-Aktionen werden im Backend bereits gesperrt, sobald `inDebtorsPrison = true`
|
||||
|
||||
Der Daemon muss daher vor allem die Zustände und Folgen zuverlässig schreiben und die dokumentierten Events senden.
|
||||
@@ -1,8 +1,9 @@
|
||||
# Falukant: UI-Anpassung – WebSocket & Familie / Liebschaften
|
||||
# Falukant: UI-Anpassung - WebSocket & Familie / Liebschaften
|
||||
|
||||
Dieses Dokument beschreibt die Nachrichten, die der externe Falukant-Daemon für Familien-, Ehe- und Liebschaftsänderungen sendet, damit die UI gezielt reagieren kann.
|
||||
Dieses Dokument beschreibt die Nachrichten, die der externe Falukant-Daemon über den WebSocket-Broadcast sendet, damit die UI gezielt reagieren kann.
|
||||
|
||||
Transport:
|
||||
|
||||
- Alle Clients erhalten denselben Broadcast.
|
||||
- Die UI muss nach `user_id` filtern und nur Events für die eingeloggte Session verarbeiten.
|
||||
|
||||
@@ -10,10 +11,13 @@ Transport:
|
||||
|
||||
| `event` | Pflichtfelder | Typische UI-Reaktion |
|
||||
|---------|----------------|----------------------|
|
||||
| `falukantUpdateFamily` | `user_id`, `reason` | Gezielter Refresh von Familie, Liebschaften und je nach `reason` auch Geld oder Ruf |
|
||||
| `falukantUpdateStatus` | `user_id` | Allgemeiner Falukant-Status-/Spielstands-Refresh |
|
||||
| `children_update` | `user_id` | Kinderliste und Familienansicht aktualisieren |
|
||||
| `falukant_family_scandal_hint` | `relationship_id` | Optionaler Hinweis oder Logeintrag; kein `user_id` |
|
||||
| `falukantUpdateFamily` | `user_id`, `reason` | Gezielter Refresh Familie/Liebe/Geld je nach `reason` |
|
||||
| `falukantUpdateStatus` | `user_id` | Allgemeiner Status-/Spielstands-Refresh |
|
||||
| `falukantUpdateProductionCertificate` | `user_id`, `reason`, `old_certificate`, `new_certificate` | Produkte / Produktions-UI / Zertifikat neu laden |
|
||||
| `children_update` | `user_id` | Kinderliste / FamilyView aktualisieren |
|
||||
| `falukant_family_scandal_hint` | `relationship_id` | Optionaler Toast oder Log; kein `user_id` |
|
||||
| `falukantUpdateChurch` | `user_id`, `reason` | Kirchenämter, Bewerbungen, Ernennungen |
|
||||
| `falukantUpdateDebt` | `user_id`, `reason` | Schuldturm, Verzug, Pfändung, Freilassung |
|
||||
|
||||
## 2. JSON-Payloads
|
||||
|
||||
@@ -28,14 +32,32 @@ Transport:
|
||||
```
|
||||
|
||||
`reason` ist immer genau einer dieser festen Strings:
|
||||
|
||||
- `daily`
|
||||
- `monthly`
|
||||
- `lover_installment`
|
||||
- `scandal`
|
||||
- `lover_birth`
|
||||
|
||||
Es gibt keine weiteren `reason`-Werte.
|
||||
### 2.2 `falukantUpdateChurch`
|
||||
|
||||
### 2.2 `falukantUpdateStatus`
|
||||
```json
|
||||
{
|
||||
"event": "falukantUpdateChurch",
|
||||
"user_id": 123,
|
||||
"reason": "applications"
|
||||
}
|
||||
```
|
||||
|
||||
Mögliche `reason`:
|
||||
|
||||
- `applications`
|
||||
- `npc_decision`
|
||||
- `appointment`
|
||||
- `vacancy_fill`
|
||||
- `promotion`
|
||||
|
||||
### 2.3 `falukantUpdateStatus`
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -44,9 +66,21 @@ Es gibt keine weiteren `reason`-Werte.
|
||||
}
|
||||
```
|
||||
|
||||
Dieses Event wird typischerweise direkt nach `falukantUpdateFamily` mit derselben `user_id` gesendet.
|
||||
Dieses Event wird typischerweise direkt nach einem fachlichen Falukant-Event mit derselben `user_id` gesendet.
|
||||
|
||||
### 2.3 `children_update`
|
||||
### 2.4 `falukantUpdateProductionCertificate`
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "falukantUpdateProductionCertificate",
|
||||
"user_id": 123,
|
||||
"reason": "daily_recalculation",
|
||||
"old_certificate": 2,
|
||||
"new_certificate": 3
|
||||
}
|
||||
```
|
||||
|
||||
### 2.5 `children_update`
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -56,10 +90,11 @@ Dieses Event wird typischerweise direkt nach `falukantUpdateFamily` mit derselbe
|
||||
```
|
||||
|
||||
Dieses Event tritt bei Geburt aus einer Liebschaft auf, meist zusammen mit:
|
||||
|
||||
- `falukantUpdateFamily` mit `reason: "lover_birth"`
|
||||
- `falukantUpdateStatus`
|
||||
|
||||
### 2.4 `falukant_family_scandal_hint`
|
||||
### 2.6 `falukant_family_scandal_hint`
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -69,56 +104,118 @@ Dieses Event tritt bei Geburt aus einer Liebschaft auf, meist zusammen mit:
|
||||
```
|
||||
|
||||
Hinweis:
|
||||
|
||||
- Dieses Event enthält kein `user_id`.
|
||||
- Die UI kann es ignorieren oder optional nur für Log-/Toast-Zwecke verwenden.
|
||||
- Die eigentliche nutzerbezogene Aktualisierung läuft über `falukantUpdateFamily` mit `reason: "scandal"`.
|
||||
|
||||
### 2.7 `falukantUpdateDebt`
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "falukantUpdateDebt",
|
||||
"user_id": 123,
|
||||
"reason": "debtors_prison_entered"
|
||||
}
|
||||
```
|
||||
|
||||
Mögliche `reason`:
|
||||
|
||||
- `delinquency`
|
||||
- `debtors_prison_entered`
|
||||
- `asset_seizure`
|
||||
- `vehicle_liquidation`
|
||||
- `house_seizure`
|
||||
- `branch_closure`
|
||||
- `debtors_prison_released`
|
||||
|
||||
## 3. Fachliche Bedeutung von `reason`
|
||||
|
||||
### `reason: "daily"`
|
||||
### 3.1 `falukantUpdateFamily`
|
||||
|
||||
#### `reason: "daily"`
|
||||
|
||||
`daily` ist der Sammelgrund für tägliche Änderungen im Familien- und Liebschaftssystem.
|
||||
|
||||
Darunter fallen insbesondere:
|
||||
|
||||
- tägliche Drift und Änderung der Ehezufriedenheit
|
||||
- `marriage_public_stability`
|
||||
- `household_tension_score`
|
||||
- Ehe-Buffs und temporäre Zähler wie Geschenk-, Fest- oder Haus-Effekte
|
||||
- tägliche Liebschaftslogik für aktive Beziehungen
|
||||
- Rufverlust bei zwei oder mehr sichtbaren Liebschaften
|
||||
- Zufalls-Mali wie Gerücht oder Tadel
|
||||
|
||||
Wichtig:
|
||||
|
||||
- Es gibt kein separates Event für „nur Ehe-Buff“.
|
||||
- Es gibt kein separates Event für „nur zwei sichtbare Liebschaften“.
|
||||
- Es gibt kein separates Event für „nur Gerücht/Tadel“.
|
||||
- Alles davon erscheint in der UI ausschließlich als `falukantUpdateFamily` mit `reason: "daily"`.
|
||||
|
||||
### `reason: "scandal"`
|
||||
#### `reason: "monthly"`
|
||||
|
||||
`monthly` steht für monatliche Verarbeitung, insbesondere:
|
||||
|
||||
- Dienerschaftskosten
|
||||
- laufende Kosten
|
||||
- Unterversorgung
|
||||
- Geldänderungen
|
||||
|
||||
#### `reason: "lover_installment"`
|
||||
|
||||
`lover_installment` steht für die 2-Stunden-Unterhaltsbelastung von Liebschaften.
|
||||
|
||||
Die UI sollte dafür mindestens:
|
||||
|
||||
- Geld neu laden
|
||||
- Family-/Liebschaftsstatus neu laden
|
||||
|
||||
#### `reason: "scandal"`
|
||||
|
||||
`scandal` wird zusätzlich zu einem gelungenen Skandalwurf gesendet.
|
||||
|
||||
Typischer Ablauf:
|
||||
|
||||
- optional `falukant_family_scandal_hint`
|
||||
- `falukantUpdateFamily` mit `reason: "scandal"`
|
||||
- `falukantUpdateStatus`
|
||||
|
||||
Danach kann für denselben Nutzer am selben Tag zusätzlich noch `daily` folgen.
|
||||
|
||||
### `reason: "monthly"`
|
||||
|
||||
`monthly` steht für Monatsverarbeitung, insbesondere:
|
||||
- laufende Kosten
|
||||
- Unterversorgung
|
||||
- Geldänderungen
|
||||
|
||||
### `reason: "lover_birth"`
|
||||
#### `reason: "lover_birth"`
|
||||
|
||||
`lover_birth` signalisiert ein neues Kind aus einer Liebschaft.
|
||||
|
||||
Meist folgen zusammen:
|
||||
|
||||
- `falukantUpdateFamily` mit `reason: "lover_birth"`
|
||||
- `children_update`
|
||||
- `falukantUpdateStatus`
|
||||
|
||||
### 3.2 `falukantUpdateChurch`
|
||||
|
||||
- `applications`: Spieler ist kirchlicher Vorgesetzter; offene Bewerbungen warten
|
||||
- `npc_decision`: NPC-Vorgesetzter hat entschieden
|
||||
- `appointment`: automatische Annahme älterer Bewerbung
|
||||
- `vacancy_fill`: Interimsbesetzung
|
||||
- `promotion`: reserviert / zukünftig
|
||||
|
||||
### 3.3 `falukantUpdateProductionCertificate`
|
||||
|
||||
- `daily_recalculation`: Zertifikat nach täglicher Prüfung geändert
|
||||
|
||||
### 3.4 `falukantUpdateDebt`
|
||||
|
||||
- `delinquency`: Mahnstufe oder Verzug aktualisiert
|
||||
- `debtors_prison_entered`: Eintritt in den Schuldturm
|
||||
- `asset_seizure`: Geld, Waren oder sonstige Vermögenswerte eingezogen
|
||||
- `vehicle_liquidation`: Fahrzeuge zwangsverkauft
|
||||
- `house_seizure`: Haus gepfändet
|
||||
- `branch_closure`: Niederlassung geschlossen
|
||||
- `debtors_prison_released`: Freilassung
|
||||
|
||||
## 4. Empfohlene Handler-Logik
|
||||
|
||||
```text
|
||||
@@ -131,10 +228,22 @@ onMessage(json):
|
||||
refreshPlayerStatus()
|
||||
return
|
||||
|
||||
case "falukantUpdateProductionCertificate":
|
||||
refreshProductsAndProductionUi()
|
||||
return
|
||||
|
||||
case "children_update":
|
||||
refreshChildrenAndFamilyView()
|
||||
return
|
||||
|
||||
case "falukantUpdateChurch":
|
||||
refreshChurchContextByReason(json.reason)
|
||||
return
|
||||
|
||||
case "falukantUpdateDebt":
|
||||
refreshDebtAndAffectedViews(json.reason)
|
||||
return
|
||||
|
||||
case "falukantUpdateFamily":
|
||||
switch json.reason:
|
||||
case "daily":
|
||||
@@ -145,6 +254,10 @@ onMessage(json):
|
||||
refreshMoney()
|
||||
refreshFamilyAndRelationships()
|
||||
break
|
||||
case "lover_installment":
|
||||
refreshMoney()
|
||||
refreshFamilyAndRelationships()
|
||||
break
|
||||
case "scandal":
|
||||
showScandalToastOptional()
|
||||
refreshFamilyAndRelationships()
|
||||
@@ -162,15 +275,23 @@ onMessage(json):
|
||||
|
||||
## 5. Deduplizierung
|
||||
|
||||
Am selben Tag kann ein Nutzer mehrere relevante Events erhalten, zum Beispiel:
|
||||
Ein Nutzer kann kurz hintereinander mehrere relevante Events erhalten, zum Beispiel:
|
||||
|
||||
- `scandal`
|
||||
- danach `daily`
|
||||
- danach `falukantUpdateStatus`
|
||||
|
||||
oder:
|
||||
|
||||
- `falukantUpdateDebt`
|
||||
- direkt danach `falukantUpdateStatus`
|
||||
- zusätzlich `falukantUpdateFamily`
|
||||
|
||||
Die UI sollte deshalb:
|
||||
|
||||
- Refreshes bündeln oder entprellen
|
||||
- idempotente Reloads verwenden
|
||||
- nicht davon ausgehen, dass jeder fachliche Effekt einen eigenen `reason` hat
|
||||
- nicht davon ausgehen, dass jeder fachliche Effekt einen eigenen Spezial-Eventpfad hat
|
||||
|
||||
## 6. Welche Daten sollten neu geladen werden?
|
||||
|
||||
@@ -178,15 +299,18 @@ Die UI sollte deshalb:
|
||||
|-----------|--------------------|
|
||||
| Jede `falukantUpdateFamily` | Family-/Relationship-Daten neu laden |
|
||||
| `reason: "monthly"` | Family-Daten plus Geld/Status neu laden |
|
||||
| `reason: "lover_installment"` | Geld plus Family-Daten neu laden |
|
||||
| `reason: "daily"` | Family-Daten neu laden, bei Bedarf auch Ruf-/Statusdaten |
|
||||
| `reason: "scandal"` | Family-Daten plus Ruf-/Statusdaten neu laden |
|
||||
| `children_update` / `lover_birth` | Kinderdaten und FamilyView neu laden |
|
||||
| `falukantUpdateChurch` | Kirchenämter, Bewerbungen, freie Positionen je nach `reason` |
|
||||
| `falukantUpdateProductionCertificate` | User-Status, Zertifikat, Produkte, Produktions-UI |
|
||||
| `falukantUpdateDebt` | Bank, Overview, House, Branch, ggf. Family |
|
||||
|
||||
## 7. Sonderfälle
|
||||
|
||||
| Fall | Verhalten |
|
||||
|------|-----------|
|
||||
| NPC ohne `user_id` | Keine nutzerbezogenen Family-Socket-Events |
|
||||
| NPC ohne `user_id` | Keine nutzerbezogenen Socket-Events |
|
||||
| Mehrere Events kurz hintereinander | Normal; UI sollte damit robust umgehen |
|
||||
| Nur `falukantUpdateStatus` ohne Family-Event | Kann von anderen Falukant-Workern kommen |
|
||||
|
||||
| Nur `falukantUpdateStatus` ohne Fach-Event | Kann von anderen Falukant-Workern kommen |
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user