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:
@@ -48,7 +48,8 @@ class VocabController {
|
||||
this.enrollInCourse = this._wrapWithUser((userId, req) => this.service.enrollInCourse(userId, req.params.courseId), { successStatus: 201 });
|
||||
this.unenrollFromCourse = this._wrapWithUser((userId, req) => this.service.unenrollFromCourse(userId, req.params.courseId));
|
||||
this.getMyCourses = this._wrapWithUser((userId) => this.service.getMyCourses(userId));
|
||||
|
||||
this.getDashboardLearningSummary = this._wrapWithUser((userId) => this.service.getDashboardLearningSummary(userId));
|
||||
|
||||
// Progress
|
||||
this.getCourseProgress = this._wrapWithUser((userId, req) => this.service.getCourseProgress(userId, req.params.courseId));
|
||||
this.updateLessonProgress = this._wrapWithUser((userId, req) => this.service.updateLessonProgress(userId, req.params.lessonId, req.body));
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addColumn(
|
||||
{
|
||||
tableName: 'falukant_user',
|
||||
schema: 'falukant_data'
|
||||
},
|
||||
'last_political_daily_salary_on',
|
||||
{
|
||||
type: Sequelize.DATEONLY,
|
||||
allowNull: true
|
||||
}
|
||||
);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'falukant_predefine'
|
||||
AND table_name = 'political_office_benefit'
|
||||
AND column_name = 'political_office_id'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'falukant_predefine'
|
||||
AND table_name = 'political_office_benefit'
|
||||
AND column_name = 'office_type_id'
|
||||
) THEN
|
||||
ALTER TABLE falukant_predefine.political_office_benefit
|
||||
RENAME COLUMN political_office_id TO office_type_id;
|
||||
ELSIF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'falukant_predefine'
|
||||
AND table_name = 'political_office_benefit'
|
||||
AND column_name = 'political_office_id'
|
||||
) AND EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'falukant_predefine'
|
||||
AND table_name = 'political_office_benefit'
|
||||
AND column_name = 'office_type_id'
|
||||
) THEN
|
||||
UPDATE falukant_predefine.political_office_benefit
|
||||
SET office_type_id = COALESCE(office_type_id, political_office_id);
|
||||
ALTER TABLE falukant_predefine.political_office_benefit
|
||||
DROP COLUMN political_office_id;
|
||||
END IF;
|
||||
END $$;
|
||||
`);
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeColumn(
|
||||
{
|
||||
tableName: 'falukant_user',
|
||||
schema: 'falukant_data'
|
||||
},
|
||||
'last_political_daily_salary_on'
|
||||
);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'falukant_predefine'
|
||||
AND table_name = 'political_office_benefit'
|
||||
AND column_name = 'office_type_id'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'falukant_predefine'
|
||||
AND table_name = 'political_office_benefit'
|
||||
AND column_name = 'political_office_id'
|
||||
) THEN
|
||||
ALTER TABLE falukant_predefine.political_office_benefit
|
||||
RENAME COLUMN office_type_id TO political_office_id;
|
||||
END IF;
|
||||
END $$;
|
||||
`);
|
||||
}
|
||||
};
|
||||
@@ -36,6 +36,11 @@ FalukantUser.init({
|
||||
lastNobilityAdvanceAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
},
|
||||
lastPoliticalDailySalaryOn: {
|
||||
type: DataTypes.DATEONLY,
|
||||
allowNull: true,
|
||||
field: 'last_political_daily_salary_on'
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// falukant/predefine/political_office_benefit.js
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../../utils/sequelize.js';
|
||||
import PoliticalOfficeBenefitType from '../type/political_office_benefit_type.js';
|
||||
|
||||
class PoliticalOfficeBenefit extends Model {}
|
||||
|
||||
@@ -9,31 +8,29 @@ PoliticalOfficeBenefit.init({
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true},
|
||||
politicalOfficeId: {
|
||||
autoIncrement: true
|
||||
},
|
||||
officeTypeId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false},
|
||||
allowNull: false,
|
||||
field: 'office_type_id'
|
||||
},
|
||||
benefitTypeId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false},
|
||||
allowNull: false,
|
||||
field: 'benefit_type_id'
|
||||
},
|
||||
value: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: false}}, {
|
||||
sequelize,
|
||||
allowNull: false
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'PoliticalOfficeBenefit',
|
||||
tableName: 'political_office_benefit',
|
||||
schema: 'falukant_predefine',
|
||||
timestamps: false,
|
||||
underscored: true});
|
||||
|
||||
// Association
|
||||
PoliticalOfficeBenefit.belongsTo(PoliticalOfficeBenefitType, {
|
||||
foreignKey: 'benefit_type_id',
|
||||
as: 'benefitType'
|
||||
});
|
||||
PoliticalOfficeBenefitType.hasMany(PoliticalOfficeBenefit, {
|
||||
foreignKey: 'benefit_type_id',
|
||||
as: 'benefits'
|
||||
underscored: true
|
||||
});
|
||||
|
||||
export default PoliticalOfficeBenefit;
|
||||
|
||||
@@ -7,6 +7,8 @@ const vocabController = new VocabController();
|
||||
|
||||
router.use(authenticate);
|
||||
|
||||
router.get('/dashboard-widget', vocabController.getDashboardLearningSummary);
|
||||
|
||||
router.get('/languages', vocabController.listLanguages);
|
||||
router.get('/languages/all', vocabController.listAllLanguages);
|
||||
router.post('/languages', vocabController.createLanguage);
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -2636,6 +2636,111 @@ export default class VocabService {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Kompakte Übersicht für das Start-Dashboard: eingeschriebene Kurse und „aktuelle“ Lektion
|
||||
* (gleiche Logik wie VocabCourseView.currentLesson: erste unvollständige, sonst letzte).
|
||||
*/
|
||||
async getDashboardLearningSummary(hashedUserId) {
|
||||
const user = await this._getUserByHashedId(hashedUserId);
|
||||
|
||||
const enrollments = await VocabCourseEnrollment.findAll({
|
||||
where: { userId: user.id },
|
||||
include: [
|
||||
{
|
||||
model: VocabCourse,
|
||||
as: 'course',
|
||||
required: true,
|
||||
attributes: ['id', 'title']
|
||||
}
|
||||
],
|
||||
order: [['enrolledAt', 'DESC']]
|
||||
});
|
||||
|
||||
const courseById = new Map();
|
||||
for (const e of enrollments) {
|
||||
const c = e.course?.get({ plain: true });
|
||||
if (!c?.id || courseById.has(c.id)) {
|
||||
continue;
|
||||
}
|
||||
courseById.set(c.id, { id: c.id, title: c.title || '' });
|
||||
}
|
||||
|
||||
const coursesMeta = [...courseById.values()];
|
||||
if (coursesMeta.length === 0) {
|
||||
return { courses: [] };
|
||||
}
|
||||
|
||||
const courseIds = coursesMeta.map((c) => c.id);
|
||||
|
||||
const lessons = await VocabCourseLesson.findAll({
|
||||
where: { courseId: { [Op.in]: courseIds } },
|
||||
attributes: ['id', 'courseId', 'lessonNumber', 'title'],
|
||||
order: [
|
||||
['courseId', 'ASC'],
|
||||
['lessonNumber', 'ASC']
|
||||
]
|
||||
});
|
||||
|
||||
const progressRows = await VocabCourseProgress.findAll({
|
||||
where: { userId: user.id, courseId: { [Op.in]: courseIds } },
|
||||
attributes: ['lessonId', 'completed']
|
||||
});
|
||||
|
||||
const completedByLessonId = new Map();
|
||||
for (const row of progressRows) {
|
||||
const plain = row.get({ plain: true });
|
||||
completedByLessonId.set(plain.lessonId, Boolean(plain.completed));
|
||||
}
|
||||
|
||||
const lessonsByCourse = new Map();
|
||||
for (const row of lessons) {
|
||||
const plain = row.get({ plain: true });
|
||||
const list = lessonsByCourse.get(plain.courseId) || [];
|
||||
list.push(plain);
|
||||
lessonsByCourse.set(plain.courseId, list);
|
||||
}
|
||||
|
||||
const courses = [];
|
||||
for (const meta of coursesMeta) {
|
||||
const sorted = lessonsByCourse.get(meta.id) || [];
|
||||
if (sorted.length === 0) {
|
||||
courses.push({
|
||||
courseId: meta.id,
|
||||
title: meta.title,
|
||||
currentLesson: null,
|
||||
allLessonsCompleted: false
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
let current = null;
|
||||
for (const lesson of sorted) {
|
||||
if (!completedByLessonId.get(lesson.id)) {
|
||||
current = lesson;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!current) {
|
||||
current = sorted[sorted.length - 1];
|
||||
}
|
||||
|
||||
const allLessonsCompleted = sorted.every((lesson) => completedByLessonId.get(lesson.id) === true);
|
||||
|
||||
courses.push({
|
||||
courseId: meta.id,
|
||||
title: meta.title,
|
||||
currentLesson: {
|
||||
id: current.id,
|
||||
lessonNumber: current.lessonNumber,
|
||||
title: current.title || ''
|
||||
},
|
||||
allLessonsCompleted
|
||||
});
|
||||
}
|
||||
|
||||
return { courses };
|
||||
}
|
||||
|
||||
/**
|
||||
* Kurse, in die der Nutzer (per Hash) eingeschrieben ist — jede courseId nur einmal,
|
||||
* bei mehrfachen Einschreibungen zählt die jeweils neueste Zeile.
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
-- Nur Schema-Fix für falukant_predefine.political_office_benefit
|
||||
-- (Fehler 23502: NULL in political_office_id trotz INSERT in office_type_id)
|
||||
--
|
||||
-- Ausführen, dann erneut falukant_political_office_benefits.sql
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = 'falukant_predefine' AND table_name = 'political_office_benefit'
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Tabelle fehlt.';
|
||||
END IF;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'falukant_predefine'
|
||||
AND table_name = 'political_office_benefit'
|
||||
AND column_name = 'political_office_id'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'falukant_predefine'
|
||||
AND table_name = 'political_office_benefit'
|
||||
AND column_name = 'office_type_id'
|
||||
) THEN
|
||||
ALTER TABLE falukant_predefine.political_office_benefit
|
||||
RENAME COLUMN political_office_id TO office_type_id;
|
||||
ELSIF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'falukant_predefine'
|
||||
AND table_name = 'political_office_benefit'
|
||||
AND column_name = 'political_office_id'
|
||||
) AND EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'falukant_predefine'
|
||||
AND table_name = 'political_office_benefit'
|
||||
AND column_name = 'office_type_id'
|
||||
) THEN
|
||||
UPDATE falukant_predefine.political_office_benefit
|
||||
SET office_type_id = COALESCE(office_type_id, political_office_id);
|
||||
ALTER TABLE falukant_predefine.political_office_benefit
|
||||
DROP COLUMN political_office_id;
|
||||
END IF;
|
||||
END $$;
|
||||
163
backend/sql/falukant_political_office_benefits.sql
Normal file
163
backend/sql/falukant_political_office_benefits.sql
Normal file
@@ -0,0 +1,163 @@
|
||||
-- =============================================================================
|
||||
-- Politische Amtsvorteile (falukant_predefine.political_office_benefit)
|
||||
-- =============================================================================
|
||||
--
|
||||
-- Fehler „SQL Error [08003]: This connection has been closed“
|
||||
-- -----------------------------------------------------------
|
||||
-- Das ist meist KEIN Inhaltfehler dieses Skripts, sondern:
|
||||
-- 1) Eine VORHERIGE Anweisung in derselben Session ist fehlgeschlagen → die
|
||||
-- Transaktion ist abgebrochen; der Client meldet dann oft nur noch 08003.
|
||||
-- → Neue Verbindung öffnen, ggf. ROLLBACK; dann erneut ausführen.
|
||||
-- 2) Fehler 23502: political_office_id NOT NULL, aber INSERT nutzt nur office_type_id
|
||||
-- (beide Spalten nach Sync) → dieses Skript gleicht das Schema zuerst an.
|
||||
-- 3) Spalte office_type_id fehlt komplett → Migration 20260401120000 ausführen.
|
||||
-- 4) PgBouncer (Pool) im Transaction-Modus: lange Skripte mit vielen
|
||||
-- Statements trennen die Verbindung → einzelne Blöcke nacheinander
|
||||
-- ausführen oder Datei falukant_political_office_benefits_steps.sql nutzen.
|
||||
-- 5) Netzwerk / Server-Neustart / Idle-Timeout.
|
||||
--
|
||||
-- Empfehlung: Im Terminal mit psql und ON_ERROR_STOP (bricht beim ersten Fehler
|
||||
-- sauber ab und zeigt die echte Meldung):
|
||||
-- psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f backend/sql/falukant_political_office_benefits.sql
|
||||
--
|
||||
-- JSON value daily_salary:
|
||||
-- • dailyAmount: fester Tagesbetrag (überschreibt base/perRank)
|
||||
-- • base + perRank: Auszahlung = base + (Amtsrang * perRank)
|
||||
-- JSON value tax_exemption:
|
||||
-- • regions: Text-Array (z. B. "city") oder "*" für alle Ebenen
|
||||
-- =============================================================================
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = 'falukant_predefine' AND table_name = 'political_office_benefit'
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Tabelle falukant_predefine.political_office_benefit fehlt.';
|
||||
END IF;
|
||||
|
||||
-- Legacy: nur political_office_id (speichert fälschlich die office_type_id)
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'falukant_predefine'
|
||||
AND table_name = 'political_office_benefit'
|
||||
AND column_name = 'political_office_id'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'falukant_predefine'
|
||||
AND table_name = 'political_office_benefit'
|
||||
AND column_name = 'office_type_id'
|
||||
) THEN
|
||||
ALTER TABLE falukant_predefine.political_office_benefit
|
||||
RENAME COLUMN political_office_id TO office_type_id;
|
||||
END IF;
|
||||
|
||||
-- Doppel-Spalten nach Sequelize: office_type_id wird befüllt, political_office_id bleibt NULL → 23502
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'falukant_predefine'
|
||||
AND table_name = 'political_office_benefit'
|
||||
AND column_name = 'political_office_id'
|
||||
) AND EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'falukant_predefine'
|
||||
AND table_name = 'political_office_benefit'
|
||||
AND column_name = 'office_type_id'
|
||||
) THEN
|
||||
UPDATE falukant_predefine.political_office_benefit
|
||||
SET office_type_id = COALESCE(office_type_id, political_office_id);
|
||||
ALTER TABLE falukant_predefine.political_office_benefit
|
||||
DROP COLUMN political_office_id;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'falukant_predefine'
|
||||
AND table_name = 'political_office_benefit'
|
||||
AND column_name = 'office_type_id'
|
||||
) THEN
|
||||
RAISE EXCEPTION
|
||||
'Spalte office_type_id fehlt. Migration 20260401120000-politics-benefits-and-daily-salary.cjs ausführen.';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Benefit-Typen (falls noch nicht aus App-Init vorhanden)
|
||||
INSERT INTO falukant_type.political_office_benefit_type (tr)
|
||||
SELECT 'tax_exemption'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM falukant_type.political_office_benefit_type t WHERE t.tr = 'tax_exemption'
|
||||
);
|
||||
|
||||
INSERT INTO falukant_type.political_office_benefit_type (tr)
|
||||
SELECT 'daily_salary'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM falukant_type.political_office_benefit_type t WHERE t.tr = 'daily_salary'
|
||||
);
|
||||
|
||||
-- Steuerbefreiungen (wie POLITICAL_TAX_EXEMPTIONS im Backend)
|
||||
INSERT INTO falukant_predefine.political_office_benefit (office_type_id, benefit_type_id, value)
|
||||
SELECT ot.id, bt.id, '{"regions":["city"]}'::jsonb
|
||||
FROM falukant_type.political_office_type ot
|
||||
JOIN falukant_type.political_office_benefit_type bt ON bt.tr = 'tax_exemption'
|
||||
WHERE ot.name = 'council'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM falukant_predefine.political_office_benefit x
|
||||
WHERE x.office_type_id = ot.id AND x.benefit_type_id = bt.id
|
||||
);
|
||||
|
||||
INSERT INTO falukant_predefine.political_office_benefit (office_type_id, benefit_type_id, value)
|
||||
SELECT ot.id, bt.id, '{"regions":["city","county"]}'::jsonb
|
||||
FROM falukant_type.political_office_type ot
|
||||
JOIN falukant_type.political_office_benefit_type bt ON bt.tr = 'tax_exemption'
|
||||
WHERE ot.name = 'taxman'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM falukant_predefine.political_office_benefit x
|
||||
WHERE x.office_type_id = ot.id AND x.benefit_type_id = bt.id
|
||||
);
|
||||
|
||||
INSERT INTO falukant_predefine.political_office_benefit (office_type_id, benefit_type_id, value)
|
||||
SELECT ot.id, bt.id, '{"regions":["city","county","shire"]}'::jsonb
|
||||
FROM falukant_type.political_office_type ot
|
||||
JOIN falukant_type.political_office_benefit_type bt ON bt.tr = 'tax_exemption'
|
||||
WHERE ot.name = 'treasurer'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM falukant_predefine.political_office_benefit x
|
||||
WHERE x.office_type_id = ot.id AND x.benefit_type_id = bt.id
|
||||
);
|
||||
|
||||
INSERT INTO falukant_predefine.political_office_benefit (office_type_id, benefit_type_id, value)
|
||||
SELECT ot.id, bt.id, '{"regions":["city","county","shire","markgrave","duchy"]}'::jsonb
|
||||
FROM falukant_type.political_office_type ot
|
||||
JOIN falukant_type.political_office_benefit_type bt ON bt.tr = 'tax_exemption'
|
||||
WHERE ot.name = 'super-state-administrator'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM falukant_predefine.political_office_benefit x
|
||||
WHERE x.office_type_id = ot.id AND x.benefit_type_id = bt.id
|
||||
);
|
||||
|
||||
INSERT INTO falukant_predefine.political_office_benefit (office_type_id, benefit_type_id, value)
|
||||
SELECT ot.id, bt.id, '{"regions":["*"]}'::jsonb
|
||||
FROM falukant_type.political_office_type ot
|
||||
JOIN falukant_type.political_office_benefit_type bt ON bt.tr = 'tax_exemption'
|
||||
WHERE ot.name = 'chancellor'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM falukant_predefine.political_office_benefit x
|
||||
WHERE x.office_type_id = ot.id AND x.benefit_type_id = bt.id
|
||||
);
|
||||
|
||||
-- Tageslohn: base + Amtsrang * perRank (Rang in App: POLITICAL_OFFICE_RANKS)
|
||||
INSERT INTO falukant_predefine.political_office_benefit (office_type_id, benefit_type_id, value)
|
||||
SELECT ot.id, bt.id, '{"base":4,"perRank":11}'::jsonb
|
||||
FROM falukant_type.political_office_type ot
|
||||
JOIN falukant_type.political_office_benefit_type bt ON bt.tr = 'daily_salary'
|
||||
WHERE ot.name IN (
|
||||
'assessor', 'councillor', 'council', 'beadle', 'town-clerk', 'mayor',
|
||||
'master-builder', 'village-major', 'judge', 'bailif', 'taxman', 'sheriff',
|
||||
'consultant', 'treasurer', 'hangman', 'territorial-council',
|
||||
'territorial-council-speaker', 'ruler-consultant', 'state-administrator',
|
||||
'super-state-administrator', 'governor', 'ministry-helper', 'minister', 'chancellor'
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM falukant_predefine.political_office_benefit x
|
||||
WHERE x.office_type_id = ot.id AND x.benefit_type_id = bt.id
|
||||
);
|
||||
141
backend/sql/falukant_political_office_benefits_steps.sql
Normal file
141
backend/sql/falukant_political_office_benefits_steps.sql
Normal file
@@ -0,0 +1,141 @@
|
||||
-- Einzelne Schritte (jeweils in neuer Verbindung oder nach ROLLBACK ausführen,
|
||||
-- falls der Client nach einem Fehler „connection closed“ meldet.)
|
||||
--
|
||||
-- Schritt 0: Schema angleichen (political_office_id vs office_type_id, siehe Haupt-SQL)
|
||||
-- Schritt 1: benefit_type Zeilen
|
||||
-- Schritt 2–6: tax_exemption
|
||||
-- Schritt 7: daily_salary für alle Ämter
|
||||
|
||||
-- ========== Schritt 0 ==========
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = 'falukant_predefine' AND table_name = 'political_office_benefit'
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Tabelle falukant_predefine.political_office_benefit fehlt.';
|
||||
END IF;
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'falukant_predefine'
|
||||
AND table_name = 'political_office_benefit'
|
||||
AND column_name = 'political_office_id'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'falukant_predefine'
|
||||
AND table_name = 'political_office_benefit'
|
||||
AND column_name = 'office_type_id'
|
||||
) THEN
|
||||
ALTER TABLE falukant_predefine.political_office_benefit
|
||||
RENAME COLUMN political_office_id TO office_type_id;
|
||||
END IF;
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'falukant_predefine'
|
||||
AND table_name = 'political_office_benefit'
|
||||
AND column_name = 'political_office_id'
|
||||
) AND EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'falukant_predefine'
|
||||
AND table_name = 'political_office_benefit'
|
||||
AND column_name = 'office_type_id'
|
||||
) THEN
|
||||
UPDATE falukant_predefine.political_office_benefit
|
||||
SET office_type_id = COALESCE(office_type_id, political_office_id);
|
||||
ALTER TABLE falukant_predefine.political_office_benefit
|
||||
DROP COLUMN political_office_id;
|
||||
END IF;
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'falukant_predefine'
|
||||
AND table_name = 'political_office_benefit'
|
||||
AND column_name = 'office_type_id'
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Spalte office_type_id fehlt – Migration 20260401120000 ausführen.';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ========== Schritt 1 ==========
|
||||
INSERT INTO falukant_type.political_office_benefit_type (tr)
|
||||
SELECT 'tax_exemption'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM falukant_type.political_office_benefit_type t WHERE t.tr = 'tax_exemption'
|
||||
);
|
||||
|
||||
INSERT INTO falukant_type.political_office_benefit_type (tr)
|
||||
SELECT 'daily_salary'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM falukant_type.political_office_benefit_type t WHERE t.tr = 'daily_salary'
|
||||
);
|
||||
|
||||
-- ========== Schritt 2 ==========
|
||||
INSERT INTO falukant_predefine.political_office_benefit (office_type_id, benefit_type_id, value)
|
||||
SELECT ot.id, bt.id, '{"regions":["city"]}'::jsonb
|
||||
FROM falukant_type.political_office_type ot
|
||||
JOIN falukant_type.political_office_benefit_type bt ON bt.tr = 'tax_exemption'
|
||||
WHERE ot.name = 'council'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM falukant_predefine.political_office_benefit x
|
||||
WHERE x.office_type_id = ot.id AND x.benefit_type_id = bt.id
|
||||
);
|
||||
|
||||
-- ========== Schritt 3 ==========
|
||||
INSERT INTO falukant_predefine.political_office_benefit (office_type_id, benefit_type_id, value)
|
||||
SELECT ot.id, bt.id, '{"regions":["city","county"]}'::jsonb
|
||||
FROM falukant_type.political_office_type ot
|
||||
JOIN falukant_type.political_office_benefit_type bt ON bt.tr = 'tax_exemption'
|
||||
WHERE ot.name = 'taxman'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM falukant_predefine.political_office_benefit x
|
||||
WHERE x.office_type_id = ot.id AND x.benefit_type_id = bt.id
|
||||
);
|
||||
|
||||
-- ========== Schritt 4 ==========
|
||||
INSERT INTO falukant_predefine.political_office_benefit (office_type_id, benefit_type_id, value)
|
||||
SELECT ot.id, bt.id, '{"regions":["city","county","shire"]}'::jsonb
|
||||
FROM falukant_type.political_office_type ot
|
||||
JOIN falukant_type.political_office_benefit_type bt ON bt.tr = 'tax_exemption'
|
||||
WHERE ot.name = 'treasurer'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM falukant_predefine.political_office_benefit x
|
||||
WHERE x.office_type_id = ot.id AND x.benefit_type_id = bt.id
|
||||
);
|
||||
|
||||
-- ========== Schritt 5 ==========
|
||||
INSERT INTO falukant_predefine.political_office_benefit (office_type_id, benefit_type_id, value)
|
||||
SELECT ot.id, bt.id, '{"regions":["city","county","shire","markgrave","duchy"]}'::jsonb
|
||||
FROM falukant_type.political_office_type ot
|
||||
JOIN falukant_type.political_office_benefit_type bt ON bt.tr = 'tax_exemption'
|
||||
WHERE ot.name = 'super-state-administrator'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM falukant_predefine.political_office_benefit x
|
||||
WHERE x.office_type_id = ot.id AND x.benefit_type_id = bt.id
|
||||
);
|
||||
|
||||
-- ========== Schritt 6 ==========
|
||||
INSERT INTO falukant_predefine.political_office_benefit (office_type_id, benefit_type_id, value)
|
||||
SELECT ot.id, bt.id, '{"regions":["*"]}'::jsonb
|
||||
FROM falukant_type.political_office_type ot
|
||||
JOIN falukant_type.political_office_benefit_type bt ON bt.tr = 'tax_exemption'
|
||||
WHERE ot.name = 'chancellor'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM falukant_predefine.political_office_benefit x
|
||||
WHERE x.office_type_id = ot.id AND x.benefit_type_id = bt.id
|
||||
);
|
||||
|
||||
-- ========== Schritt 7 ==========
|
||||
INSERT INTO falukant_predefine.political_office_benefit (office_type_id, benefit_type_id, value)
|
||||
SELECT ot.id, bt.id, '{"base":4,"perRank":11}'::jsonb
|
||||
FROM falukant_type.political_office_type ot
|
||||
JOIN falukant_type.political_office_benefit_type bt ON bt.tr = 'daily_salary'
|
||||
WHERE ot.name IN (
|
||||
'assessor', 'councillor', 'council', 'beadle', 'town-clerk', 'mayor',
|
||||
'master-builder', 'village-major', 'judge', 'bailif', 'taxman', 'sheriff',
|
||||
'consultant', 'treasurer', 'hangman', 'territorial-council',
|
||||
'territorial-council-speaker', 'ruler-consultant', 'state-administrator',
|
||||
'super-state-administrator', 'governor', 'ministry-helper', 'minister', 'chancellor'
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM falukant_predefine.political_office_benefit x
|
||||
WHERE x.office_type_id = ot.id AND x.benefit_type_id = bt.id
|
||||
);
|
||||
@@ -20,6 +20,7 @@ import PoliticalOfficeType from "../../models/falukant/type/political_office_typ
|
||||
import ChurchOfficeType from "../../models/falukant/type/church_office_type.js";
|
||||
import ChurchOfficeRequirement from "../../models/falukant/predefine/church_office_requirement.js";
|
||||
import PoliticalOfficeBenefitType from "../../models/falukant/type/political_office_benefit_type.js";
|
||||
import PoliticalOfficeBenefit from "../../models/falukant/predefine/political_office_benefit.js";
|
||||
import PoliticalOfficePrerequisite from "../../models/falukant/predefine/political_office_prerequisite.js";
|
||||
import UndergroundType from "../../models/falukant/type/underground.js";
|
||||
import WeatherType from "../../models/falukant/type/weather.js";
|
||||
@@ -51,6 +52,7 @@ export const initializeFalukantTypes = async () => {
|
||||
await initializePoliticalOfficeBenefitTypes();
|
||||
await initializePoliticalOfficeTypes();
|
||||
await initializePoliticalOfficePrerequisites();
|
||||
await initializePoliticalOfficeBenefits();
|
||||
await initializeChurchOfficeTypes();
|
||||
await initializeChurchOfficePrerequisites();
|
||||
await initializeUndergroundTypes();
|
||||
@@ -352,6 +354,7 @@ const vehicleTypes = [
|
||||
|
||||
const politicalOfficeBenefitTypes = [
|
||||
{ tr: 'salary' },
|
||||
{ tr: 'daily_salary' },
|
||||
{ tr: 'reputation' },
|
||||
{ tr: 'influence' },
|
||||
{ tr: 'access_level' },
|
||||
@@ -1038,6 +1041,62 @@ export const initializePoliticalOfficePrerequisites = async () => {
|
||||
console.log(`[Falukant] OfficePrereq neu=${created} exist=${existing}${skipped?` skip=${skipped}`:''}`);
|
||||
};
|
||||
|
||||
/** Amtsvorteile für Politik-UI und Tageshonorar (Testsysteme / frische DBs). Produktion alternativ: backend/sql/falukant_political_office_benefits.sql */
|
||||
export const initializePoliticalOfficeBenefits = async () => {
|
||||
const taxType = await PoliticalOfficeBenefitType.findOne({ where: { tr: 'tax_exemption' } });
|
||||
const dailyType = await PoliticalOfficeBenefitType.findOne({ where: { tr: 'daily_salary' } });
|
||||
if (!dailyType) {
|
||||
if (falukantDebug) console.warn('[Falukant] daily_salary Benefit-Typ fehlt, überspringe Office-Benefits');
|
||||
return;
|
||||
}
|
||||
|
||||
const taxByOffice = [
|
||||
{ officeTr: 'council', regions: ['city'] },
|
||||
{ officeTr: 'taxman', regions: ['city', 'county'] },
|
||||
{ officeTr: 'treasurer', regions: ['city', 'county', 'shire'] },
|
||||
{
|
||||
officeTr: 'super-state-administrator',
|
||||
regions: ['city', 'county', 'shire', 'markgrave', 'duchy']
|
||||
},
|
||||
{ officeTr: 'chancellor', regions: ['*'] }
|
||||
];
|
||||
|
||||
let taxCreated = 0;
|
||||
if (taxType) {
|
||||
for (const { officeTr, regions } of taxByOffice) {
|
||||
const office = await PoliticalOfficeType.findOne({ where: { name: officeTr } });
|
||||
if (!office) continue;
|
||||
const [, wasCreated] = await PoliticalOfficeBenefit.findOrCreate({
|
||||
where: { officeTypeId: office.id, benefitTypeId: taxType.id },
|
||||
defaults: {
|
||||
officeTypeId: office.id,
|
||||
benefitTypeId: taxType.id,
|
||||
value: { regions }
|
||||
}
|
||||
});
|
||||
if (wasCreated) taxCreated += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const allOffices = await PoliticalOfficeType.findAll({ attributes: ['id'] });
|
||||
let dailyCreated = 0;
|
||||
for (const office of allOffices) {
|
||||
const [, wasCreated] = await PoliticalOfficeBenefit.findOrCreate({
|
||||
where: { officeTypeId: office.id, benefitTypeId: dailyType.id },
|
||||
defaults: {
|
||||
officeTypeId: office.id,
|
||||
benefitTypeId: dailyType.id,
|
||||
value: { base: 4, perRank: 11 }
|
||||
}
|
||||
});
|
||||
if (wasCreated) dailyCreated += 1;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[Falukant] PoliticalOfficeBenefits: Steuer neu=${taxCreated}, Tageslohn neu=${dailyCreated} (gesamt Ämter=${allOffices.length})`
|
||||
);
|
||||
};
|
||||
|
||||
// — Church Offices —
|
||||
|
||||
const churchOffices = [
|
||||
|
||||
@@ -10,7 +10,8 @@ const DEFAULT_WIDGET_TYPES = [
|
||||
{ label: 'News', endpoint: '/api/news?language=de&category=top', description: 'Nachrichten (newsdata.io), Counter für Pagination', orderId: 3 },
|
||||
{ label: 'Geburtstage', endpoint: '/api/calendar/widget/birthdays', description: 'Nächste Geburtstage von Freunden', orderId: 4 },
|
||||
{ label: 'Nächste Termine', endpoint: '/api/calendar/widget/upcoming', description: 'Anstehende Kalendertermine', orderId: 5 },
|
||||
{ label: 'Kalender', endpoint: '/api/calendar/widget/mini', description: 'Mini-Kalenderansicht', orderId: 6 }
|
||||
{ label: 'Kalender', endpoint: '/api/calendar/widget/mini', description: 'Mini-Kalenderansicht', orderId: 6 },
|
||||
{ label: 'Sprachkurse', endpoint: '/api/vocab/dashboard-widget', description: 'Vokabelkurse, aktuelle Lektion, Sprung zur Lektion', orderId: 7 }
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user