Revert "Refactor DirectorInfo and SaleSection components to unify speedLabel logic and remove unnecessary watch properties"

This reverts commit 8c40144734.
This commit is contained in:
Torsten Schulz (local)
2026-02-09 15:56:48 +01:00
parent 9c91d99bed
commit a7688e4ed5
12 changed files with 2245 additions and 515 deletions

View File

@@ -58,6 +58,10 @@ class FalukantController {
if (!page) page = 1;
return this.service.moneyHistory(userId, page, filter);
});
this.moneyHistoryGraph = this._wrapWithUser((userId, req) => {
const { range } = req.body || {};
return this.service.moneyHistoryGraph(userId, range || '24h');
});
this.getStorage = this._wrapWithUser((userId, req) => this.service.getStorage(userId, req.params.branchId));
this.buyStorage = this._wrapWithUser((userId, req) => {
const { branchId, amount, stockTypeId } = req.body;
@@ -123,6 +127,9 @@ class FalukantController {
});
this.getTitlesOfNobility = this._wrapWithUser((userId) => this.service.getTitlesOfNobility(userId));
this.getReputationActions = this._wrapWithUser((userId) => this.service.getReputationActions(userId));
this.executeReputationAction = this._wrapWithUser((userId, req) =>
this.service.executeReputationAction(userId, req.body?.actionTypeId), { successStatus: 201 });
this.getHouseTypes = this._wrapWithUser((userId) => this.service.getHouseTypes(userId));
this.getMoodAffect = this._wrapWithUser((userId) => this.service.getMoodAffect(userId));
this.getCharacterAffect = this._wrapWithUser((userId) => this.service.getCharacterAffect(userId));
@@ -142,6 +149,17 @@ class FalukantController {
const { characterId: childId, firstName } = req.body;
return this.service.baptise(userId, childId, firstName);
});
this.getChurchOverview = this._wrapWithUser((userId) => this.service.getChurchOverview(userId));
this.getAvailableChurchPositions = this._wrapWithUser((userId) => this.service.getAvailableChurchPositions(userId));
this.getSupervisedApplications = this._wrapWithUser((userId) => this.service.getSupervisedApplications(userId));
this.applyForChurchPosition = this._wrapWithUser((userId, req) => {
const { officeTypeId, regionId } = req.body;
return this.service.applyForChurchPosition(userId, officeTypeId, regionId);
});
this.decideOnChurchApplication = this._wrapWithUser((userId, req) => {
const { applicationId, decision } = req.body;
return this.service.decideOnChurchApplication(userId, applicationId, decision);
});
this.getEducation = this._wrapWithUser((userId) => this.service.getEducation(userId));
this.sendToSchool = this._wrapWithUser((userId, req) => {
@@ -154,25 +172,20 @@ class FalukantController {
this.takeBankCredits = this._wrapWithUser((userId, req) => this.service.takeBankCredits(userId, req.body.height));
this.getNobility = this._wrapWithUser((userId) => this.service.getNobility(userId));
this.advanceNobility = this._wrapWithUser(async (userId) => {
this.advanceNobility = this._wrapWithUser((userId) => this.service.advanceNobility(userId));
this.getHealth = this._wrapWithUser((userId) => this.service.getHealth(userId));
this.healthActivity = this._wrapWithUser(async (userId, req) => {
try {
return await this.service.advanceNobility(userId);
return await this.service.healthActivity(userId, req.body.measureTr);
} catch (e) {
if (e && e.name === 'PreconditionError') {
if (e.message === 'nobilityTooSoon') {
throw { status: 412, message: 'nobilityTooSoon', retryAt: e.meta?.retryAt };
}
if (e.message === 'nobilityRequirements') {
throw { status: 412, message: 'nobilityRequirements', unmet: e.meta?.unmet || [] };
}
if (e && e.name === 'PreconditionError' && e.message === 'tooClose') {
throw { status: 412, message: 'tooClose', retryAt: e.meta?.retryAt };
}
throw e;
}
});
this.getHealth = this._wrapWithUser((userId) => this.service.getHealth(userId));
this.healthActivity = this._wrapWithUser((userId, req) => this.service.healthActivity(userId, req.body.measureTr));
this.getPoliticsOverview = this._wrapWithUser((userId) => this.service.getPoliticsOverview(userId));
this.getOpenPolitics = this._wrapWithUser((userId) => this.service.getOpenPolitics(userId));
this.getElections = this._wrapWithUser((userId) => this.service.getElections(userId));
@@ -189,6 +202,13 @@ class FalukantController {
}
return this.service.getProductPriceInRegion(userId, productId, regionId);
});
this.getAllProductPricesInRegion = this._wrapWithUser((userId, req) => {
const regionId = parseInt(req.query.regionId, 10);
if (Number.isNaN(regionId)) {
throw new Error('regionId is required');
}
return this.service.getAllProductPricesInRegion(userId, regionId);
});
this.getProductPricesInCities = this._wrapWithUser((userId, req) => {
const productId = parseInt(req.query.productId, 10);
const currentPrice = parseFloat(req.query.currentPrice);
@@ -198,6 +218,16 @@ class FalukantController {
}
return this.service.getProductPricesInCities(userId, productId, currentPrice, currentRegionId);
});
this.getProductPricesInCitiesBatch = this._wrapWithUser((userId, req) => {
const body = req.body || {};
const items = Array.isArray(body.items) ? body.items : [];
const currentRegionId = body.currentRegionId != null ? parseInt(body.currentRegionId, 10) : null;
const valid = items.map(i => ({
productId: parseInt(i.productId, 10),
currentPrice: parseFloat(i.currentPrice)
})).filter(i => !Number.isNaN(i.productId) && !Number.isNaN(i.currentPrice));
return this.service.getProductPricesInCitiesBatch(userId, valid, Number.isNaN(currentRegionId) ? null : currentRegionId);
});
this.renovate = this._wrapWithUser((userId, req) => this.service.renovate(userId, req.body.element));
this.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(userId));
@@ -205,6 +235,7 @@ class FalukantController {
this.getNotifications = this._wrapWithUser((userId) => this.service.getNotifications(userId));
this.getAllNotifications = this._wrapWithUser((userId, req) => this.service.getAllNotifications(userId, req.query.page, req.query.size));
this.markNotificationsShown = this._wrapWithUser((userId) => this.service.markNotificationsShown(userId), { successStatus: 202 });
this.getDashboardWidget = this._wrapWithUser((userId) => this.service.getDashboardWidget(userId));
this.getUndergroundTargets = this._wrapWithUser((userId) => this.service.getPoliticalOfficeHolders(userId));
this.searchUsers = this._wrapWithUser((userId, req) => {
@@ -279,7 +310,13 @@ class FalukantController {
} catch (error) {
console.error('Controller error:', error);
const status = error.status && typeof error.status === 'number' ? error.status : 500;
res.status(status).json({ error: error.message || 'Internal error' });
// Wenn error ein Objekt mit status ist, alle Felder außer status übernehmen
if (error && typeof error === 'object' && error.status && typeof error.status === 'number') {
const { status: errorStatus, ...errorData } = error;
res.status(errorStatus).json({ error: error.message || errorData.message || 'Internal error', ...errorData });
} else {
res.status(status).json({ error: error.message || 'Internal error' });
}
}
};
}

View File

@@ -15,17 +15,7 @@ ProductType.init({
allowNull: false},
sellCost: {
type: DataTypes.INTEGER,
allowNull: false}
,
sellCostMinNeutral: {
type: DataTypes.DECIMAL,
allowNull: true,
field: 'sell_cost_min_neutral'
},
sellCostMaxNeutral: {
type: DataTypes.DECIMAL,
allowNull: true,
field: 'sell_cost_max_neutral'
allowNull: false
}
}, {
sequelize,

View File

@@ -11,6 +11,7 @@ router.get('/character/affect', falukantController.getCharacterAffect);
router.get('/name/randomfirstname/:gender', falukantController.randomFirstName);
router.get('/name/randomlastname', falukantController.randomLastName);
router.get('/info', falukantController.getInfo);
router.get('/dashboard-widget', falukantController.getDashboardWidget);
router.get('/branches/types', falukantController.getBranchTypes);
router.get('/branches/:branch', falukantController.getBranch);
router.get('/branches', falukantController.getBranches);
@@ -28,6 +29,7 @@ router.get('/inventory/?:branchId', falukantController.getInventory);
router.post('/sell/all', falukantController.sellAllProducts);
router.post('/sell', falukantController.sellProduct);
router.post('/moneyhistory', falukantController.moneyHistory);
router.post('/moneyhistory/graph', falukantController.moneyHistoryGraph);
router.get('/storage/:branchId', falukantController.getStorage);
router.post('/storage', falukantController.buyStorage);
router.delete('/storage', falukantController.sellStorage);
@@ -50,6 +52,8 @@ router.get('/family/children', falukantController.getChildren);
router.post('/family/gift', falukantController.sendGift);
router.get('/family', falukantController.getFamily);
router.get('/nobility/titels', falukantController.getTitlesOfNobility);
router.get('/reputation/actions', falukantController.getReputationActions);
router.post('/reputation/actions', falukantController.executeReputationAction);
router.get('/houses/types', falukantController.getHouseTypes);
router.get('/houses/buyable', falukantController.getBuyableHouses);
router.get('/houses', falukantController.getUserHouse);
@@ -61,6 +65,11 @@ router.post('/party', falukantController.createParty);
router.get('/party', falukantController.getParties);
router.get('/family/notbaptised', falukantController.getNotBaptisedChildren);
router.post('/church/baptise', falukantController.baptise);
router.get('/church/overview', falukantController.getChurchOverview);
router.get('/church/positions/available', falukantController.getAvailableChurchPositions);
router.get('/church/applications/supervised', falukantController.getSupervisedApplications);
router.post('/church/positions/apply', falukantController.applyForChurchPosition);
router.post('/church/applications/decide', falukantController.decideOnChurchApplication);
router.get('/education', falukantController.getEducation);
router.post('/education', falukantController.sendToSchool);
router.get('/bank/overview', falukantController.getBankOverview);
@@ -72,13 +81,14 @@ router.get('/health', falukantController.getHealth);
router.post('/health', falukantController.healthActivity);
router.get('/politics/overview', falukantController.getPoliticsOverview);
router.get('/politics/open', falukantController.getOpenPolitics);
router.post('/politics/open', falukantController.applyForElections);
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);
router.get('/products/price-in-region', falukantController.getProductPriceInRegion);
router.get('/products/prices-in-region', falukantController.getAllProductPricesInRegion);
router.get('/products/prices-in-cities', falukantController.getProductPricesInCities);
router.post('/products/prices-in-cities-batch', falukantController.getProductPricesInCitiesBatch);
router.get('/branches/:branchId/taxes', falukantController.getBranchTaxes);
router.get('/vehicles/types', falukantController.getVehicleTypes);
router.post('/vehicles', falukantController.buyVehicles);

File diff suppressed because it is too large Load Diff

View File

@@ -210,6 +210,14 @@ export default {
},
};
},
watch: {
branchId: {
immediate: false,
handler() {
this.loadDirector();
},
},
},
async mounted() {
await this.loadDirector();
},
@@ -256,11 +264,17 @@ export default {
},
speedLabel(value) {
const key = value == null ? 'unknown' : String(value);
if (value == null) return this.$t('falukant.branch.transport.speed.unknown') || '—';
if (typeof value === 'object') {
const k = value.tr ?? value.id ?? 'unknown';
const tKey = `falukant.branch.transport.speed.${k}`;
const t = this.$t(tKey);
return (t && t !== tKey) ? t : String(k);
}
const key = String(value);
const tKey = `falukant.branch.transport.speed.${key}`;
const translated = this.$t(tKey);
if (!translated || translated === tKey) return value;
return translated;
return (!translated || translated === tKey) ? key : translated;
},
openNewDirectorDialog() {

View File

@@ -251,13 +251,6 @@
return new Date(a.eta).getTime() - new Date(b.eta).getTime();
});
},
speedLabel(value) {
const key = value == null ? 'unknown' : String(value);
const tKey = `falukant.branch.transport.speed.${key}`;
const translated = this.$t(tKey);
if (!translated || translated === tKey) return value;
return translated;
},
},
async mounted() {
await this.loadInventory();
@@ -274,6 +267,19 @@
}
},
methods: {
speedLabel(value) {
if (value == null) return this.$t('falukant.branch.transport.speed.unknown') || '—';
if (typeof value === 'object') {
const k = value.tr ?? value.id ?? 'unknown';
const tKey = `falukant.branch.transport.speed.${k}`;
const t = this.$t(tKey);
return (t && t !== tKey) ? t : String(k);
}
const key = String(value);
const tKey = `falukant.branch.transport.speed.${key}`;
const translated = this.$t(tKey);
return (!translated || translated === tKey) ? key : translated;
},
async loadInventory() {
try {
const response = await apiClient.get(`/api/falukant/inventory/${this.branchId}`);
@@ -287,25 +293,24 @@
}
},
async loadPricesForInventory() {
for (const item of this.inventory) {
const itemKey = `${item.region.id}-${item.product.id}-${item.quality}`;
if (this.loadingPrices.has(itemKey)) continue;
this.loadingPrices.add(itemKey);
try {
// Aktueller Preis basierend auf sellCost
const currentPrice = item.product.sellCost || 0;
const { data } = await apiClient.get('/api/falukant/products/prices-in-cities', {
params: {
productId: item.product.id,
currentPrice: currentPrice
}
});
this.$set(item, 'betterPrices', data || []);
} catch (error) {
console.error(`Error loading prices for item ${itemKey}:`, error);
this.$set(item, 'betterPrices', []);
} finally {
this.loadingPrices.delete(itemKey);
if (this.inventory.length === 0) return;
const currentRegionId = this.inventory[0]?.region?.id ?? null;
const items = this.inventory.map(item => ({
productId: item.product.id,
currentPrice: item.product.sellCost || 0
}));
try {
const { data } = await apiClient.post('/api/falukant/products/prices-in-cities-batch', {
currentRegionId,
items
});
for (const item of this.inventory) {
item.betterPrices = data && data[item.product.id] ? data[item.product.id] : [];
}
} catch (error) {
console.error('Error loading prices for inventory:', error);
for (const item of this.inventory) {
item.betterPrices = [];
}
}
},

View File

@@ -114,12 +114,21 @@
},
"overview": {
"title": "Falukant - Übersicht",
"heirSelection": {
"title": "Erben-Auswahl",
"description": "Dein bisheriger Charakter ist nicht mehr verfügbar. Wähle einen Erben aus der Liste, um mit diesem weiterzuspielen.",
"loading": "Lade mögliche Erben…",
"noHeirs": "Keine Erben verfügbar.",
"select": "Als Spielcharakter wählen",
"error": "Fehler beim Auswählen des Erben."
},
"metadata": {
"title": "Persönliches",
"name": "Name",
"money": "Vermögen",
"age": "Alter",
"years": "Jahre",
"days": "Tage",
"mainbranch": "Heimatstadt",
"nobleTitle": "Stand"
},
@@ -138,32 +147,6 @@
}
}
},
"genderAge": {
"ageGroups": "infant:2|toddler:4|child:12|teen:18|youngAdult:25|adult:50|mature:70|elder:999",
"neutral": {
"child": "Kind"
},
"male": {
"infant": "Säugling",
"toddler": "Bübchen",
"child": "Knabe",
"teen": "Jüngling",
"youngAdult": "Junker",
"adult": "Mann",
"mature": "Herr",
"elder": "Greis"
},
"female": {
"infant": "Säugling",
"toddler": "Mädel",
"child": "Göre",
"teen": "Dirn",
"youngAdult": "Jungfrau",
"adult": "Frau",
"mature": "Dame",
"elder": "Greisin"
}
},
"titles": {
"male": {
"noncivil": "Leibeigener",
@@ -334,6 +317,9 @@
"current": "Laufende Produktionen",
"product": "Produkt",
"remainingTime": "Verbleibende Zeit (Sekunden)",
"status": "Status",
"sleep": "Pausiert",
"active": "Aktiv",
"noProductions": "Keine laufenden Produktionen."
},
"columns": {
@@ -605,6 +591,23 @@
"time": "Zeit",
"prev": "Zurück",
"next": "Weiter",
"graph": {
"open": "Verlauf anzeigen",
"title": "Geldentwicklung",
"close": "Schließen",
"loading": "Lade Verlauf...",
"noData": "Für den gewählten Zeitraum liegen keine Buchungen vor.",
"yesterday": "Gestern",
"range": {
"label": "Zeitraum",
"today": "Heute",
"24h": "Letzte 24 Stunden",
"week": "Letzte Woche",
"month": "Letzter Monat",
"year": "Letztes Jahr",
"all": "Gesamter Verlauf"
}
},
"activities": {
"Product sale": "Produkte verkauft",
"Production cost": "Produktionskosten",
@@ -675,6 +678,7 @@
"happy": "Glücklich",
"sad": "Traurig",
"angry": "Wütend",
"calm": "Ruhig",
"nervous": "Nervös",
"excited": "Aufgeregt",
"bored": "Gelangweilt",
@@ -765,17 +769,39 @@
"advance": {
"confirm": "Aufsteigen beantragen"
},
"cooldown": "Du kannst frühestens wieder am {date} aufsteigen.",
"errors": {
"tooSoon": "Aufstieg zu früh.",
"unmet": "Folgende Voraussetzungen fehlen:",
"generic": "Der Aufstieg ist fehlgeschlagen."
}
"cooldown": "Du kannst frühestens wieder am {date} aufsteigen."
},
"reputation": {
"title": "Reputation",
"overview": {
"title": "Übersicht"
"title": "Übersicht",
"current": "Aktuelle Reputation"
},
"actions": {
"title": "Reputations-Aktionen",
"description": "Du kannst verschiedene Aktionen durchführen, um deine Reputation zu verbessern.",
"none": "Keine Reputations-Aktionen verfügbar.",
"action": "Aktion",
"cost": "Kosten",
"gain": "Gewinn",
"timesUsed": "Verwendet",
"execute": "Ausführen",
"running": "Läuft...",
"dailyLimit": "Tägliches Limit: {remaining} von {cap} Aktionen übrig",
"cooldown": "Cooldown: Noch {minutes} Minuten",
"type": {
"soup_kitchen": "Suppenküche",
"library_donation": "Bibliotheksspende",
"scholarships": "Stipendien",
"church_hospice": "Kirchenhospiz",
"school_funding": "Schulfinanzierung",
"orphanage_build": "Waisenhaus bauen",
"bridge_build": "Brücke bauen",
"hospital_donation": "Krankenhausspende",
"patronage": "Mäzenatentum",
"statue_build": "Statue errichten",
"well_build": "Brunnen bauen"
}
},
"party": {
"title": "Feste",
@@ -826,6 +852,53 @@
},
"church": {
"title": "Kirche",
"tabs": {
"current": "Aktuelle Positionen",
"available": "Verfügbare Positionen",
"applications": "Bewerbungen"
},
"current": {
"office": "Amt",
"region": "Region",
"holder": "Inhaber",
"supervisor": "Vorgesetzter",
"none": "Keine aktuellen Positionen vorhanden."
},
"available": {
"office": "Amt",
"region": "Region",
"supervisor": "Vorgesetzter",
"seats": "Verfügbare Plätze",
"action": "Aktion",
"apply": "Bewerben",
"applySuccess": "Bewerbung erfolgreich eingereicht.",
"applyError": "Fehler beim Einreichen der Bewerbung.",
"none": "Keine verfügbaren Positionen."
},
"applications": {
"office": "Amt",
"region": "Region",
"applicant": "Bewerber",
"date": "Datum",
"action": "Aktion",
"approve": "Annehmen",
"reject": "Ablehnen",
"approveSuccess": "Bewerbung angenommen.",
"rejectSuccess": "Bewerbung abgelehnt.",
"decideError": "Fehler bei der Entscheidung.",
"none": "Keine Bewerbungen vorhanden."
},
"offices": {
"lay-preacher": "Laienprediger",
"village-priest": "Dorfgeistlicher",
"parish-priest": "Pfarrer",
"dean": "Dekan",
"archdeacon": "Erzdiakon",
"bishop": "Bischof",
"archbishop": "Erzbischof",
"cardinal": "Kardinal",
"pope": "Papst"
},
"baptism": {
"title": "Taufen",
"table": {
@@ -927,7 +1000,12 @@
"drunkOfLife": "Trunk des Lebens",
"barber": "Barbier"
},
"choose": "Bitte auswählen"
"choose": "Bitte auswählen",
"errors": {
"tooClose": "Du kannst nicht so oft Maßnahmen durchführen.",
"generic": "Ein Fehler ist aufgetreten."
},
"nextMeasureAt": "Nächste Maßnahme ab"
},
"politics": {
"title": "Politik",
@@ -951,9 +1029,13 @@
"region": "Region",
"date": "Datum",
"candidacy": "Kandidatur",
"candidacyWithAge": "Kandidatur (ab 16 Jahren)",
"none": "Keine offenen Positionen.",
"apply": "Für ausgewählte Positionen kandidieren"
"apply": "Für ausgewählte Positionen kandidieren",
"minAgeHint": "Kandidatur erst ab 16 Jahren möglich.",
"ageRequirement": "Für alle politischen Ämter gilt: Kandidatur erst ab 16 Jahren."
},
"too_young": "Dein Charakter ist noch zu jung. Eine Bewerbung ist erst ab 16 Jahren möglich.",
"upcoming": {
"office": "Amt",
"region": "Region",

View File

@@ -94,29 +94,24 @@
"children_unbaptised": "Unbaptised children"
},
"overview": {
"metadata": {
"years": "years"
}
},
"genderAge": {
"ageGroups": "infant:2|toddler:5|child:13|maidenhood:20|adult:50|mature:70|elder:999",
"male": {
"infant": "babe",
"toddler": "wee one",
"child": "lad",
"maidenhood": "youth",
"adult": "man",
"mature": "goodman",
"elder": "old fellow"
"title": "Falukant - Overview",
"heirSelection": {
"title": "Heir Selection",
"description": "Your previous character is no longer available. Choose an heir from the list to continue playing.",
"loading": "Loading potential heirs…",
"noHeirs": "No heirs available.",
"select": "Select as play character",
"error": "Error selecting heir."
},
"female": {
"infant": "babe",
"toddler": "wee one",
"child": "lass",
"maidenhood": "maiden",
"adult": "woman",
"mature": "goodwife",
"elder": "old dame"
"metadata": {
"title": "Personal",
"name": "Name",
"money": "Wealth",
"age": "Age",
"years": "Years",
"days": "Days",
"mainbranch": "Home city",
"nobleTitle": "Title"
}
},
"health": {
@@ -137,6 +132,23 @@
"time": "Time",
"prev": "Previous",
"next": "Next",
"graph": {
"open": "Show graph",
"title": "Money over time",
"close": "Close",
"loading": "Loading history...",
"noData": "No entries for the selected period.",
"yesterday": "Yesterday",
"range": {
"label": "Range",
"today": "Today",
"24h": "Last 24 hours",
"week": "Last week",
"month": "Last month",
"year": "Last year",
"all": "All history"
}
},
"activities": {
"Product sale": "Product sale",
"Production cost": "Production cost",
@@ -191,6 +203,29 @@
"income": "Income",
"incomeUpdated": "Salary has been successfully updated."
},
"production": {
"title": "Production",
"info": "Details about production in the branch.",
"selectProduct": "Select product",
"quantity": "Quantity",
"storageAvailable": "Free storage",
"cost": "Cost",
"duration": "Duration",
"revenue": "Revenue",
"start": "Start production",
"success": "Production started successfully!",
"error": "Error starting production.",
"minutes": "Minutes",
"ending": "Ending:",
"time": "Time",
"current": "Running productions",
"product": "Product",
"remainingTime": "Remaining time (seconds)",
"status": "Status",
"sleep": "Paused",
"active": "Active",
"noProductions": "No running productions."
},
"vehicles": {
"cargo_cart": "Cargo cart",
"ox_cart": "Ox cart",
@@ -222,13 +257,87 @@
}
},
"nobility": {
"cooldown": "You can only advance again on {date}.",
"cooldown": "You can only advance again on {date}."
},
"mood": {
"happy": "Happy",
"sad": "Sad",
"angry": "Angry",
"calm": "Calm",
"nervous": "Nervous",
"excited": "Excited",
"bored": "Bored",
"fearful": "Fearful",
"confident": "Confident",
"curious": "Curious",
"hopeful": "Hopeful",
"frustrated": "Frustrated",
"lonely": "Lonely",
"grateful": "Grateful",
"jealous": "Jealous",
"guilty": "Guilty",
"apathetic": "Apathetic",
"relieved": "Relieved",
"proud": "Proud",
"ashamed": "Ashamed"
},
"character": {
"brave": "Brave",
"kind": "Kind",
"greedy": "Greedy",
"wise": "Wise",
"loyal": "Loyal",
"cunning": "Cunning",
"generous": "Generous",
"arrogant": "Arrogant",
"honest": "Honest",
"ambitious": "Ambitious",
"patient": "Patient",
"impatient": "Impatient",
"selfish": "Selfish",
"charismatic": "Charismatic",
"empathetic": "Empathetic",
"timid": "Timid",
"stubborn": "Stubborn",
"resourceful": "Resourceful",
"reckless": "Reckless",
"disciplined": "Disciplined",
"optimistic": "Optimistic",
"pessimistic": "Pessimistic",
"manipulative": "Manipulative",
"independent": "Independent",
"dependent": "Dependent",
"adventurous": "Adventurous",
"humble": "Humble",
"vengeful": "Vengeful",
"pragmatic": "Pragmatic",
"idealistic": "Idealistic"
},
"healthview": {
"title": "Health",
"age": "Age",
"status": "Health Status",
"measuresTaken": "Measures Taken",
"measure": "Measure",
"date": "Date",
"cost": "Cost",
"success": "Success",
"selectMeasure": "Select Measure",
"perform": "Perform",
"measures": {
"pill": "Pill",
"doctor": "Doctor Visit",
"witch": "Witch",
"drunkOfLife": "Elixir of Life",
"barber": "Barber"
},
"choose": "Please select",
"errors": {
"tooSoon": "Advancement too soon.",
"unmet": "The following requirements are not met:",
"generic": "Advancement failed."
}
},
"tooClose": "You cannot perform measures so often.",
"generic": "An error occurred."
},
"nextMeasureAt": "Next measure from"
},
"branchProduction": {
"storageAvailable": "Free storage"
},
@@ -254,9 +363,13 @@
"region": "Region",
"date": "Date",
"candidacy": "Candidacy",
"candidacyWithAge": "Candidacy (from age 16)",
"none": "No open positions.",
"apply": "Apply for selected positions"
"apply": "Apply for selected positions",
"minAgeHint": "Candidacy is only possible from age 16.",
"ageRequirement": "All political offices require candidates to be at least 16 years old."
},
"too_young": "Your character is too young. Applications are only possible from age 16.",
"upcoming": {
"office": "Office",
"region": "Region",
@@ -353,6 +466,143 @@
"success": "The gift has been given.",
"nextGiftAt": "Next gift from"
}
},
"church": {
"title": "Church",
"tabs": {
"current": "Current Positions",
"available": "Available Positions",
"applications": "Applications"
},
"current": {
"office": "Office",
"region": "Region",
"holder": "Holder",
"supervisor": "Supervisor",
"none": "No current positions available."
},
"available": {
"office": "Office",
"region": "Region",
"supervisor": "Supervisor",
"seats": "Available Seats",
"action": "Action",
"apply": "Apply",
"applySuccess": "Application submitted successfully.",
"applyError": "Error submitting application.",
"none": "No available positions."
},
"applications": {
"office": "Office",
"region": "Region",
"applicant": "Applicant",
"date": "Date",
"action": "Action",
"approve": "Approve",
"reject": "Reject",
"approveSuccess": "Application approved.",
"rejectSuccess": "Application rejected.",
"decideError": "Error making decision.",
"none": "No applications available."
},
"offices": {
"lay-preacher": "Lay Preacher",
"village-priest": "Village Priest",
"parish-priest": "Parish Priest",
"dean": "Dean",
"archdeacon": "Archdeacon",
"bishop": "Bishop",
"archbishop": "Archbishop",
"cardinal": "Cardinal",
"pope": "Pope"
},
"baptism": {
"title": "Baptism",
"table": {
"name": "First Name",
"gender": "Gender",
"age": "Age",
"baptise": "Baptize (50)",
"newName": "Suggest Name"
},
"gender": {
"male": "Boy",
"female": "Girl"
},
"success": "The child has been baptized.",
"error": "The child could not be baptized."
}
},
"reputation": {
"title": "Reputation",
"overview": {
"title": "Overview",
"current": "Current Reputation"
},
"actions": {
"title": "Reputation Actions",
"description": "You can perform various actions to improve your reputation.",
"none": "No reputation actions available.",
"action": "Action",
"cost": "Cost",
"gain": "Gain",
"timesUsed": "Used",
"execute": "Execute",
"running": "Running...",
"dailyLimit": "Daily limit: {remaining} of {cap} actions remaining",
"cooldown": "Cooldown: {minutes} minutes remaining",
"type": {
"soup_kitchen": "Soup Kitchen",
"library_donation": "Library Donation",
"scholarships": "Scholarships",
"church_hospice": "Church Hospice",
"school_funding": "School Funding",
"orphanage_build": "Build Orphanage",
"bridge_build": "Build Bridge",
"hospital_donation": "Hospital Donation",
"patronage": "Patronage",
"statue_build": "Build Statue",
"well_build": "Build Well"
}
},
"party": {
"title": "Parties",
"totalCost": "Total Cost",
"order": "Order Party",
"inProgress": "Parties in Preparation",
"completed": "Completed Parties",
"newpartyview": {
"open": "Create New Party",
"close": "Hide New Party",
"type": "Party Type"
},
"music": {
"label": "Music",
"none": "No Music",
"bard": "A Bard",
"villageBand": "A Village Band",
"chamberOrchestra": "A Chamber Orchestra",
"symphonyOrchestra": "A Symphony Orchestra",
"symphonyOrchestraWithChorusAndSolists": "A Symphony Orchestra with Chorus and Soloists"
},
"banquette": {
"label": "Food",
"bread": "Bread",
"roastWithBeer": "Roast with Beer",
"poultryWithVegetablesAndWine": "Poultry with Vegetables and Wine",
"extensiveBuffet": "Festive Meal"
},
"servants": {
"label": "One servant per ",
"perPersons": " persons"
},
"esteemedInvites": {
"label": "Invited Estates"
},
"type": "Party Type",
"cost": "Cost",
"date": "Date"
}
}
}
}

