feat(political-office): enhance political office benefits and salary computation
All checks were successful
Deploy to production / deploy (push) Successful in 3m6s
All checks were successful
Deploy to production / deploy (push) Successful in 3m6s
- Added a new hierarchyLevel field to PoliticalOfficeType for better categorization of political roles. - Updated computePoliticalDailySalaryPayout function to incorporate hierarchy level in salary calculations, allowing for more dynamic salary adjustments based on office rank. - Modified SQL scripts to reflect changes in political office benefits, ensuring compatibility with the new salary structure. - Enhanced localization files to support updated benefit descriptions and salary formats across multiple languages. - Improved UI components to display the new salary calculations and benefits accurately in the PoliticsView.
This commit is contained in:
@@ -0,0 +1,50 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/** Stufe pro politischem Amt (Tageshonorar: base + perRank × hierarchy_level). */
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_type.political_office_type
|
||||||
|
ADD COLUMN IF NOT EXISTS hierarchy_level INTEGER NOT NULL DEFAULT 1;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
UPDATE falukant_type.political_office_type AS pot
|
||||||
|
SET hierarchy_level = sub.lvl
|
||||||
|
FROM (VALUES
|
||||||
|
('assessor', 1),
|
||||||
|
('councillor', 1),
|
||||||
|
('council', 2),
|
||||||
|
('beadle', 2),
|
||||||
|
('town-clerk', 2),
|
||||||
|
('mayor', 3),
|
||||||
|
('master-builder', 2),
|
||||||
|
('village-major', 2),
|
||||||
|
('judge', 3),
|
||||||
|
('bailif', 3),
|
||||||
|
('taxman', 2),
|
||||||
|
('sheriff', 3),
|
||||||
|
('consultant', 3),
|
||||||
|
('treasurer', 4),
|
||||||
|
('hangman', 2),
|
||||||
|
('territorial-council', 3),
|
||||||
|
('territorial-council-speaker', 4),
|
||||||
|
('ruler-consultant', 4),
|
||||||
|
('state-administrator', 4),
|
||||||
|
('super-state-administrator', 5),
|
||||||
|
('governor', 5),
|
||||||
|
('ministry-helper', 4),
|
||||||
|
('minister', 5),
|
||||||
|
('chancellor', 6)
|
||||||
|
) AS sub(name, lvl)
|
||||||
|
WHERE pot.name = sub.name;
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_type.political_office_type
|
||||||
|
DROP COLUMN IF EXISTS hierarchy_level;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -17,10 +17,18 @@ PoliticalOfficeType.init({
|
|||||||
regionType: {
|
regionType: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false},
|
allowNull: false},
|
||||||
termLength: {
|
termLength: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: 0}}, {
|
defaultValue: 0},
|
||||||
|
/** Stufe für Tageshonorar (base + perRank × level) und Sortierung; 1 = niedrigstes politisches Level */
|
||||||
|
hierarchyLevel: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 1,
|
||||||
|
field: 'hierarchy_level'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
sequelize,
|
sequelize,
|
||||||
modelName: 'PoliticalOfficeType',
|
modelName: 'PoliticalOfficeType',
|
||||||
tableName: 'political_office_type',
|
tableName: 'political_office_type',
|
||||||
|
|||||||
@@ -134,15 +134,53 @@ const POLITICAL_OFFICE_RANKS = {
|
|||||||
chancellor: 6
|
chancellor: 6
|
||||||
};
|
};
|
||||||
|
|
||||||
function computePoliticalDailySalaryPayout(value, officeName) {
|
const _envPolSalaryStart = Number(process.env.POLITICAL_DAILY_SALARY_START);
|
||||||
|
const _envPolSalaryGrowth = Number(process.env.POLITICAL_DAILY_SALARY_GROWTH);
|
||||||
|
/** Tageshonorar niedrigste Stufe (Default ~50; per ENV überschreibbar). */
|
||||||
|
const POLITICAL_DAILY_SALARY_START_DEFAULT =
|
||||||
|
Number.isFinite(_envPolSalaryStart) && _envPolSalaryStart > 0 ? _envPolSalaryStart : 50;
|
||||||
|
/** Pro hierarchy_level leicht exponentiell (Default 1,22; per ENV überschreibbar). */
|
||||||
|
const POLITICAL_DAILY_SALARY_GROWTH_DEFAULT =
|
||||||
|
Number.isFinite(_envPolSalaryGrowth) && _envPolSalaryGrowth > 1 ? _envPolSalaryGrowth : 1.22;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tageshonorar: value.dailyAmount fest, sonst salaryStart × salaryGrowth^(Stufe−1).
|
||||||
|
* Stufe: value.rank (JSON), sonst hierarchy_level, sonst POLITICAL_OFFICE_RANKS[name].
|
||||||
|
* Optional im JSON: salaryStart, salaryGrowth (pro Amt); ENV POLITICAL_DAILY_SALARY_* (global).
|
||||||
|
* Legacy linear: nur bei salaryFormula === 'linear' mit base und perRank.
|
||||||
|
*/
|
||||||
|
function computePoliticalDailySalaryPayout(value, officeName, hierarchyLevelFromType) {
|
||||||
const v = value && typeof value === 'object' ? value : {};
|
const v = value && typeof value === 'object' ? value : {};
|
||||||
if (v.dailyAmount != null && Number.isFinite(Number(v.dailyAmount))) {
|
if (v.dailyAmount != null && Number.isFinite(Number(v.dailyAmount))) {
|
||||||
return Math.round(Number(v.dailyAmount) * 100) / 100;
|
return Math.round(Number(v.dailyAmount) * 100) / 100;
|
||||||
}
|
}
|
||||||
const rank = POLITICAL_OFFICE_RANKS[officeName] ?? 0;
|
const nameKey = typeof officeName === 'string' ? officeName.trim() : officeName;
|
||||||
const base = Number(v.base ?? 0);
|
let rank = 0;
|
||||||
const perRank = Number(v.perRank ?? v.per_rank ?? 0);
|
if (v.rank != null && Number.isFinite(Number(v.rank))) {
|
||||||
return Math.round((base + perRank * rank) * 100) / 100;
|
rank = Math.max(0, Number(v.rank));
|
||||||
|
} else {
|
||||||
|
const fromDb = Number(hierarchyLevelFromType);
|
||||||
|
if (Number.isFinite(fromDb) && fromDb > 0) {
|
||||||
|
rank = fromDb;
|
||||||
|
} else {
|
||||||
|
rank = POLITICAL_OFFICE_RANKS[nameKey] ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rank < 1) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (v.salaryFormula === 'linear') {
|
||||||
|
const base = Number(v.base ?? 0);
|
||||||
|
const perRank = Number(v.perRank ?? v.per_rank ?? 0);
|
||||||
|
return Math.round((base + perRank * rank) * 100) / 100;
|
||||||
|
}
|
||||||
|
const startRaw = Number(v.salaryStart ?? v.salary_start);
|
||||||
|
const growthRaw = Number(v.salaryGrowth ?? v.salary_growth);
|
||||||
|
const start = Number.isFinite(startRaw) && startRaw > 0 ? startRaw : POLITICAL_DAILY_SALARY_START_DEFAULT;
|
||||||
|
const growth =
|
||||||
|
Number.isFinite(growthRaw) && growthRaw > 1 ? growthRaw : POLITICAL_DAILY_SALARY_GROWTH_DEFAULT;
|
||||||
|
const amount = start * growth ** (rank - 1);
|
||||||
|
return Math.round(amount * 100) / 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CERTIFICATE_THRESHOLDS = {
|
const CERTIFICATE_THRESHOLDS = {
|
||||||
@@ -6089,7 +6127,7 @@ class FalukantService extends BaseService {
|
|||||||
return this.healthChange(user, delta);
|
return this.healthChange(user, delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
politicsBenefitEntriesFromRows(benefitRows, officeName) {
|
politicsBenefitEntriesFromRows(benefitRows, officeName, hierarchyLevelFromType) {
|
||||||
if (!benefitRows?.length) {
|
if (!benefitRows?.length) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -6106,7 +6144,7 @@ class FalukantService extends BaseService {
|
|||||||
out.push({ tr: 'tax_exemption', params: { regions } });
|
out.push({ tr: 'tax_exemption', params: { regions } });
|
||||||
}
|
}
|
||||||
} else if (tr === 'daily_salary' || tr === 'salary') {
|
} else if (tr === 'daily_salary' || tr === 'salary') {
|
||||||
const amount = computePoliticalDailySalaryPayout(v, officeName);
|
const amount = computePoliticalDailySalaryPayout(v, officeName, hierarchyLevelFromType);
|
||||||
if (amount > 0) {
|
if (amount > 0) {
|
||||||
out.push({ tr: 'daily_salary', params: { amount } });
|
out.push({ tr: 'daily_salary', params: { amount } });
|
||||||
}
|
}
|
||||||
@@ -6150,7 +6188,7 @@ class FalukantService extends BaseService {
|
|||||||
|
|
||||||
const held = await PoliticalOffice.findAll({
|
const held = await PoliticalOffice.findAll({
|
||||||
where: { characterId },
|
where: { characterId },
|
||||||
include: [{ model: PoliticalOfficeType, as: 'type', attributes: ['name'] }]
|
include: [{ model: PoliticalOfficeType, as: 'type', attributes: ['name', 'hierarchyLevel'] }]
|
||||||
});
|
});
|
||||||
if (!held.length) {
|
if (!held.length) {
|
||||||
return;
|
return;
|
||||||
@@ -6180,7 +6218,8 @@ class FalukantService extends BaseService {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const v = typeof row.value === 'object' && row.value ? row.value : {};
|
const v = typeof row.value === 'object' && row.value ? row.value : {};
|
||||||
total += computePoliticalDailySalaryPayout(v, name);
|
const level = h.type?.hierarchyLevel;
|
||||||
|
total += computePoliticalDailySalaryPayout(v, name, level);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6235,7 +6274,7 @@ class FalukantService extends BaseService {
|
|||||||
{
|
{
|
||||||
model: PoliticalOfficeType,
|
model: PoliticalOfficeType,
|
||||||
as: 'type',
|
as: 'type',
|
||||||
attributes: ['name', 'termLength']
|
attributes: ['name', 'termLength', 'hierarchyLevel']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
model: RegionData,
|
model: RegionData,
|
||||||
@@ -6311,7 +6350,8 @@ class FalukantService extends BaseService {
|
|||||||
const officeName = o.type?.name;
|
const officeName = o.type?.name;
|
||||||
const benefit = this.politicsBenefitEntriesFromRows(
|
const benefit = this.politicsBenefitEntriesFromRows(
|
||||||
benefitByType.get(o.officeTypeId) || [],
|
benefitByType.get(o.officeTypeId) || [],
|
||||||
officeName
|
officeName,
|
||||||
|
o.type?.hierarchyLevel
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
35
backend/sql/add_political_office_hierarchy_level.sql
Normal file
35
backend/sql/add_political_office_hierarchy_level.sql
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
-- Stufe pro politischem Amt (Tageshonorar: exponentielles Modell nach Stufe im Backend).
|
||||||
|
-- Entspricht Migration 20260403120000-political-office-hierarchy-level.cjs
|
||||||
|
|
||||||
|
ALTER TABLE falukant_type.political_office_type
|
||||||
|
ADD COLUMN IF NOT EXISTS hierarchy_level INTEGER NOT NULL DEFAULT 1;
|
||||||
|
|
||||||
|
UPDATE falukant_type.political_office_type AS pot
|
||||||
|
SET hierarchy_level = sub.lvl
|
||||||
|
FROM (VALUES
|
||||||
|
('assessor', 1),
|
||||||
|
('councillor', 1),
|
||||||
|
('council', 2),
|
||||||
|
('beadle', 2),
|
||||||
|
('town-clerk', 2),
|
||||||
|
('mayor', 3),
|
||||||
|
('master-builder', 2),
|
||||||
|
('village-major', 2),
|
||||||
|
('judge', 3),
|
||||||
|
('bailif', 3),
|
||||||
|
('taxman', 2),
|
||||||
|
('sheriff', 3),
|
||||||
|
('consultant', 3),
|
||||||
|
('treasurer', 4),
|
||||||
|
('hangman', 2),
|
||||||
|
('territorial-council', 3),
|
||||||
|
('territorial-council-speaker', 4),
|
||||||
|
('ruler-consultant', 4),
|
||||||
|
('state-administrator', 4),
|
||||||
|
('super-state-administrator', 5),
|
||||||
|
('governor', 5),
|
||||||
|
('ministry-helper', 4),
|
||||||
|
('minister', 5),
|
||||||
|
('chancellor', 6)
|
||||||
|
) AS sub(name, lvl)
|
||||||
|
WHERE pot.name = sub.name;
|
||||||
@@ -21,8 +21,11 @@
|
|||||||
-- psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f backend/sql/falukant_political_office_benefits.sql
|
-- psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f backend/sql/falukant_political_office_benefits.sql
|
||||||
--
|
--
|
||||||
-- JSON value daily_salary:
|
-- JSON value daily_salary:
|
||||||
-- • dailyAmount: fester Tagesbetrag (überschreibt base/perRank)
|
-- • dailyAmount: fester Tagesbetrag (überschreibt alles)
|
||||||
-- • base + perRank: Auszahlung = base + (Amtsrang * perRank)
|
-- • rank: optional feste Stufe in JSON (überschreibt DB-Stufe)
|
||||||
|
-- • Standard: salaryStart × salaryGrowth^(Stufe−1); Stufe = hierarchy_level (s. Migration political-office-hierarchy-level)
|
||||||
|
-- • Optional: salaryStart, salaryGrowth pro Amt; ENV POLITICAL_DAILY_SALARY_START / POLITICAL_DAILY_SALARY_GROWTH
|
||||||
|
-- • Legacy linear: salaryFormula = 'linear' und base, perRank
|
||||||
-- JSON value tax_exemption:
|
-- JSON value tax_exemption:
|
||||||
-- • regions: Text-Array (z. B. "city") oder "*" für alle Ebenen
|
-- • regions: Text-Array (z. B. "city") oder "*" für alle Ebenen
|
||||||
-- =============================================================================
|
-- =============================================================================
|
||||||
@@ -145,9 +148,9 @@ WHERE ot.name = 'chancellor'
|
|||||||
WHERE x.office_type_id = ot.id AND x.benefit_type_id = bt.id
|
WHERE x.office_type_id = ot.id AND x.benefit_type_id = bt.id
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Tageslohn: base + Amtsrang * perRank (Rang in App: POLITICAL_OFFICE_RANKS)
|
-- Tageslohn: Betrag aus App-Formel (exponentiell nach hierarchy_level); value leer = Defaults
|
||||||
INSERT INTO falukant_predefine.political_office_benefit (office_type_id, benefit_type_id, value)
|
INSERT INTO falukant_predefine.political_office_benefit (office_type_id, benefit_type_id, value)
|
||||||
SELECT ot.id, bt.id, '{"base":4,"perRank":11}'::jsonb
|
SELECT ot.id, bt.id, '{}'::jsonb
|
||||||
FROM falukant_type.political_office_type ot
|
FROM falukant_type.political_office_type ot
|
||||||
JOIN falukant_type.political_office_benefit_type bt ON bt.tr = 'daily_salary'
|
JOIN falukant_type.political_office_benefit_type bt ON bt.tr = 'daily_salary'
|
||||||
WHERE ot.name IN (
|
WHERE ot.name IN (
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ WHERE ot.name = 'chancellor'
|
|||||||
|
|
||||||
-- ========== Schritt 7 ==========
|
-- ========== Schritt 7 ==========
|
||||||
INSERT INTO falukant_predefine.political_office_benefit (office_type_id, benefit_type_id, value)
|
INSERT INTO falukant_predefine.political_office_benefit (office_type_id, benefit_type_id, value)
|
||||||
SELECT ot.id, bt.id, '{"base":4,"perRank":11}'::jsonb
|
SELECT ot.id, bt.id, '{}'::jsonb
|
||||||
FROM falukant_type.political_office_type ot
|
FROM falukant_type.political_office_type ot
|
||||||
JOIN falukant_type.political_office_benefit_type bt ON bt.tr = 'daily_salary'
|
JOIN falukant_type.political_office_benefit_type bt ON bt.tr = 'daily_salary'
|
||||||
WHERE ot.name IN (
|
WHERE ot.name IN (
|
||||||
|
|||||||
@@ -421,30 +421,30 @@ const politicalOfficeExtraBenefitSeeds = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const politicalOffices = [
|
const politicalOffices = [
|
||||||
{ tr: "assessor", seatsPerRegion: 10, regionType: "city", termLength: 5 },
|
{ tr: "assessor", seatsPerRegion: 10, regionType: "city", termLength: 5, hierarchyLevel: 1 },
|
||||||
{ tr: "councillor", seatsPerRegion: 7, regionType: "city", termLength: 7 },
|
{ tr: "councillor", seatsPerRegion: 7, regionType: "city", termLength: 7, hierarchyLevel: 1 },
|
||||||
{ tr: "council", seatsPerRegion: 5, regionType: "city", termLength: 4 },
|
{ tr: "council", seatsPerRegion: 5, regionType: "city", termLength: 4, hierarchyLevel: 2 },
|
||||||
{ tr: "beadle", seatsPerRegion: 1, regionType: "city", termLength: 6 },
|
{ tr: "beadle", seatsPerRegion: 1, regionType: "city", termLength: 6, hierarchyLevel: 2 },
|
||||||
{ tr: "town-clerk", seatsPerRegion: 3, regionType: "city", termLength: 10 },
|
{ tr: "town-clerk", seatsPerRegion: 3, regionType: "city", termLength: 10, hierarchyLevel: 2 },
|
||||||
{ tr: "mayor", seatsPerRegion: 1, regionType: "city", termLength: 3 },
|
{ tr: "mayor", seatsPerRegion: 1, regionType: "city", termLength: 3, hierarchyLevel: 3 },
|
||||||
{ tr: "master-builder", seatsPerRegion: 10, regionType: "county", termLength: 10 },
|
{ tr: "master-builder", seatsPerRegion: 10, regionType: "county", termLength: 10, hierarchyLevel: 2 },
|
||||||
{ tr: "village-major", seatsPerRegion: 6, regionType: "county", termLength: 5 },
|
{ tr: "village-major", seatsPerRegion: 6, regionType: "county", termLength: 5, hierarchyLevel: 2 },
|
||||||
{ tr: "judge", seatsPerRegion: 3, regionType: "county", termLength: 8 },
|
{ tr: "judge", seatsPerRegion: 3, regionType: "county", termLength: 8, hierarchyLevel: 3 },
|
||||||
{ tr: "bailif", seatsPerRegion: 1, regionType: "county", termLength: 4 },
|
{ tr: "bailif", seatsPerRegion: 1, regionType: "county", termLength: 4, hierarchyLevel: 3 },
|
||||||
{ tr: "taxman", seatsPerRegion: 8, regionType: "shire", termLength: 5 },
|
{ tr: "taxman", seatsPerRegion: 8, regionType: "shire", termLength: 5, hierarchyLevel: 2 },
|
||||||
{ tr: "sheriff", seatsPerRegion: 5, regionType: "shire", termLength: 8 },
|
{ tr: "sheriff", seatsPerRegion: 5, regionType: "shire", termLength: 8, hierarchyLevel: 3 },
|
||||||
{ tr: "consultant", seatsPerRegion: 3, regionType: "shire", termLength: 9 },
|
{ tr: "consultant", seatsPerRegion: 3, regionType: "shire", termLength: 9, hierarchyLevel: 3 },
|
||||||
{ tr: "treasurer", seatsPerRegion: 1, regionType: "shire", termLength: 7 },
|
{ tr: "treasurer", seatsPerRegion: 1, regionType: "shire", termLength: 7, hierarchyLevel: 4 },
|
||||||
{ tr: "hangman", seatsPerRegion: 9, regionType: "markgravate", termLength: 5 },
|
{ tr: "hangman", seatsPerRegion: 9, regionType: "markgravate", termLength: 5, hierarchyLevel: 2 },
|
||||||
{ tr: "territorial-council", seatsPerRegion: 6, regionType: "markgravate", termLength: 6 },
|
{ tr: "territorial-council", seatsPerRegion: 6, regionType: "markgravate", termLength: 6, hierarchyLevel: 3 },
|
||||||
{ tr: "territorial-council-speaker", seatsPerRegion: 4, regionType: "markgravate", termLength: 8 },
|
{ tr: "territorial-council-speaker", seatsPerRegion: 4, regionType: "markgravate", termLength: 8, hierarchyLevel: 4 },
|
||||||
{ tr: "ruler-consultant", seatsPerRegion: 1, regionType: "markgravate", termLength: 3 },
|
{ tr: "ruler-consultant", seatsPerRegion: 1, regionType: "markgravate", termLength: 3, hierarchyLevel: 4 },
|
||||||
{ tr: "state-administrator", seatsPerRegion: 7, regionType: "duchy", termLength: 3 },
|
{ tr: "state-administrator", seatsPerRegion: 7, regionType: "duchy", termLength: 3, hierarchyLevel: 4 },
|
||||||
{ tr: "super-state-administrator", seatsPerRegion: 5, regionType: "duchy", termLength: 6 },
|
{ tr: "super-state-administrator", seatsPerRegion: 5, regionType: "duchy", termLength: 6, hierarchyLevel: 5 },
|
||||||
{ tr: "governor", seatsPerRegion: 1, regionType: "duchy", termLength: 5 },
|
{ tr: "governor", seatsPerRegion: 1, regionType: "duchy", termLength: 5, hierarchyLevel: 5 },
|
||||||
{ tr: "ministry-helper", seatsPerRegion: 12, regionType: "country", termLength: 4 },
|
{ tr: "ministry-helper", seatsPerRegion: 12, regionType: "country", termLength: 4, hierarchyLevel: 4 },
|
||||||
{ tr: "minister", seatsPerRegion: 3, regionType: "country", termLength: 4 },
|
{ tr: "minister", seatsPerRegion: 3, regionType: "country", termLength: 4, hierarchyLevel: 5 },
|
||||||
{ tr: "chancellor", seatsPerRegion: 1, regionType: "country", termLength: 4 }
|
{ tr: "chancellor", seatsPerRegion: 1, regionType: "country", termLength: 4, hierarchyLevel: 6 }
|
||||||
];
|
];
|
||||||
|
|
||||||
const politicalOfficePrerequisites = [
|
const politicalOfficePrerequisites = [
|
||||||
@@ -1063,14 +1063,22 @@ export const initializePoliticalOfficeBenefitTypes = async () => {
|
|||||||
|
|
||||||
export const initializePoliticalOfficeTypes = async () => {
|
export const initializePoliticalOfficeTypes = async () => {
|
||||||
for (const po of politicalOffices) {
|
for (const po of politicalOffices) {
|
||||||
await PoliticalOfficeType.findOrCreate({
|
const level = Number(po.hierarchyLevel) >= 1 ? Number(po.hierarchyLevel) : 1;
|
||||||
|
const [row, created] = await PoliticalOfficeType.findOrCreate({
|
||||||
where: { name: po.tr },
|
where: { name: po.tr },
|
||||||
defaults: {
|
defaults: {
|
||||||
seatsPerRegion: po.seatsPerRegion,
|
seatsPerRegion: po.seatsPerRegion,
|
||||||
regionType: po.regionType,
|
regionType: po.regionType,
|
||||||
termLength: po.termLength
|
termLength: po.termLength,
|
||||||
|
hierarchyLevel: level
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (!created) {
|
||||||
|
await PoliticalOfficeType.update(
|
||||||
|
{ hierarchyLevel: level },
|
||||||
|
{ where: { id: row.id } }
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1141,7 +1149,7 @@ export const initializePoliticalOfficeBenefits = async () => {
|
|||||||
defaults: {
|
defaults: {
|
||||||
officeTypeId: office.id,
|
officeTypeId: office.id,
|
||||||
benefitTypeId: dailyType.id,
|
benefitTypeId: dailyType.id,
|
||||||
value: { base: 4, perRank: 11 }
|
value: {}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (wasCreated) dailyCreated += 1;
|
if (wasCreated) dailyCreated += 1;
|
||||||
|
|||||||
@@ -436,7 +436,7 @@
|
|||||||
"voteAllError": "Sayop sa paghatag sa mga boto",
|
"voteAllError": "Sayop sa paghatag sa mga boto",
|
||||||
"applyError": "Dili mapadala ang aplikasyon.",
|
"applyError": "Dili mapadala ang aplikasyon.",
|
||||||
"benefits": {
|
"benefits": {
|
||||||
"daily_salary": "Adlaw-adlaw nga suhol (usa ra kada adlaw): mga {amount}",
|
"daily_salary": "Adlaw-adlaw nga suhol (usa ra kada adlaw): {amount}",
|
||||||
"tax_exemption": "Wa’y buhis: {regions}",
|
"tax_exemption": "Wa’y buhis: {regions}",
|
||||||
"tax_exemption_all": "Wa’y buhis: tanang lebel sa rehiyon",
|
"tax_exemption_all": "Wa’y buhis: tanang lebel sa rehiyon",
|
||||||
"reputation_periodic": "+{gain} reputasyon matag {days} ka adlaw (benepisyo sa opisina)",
|
"reputation_periodic": "+{gain} reputasyon matag {days} ka adlaw (benepisyo sa opisina)",
|
||||||
|
|||||||
@@ -391,6 +391,8 @@
|
|||||||
"reviewPriorityTitle": "Hinay-hinay nga gisagol ang balik-balik",
|
"reviewPriorityTitle": "Hinay-hinay nga gisagol ang balik-balik",
|
||||||
"reviewPriorityIntro": "Sa sinugdan, ang pokus anaa sa bag-ong mga pulong niining leksiyona. Samtang mopadayon ka, hinay-hinay nga masagol ang daan nga bokabularyo.",
|
"reviewPriorityIntro": "Sa sinugdan, ang pokus anaa sa bag-ong mga pulong niining leksiyona. Samtang mopadayon ka, hinay-hinay nga masagol ang daan nga bokabularyo.",
|
||||||
"exerciseLockTitle": "Naka-lock pa ang chapter test",
|
"exerciseLockTitle": "Naka-lock pa ang chapter test",
|
||||||
|
"exerciseUnlockHintTrainerCore": "Ma-unlock ang chapter test kung natuman ang tulo ka kondisyon: labing menos {newTarget} ka pangutana bahin sa bag-ong sulod (tan-awa ang “Bag-ong sulod”), mga {attempts} ka pangutana sa trainer sa kinatibuk-an, ug dili ubos sa {rate}% nga success rate.",
|
||||||
|
"exerciseUnlockHintTrainerMixSuffix": "Ang daan nga bokabularyo hinay-hinay nga gisagol.",
|
||||||
"trainerStartWithReview": "Sugdi sa bag-ong bokabularyo niining leksiyona. Samtang nagpraktis ka, awtomatikong isagol sa trainer ang angay nga balik-balik.",
|
"trainerStartWithReview": "Sugdi sa bag-ong bokabularyo niining leksiyona. Samtang nagpraktis ka, awtomatikong isagol sa trainer ang angay nga balik-balik.",
|
||||||
"startLesson": "Sugdi ang leksiyon",
|
"startLesson": "Sugdi ang leksiyon",
|
||||||
"trainerProgressNewContent": "Bag-ong sulod: {current}/{target}",
|
"trainerProgressNewContent": "Bag-ong sulod: {current}/{target}",
|
||||||
|
|||||||
@@ -1357,7 +1357,7 @@
|
|||||||
"voteAllError": "Fehler beim Abgeben der Stimmen",
|
"voteAllError": "Fehler beim Abgeben der Stimmen",
|
||||||
"applyError": "Bewerbung konnte nicht eingereicht werden.",
|
"applyError": "Bewerbung konnte nicht eingereicht werden.",
|
||||||
"benefits": {
|
"benefits": {
|
||||||
"daily_salary": "Tagesamtshonorar (einmal pro Tag): ca. {amount}",
|
"daily_salary": "Tagesamtshonorar (einmal pro Tag): {amount}",
|
||||||
"tax_exemption": "Steuerbefreiung: {regions}",
|
"tax_exemption": "Steuerbefreiung: {regions}",
|
||||||
"tax_exemption_all": "Steuerbefreiung: alle Regionsebenen",
|
"tax_exemption_all": "Steuerbefreiung: alle Regionsebenen",
|
||||||
"reputation_periodic": "+{gain} Ansehen alle {days} Tage (Amtsbonus)",
|
"reputation_periodic": "+{gain} Ansehen alle {days} Tage (Amtsbonus)",
|
||||||
|
|||||||
@@ -746,6 +746,8 @@
|
|||||||
"reviewPriorityTitle": "Wiederholung läuft schrittweise mit",
|
"reviewPriorityTitle": "Wiederholung läuft schrittweise mit",
|
||||||
"reviewPriorityIntro": "Zuerst liegt der Fokus auf den neuen Begriffen dieser Lektion. Mit deinem Fortschritt fließen ältere Vokabeln dann zunehmend mit ein.",
|
"reviewPriorityIntro": "Zuerst liegt der Fokus auf den neuen Begriffen dieser Lektion. Mit deinem Fortschritt fließen ältere Vokabeln dann zunehmend mit ein.",
|
||||||
"exerciseLockTitle": "Kapitel-Prüfung noch gesperrt",
|
"exerciseLockTitle": "Kapitel-Prüfung noch gesperrt",
|
||||||
|
"exerciseUnlockHintTrainerCore": "Die Kapitel-Prüfung wird freigeschaltet, wenn alle drei Bedingungen erfüllt sind: mindestens {newTarget} Fragen zu den neuen Inhalten (Zeile „Neue Inhalte“), insgesamt etwa {attempts} Trainerfragen und mindestens {rate} % Erfolgsrate.",
|
||||||
|
"exerciseUnlockHintTrainerMixSuffix": "Ältere Vokabeln fließen dabei schrittweise mit ein.",
|
||||||
"trainerStartWithReview": "Starte mit den neuen Vokabeln dieser Lektion. Mit fortschreitendem Üben mischt der Trainer automatisch passende Wiederholungen ein.",
|
"trainerStartWithReview": "Starte mit den neuen Vokabeln dieser Lektion. Mit fortschreitendem Üben mischt der Trainer automatisch passende Wiederholungen ein.",
|
||||||
"startLesson": "Lektion starten",
|
"startLesson": "Lektion starten",
|
||||||
"trainerProgressNewContent": "Neue Inhalte: {current}/{target}",
|
"trainerProgressNewContent": "Neue Inhalte: {current}/{target}",
|
||||||
|
|||||||
@@ -576,7 +576,7 @@
|
|||||||
"voteAllError": "Error while submitting the votes",
|
"voteAllError": "Error while submitting the votes",
|
||||||
"applyError": "Application could not be submitted.",
|
"applyError": "Application could not be submitted.",
|
||||||
"benefits": {
|
"benefits": {
|
||||||
"daily_salary": "Daily office stipend (once per day): about {amount}",
|
"daily_salary": "Daily office stipend (once per day): {amount}",
|
||||||
"tax_exemption": "Tax exemption: {regions}",
|
"tax_exemption": "Tax exemption: {regions}",
|
||||||
"tax_exemption_all": "Tax exemption: all regional levels",
|
"tax_exemption_all": "Tax exemption: all regional levels",
|
||||||
"reputation_periodic": "+{gain} reputation every {days} days (office bonus)",
|
"reputation_periodic": "+{gain} reputation every {days} days (office bonus)",
|
||||||
|
|||||||
@@ -746,6 +746,8 @@
|
|||||||
"reviewPriorityTitle": "Review is mixed in step by step",
|
"reviewPriorityTitle": "Review is mixed in step by step",
|
||||||
"reviewPriorityIntro": "The focus starts on the new terms of this lesson. As you progress, older vocabulary is gradually mixed in.",
|
"reviewPriorityIntro": "The focus starts on the new terms of this lesson. As you progress, older vocabulary is gradually mixed in.",
|
||||||
"exerciseLockTitle": "Chapter test still locked",
|
"exerciseLockTitle": "Chapter test still locked",
|
||||||
|
"exerciseUnlockHintTrainerCore": "The chapter test unlocks when all three conditions are met: at least {newTarget} questions on new content (see “New content”), about {attempts} trainer questions in total, and a success rate of at least {rate}%.",
|
||||||
|
"exerciseUnlockHintTrainerMixSuffix": "Older vocabulary is mixed in gradually.",
|
||||||
"trainerStartWithReview": "Start with the new vocabulary from this lesson. As you practice, the trainer will automatically mix in fitting review items.",
|
"trainerStartWithReview": "Start with the new vocabulary from this lesson. As you practice, the trainer will automatically mix in fitting review items.",
|
||||||
"startLesson": "Start lesson",
|
"startLesson": "Start lesson",
|
||||||
"trainerProgressNewContent": "New content: {current}/{target}",
|
"trainerProgressNewContent": "New content: {current}/{target}",
|
||||||
|
|||||||
@@ -1265,7 +1265,7 @@
|
|||||||
"elections": "Elecciones"
|
"elections": "Elecciones"
|
||||||
},
|
},
|
||||||
"benefits": {
|
"benefits": {
|
||||||
"daily_salary": "Estipendio diario (una vez al día): unos {amount}",
|
"daily_salary": "Estipendio diario (una vez al día): {amount}",
|
||||||
"tax_exemption": "Exención fiscal: {regions}",
|
"tax_exemption": "Exención fiscal: {regions}",
|
||||||
"tax_exemption_all": "Exención fiscal: todos los niveles regionales",
|
"tax_exemption_all": "Exención fiscal: todos los niveles regionales",
|
||||||
"reputation_periodic": "+{gain} reputación cada {days} días (bono de cargo)",
|
"reputation_periodic": "+{gain} reputación cada {days} días (bono de cargo)",
|
||||||
|
|||||||
@@ -744,6 +744,8 @@
|
|||||||
"reviewPriorityTitle": "El repaso se mezcla paso a paso",
|
"reviewPriorityTitle": "El repaso se mezcla paso a paso",
|
||||||
"reviewPriorityIntro": "Primero el foco está en los términos nuevos de esta lección. Con tu progreso se van mezclando cada vez más vocablos anteriores.",
|
"reviewPriorityIntro": "Primero el foco está en los términos nuevos de esta lección. Con tu progreso se van mezclando cada vez más vocablos anteriores.",
|
||||||
"exerciseLockTitle": "La prueba del capítulo sigue bloqueada",
|
"exerciseLockTitle": "La prueba del capítulo sigue bloqueada",
|
||||||
|
"exerciseUnlockHintTrainerCore": "La prueba del capítulo se desbloquea cuando se cumplen las tres condiciones: al menos {newTarget} preguntas sobre el contenido nuevo (línea «Contenido nuevo»), unas {attempts} preguntas del entrenador en total y una tasa de aciertos de al menos {rate}%.",
|
||||||
|
"exerciseUnlockHintTrainerMixSuffix": "El vocabulario anterior se mezcla poco a poco.",
|
||||||
"trainerStartWithReview": "Empieza con el vocabulario nuevo de esta lección. A medida que avances, el entrenador mezclará automáticamente repasos adecuados.",
|
"trainerStartWithReview": "Empieza con el vocabulario nuevo de esta lección. A medida que avances, el entrenador mezclará automáticamente repasos adecuados.",
|
||||||
"startLesson": "Empezar lección",
|
"startLesson": "Empezar lección",
|
||||||
"trainerProgressNewContent": "Contenido nuevo: {current}/{target}",
|
"trainerProgressNewContent": "Contenido nuevo: {current}/{target}",
|
||||||
|
|||||||
@@ -335,7 +335,9 @@ export default {
|
|||||||
return this.$t('falukant.politics.benefits.tax_exemption', { regions: labels });
|
return this.$t('falukant.politics.benefits.tax_exemption', { regions: labels });
|
||||||
}
|
}
|
||||||
if (b.tr === 'daily_salary') {
|
if (b.tr === 'daily_salary') {
|
||||||
return this.$t('falukant.politics.benefits.daily_salary', { amount: b.params?.amount ?? '—' });
|
return this.$t('falukant.politics.benefits.daily_salary', {
|
||||||
|
amount: this.formatPoliticsMoney(b.params?.amount)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (b.tr === 'reputation_periodic') {
|
if (b.tr === 'reputation_periodic') {
|
||||||
return this.$t('falukant.politics.benefits.reputation_periodic', {
|
return this.$t('falukant.politics.benefits.reputation_periodic', {
|
||||||
@@ -586,6 +588,18 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Geldbetrag wie in Familie/Bank (exakter Wert, kein „ca.“). */
|
||||||
|
formatPoliticsMoney(value) {
|
||||||
|
const n = Number(value);
|
||||||
|
if (!Number.isFinite(n)) {
|
||||||
|
return '—';
|
||||||
|
}
|
||||||
|
return new Intl.NumberFormat(this.$i18n.locale, {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2
|
||||||
|
}).format(n);
|
||||||
|
},
|
||||||
|
|
||||||
async loadOwnCharacterId() {
|
async loadOwnCharacterId() {
|
||||||
try {
|
try {
|
||||||
const { data } = await apiClient.get('/api/falukant/info');
|
const { data } = await apiClient.get('/api/falukant/info');
|
||||||
|
|||||||
@@ -874,6 +874,8 @@ import apiClient from '@/utils/axios.js';
|
|||||||
const debugLog = () => {};
|
const debugLog = () => {};
|
||||||
const LESSON_STATE_VERSION = 1;
|
const LESSON_STATE_VERSION = 1;
|
||||||
const VOCAB_REPEAT_INTERVALS = [1, 2, 4];
|
const VOCAB_REPEAT_INTERVALS = [1, 2, 4];
|
||||||
|
/** Mindest-Erfolgsquote im Vokabeltrainer (gesamt), damit die Kapitel-Prüfung freigeschaltet wird. */
|
||||||
|
const EXERCISE_UNLOCK_MIN_SUCCESS_PERCENT = 70;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'VocabLessonView',
|
name: 'VocabLessonView',
|
||||||
@@ -991,6 +993,9 @@ export default {
|
|||||||
const unlockTarget = this.trainerNewFocusTarget + Math.ceil((this.effectiveExercises?.length || 0) * 0.25);
|
const unlockTarget = this.trainerNewFocusTarget + Math.ceil((this.effectiveExercises?.length || 0) * 0.25);
|
||||||
return Math.max(6, Math.min(140, unlockTarget));
|
return Math.max(6, Math.min(140, unlockTarget));
|
||||||
},
|
},
|
||||||
|
exerciseUnlockMinSuccessPercent() {
|
||||||
|
return EXERCISE_UNLOCK_MIN_SUCCESS_PERCENT;
|
||||||
|
},
|
||||||
currentReviewShare() {
|
currentReviewShare() {
|
||||||
if (!this.hasPreviousVocab) {
|
if (!this.hasPreviousVocab) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -1018,10 +1023,15 @@ export default {
|
|||||||
if (this.trainableLessonVocab.length === 0 && this.prepItems.length > 0) {
|
if (this.trainableLessonVocab.length === 0 && this.prepItems.length > 0) {
|
||||||
return this.$t('socialnetwork.vocab.courses.exerciseUnlockHintAfterPrep');
|
return this.$t('socialnetwork.vocab.courses.exerciseUnlockHintAfterPrep');
|
||||||
}
|
}
|
||||||
|
const core = this.$t('socialnetwork.vocab.courses.exerciseUnlockHintTrainerCore', {
|
||||||
|
newTarget: this.trainerNewFocusTarget,
|
||||||
|
attempts: this.trainerExerciseUnlockAttempts,
|
||||||
|
rate: this.exerciseUnlockMinSuccessPercent
|
||||||
|
});
|
||||||
if (this.hasPreviousVocab) {
|
if (this.hasPreviousVocab) {
|
||||||
return `Lerne zuerst die neuen Inhalte der Lektion und arbeite dich durch ungefähr ${this.trainerExerciseUnlockAttempts} Trainerfragen. Ältere Vokabeln werden dabei nach und nach zugemischt.`;
|
return `${core} ${this.$t('socialnetwork.vocab.courses.exerciseUnlockHintTrainerMixSuffix')}`;
|
||||||
}
|
}
|
||||||
return `Arbeite zuerst durch ungefähr ${this.trainerExerciseUnlockAttempts} Trainerfragen aus dieser Lektion. Danach wird die Kapitel-Prüfung freigeschaltet.`;
|
return core;
|
||||||
},
|
},
|
||||||
/** Für Wiederholungslektionen: Übungen aus vorherigen Lektionen (Kapitelprüfung). Sonst: eigene Grammatik-Übungen. */
|
/** Für Wiederholungslektionen: Übungen aus vorherigen Lektionen (Kapitelprüfung). Sonst: eigene Grammatik-Übungen. */
|
||||||
effectiveExercises() {
|
effectiveExercises() {
|
||||||
@@ -1697,7 +1707,11 @@ export default {
|
|||||||
: 0;
|
: 0;
|
||||||
const currentLessonReady = this.vocabTrainerCurrentAttempts >= this.trainerNewFocusTarget;
|
const currentLessonReady = this.vocabTrainerCurrentAttempts >= this.trainerNewFocusTarget;
|
||||||
|
|
||||||
if (currentLessonReady && this.vocabTrainerTotalAttempts >= minimumAttempts && successRate >= 70) {
|
if (
|
||||||
|
currentLessonReady
|
||||||
|
&& this.vocabTrainerTotalAttempts >= minimumAttempts
|
||||||
|
&& successRate >= EXERCISE_UNLOCK_MIN_SUCCESS_PERCENT
|
||||||
|
) {
|
||||||
this.exercisePreparationCompleted = true;
|
this.exercisePreparationCompleted = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user