feat(vocab): add dashboard learning summary and related endpoints
All checks were successful
Deploy to production / deploy (push) Successful in 2m52s
All checks were successful
Deploy to production / deploy (push) Successful in 2m52s
- Introduced `getDashboardLearningSummary` method in `VocabService` to provide a compact overview of enrolled courses and current lessons for users. - Updated `vocabController` to include a new route for the dashboard widget, allowing users to access their learning summary. - Enhanced `vocabRouter` to route requests for the new dashboard widget endpoint. - Added localization support for the new dashboard features across multiple languages, improving user engagement and accessibility. - Updated UI components to integrate the new dashboard widget, ensuring a seamless user experience.
This commit is contained in:
@@ -65,6 +65,8 @@ import PoliticalOfficeHistory from '../models/falukant/log/political_office_hist
|
||||
import UndergroundType from '../models/falukant/type/underground.js';
|
||||
import Notification from '../models/falukant/log/notification.js';
|
||||
import PoliticalOffice from '../models/falukant/data/political_office.js';
|
||||
import PoliticalOfficeBenefit from '../models/falukant/predefine/political_office_benefit.js';
|
||||
import PoliticalOfficeBenefitType from '../models/falukant/type/political_office_benefit_type.js';
|
||||
import Underground from '../models/falukant/data/underground.js';
|
||||
import VehicleType from '../models/falukant/type/vehicle.js';
|
||||
import Vehicle from '../models/falukant/data/vehicle.js';
|
||||
@@ -131,6 +133,17 @@ const POLITICAL_OFFICE_RANKS = {
|
||||
chancellor: 6
|
||||
};
|
||||
|
||||
function computePoliticalDailySalaryPayout(value, officeName) {
|
||||
const v = value && typeof value === 'object' ? value : {};
|
||||
if (v.dailyAmount != null && Number.isFinite(Number(v.dailyAmount))) {
|
||||
return Math.round(Number(v.dailyAmount) * 100) / 100;
|
||||
}
|
||||
const rank = POLITICAL_OFFICE_RANKS[officeName] ?? 0;
|
||||
const base = Number(v.base ?? 0);
|
||||
const perRank = Number(v.perRank ?? v.per_rank ?? 0);
|
||||
return Math.round((base + perRank * rank) * 100) / 100;
|
||||
}
|
||||
|
||||
const CERTIFICATE_THRESHOLDS = {
|
||||
2: { avgKnowledge: 15, completedProductions: 4, statusMode: 'none', statusRequiredCount: 0 },
|
||||
3: { avgKnowledge: 28, completedProductions: 15, statusMode: 'none', statusRequiredCount: 0 },
|
||||
@@ -6067,9 +6080,106 @@ class FalukantService extends BaseService {
|
||||
return this.healthChange(user, delta);
|
||||
}
|
||||
|
||||
politicsBenefitEntriesFromRows(benefitRows, officeName) {
|
||||
if (!benefitRows?.length) {
|
||||
return [];
|
||||
}
|
||||
const out = [];
|
||||
for (const row of benefitRows) {
|
||||
const plain = row.get ? row.get({ plain: true }) : row;
|
||||
const tr = plain.benefitDefinition?.tr || '';
|
||||
const v = plain.value && typeof plain.value === 'object' ? plain.value : {};
|
||||
if (tr === 'tax_exemption') {
|
||||
const regions = Array.isArray(v.regions) ? v.regions : [];
|
||||
if (regions.includes('*')) {
|
||||
out.push({ tr: 'tax_exemption', params: { all: true } });
|
||||
} else {
|
||||
out.push({ tr: 'tax_exemption', params: { regions } });
|
||||
}
|
||||
} else if (tr === 'daily_salary' || tr === 'salary') {
|
||||
const amount = computePoliticalDailySalaryPayout(v, officeName);
|
||||
if (amount > 0) {
|
||||
out.push({ tr: 'daily_salary', params: { amount } });
|
||||
}
|
||||
} else if (tr) {
|
||||
out.push({ tr: 'generic_benefit', params: { code: tr } });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async _maybeGrantPoliticalDailySalary(falukantUser, characterId) {
|
||||
const rawLast = falukantUser.lastPoliticalDailySalaryOn;
|
||||
const lastStr = rawLast
|
||||
? (typeof rawLast === 'string'
|
||||
? rawLast.slice(0, 10)
|
||||
: new Date(rawLast).toISOString().slice(0, 10))
|
||||
: '';
|
||||
const todayStr = new Date().toISOString().slice(0, 10);
|
||||
if (lastStr === todayStr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const held = await PoliticalOffice.findAll({
|
||||
where: { characterId },
|
||||
include: [{ model: PoliticalOfficeType, as: 'type', attributes: ['name'] }]
|
||||
});
|
||||
if (!held.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const officeTypeIds = [...new Set(held.map((h) => h.officeTypeId))];
|
||||
const benefitRows = await PoliticalOfficeBenefit.findAll({
|
||||
where: { officeTypeId: { [Op.in]: officeTypeIds } },
|
||||
include: [{ model: PoliticalOfficeBenefitType, as: 'benefitDefinition', attributes: ['tr'] }]
|
||||
});
|
||||
const byType = new Map();
|
||||
for (const br of benefitRows) {
|
||||
const oid = br.officeTypeId;
|
||||
if (!byType.has(oid)) {
|
||||
byType.set(oid, []);
|
||||
}
|
||||
byType.get(oid).push(br);
|
||||
}
|
||||
|
||||
let total = 0;
|
||||
for (const h of held) {
|
||||
const name = h.type?.name;
|
||||
const rows = byType.get(h.officeTypeId) || [];
|
||||
for (const row of rows) {
|
||||
const tr = row.benefitDefinition?.tr;
|
||||
if (tr !== 'daily_salary') {
|
||||
continue;
|
||||
}
|
||||
const v = typeof row.value === 'object' && row.value ? row.value : {};
|
||||
total += computePoliticalDailySalaryPayout(v, name);
|
||||
}
|
||||
}
|
||||
|
||||
total = Math.round(total * 100) / 100;
|
||||
if (total <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const moneyResult = await updateFalukantUserMoney(
|
||||
falukantUser.id,
|
||||
total,
|
||||
'Politisches Tagesamtshonorar',
|
||||
falukantUser.id
|
||||
);
|
||||
if (!moneyResult.success) {
|
||||
console.error('[getPoliticsOverview] Tageshonorar konnte nicht gebucht werden');
|
||||
return;
|
||||
}
|
||||
await FalukantUser.update(
|
||||
{ lastPoliticalDailySalaryOn: todayStr },
|
||||
{ where: { id: falukantUser.id } }
|
||||
);
|
||||
}
|
||||
|
||||
async getPoliticsOverview(hashedUserId) {
|
||||
// Liefert alle aktuell besetzten Ämter im eigenen Gebiet inklusive
|
||||
// Inhaber und berechnetem Enddatum der Amtszeit.
|
||||
// Inhaber, Vorteile (aus political_office_benefit) und berechnetem Enddatum der Amtszeit.
|
||||
const user = await getFalukantUserOrFail(hashedUserId);
|
||||
|
||||
// Charakter des Users bestimmen (Region ist dort hinterlegt)
|
||||
@@ -6081,6 +6191,8 @@ class FalukantService extends BaseService {
|
||||
return [];
|
||||
}
|
||||
|
||||
await this._maybeGrantPoliticalDailySalary(user, character.id);
|
||||
|
||||
// Alle relevanten Regionen (Region + Eltern) laden
|
||||
const relevantRegionIds = await this.getRegionAndParentIds(character.regionId);
|
||||
|
||||
@@ -6138,7 +6250,23 @@ class FalukantService extends BaseService {
|
||||
]
|
||||
});
|
||||
|
||||
return offices.map(office => {
|
||||
const officeTypeIds = [...new Set(offices.map((o) => o.officeTypeId))];
|
||||
let benefitByType = new Map();
|
||||
if (officeTypeIds.length) {
|
||||
const allBenefits = await PoliticalOfficeBenefit.findAll({
|
||||
where: { officeTypeId: { [Op.in]: officeTypeIds } },
|
||||
include: [{ model: PoliticalOfficeBenefitType, as: 'benefitDefinition', attributes: ['tr'] }]
|
||||
});
|
||||
for (const br of allBenefits) {
|
||||
const oid = br.officeTypeId;
|
||||
if (!benefitByType.has(oid)) {
|
||||
benefitByType.set(oid, []);
|
||||
}
|
||||
benefitByType.get(oid).push(br);
|
||||
}
|
||||
}
|
||||
|
||||
return offices.map((office) => {
|
||||
const o = office.get({ plain: true });
|
||||
|
||||
// Enddatum der Amtszeit berechnen: Start = createdAt, Dauer = termLength Jahre
|
||||
@@ -6152,10 +6280,16 @@ class FalukantService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
const officeName = o.type?.name;
|
||||
const benefit = this.politicsBenefitEntriesFromRows(
|
||||
benefitByType.get(o.officeTypeId) || [],
|
||||
officeName
|
||||
);
|
||||
|
||||
return {
|
||||
id: o.id,
|
||||
officeType: {
|
||||
name: o.type?.name
|
||||
name: officeName
|
||||
},
|
||||
region: {
|
||||
name: o.region?.name,
|
||||
@@ -6172,7 +6306,8 @@ class FalukantService extends BaseService {
|
||||
gender: o.holder.gender
|
||||
}
|
||||
: null,
|
||||
termEnds
|
||||
termEnds,
|
||||
benefit
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -7006,11 +7141,11 @@ ORDER BY r.id`,
|
||||
// Unikate nach character.id
|
||||
const map = new Map();
|
||||
const POLITICAL_TAX_EXEMPTIONS = {
|
||||
'council': ['city'],
|
||||
'taxman': ['city','county'],
|
||||
'treasurerer': ['city','county','shire'],
|
||||
'super-state-administrator': ['city','county','shire','markgrave','duchy'],
|
||||
'chancellor': ['*']
|
||||
council: ['city'],
|
||||
taxman: ['city', 'county'],
|
||||
treasurer: ['city', 'county', 'shire'],
|
||||
'super-state-administrator': ['city', 'county', 'shire', 'markgrave', 'duchy'],
|
||||
chancellor: ['*']
|
||||
};
|
||||
|
||||
histories.forEach(h => {
|
||||
|
||||
Reference in New Issue
Block a user