View File

@@ -360,6 +360,7 @@ export default {
vehicles: [],
activeTab: 'production',
productPricesCache: {}, // Cache für regionale Preise: { productId: price }
productPricesCacheRegionId: null, // regionId, für die der Cache gültig ist
tabs: [
{ value: 'production', label: 'falukant.branch.tabs.production' },
{ value: 'inventory', label: 'falukant.branch.tabs.inventory' },
@@ -569,30 +570,46 @@ export default {
async loadProductPricesForCurrentBranch() {
if (!this.selectedBranch || !this.selectedBranch.regionId) {
this.productPricesCache = {};
this.productPricesCacheRegionId = null;
return;
}
// Lade Preise für alle Produkte in der aktuellen Region
const prices = {};
for (const product of this.products) {
try {
const { data } = await apiClient.get('/api/falukant/products/price-in-region', {
params: {
productId: product.id,
regionId: this.selectedBranch.regionId
}
});
prices[product.id] = data.price;
} catch (error) {
console.error(`Error loading price for product ${product.id}:`, error);
// Fallback auf Standard-Berechnung
const knowledgeFactor = product.knowledges?.[0]?.knowledge || 0;
const maxPrice = product.sellCost;
const minPrice = maxPrice * 0.6;
prices[product.id] = minPrice + (maxPrice - minPrice) * (knowledgeFactor / 100);
}
if (this.productPricesCacheRegionId === this.selectedBranch.regionId && Object.keys(this.productPricesCache).length > 0) {
return;
}
try {
const { data } = await apiClient.get('/api/falukant/products/prices-in-region', {
params: {
regionId: this.selectedBranch.regionId
}
});
this.productPricesCache = data.prices || {};
this.productPricesCacheRegionId = this.selectedBranch.regionId;
} catch (error) {
console.error(`Error loading product prices for region ${this.selectedBranch.regionId}:`, error);
// Fallback: Lade Preise einzeln (alte Methode)
console.warn('[BranchView] Falling back to individual product price requests');
const prices = {};
for (const product of this.products) {
try {
const { data } = await apiClient.get('/api/falukant/products/price-in-region', {
params: {
productId: product.id,
regionId: this.selectedBranch.regionId
}
});
prices[product.id] = data.price;
} catch (err) {
console.error(`Error loading price for product ${product.id}:`, err);
// Fallback auf Standard-Berechnung
const knowledgeFactor = product.knowledges?.[0]?.knowledge || 0;
const maxPrice = product.sellCost;
const minPrice = maxPrice * 0.6;
prices[product.id] = minPrice + (maxPrice - minPrice) * (knowledgeFactor / 100);
}
}
this.productPricesCache = prices;
this.productPricesCacheRegionId = this.selectedBranch?.regionId ?? null;
}
this.productPricesCache = prices;
},
formatPercent(value) {
@@ -704,13 +721,17 @@ export default {
},
speedLabel(value) {
// Expect numeric speeds 1..4; provide localized labels as fallback to raw value
const key = value == null ? 'unknown' : String(value);
if (value == null) return this.$t('falukant.branch.transport.speed.unknown') || '—';
if (typeof value === 'object') {
const k = value.tr ?? value.id ?? 'unknown';
const tKey = `falukant.branch.transport.speed.${k}`;
const t = this.$t(tKey);
return (t && t !== tKey) ? t : String(k);
}
const key = String(value);
const tKey = `falukant.branch.transport.speed.${key}`;
const translated = this.$t(tKey);
// If translation returns the key (no translation found), fall back to the numeric value
if (!translated || translated === tKey) return value;
return translated;
return (!translated || translated === tKey) ? key : translated;
},
transportModeLabel(mode) {

View File

@@ -25,7 +25,7 @@
</tr>
<tr>
<td>{{ $t('falukant.family.spouse.mood') }}</td>
<td>{{ $t(`falukant.mood.${relationships[0].character2.mood.tr}`) }}</td>
<td>{{ relationships[0].character2.mood?.tr ? $t(`falukant.mood.${relationships[0].character2.mood.tr}`) : '—' }}</td>
</tr>
<tr>
<td>{{ $t('falukant.family.spouse.status') }}</td>

View File

@@ -9,6 +9,12 @@
<button @click="fetchMoneyHistory(1)">{{ $t('falukant.moneyHistory.search') }}</button>
</div>
<div class="graph-section">
<button @click="openGraphDialog">
{{ $t('falukant.moneyHistory.graph.open') }}
</button>
</div>
<table>
<thead>
<tr>
@@ -42,17 +48,21 @@
{{ $t('falukant.moneyHistory.next') }}
</button>
</div>
<MoneyHistoryGraphDialog ref="graphDialog" />
</div>
</template>
<script>
import StatusBar from '@/components/falukant/StatusBar.vue'
import MoneyHistoryGraphDialog from '@/dialogues/falukant/MoneyHistoryGraphDialog.vue'
import apiClient from '@/utils/axios.js';
export default {
name: 'MoneyHistoryView',
components: {
StatusBar,
MoneyHistoryGraphDialog,
},
computed: {
locale() {
@@ -97,6 +107,9 @@ export default {
}
return translation !== key ? translation : activity;
},
openGraphDialog() {
this.$refs.graphDialog.open();
},
},
};
</script>
@@ -106,6 +119,10 @@ export default {
margin-bottom: 1rem;
}
.graph-section {
margin-bottom: 1rem;
}
table {
width: 100%;
border-collapse: collapse;

View File

@@ -23,7 +23,7 @@
</tr>
</thead>
<tbody>
<tr v-for="pos in currentPositions" :key="pos.id">
<tr v-for="pos in currentPositions" :key="pos.id" :class="{ 'own-position': isOwnPosition(pos) }">
<td>{{ $t(`falukant.politics.offices.${pos.officeType.name}`) }}</td>
<td>{{ pos.region.name }}</td>
<td>
@@ -57,6 +57,7 @@
<!-- OPEN Tab: hier zeigen wir 'openPolitics' -->
<div v-else-if="activeTab === 'openPolitics'" class="tab-pane">
<p class="politics-age-requirement">{{ $t('falukant.politics.open.ageRequirement') }}</p>
<div v-if="loading.openPolitics" class="loading">{{ $t('loading') }}</div>
<div v-else class="table-scroll">
<table class="politics-table">
@@ -65,7 +66,7 @@
<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>
<th>{{ $t('falukant.politics.open.candidacyWithAge') }}</th>
</tr>
</thead>
<tbody>
@@ -74,13 +75,13 @@
<td>{{ e.region.name }}</td>
<td>{{ formatDate(e.date) }}</td>
<!-- Checkbox ganz am Ende -->
<td>
<td :title="e.canApplyByAge === false ? $t('falukant.politics.open.minAgeHint') : null">
<input
type="checkbox"
:id="`apply-${e.id}`"
v-model="selectedApplications"
:value="e.id"
:disabled="e.alreadyApplied"
:disabled="e.alreadyApplied || e.canApplyByAge === false"
/>
</td>
</tr>
@@ -193,6 +194,7 @@ export default {
elections: [],
selectedCandidates: {},
selectedApplications: [],
ownCharacterId: null,
loading: {
current: false,
openPolitics: false,
@@ -210,6 +212,7 @@ export default {
}
},
mounted() {
this.loadOwnCharacterId();
this.loadCurrentPositions();
},
methods: {
@@ -229,9 +232,12 @@ export default {
this.loading.current = true;
try {
const { data } = await apiClient.get('/api/falukant/politics/overview');
console.log('[PoliticsView] loadCurrentPositions - API response:', data);
console.log('[PoliticsView] loadCurrentPositions - ownCharacterId at load time:', this.ownCharacterId);
this.currentPositions = data;
console.log('[PoliticsView] loadCurrentPositions - Loaded', data.length, 'positions');
} catch (err) {
console.error('Error loading current positions', err);
console.error('[PoliticsView] Error loading current positions', err);
} finally {
this.loading.current = false;
}
@@ -241,10 +247,10 @@ export default {
this.loading.openPolitics = true;
try {
const { data } = await apiClient.get('/api/falukant/politics/open');
this.openPolitics = data;
this.openPolitics = Array.isArray(data) ? data : [];
// Bereits beworbene Positionen vorselektieren, damit die Checkbox
// sichtbar markiert bleibt.
this.selectedApplications = data
this.selectedApplications = this.openPolitics
.filter(e => e.alreadyApplied)
.map(e => e.id);
} catch (err) {
@@ -330,6 +336,44 @@ export default {
});
},
async loadOwnCharacterId() {
try {
const { data } = await apiClient.get('/api/falukant/info');
console.log('[PoliticsView] loadOwnCharacterId - API response:', data);
console.log('[PoliticsView] loadOwnCharacterId - data.character:', data.character);
console.log('[PoliticsView] loadOwnCharacterId - data.character?.id:', data.character?.id);
if (data.character && data.character.id) {
this.ownCharacterId = data.character.id;
console.log('[PoliticsView] loadOwnCharacterId - Set ownCharacterId to:', this.ownCharacterId);
} else {
console.warn('[PoliticsView] loadOwnCharacterId - No character ID found in response', {
hasCharacter: !!data.character,
characterKeys: data.character ? Object.keys(data.character) : null,
characterId: data.character?.id
});
}
} catch (err) {
console.error('[PoliticsView] Error loading own character ID', err);
}
},
isOwnPosition(pos) {
console.log('[PoliticsView] isOwnPosition - Checking position:', {
posId: pos.id,
posCharacter: pos.character,
posCharacterId: pos.character?.id,
ownCharacterId: this.ownCharacterId,
match: pos.character?.id === this.ownCharacterId
});
if (!this.ownCharacterId || !pos.character) {
console.log('[PoliticsView] isOwnPosition - Returning false (missing ownCharacterId or pos.character)');
return false;
}
const isMatch = pos.character.id === this.ownCharacterId;
console.log('[PoliticsView] isOwnPosition - Result:', isMatch);
return isMatch;
},
async submitApplications() {
try {
const response = await apiClient.post(
@@ -346,6 +390,10 @@ export default {
.map(e => e.id);
} catch (err) {
console.error('Error submitting applications', err);
const msg = err?.response?.data?.error === 'too_young'
? this.$t('falukant.politics.too_young')
: (err?.response?.data?.error || err?.message || this.$t('falukant.politics.applyError'));
this.$root.$refs?.messageDialog?.open?.(msg, this.$t('falukant.politics.title'));
}
}
}
@@ -384,6 +432,13 @@ h2 {
overflow: hidden;
}
.politics-age-requirement {
flex: 0 0 auto;
margin: 0 0 10px 0;
font-size: 0.95em;
color: #555;
}
.table-scroll {
flex: 1;
overflow-y: auto;
@@ -411,6 +466,11 @@ h2 {
border: 1px solid #ddd;
}
.politics-table tbody tr.own-position {
background-color: #e0e0e0;
font-weight: bold;
}
.loading {
text-align: center;
font-style: italic;