Refactor SaleSection component: Simplify sell item and sell all logic, remove unnecessary state management, and improve UI feedback. Update translations and clean up unused code in i18n files. Optimize price loading in BranchView and remove legacy product loading in MoneyHistoryView. Streamline PoliticsView by removing own character ID handling and related logic.

This commit is contained in:
Torsten Schulz (local)
2026-01-26 16:03:48 +01:00
parent 80b639b511
commit 71748f6aa0
15 changed files with 822 additions and 3815 deletions

View File

@@ -93,8 +93,6 @@ class FalukantController {
return result;
});
this.setHeir = this._wrapWithUser((userId, req) => this.service.setHeir(userId, req.body.childCharacterId));
this.getPotentialHeirs = this._wrapWithUser((userId) => this.service.getPotentialHeirs(userId));
this.selectHeir = this._wrapWithUser((userId, req) => this.service.selectHeir(userId, req.body.heirId));
this.acceptMarriageProposal = this._wrapWithUser((userId, req) => this.service.acceptMarriageProposal(userId, req.body.proposalId));
this.getGifts = this._wrapWithUser((userId) => {
console.log('🔍 getGifts called with userId:', userId);
@@ -118,12 +116,6 @@ class FalukantController {
}, { successStatus: 201 });
this.getParties = this._wrapWithUser((userId) => this.service.getParties(userId));
this.getReputationActions = this._wrapWithUser((userId) => this.service.getReputationActions(userId));
this.executeReputationAction = this._wrapWithUser((userId, req) => {
const { actionTypeId } = req.body;
return this.service.executeReputationAction(userId, actionTypeId);
}, { successStatus: 201 });
this.getNotBaptisedChildren = this._wrapWithUser((userId) => this.service.getNotBaptisedChildren(userId));
this.baptise = this._wrapWithUser((userId, req) => {
const { characterId: childId, firstName } = req.body;
@@ -148,20 +140,6 @@ class FalukantController {
this.getPoliticsOverview = this._wrapWithUser((userId) => this.service.getPoliticsOverview(userId));
this.getOpenPolitics = this._wrapWithUser((userId) => this.service.getOpenPolitics(userId));
// Church career endpoints
this.getChurchOverview = this._wrapWithUser((userId) => this.service.getChurchOverview(userId));
this.getAvailableChurchPositions = this._wrapWithUser((userId) => this.service.getAvailableChurchPositions(userId));
this.applyForChurchPosition = this._wrapWithUser((userId, req) => {
const { officeTypeId, regionId } = req.body;
return this.service.applyForChurchPosition(userId, officeTypeId, regionId);
}, { successStatus: 201 });
this.getSupervisedApplications = this._wrapWithUser((userId) => this.service.getSupervisedApplications(userId));
this.decideOnChurchApplication = this._wrapWithUser((userId, req) => {
const { applicationId, decision } = req.body;
return this.service.decideOnChurchApplication(userId, applicationId, decision);
});
this.hasChurchCareer = this._wrapWithUser((userId) => this.service.hasChurchCareer(userId));
this.getElections = this._wrapWithUser((userId) => this.service.getElections(userId));
this.vote = this._wrapWithUser((userId, req) => this.service.vote(userId, req.body.votes));
this.applyForElections = this._wrapWithUser((userId, req) => this.service.applyForElections(userId, req.body.electionIds));
@@ -176,18 +154,6 @@ class FalukantController {
}
return this.service.getProductPriceInRegion(userId, productId, regionId);
});
this.getProductPricesInRegionBatch = this._wrapWithUser((userId, req) => {
const productIds = req.query.productIds;
const regionId = parseInt(req.query.regionId, 10);
if (!productIds || Number.isNaN(regionId)) {
throw new Error('productIds (comma-separated) and regionId are required');
}
const productIdArray = productIds.split(',').map(id => parseInt(id.trim(), 10)).filter(id => !Number.isNaN(id));
if (productIdArray.length === 0) {
throw new Error('At least one valid productId is required');
}
return this.service.getProductPricesInRegionBatch(userId, productIdArray, regionId);
});
this.getProductPricesInCities = this._wrapWithUser((userId, req) => {
const productId = parseInt(req.query.productId, 10);
const currentPrice = parseFloat(req.query.currentPrice);

View File

@@ -10,11 +10,20 @@ RegionData.init({
allowNull: false},
regionTypeId: {
type: DataTypes.INTEGER,
allowNull: false
allowNull: false,
references: {
model: RegionType,
key: 'id',
schema: 'falukant_type'
}
},
parentId: {
type: DataTypes.INTEGER,
allowNull: true
allowNull: true,
references: {
model: 'region',
key: 'id',
schema: 'falukant_data'}
},
map: {
type: DataTypes.JSONB,

View File

@@ -6,7 +6,8 @@ class FalukantStock extends Model { }
FalukantStock.init({
branchId: {
type: DataTypes.INTEGER,
allowNull: false
allowNull: false,
defaultValue: 0
},
stockTypeId: {
type: DataTypes.INTEGER,

View File

@@ -16,6 +16,17 @@ ProductType.init({
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'
}
}, {
sequelize,
modelName: 'ProductType',

View File

@@ -39,8 +39,6 @@ router.get('/directors', falukantController.getAllDirectors);
router.post('/directors', falukantController.updateDirector);
router.post('/family/acceptmarriageproposal', falukantController.acceptMarriageProposal);
router.post('/family/set-heir', falukantController.setHeir);
router.get('/heirs/potential', falukantController.getPotentialHeirs);
router.post('/heirs/select', falukantController.selectHeir);
router.get('/family/gifts', falukantController.getGifts);
router.get('/family/children', falukantController.getChildren);
router.post('/family/gift', falukantController.sendGift);
@@ -55,16 +53,8 @@ router.post('/houses', falukantController.buyUserHouse);
router.get('/party/types', falukantController.getPartyTypes);
router.post('/party', falukantController.createParty);
router.get('/party', falukantController.getParties);
router.get('/reputation/actions', falukantController.getReputationActions);
router.post('/reputation/actions', falukantController.executeReputationAction);
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.post('/church/positions/apply', falukantController.applyForChurchPosition);
router.get('/church/applications/supervised', falukantController.getSupervisedApplications);
router.post('/church/applications/decide', falukantController.decideOnChurchApplication);
router.get('/church/career/check', falukantController.hasChurchCareer);
router.get('/education', falukantController.getEducation);
router.post('/education', falukantController.sendToSchool);
router.get('/bank/overview', falukantController.getBankOverview);
@@ -82,7 +72,6 @@ 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-batch', falukantController.getProductPricesInRegionBatch);
router.get('/products/prices-in-cities', falukantController.getProductPricesInCities);
router.get('/branches/:branchId/taxes', falukantController.getBranchTaxes);
router.get('/vehicles/types', falukantController.getVehicleTypes);

File diff suppressed because it is too large Load Diff

View File

@@ -282,7 +282,11 @@ async function initializeFalukantProducts() {
{ labelTr: 'ox', category: 5, productionTime: 5, sellCost: 60 },
];
const productsToInsert = baseProducts;
const productsToInsert = baseProducts.map(p => ({
...p,
sellCostMinNeutral: Math.ceil(p.sellCost * factorMin),
sellCostMaxNeutral: Math.ceil(p.sellCost * factorMax),
}));
await ProductType.bulkCreate(productsToInsert, {
ignoreDuplicates: true,

View File

@@ -104,14 +104,6 @@
/>
{{ $t('falukant.branch.director.starttransport') }}
</label>
<label>
<input
type="checkbox"
v-model="director.mayRepairVehicles"
@change="saveSetting('mayRepairVehicles', director.mayRepairVehicles)"
/>
{{ $t('falukant.branch.director.repairVehicles') }}
</label>
</div>
<div class="field">
@@ -181,7 +173,7 @@
</div>
</div>
</div>
<NewDirectorDialog ref="newDirectorDialog" @directorHired="handleDirectorHired" />
<NewDirectorDialog ref="newDirectorDialog" />
</template>
<script>
@@ -276,11 +268,6 @@ export default {
this.$refs.newDirectorDialog.open(this.branchId);
},
async handleDirectorHired() {
// Nach dem Einstellen eines Direktors die Daten neu laden
await this.loadDirector();
},
async updateDirector() {
if (!this.director || this.editIncome == null) return;
try {

File diff suppressed because it is too large Load Diff

View File

@@ -20,10 +20,8 @@
<td>{{ item.quality }}</td>
<td>{{ item.totalQuantity }}</td>
<td>
<input type="number" v-model.number="item.sellQuantity" :min="1" :max="item.totalQuantity" :disabled="sellingItemIndex === index" />
<button @click="sellItem(index)" :disabled="sellingItemIndex === index || sellingAll">
{{ sellingItemIndex === index ? $t('falukant.branch.sale.selling') : $t('falukant.branch.sale.sellButton') }}
</button>
<input type="number" v-model.number="item.sellQuantity" :min="1" :max="item.totalQuantity" />
<button @click="sellItem(index)">{{ $t('falukant.branch.sale.sellButton') }}</button>
</td>
<td>
<div v-if="item.betterPrices && item.betterPrices.length > 0" class="price-cities">
@@ -38,12 +36,7 @@
</tr>
</tbody>
</table>
<button @click="sellAll" :disabled="sellingAll || sellingItemIndex !== null">
{{ sellingAll ? $t('falukant.branch.sale.selling') : $t('falukant.branch.sale.sellAllButton') }}
</button>
<div v-if="sellAllStatus" class="sell-all-status" :class="sellAllStatus.type">
{{ sellAllStatus.message }}
</div>
<button @click="sellAll">{{ $t('falukant.branch.sale.sellAllButton') }}</button>
</div>
<div v-else>
<p>{{ $t('falukant.branch.sale.noInventory') }}</p>
@@ -190,9 +183,6 @@
data() {
return {
inventory: [],
sellingItemIndex: null,
sellingAll: false,
sellAllStatus: null,
transportForm: {
sourceKey: null,
vehicleTypeId: null,
@@ -261,6 +251,13 @@
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();
@@ -277,22 +274,12 @@
}
},
methods: {
speedLabel(value) {
// Muss in methods liegen (Vue3): in computed wäre es ein Getter und keine aufrufbare Funktion.
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 loadInventory() {
try {
const response = await apiClient.get(`/api/falukant/inventory/${this.branchId}`);
this.inventory = response.data.map(item => ({
...item,
sellQuantity: item.totalQuantity,
// Vue3: besserPrices direkt als Property setzen (statt this.$set)
betterPrices: Array.isArray(item.betterPrices) ? item.betterPrices : [],
}));
await this.loadPricesForInventory();
} catch (error) {
@@ -313,11 +300,10 @@
currentPrice: currentPrice
}
});
// Vue3: direkte Zuweisung ist reaktiv
item.betterPrices = Array.isArray(data) ? data : [];
this.$set(item, 'betterPrices', data || []);
} catch (error) {
console.error(`Error loading prices for item ${itemKey}:`, error);
item.betterPrices = [];
this.$set(item, 'betterPrices', []);
} finally {
this.loadingPrices.delete(itemKey);
}
@@ -334,61 +320,23 @@
maximumFractionDigits: 2,
}).format(price);
},
async sellItem(index) {
if (this.sellingItemIndex !== null || this.sellingAll) return;
sellItem(index) {
const item = this.inventory[index];
const quantityToSell = item.sellQuantity || item.totalQuantity;
this.sellingItemIndex = index;
try {
await apiClient.post(`/api/falukant/sell`, {
apiClient.post(`/api/falukant/sell`, {
branchId: this.branchId,
productId: item.product.id,
quantity: quantityToSell,
quality: item.quality,
});
// UI sofort freigeben (Label/Disabled zurücksetzen), dann Inventory refreshen
this.sellingItemIndex = null;
await this.loadInventory();
} catch (error) {
}).catch(() => {
alert(this.$t('falukant.branch.sale.sellError'));
} finally {
this.sellingItemIndex = null;
}
});
},
async sellAll() {
if (this.sellingAll || this.sellingItemIndex !== null) return;
this.sellingAll = true;
this.sellAllStatus = null;
try {
const response = await apiClient.post(`/api/falukant/sell/all`, { branchId: this.branchId });
const revenue = response.data?.revenue || 0;
// UI sofort freigeben + Status setzen, danach Inventory refreshen
this.sellingAll = false;
this.sellAllStatus = {
type: 'success',
message: this.$t('falukant.branch.sale.sellAllSuccess', { revenue: this.formatMoney(revenue) })
};
// Inventory neu laden nach erfolgreichem Verkauf
await this.loadInventory();
} catch (error) {
// UI sofort freigeben + Fehlerstatus setzen
this.sellingAll = false;
this.sellAllStatus = {
type: 'error',
message: this.$t('falukant.branch.sale.sellAllError')
};
} finally {
// Falls noch nicht freigegeben (z.B. wenn ein unerwarteter Fehler vor Response passiert)
this.sellingAll = false;
// Status nach 5 Sekunden löschen
setTimeout(() => {
this.sellAllStatus = null;
}, 5000);
}
sellAll() {
apiClient.post(`/api/falukant/sell/all`, { branchId: this.branchId })
.catch(() => {
alert(this.$t('falukant.branch.sale.sellAllError'));
});
},
inventoryOptions() {
return this.inventory.map((item, index) => ({
@@ -627,11 +575,11 @@
cursor: help;
}
.city-price-green {
background-color: var(--color-primary-green);
background-color: #90EE90;
color: #000;
}
.city-price-orange {
background-color: var(--color-primary-orange);
background-color: #FFA500;
color: #000;
}
.city-price-red {
@@ -642,19 +590,5 @@
color: #999;
font-style: italic;
}
.sell-all-status {
margin-top: 10px;
padding: 8px;
border-radius: 4px;
}
.sell-all-status.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.sell-all-status.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
</style>

View File

@@ -32,9 +32,8 @@
},
"notifications": {
"notify_election_created": "Es wurde eine neue Wahl ausgeschrieben.",
"notify_office_filled": "Ein politisches Amt wurde neu besetzt.",
"production": {
"overproduction": "Überproduktion: Deine Produktion liegt {value} Einheiten über dem Bedarf{branch_info}."
"overproduction": "Überproduktion: Deine Produktion liegt {value}% über dem Bedarf."
},
"transport": {
"waiting": "Transport wartet"
@@ -136,14 +135,6 @@
"store": "Verkauf",
"fullstack": "Produktion mit Verkauf"
}
},
"heirSelection": {
"title": "Charakter verloren - Erben auswählen",
"description": "Dein Charakter wurde durch einen Fehler verloren. Bitte wähle einen Erben aus deiner Hauptregion aus, um fortzufahren.",
"loading": "Lade mögliche Erben...",
"noHeirs": "Es wurden keine passenden Erben gefunden.",
"select": "Als Erben wählen",
"error": "Fehler beim Auswählen des Erben."
}
},
"titles": {
@@ -241,7 +232,6 @@
"produce": "Darf produzieren",
"sell": "Darf verkaufen",
"starttransport": "Darf Transporte veranlassen",
"repairVehicles": "Darf Fahrzeuge reparieren",
"emptyTransport": {
"title": "Transport ohne Produkte",
"description": "Bewege Transportmittel von dieser Niederlassung zu einer anderen, um sie besser zu nutzen.",
@@ -270,10 +260,6 @@
"sell": "Verkauf",
"sellButton": "Verkaufen",
"sellAllButton": "Alles verkaufen",
"selling": "Verkauf läuft...",
"sellError": "Fehler beim Verkauf des Produkts.",
"sellAllError": "Fehler beim Verkauf aller Produkte.",
"sellAllSuccess": "Alle Produkte wurden erfolgreich verkauft. Einnahmen: {revenue}",
"transportTitle": "Transport anlegen",
"transportSource": "Artikel",
"transportSourcePlaceholder": "Artikel wählen",
@@ -321,10 +307,7 @@
"current": "Laufende Produktionen",
"product": "Produkt",
"remainingTime": "Verbleibende Zeit (Sekunden)",
"noProductions": "Keine laufenden Produktionen.",
"status": "Status",
"sleep": "Zurückgestellt",
"active": "Aktiv"
"noProductions": "Keine laufenden Produktionen."
},
"columns": {
"city": "Stadt",
@@ -595,7 +578,6 @@
"Production cost": "Produktionskosten",
"Sell all products": "Alle Produkte verkauft",
"sell products": "Produkte verkauft",
"taxFromSaleProduct": "Steuer aus Verkauf: {product}",
"director starts production": "Direktor beginnt Produktion",
"director payed out": "Direktorgehalt ausgezahlt",
"Buy storage (type: field)": "Lagerplatz gekauft (Typ: Feld)",
@@ -614,9 +596,6 @@
"new nobility title": "Neuer Adelstitel",
"partyOrder": "Fest bestellt",
"renovation_all": "Haus komplett renoviert",
"reputationAction": {
"school_funding": "Sozialstatus: Schule/Lehrstuhl finanziert"
},
"health": {
"pill": "Gesundheitsmaßnahme: Tablette",
"doctor": "Gesundheitsmaßnahme: Arztbesuch",
@@ -759,8 +738,7 @@
"reputation": {
"title": "Reputation",
"overview": {
"title": "Übersicht",
"current": "Deine aktuelle Reputation"
"title": "Übersicht"
},
"party": {
"title": "Feste",
@@ -799,34 +777,6 @@
"type": "Festart",
"cost": "Kosten",
"date": "Datum"
},
"actions": {
"title": "Aktionen",
"description": "Mit diesen Aktionen kannst du Reputation gewinnen. Je öfter du dieselbe Aktion ausführst, desto weniger Reputation bringt sie (unabhängig von den Kosten).",
"action": "Aktion",
"cost": "Kosten",
"gain": "Reputation",
"timesUsed": "Bereits genutzt",
"execute": "Ausführen",
"running": "Läuft...",
"none": "Keine Aktionen verfügbar.",
"dailyLimit": "Heute noch verfügbar: {remaining} / {cap} Reputation (durch Aktionen).",
"cooldown": "Nächste Sozialstatus-Aktion in ca. {minutes} Minuten möglich.",
"success": "Aktion erfolgreich! Reputation +{gain}, Kosten {cost}.",
"successSimple": "Aktion erfolgreich!",
"type": {
"library_donation": "Spende für eine Bibliothek",
"orphanage_build": "Waisenhaus aufbauen",
"statue_build": "Statue errichten",
"hospital_donation": "Krankenhaus/Heilhaus stiften",
"school_funding": "Schule/Lehrstuhl finanzieren",
"well_build": "Brunnen/Wasserwerk bauen",
"bridge_build": "Straßen-/Brückenbau finanzieren",
"soup_kitchen": "Armenspeisung organisieren",
"patronage": "Kunst & Mäzenatentum",
"church_hospice": "Hospiz-/Kirchenspende",
"scholarships": "Stipendienfonds finanzieren"
}
}
},
"party": {
@@ -838,58 +788,6 @@
}
},
"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": {
"village-priest": "Dorfgeistlicher",
"parish-priest": "Pfarrer",
"dean": "Dekan",
"archdeacon": "Erzdiakon",
"bishop": "Bischof",
"archbishop": "Erzbischof",
"cardinal": "Kardinal",
"pope": "Papst"
},
"application": {
"received": "Neue Bewerbung erhalten",
"approved": "Bewerbung angenommen",
"rejected": "Bewerbung abgelehnt"
},
"title": "Kirche",
"baptism": {
"title": "Taufen",
@@ -985,9 +883,6 @@
"success": "Erfolg",
"selectMeasure": "Maßnahme",
"perform": "Durchführen",
"errors": {
"tooClose": "Aktionen zu dicht hintereinander (maximal 1× pro 24 Stunden)."
},
"measures": {
"pill": "Tablette",
"doctor": "Arztbesuch",

View File

@@ -18,9 +18,8 @@
},
"notifications": {
"notify_election_created": "A new election has been scheduled.",
"notify_office_filled": "A political office has been filled.",
"production": {
"overproduction": "Overproduction: your production is {value} units above demand{branch_info}."
"overproduction": "Overproduction: your production is {value}% above demand."
},
"transport": {
"waiting": "Transport waiting"
@@ -101,12 +100,6 @@
"bad": "Bad",
"very_bad": "Very bad"
},
"healthview": {
"title": "Health",
"errors": {
"tooClose": "Actions too close together (max once per 24 hours)."
}
},
"moneyHistory": {
"title": "Money history",
"filter": "Filter",
@@ -123,7 +116,6 @@
"Production cost": "Production cost",
"Sell all products": "Sell all products",
"sell products": "Sell products",
"taxFromSaleProduct": "Tax from product sale: {product}",
"director starts production": "Director starts production",
"director payed out": "Director salary paid out",
"Buy storage (type: field)": "Bought storage (type: field)",
@@ -142,9 +134,6 @@
"new nobility title": "New title of nobility",
"partyOrder": "Party ordered",
"renovation_all": "House fully renovated",
"reputationAction": {
"school_funding": "Social status: funded a school/chair"
},
"health": {
"pill": "Health measure: pill",
"doctor": "Health measure: doctor",
@@ -174,8 +163,7 @@
},
"director": {
"income": "Income",
"incomeUpdated": "Salary has been successfully updated.",
"repairVehicles": "May repair vehicles"
"incomeUpdated": "Salary has been successfully updated."
},
"vehicles": {
"cargo_cart": "Cargo cart",
@@ -193,31 +181,8 @@
"storage": "Storage",
"transport": "Transport",
"taxes": "Taxes"
},
"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": "Completed:",
"time": "Time",
"current": "Running productions",
"product": "Product",
"remainingTime": "Remaining time (seconds)",
"noProductions": "No running productions.",
"status": "Status",
"sleep": "Suspended",
"active": "Active"
},
"taxes": {
}
,"taxes": {
"title": "Taxes",
"loading": "Loading tax data...",
"loadingError": "Failed to load tax data: {error}",
@@ -230,80 +195,9 @@
}
}
},
"overview": {
"title": "Falukant - Overview",
"metadata": {
"title": "Personal",
"name": "Name",
"money": "Wealth",
"age": "Age",
"mainbranch": "Home City",
"nobleTitle": "Status"
},
"productions": {
"title": "Productions"
},
"stock": {
"title": "Stock"
},
"branches": {
"title": "Branches",
"level": {
"production": "Production",
"store": "Store",
"fullstack": "Production with Store"
}
},
"heirSelection": {
"title": "Character Lost - Select Heir",
"description": "Your character was lost due to an error. Please select an heir from your main region to continue.",
"loading": "Loading potential heirs...",
"noHeirs": "No suitable heirs were found.",
"select": "Select as Heir",
"error": "Error selecting heir."
}
},
"nobility": {
"cooldown": "You can only advance again on {date}."
},
"reputation": {
"title": "Reputation",
"overview": {
"title": "Overview",
"current": "Your current reputation"
},
"party": {
"title": "Parties"
},
"actions": {
"title": "Actions",
"description": "These actions let you gain reputation. The more often you repeat the same action, the less reputation it yields (independent of cost).",
"action": "Action",
"cost": "Cost",
"gain": "Reputation",
"timesUsed": "Times used",
"execute": "Execute",
"running": "Running...",
"none": "No actions available.",
"dailyLimit": "Available today: {remaining} / {cap} reputation (from actions).",
"cooldown": "Next social status action available in about {minutes} minutes.",
"success": "Action successful! Reputation +{gain}, cost {cost}.",
"successSimple": "Action successful!",
"type": {
"library_donation": "Donate to a library",
"orphanage_build": "Build an orphanage",
"statue_build": "Erect a statue",
"hospital_donation": "Found a hospital/infirmary",
"school_funding": "Fund a school/chair",
"well_build": "Build a well/waterworks",
"bridge_build": "Fund roads/bridges",
"soup_kitchen": "Organize a soup kitchen",
"patronage": "Arts & patronage",
"church_hospice": "Hospice/church donation",
"scholarships": "Fund scholarships"
}
}
},
"branchProduction": {
"storageAvailable": "Free storage"
},
@@ -377,76 +271,6 @@
"assessor": "Assessor"
}
},
"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": {
"village-priest": "Village Priest",
"parish-priest": "Parish Priest",
"dean": "Dean",
"archdeacon": "Archdeacon",
"bishop": "Bishop",
"archbishop": "Archbishop",
"cardinal": "Cardinal",
"pope": "Pope"
},
"application": {
"received": "New application received",
"approved": "Application approved",
"rejected": "Application rejected"
},
"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."
}
},
"family": {
"children": {
"title": "Children",

View File

@@ -572,25 +572,9 @@ export default {
return;
}
if (!this.products || this.products.length === 0) {
this.productPricesCache = {};
return;
}
// OPTIMIERUNG: Lade alle Preise in einem Batch-Request
try {
const productIds = this.products.map(p => p.id).join(',');
const { data } = await apiClient.get('/api/falukant/products/prices-in-region-batch', {
params: {
productIds: productIds,
regionId: this.selectedBranch.regionId
}
});
this.productPricesCache = data || {};
} catch (error) {
console.error('Error loading prices in batch:', error);
// Fallback: Lade Preise einzeln (aber parallel)
const pricePromises = this.products.map(async (product) => {
// 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: {
@@ -598,23 +582,17 @@ export default {
regionId: this.selectedBranch.regionId
}
});
return { productId: product.id, price: data.price };
} catch (err) {
console.error(`Error loading price for product ${product.id}:`, err);
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;
return { productId: product.id, price: minPrice + (maxPrice - minPrice) * (knowledgeFactor / 100) };
prices[product.id] = minPrice + (maxPrice - minPrice) * (knowledgeFactor / 100);
}
});
const results = await Promise.all(pricePromises);
this.productPricesCache = {};
results.forEach(({ productId, price }) => {
this.productPricesCache[productId] = price;
});
}
this.productPricesCache = prices;
},
formatPercent(value) {
@@ -714,10 +692,7 @@ export default {
},
conditionLabel(value) {
// 0 ist ein gültiger Zustand (z.B. komplett kaputt) und darf nicht als "Unbekannt" enden.
if (value === null || value === undefined) return 'Unbekannt';
const v = Number(value);
if (!Number.isFinite(v)) return 'Unbekannt';
const v = Number(value) || 0;
if (v >= 95) return 'Ausgezeichnet'; // 95100
if (v >= 72) return 'Sehr gut'; // 7294
if (v >= 54) return 'Gut'; // 5471
@@ -725,7 +700,7 @@ export default {
if (v >= 22) return 'Schlecht'; // 2238
if (v >= 6) return 'Sehr schlecht'; // 621
if (v >= 1) return 'Katastrophal'; // 15
return 'Katastrophal'; // 0 oder kleiner
return 'Unbekannt';
},
speedLabel(value) {
@@ -1039,15 +1014,12 @@ export default {
});
await this.loadVehicles();
this.closeRepairAllVehiclesDialog();
// Statt JS-alert: Dialog schließen und MessageDialog anzeigen
this.$root.$refs.messageDialog?.open('tr:falukant.branch.transport.repairAllSuccess');
alert(this.$t('falukant.branch.transport.repairAllSuccess'));
this.$refs.statusBar?.fetchStatus();
} catch (error) {
console.error('Error repairing all vehicles:', error);
const errorMessage = error.response?.data?.message || this.$t('falukant.branch.transport.repairAllError');
// Bestätigungsdialog ebenfalls schließen und Fehler im MessageDialog anzeigen
this.closeRepairAllVehiclesDialog();
this.$root.$refs.messageDialog?.open(String(errorMessage), this.$t('error.title'));
alert(errorMessage);
}
},
@@ -1104,15 +1076,12 @@ export default {
await apiClient.post(`/api/falukant/vehicles/${this.repairVehicleDialog.vehicle.id}/repair`);
await this.loadVehicles();
this.closeRepairVehicleDialog();
// Statt JS-alert: Dialog schließen und MessageDialog anzeigen
this.$root.$refs.messageDialog?.open('tr:falukant.branch.transport.repairSuccess');
alert(this.$t('falukant.branch.transport.repairSuccess'));
this.$refs.statusBar?.fetchStatus();
} catch (error) {
console.error('Error repairing vehicle:', error);
const errorMessage = error.response?.data?.message || this.$t('falukant.branch.transport.repairError');
// Bestätigungsdialog ebenfalls schließen und Fehler im MessageDialog anzeigen
this.closeRepairVehicleDialog();
this.$root.$refs.messageDialog?.open(String(errorMessage), this.$t('error.title'));
alert(errorMessage);
}
},
},

View File

@@ -67,28 +67,12 @@ export default {
currentPage: 1,
totalPages: 1,
},
productsById: {},
};
},
async mounted() {
await Promise.all([this.loadProducts(), this.fetchMoneyHistory(1)]);
await this.fetchMoneyHistory(1);
},
methods: {
async loadProducts() {
try {
const { data } = await apiClient.get('/api/falukant/products');
const map = {};
for (const p of (data || [])) {
if (p && p.id != null && p.labelTr) {
map[String(p.id)] = p.labelTr;
}
}
this.productsById = map;
} catch (e) {
console.error('Error loading products for money history', e);
this.productsById = {};
}
},
async fetchMoneyHistory(page) {
try {
const response = await apiClient.post('/api/falukant/moneyhistory', {
@@ -101,25 +85,6 @@ export default {
}
},
translateActivity(activity) {
try {
const raw = String(activity ?? '');
// Handle legacy format: "tax from sale product 3"
const m = raw.match(/^tax\s+from\s+sale\s+product\s+(\d+)$/i);
if (m && m[1]) {
const id = m[1];
const labelTr = this.productsById[String(id)];
const productName = labelTr ? this.$t(`falukant.product.${labelTr}`) : `#${id}`;
return this.$t('falukant.moneyHistory.activities.taxFromSaleProduct', { product: productName });
}
// New/structured format: "taxFromSaleProduct.<labelTr>"
if (raw.startsWith('taxFromSaleProduct.')) {
const labelTr = raw.substring('taxFromSaleProduct.'.length);
const productName = labelTr ? this.$t(`falukant.product.${labelTr}`) : labelTr;
return this.$t('falukant.moneyHistory.activities.taxFromSaleProduct', { product: productName });
}
} catch (_) {
// ignore and fall back
}
// Handle nested keys like "health.pill" -> "health.pill"
const key = `falukant.moneyHistory.activities.${activity}`;
const translation = this.$t(key);

View File

@@ -23,7 +23,7 @@
</tr>
</thead>
<tbody>
<tr v-for="pos in currentPositions" :key="pos.id" :class="{ 'own-position': isOwnPosition(pos) }">
<tr v-for="pos in currentPositions" :key="pos.id">
<td>{{ $t(`falukant.politics.offices.${pos.officeType.name}`) }}</td>
<td>{{ pos.region.name }}</td>
<td>
@@ -193,7 +193,6 @@ export default {
elections: [],
selectedCandidates: {},
selectedApplications: [],
ownCharacterId: null,
loading: {
current: false,
openPolitics: false,
@@ -210,8 +209,7 @@ export default {
return this.elections.some(e => !e.voted);
}
},
async mounted() {
await this.loadOwnCharacterId();
mounted() {
this.loadCurrentPositions();
},
methods: {
@@ -332,24 +330,6 @@ export default {
});
},
async loadOwnCharacterId() {
try {
const { data } = await apiClient.get('/api/falukant/info');
if (data.character && data.character.id) {
this.ownCharacterId = data.character.id;
}
} catch (err) {
console.error('Error loading own character ID', err);
}
},
isOwnPosition(pos) {
if (!this.ownCharacterId || !pos.character) {
return false;
}
return pos.character.id === this.ownCharacterId;
},
async submitApplications() {
try {
const response = await apiClient.post(
@@ -431,11 +411,6 @@ h2 {
border: 1px solid #ddd;
}
.politics-table tbody tr.own-position {
background-color: #e0e0e0;
font-weight: bold;
}
.loading {
text-align: center;
font-style: italic;