feat(falukant): add scandalExtraDailyPct field and update related components
All checks were successful
Deploy to production / deploy (push) Successful in 2m59s

- Introduced a new field `scandalExtraDailyPct` in the relationship state model to track additional scandal risk per day, with validation constraints.
- Updated the FalukantService to include the new field in relevant calculations and data handling.
- Enhanced the frontend components, including RevenueSection and FamilyView, to display the scandal risk information and updated price calculations.
- Added localization entries for the new field in multiple languages to ensure clarity for users.
This commit is contained in:
Torsten Schulz (local)
2026-04-13 15:31:47 +02:00
parent b0624422b8
commit 86e14a875d
12 changed files with 176 additions and 26 deletions

View File

@@ -0,0 +1,32 @@
/* eslint-disable */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
ADD COLUMN IF NOT EXISTS scandal_extra_daily_pct double precision NOT NULL DEFAULT 0;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
DROP CONSTRAINT IF EXISTS relationship_state_scandal_extra_daily_pct_chk;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
ADD CONSTRAINT relationship_state_scandal_extra_daily_pct_chk
CHECK (scandal_extra_daily_pct >= 0 AND scandal_extra_daily_pct <= 100);
`);
},
async down(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
DROP CONSTRAINT IF EXISTS relationship_state_scandal_extra_daily_pct_chk;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
DROP COLUMN IF EXISTS scandal_extra_daily_pct;
`);
},
};

View File

