Some fixes and additions
This commit is contained in:
@@ -48,6 +48,16 @@ class FalukantController {
|
|||||||
this.advanceNobility = this.advanceNobility.bind(this);
|
this.advanceNobility = this.advanceNobility.bind(this);
|
||||||
this.getHealth = this.getHealth.bind(this);
|
this.getHealth = this.getHealth.bind(this);
|
||||||
this.healthActivity = this.healthActivity.bind(this);
|
this.healthActivity = this.healthActivity.bind(this);
|
||||||
|
this.getPoliticsOverview = this.getPoliticsOverview.bind(this);
|
||||||
|
this.getOpenPolitics = this.getOpenPolitics.bind(this);
|
||||||
|
this.getElections = this.getElections.bind(this);
|
||||||
|
this.vote = this.vote.bind(this);
|
||||||
|
this.getOpenPolitics = this.getOpenPolitics.bind(this);
|
||||||
|
this.applyForElections = this.applyForElections.bind(this);
|
||||||
|
this.getRegions = this.getRegions.bind(this);
|
||||||
|
this.renovate = this.renovate.bind(this);
|
||||||
|
this.renovateAll = this.renovateAll.bind(this);
|
||||||
|
this.createBranch = this.createBranch.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUser(req, res) {
|
async getUser(req, res) {
|
||||||
@@ -117,6 +127,29 @@ class FalukantController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createBranch(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: hashedUserId } = req.headers;
|
||||||
|
const { cityId, branchTypeId } = req.body;
|
||||||
|
const result = await FalukantService.createBranch(hashedUserId, cityId, branchTypeId);
|
||||||
|
res.status(200).json(result);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBranchTypes(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: hashedUserId } = req.headers;
|
||||||
|
const result = await FalukantService.getBranchTypes(hashedUserId);
|
||||||
|
res.status(200).json(result);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getBranch(req, res) {
|
async getBranch(req, res) {
|
||||||
try {
|
try {
|
||||||
const { userid: hashedUserId } = req.headers;
|
const { userid: hashedUserId } = req.headers;
|
||||||
@@ -325,6 +358,7 @@ class FalukantController {
|
|||||||
const result = await FalukantService.convertProposalToDirector(hashedUserId, proposalId);
|
const result = await FalukantService.convertProposalToDirector(hashedUserId, proposalId);
|
||||||
res.status(200).json(result);
|
res.status(200).json(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log(error.message, error.stack);
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -482,7 +516,6 @@ class FalukantController {
|
|||||||
try {
|
try {
|
||||||
const { userid: hashedUserId } = req.headers;
|
const { userid: hashedUserId } = req.headers;
|
||||||
const result = await FalukantService.getUserHouse(hashedUserId);
|
const result = await FalukantService.getUserHouse(hashedUserId);
|
||||||
console.log(result);
|
|
||||||
res.status(200).json(result);
|
res.status(200).json(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
@@ -671,6 +704,108 @@ class FalukantController {
|
|||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getPoliticsOverview(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: hashedUserId } = req.headers;
|
||||||
|
const result = await FalukantService.getPoliticsOverview(hashedUserId);
|
||||||
|
res.status(200).json(result);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOpenPolitics(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: hashedUserId } = req.headers;
|
||||||
|
const result = await FalukantService.getOpenPolitics(hashedUserId);
|
||||||
|
res.status(200).json(result);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getElections(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: hashedUserId } = req.headers;
|
||||||
|
const result = await FalukantService.getElections(hashedUserId);
|
||||||
|
res.status(200).json(result);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async vote(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: hashedUserId } = req.headers;
|
||||||
|
const { votes } = req.body;
|
||||||
|
const result = await FalukantService.vote(hashedUserId, votes);
|
||||||
|
res.status(200).json(result);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOpenPolitics(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: hashedUserId } = req.headers;
|
||||||
|
const result = await FalukantService.getOpenPolitics(hashedUserId);
|
||||||
|
res.status(200).json(result);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async applyForElections(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: hashedUserId } = req.headers;
|
||||||
|
const { electionIds } = req.body;
|
||||||
|
const result = await FalukantService.applyForElections(hashedUserId, electionIds);
|
||||||
|
res.status(200).json(result);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRegions(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: hashedUserId } = req.headers;
|
||||||
|
const result = await FalukantService.getRegions(hashedUserId);
|
||||||
|
res.status(200).json(result);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async renovate(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: hashedUserId } = req.headers;
|
||||||
|
const { element } = req.body;
|
||||||
|
const result = await FalukantService.renovate(hashedUserId, element);
|
||||||
|
res.status(200).json(result);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async renovateAll(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: hashedUserId } = req.headers;
|
||||||
|
const result = await FalukantService.renovateAll(hashedUserId);
|
||||||
|
res.status(200).json(result);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FalukantController;
|
export default FalukantController;
|
||||||
|
|||||||
@@ -85,6 +85,9 @@ import PoliticalOffice from './falukant/data/political_office.js';
|
|||||||
import PoliticalOfficeBenefit from './falukant/predefine/political_office_benefit.js';
|
import PoliticalOfficeBenefit from './falukant/predefine/political_office_benefit.js';
|
||||||
import PoliticalOfficeBenefitType from './falukant/type/political_office_benefit_type.js';
|
import PoliticalOfficeBenefitType from './falukant/type/political_office_benefit_type.js';
|
||||||
import PoliticalOfficeRequirement from './falukant/predefine/political_office_prerequisite.js';
|
import PoliticalOfficeRequirement from './falukant/predefine/political_office_prerequisite.js';
|
||||||
|
import PoliticalOfficePrerequisite from './falukant/predefine/political_office_prerequisite.js';
|
||||||
|
import PoliticalOfficeHistory from './falukant/log/political_office_history.js';
|
||||||
|
import ElectionHistory from './falukant/log/election_history.js';
|
||||||
|
|
||||||
export default function setupAssociations() {
|
export default function setupAssociations() {
|
||||||
// UserParam related associations
|
// UserParam related associations
|
||||||
@@ -555,9 +558,9 @@ PoliticalOfficeRequirement.belongsTo(PoliticalOfficeType, {
|
|||||||
foreignKey: 'characterId',
|
foreignKey: 'characterId',
|
||||||
as: 'holder'
|
as: 'holder'
|
||||||
});
|
});
|
||||||
FalukantCharacter.hasMany(PoliticalOffice, {
|
FalukantCharacter.hasOne(PoliticalOffice, {
|
||||||
foreignKey: 'characterId',
|
foreignKey: 'characterId',
|
||||||
as: 'heldOffices'
|
as: 'heldOffice'
|
||||||
});
|
});
|
||||||
|
|
||||||
// elections
|
// elections
|
||||||
@@ -570,6 +573,16 @@ PoliticalOfficeRequirement.belongsTo(PoliticalOfficeType, {
|
|||||||
as: 'elections'
|
as: 'elections'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Election.belongsTo(RegionData, {
|
||||||
|
foreignKey: 'regionId',
|
||||||
|
as: 'region'
|
||||||
|
});
|
||||||
|
|
||||||
|
RegionData.hasMany(Election, {
|
||||||
|
foreignKey: 'regionId',
|
||||||
|
as: 'elections'
|
||||||
|
});
|
||||||
|
|
||||||
// candidates in an election
|
// candidates in an election
|
||||||
Candidate.belongsTo(Election, {
|
Candidate.belongsTo(Election, {
|
||||||
foreignKey: 'electionId',
|
foreignKey: 'electionId',
|
||||||
@@ -645,4 +658,43 @@ PoliticalOfficeRequirement.belongsTo(PoliticalOfficeType, {
|
|||||||
as: 'offices'
|
as: 'offices'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
PoliticalOfficePrerequisite.belongsTo(PoliticalOfficeType, {
|
||||||
|
foreignKey: 'office_type_id',
|
||||||
|
as: 'officeTypePrerequisite',
|
||||||
|
});
|
||||||
|
|
||||||
|
PoliticalOfficeType.hasMany(PoliticalOfficePrerequisite, {
|
||||||
|
foreignKey: 'office_type_id',
|
||||||
|
as: 'prerequisites',
|
||||||
|
});
|
||||||
|
|
||||||
|
PoliticalOfficeHistory.belongsTo(PoliticalOfficeType, {
|
||||||
|
foreignKey: 'officeTypeId',
|
||||||
|
as: 'officeTypeHistory',
|
||||||
|
});
|
||||||
|
|
||||||
|
PoliticalOfficeType.hasMany(PoliticalOfficeHistory, {
|
||||||
|
foreignKey: 'officeTypeId',
|
||||||
|
as: 'history',
|
||||||
|
});
|
||||||
|
|
||||||
|
FalukantCharacter.hasMany(PoliticalOfficeHistory, {
|
||||||
|
foreignKey: 'characterId',
|
||||||
|
as: 'officeHistories',
|
||||||
|
});
|
||||||
|
PoliticalOfficeHistory.belongsTo(FalukantCharacter, {
|
||||||
|
foreignKey: 'characterId',
|
||||||
|
as: 'character',
|
||||||
|
});
|
||||||
|
|
||||||
|
ElectionHistory.belongsTo(PoliticalOfficeType, {
|
||||||
|
foreignKey: 'officeTypeId',
|
||||||
|
as: 'officeTypeHistory',
|
||||||
|
});
|
||||||
|
|
||||||
|
PoliticalOfficeType.hasMany(ElectionHistory, {
|
||||||
|
foreignKey: 'officeTypeId',
|
||||||
|
as: 'electionHistory',
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ ChildRelation.init(
|
|||||||
allowNull: false,
|
allowNull: false,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
isHeir: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: true,
|
||||||
|
default: false,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sequelize,
|
sequelize,
|
||||||
|
|||||||
@@ -10,15 +10,19 @@ Election.init({
|
|||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
autoIncrement: true,
|
autoIncrement: true,
|
||||||
},
|
},
|
||||||
political_office_id: {
|
officeTypeId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: true,
|
||||||
|
},
|
||||||
|
regionId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
},
|
},
|
||||||
date: {
|
date: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
},
|
},
|
||||||
posts_to_fill: {
|
postsToFill: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ RegionData.init({
|
|||||||
key: 'id',
|
key: 'id',
|
||||||
schema: 'falukant_data',
|
schema: 'falukant_data',
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
map: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: {}
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
sequelize,
|
sequelize,
|
||||||
|
|||||||
@@ -4,21 +4,18 @@ import { sequelize } from '../../../utils/sequelize.js';
|
|||||||
|
|
||||||
class Vote extends Model {}
|
class Vote extends Model {}
|
||||||
|
|
||||||
Vote.init({
|
Vote.init(
|
||||||
|
{
|
||||||
id: {
|
id: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
autoIncrement: true,
|
autoIncrement: true,
|
||||||
},
|
},
|
||||||
election_id: {
|
electionId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
},
|
},
|
||||||
voter_character_id: {
|
candidateId: {
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
candidate_id: {
|
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
},
|
},
|
||||||
@@ -27,13 +24,25 @@ Vote.init({
|
|||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: DataTypes.NOW,
|
defaultValue: DataTypes.NOW,
|
||||||
},
|
},
|
||||||
}, {
|
falukantUserId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
sequelize,
|
sequelize,
|
||||||
modelName: 'Vote',
|
modelName: 'Vote',
|
||||||
tableName: 'vote',
|
tableName: 'vote',
|
||||||
schema: 'falukant_data',
|
schema: 'falukant_data',
|
||||||
timestamps: false,
|
timestamps: true,
|
||||||
underscored: true,
|
underscored: true,
|
||||||
});
|
indexes: [
|
||||||
|
{
|
||||||
|
unique: true,
|
||||||
|
fields: ['election_id', 'candidate_id'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export default Vote;
|
export default Vote;
|
||||||
|
|||||||
32
backend/models/falukant/log/election_history.js
Normal file
32
backend/models/falukant/log/election_history.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Model, DataTypes } from 'sequelize';
|
||||||
|
import { sequelize } from '../../../utils/sequelize.js';
|
||||||
|
|
||||||
|
class ElectionHistory extends Model { }
|
||||||
|
|
||||||
|
ElectionHistory.init({
|
||||||
|
electionId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
politicalOfficeTypeId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
electionDate: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
electionResult: {
|
||||||
|
type: DataTypes.JSON,
|
||||||
|
allowNull: false,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'ElectionHistory',
|
||||||
|
tableName: 'election_history',
|
||||||
|
schema: 'falukant_log',
|
||||||
|
timestamps: true,
|
||||||
|
underscored: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ElectionHistory;
|
||||||
35
backend/models/falukant/log/political_office_history.js
Normal file
35
backend/models/falukant/log/political_office_history.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { Model, DataTypes } from 'sequelize';
|
||||||
|
import { sequelize } from '../../../utils/sequelize.js';
|
||||||
|
|
||||||
|
class PoliticalOfficeHistory extends Model { }
|
||||||
|
|
||||||
|
PoliticalOfficeHistory.init(
|
||||||
|
{
|
||||||
|
characterId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
officeTypeId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
startDate: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
endDate: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sequelize,
|
||||||
|
modelName: 'PoliticalOfficeHistory',
|
||||||
|
tableName: 'political_office_history',
|
||||||
|
schema: 'falukant_log',
|
||||||
|
timestamps: true,
|
||||||
|
underscored: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default PoliticalOfficeHistory;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
// falukant/predefine/political_office_prerequisite.js
|
// models/falukant/predefine/political_office_prerequisite.js
|
||||||
|
|
||||||
import { Model, DataTypes } from 'sequelize';
|
import { Model, DataTypes } from 'sequelize';
|
||||||
import { sequelize } from '../../../utils/sequelize.js';
|
import { sequelize } from '../../../utils/sequelize.js';
|
||||||
|
|
||||||
@@ -10,10 +11,13 @@ PoliticalOfficePrerequisite.init({
|
|||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
autoIncrement: true,
|
autoIncrement: true,
|
||||||
},
|
},
|
||||||
political_office_id: {
|
|
||||||
|
// Neu: Feld heißt jetzt eindeutig "office_type_id"
|
||||||
|
office_type_id: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
prerequisite: {
|
prerequisite: {
|
||||||
type: DataTypes.JSONB,
|
type: DataTypes.JSONB,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
|
|||||||
@@ -94,6 +94,8 @@ import Election from './falukant/data/election.js';
|
|||||||
import Candidate from './falukant/data/candidate.js';
|
import Candidate from './falukant/data/candidate.js';
|
||||||
import Vote from './falukant/data/vote.js';
|
import Vote from './falukant/data/vote.js';
|
||||||
import ElectionResult from './falukant/data/election_result.js';
|
import ElectionResult from './falukant/data/election_result.js';
|
||||||
|
import PoliticalOfficeHistory from './falukant/log/political_office_history.js';
|
||||||
|
import ElectionHistory from './falukant/log/election_history.js';
|
||||||
|
|
||||||
const models = {
|
const models = {
|
||||||
SettingsType,
|
SettingsType,
|
||||||
@@ -191,6 +193,8 @@ const models = {
|
|||||||
Candidate,
|
Candidate,
|
||||||
Vote,
|
Vote,
|
||||||
ElectionResult,
|
ElectionResult,
|
||||||
|
PoliticalOfficeHistory,
|
||||||
|
ElectionHistory,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default models;
|
export default models;
|
||||||
|
|||||||
@@ -241,6 +241,180 @@ export async function createTriggers() {
|
|||||||
$$ LANGUAGE plpgsql VOLATILE;
|
$$ LANGUAGE plpgsql VOLATILE;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// process_elections–Stored-Procedure anlegen
|
||||||
|
const createProcessElectionsFunction = `
|
||||||
|
CREATE OR REPLACE FUNCTION falukant_data.process_elections()
|
||||||
|
RETURNS TABLE (
|
||||||
|
office_id INTEGER,
|
||||||
|
office_type_id INTEGER,
|
||||||
|
character_id INTEGER,
|
||||||
|
region_id INTEGER
|
||||||
|
)
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN QUERY
|
||||||
|
WITH
|
||||||
|
-- 1) Alle Wahlen, die vor mindestens 3 Tagen erstellt wurden
|
||||||
|
to_process AS (
|
||||||
|
SELECT
|
||||||
|
e.id AS election_id,
|
||||||
|
e.office_type_id AS tp_office_type_id,
|
||||||
|
e.region_id AS tp_region_id,
|
||||||
|
e.posts_to_fill AS tp_posts_to_fill,
|
||||||
|
e.date AS tp_election_date
|
||||||
|
FROM falukant_data.election e
|
||||||
|
WHERE (e.created_at::date + INTERVAL '3 days') <= NOW()::date
|
||||||
|
),
|
||||||
|
|
||||||
|
-- 2) Stimmen pro Kandidat zählen
|
||||||
|
votes AS (
|
||||||
|
SELECT
|
||||||
|
tp.election_id,
|
||||||
|
tp.tp_posts_to_fill AS posts_to_fill,
|
||||||
|
c.character_id,
|
||||||
|
COUNT(v.*) AS votes_received
|
||||||
|
FROM to_process tp
|
||||||
|
JOIN falukant_data.candidate c
|
||||||
|
ON c.election_id = tp.election_id
|
||||||
|
LEFT JOIN falukant_data.vote v
|
||||||
|
ON v.election_id = c.election_id
|
||||||
|
AND v.candidate_id = c.id
|
||||||
|
GROUP BY tp.election_id, tp.tp_posts_to_fill, c.character_id
|
||||||
|
),
|
||||||
|
|
||||||
|
-- 3) Ranking nach Stimmen
|
||||||
|
ranked AS (
|
||||||
|
SELECT
|
||||||
|
v.election_id,
|
||||||
|
v.character_id,
|
||||||
|
v.votes_received,
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY v.election_id
|
||||||
|
ORDER BY v.votes_received DESC, RANDOM()
|
||||||
|
) AS rn
|
||||||
|
FROM votes v
|
||||||
|
),
|
||||||
|
|
||||||
|
-- 4) Top-N (posts_to_fill) sind Gewinner
|
||||||
|
winners AS (
|
||||||
|
SELECT
|
||||||
|
r.election_id,
|
||||||
|
r.character_id
|
||||||
|
FROM ranked r
|
||||||
|
JOIN to_process tp
|
||||||
|
ON tp.election_id = r.election_id
|
||||||
|
WHERE r.rn <= tp.tp_posts_to_fill
|
||||||
|
),
|
||||||
|
|
||||||
|
-- 5) Verbleibende Kandidaten ohne Gewinner
|
||||||
|
remaining AS (
|
||||||
|
SELECT
|
||||||
|
tp.election_id,
|
||||||
|
c.character_id
|
||||||
|
FROM to_process tp
|
||||||
|
JOIN falukant_data.candidate c
|
||||||
|
ON c.election_id = tp.election_id
|
||||||
|
WHERE c.character_id NOT IN (
|
||||||
|
SELECT w.character_id
|
||||||
|
FROM winners w
|
||||||
|
WHERE w.election_id = tp.election_id
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
-- 6) Zufalls-Nachrücker bis alle Plätze gefüllt sind
|
||||||
|
random_fill AS (
|
||||||
|
SELECT
|
||||||
|
rp.election_id,
|
||||||
|
rp.character_id
|
||||||
|
FROM remaining rp
|
||||||
|
JOIN to_process tp
|
||||||
|
ON tp.election_id = rp.election_id
|
||||||
|
JOIN LATERAL (
|
||||||
|
SELECT r2.character_id
|
||||||
|
FROM remaining r2
|
||||||
|
WHERE r2.election_id = rp.election_id
|
||||||
|
ORDER BY RANDOM()
|
||||||
|
LIMIT GREATEST(
|
||||||
|
0,
|
||||||
|
tp.tp_posts_to_fill
|
||||||
|
- (SELECT COUNT(*) FROM winners w2 WHERE w2.election_id = tp.election_id)
|
||||||
|
)
|
||||||
|
) sub
|
||||||
|
ON sub.character_id = rp.character_id
|
||||||
|
),
|
||||||
|
|
||||||
|
-- 7) Finale Gewinner (Winners ∪ random_fill)
|
||||||
|
final_winners AS (
|
||||||
|
SELECT * FROM winners
|
||||||
|
UNION ALL
|
||||||
|
SELECT * FROM random_fill
|
||||||
|
),
|
||||||
|
|
||||||
|
-- 8) Neue Ämter anlegen und sofort zurückliefern
|
||||||
|
created_offices AS (
|
||||||
|
INSERT INTO falukant_data.political_office
|
||||||
|
(office_type_id, character_id, created_at, updated_at, region_id)
|
||||||
|
SELECT
|
||||||
|
tp.tp_office_type_id,
|
||||||
|
fw.character_id,
|
||||||
|
NOW() AS created_at,
|
||||||
|
NOW() AS updated_at,
|
||||||
|
tp.tp_region_id
|
||||||
|
FROM final_winners fw
|
||||||
|
JOIN to_process tp
|
||||||
|
ON tp.election_id = fw.election_id
|
||||||
|
RETURNING
|
||||||
|
id AS co_office_id,
|
||||||
|
falukant_data.political_office.office_type_id AS co_office_type_id,
|
||||||
|
falukant_data.political_office.character_id AS co_character_id,
|
||||||
|
falukant_data.political_office.region_id AS co_region_id
|
||||||
|
),
|
||||||
|
|
||||||
|
-- 9) election_history befüllen
|
||||||
|
_hist AS (
|
||||||
|
INSERT INTO falukant_log.election_history
|
||||||
|
(election_id, political_office_type_id, election_date, election_result, created_at, updated_at)
|
||||||
|
SELECT
|
||||||
|
tp.election_id,
|
||||||
|
tp.tp_office_type_id,
|
||||||
|
tp.tp_election_date,
|
||||||
|
(
|
||||||
|
SELECT json_agg(vr)
|
||||||
|
FROM votes vr
|
||||||
|
WHERE vr.election_id = tp.election_id
|
||||||
|
),
|
||||||
|
NOW() AS created_at,
|
||||||
|
NOW() AS updated_at
|
||||||
|
FROM to_process tp
|
||||||
|
),
|
||||||
|
|
||||||
|
-- 10) Cleanup: Stimmen, Kandidaten und Wahlen löschen
|
||||||
|
_del_votes AS (
|
||||||
|
DELETE FROM falukant_data.vote
|
||||||
|
WHERE election_id IN (SELECT election_id FROM to_process)
|
||||||
|
),
|
||||||
|
_del_candidates AS (
|
||||||
|
DELETE FROM falukant_data.candidate
|
||||||
|
WHERE election_id IN (SELECT election_id FROM to_process)
|
||||||
|
),
|
||||||
|
_del_elections AS (
|
||||||
|
DELETE FROM falukant_data.election
|
||||||
|
WHERE id IN (SELECT election_id FROM to_process)
|
||||||
|
)
|
||||||
|
|
||||||
|
-- 11) Ergebnis wirklich zurückliefern
|
||||||
|
SELECT
|
||||||
|
co.co_office_id AS office_id,
|
||||||
|
co.co_office_type_id,
|
||||||
|
co.co_character_id,
|
||||||
|
co.co_region_id
|
||||||
|
FROM created_offices co
|
||||||
|
ORDER BY co.co_region_id, co.co_office_id;
|
||||||
|
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await sequelize.query(createTriggerFunction);
|
await sequelize.query(createTriggerFunction);
|
||||||
await sequelize.query(createInsertTrigger);
|
await sequelize.query(createInsertTrigger);
|
||||||
@@ -257,6 +431,7 @@ export async function createTriggers() {
|
|||||||
await sequelize.query(createChildRelationNameFunction);
|
await sequelize.query(createChildRelationNameFunction);
|
||||||
await sequelize.query(createChildRelationNameTrigger);
|
await sequelize.query(createChildRelationNameTrigger);
|
||||||
await sequelize.query(createRandomMoodUpdateMethod);
|
await sequelize.query(createRandomMoodUpdateMethod);
|
||||||
|
await sequelize.query(createProcessElectionsFunction);
|
||||||
await initializeCharacterTraitTrigger();
|
await initializeCharacterTraitTrigger();
|
||||||
|
|
||||||
console.log('Triggers created successfully');
|
console.log('Triggers created successfully');
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ router.get('/character/affect', falukantController.getCharacterAffect);
|
|||||||
router.get('/name/randomfirstname/:gender', falukantController.randomFirstName);
|
router.get('/name/randomfirstname/:gender', falukantController.randomFirstName);
|
||||||
router.get('/name/randomlastname', falukantController.randomLastName);
|
router.get('/name/randomlastname', falukantController.randomLastName);
|
||||||
router.get('/info', falukantController.getInfo);
|
router.get('/info', falukantController.getInfo);
|
||||||
|
router.get('/branches/types', falukantController.getBranchTypes);
|
||||||
router.get('/branches/:branch', falukantController.getBranch);
|
router.get('/branches/:branch', falukantController.getBranch);
|
||||||
router.get('/branches', falukantController.getBranches);
|
router.get('/branches', falukantController.getBranches);
|
||||||
|
router.post('/branches', falukantController.createBranch);
|
||||||
router.get('/productions', falukantController.getAllProductions);
|
router.get('/productions', falukantController.getAllProductions);
|
||||||
router.post('/production', falukantController.createProduction);
|
router.post('/production', falukantController.createProduction);
|
||||||
router.get('/production/:branchId', falukantController.getProduction);
|
router.get('/production/:branchId', falukantController.getProduction);
|
||||||
@@ -43,6 +45,8 @@ router.get('/nobility/titels', falukantController.getTitelsOfNobility);
|
|||||||
router.get('/houses/types', falukantController.getHouseTypes);
|
router.get('/houses/types', falukantController.getHouseTypes);
|
||||||
router.get('/houses/buyable', falukantController.getBuyableHouses);
|
router.get('/houses/buyable', falukantController.getBuyableHouses);
|
||||||
router.get('/houses', falukantController.getUserHouse);
|
router.get('/houses', falukantController.getUserHouse);
|
||||||
|
router.post('/houses/renovate-all', falukantController.renovateAll);
|
||||||
|
router.post('/houses/renovate', falukantController.renovate);
|
||||||
router.post('/houses', falukantController.buyUserHouse);
|
router.post('/houses', falukantController.buyUserHouse);
|
||||||
router.get('/party/types', falukantController.getPartyTypes);
|
router.get('/party/types', falukantController.getPartyTypes);
|
||||||
router.post('/party', falukantController.createParty);
|
router.post('/party', falukantController.createParty);
|
||||||
@@ -57,5 +61,13 @@ router.post('/bank/credits', falukantController.takeBankCredits);
|
|||||||
router.get('/nobility', falukantController.getNobility);
|
router.get('/nobility', falukantController.getNobility);
|
||||||
router.post('/nobility', falukantController.advanceNobility);
|
router.post('/nobility', falukantController.advanceNobility);
|
||||||
router.get('/health', falukantController.getHealth);
|
router.get('/health', falukantController.getHealth);
|
||||||
router.post('/health', falukantController.healthActivity)
|
router.post('/health', falukantController.healthActivity);
|
||||||
|
router.get('/politics/overview', falukantController.getPoliticsOverview);
|
||||||
|
router.get('/politics/open', falukantController.getOpenPolitics);
|
||||||
|
router.get('/politics/elections', falukantController.getElections);
|
||||||
|
router.post('/politics/elections', falukantController.vote);
|
||||||
|
router.get('/politics/open', falukantController.getOpenPolitics);
|
||||||
|
router.post('/politics/open', falukantController.applyForElections);
|
||||||
|
router.get('/cities', falukantController.getRegions);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import BaseService from './BaseService.js';
|
import BaseService from './BaseService.js';
|
||||||
import { Sequelize, Op, where } from 'sequelize';
|
import { Sequelize, Op } from 'sequelize';
|
||||||
|
import { sequelize } from '../utils/sequelize.js';
|
||||||
import FalukantPredefineFirstname from '../models/falukant/predefine/firstname.js';
|
import FalukantPredefineFirstname from '../models/falukant/predefine/firstname.js';
|
||||||
import FalukantPredefineLastname from '../models/falukant/predefine/lastname.js';
|
import FalukantPredefineLastname from '../models/falukant/predefine/lastname.js';
|
||||||
import FalukantUser from '../models/falukant/data/user.js';
|
import FalukantUser from '../models/falukant/data/user.js';
|
||||||
@@ -47,6 +47,12 @@ import LearnRecipient from '../models/falukant/type/learn_recipient.js';
|
|||||||
import Credit from '../models/falukant/data/credit.js';
|
import Credit from '../models/falukant/data/credit.js';
|
||||||
import TitleRequirement from '../models/falukant/type/title_requirement.js';
|
import TitleRequirement from '../models/falukant/type/title_requirement.js';
|
||||||
import HealthActivity from '../models/falukant/log/health_activity.js';
|
import HealthActivity from '../models/falukant/log/health_activity.js';
|
||||||
|
import Election from '../models/falukant/data/election.js';
|
||||||
|
import PoliticalOfficeType from '../models/falukant/type/political_office_type.js';
|
||||||
|
import Candidate from '../models/falukant/data/candidate.js';
|
||||||
|
import Vote from '../models/falukant/data/vote.js';
|
||||||
|
import PoliticalOfficePrerequisite from '../models/falukant/predefine/political_office_prerequisite.js';
|
||||||
|
import PoliticalOfficeHistory from '../models/falukant/log/political_office_history.js';
|
||||||
|
|
||||||
function calcAge(birthdate) {
|
function calcAge(birthdate) {
|
||||||
const b = new Date(birthdate); b.setHours(0, 0);
|
const b = new Date(birthdate); b.setHours(0, 0);
|
||||||
@@ -103,6 +109,26 @@ class FalukantService extends BaseService {
|
|||||||
{ tr: "drunkOfLife", method: "healthDruckOfLife", cost: 5000000 }
|
{ tr: "drunkOfLife", method: "healthDruckOfLife", cost: 5000000 }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
static RECURSIVE_REGION_SEARCH = `
|
||||||
|
WITH RECURSIVE ancestors AS (
|
||||||
|
SELECT
|
||||||
|
r.id,
|
||||||
|
r.parent_id
|
||||||
|
FROM falukant_data.region r
|
||||||
|
join falukant_data."character" c
|
||||||
|
on c.region_id = r.id
|
||||||
|
WHERE c.user_id = :user_id
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
r.id,
|
||||||
|
r.parent_id
|
||||||
|
FROM falukant_data.region r
|
||||||
|
JOIN ancestors a ON r.id = a.parent_id
|
||||||
|
)
|
||||||
|
SELECT id
|
||||||
|
FROM ancestors;
|
||||||
|
`;
|
||||||
|
|
||||||
async getFalukantUserByHashedId(hashedId) {
|
async getFalukantUserByHashedId(hashedId) {
|
||||||
const user = await FalukantUser.findOne({
|
const user = await FalukantUser.findOne({
|
||||||
include: [
|
include: [
|
||||||
@@ -168,9 +194,21 @@ class FalukantService extends BaseService {
|
|||||||
attributes: ['name']
|
attributes: ['name']
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
model: UserHouse,
|
||||||
|
as: 'userHouse',
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: HouseType,
|
||||||
|
as: 'houseType',
|
||||||
|
'attributes': ['labelTr', 'position']
|
||||||
|
},
|
||||||
],
|
],
|
||||||
attributes: ['money', 'creditAmount', 'todayCreditTaken']
|
attributes: ['roofCondition'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
attributes: ['money', 'creditAmount', 'todayCreditTaken',]
|
||||||
});
|
});
|
||||||
if (!u) throw new Error('User not found');
|
if (!u) throw new Error('User not found');
|
||||||
if (u.character?.birthdate) u.character.setDataValue('age', calcAge(u.character.birthdate));
|
if (u.character?.birthdate) u.character.setDataValue('age', calcAge(u.character.birthdate));
|
||||||
@@ -277,6 +315,38 @@ class FalukantService extends BaseService {
|
|||||||
return bs.map(b => ({ ...b.toJSON(), isMainBranch: u.mainBranchRegionId === b.regionId }));
|
return bs.map(b => ({ ...b.toJSON(), isMainBranch: u.mainBranchRegionId === b.regionId }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createBranch(hashedUserId, cityId, branchTypeId) {
|
||||||
|
const user = await getFalukantUserOrFail(hashedUserId);
|
||||||
|
const branchType = await BranchType.findByPk(branchTypeId);
|
||||||
|
if (!branchType) {
|
||||||
|
throw new Error(`Unknown branchTypeId ${branchTypeId}`);
|
||||||
|
}
|
||||||
|
const existingCount = await Branch.count({
|
||||||
|
where: { falukantUserId: user.id }
|
||||||
|
});
|
||||||
|
const exponentBase = Math.max(existingCount, 1);
|
||||||
|
const rawCost = branchType.baseCost * Math.pow(exponentBase, 1.2);
|
||||||
|
const cost = Math.round(rawCost * 100) / 100;
|
||||||
|
await updateFalukantUserMoney(
|
||||||
|
user.id,
|
||||||
|
-cost,
|
||||||
|
'create_branch'
|
||||||
|
);
|
||||||
|
const branch = await Branch.create({
|
||||||
|
branchTypeId,
|
||||||
|
regionId: cityId,
|
||||||
|
falukantUserId: user.id
|
||||||
|
});
|
||||||
|
return branch;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBranchTypes(hashedUserId) {
|
||||||
|
const user = await getFalukantUserOrFail(hashedUserId);
|
||||||
|
const branchTypes = await BranchType.findAll();
|
||||||
|
return branchTypes;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
async getBranch(hashedUserId, branchId) {
|
async getBranch(hashedUserId, branchId) {
|
||||||
const u = await getFalukantUserOrFail(hashedUserId);
|
const u = await getFalukantUserOrFail(hashedUserId);
|
||||||
const br = await Branch.findOne({
|
const br = await Branch.findOne({
|
||||||
@@ -318,6 +388,8 @@ class FalukantService extends BaseService {
|
|||||||
const runningProductions = await Production.findAll({ where: { branchId: b.id } });
|
const runningProductions = await Production.findAll({ where: { branchId: b.id } });
|
||||||
if (runningProductions.length >= 2) {
|
if (runningProductions.length >= 2) {
|
||||||
throw new Error('Too many productions');
|
throw new Error('Too many productions');
|
||||||
|
return; // wird später implementiert, wenn familie implementiert ist.
|
||||||
|
|
||||||
}
|
}
|
||||||
if (!p) throw new Error('Product not found');
|
if (!p) throw new Error('Product not found');
|
||||||
quantity = Math.min(100, quantity);
|
quantity = Math.min(100, quantity);
|
||||||
@@ -822,9 +894,12 @@ class FalukantService extends BaseService {
|
|||||||
await this.deleteExpiredProposals();
|
await this.deleteExpiredProposals();
|
||||||
const existingProposals = await this.fetchProposals(falukantUserId, regionId);
|
const existingProposals = await this.fetchProposals(falukantUserId, regionId);
|
||||||
if (existingProposals.length > 0) {
|
if (existingProposals.length > 0) {
|
||||||
|
console.log('Existing proposals:', existingProposals);
|
||||||
return this.formatProposals(existingProposals);
|
return this.formatProposals(existingProposals);
|
||||||
}
|
}
|
||||||
|
console.log('No existing proposals, generating new ones');
|
||||||
await this.generateProposals(falukantUserId, regionId);
|
await this.generateProposals(falukantUserId, regionId);
|
||||||
|
console.log('Fetch new proposals');
|
||||||
const newProposals = await this.fetchProposals(falukantUserId, regionId);
|
const newProposals = await this.fetchProposals(falukantUserId, regionId);
|
||||||
return this.formatProposals(newProposals);
|
return this.formatProposals(newProposals);
|
||||||
}
|
}
|
||||||
@@ -867,13 +942,14 @@ class FalukantService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async generateProposals(falukantUserId, regionId) {
|
async generateProposals(falukantUserId, regionId) {
|
||||||
|
try {
|
||||||
|
const threeWeeksAgo = new Date(Date.now() - 21 * 24 * 60 * 60 * 1000);
|
||||||
const proposalCount = Math.floor(Math.random() * 3) + 3;
|
const proposalCount = Math.floor(Math.random() * 3) + 3;
|
||||||
for (let i = 0; i < proposalCount; i++) {
|
for (let i = 0; i < proposalCount; i++) {
|
||||||
const directorCharacter = await FalukantCharacter.findOne({
|
const directorCharacter = await FalukantCharacter.findOne({
|
||||||
where: {
|
where: {
|
||||||
regionId,
|
regionId,
|
||||||
createdAt: {
|
createdAt: { [Op.lt]: threeWeeksAgo },
|
||||||
[Op.lt]: new Date(Date.now() - 21 * 24 * 60 * 60 * 1000),
|
|
||||||
},
|
},
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
@@ -881,9 +957,8 @@ class FalukantService extends BaseService {
|
|||||||
as: 'nobleTitle',
|
as: 'nobleTitle',
|
||||||
attributes: ['level'],
|
attributes: ['level'],
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
},
|
order: sequelize.literal('RANDOM()'),
|
||||||
order: Sequelize.fn('RANDOM'),
|
|
||||||
});
|
});
|
||||||
if (!directorCharacter) {
|
if (!directorCharacter) {
|
||||||
throw new Error('No directors available for the region');
|
throw new Error('No directors available for the region');
|
||||||
@@ -898,6 +973,10 @@ class FalukantService extends BaseService {
|
|||||||
proposedIncome,
|
proposedIncome,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error.message, error.stack);
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async calculateAverageKnowledge(characterId) {
|
async calculateAverageKnowledge(characterId) {
|
||||||
@@ -1394,39 +1473,65 @@ class FalukantService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getGifts(hashedUserId) {
|
async getGifts(hashedUserId) {
|
||||||
|
// 1) Mein User & Character
|
||||||
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
||||||
const character = await FalukantCharacter.findOne({
|
const myChar = await FalukantCharacter.findOne({ where: { userId: user.id } });
|
||||||
where: { userId: user.id },
|
if (!myChar) throw new Error('Character not found');
|
||||||
|
|
||||||
|
// 2) Beziehung finden und „anderen“ Character bestimmen
|
||||||
|
const rel = await Relationship.findOne({
|
||||||
|
where: {
|
||||||
|
[Op.or]: [
|
||||||
|
{ character1Id: myChar.id },
|
||||||
|
{ character2Id: myChar.id }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{ model: FalukantCharacter, as: 'character1', include: [{ model: CharacterTrait, as: 'traits' }] },
|
||||||
|
{ model: FalukantCharacter, as: 'character2', include: [{ model: CharacterTrait, as: 'traits' }] }
|
||||||
|
]
|
||||||
});
|
});
|
||||||
if (!character) {
|
if (!rel) throw new Error('Beziehung nicht gefunden');
|
||||||
throw new Error('Character not found');
|
|
||||||
}
|
const relatedChar = rel.character1.id === myChar.id ? rel.character2 : rel.character1;
|
||||||
let gifts = await PromotionalGift.findAll({
|
|
||||||
|
// 3) Trait-IDs und Mood des relatedChar
|
||||||
|
const relatedTraitIds = relatedChar.traits.map(t => t.id);
|
||||||
|
const relatedMoodId = relatedChar.moodId;
|
||||||
|
|
||||||
|
// 4) Gifts laden – aber nur die passenden Moods und Traits als Unter-Arrays
|
||||||
|
const gifts = await PromotionalGift.findAll({
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: PromotionalGiftMood,
|
model: PromotionalGiftMood,
|
||||||
as: 'promotionalgiftmoods',
|
as: 'promotionalgiftmoods',
|
||||||
attributes: ['mood_id', 'suitability']
|
attributes: ['mood_id', 'suitability'],
|
||||||
|
where: { mood_id: relatedMoodId },
|
||||||
|
required: false // Gifts ohne Mood-Match bleiben erhalten, haben dann leeres Array
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
model: PromotionalGiftCharacterTrait,
|
model: PromotionalGiftCharacterTrait,
|
||||||
as: 'characterTraits',
|
as: 'characterTraits',
|
||||||
attributes: ['trait_id', 'suitability']
|
attributes: ['trait_id', 'suitability'],
|
||||||
|
where: { trait_id: relatedTraitIds },
|
||||||
|
required: false // Gifts ohne Trait-Match bleiben erhalten
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
const lowestTitleOfNobility = await TitleOfNobility.findOne({
|
|
||||||
order: [['id', 'ASC']],
|
// 5) Rest wie gehabt: Kosten berechnen und zurückgeben
|
||||||
});
|
const lowestTitleOfNobility = await TitleOfNobility.findOne({ order: [['id', 'ASC']] });
|
||||||
return await Promise.all(gifts.map(async (gift) => {
|
return Promise.all(gifts.map(async gift => ({
|
||||||
return {
|
|
||||||
id: gift.id,
|
id: gift.id,
|
||||||
name: gift.name,
|
name: gift.name,
|
||||||
cost: await this.getGiftCost(gift.value, character.titleOfNobility, lowestTitleOfNobility.id),
|
cost: await this.getGiftCost(
|
||||||
moodsAffects: gift.promotionalgiftmoods,
|
gift.value,
|
||||||
charactersAffects: gift.characterTraits,
|
myChar.titleOfNobility,
|
||||||
};
|
lowestTitleOfNobility.id
|
||||||
}));
|
),
|
||||||
|
moodsAffects: gift.promotionalgiftmoods, // nur Einträge mit relatedMoodId
|
||||||
|
charactersAffects: gift.characterTraits // nur Einträge mit relatedTraitIds
|
||||||
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
async getChildren(hashedUserId) {
|
async getChildren(hashedUserId) {
|
||||||
@@ -2282,6 +2387,398 @@ class FalukantService extends BaseService {
|
|||||||
const raw = Math.floor(Math.random() * 26);
|
const raw = Math.floor(Math.random() * 26);
|
||||||
return this.healthChange(user, raw);
|
return this.healthChange(user, raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getPoliticsOverview(hashedUserId) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOpenPolitics(hashedUserId) {
|
||||||
|
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
||||||
|
if (!user || user.character.nobleTitle.labelTr === 'noncivil') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async getElections(hashedUserId) {
|
||||||
|
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
||||||
|
if (!user || user.character.nobleTitle.labelTr === 'noncivil') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const rows = await sequelize.query(
|
||||||
|
FalukantService.RECURSIVE_REGION_SEARCH,
|
||||||
|
{
|
||||||
|
replacements: { user_id: user.id },
|
||||||
|
type: sequelize.QueryTypes.SELECT
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const regionIds = rows.map(r => r.id);
|
||||||
|
|
||||||
|
// 3) Zeitbereich "heute"
|
||||||
|
const todayStart = new Date();
|
||||||
|
todayStart.setHours(0, 0, 0, 0);
|
||||||
|
const todayEnd = new Date();
|
||||||
|
todayEnd.setHours(23, 59, 59, 999);
|
||||||
|
|
||||||
|
// 4) Wahlen laden (inkl. Kandidaten, Stimmen und Verknüpfungen)
|
||||||
|
const rawElections = await Election.findAll({
|
||||||
|
where: {
|
||||||
|
regionId: { [Op.in]: regionIds },
|
||||||
|
date: { [Op.between]: [todayStart, todayEnd] }
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: RegionData,
|
||||||
|
as: 'region',
|
||||||
|
attributes: ['name'],
|
||||||
|
include: [{
|
||||||
|
model: RegionType,
|
||||||
|
as: 'regionType',
|
||||||
|
attributes: ['labelTr']
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PoliticalOfficeType,
|
||||||
|
as: 'officeType',
|
||||||
|
attributes: ['name']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Candidate,
|
||||||
|
as: 'candidates',
|
||||||
|
attributes: ['id'],
|
||||||
|
include: [{
|
||||||
|
model: FalukantCharacter,
|
||||||
|
as: 'character',
|
||||||
|
attributes: ['birthdate', 'gender'],
|
||||||
|
include: [
|
||||||
|
{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
|
||||||
|
{ model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] },
|
||||||
|
{ model: TitleOfNobility, as: 'nobleTitle', attributes: ['labelTr'] }
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Vote,
|
||||||
|
as: 'votes',
|
||||||
|
attributes: ['candidateId'],
|
||||||
|
where: {
|
||||||
|
falukantUserId: user.id
|
||||||
|
},
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return rawElections.map(election => {
|
||||||
|
const e = election.get({ plain: true });
|
||||||
|
|
||||||
|
const voted = Array.isArray(e.votes) && e.votes.length > 0;
|
||||||
|
const reducedCandidates = (e.candidates || []).map(cand => {
|
||||||
|
const ch = cand.character || {};
|
||||||
|
const firstname = ch.definedFirstName?.name || '';
|
||||||
|
const lastname = ch.definedLastName?.name || '';
|
||||||
|
return {
|
||||||
|
id: cand.id,
|
||||||
|
title: ch.nobleTitle?.labelTr || null,
|
||||||
|
name: `${firstname} ${lastname}`.trim(),
|
||||||
|
age: calcAge(ch.birthdate),
|
||||||
|
gender: ch.gender
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: e.id,
|
||||||
|
officeType: { name: e.officeType.name },
|
||||||
|
region: {
|
||||||
|
name: e.region.name,
|
||||||
|
regionType: { labelTr: e.region.regionType.labelTr }
|
||||||
|
},
|
||||||
|
date: e.date,
|
||||||
|
postsToFill: e.postsToFill,
|
||||||
|
candidates: reducedCandidates,
|
||||||
|
voted: voted,
|
||||||
|
votedFor: voted ? e.votes.map(vote => { return vote.candidateId }) : null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async vote(hashedUserId, votes) {
|
||||||
|
const elections = await this.getElections(hashedUserId);
|
||||||
|
if (!Array.isArray(elections) || elections.length === 0) {
|
||||||
|
throw new Error('No elections found');
|
||||||
|
}
|
||||||
|
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('User not found');
|
||||||
|
}
|
||||||
|
const validElections = votes.filter(voteEntry => {
|
||||||
|
const e = elections.find(el => el.id === voteEntry.electionId);
|
||||||
|
return e && !e.voted;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (validElections.length === 0) {
|
||||||
|
throw new Error('No valid elections to vote for (either non‐existent or already voted)');
|
||||||
|
}
|
||||||
|
validElections.forEach(voteEntry => {
|
||||||
|
const e = elections.find(el => el.id === voteEntry.electionId);
|
||||||
|
const allowedIds = e.candidates.map(c => c.id);
|
||||||
|
voteEntry.candidateIds.forEach(cid => {
|
||||||
|
if (!allowedIds.includes(cid)) {
|
||||||
|
throw new Error(`Candidate ID ${cid} is not valid for election ${e.id}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (voteEntry.candidateIds.length > e.postsToFill) {
|
||||||
|
throw new Error(`Too many candidates selected for election ${e.id}. Allowed: ${e.postsToFill}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return await sequelize.transaction(async (tx) => {
|
||||||
|
const toCreate = [];
|
||||||
|
validElections.forEach(voteEntry => {
|
||||||
|
voteEntry.candidateIds.forEach(candidateId => {
|
||||||
|
toCreate.push({
|
||||||
|
electionId: voteEntry.electionId,
|
||||||
|
candidateId,
|
||||||
|
falukantUserId: user.id
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await Vote.bulkCreate(toCreate, {
|
||||||
|
transaction: tx,
|
||||||
|
ignoreDuplicates: true,
|
||||||
|
returning: false
|
||||||
|
});
|
||||||
|
return { success: true };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOpenPolitics(hashedUserId) {
|
||||||
|
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
||||||
|
const characterId = user.character.id;
|
||||||
|
const rows = await sequelize.query(
|
||||||
|
FalukantService.RECURSIVE_REGION_SEARCH,
|
||||||
|
{
|
||||||
|
replacements: { user_id: user.id },
|
||||||
|
type: sequelize.QueryTypes.SELECT
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const regionIds = rows.map(r => r.id);
|
||||||
|
const histories = await PoliticalOfficeHistory.findAll({
|
||||||
|
where: { characterId },
|
||||||
|
attributes: ['officeTypeId', 'startDate', 'endDate']
|
||||||
|
});
|
||||||
|
const heldOfficeTypeIds = histories.map(h => h.officeTypeId);
|
||||||
|
const allTypes = await PoliticalOfficeType.findAll({ attributes: ['id', 'name'] });
|
||||||
|
const nameToId = Object.fromEntries(allTypes.map(t => [t.name, t.id]));
|
||||||
|
const openPositions = await Election.findAll({
|
||||||
|
where: {
|
||||||
|
regionId: { [Op.in]: regionIds },
|
||||||
|
date: { [Op.lt]: new Date() }
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: RegionData,
|
||||||
|
as: 'region',
|
||||||
|
attributes: ['name'],
|
||||||
|
include: [
|
||||||
|
{ model: RegionType, as: 'regionType', attributes: ['labelTr'] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ model: Candidate, as: 'candidates' },
|
||||||
|
{
|
||||||
|
model: PoliticalOfficeType, as: 'officeType',
|
||||||
|
include: [{ model: PoliticalOfficePrerequisite, as: 'prerequisites' }]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
const result = openPositions
|
||||||
|
.filter(election => {
|
||||||
|
const prereqs = election.officeType.prerequisites || [];
|
||||||
|
return prereqs.some(pr => {
|
||||||
|
const jobs = pr.prerequisite.jobs;
|
||||||
|
if (!Array.isArray(jobs) || jobs.length === 0) return true;
|
||||||
|
return jobs.some(jobName => {
|
||||||
|
const reqId = nameToId[jobName];
|
||||||
|
return heldOfficeTypeIds.includes(reqId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.map(election => {
|
||||||
|
const e = election.get({ plain: true });
|
||||||
|
const jobs = e.officeType.prerequisites[0]?.prerequisite.jobs || [];
|
||||||
|
const matchingHistory = histories
|
||||||
|
.filter(h => jobs.includes(allTypes.find(t => t.id === h.officeTypeId)?.name))
|
||||||
|
.map(h => ({
|
||||||
|
officeTypeId: h.officeTypeId,
|
||||||
|
startDate: h.startDate,
|
||||||
|
endDate: h.endDate
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...e,
|
||||||
|
history: matchingHistory
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async applyForElections(hashedUserId, electionIds) {
|
||||||
|
// 1) Hole FalukantUser + Character
|
||||||
|
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('User nicht gefunden');
|
||||||
|
}
|
||||||
|
const character = user.character;
|
||||||
|
if (!character) {
|
||||||
|
throw new Error('Kein Charakter zum User gefunden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Noncivil‐Titel aussperren
|
||||||
|
if (character.nobleTitle.labelTr === 'noncivil') {
|
||||||
|
return { applied: [], skipped: electionIds };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Ermittle die heute offenen Wahlen, auf die er zugreifen darf
|
||||||
|
// (getElections liefert id, officeType, region, date, postsToFill, candidates, voted…)
|
||||||
|
const openElections = await this.getElections(hashedUserId);
|
||||||
|
const allowedIds = new Set(openElections.map(e => e.id));
|
||||||
|
|
||||||
|
// 4) Filter alle electionIds auf gültige/erlaubte
|
||||||
|
const toTry = electionIds.filter(id => allowedIds.has(id));
|
||||||
|
if (toTry.length === 0) {
|
||||||
|
return { applied: [], skipped: electionIds };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5) Prüfe, auf welche dieser Wahlen der Character bereits als Candidate eingetragen ist
|
||||||
|
const existing = await Candidate.findAll({
|
||||||
|
where: {
|
||||||
|
electionId: { [Op.in]: toTry },
|
||||||
|
characterId: character.id
|
||||||
|
},
|
||||||
|
attributes: ['electionId']
|
||||||
|
});
|
||||||
|
const alreadyIds = new Set(existing.map(c => c.electionId));
|
||||||
|
|
||||||
|
// 6) Erstelle Liste der Wahlen, für die er sich noch nicht beworben hat
|
||||||
|
const newApplications = toTry.filter(id => !alreadyIds.has(id));
|
||||||
|
const skipped = electionIds.filter(id => !newApplications.includes(id));
|
||||||
|
|
||||||
|
console.log(newApplications, skipped);
|
||||||
|
|
||||||
|
// 7) Bulk-Insert aller neuen Bewerbungen
|
||||||
|
if (newApplications.length > 0) {
|
||||||
|
const toInsert = newApplications.map(eid => ({
|
||||||
|
electionId: eid,
|
||||||
|
characterId: character.id
|
||||||
|
}));
|
||||||
|
await Candidate.bulkCreate(toInsert);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
applied: newApplications,
|
||||||
|
skipped: skipped
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRegions(hashedUserId) {
|
||||||
|
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
||||||
|
const regions = await RegionData.findAll({
|
||||||
|
attributes: ['id', 'name', 'map'],
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: RegionType,
|
||||||
|
as: 'regionType',
|
||||||
|
where: {
|
||||||
|
labelTr: 'city'
|
||||||
|
},
|
||||||
|
attributes: ['labelTr']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Branch,
|
||||||
|
as: 'branches',
|
||||||
|
where: {
|
||||||
|
falukantUserId: user.id
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: BranchType,
|
||||||
|
as: 'branchType',
|
||||||
|
attributes: ['labelTr'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
attributes: ['branchTypeId'],
|
||||||
|
required: false,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
return regions;
|
||||||
|
}
|
||||||
|
|
||||||
|
async renovate(hashedUserId, element) {
|
||||||
|
const user = await getFalukantUserOrFail(hashedUserId);
|
||||||
|
const house = await UserHouse.findOne({
|
||||||
|
where: { userId: user.id },
|
||||||
|
include: [{ model: HouseType, as: 'houseType' }]
|
||||||
|
});
|
||||||
|
if (!house) throw new Error('House not found');
|
||||||
|
const oldValue = house[element];
|
||||||
|
if (oldValue >= 100) {
|
||||||
|
return { cost: 0 };
|
||||||
|
}
|
||||||
|
const baseCost = house.houseType?.cost || 0;
|
||||||
|
const cost = this._calculateRenovationCost(baseCost, element, oldValue);
|
||||||
|
house[element] = 100;
|
||||||
|
await house.save();
|
||||||
|
await updateFalukantUserMoney(
|
||||||
|
user.id,
|
||||||
|
-cost,
|
||||||
|
`renovation_${element}`
|
||||||
|
);
|
||||||
|
return { cost };
|
||||||
|
}
|
||||||
|
|
||||||
|
_calculateRenovationCost(baseCost, key, currentVal) {
|
||||||
|
const weights = {
|
||||||
|
roofCondition: 0.25,
|
||||||
|
wallCondition: 0.25,
|
||||||
|
floorCondition: 0.25,
|
||||||
|
windowCondition: 0.25
|
||||||
|
};
|
||||||
|
const weight = weights[key] || 0;
|
||||||
|
const missing = 100 - currentVal;
|
||||||
|
const raw = (missing / 100) * baseCost * weight;
|
||||||
|
return Math.round(raw * 100) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
async renovateAll(hashedUserId) {
|
||||||
|
const user = await getFalukantUserOrFail(hashedUserId);
|
||||||
|
const house = await UserHouse.findOne({
|
||||||
|
where: { userId: user.id },
|
||||||
|
include: [{ model: HouseType, as: 'houseType' }]
|
||||||
|
});
|
||||||
|
if (!house) throw new Error('House not found');
|
||||||
|
const baseCost = house.houseType?.cost || 0;
|
||||||
|
const keys = ['roofCondition', 'wallCondition', 'floorCondition', 'windowCondition'];
|
||||||
|
let rawSum = 0;
|
||||||
|
for (const key of keys) {
|
||||||
|
const current = house[key];
|
||||||
|
if (current < 100) {
|
||||||
|
rawSum += this._calculateRenovationCost(baseCost, key, current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const totalCost = Math.round(rawSum * 0.8 * 100) / 100;
|
||||||
|
for (const key of keys) {
|
||||||
|
house[key] = 100;
|
||||||
|
}
|
||||||
|
await house.save();
|
||||||
|
await updateFalukantUserMoney(
|
||||||
|
user.id,
|
||||||
|
-totalCost,
|
||||||
|
'renovation_all'
|
||||||
|
);
|
||||||
|
|
||||||
|
return { cost: totalCost };
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new FalukantService();
|
export default new FalukantService();
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import PartyType from "../../models/falukant/type/party.js";
|
|||||||
import MusicType from "../../models/falukant/type/music.js";
|
import MusicType from "../../models/falukant/type/music.js";
|
||||||
import BanquetteType from "../../models/falukant/type/banquette.js";
|
import BanquetteType from "../../models/falukant/type/banquette.js";
|
||||||
import LearnRecipient from "../../models/falukant/type/learn_recipient.js";
|
import LearnRecipient from "../../models/falukant/type/learn_recipient.js";
|
||||||
|
import PoliticalOfficeType from "../../models/falukant/type/political_office_type.js";
|
||||||
|
import PoliticalOfficePrerequisite from "../../models/falukant/predefine/political_office_prerequisite.js";
|
||||||
|
|
||||||
export const initializeFalukantTypes = async () => {
|
export const initializeFalukantTypes = async () => {
|
||||||
await initializeFalukantTypeRegions();
|
await initializeFalukantTypeRegions();
|
||||||
@@ -25,6 +27,8 @@ export const initializeFalukantTypes = async () => {
|
|||||||
await initializeFalukantMusicTypes();
|
await initializeFalukantMusicTypes();
|
||||||
await initializeFalukantBanquetteTypes();
|
await initializeFalukantBanquetteTypes();
|
||||||
await initializeLearnerTypes();
|
await initializeLearnerTypes();
|
||||||
|
await initializePoliticalOfficeTypes();
|
||||||
|
await initializePoliticalOfficePrerequisites();
|
||||||
};
|
};
|
||||||
|
|
||||||
const regionTypes = [];
|
const regionTypes = [];
|
||||||
@@ -39,14 +43,15 @@ const regionTypeTrs = [
|
|||||||
|
|
||||||
const regions = [
|
const regions = [
|
||||||
{ labelTr: "falukant", regionType: "country", parentTr: null },
|
{ labelTr: "falukant", regionType: "country", parentTr: null },
|
||||||
{ labelTr: "duchy1", regionType: "duchy", parentTr: "falukant" },
|
{ labelTr: "Hessen", regionType: "duchy", parentTr: "falukant" },
|
||||||
{ labelTr: "markgravate", regionType: "markgravate", parentTr: "duchy1" },
|
{ labelTr: "Groß-Benbach", regionType: "markgravate", parentTr: "Hessen" },
|
||||||
{ labelTr: "shire1", regionType: "shire", parentTr: "markgravate" },
|
{ labelTr: "Siebenbachen", regionType: "shire", parentTr: "Groß-Benbach" },
|
||||||
{ labelTr: "county1", regionType: "county", parentTr: "shire1" },
|
{ labelTr: "Bad Homburg", regionType: "county", parentTr: "Siebenbachen" },
|
||||||
{ labelTr: "town1", regionType: "city", parentTr: "county1" },
|
{ labelTr: "Maintal", regionType: "county", parentTr: "Siebenbachen" },
|
||||||
{ labelTr: "town2", regionType: "city", parentTr: "county1" },
|
{ labelTr: "Frankfurt", regionType: "city", parentTr: "Bad Homburg", map: {x: 187, y: 117, w: 10, h:11} },
|
||||||
{ labelTr: "town3", regionType: "city", parentTr: "county1" },
|
{ labelTr: "Oberursel", regionType: "city", parentTr: "Bad Homburg", map: {x: 168, y: 121, w: 10, h:11} },
|
||||||
{ labelTr: "town4", regionType: "city", parentTr: "county1" },
|
{ labelTr: "Offenbach", regionType: "city", parentTr: "Bad Homburg", map: {x: 171, y: 142, w: 10, h:11} },
|
||||||
|
{ labelTr: "Königstein", regionType: "city", parentTr: "Maintal", map: {x: 207, y: 124, w: 24, h:18} },
|
||||||
];
|
];
|
||||||
|
|
||||||
const relationships = [
|
const relationships = [
|
||||||
@@ -256,6 +261,282 @@ const learnerTypes = [
|
|||||||
{ tr: 'director', },
|
{ tr: 'director', },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const politicalOffices = [
|
||||||
|
{ tr: "assessor", seatsPerRegion: 10, regionType: "city", termLength: 5 },
|
||||||
|
{ tr: "councillor", seatsPerRegion: 7, regionType: "city", termLength: 7 },
|
||||||
|
{ tr: "council", seatsPerRegion: 5, regionType: "city", termLength: 4 },
|
||||||
|
{ tr: "beadle", seatsPerRegion: 1, regionType: "city", termLength: 6 },
|
||||||
|
{ tr: "town-clerk", seatsPerRegion: 3, regionType: "city", termLength: 10 },
|
||||||
|
{ tr: "mayor", seatsPerRegion: 1, regionType: "city", termLength: 3 },
|
||||||
|
{ tr: "master-builder", seatsPerRegion: 10, regionType: "county", termLength: 10 },
|
||||||
|
{ tr: "village-major", seatsPerRegion: 6, regionType: "county", termLength: 5 },
|
||||||
|
{ tr: "judge", seatsPerRegion: 3, regionType: "county", termLength: 8 },
|
||||||
|
{ tr: "bailif", seatsPerRegion: 1, regionType: "county", termLength: 4 },
|
||||||
|
{ tr: "taxman", seatsPerRegion: 8, regionType: "shire", termLength: 5 },
|
||||||
|
{ tr: "sheriff", seatsPerRegion: 5, regionType: "shire", termLength: 8 },
|
||||||
|
{ tr: "consultant", seatsPerRegion: 3, regionType: "shire", termLength: 9 },
|
||||||
|
{ tr: "treasurer", seatsPerRegion: 1, regionType: "shire", termLength: 7 },
|
||||||
|
{ tr: "hangman", seatsPerRegion: 9, regionType: "markgravate", termLength: 5 },
|
||||||
|
{ tr: "territorial-council", seatsPerRegion: 6, regionType: "markgravate", termLength: 6 },
|
||||||
|
{ tr: "territorial-council-speaker", seatsPerRegion: 4, regionType: "markgravate", termLength: 8 },
|
||||||
|
{ tr: "ruler-consultant", seatsPerRegion: 1, regionType: "markgravate", termLength: 3 },
|
||||||
|
{ tr: "state-administrator", seatsPerRegion: 7, regionType: "duchy", termLength: 3 },
|
||||||
|
{ tr: "super-state-administrator", seatsPerRegion: 5, regionType: "duchy", termLength: 6 },
|
||||||
|
{ tr: "governor", seatsPerRegion: 1, regionType: "duchy", termLength: 5 },
|
||||||
|
{ tr: "ministry-helper", seatsPerRegion: 12, regionType: "country", termLength: 4 },
|
||||||
|
{ tr: "minister", seatsPerRegion: 3, regionType: "country", termLength: 4 },
|
||||||
|
{ tr: "chancellor", seatsPerRegion: 1, regionType: "country", termLength: 4 }
|
||||||
|
];
|
||||||
|
|
||||||
|
const politicalOfficePrerequisites = [
|
||||||
|
{
|
||||||
|
officeTr: "assessor",
|
||||||
|
prerequisite: {
|
||||||
|
titles: [
|
||||||
|
"civil", "sir", "townlord", "by", "landlord", "knight",
|
||||||
|
"baron", "count", "palsgrave", "margrave", "landgrave",
|
||||||
|
"ruler", "elector", "imperial-prince", "duke", "grand-duke",
|
||||||
|
"prince-regent", "king"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
officeTr: "councillor",
|
||||||
|
prerequisite: {
|
||||||
|
titles: [
|
||||||
|
"civil", "sir", "townlord", "by", "landlord", "knight",
|
||||||
|
"baron", "count", "palsgrave", "margrave", "landgrave",
|
||||||
|
"ruler", "elector", "imperial-prince", "duke", "grand-duke",
|
||||||
|
"prince-regent", "king"
|
||||||
|
],
|
||||||
|
jobs: ["assessor"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
officeTr: "council",
|
||||||
|
prerequisite: {
|
||||||
|
titles: [
|
||||||
|
"civil", "sir", "townlord", "by", "landlord", "knight",
|
||||||
|
"baron", "count", "palsgrave", "margrave", "landgrave",
|
||||||
|
"ruler", "elector", "imperial-prince", "duke", "grand-duke",
|
||||||
|
"prince-regent", "king"
|
||||||
|
],
|
||||||
|
jobs: ["councillor"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
officeTr: "beadle",
|
||||||
|
prerequisite: {
|
||||||
|
titles: [
|
||||||
|
"sir", "townlord", "by", "landlord", "knight", "baron",
|
||||||
|
"count", "palsgrave", "margrave", "landgrave", "ruler",
|
||||||
|
"elector", "imperial-prince", "duke", "grand-duke", "prince-regent", "king"
|
||||||
|
],
|
||||||
|
jobs: ["council"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
officeTr: "town-clerk",
|
||||||
|
prerequisite: {
|
||||||
|
titles: [
|
||||||
|
"sir", "townlord", "by", "landlord", "knight", "baron",
|
||||||
|
"count", "palsgrave", "margrave", "landgrave", "ruler",
|
||||||
|
"elector", "imperial-prince", "duke", "grand-duke", "prince-regent", "king"
|
||||||
|
],
|
||||||
|
jobs: ["council"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
officeTr: "mayor",
|
||||||
|
prerequisite: {
|
||||||
|
titles: [
|
||||||
|
"townlord", "by", "landlord", "knight", "baron", "count",
|
||||||
|
"palsgrave", "margrave", "landgrave", "ruler", "elector",
|
||||||
|
"imperial-prince", "duke", "grand-duke", "prince-regent", "king"
|
||||||
|
],
|
||||||
|
jobs: ["beadle", "town-clerk"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
officeTr: "master-builder",
|
||||||
|
prerequisite: {
|
||||||
|
titles: [
|
||||||
|
"by", "landlord", "knight", "baron", "count", "palsgrave",
|
||||||
|
"margrave", "landgrave", "ruler", "elector", "imperial-prince",
|
||||||
|
"duke", "grand-duke", "prince-regent", "king"
|
||||||
|
],
|
||||||
|
jobs: ["mayor"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
officeTr: "village-major",
|
||||||
|
prerequisite: {
|
||||||
|
titles: [
|
||||||
|
"by", "landlord", "knight", "baron", "count", "palsgrave",
|
||||||
|
"margrave", "landgrave", "ruler", "elector", "imperial-prince",
|
||||||
|
"duke", "grand-duke", "prince-regent", "king"
|
||||||
|
],
|
||||||
|
jobs: ["master-builder"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
officeTr: "judge",
|
||||||
|
prerequisite: {
|
||||||
|
titles: [
|
||||||
|
"landlord", "knight", "baron", "count", "palsgrave", "margrave",
|
||||||
|
"landgrave", "ruler", "elector", "imperial-prince", "duke",
|
||||||
|
"grand-duke", "prince-regent", "king"
|
||||||
|
],
|
||||||
|
jobs: ["village-major"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
officeTr: "bailif",
|
||||||
|
prerequisite: {
|
||||||
|
titles: [
|
||||||
|
"landlord", "knight", "baron", "count", "palsgrave", "margrave",
|
||||||
|
"landgrave", "ruler", "elector", "imperial-prince", "duke",
|
||||||
|
"grand-duke", "prince-regent", "king"
|
||||||
|
],
|
||||||
|
jobs: ["judge"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
officeTr: "taxman",
|
||||||
|
prerequisite: {
|
||||||
|
titles: [
|
||||||
|
"knight", "baron", "count", "palsgrave", "margrave", "landgrave",
|
||||||
|
"ruler", "elector", "imperial-prince", "duke", "grand-duke", "prince-regent", "king"
|
||||||
|
],
|
||||||
|
jobs: ["bailif"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
officeTr: "sheriff",
|
||||||
|
prerequisite: {
|
||||||
|
titles: [
|
||||||
|
"baron", "count", "palsgrave", "margrave", "landgrave", "ruler",
|
||||||
|
"elector", "imperial-prince", "duke", "grand-duke", "prince-regent", "king"
|
||||||
|
],
|
||||||
|
jobs: ["taxman"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
officeTr: "consultant",
|
||||||
|
prerequisite: {
|
||||||
|
titles: [
|
||||||
|
"baron", "count", "palsgrave", "margrave", "landgrave", "ruler",
|
||||||
|
"elector", "imperial-prince", "duke", "grand-duke", "prince-regent", "king"
|
||||||
|
],
|
||||||
|
jobs: ["sheriff"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
officeTr: "treasurer",
|
||||||
|
prerequisite: {
|
||||||
|
titles: [
|
||||||
|
"count", "palsgrave", "margrave", "landgrave", "ruler", "elector",
|
||||||
|
"imperial-prince", "duke", "grand-duke", "prince-regent", "king"
|
||||||
|
],
|
||||||
|
jobs: ["consultant"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
officeTr: "hangman",
|
||||||
|
prerequisite: {
|
||||||
|
titles: [
|
||||||
|
"count", "palsgrave", "margrave", "landgrave", "ruler", "elector",
|
||||||
|
"imperial-prince", "duke", "grand-duke", "prince-regent", "king"
|
||||||
|
],
|
||||||
|
jobs: ["consultant"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
officeTr: "territorial-council",
|
||||||
|
prerequisite: {
|
||||||
|
titles: [
|
||||||
|
"palsgrave", "margrave", "landgrave", "ruler", "elector", "imperial-prince",
|
||||||
|
"duke", "grand-duke", "prince-regent", "king"
|
||||||
|
],
|
||||||
|
jobs: ["treasurer", "hangman"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
officeTr: "territorial-council-speaker",
|
||||||
|
prerequisite: {
|
||||||
|
titles: [
|
||||||
|
"margrave", "landgrave", "ruler", "elector", "imperial-prince", "duke",
|
||||||
|
"grand-duke", "prince-regent", "king"
|
||||||
|
],
|
||||||
|
jobs: ["territorial-council"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
officeTr: "ruler-consultant",
|
||||||
|
prerequisite: {
|
||||||
|
titles: [
|
||||||
|
"landgrave", "ruler", "elector", "imperial-prince", "duke", "grand-duke",
|
||||||
|
"prince-regent", "king"
|
||||||
|
],
|
||||||
|
jobs: ["territorial-council-speaker"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
officeTr: "state-administrator",
|
||||||
|
prerequisite: {
|
||||||
|
titles: [
|
||||||
|
"ruler", "elector", "imperial-prince", "duke", "grand-duke", "prince-regent", "king"
|
||||||
|
],
|
||||||
|
jobs: ["ruler-consultant"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
officeTr: "super-state-administrator",
|
||||||
|
prerequisite: {
|
||||||
|
titles: [
|
||||||
|
"elector", "imperial-prince", "duke", "grand-duke", "prince-regent", "king"
|
||||||
|
],
|
||||||
|
jobs: ["state-administrator"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
officeTr: "governor",
|
||||||
|
prerequisite: {
|
||||||
|
titles: [
|
||||||
|
"imperial-prince", "duke", "grand-duke", "prince-regent", "king"
|
||||||
|
],
|
||||||
|
jobs: ["super-state-administrator"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
officeTr: "ministry-helper",
|
||||||
|
prerequisite: {
|
||||||
|
titles: [
|
||||||
|
"grand-duke", "prince-regent", "king"
|
||||||
|
],
|
||||||
|
jobs: ["governor"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
officeTr: "minister",
|
||||||
|
prerequisite: {
|
||||||
|
titles: [
|
||||||
|
"prince-regent", "king"
|
||||||
|
],
|
||||||
|
jobs: ["ministry-helper"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
officeTr: "chancellor",
|
||||||
|
prerequisite: {
|
||||||
|
titles: [
|
||||||
|
"prince-regent", "king"
|
||||||
|
],
|
||||||
|
jobs: ["minister"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
{
|
{
|
||||||
const giftNames = promotionalGifts.map(g => g.name);
|
const giftNames = promotionalGifts.map(g => g.name);
|
||||||
const traitNames = characterTraits.map(t => t.name);
|
const traitNames = characterTraits.map(t => t.name);
|
||||||
@@ -330,7 +611,8 @@ export const initializeFalukantRegions = async () => {
|
|||||||
where: { name: region.labelTr },
|
where: { name: region.labelTr },
|
||||||
defaults: {
|
defaults: {
|
||||||
regionTypeId: regionType.id,
|
regionTypeId: regionType.id,
|
||||||
parentId: parentRegion?.id || null
|
parentId: parentRegion?.id || null,
|
||||||
|
map: parentRegion?.map || {},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -479,3 +761,36 @@ export const initializeLearnerTypes = async () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const initializePoliticalOfficeTypes = async () => {
|
||||||
|
for (const po of politicalOffices) {
|
||||||
|
await PoliticalOfficeType.findOrCreate({
|
||||||
|
where: { name: po.tr },
|
||||||
|
defaults: {
|
||||||
|
seatsPerRegion: po.seatsPerRegion,
|
||||||
|
regionType: po.regionType,
|
||||||
|
termLength: po.termLength
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initializePoliticalOfficePrerequisites = async () => {
|
||||||
|
for (const prereq of politicalOfficePrerequisites) {
|
||||||
|
// zunächst den OfficeType anhand seines Namens (tr) ermitteln
|
||||||
|
const office = await PoliticalOfficeType.findOne({
|
||||||
|
where: { name: prereq.officeTr }
|
||||||
|
});
|
||||||
|
if (!office) continue;
|
||||||
|
|
||||||
|
// Nun findOrCreate mit dem neuen Spaltennamen:
|
||||||
|
await PoliticalOfficePrerequisite.findOrCreate({
|
||||||
|
where: { office_type_id: office.id },
|
||||||
|
defaults: {
|
||||||
|
office_type_id: office.id,
|
||||||
|
prerequisite: prereq.prerequisite
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
1393
frontend/package-lock.json
generated
1393
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,13 +8,13 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tinymce/tinymce-vue": "^6.0.1",
|
"@tiptap/starter-kit": "^2.14.0",
|
||||||
|
"@tiptap/vue-3": "^2.14.0",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"tinymce": "^7.3.0",
|
|
||||||
"vue": "~3.4.31",
|
"vue": "~3.4.31",
|
||||||
"vue-i18n": "^10.0.0-beta.2",
|
"vue-i18n": "^10.0.0-beta.2",
|
||||||
"vue-multiselect": "^3.1.0",
|
"vue-multiselect": "^3.1.0",
|
||||||
@@ -30,6 +30,6 @@
|
|||||||
"sass": "^1.77.8",
|
"sass": "^1.77.8",
|
||||||
"stream-browserify": "^3.0.0",
|
"stream-browserify": "^3.0.0",
|
||||||
"util": "^0.12.5",
|
"util": "^0.12.5",
|
||||||
"vite": "^5.4.4"
|
"vite": "^6.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
frontend/public/images/falukant/map.png
Normal file
BIN
frontend/public/images/falukant/map.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 MiB |
@@ -4,7 +4,7 @@
|
|||||||
v-for="tab in tabs"
|
v-for="tab in tabs"
|
||||||
:key="tab.value"
|
:key="tab.value"
|
||||||
:class="['simple-tab', { active: internalValue === tab.value }]"
|
:class="['simple-tab', { active: internalValue === tab.value }]"
|
||||||
@click="$emit('update:modelValue', tab.value)"
|
@click="selectTab(tab.value)"
|
||||||
>
|
>
|
||||||
<slot name="label" :tab="tab">
|
<slot name="label" :tab="tab">
|
||||||
{{ $t(tab.label) }}
|
{{ $t(tab.label) }}
|
||||||
@@ -30,6 +30,14 @@
|
|||||||
internalValue() {
|
internalValue() {
|
||||||
return this.modelValue;
|
return this.modelValue;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
selectTab(value) {
|
||||||
|
// 1) v-model aktualisieren
|
||||||
|
this.$emit('update:modelValue', value);
|
||||||
|
// 2) zusätzliches change-Event
|
||||||
|
this.$emit('change', value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -53,4 +61,3 @@
|
|||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
@@ -11,19 +11,36 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button @click="$emit('createBranch')">{{ $t('falukant.branch.actions.create') }}</button>
|
<button @click="openCreateBranchDialog">
|
||||||
<button @click="$emit('upgradeBranch')" :disabled="!localSelectedBranch">
|
{{ $t('falukant.branch.actions.create') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="$emit('upgradeBranch')"
|
||||||
|
:disabled="!localSelectedBranch"
|
||||||
|
>
|
||||||
{{ $t('falukant.branch.actions.upgrade') }}
|
{{ $t('falukant.branch.actions.upgrade') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Dialog-Komponente -->
|
||||||
|
<CreateBranchDialog
|
||||||
|
ref="createBranchDialog"
|
||||||
|
:regions="availableRegions"
|
||||||
|
@create-branch="handleCreateBranch"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import FormattedDropdown from '@/components/form/FormattedDropdown.vue';
|
import FormattedDropdown from '@/components/form/FormattedDropdown.vue';
|
||||||
|
import CreateBranchDialog from '@/dialogues/falukant/CreateBranchDialog.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "BranchSelection",
|
name: "BranchSelection",
|
||||||
components: { FormattedDropdown },
|
components: {
|
||||||
|
FormattedDropdown,
|
||||||
|
CreateBranchDialog,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
branches: { type: Array, required: true },
|
branches: { type: Array, required: true },
|
||||||
selectedBranch: { type: Object, default: null },
|
selectedBranch: { type: Object, default: null },
|
||||||
@@ -42,10 +59,20 @@
|
|||||||
this.localSelectedBranch = newVal;
|
this.localSelectedBranch = newVal;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
updateSelectedBranch(value) {
|
updateSelectedBranch(value) {
|
||||||
this.$emit('branchSelected', value);
|
this.$emit('branchSelected', value);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
openCreateBranchDialog() {
|
||||||
|
this.$refs.createBranchDialog.open();
|
||||||
|
},
|
||||||
|
|
||||||
|
handleCreateBranch() {
|
||||||
|
// wird ausgelöst, sobald der Dialog onConfirm erfolgreich abschließt
|
||||||
|
this.$emit('createBranch');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -57,8 +84,8 @@
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
margin: 5px;
|
margin: 5px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<p>{{ contact.message }}</p>
|
<p>{{ contact.message }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="editor-container">
|
<div class="editor-container">
|
||||||
<Editor v-model="answer" :init="tinymceInitOptions" :api-key="apiKey" />
|
<EditorContent :editor="editor" class="editor" />
|
||||||
</div>
|
</div>
|
||||||
</DialogWidget>
|
</DialogWidget>
|
||||||
|
|
||||||
@@ -19,8 +19,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { ref, onBeforeUnmount } from 'vue'
|
import { Editor, EditorContent } from '@tiptap/vue-3'
|
||||||
import Editor from '@tinymce/tinymce-vue'
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
import apiClient from '@/utils/axios.js'
|
import apiClient from '@/utils/axios.js'
|
||||||
import DialogWidget from '@/components/DialogWidget.vue'
|
import DialogWidget from '@/components/DialogWidget.vue'
|
||||||
|
|
||||||
@@ -28,29 +28,15 @@ export default {
|
|||||||
name: 'AnswerContact',
|
name: 'AnswerContact',
|
||||||
components: {
|
components: {
|
||||||
DialogWidget,
|
DialogWidget,
|
||||||
Editor,
|
EditorContent,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
apiKey: import.meta.env.VITE_TINYMCE_API_KEY,
|
|
||||||
dialog: null,
|
dialog: null,
|
||||||
errorDialog: null,
|
errorDialog: null,
|
||||||
contact: null,
|
contact: null,
|
||||||
answer: '',
|
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
tinymceInitOptions: {
|
editor: null,
|
||||||
height: 300,
|
|
||||||
menubar: false,
|
|
||||||
plugins: [
|
|
||||||
'advlist autolink lists link image charmap print preview anchor',
|
|
||||||
'searchreplace visualblocks code fullscreen',
|
|
||||||
'insertdatetime media table paste code help wordcount'
|
|
||||||
],
|
|
||||||
toolbar:
|
|
||||||
'undo redo cut copy paste | bold italic forecolor fontfamily fontsize | \
|
|
||||||
alignleft aligncenter alignright alignjustify | \
|
|
||||||
bullist numlist outdent indent | removeformat | help'
|
|
||||||
},
|
|
||||||
buttons: [
|
buttons: [
|
||||||
{ text: 'OK', action: this.sendAnswer },
|
{ text: 'OK', action: this.sendAnswer },
|
||||||
{ text: 'Cancel', action: this.closeDialog }
|
{ text: 'Cancel', action: this.closeDialog }
|
||||||
@@ -64,24 +50,25 @@ export default {
|
|||||||
open(contactData) {
|
open(contactData) {
|
||||||
this.contact = contactData;
|
this.contact = contactData;
|
||||||
this.dialog.open();
|
this.dialog.open();
|
||||||
this.answer = '';
|
if (this.editor) this.editor.commands.setContent('');
|
||||||
},
|
},
|
||||||
closeDialog() {
|
closeDialog() {
|
||||||
this.dialog.close();
|
this.dialog.close();
|
||||||
this.answer = '';
|
if (this.editor) this.editor.commands.clearContent();
|
||||||
},
|
},
|
||||||
closeErrorDialog() {
|
closeErrorDialog() {
|
||||||
this.errorDialog.close();
|
this.errorDialog.close();
|
||||||
},
|
},
|
||||||
async sendAnswer() {
|
async sendAnswer() {
|
||||||
|
const answer = this.editor ? this.editor.getHTML() : '';
|
||||||
try {
|
try {
|
||||||
await apiClient.post('/api/admin/contacts/answer', {
|
await apiClient.post('/api/admin/contacts/answer', {
|
||||||
id: this.contact.id,
|
id: this.contact.id,
|
||||||
answer: this.answer,
|
answer,
|
||||||
});
|
});
|
||||||
this.dialog.close();
|
this.dialog.close();
|
||||||
this.$emit('refresh');
|
this.$emit('refresh');
|
||||||
this.answer = '';
|
if (this.editor) this.editor.commands.clearContent();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorText = error.response?.data?.error || 'An unexpected error occurred.';
|
const errorText = error.response?.data?.error || 'An unexpected error occurred.';
|
||||||
this.errorMessage = errorText;
|
this.errorMessage = errorText;
|
||||||
@@ -92,9 +79,16 @@ export default {
|
|||||||
mounted() {
|
mounted() {
|
||||||
this.dialog = this.$refs.dialog;
|
this.dialog = this.$refs.dialog;
|
||||||
this.errorDialog = this.$refs.errorDialog;
|
this.errorDialog = this.$refs.errorDialog;
|
||||||
|
|
||||||
|
this.editor = new Editor({
|
||||||
|
extensions: [StarterKit],
|
||||||
|
content: '',
|
||||||
|
});
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
// Aufräumarbeiten falls nötig
|
if (this.editor) {
|
||||||
|
this.editor.destroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -106,5 +100,13 @@ export default {
|
|||||||
|
|
||||||
.editor-container {
|
.editor-container {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 10px;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor {
|
||||||
|
min-height: 150px;
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
342
frontend/src/dialogues/falukant/CreateBranchDialog.vue
Normal file
342
frontend/src/dialogues/falukant/CreateBranchDialog.vue
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
<template>
|
||||||
|
<DialogWidget
|
||||||
|
ref="dialog"
|
||||||
|
name="create-branch"
|
||||||
|
:title="$t('falukant.branch.actions.create')"
|
||||||
|
icon="branch.png"
|
||||||
|
showClose
|
||||||
|
:buttons="dialogButtons"
|
||||||
|
@close="onClose"
|
||||||
|
>
|
||||||
|
<div class="create-branch-form">
|
||||||
|
<div class="map-wrapper">
|
||||||
|
<!-- linke Spalte: Karte + Regionen + Dev-Draw -->
|
||||||
|
<div class="map-container">
|
||||||
|
<img
|
||||||
|
ref="mapImage"
|
||||||
|
src="/images/falukant/map.png"
|
||||||
|
class="map"
|
||||||
|
@mousedown="onMouseDown"
|
||||||
|
@mousemove="onMouseMove"
|
||||||
|
@mouseup="onMouseUp"
|
||||||
|
@mouseleave="onMouseUp"
|
||||||
|
@dragstart.prevent
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-for="city in cities"
|
||||||
|
:key="city.name"
|
||||||
|
class="city-region"
|
||||||
|
:class="city.branches.length > 0 ? 'has-branch' : 'clickable'"
|
||||||
|
:style="{
|
||||||
|
top: city.map.y + 'px',
|
||||||
|
left: city.map.x + 'px',
|
||||||
|
width: city.map.w + 'px',
|
||||||
|
height: city.map.h + 'px'
|
||||||
|
}"
|
||||||
|
@click="city.branches.length === 0 && onCityClick(city)"
|
||||||
|
:title="city.name"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
v-if="devMode && rect"
|
||||||
|
class="dev-rect"
|
||||||
|
:style="{
|
||||||
|
top: rect.y + 'px',
|
||||||
|
left: rect.x + 'px',
|
||||||
|
width: rect.width + 'px',
|
||||||
|
height: rect.height + 'px'
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- rechte Spalte: Dev-Info + Auswahl -->
|
||||||
|
<div class="sidebar">
|
||||||
|
<div v-if="devMode" class="dev-info">
|
||||||
|
<span class="dev-badge">DEV MODE</span>
|
||||||
|
<span v-if="rect" class="dev-label-outside">
|
||||||
|
{{ rect.x }},{{ rect.y }} {{ rect.width }}×{{ rect.height }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="selectedRegion" class="selected-region-wrapper">
|
||||||
|
<div class="selected-region">
|
||||||
|
{{ $t('falukant.branch.selection.selected') }}:
|
||||||
|
<strong>{{ selectedRegion.name }}</strong>
|
||||||
|
</div>
|
||||||
|
<label class="form-label">
|
||||||
|
{{ $t('falukant.branch.columns.type') }}
|
||||||
|
<select v-model="selectedType" class="form-control">
|
||||||
|
<option
|
||||||
|
v-for="type in branchTypes"
|
||||||
|
:key="type.id"
|
||||||
|
:value="type.id"
|
||||||
|
>
|
||||||
|
{{ $t(`falukant.branch.types.${type.labelTr}`) }}
|
||||||
|
({{ formatCost(computeBranchCost(type)) }})
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogWidget>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import DialogWidget from '@/components/DialogWidget.vue';
|
||||||
|
import apiClient from '@/utils/axios.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CreateBranchDialog',
|
||||||
|
components: { DialogWidget },
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
cities: [],
|
||||||
|
branchTypes: [],
|
||||||
|
selectedRegion: null,
|
||||||
|
selectedType: null,
|
||||||
|
devMode: false,
|
||||||
|
rect: null,
|
||||||
|
startX: null,
|
||||||
|
startY: null,
|
||||||
|
currentX: 0,
|
||||||
|
currentY: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
dialogButtons() {
|
||||||
|
return [
|
||||||
|
{ text: this.$t('Cancel'), action: this.close },
|
||||||
|
{ text: this.$t('falukant.branch.actions.create'), action: this.onConfirm },
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
window.addEventListener('keydown', this.onKeyDown);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
this.loadCities(),
|
||||||
|
this.loadBranchTypes(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.selectedType = this.branchTypes.length ? this.branchTypes[0].id : null;
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener('keydown', this.onKeyDown);
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
open() {
|
||||||
|
this.$refs.dialog.open();
|
||||||
|
},
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.$refs.dialog.close();
|
||||||
|
},
|
||||||
|
|
||||||
|
async onConfirm() {
|
||||||
|
if (!this.selectedRegion || !this.selectedType) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await apiClient.post('/api/falukant/branches', {
|
||||||
|
cityId: this.selectedRegion.id,
|
||||||
|
branchTypeId: this.selectedType,
|
||||||
|
});
|
||||||
|
this.$emit('create-branch');
|
||||||
|
this.close();
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onClose() {
|
||||||
|
this.close();
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
|
||||||
|
onKeyDown(e) {
|
||||||
|
if (e.ctrlKey && e.altKey && e.code === 'KeyD') {
|
||||||
|
this.devMode = !this.devMode;
|
||||||
|
if (!this.devMode) this.rect = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onMouseDown(e) {
|
||||||
|
if (!this.devMode) return;
|
||||||
|
|
||||||
|
const bounds = this.$refs.mapImage.getBoundingClientRect();
|
||||||
|
this.startX = e.clientX - bounds.left;
|
||||||
|
this.startY = e.clientY - bounds.top;
|
||||||
|
this.currentX = this.startX;
|
||||||
|
this.currentY = this.startY;
|
||||||
|
|
||||||
|
this.updateRect();
|
||||||
|
e.preventDefault();
|
||||||
|
},
|
||||||
|
|
||||||
|
onMouseMove(e) {
|
||||||
|
if (!this.devMode || this.startX === null) return;
|
||||||
|
|
||||||
|
const bounds = this.$refs.mapImage.getBoundingClientRect();
|
||||||
|
this.currentX = e.clientX - bounds.left;
|
||||||
|
this.currentY = e.clientY - bounds.top;
|
||||||
|
|
||||||
|
this.updateRect();
|
||||||
|
},
|
||||||
|
|
||||||
|
onMouseUp() {
|
||||||
|
if (!this.devMode) return;
|
||||||
|
this.startX = null;
|
||||||
|
this.startY = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateRect() {
|
||||||
|
if (this.startX === null || this.startY === null) return;
|
||||||
|
|
||||||
|
const x = Math.min(this.startX, this.currentX);
|
||||||
|
const y = Math.min(this.startY, this.currentY);
|
||||||
|
const width = Math.abs(this.currentX - this.startX);
|
||||||
|
const height = Math.abs(this.currentY - this.startY);
|
||||||
|
|
||||||
|
this.rect = {
|
||||||
|
x: Math.round(x),
|
||||||
|
y: Math.round(y),
|
||||||
|
width: Math.round(width),
|
||||||
|
height: Math.round(height),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadCities() {
|
||||||
|
const { data } = await apiClient.get('/api/falukant/cities');
|
||||||
|
this.cities = data;
|
||||||
|
},
|
||||||
|
|
||||||
|
onCityClick(city) {
|
||||||
|
this.selectedRegion = city;
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadBranchTypes() {
|
||||||
|
const { data } = await apiClient.get('/api/falukant/branches/types');
|
||||||
|
this.branchTypes = data;
|
||||||
|
},
|
||||||
|
|
||||||
|
computeBranchCost(type) {
|
||||||
|
const total = this.cities.reduce((sum, city) => sum + city.branches.length, 0);
|
||||||
|
const factor = Math.pow(Math.max(total, 1), 1.2);
|
||||||
|
const raw = type.baseCost * factor;
|
||||||
|
return Math.round(raw * 100) / 100;
|
||||||
|
},
|
||||||
|
|
||||||
|
formatCost(value) {
|
||||||
|
return new Intl.NumberFormat(navigator.language, {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
}).format(value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.create-branch-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-container {
|
||||||
|
position: relative;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map {
|
||||||
|
max-width: 500px;
|
||||||
|
max-height: 400px;
|
||||||
|
user-select: none;
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
|
||||||
|
.city-region {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.city-region.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
background: rgba(0, 0, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.city-region.has-branch {
|
||||||
|
cursor: default;
|
||||||
|
background: transparent;
|
||||||
|
border: 2px solid #00ff00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-rect {
|
||||||
|
position: absolute;
|
||||||
|
border: 2px dashed red;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-badge {
|
||||||
|
background: rgba(255, 0, 0, 0.7);
|
||||||
|
color: white;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-label-outside {
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-region-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-region {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -51,8 +51,8 @@ export default {
|
|||||||
selectedProposal: null,
|
selectedProposal: null,
|
||||||
products: [],
|
products: [],
|
||||||
buttons: [
|
buttons: [
|
||||||
{ text: 'Einstellen', action: this.hireDirector },
|
{ text: this.$t('falukant.newdirector.hire'), action: this.hireDirector },
|
||||||
{ text: 'Abbrechen', action: 'close' },
|
{ text: this.$t('Cancel'), action: 'close' },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -55,8 +55,7 @@
|
|||||||
<img :src="imagePreview" alt="Image Preview"
|
<img :src="imagePreview" alt="Image Preview"
|
||||||
style="max-width: 100px; max-height: 100px;" />
|
style="max-width: 100px; max-height: 100px;" />
|
||||||
</div>
|
</div>
|
||||||
<editor v-model="newEntryContent" :init="tinymceInitOptions" :api-key="apiKey"
|
<EditorContent :editor="editor" class="editor" />
|
||||||
tinymce-script-src="/tinymce/tinymce.min.js"></editor>
|
|
||||||
</div>
|
</div>
|
||||||
<button @click="submitGuestbookEntry">{{ $t('socialnetwork.profile.guestbook.submit')
|
<button @click="submitGuestbookEntry">{{ $t('socialnetwork.profile.guestbook.submit')
|
||||||
}}</button>
|
}}</button>
|
||||||
@@ -95,14 +94,15 @@
|
|||||||
import DialogWidget from '@/components/DialogWidget.vue';
|
import DialogWidget from '@/components/DialogWidget.vue';
|
||||||
import apiClient from '@/utils/axios.js';
|
import apiClient from '@/utils/axios.js';
|
||||||
import FolderItem from '../../components/FolderItem.vue';
|
import FolderItem from '../../components/FolderItem.vue';
|
||||||
import TinyMCEEditor from '@tinymce/tinymce-vue';
|
import { Editor, EditorContent } from '@tiptap/vue-3'
|
||||||
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'UserProfileDialog',
|
name: 'UserProfileDialog',
|
||||||
components: {
|
components: {
|
||||||
DialogWidget,
|
DialogWidget,
|
||||||
FolderItem,
|
FolderItem,
|
||||||
editor: TinyMCEEditor,
|
EditorContent,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -126,27 +126,20 @@ export default {
|
|||||||
{ name: 'guestbook', label: this.$t('socialnetwork.profile.tab.guestbook') }
|
{ name: 'guestbook', label: this.$t('socialnetwork.profile.tab.guestbook') }
|
||||||
],
|
],
|
||||||
apiKey: import.meta.env.VITE_TINYMCE_API_KEY,
|
apiKey: import.meta.env.VITE_TINYMCE_API_KEY,
|
||||||
tinymceInitOptions: {
|
editor: null,
|
||||||
script_url: '/tinymce/tinymce.min.js',
|
|
||||||
height: 300,
|
|
||||||
menubar: true,
|
|
||||||
plugins: [
|
|
||||||
'lists', 'link',
|
|
||||||
'searchreplace', 'visualblocks', 'code',
|
|
||||||
'insertdatetime', 'table'
|
|
||||||
],
|
|
||||||
toolbar:
|
|
||||||
'undo redo cut copy paste | bold italic forecolor backcolor fontfamily fontsize| \
|
|
||||||
alignleft aligncenter alignright alignjustify | \
|
|
||||||
bullist numlist outdent indent | removeformat | link visualblocks code',
|
|
||||||
contextmenu: 'link image table',
|
|
||||||
menubar: 'edit format table',
|
|
||||||
promotion: false,
|
|
||||||
},
|
|
||||||
hasSendFriendshipRequest: false,
|
hasSendFriendshipRequest: false,
|
||||||
friendshipState: 'none',
|
friendshipState: 'none',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
mounted: async function () {
|
||||||
|
this.editor = new Editor({
|
||||||
|
extensions: [StarterKit],
|
||||||
|
content: '',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
beforeUnmount: function () {
|
||||||
|
if (this.editor) this.editor.destroy();
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
open() {
|
open() {
|
||||||
this.$refs.dialog.open();
|
this.$refs.dialog.open();
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
"age": "Alter",
|
"age": "Alter",
|
||||||
"wealth": "Vermögen",
|
"wealth": "Vermögen",
|
||||||
"health": "Gesundheit",
|
"health": "Gesundheit",
|
||||||
"events": "Ereignisse"
|
"events": "Ereignisse",
|
||||||
|
"relationship": "Beziehung"
|
||||||
},
|
},
|
||||||
"health": {
|
"health": {
|
||||||
"amazing": "Super",
|
"amazing": "Super",
|
||||||
@@ -97,7 +98,8 @@
|
|||||||
"selection": {
|
"selection": {
|
||||||
"title": "Niederlassungsauswahl",
|
"title": "Niederlassungsauswahl",
|
||||||
"selected": "Ausgewählte Niederlassung",
|
"selected": "Ausgewählte Niederlassung",
|
||||||
"placeholder": "Noch keine Niederlassung ausgewählt"
|
"placeholder": "Noch keine Niederlassung ausgewählt",
|
||||||
|
"selectedcity": "Ausgewählte Stadt"
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"create": "Neue Niederlassung erstellen",
|
"create": "Neue Niederlassung erstellen",
|
||||||
@@ -223,7 +225,6 @@
|
|||||||
"3": "Mittel",
|
"3": "Mittel",
|
||||||
"4": "Hoch",
|
"4": "Hoch",
|
||||||
"5": "Sehr hoch"
|
"5": "Sehr hoch"
|
||||||
|
|
||||||
},
|
},
|
||||||
"mood": "Stimmung",
|
"mood": "Stimmung",
|
||||||
"progress": "Zuneigung",
|
"progress": "Zuneigung",
|
||||||
@@ -363,9 +364,22 @@
|
|||||||
"happy": "Glücklich",
|
"happy": "Glücklich",
|
||||||
"sad": "Traurig",
|
"sad": "Traurig",
|
||||||
"angry": "Wütend",
|
"angry": "Wütend",
|
||||||
"scared": "Verängstigt",
|
"nervous": "Nervös",
|
||||||
"surprised": "Überrascht",
|
"excited": "Aufgeregt",
|
||||||
"normal": "Normal"
|
"bored": "Gelangweilt",
|
||||||
|
"fearful": "Ängstlich",
|
||||||
|
"confident": "Selbstbewusst",
|
||||||
|
"curious": "Neugierig",
|
||||||
|
"hopeful": "Hoffnungsvoll",
|
||||||
|
"frustrated": "Frustriert",
|
||||||
|
"lonely": "Einsam",
|
||||||
|
"grateful": "Dankbar",
|
||||||
|
"jealous": "Eifersüchtig",
|
||||||
|
"guilty": "Schuldig",
|
||||||
|
"apathetic": "Apathisch",
|
||||||
|
"relieved": "Erleichtert",
|
||||||
|
"proud": "Stolz",
|
||||||
|
"ashamed": "Beschämt"
|
||||||
},
|
},
|
||||||
"character": {
|
"character": {
|
||||||
"brave": "Mutig",
|
"brave": "Mutig",
|
||||||
@@ -595,6 +609,72 @@
|
|||||||
"barber": "Barbier"
|
"barber": "Barbier"
|
||||||
},
|
},
|
||||||
"choose": "Bitte auswählen"
|
"choose": "Bitte auswählen"
|
||||||
|
},
|
||||||
|
"politics": {
|
||||||
|
"title": "Politik",
|
||||||
|
"tabs": {
|
||||||
|
"current": "Aktuelle Position",
|
||||||
|
"upcoming": "Anstehende Neuwahl-Positionen",
|
||||||
|
"elections": "Wahlen"
|
||||||
|
},
|
||||||
|
"current": {
|
||||||
|
"office": "Amt",
|
||||||
|
"region": "Region",
|
||||||
|
"termEnds": "Läuft ab am",
|
||||||
|
"income": "Einkommen",
|
||||||
|
"none": "Keine aktuelle Position vorhanden.",
|
||||||
|
"holder": "Inhaber"
|
||||||
|
},
|
||||||
|
"open": {
|
||||||
|
"office": "Amt",
|
||||||
|
"region": "Region",
|
||||||
|
"date": "Datum",
|
||||||
|
"candidacy": "Kandidatur",
|
||||||
|
"none": "Keine offenen Positionen."
|
||||||
|
},
|
||||||
|
"upcoming": {
|
||||||
|
"office": "Amt",
|
||||||
|
"region": "Region",
|
||||||
|
"postDate": "Datum",
|
||||||
|
"none": "Keine anstehenden Positionen."
|
||||||
|
},
|
||||||
|
"elections": {
|
||||||
|
"office": "Amt",
|
||||||
|
"region": "Region",
|
||||||
|
"date": "Datum",
|
||||||
|
"posts": "Zu besetzende Posten",
|
||||||
|
"none": "Keine Wahlen vorhanden.",
|
||||||
|
"choose": "Kandidaten",
|
||||||
|
"vote": "Stimme abgeben",
|
||||||
|
"voteAll": "Alle Stimmen abgeben",
|
||||||
|
"candidates": "Kandidaten",
|
||||||
|
"action": "Aktion"
|
||||||
|
},
|
||||||
|
"offices": {
|
||||||
|
"chancellor": "Kanzler",
|
||||||
|
"minister": "Minister",
|
||||||
|
"ministry-helper": "Ministerhelfer",
|
||||||
|
"governor": "Gouverneur",
|
||||||
|
"super-state-administrator": "Oberstaatsverwalter",
|
||||||
|
"state-administrator": "Staatsverwalter",
|
||||||
|
"ruler-consultant": "Berater des Herrschers",
|
||||||
|
"territorial-council-speaker": "Sprecher des Territorialrats",
|
||||||
|
"territorial-council": "Territorialrat",
|
||||||
|
"hangman": "Henker",
|
||||||
|
"treasurer": "Schatzmeister",
|
||||||
|
"sheriff": "Sheriff",
|
||||||
|
"taxman": "Steuereintreiber",
|
||||||
|
"bailif": "Gerichtsdiener",
|
||||||
|
"judge": "Richter",
|
||||||
|
"village-major": "Dorfvorsteher",
|
||||||
|
"master-builder": "Baumeister",
|
||||||
|
"mayor": "Bürgermeister",
|
||||||
|
"town-clerk": "Stadtschreiber",
|
||||||
|
"beadle": "Schulze",
|
||||||
|
"council": "Ratsherr",
|
||||||
|
"councillor": "Stadtrat",
|
||||||
|
"assessor": "Schätzer"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,7 @@ import EducationView from '../views/falukant/EducationView.vue';
|
|||||||
import BankView from '../views/falukant/BankView.vue';
|
import BankView from '../views/falukant/BankView.vue';
|
||||||
import DirectorView from '../views/falukant/DirectorView.vue';
|
import DirectorView from '../views/falukant/DirectorView.vue';
|
||||||
import HealthView from '../views/falukant/HealthView.vue';
|
import HealthView from '../views/falukant/HealthView.vue';
|
||||||
|
import PoliticsView from '../views/falukant/PoliticsView.vue';
|
||||||
|
|
||||||
const falukantRoutes = [
|
const falukantRoutes = [
|
||||||
{
|
{
|
||||||
@@ -91,6 +92,12 @@ const falukantRoutes = [
|
|||||||
component: HealthView,
|
component: HealthView,
|
||||||
meta: { requiresAuth: true }
|
meta: { requiresAuth: true }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/falukant/politics',
|
||||||
|
name: 'PoliticsView',
|
||||||
|
component: PoliticsView,
|
||||||
|
meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default falukantRoutes;
|
export default falukantRoutes;
|
||||||
|
|||||||
@@ -3,16 +3,48 @@
|
|||||||
<StatusBar ref="statusBar" />
|
<StatusBar ref="statusBar" />
|
||||||
<div class="contentscroll">
|
<div class="contentscroll">
|
||||||
<h2>{{ $t('falukant.branch.title') }}</h2>
|
<h2>{{ $t('falukant.branch.title') }}</h2>
|
||||||
<BranchSelection :branches="branches" :selectedBranch="selectedBranch" @branchSelected="onBranchSelected"
|
|
||||||
@createBranch="createBranch" @upgradeBranch="upgradeBranch" ref="branchSelection" />
|
<BranchSelection
|
||||||
<DirectorInfo v-if="selectedBranch" :branchId="selectedBranch.id" ref="directorInfo" />
|
:branches="branches"
|
||||||
<SaleSection v-if="selectedBranch" :branchId="selectedBranch.id" ref="saleSection" />
|
:selectedBranch="selectedBranch"
|
||||||
<ProductionSection v-if="selectedBranch" :branchId="selectedBranch.id" :products="products"
|
@branchSelected="onBranchSelected"
|
||||||
ref="productionSection" />
|
@createBranch="createBranch"
|
||||||
<StorageSection v-if="selectedBranch" :branchId="selectedBranch.id" ref="storageSection" />
|
@upgradeBranch="upgradeBranch"
|
||||||
<RevenueSection v-if="selectedBranch" :products="products"
|
ref="branchSelection"
|
||||||
:calculateProductRevenue="calculateProductRevenue" :calculateProductProfit="calculateProductProfit"
|
/>
|
||||||
ref="revenueSection" />
|
|
||||||
|
<DirectorInfo
|
||||||
|
v-if="selectedBranch"
|
||||||
|
:branchId="selectedBranch.id"
|
||||||
|
ref="directorInfo"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SaleSection
|
||||||
|
v-if="selectedBranch"
|
||||||
|
:branchId="selectedBranch.id"
|
||||||
|
ref="saleSection"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProductionSection
|
||||||
|
v-if="selectedBranch"
|
||||||
|
:branchId="selectedBranch.id"
|
||||||
|
:products="products"
|
||||||
|
ref="productionSection"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StorageSection
|
||||||
|
v-if="selectedBranch"
|
||||||
|
:branchId="selectedBranch.id"
|
||||||
|
ref="storageSection"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RevenueSection
|
||||||
|
v-if="selectedBranch"
|
||||||
|
:products="products"
|
||||||
|
:calculateProductRevenue="calculateProductRevenue"
|
||||||
|
:calculateProductProfit="calculateProductProfit"
|
||||||
|
ref="revenueSection"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -39,6 +71,7 @@ export default {
|
|||||||
StorageSection,
|
StorageSection,
|
||||||
RevenueSection,
|
RevenueSection,
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
branches: [],
|
branches: [],
|
||||||
@@ -46,42 +79,32 @@ export default {
|
|||||||
products: [],
|
products: [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['socket', 'daemonSocket']),
|
...mapState(['socket', 'daemonSocket']),
|
||||||
},
|
},
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await this.loadBranches();
|
await this.loadBranches();
|
||||||
|
|
||||||
const branchId = this.$route.params.branchId;
|
const branchId = this.$route.params.branchId;
|
||||||
await this.loadProducts();
|
await this.loadProducts();
|
||||||
|
|
||||||
if (branchId) {
|
if (branchId) {
|
||||||
this.selectedBranch =
|
this.selectedBranch = this.branches.find(
|
||||||
this.branches.find(branch => branch.id === parseInt(branchId)) || null;
|
b => b.id === parseInt(branchId, 10)
|
||||||
|
) || null;
|
||||||
} else {
|
} else {
|
||||||
this.selectMainBranch();
|
this.selectMainBranch();
|
||||||
}
|
}
|
||||||
const events = [
|
|
||||||
"production_ready",
|
// Daemon-Socket
|
||||||
"stock_change",
|
|
||||||
"price_update",
|
|
||||||
"director_death",
|
|
||||||
"production_started",
|
|
||||||
"selled_items",
|
|
||||||
"falukantUpdateStatus",
|
|
||||||
"falukantBranchUpdate",
|
|
||||||
];
|
|
||||||
if (this.daemonSocket) {
|
if (this.daemonSocket) {
|
||||||
this.daemonSocket.addEventListener('message', this.handleDaemonMessage);
|
this.daemonSocket.addEventListener('message', this.handleDaemonMessage);
|
||||||
}
|
}
|
||||||
events.forEach(eventName => {
|
|
||||||
if (this.socket) {
|
// Live-Socket-Events
|
||||||
this.socket.on(eventName, (data) => {
|
[
|
||||||
this.handleEvent({ event: eventName, ...data });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
beforeUnmount() {
|
|
||||||
const events = [
|
|
||||||
"production_ready",
|
"production_ready",
|
||||||
"stock_change",
|
"stock_change",
|
||||||
"price_update",
|
"price_update",
|
||||||
@@ -90,16 +113,36 @@ export default {
|
|||||||
"selled_items",
|
"selled_items",
|
||||||
"falukantUpdateStatus",
|
"falukantUpdateStatus",
|
||||||
"falukantBranchUpdate",
|
"falukantBranchUpdate",
|
||||||
];
|
"knowledge_update"
|
||||||
events.forEach(eventName => {
|
].forEach(eventName => {
|
||||||
|
if (this.socket) {
|
||||||
|
this.socket.on(eventName, data => this.handleEvent({ event: eventName, ...data }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeUnmount() {
|
||||||
|
[
|
||||||
|
"production_ready",
|
||||||
|
"stock_change",
|
||||||
|
"price_update",
|
||||||
|
"director_death",
|
||||||
|
"production_started",
|
||||||
|
"selled_items",
|
||||||
|
"falukantUpdateStatus",
|
||||||
|
"falukantBranchUpdate",
|
||||||
|
"knowledge_update"
|
||||||
|
].forEach(eventName => {
|
||||||
if (this.socket) {
|
if (this.socket) {
|
||||||
this.socket.off(eventName, this.handleEvent);
|
this.socket.off(eventName, this.handleEvent);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.daemonSocket) {
|
if (this.daemonSocket) {
|
||||||
this.daemonSocket.removeEventListener('message', this.handleDaemonMessage);
|
this.daemonSocket.removeEventListener('message', this.handleDaemonMessage);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async loadBranches() {
|
async loadBranches() {
|
||||||
try {
|
try {
|
||||||
@@ -117,6 +160,7 @@ export default {
|
|||||||
console.error('Error loading branches:', error);
|
console.error('Error loading branches:', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async loadProducts() {
|
async loadProducts() {
|
||||||
try {
|
try {
|
||||||
const productsResult = await apiClient.get('/api/falukant/products');
|
const productsResult = await apiClient.get('/api/falukant/products');
|
||||||
@@ -125,31 +169,34 @@ export default {
|
|||||||
console.error('Error loading products:', error);
|
console.error('Error loading products:', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleEvent(event) {
|
|
||||||
if (event.type === 'branchUpdated') {
|
onBranchSelected(newBranch) {
|
||||||
this.loadBranches();
|
this.selectedBranch = newBranch;
|
||||||
|
},
|
||||||
|
|
||||||
|
async createBranch() {
|
||||||
|
// Nach erfolgreichem Dialog-Event: neu laden
|
||||||
|
await this.loadBranches();
|
||||||
|
},
|
||||||
|
|
||||||
|
upgradeBranch() {
|
||||||
|
if (this.selectedBranch) {
|
||||||
|
alert(
|
||||||
|
this.$t(
|
||||||
|
'falukant.branch.actions.upgradeAlert',
|
||||||
|
{ branchId: this.selectedBranch.id }
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleDaemonMessage(event) {
|
|
||||||
const message = JSON.parse(event.data);
|
|
||||||
},
|
|
||||||
selectMainBranch() {
|
selectMainBranch() {
|
||||||
const main = this.branches.find(b => b.isMainBranch) || null;
|
const main = this.branches.find(b => b.isMainBranch) || null;
|
||||||
if (main && main !== this.selectedBranch) {
|
if (main && main !== this.selectedBranch) {
|
||||||
this.selectedBranch = main;
|
this.selectedBranch = main;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onBranchSelected(newBranch) {
|
|
||||||
this.selectedBranch = newBranch;
|
|
||||||
},
|
|
||||||
createBranch() {
|
|
||||||
alert(this.$t('falukant.branch.actions.createAlert'));
|
|
||||||
},
|
|
||||||
upgradeBranch() {
|
|
||||||
if (this.selectedBranch) {
|
|
||||||
alert(this.$t('falukant.branch.actions.upgradeAlert', { branchId: this.selectedBranch.id }));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
calculateProductRevenue(product) {
|
calculateProductRevenue(product) {
|
||||||
if (!product.knowledges || product.knowledges.length === 0) {
|
if (!product.knowledges || product.knowledges.length === 0) {
|
||||||
return { absolute: 0, perMinute: 0 };
|
return { absolute: 0, perMinute: 0 };
|
||||||
@@ -158,55 +205,61 @@ export default {
|
|||||||
const maxPrice = product.sellCost;
|
const maxPrice = product.sellCost;
|
||||||
const minPrice = maxPrice * 0.6;
|
const minPrice = maxPrice * 0.6;
|
||||||
const revenuePerUnit = minPrice + (maxPrice - minPrice) * (knowledgeFactor / 100);
|
const revenuePerUnit = minPrice + (maxPrice - minPrice) * (knowledgeFactor / 100);
|
||||||
const perMinute = product.productionTime > 0 ? revenuePerUnit / product.productionTime : 0;
|
const perMinute = product.productionTime > 0
|
||||||
|
? revenuePerUnit / product.productionTime
|
||||||
|
: 0;
|
||||||
return {
|
return {
|
||||||
absolute: revenuePerUnit.toFixed(2),
|
absolute: revenuePerUnit.toFixed(2),
|
||||||
perMinute: perMinute.toFixed(2),
|
perMinute: perMinute.toFixed(2),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
calculateProductProfit(product) {
|
calculateProductProfit(product) {
|
||||||
const { absolute: revenueAbsoluteStr, perMinute: revenuePerMinuteStr } = this.calculateProductRevenue(product);
|
const { absolute: revenueAbsoluteStr, perMinute: revenuePerMinuteStr }
|
||||||
|
= this.calculateProductRevenue(product);
|
||||||
const revenueAbsolute = parseFloat(revenueAbsoluteStr);
|
const revenueAbsolute = parseFloat(revenueAbsoluteStr);
|
||||||
const costPerUnit = 6 * product.category;
|
const costPerUnit = 6 * product.category;
|
||||||
const profitAbsolute = revenueAbsolute - costPerUnit;
|
const profitAbsolute = revenueAbsolute - costPerUnit;
|
||||||
const costPerMinute = product.productionTime > 0 ? costPerUnit / product.productionTime : 0;
|
const costPerMinute = product.productionTime > 0
|
||||||
|
? costPerUnit / product.productionTime
|
||||||
|
: 0;
|
||||||
const profitPerMinute = parseFloat(revenuePerMinuteStr) - costPerMinute;
|
const profitPerMinute = parseFloat(revenuePerMinuteStr) - costPerMinute;
|
||||||
return {
|
return {
|
||||||
absolute: profitAbsolute.toFixed(2),
|
absolute: profitAbsolute.toFixed(2),
|
||||||
perMinute: profitPerMinute.toFixed(2),
|
perMinute: profitPerMinute.toFixed(2),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
// Gemeinsamer Event-Handler für socket-Events
|
|
||||||
handleEvent(eventData) {
|
handleEvent(eventData) {
|
||||||
switch (eventData.event || eventData) {
|
switch (eventData.event) {
|
||||||
case 'production_ready':
|
case 'production_ready':
|
||||||
this.$refs.productionSection && this.$refs.productionSection.loadProductions();
|
this.$refs.productionSection?.loadProductions();
|
||||||
this.$refs.storageSection && this.$refs.storageSection.loadStorageData();
|
this.$refs.storageSection ?.loadStorageData();
|
||||||
this.$refs.saleSection && this.$refs.saleSection.loadInventory();
|
this.$refs.saleSection ?.loadInventory();
|
||||||
break;
|
break;
|
||||||
case 'stock_change':
|
case 'stock_change':
|
||||||
this.$refs.storageSection && this.$refs.storageSection.loadStorageData();
|
this.$refs.storageSection ?.loadStorageData();
|
||||||
this.$refs.saleSection && this.$refs.saleSection.loadInventory();
|
this.$refs.saleSection ?.loadInventory();
|
||||||
break;
|
break;
|
||||||
case 'price_update':
|
case 'price_update':
|
||||||
this.$refs.revenueSection && this.$refs.revenueSection.refresh && this.$refs.revenueSection.refresh();
|
this.$refs.revenueSection?.refresh();
|
||||||
break;
|
break;
|
||||||
case 'director_death':
|
case 'director_death':
|
||||||
this.$refs.directorInfo && this.$refs.directorInfo.loadDirector();
|
this.$refs.directorInfo?.loadDirector();
|
||||||
break;
|
break;
|
||||||
case 'production_started':
|
case 'production_started':
|
||||||
this.$refs.productionSection && this.$refs.productionSection.loadProductions();
|
this.$refs.productionSection?.loadProductions();
|
||||||
break;
|
break;
|
||||||
case 'selled_items':
|
case 'selled_items':
|
||||||
this.$refs.saleSection && this.$refs.saleSection.loadInventory();
|
this.$refs.saleSection ?.loadInventory();
|
||||||
this.$refs.storageSection && this.$refs.storageSection.loadStorageData();
|
this.$refs.storageSection?.loadStorageData();
|
||||||
break;
|
break;
|
||||||
case 'falukantUpdateStatus':
|
case 'falukantUpdateStatus':
|
||||||
case 'falukantBranchUpdate':
|
case 'falukantBranchUpdate':
|
||||||
this.$refs.statusBar && this.$refs.statusBar.updateStatus && this.$refs.statusBar.updateStatus(eventData);
|
this.$refs.statusBar?.fetchStatus();
|
||||||
this.$refs.productionSection && this.$refs.productionSection.loadProductions();
|
this.$refs.productionSection?.loadProductions();
|
||||||
this.$refs.storageSection && this.$refs.storageSection.loadStorageData();
|
this.$refs.storageSection ?.loadStorageData();
|
||||||
this.$refs.saleSection && this.$refs.saleSection.loadInventory();
|
this.$refs.saleSection ?.loadInventory();
|
||||||
break;
|
break;
|
||||||
case 'knowledge_update':
|
case 'knowledge_update':
|
||||||
this.loadProducts();
|
this.loadProducts();
|
||||||
@@ -216,13 +269,14 @@ export default {
|
|||||||
console.log('Unhandled event:', eventData);
|
console.log('Unhandled event:', eventData);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleDaemonMessage(event) {
|
handleDaemonMessage(event) {
|
||||||
if (event.data === "ping") return;
|
if (event.data === 'ping') return;
|
||||||
try {
|
try {
|
||||||
const message = JSON.parse(event.data);
|
const message = JSON.parse(event.data);
|
||||||
this.handleEvent(message);
|
this.handleEvent(message);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error processing daemon message in BranchView:', error);
|
console.error('Error processing daemon message:', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -234,3 +288,4 @@ h2 {
|
|||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
@@ -47,8 +47,9 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="characteristic in relationships[0].character2.characterTrait"
|
<li v-for="trait in relationships[0].character2.traits" :key="trait.id">
|
||||||
:key="characteristic.id">{{ $t(`falukant.character.${characteristic.tr}`) }}</li>
|
{{ $t(`falukant.character.${trait.tr}`) }}
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="relationships[0].relationshipType === 'wooing'">
|
<div v-if="relationships[0].relationshipType === 'wooing'">
|
||||||
@@ -64,9 +65,11 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="gift in gifts" :key="gift.id">
|
<tr v-for="gift in gifts" :key="gift.id">
|
||||||
<td><input type="radio" name="gift" :value="gift.id" v-model="selectedGiftId"></td>
|
<td>
|
||||||
|
<input type="radio" name="gift" :value="gift.id" v-model="selectedGiftId" />
|
||||||
|
</td>
|
||||||
<td>{{ $t(`falukant.gifts.${gift.name}`) }}</td>
|
<td>{{ $t(`falukant.gifts.${gift.name}`) }}</td>
|
||||||
<td>{{ $t(`falukant.family.spouse.giftAffect.${getEffect(gift)}`) }}</td>
|
<td>{{ getEffect(gift) }}</td>
|
||||||
<td>{{ formatCost(gift.cost) }}</td>
|
<td>{{ formatCost(gift.cost) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -123,7 +126,8 @@
|
|||||||
{{ child.name }}
|
{{ child.name }}
|
||||||
</td>
|
</td>
|
||||||
<td v-else>
|
<td v-else>
|
||||||
<button @click="jumpToChurchForm">{{ $t('falukant.family.children.baptism') }}</button>
|
<button @click="jumpToChurchForm">{{ $t('falukant.family.children.baptism')
|
||||||
|
}}</button>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ child.age }}</td>
|
<td>{{ child.age }}</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -308,13 +312,39 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
handleDaemonMessage() {
|
handleDaemonMessage(event) {
|
||||||
|
if (event.data === 'ping') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const message = JSON.parse(event.data);
|
const message = JSON.parse(event.data);
|
||||||
if (message.event === 'children_update') {
|
if (message.event === 'children_update') {
|
||||||
this.loadFamilyData();
|
this.loadFamilyData();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getEffect(gift) {
|
||||||
|
// aktueller Partner
|
||||||
|
const partner = this.relationships[0].character2;
|
||||||
|
// seine aktuelle Mood-ID
|
||||||
|
const moodId = partner.mood?.id ?? partner.mood_id;
|
||||||
|
|
||||||
|
// 1) Mood-Eintrag finden
|
||||||
|
const moodEntry = gift.moodsAffects.find(ma => ma.mood_id === moodId);
|
||||||
|
const moodValue = moodEntry ? moodEntry.suitability : 0;
|
||||||
|
|
||||||
|
// 2) Trait-Einträge matchen
|
||||||
|
let highestTraitValue = 0;
|
||||||
|
for (const trait of partner.traits) {
|
||||||
|
const charEntry = gift.charactersAffects.find(ca => ca.trait_id === trait.id);
|
||||||
|
if (charEntry && charEntry.suitability > highestTraitValue) {
|
||||||
|
highestTraitValue = charEntry.suitability;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Durchschnitt, gerundet
|
||||||
|
return Math.round((moodValue + highestTraitValue) / 2);
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,8 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ $t('falukant.overview.metadata.nobleTitle') }}</td>
|
<td>{{ $t('falukant.overview.metadata.nobleTitle') }}</td>
|
||||||
<td>{{ $t('falukant.titles.' + falukantUser?.character.gender + '.' + falukantUser?.character.nobleTitle.labelTr) }}</td>
|
<td>{{ $t('falukant.titles.' + falukantUser?.character.gender + '.' +
|
||||||
|
falukantUser?.character.nobleTitle.labelTr) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ $t('falukant.overview.metadata.money') }}</td>
|
<td>{{ $t('falukant.overview.metadata.money') }}</td>
|
||||||
@@ -170,17 +171,24 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
getHouseStyle() {
|
getHouseStyle() {
|
||||||
if (!this.falukantUser) return {};
|
console.log(this.falukantUser);
|
||||||
|
if (!this.falukantUser || !this.falukantUser.userHouse?.houseType) return {};
|
||||||
const imageUrl = '/images/falukant/houses.png';
|
const imageUrl = '/images/falukant/houses.png';
|
||||||
const housePosition = this.falukantUser.house ? this.falukantUser.house.type.position : 0;
|
const pos = this.falukantUser.userHouse.houseType.position;
|
||||||
const x = housePosition % 3;
|
const index = pos - 1;
|
||||||
const y = Math.floor(housePosition / 3);
|
const columns = 3;
|
||||||
|
const spriteSize = 300;
|
||||||
|
const x = (index % columns) * spriteSize;
|
||||||
|
const y = Math.floor(index / columns) * spriteSize;
|
||||||
return {
|
return {
|
||||||
backgroundImage: `url(${imageUrl})`,
|
backgroundImage: `url(${imageUrl})`,
|
||||||
backgroundPosition: `-${x * 341}px -${y * 341}px`,
|
backgroundPosition: `-${x}px -${y}px`,
|
||||||
backgroundSize: "341px 341px",
|
backgroundSize: `${columns * spriteSize}px auto`,
|
||||||
width: "114px",
|
width: `300px`,
|
||||||
height: "114px",
|
height: `300px`,
|
||||||
|
border: '1px solid #ccc',
|
||||||
|
borderRadius: '4px',
|
||||||
|
imageRendering: 'crisp-edges',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getAgeColor(age) {
|
getAgeColor(age) {
|
||||||
@@ -321,12 +329,10 @@ export default {
|
|||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: cover;
|
|
||||||
image-rendering: crisp-edges;
|
image-rendering: crisp-edges;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
405
frontend/src/views/falukant/PoliticsView.vue
Normal file
405
frontend/src/views/falukant/PoliticsView.vue
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
<template>
|
||||||
|
<div class="politics-view">
|
||||||
|
<StatusBar />
|
||||||
|
|
||||||
|
<h2>{{ $t('falukant.politics.title') }}</h2>
|
||||||
|
|
||||||
|
<SimpleTabs v-model="activeTab" :tabs="tabs" @change="onTabChange" />
|
||||||
|
|
||||||
|
<!-- Tab‐Inhalt -->
|
||||||
|
<div class="tab-content">
|
||||||
|
<!-- Aktuelle Positionen -->
|
||||||
|
<div v-if="activeTab === 'current'" class="tab-pane">
|
||||||
|
<div v-if="loading.current" class="loading">{{ $t('loading') }}</div>
|
||||||
|
<div v-else class="table-scroll">
|
||||||
|
<table class="politics-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ $t('falukant.politics.current.office') }}</th>
|
||||||
|
<th>{{ $t('falukant.politics.current.region') }}</th>
|
||||||
|
<th>{{ $t('falukant.politics.current.holder') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="pos in currentPositions" :key="pos.id">
|
||||||
|
<td>{{ $t(`falukant.politics.offices.${pos.officeType.name}`) }}</td>
|
||||||
|
<td>{{ pos.region.name }}</td>
|
||||||
|
<td>
|
||||||
|
<span v-if="pos.character">
|
||||||
|
{{ pos.character.definedFirstName.name }}
|
||||||
|
{{ pos.character.definedLastName.name }}
|
||||||
|
</span>
|
||||||
|
<span v-else>—</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="!currentPositions.length">
|
||||||
|
<td colspan="3">{{ $t('falukant.politics.current.none') }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- OPEN Tab: hier zeigen wir 'openPolitics' -->
|
||||||
|
<div v-else-if="activeTab === 'openPolitics'" class="tab-pane">
|
||||||
|
<div v-if="loading.openPolitics" class="loading">{{ $t('loading') }}</div>
|
||||||
|
<div v-else class="table-scroll">
|
||||||
|
<table class="politics-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ $t('falukant.politics.open.office') }}</th>
|
||||||
|
<th>{{ $t('falukant.politics.open.region') }}</th>
|
||||||
|
<th>{{ $t('falukant.politics.open.date') }}</th>
|
||||||
|
<th>{{ $t('falukant.politics.open.candidacy') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="e in openPolitics" :key="e.id">
|
||||||
|
<td>{{ $t(`falukant.politics.offices.${e.officeType.name}`) }}</td>
|
||||||
|
<td>{{ e.region.name }}</td>
|
||||||
|
<td>{{ formatDate(e.date) }}</td>
|
||||||
|
<!-- Checkbox ganz am Ende -->
|
||||||
|
<td>
|
||||||
|
<input type="checkbox" :id="`apply-${e.id}`" v-model="selectedApplications"
|
||||||
|
:value="e.id" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="!openPolitics.length">
|
||||||
|
<td colspan="4">{{ $t('falukant.politics.open.none') }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="apply-button">
|
||||||
|
<button :disabled="!selectedApplications.length" @click="submitApplications">
|
||||||
|
{{ $t('falukant.politics.open.apply') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Wahlen -->
|
||||||
|
<div v-else-if="activeTab === 'elections'" class="tab-pane">
|
||||||
|
<div v-if="loading.elections" class="loading">{{ $t('loading') }}</div>
|
||||||
|
<div v-else class="table-scroll">
|
||||||
|
<table class="politics-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ $t('falukant.politics.elections.office') }}</th>
|
||||||
|
<th>{{ $t('falukant.politics.elections.region') }}</th>
|
||||||
|
<th>{{ $t('falukant.politics.elections.date') }}</th>
|
||||||
|
<th>{{ $t('falukant.politics.elections.posts') }}</th>
|
||||||
|
<th>{{ $t('falukant.politics.elections.candidates') }}</th>
|
||||||
|
<th>{{ $t('falukant.politics.elections.action') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="e in elections" :key="e.id">
|
||||||
|
<td>{{ $t(`falukant.politics.offices.${e.officeType.name}`) }}</td>
|
||||||
|
<td>{{ e.region.name }}</td>
|
||||||
|
<td>{{ formatDate(e.date) }}</td>
|
||||||
|
<td>{{ e.postsToFill }}</td>
|
||||||
|
<td v-if="!e.voted">
|
||||||
|
<Multiselect v-model="selectedCandidates[e.id]" :options="e.candidates" multiple
|
||||||
|
:max="e.postsToFill" :close-on-select="false" :clear-on-select="false"
|
||||||
|
track-by="id" label="name" :custom-label="candidateLabel" placeholder="">
|
||||||
|
<template #option="{ option }">
|
||||||
|
{{ $t(`falukant.titles.${option.gender}.${option.title}`) }}
|
||||||
|
{{ option.name }} ({{ option.age }})
|
||||||
|
</template>
|
||||||
|
<template #selected="{ option }">
|
||||||
|
{{ $t(`falukant.titles.${option.gender}.${option.title}`) }}
|
||||||
|
{{ option.name }}
|
||||||
|
</template>
|
||||||
|
</Multiselect>
|
||||||
|
</td>
|
||||||
|
<td v-else>
|
||||||
|
<ul class="voted-list">
|
||||||
|
<li v-for="cid in e.votedFor" :key="cid">
|
||||||
|
<span v-if="findCandidateById(e, cid)">
|
||||||
|
{{ formatCandidateTitle(findCandidateById(e, cid)) }}
|
||||||
|
{{ findCandidateById(e, cid).name }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li v-if="!e.votedFor || !e.votedFor.length">—</li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<button v-if="!e.voted"
|
||||||
|
:disabled="!selectedCandidates[e.id] || !selectedCandidates[e.id].length"
|
||||||
|
@click="submitVote(e.id)">
|
||||||
|
{{ $t('falukant.politics.elections.vote') }}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr v-if="!elections.length">
|
||||||
|
<td colspan="6">{{ $t('falukant.politics.elections.none') }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="all-vote-button" v-if="hasAnyUnvoted">
|
||||||
|
<button :disabled="!hasAnySelection" @click="submitAllVotes">
|
||||||
|
{{ $t('falukant.politics.elections.voteAll') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import StatusBar from '@/components/falukant/StatusBar.vue';
|
||||||
|
import SimpleTabs from '@/components/SimpleTabs.vue';
|
||||||
|
import Multiselect from 'vue-multiselect';
|
||||||
|
import apiClient from '@/utils/axios.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PoliticsView',
|
||||||
|
components: { StatusBar, SimpleTabs, Multiselect },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
activeTab: 'current',
|
||||||
|
tabs: [
|
||||||
|
{ value: 'current', label: 'falukant.politics.tabs.current' },
|
||||||
|
{ value: 'openPolitics', label: 'falukant.politics.tabs.upcoming' },
|
||||||
|
{ value: 'elections', label: 'falukant.politics.tabs.elections' }
|
||||||
|
],
|
||||||
|
currentPositions: [],
|
||||||
|
openPolitics: [],
|
||||||
|
elections: [],
|
||||||
|
selectedCandidates: {},
|
||||||
|
selectedApplications: [],
|
||||||
|
loading: {
|
||||||
|
current: false,
|
||||||
|
openPolitics: false,
|
||||||
|
elections: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
hasAnySelection() {
|
||||||
|
return Object.values(this.selectedCandidates)
|
||||||
|
.some(arr => Array.isArray(arr) && arr.length > 0);
|
||||||
|
},
|
||||||
|
hasAnyUnvoted() {
|
||||||
|
return this.elections.some(e => !e.voted);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadCurrentPositions();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onTabChange(tab) {
|
||||||
|
if (tab === 'current' && !this.currentPositions.length) {
|
||||||
|
this.loadCurrentPositions();
|
||||||
|
}
|
||||||
|
if (tab === 'openPolitics' && !this.openPolitics.length) {
|
||||||
|
this.loadOpenPolitics();
|
||||||
|
}
|
||||||
|
if (tab === 'elections' && !this.elections.length) {
|
||||||
|
this.loadElections();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadCurrentPositions() {
|
||||||
|
this.loading.current = true;
|
||||||
|
try {
|
||||||
|
const { data } = await apiClient.get('/api/falukant/politics/overview');
|
||||||
|
this.currentPositions = data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading current positions', err);
|
||||||
|
} finally {
|
||||||
|
this.loading.current = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadOpenPolitics() {
|
||||||
|
this.loading.openPolitics = true;
|
||||||
|
try {
|
||||||
|
const { data } = await apiClient.get('/api/falukant/politics/open');
|
||||||
|
this.openPolitics = data;
|
||||||
|
this.selectedApplications = [];
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading open politics', err);
|
||||||
|
} finally {
|
||||||
|
this.loading.openPolitics = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadElections() {
|
||||||
|
this.loading.elections = true;
|
||||||
|
try {
|
||||||
|
const { data } = await apiClient.get('/api/falukant/politics/elections');
|
||||||
|
this.elections = data;
|
||||||
|
|
||||||
|
data.forEach(e => {
|
||||||
|
this.selectedCandidates[e.id] = [];
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading elections', err);
|
||||||
|
} finally {
|
||||||
|
this.loading.elections = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
candidateLabel(option) {
|
||||||
|
const title = this.$t(`falukant.titles.${option.gender}.${option.title}`);
|
||||||
|
return `${title} ${option.name} (${option.age})`;
|
||||||
|
},
|
||||||
|
|
||||||
|
findCandidateById(election, candidateId) {
|
||||||
|
return election.candidates.find(c => c.id === candidateId) || {};
|
||||||
|
},
|
||||||
|
|
||||||
|
formatCandidateTitle(candidate) {
|
||||||
|
if (!candidate) return '';
|
||||||
|
return this.$t(`falukant.titles.${candidate.gender}.${candidate.title}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async submitVote(electionId) {
|
||||||
|
const singlePayload = [
|
||||||
|
{
|
||||||
|
electionId: electionId,
|
||||||
|
candidateIds: this.selectedCandidates[electionId].map(c => c.id)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await apiClient.post(
|
||||||
|
'/api/falukant/politics/elections',
|
||||||
|
{ votes: singlePayload }
|
||||||
|
);
|
||||||
|
await this.loadElections();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error submitting vote for election ${electionId}`, err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async submitAllVotes() {
|
||||||
|
const payload = Object.entries(this.selectedCandidates)
|
||||||
|
.filter(([eid, arr]) => Array.isArray(arr) && arr.length > 0)
|
||||||
|
.map(([eid, arr]) => ({
|
||||||
|
electionId: parseInt(eid, 10),
|
||||||
|
candidateIds: arr.map(c => c.id)
|
||||||
|
}));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await apiClient.post(
|
||||||
|
'/api/falukant/politics/elections',
|
||||||
|
{ votes: payload }
|
||||||
|
);
|
||||||
|
await this.loadElections();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error submitting all votes', err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
formatDate(ts) {
|
||||||
|
return new Date(ts).toLocaleDateString(this.$i18n.locale, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async submitApplications() {
|
||||||
|
try {
|
||||||
|
await apiClient.post(
|
||||||
|
'/api/falukant/politics/open',
|
||||||
|
{ electionIds: this.selectedApplications }
|
||||||
|
);
|
||||||
|
await this.loadOpenPolitics();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error submitting applications', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.politics-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px 0 0 0;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.simple-tabs {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-pane {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-scroll {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.politics-table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: auto;
|
||||||
|
/* kein 100% */
|
||||||
|
}
|
||||||
|
|
||||||
|
.politics-table thead th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background: #FFF;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.politics-table tbody td {
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
font-style: italic;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voted-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.all-vote-button {
|
||||||
|
padding: 10px 0;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.all-vote-button button {
|
||||||
|
padding: 6px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 2em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,89 +2,84 @@
|
|||||||
<h2 class="link" @click="openForum()">{{ $t('socialnetwork.forum.title') }} {{ forumName }}</h2>
|
<h2 class="link" @click="openForum()">{{ $t('socialnetwork.forum.title') }} {{ forumName }}</h2>
|
||||||
<h3 v-if="forumTopic">{{ forumTopic }}</h3>
|
<h3 v-if="forumTopic">{{ forumTopic }}</h3>
|
||||||
<ul class="messages">
|
<ul class="messages">
|
||||||
<li v-for="message in messages">
|
<li v-for="message in messages" :key="message.id">
|
||||||
<div v-html="message.text"></div>
|
<div v-html="message.text"></div>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<span class="link" @click="openProfile(message.lastMessageUser.hashedId)">{{
|
<span class="link" @click="openProfile(message.lastMessageUser.hashedId)">
|
||||||
message.lastMessageUser.username }}</span>
|
{{ message.lastMessageUser.username }}
|
||||||
|
</span>
|
||||||
<span>{{ new Date(message.createdAt).toLocaleString() }}</span>
|
<span>{{ new Date(message.createdAt).toLocaleString() }}</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<editor v-model="newContent" :init="tinymceInitOptions" :api-key="apiKey"
|
|
||||||
tinymce-script-src="/tinymce/tinymce.min.js"></editor>
|
<div class="editor-container">
|
||||||
|
<EditorContent :editor="editor" class="editor" />
|
||||||
|
</div>
|
||||||
<button @click="saveNewMessage">{{ $t('socialnetwork.forum.createNewMesssage') }}</button>
|
<button @click="saveNewMessage">{{ $t('socialnetwork.forum.createNewMesssage') }}</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import apiClient from '../../utils/axios';
|
import { Editor, EditorContent } from '@tiptap/vue-3'
|
||||||
import TinyMCEEditor from '@tinymce/tinymce-vue';
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
|
import apiClient from '../../utils/axios'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ForumTopicView',
|
name: 'ForumTopicView',
|
||||||
components: {
|
components: {
|
||||||
editor: TinyMCEEditor,
|
EditorContent,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
forumTopicId: String,
|
forumTopicId: '',
|
||||||
forumTopic: null,
|
forumTopic: null,
|
||||||
forumName: null,
|
forumName: null,
|
||||||
forumId: 0,
|
forumId: 0,
|
||||||
newContent: '',
|
messages: [],
|
||||||
apiKey: import.meta.env.VITE_TINYMCE_API_KEY ?? '',
|
editor: null,
|
||||||
tinymceInitOptions: {
|
|
||||||
script_url: '/tinymce/tinymce.min.js',
|
|
||||||
height: 300,
|
|
||||||
menubar: true,
|
|
||||||
plugins: [
|
|
||||||
'lists', 'link',
|
|
||||||
'searchreplace', 'visualblocks', 'code',
|
|
||||||
'insertdatetime', 'table'
|
|
||||||
],
|
|
||||||
toolbar:
|
|
||||||
'undo redo cut copy paste | bold italic forecolor backcolor fontfamily fontsize| \
|
|
||||||
alignleft aligncenter alignright alignjustify | \
|
|
||||||
bullist numlist outdent indent | removeformat | link visualblocks code',
|
|
||||||
contextmenu: 'link image table',
|
|
||||||
menubar: 'edit format table',
|
|
||||||
promotion: false,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
mounted() {
|
||||||
this.forumTopicId = this.$route.params.id;
|
this.forumTopicId = this.$route.params.id;
|
||||||
this.loadForumTopic();
|
this.loadForumTopic();
|
||||||
|
|
||||||
|
this.editor = new Editor({
|
||||||
|
extensions: [StarterKit],
|
||||||
|
content: '',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
if (this.editor) {
|
||||||
|
this.editor.destroy();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async loadForumTopic() {
|
async loadForumTopic() {
|
||||||
try {
|
try {
|
||||||
console.log(this.forumTopicId);
|
const response = await apiClient.get(`/api/forum/topic/${this.forumTopicId}`);
|
||||||
const url = `/api/forum/topic/${this.forumTopicId}`;
|
|
||||||
console.log('url', url);
|
|
||||||
const response = await apiClient.get(url);
|
|
||||||
this.setContent(response.data);
|
this.setContent(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setContent(responseData) {
|
setContent(data) {
|
||||||
this.forumTopic = responseData.title;
|
this.forumTopic = data.title;
|
||||||
this.forumName = responseData.forum.name;
|
this.forumName = data.forum.name;
|
||||||
this.forumId = responseData.forum.id;
|
this.forumId = data.forum.id;
|
||||||
this.messages = responseData.messages;
|
this.messages = data.messages;
|
||||||
},
|
},
|
||||||
async openProfile(id) {
|
async openProfile(id) {
|
||||||
this.$root.$refs.userProfileDialog.userId = id;
|
this.$root.$refs.userProfileDialog.userId = id;
|
||||||
this.$root.$refs.userProfileDialog.open();
|
this.$root.$refs.userProfileDialog.open();
|
||||||
},
|
},
|
||||||
async saveNewMessage() {
|
async saveNewMessage() {
|
||||||
|
const content = this.editor ? this.editor.getHTML() : '';
|
||||||
|
if (!content.trim()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = `/api/forum/topic/${this.forumTopicId}/message`;
|
const url = `/api/forum/topic/${this.forumTopicId}/message`;
|
||||||
const response = await apiClient.post(url, {
|
const response = await apiClient.post(url, { content });
|
||||||
content: this.newContent,
|
this.editor.commands.clearContent();
|
||||||
});
|
|
||||||
this.newContent = '';
|
|
||||||
this.setContent(response.data);
|
this.setContent(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -96,7 +91,6 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.messages {
|
.messages {
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
@@ -124,4 +118,17 @@ export default {
|
|||||||
.messages > li > .footer > span:last-child {
|
.messages > li > .footer > span:last-child {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editor-container {
|
||||||
|
margin-top: 1rem;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 10px;
|
||||||
|
min-height: 200px;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor {
|
||||||
|
min-height: 150px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -11,71 +11,27 @@
|
|||||||
<input type="text" v-model="newTitle" />
|
<input type="text" v-model="newTitle" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<editor v-model="newContent" :init="tinymceInitOptions" :api-key="apiKey"
|
<div class="editor-container">
|
||||||
tinymce-script-src="/tinymce/tinymce.min.js"></editor>
|
<EditorContent :editor="editor" class="editor" />
|
||||||
|
</div>
|
||||||
<button @click="saveNewTopic">{{ $t('socialnetwork.forum.createNewTopic') }}</button>
|
<button @click="saveNewTopic">{{ $t('socialnetwork.forum.createNewTopic') }}</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="titles.length > 0">
|
<div v-else-if="titles.length > 0">
|
||||||
<div class="pagination">
|
<!-- PAGINATION + TABLE bleibt unverändert -->
|
||||||
<button @click="goToPage(1)" v-if="page != 1">« {{ $t('socialnetwork.forum.pagination.first')
|
<!-- ... -->
|
||||||
}}</button>
|
|
||||||
<button @click="goToPage(page - 1)" v-if="page != 1">‹ {{
|
|
||||||
$t('socialnetwork.forum.pagination.previous') }}</button>
|
|
||||||
<span>{{ $t('socialnetwork.forum.pagination.page').replace("<<page>>", page).replace("<<of>>", totalPages)
|
|
||||||
}}</span>
|
|
||||||
<button @click="goToPage(page + 1)" v-if="page != totalPages">{{ $t('socialnetwork.forum.pagination.next')
|
|
||||||
}}
|
|
||||||
›</button>
|
|
||||||
<button @click="goToPage(totalPages)" v-if="page != totalPages">{{ $t('socialnetwork.forum.pagination.last')
|
|
||||||
}}
|
|
||||||
»</button>
|
|
||||||
</div>
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>{{ $t('socialnetwork.forum.topic') }}</th>
|
|
||||||
<th>{{ $t('socialnetwork.forum.createdBy') }}</th>
|
|
||||||
<th>{{ $t('socialnetwork.forum.createdAt') }}</th>
|
|
||||||
<th>{{ $t('socialnetwork.forum.reactions') }}</th>
|
|
||||||
<th>{{ $t('socialnetwork.forum.lastReaction') }}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="title in titles">
|
|
||||||
<td><span class="link" @click="openTopic(title.id)">{{ title.title }}</span></td>
|
|
||||||
<td><span class="link" @click="openProfile(title.createdByHash)">{{ title.createdBy }}</span></td>
|
|
||||||
<td>{{ new Date(title.createdAt).toLocaleString() }}</td>
|
|
||||||
<td>{{ title.numberOfItems }}</td>
|
|
||||||
<td>{{ new Date(title.lastMessageDate).toLocaleString() }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div class="pagination">
|
|
||||||
<button @click="goToPage(1)" v-if="page != 1">« {{ $t('socialnetwork.forum.pagination.first')
|
|
||||||
}}</button>
|
|
||||||
<button @click="goToPage(page - 1)" v-if="page != 1">‹ {{
|
|
||||||
$t('socialnetwork.forum.pagination.previous') }}</button>
|
|
||||||
<span>{{ $t('socialnetwork.forum.pagination.page').replace("<<page>>", page).replace("<<of>>", totalPages)
|
|
||||||
}}</span>
|
|
||||||
<button @click="goToPage(page + 1)" v-if="page != totalPages">{{ $t('socialnetwork.forum.pagination.next')
|
|
||||||
}}
|
|
||||||
›</button>
|
|
||||||
<button @click="goToPage(totalPages)" v-if="page != totalPages">{{ $t('socialnetwork.forum.pagination.last')
|
|
||||||
}}
|
|
||||||
»</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else>{{ $t('socialnetwork.forum.noTitles') }}</div>
|
<div v-else>{{ $t('socialnetwork.forum.noTitles') }}</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import apiClient from '../../utils/axios';
|
import { Editor, EditorContent } from '@tiptap/vue-3'
|
||||||
import TinyMCEEditor from '@tinymce/tinymce-vue';
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
|
import apiClient from '../../utils/axios'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ForumView',
|
name: 'ForumView',
|
||||||
components: {
|
components: {
|
||||||
editor: TinyMCEEditor,
|
EditorContent,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
totalPages() {
|
totalPages() {
|
||||||
@@ -91,30 +47,20 @@ export default {
|
|||||||
titles: [],
|
titles: [],
|
||||||
inCreation: false,
|
inCreation: false,
|
||||||
newTitle: '',
|
newTitle: '',
|
||||||
newContent: '',
|
editor: null,
|
||||||
apiKey: import.meta.env.VITE_TINYMCE_API_KEY ?? '',
|
|
||||||
tinymceInitOptions: {
|
|
||||||
script_url: '/tinymce/tinymce.min.js',
|
|
||||||
height: 300,
|
|
||||||
menubar: true,
|
|
||||||
plugins: [
|
|
||||||
'lists', 'link',
|
|
||||||
'searchreplace', 'visualblocks', 'code',
|
|
||||||
'insertdatetime', 'table'
|
|
||||||
],
|
|
||||||
toolbar:
|
|
||||||
'undo redo cut copy paste | bold italic forecolor backcolor fontfamily fontsize| \
|
|
||||||
alignleft aligncenter alignright alignjustify | \
|
|
||||||
bullist numlist outdent indent | removeformat | link visualblocks code',
|
|
||||||
contextmenu: 'link image table',
|
|
||||||
menubar: 'edit format table',
|
|
||||||
promotion: false,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
this.forumId = this.$route.params.id;
|
this.forumId = this.$route.params.id;
|
||||||
await this.loadForum();
|
await this.loadForum();
|
||||||
|
|
||||||
|
this.editor = new Editor({
|
||||||
|
extensions: [StarterKit],
|
||||||
|
content: '',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
if (this.editor) this.editor.destroy();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async loadForum() {
|
async loadForum() {
|
||||||
@@ -123,12 +69,16 @@ export default {
|
|||||||
},
|
},
|
||||||
createNewTopic() {
|
createNewTopic() {
|
||||||
this.inCreation = !this.inCreation;
|
this.inCreation = !this.inCreation;
|
||||||
|
if (this.inCreation && this.editor) {
|
||||||
|
this.editor.commands.setContent('');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async saveNewTopic() {
|
async saveNewTopic() {
|
||||||
|
const content = this.editor ? this.editor.getHTML() : '';
|
||||||
const response = await apiClient.post('/api/forum/topic', {
|
const response = await apiClient.post('/api/forum/topic', {
|
||||||
forumId: this.forumId,
|
forumId: this.forumId,
|
||||||
title: this.newTitle,
|
title: this.newTitle,
|
||||||
content: this.newEntryContent
|
content,
|
||||||
});
|
});
|
||||||
this.setData(response.data);
|
this.setData(response.data);
|
||||||
this.inCreation = false;
|
this.inCreation = false;
|
||||||
@@ -171,6 +121,19 @@ export default {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editor-container {
|
||||||
|
margin: 1em 0;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 10px;
|
||||||
|
min-height: 200px;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor {
|
||||||
|
min-height: 150px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
.pagination {
|
.pagination {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user