@@ -88,6 +88,16 @@ RelationshipState.init(
min: 0, min: 0,
}, },
}, },
scandalExtraDailyPct: {
type: DataTypes.FLOAT,
allowNull: false,
defaultValue: 0,
validate: {
min: 0,
max: 100,
},
field: 'scandal_extra_daily_pct',
},
monthsUnderfunded: { monthsUnderfunded: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false,

View File

@@ -587,6 +587,7 @@ class FalukantService extends BaseService {
maintenanceLevel: 50, maintenanceLevel: 50,
statusFit: 0, statusFit: 0,
monthlyBaseCost: 0, monthlyBaseCost: 0,
scandalExtraDailyPct: 0,
monthsUnderfunded: 0, monthsUnderfunded: 0,
active: true, active: true,
acknowledged: false, acknowledged: false,
@@ -3514,6 +3515,7 @@ class FalukantService extends BaseService {
maintenanceLevel: state.maintenanceLevel, maintenanceLevel: state.maintenanceLevel,
statusFit: state.statusFit, statusFit: state.statusFit,
monthlyBaseCost: state.monthlyBaseCost, monthlyBaseCost: state.monthlyBaseCost,
scandalExtraDailyPct: state.scandalExtraDailyPct,
monthsUnderfunded: state.monthsUnderfunded, monthsUnderfunded: state.monthsUnderfunded,
active: state.active, active: state.active,
acknowledged: state.acknowledged, acknowledged: state.acknowledged,
@@ -3649,6 +3651,7 @@ class FalukantService extends BaseService {
statusFit: state.statusFit, statusFit: state.statusFit,
monthlyBaseCost: monthlyCost, monthlyBaseCost: monthlyCost,
monthlyCost, monthlyCost,
scandalExtraDailyPct: state.scandalExtraDailyPct,
acknowledged: state.acknowledged, acknowledged: state.acknowledged,
active: state.active, active: state.active,
monthsUnderfunded: state.monthsUnderfunded, monthsUnderfunded: state.monthsUnderfunded,
@@ -6889,7 +6892,7 @@ class FalukantService extends BaseService {
} }
const [products, knowledges, townWorths] = await Promise.all([ const [products, knowledges, townWorths] = await Promise.all([
ProductType.findAll({ attributes: ['id', 'sellCost'] }), ProductType.findAll({ attributes: ['id', 'sellCost', 'category', 'productionTime'] }),
Knowledge.findAll({ Knowledge.findAll({
where: { characterId: character.id }, where: { characterId: character.id },
attributes: ['productId', 'knowledge'] attributes: ['productId', 'knowledge']
@@ -6900,22 +6903,63 @@ class FalukantService extends BaseService {
}) })
]); ]);
const knowledgeMap = new Map(knowledges.map(k => [k.productId, k.knowledge || 0])); const knowledgeMap = new Map(knowledges.map((k) => [k.productId, k.knowledge || 0]));
const maxWorthByProduct = new Map(); const worthByProductRegion = new Map(
for (const tw of townWorths) { townWorths.map((tw) => [`${tw.productId}-${tw.regionId}`, tw.worthPercent ?? 50])
const w = tw.worthPercent ?? 50; );
const prev = maxWorthByProduct.get(tw.productId); const certificate = Number(user.certificate || 1);
maxWorthByProduct.set(tw.productId, prev == null ? w : Math.max(prev, w)); const taxByRegion = new Map();
} await Promise.all(
worthRegionIds.map(async (rid) => {
taxByRegion.set(rid, await getCumulativeTaxPercentWithExemptions(user.id, rid));
})
);
const prices = {}; const prices = {};
const netMetrics = {};
for (const product of products) { for (const product of products) {
const worthPercent = maxWorthByProduct.get(product.id) ?? 50;
const knowledgeFactor = knowledgeMap.get(product.id) || 0; const knowledgeFactor = knowledgeMap.get(product.id) || 0;
const price = calcRegionalSellPriceSync(product, knowledgeFactor, worthPercent); const productionTime = Number(product.productionTime || 0);
if (price !== null) prices[product.id] = price; const pieceCost = productionPieceCost(certificate, product.category);
let best = null;
for (const rid of worthRegionIds) {
const worthPercent = worthByProductRegion.get(`${product.id}-${rid}`) ?? 50;
const grossPrice = calcRegionalSellPriceSync(product, knowledgeFactor, worthPercent);
if (grossPrice == null) continue;
const taxPercent = Number(taxByRegion.get(rid) || 0);
const transportPerPiece = networkWorth && rid !== regionId ? (grossPrice * 0.01) : 0;
const taxableProfit = Math.max(0, grossPrice - pieceCost - transportPerPiece);
const taxPerPiece = taxableProfit * (taxPercent / 100);
const netPerPiece = grossPrice - pieceCost - taxPerPiece - transportPerPiece;
const netPerMinute = productionTime > 0 ? (netPerPiece / productionTime) : 0;
const candidate = {
regionId: rid,
grossPrice,
netPerPiece,
netPerMinute,
pieceCost,
taxPercent,
taxPerPiece,
transportPerPiece,
};
if (!best || candidate.netPerMinute > best.netPerMinute) {
best = candidate;
}
}
if (!best) continue;
prices[product.id] = best.grossPrice;
netMetrics[product.id] = {
regionId: best.regionId,
grossPerPiece: Number(best.grossPrice.toFixed(2)),
netPerPiece: Number(best.netPerPiece.toFixed(2)),
netPerMinute: Number(best.netPerMinute.toFixed(2)),
pieceCost: Number(best.pieceCost.toFixed(2)),
taxPercent: Number(best.taxPercent.toFixed(2)),
taxPerPiece: Number(best.taxPerPiece.toFixed(2)),
transportPerPiece: Number(best.transportPerPiece.toFixed(2)),
};
} }
return { prices }; return { prices, netMetrics };
} catch (error) { } catch (error) {
console.error(`[getAllProductPricesInRegion] Error for regionId=${regionId}:`, error); console.error(`[getAllProductPricesInRegion] Error for regionId=${regionId}:`, error);
throw error; throw error;
@@ -7061,7 +7105,10 @@ ORDER BY r.id`,
const { cities, branchTypeByCityId } = citiesWithBranchType; const { cities, branchTypeByCityId } = citiesWithBranchType;
const [products, knowledges, townWorths] = await Promise.all([ const [products, knowledges, townWorths] = await Promise.all([
ProductType.findAll({ where: { id: { [Op.in]: productIds } }, attributes: ['id', 'sellCost'] }), ProductType.findAll({
where: { id: { [Op.in]: productIds } },
attributes: ['id', 'sellCost', 'category', 'productionTime']
}),
Knowledge.findAll({ Knowledge.findAll({
where: { characterId: characterId, productId: { [Op.in]: productIds } }, where: { characterId: characterId, productId: { [Op.in]: productIds } },
attributes: ['productId', 'knowledge'] attributes: ['productId', 'knowledge']
@@ -7075,44 +7122,76 @@ ORDER BY r.id`,
const knowledgeByProduct = new Map(knowledges.map(k => [k.productId, k.knowledge || 0])); const knowledgeByProduct = new Map(knowledges.map(k => [k.productId, k.knowledge || 0]));
const worthByProductRegion = new Map(townWorths.map(tw => [`${tw.productId}-${tw.regionId}`, tw.worthPercent])); const worthByProductRegion = new Map(townWorths.map(tw => [`${tw.productId}-${tw.regionId}`, tw.worthPercent]));
const PRICE_TOLERANCE = 0.01; const certificate = Number(user.certificate || 1);
const taxByCity = new Map();
await Promise.all(
cities.map(async (city) => {
taxByCity.set(city.id, await getCumulativeTaxPercentWithExemptions(user.id, city.id));
})
);
const PRICE_TOLERANCE = 0.0001;
const out = {}; const out = {};
for (const product of products) { for (const product of products) {
const knowledgeFactor = knowledgeByProduct.get(product.id) || 0; const knowledgeFactor = knowledgeByProduct.get(product.id) || 0;
const pieceCost = productionPieceCost(certificate, product.category);
const productionTime = Number(product.productionTime || 0);
const clientPriceRaw = priceByProduct.get(product.id); const clientPriceRaw = priceByProduct.get(product.id);
const clientPriceNum = clientPriceRaw != null && clientPriceRaw !== '' const clientPriceNum = clientPriceRaw != null && clientPriceRaw !== ''
? Number(clientPriceRaw) ? Number(clientPriceRaw)
: NaN; : NaN;
let currentRegionalPrice; let currentRegionalNetPerMinute;
// Wie getProductPricesInCities: bei bekannter Standort-Region immer // Wie getProductPricesInCities: bei bekannter Standort-Region immer
// serverseitigen Verkaufspreis dieser Region als Referenz — nicht den // serverseitigen Verkaufspreis dieser Region als Referenz — nicht den
// Client-Wert (Ertrags-Tabelle kann MAX-Worth über Filialen nutzen). // Client-Wert (Ertrags-Tabelle kann MAX-Worth über Filialen nutzen).
if (currentRegionId) { if (currentRegionId) {
const wp = worthByProductRegion.get(`${product.id}-${currentRegionId}`) ?? 50; const wp = worthByProductRegion.get(`${product.id}-${currentRegionId}`) ?? 50;
currentRegionalPrice = calcRegionalSellPriceSync(product, knowledgeFactor, wp) const grossCurrent = calcRegionalSellPriceSync(product, knowledgeFactor, wp)
?? (!Number.isNaN(clientPriceNum) ? clientPriceNum : Number(product.sellCost) ?? 0); ?? (!Number.isNaN(clientPriceNum) ? clientPriceNum : Number(product.sellCost) ?? 0);
const taxPercentCurrent = Number(taxByCity.get(currentRegionId) || 0);
const taxableProfitCurrent = Math.max(0, grossCurrent - pieceCost);
const taxCurrent = taxableProfitCurrent * (taxPercentCurrent / 100);
const netPieceCurrent = grossCurrent - pieceCost - taxCurrent;
currentRegionalNetPerMinute = productionTime > 0 ? (netPieceCurrent / productionTime) : 0;
} else if (!Number.isNaN(clientPriceNum)) { } else if (!Number.isNaN(clientPriceNum)) {
currentRegionalPrice = clientPriceNum; const taxPercentCurrent = Number(taxByCity.get(currentRegionId) || 0);
const taxableProfitCurrent = Math.max(0, clientPriceNum - pieceCost);
const taxCurrent = taxableProfitCurrent * (taxPercentCurrent / 100);
const netPieceCurrent = clientPriceNum - pieceCost - taxCurrent;
currentRegionalNetPerMinute = productionTime > 0 ? (netPieceCurrent / productionTime) : 0;
} else { } else {
currentRegionalPrice = Number(product.sellCost) || 0; const fallbackGross = Number(product.sellCost) || 0;
const netPieceCurrent = fallbackGross - pieceCost;
currentRegionalNetPerMinute = productionTime > 0 ? (netPieceCurrent / productionTime) : 0;
} }
const results = []; const results = [];
for (const city of cities) { for (const city of cities) {
if (currentRegionId && city.id === currentRegionId) continue; if (currentRegionId && city.id === currentRegionId) continue;
const worthPercent = worthByProductRegion.get(`${product.id}-${city.id}`) ?? 50; const worthPercent = worthByProductRegion.get(`${product.id}-${city.id}`) ?? 50;
const priceInCity = calcRegionalSellPriceSync(product, knowledgeFactor, worthPercent); const grossPrice = calcRegionalSellPriceSync(product, knowledgeFactor, worthPercent);
if (priceInCity == null) continue; if (grossPrice == null) continue;
if (priceInCity <= currentRegionalPrice - PRICE_TOLERANCE) continue; const taxPercent = Number(taxByCity.get(city.id) || 0);
const transportPerPiece = currentRegionId && city.id !== currentRegionId ? (grossPrice * 0.01) : 0;
const taxableProfit = Math.max(0, grossPrice - pieceCost - transportPerPiece);
const taxPerPiece = taxableProfit * (taxPercent / 100);
const netPerPiece = grossPrice - pieceCost - taxPerPiece - transportPerPiece;
const netPerMinute = productionTime > 0 ? (netPerPiece / productionTime) : 0;
if (netPerMinute <= currentRegionalNetPerMinute + PRICE_TOLERANCE) continue;
results.push({ results.push({
regionId: city.id, regionId: city.id,
regionName: city.name, regionName: city.name,
price: priceInCity, price: Number(grossPrice.toFixed(2)),
branchType: branchTypeByCityId.get(city.id) ?? null grossPerMinute: Number((productionTime > 0 ? grossPrice / productionTime : 0).toFixed(2)),
netPerPiece: Number(netPerPiece.toFixed(2)),
netPerMinute: Number(netPerMinute.toFixed(2)),
taxPercent: Number(taxPercent.toFixed(2)),
transportPerPiece: Number(transportPerPiece.toFixed(2)),
branchType: branchTypeByCityId.get(city.id) ?? null,
}); });
} }
results.sort((a, b) => b.price - a.price); results.sort((a, b) => b.netPerMinute - a.netPerMinute);
out[product.id] = results; out[product.id] = results;
} }

View File

@@ -16,6 +16,7 @@ CREATE TABLE IF NOT EXISTS falukant_data.relationship_state (
maintenance_level integer NOT NULL DEFAULT 50 CHECK (maintenance_level >= 0 AND maintenance_level <= 100), maintenance_level integer NOT NULL DEFAULT 50 CHECK (maintenance_level >= 0 AND maintenance_level <= 100),
status_fit integer NOT NULL DEFAULT 0 CHECK (status_fit >= -2 AND status_fit <= 2), status_fit integer NOT NULL DEFAULT 0 CHECK (status_fit >= -2 AND status_fit <= 2),
monthly_base_cost integer NOT NULL DEFAULT 0 CHECK (monthly_base_cost >= 0), monthly_base_cost integer NOT NULL DEFAULT 0 CHECK (monthly_base_cost >= 0),
scandal_extra_daily_pct double precision NOT NULL DEFAULT 0 CHECK (scandal_extra_daily_pct >= 0 AND scandal_extra_daily_pct <= 100),
months_underfunded integer NOT NULL DEFAULT 0 CHECK (months_underfunded >= 0), months_underfunded integer NOT NULL DEFAULT 0 CHECK (months_underfunded >= 0),
active boolean NOT NULL DEFAULT true, active boolean NOT NULL DEFAULT true,
acknowledged boolean NOT NULL DEFAULT false, acknowledged boolean NOT NULL DEFAULT false,

View File

@@ -41,10 +41,10 @@
>, </span> >, </span>
<span <span
:class="['city-price', getCityPriceClass(city.branchType)]" :class="['city-price', getCityPriceClass(city.branchType)]"
:title="`${city.regionName}: ${formatPrice(city.price)}`" :title="`${city.regionName}: ${formatCityPrice(city, product)}`"
> >
<span class="city-name">{{ city.regionName }}</span> <span class="city-name">{{ city.regionName }}</span>
<span class="city-price-value">({{ formatPrice(city.price) }})</span> <span class="city-price-value">({{ formatCityPrice(city, product) }})</span>
</span> </span>
</template> </template>
</div> </div>
@@ -169,6 +169,14 @@
maximumFractionDigits: 2, maximumFractionDigits: 2,
}).format(price); }).format(price);
}, },
formatCityPrice(city, product) {
const absolute = Number(city?.price || 0);
const productionTime = Number(product?.productionTime || 0);
const perMinute = Number.isFinite(Number(city?.grossPerMinute))
? Number(city.grossPerMinute)
: (productionTime > 0 ? (absolute / productionTime) : 0);
return `${this.formatPrice(absolute)} / ${this.formatPrice(perMinute)}`;
},
}, },
}; };
</script> </script>

View File

@@ -796,6 +796,7 @@
"visibility": "Klaro ba", "visibility": "Klaro ba",
"discretion": "Diskreto", "discretion": "Diskreto",
"maintenance": "Suporta bulanan", "maintenance": "Suporta bulanan",
"scandalExtraDailyPct": "Dugang risgo sa iskandalo/adlaw",
"monthlyCost": "Gasto kada bulan", "monthlyCost": "Gasto kada bulan",
"politicalFreeSlotsHint": "Ang mga politikal nga opisina naghatag og {count} ka affair slot nga walay bulan nga suporta (ang barato nga relasyon una).", "politicalFreeSlotsHint": "Ang mga politikal nga opisina naghatag og {count} ka affair slot nga walay bulan nga suporta (ang barato nga relasyon una).",
"politicalFreeMaintenance": "Opisina (libre)", "politicalFreeMaintenance": "Opisina (libre)",

View File

@@ -761,6 +761,7 @@
"visibility": "Sichtbarkeit", "visibility": "Sichtbarkeit",
"discretion": "Diskretion", "discretion": "Diskretion",
"maintenance": "Unterhalt", "maintenance": "Unterhalt",
"scandalExtraDailyPct": "Zusatz-Skandalrisiko/Tag",
"monthlyCost": "Monatskosten", "monthlyCost": "Monatskosten",
"politicalFreeSlotsHint": "Politische Ämter gewähren dir {count} Liebschaftsplatz/-plätze ohne monatlichen Unterhalt (die günstigsten Beziehungen zählen zuerst).", "politicalFreeSlotsHint": "Politische Ämter gewähren dir {count} Liebschaftsplatz/-plätze ohne monatlichen Unterhalt (die günstigsten Beziehungen zählen zuerst).",
"politicalFreeMaintenance": "Amt (frei)", "politicalFreeMaintenance": "Amt (frei)",

View File

@@ -907,6 +907,7 @@
"visibility": "Visibility", "visibility": "Visibility",
"discretion": "Discretion", "discretion": "Discretion",
"maintenance": "Maintenance", "maintenance": "Maintenance",
"scandalExtraDailyPct": "Extra scandal risk/day",
"monthlyCost": "Monthly Cost", "monthlyCost": "Monthly Cost",
"politicalFreeSlotsHint": "Political offices grant you {count} affair slot(s) with no monthly upkeep (cheapest relationships count first).", "politicalFreeSlotsHint": "Political offices grant you {count} affair slot(s) with no monthly upkeep (cheapest relationships count first).",
"politicalFreeMaintenance": "Office (free)", "politicalFreeMaintenance": "Office (free)",

View File

@@ -761,6 +761,7 @@
"visibility": "Visibilidad", "visibility": "Visibilidad",
"discretion": "Discreción", "discretion": "Discreción",
"maintenance": "Mantenimiento", "maintenance": "Mantenimiento",
"scandalExtraDailyPct": "Riesgo extra de escándalo/día",
"monthlyCost": "Coste mensual", "monthlyCost": "Coste mensual",
"politicalFreeSlotsHint": "Los cargos políticos te conceden {count} plaza(s) de relación sin mantenimiento mensual (primero cuentan las relaciones más baratas).", "politicalFreeSlotsHint": "Los cargos políticos te conceden {count} plaza(s) de relación sin mantenimiento mensual (primero cuentan las relaciones más baratas).",
"politicalFreeMaintenance": "Cargo (gratis)", "politicalFreeMaintenance": "Cargo (gratis)",

View File

@@ -759,6 +759,7 @@
"visibility": "visibilité", "visibility": "visibilité",
"discretion": "discrétion", "discretion": "discrétion",
"maintenance": "Entretien", "maintenance": "Entretien",
"scandalExtraDailyPct": "Risque de scandale supplémentaire/jour",
"monthlyCost": "Coûts mensuels", "monthlyCost": "Coûts mensuels",
"politicalFreeSlotsHint": "Les bureaux politiques vous accordent {count} intérêts amoureux sans entretien mensuel (les relations les moins chères comptent en premier).", "politicalFreeSlotsHint": "Les bureaux politiques vous accordent {count} intérêts amoureux sans entretien mensuel (les relations les moins chères comptent en premier).",
"politicalFreeMaintenance": "Bureau (vacant)", "politicalFreeMaintenance": "Bureau (vacant)",

View File

@@ -436,6 +436,7 @@ export default {
vehicles: [], vehicles: [],
activeTab: 'production', activeTab: 'production',
productPricesCache: {}, // Cache für regionale Preise: { productId: price } productPricesCache: {}, // Cache für regionale Preise: { productId: price }
productNetMetricsCache: {}, // Netto-Metriken vom Backend: { productId: { netPerPiece, netPerMinute, ... } }
/** Cache-Schlüssel: Region + ob MAX(worth) über alle Filialregionen (bei Fahrzeug) */ /** Cache-Schlüssel: Region + ob MAX(worth) über alle Filialregionen (bei Fahrzeug) */
productPricesCacheKey: null, productPricesCacheKey: null,
tabs: [ tabs: [
@@ -739,6 +740,7 @@ export default {
async loadProductPricesForCurrentBranch() { async loadProductPricesForCurrentBranch() {
if (!this.selectedBranch || !this.selectedBranch.regionId) { if (!this.selectedBranch || !this.selectedBranch.regionId) {
this.productPricesCache = {}; this.productPricesCache = {};
this.productNetMetricsCache = {};
this.productPricesCacheKey = null; this.productPricesCacheKey = null;
return; return;
} }
@@ -755,6 +757,7 @@ export default {
} }
const { data } = await apiClient.get('/api/falukant/products/prices-in-region', { params }); const { data } = await apiClient.get('/api/falukant/products/prices-in-region', { params });
this.productPricesCache = data.prices || {}; this.productPricesCache = data.prices || {};
this.productNetMetricsCache = data.netMetrics || {};
this.productPricesCacheKey = cacheKey; this.productPricesCacheKey = cacheKey;
} catch (error) { } catch (error) {
console.error(`Error loading product prices for region ${this.selectedBranch.regionId}:`, error); console.error(`Error loading product prices for region ${this.selectedBranch.regionId}:`, error);
@@ -780,6 +783,7 @@ export default {
} }
} }
this.productPricesCache = prices; this.productPricesCache = prices;
this.productNetMetricsCache = {};
this.productPricesCacheKey = cacheKey; this.productPricesCacheKey = cacheKey;
} }
}, },
@@ -873,6 +877,13 @@ export default {
}, },
calculateProductProfit(product) { calculateProductProfit(product) {
const netMetrics = this.productNetMetricsCache?.[product.id];
if (netMetrics && Number.isFinite(Number(netMetrics.netPerPiece)) && Number.isFinite(Number(netMetrics.netPerMinute))) {
return {
absolute: Number(netMetrics.netPerPiece).toFixed(2),
perMinute: Number(netMetrics.netPerMinute).toFixed(2),
};
}
const { absolute: revenueAbsoluteStr, perMinute: revenuePerMinuteStr } const { absolute: revenueAbsoluteStr, perMinute: revenuePerMinuteStr }
= this.calculateProductRevenue(product); = this.calculateProductRevenue(product);
const revenueAbsolute = parseFloat(revenueAbsoluteStr); const revenueAbsolute = parseFloat(revenueAbsoluteStr);

View File

@@ -397,6 +397,10 @@
<dt>{{ $t('falukant.family.lovers.maintenance') }}</dt> <dt>{{ $t('falukant.family.lovers.maintenance') }}</dt>
<dd>{{ lover.maintenanceLevel }}</dd> <dd>{{ lover.maintenanceLevel }}</dd>
</div> </div>
<div>
<dt>{{ $t('falukant.family.lovers.scandalExtraDailyPct') }}</dt>
<dd>{{ Number(lover.scandalExtraDailyPct || 0).toFixed(1) }}%</dd>
</div>
<div> <div>
<dt>{{ $t('falukant.family.lovers.monthlyCost') }}</dt> <dt>{{ $t('falukant.family.lovers.monthlyCost') }}</dt>
<dd> <dd>