Falukant production, family and administration enhancements

This commit is contained in:
Torsten Schulz
2025-04-14 15:17:35 +02:00
parent 90b4f51dcb
commit b15d93a798
77 changed files with 2429 additions and 1093 deletions

View File

@@ -1,6 +1,6 @@
<template>
<footer>
<div class="logo"><img src="/images/icons/logo_color.png"></div>
<div class="logo" @click="showFalukantDaemonStatus"><img src="/images/icons/logo_color.png"></div>
<div class="window-bar">
<button v-for="dialog in openDialogs" :key="dialog.dialog.name" class="dialog-button"
@click="toggleDialogMinimize(dialog.dialog.name)" :title="dialog.dialog.localTitle">
@@ -18,14 +18,21 @@
</template>
<script>
import { mapGetters } from 'vuex';
import { mapGetters, mapState } from 'vuex';
export default {
name: 'AppFooter',
components: {
},
computed: {
...mapGetters('dialogs', ['openDialogs'])
...mapGetters('dialogs', ['openDialogs']),
...mapState(['daemonSocket']),
},
mounted() {
this.daemonSocket.addEventListener('workerStatus', () => { console.log('----'); });
},
beforeUnmount() {
this.daemonSocket.removeEventListener('workerStatus', this.handleDaemonMessage);
},
methods: {
openImprintDialog() {
@@ -39,6 +46,13 @@ export default {
},
toggleDialogMinimize(dialogName) {
this.$store.dispatch('dialogs/toggleDialogMinimize', dialogName);
},
async showFalukantDaemonStatus() {
this.daemonSocket.send('{"event": "getWorkerStatus"}');
},
handleDaemonMessage(event) {
const status = JSON.parse(event.data);
console.log(event);
}
}
};

View File

@@ -13,11 +13,20 @@
class="submenu-icon">&nbsp;</span>
<span>{{ $t(`navigation.m-${key}.${subkey}`) }}</span>
<span v-if="subkey === 'forum'" class="subsubmenu">&#x25B6;</span>
<span v-else-if="subitem.children" class="subsubmenu">&#x25B6;</span>
<ul v-if="subkey === 'forum' && forumList.length > 0" class="submenu2">
<li v-for="forum in forumList" :key="forum.id" @click="openForum(forum.id, $event)">
{{ forum.name }}
</li>
</ul>
<ul v-else-if="subitem.children" class="submenu2">
<li v-for="(subsubitem, subsubkey) in subitem.children" :key="subsubitem.text"
@click="openPage(subsubitem.path ?? null)">
<span v-if="subsubitem.icon" :style="`background-image:url('/images/icons/${subsubitem.icon}')`"
class="submenu-icon">&nbsp;</span>
<span>{{ $t(`navigation.m-${key}.m-${subkey}.${subsubkey}`) }}</span>
</li>
</ul>
</li>
<li v-if="item.showLoggedinFriends === 1 && friendsList.length > 0" v-for="friend in friendsList" :key="friend.id">
{{ friend.username }}

View File

@@ -53,7 +53,7 @@ export default {
watch: {
visible(newValue) {
if (!newValue) {
this.minimized = false; // Reset minimized state when dialog is closed
this.minimized = false;
}
}
},

View File

@@ -0,0 +1,64 @@
<template>
<div class="branch-selection">
<h3>{{ $t('falukant.branch.selection.title') }}</h3>
<div>
<FormattedDropdown
:options="branches"
:columns="branchColumns"
v-model="localSelectedBranch"
:placeholder="$t('falukant.branch.selection.placeholder')"
@input="updateSelectedBranch"
/>
</div>
<div>
<button @click="$emit('createBranch')">{{ $t('falukant.branch.actions.create') }}</button>
<button @click="$emit('upgradeBranch')" :disabled="!localSelectedBranch">
{{ $t('falukant.branch.actions.upgrade') }}
</button>
</div>
</div>
</template>
<script>
import FormattedDropdown from '@/components/form/FormattedDropdown.vue';
export default {
name: "BranchSelection",
components: { FormattedDropdown },
props: {
branches: { type: Array, required: true },
selectedBranch: { type: Object, default: null },
},
data() {
return {
localSelectedBranch: this.selectedBranch,
branchColumns: [
{ field: "cityName", label: this.$t('falukant.branch.columns.city') },
{ field: "type", label: this.$t('falukant.branch.columns.type') },
],
};
},
watch: {
selectedBranch(newVal) {
this.localSelectedBranch = newVal;
},
},
methods: {
updateSelectedBranch(value) {
this.$emit('branchSelected', value);
},
},
};
</script>
<style scoped>
.branch-selection {
border: 1px solid #ccc;
margin: 10px 0;
border-radius: 4px;
padding: 10px;
}
button {
margin: 5px;
}
</style>

View File

@@ -0,0 +1,120 @@
<template>
<div class="director-info">
<h3>{{ $t('falukant.branch.director.title') }}</h3>
<div v-if="!director || director === null">
<button @click="openNewDirectorDialog">{{ $t('falukant.branch.director.actions.new') }}</button>
</div>
<div v-else class="director-info-container">
<div>
<table>
<tr>
<td>{{ $t('falukant.branch.director.name') }}</td>
<td>
{{ $t('falukant.titles.' + director.character.gender + '.' + director.character.title) }}
{{ director.character.name }}
</td>
</tr>
<tr>
<td>{{ $t('falukant.branch.director.salary') }}</td>
<td>{{ director.income }}</td>
</tr>
<tr>
<td>{{ $t('falukant.branch.director.satisfaction') }}</td>
<td>{{ director.satisfaction }} %</td>
</tr>
</table>
</div>
<div>
<table>
<tr>
<td><button @click="fireDirector">{{ $t('falukant.branch.director.fire') }}</button></td>
</tr>
<tr>
<td><button @click="teachDirector">{{ $t('falukant.branch.director.teach') }}</button></td>
</tr>
<tr>
<td>
<label>
<input type="checkbox" v-model="director.mayProduce" @change="saveSetting('mayProduce', director.mayProduce)">
{{ $t('falukant.branch.director.produce') }}
</label>
</td>
</tr>
<!-- Ähnliche Checkboxen für maySell und mayStartTransport -->
</table>
</div>
</div>
</div>
<NewDirectorDialog ref="newDirectorDialog" />
</template>
<script>
import apiClient from '@/utils/axios.js';
import NewDirectorDialog from '@/dialogues/falukant/NewDirectorDialog.vue';
export default {
name: "DirectorInfo",
props: { branchId: { type: Number, required: true } },
components: {
NewDirectorDialog
},
data() {
return {
director: null,
showNewDirectorDialog: false,
};
},
async mounted() {
await this.loadDirector();
},
methods: {
async loadDirector() {
try {
const response = await apiClient.get(`/api/falukant/director/${this.branchId}`);
this.director = Object.keys(response.data).length === 0 || !response.data.director ? null : response.data.director;
} catch (error) {
console.error('Error loading director:', error);
}
},
async saveSetting(settingKey, value) {
if (!this.director) return;
try {
await apiClient.post(`/api/falukant/director/settings`, {
branchId: this.branchId,
directorId: this.director.id,
settingKey,
value,
});
} catch (error) {
console.error(`Error saving setting ${settingKey}:`, error);
}
},
openNewDirectorDialog() {
console.log('openNewDirectorDialog');
this.$refs.newDirectorDialog.open(this.branchId);
},
fireDirector() {
alert(this.$t('falukant.branch.director.fireAlert'));
},
teachDirector() {
alert(this.$t('falukant.branch.director.teachAlert'));
},
},
};
</script>
<style scoped>
.director-info {
border: 1px solid #ccc;
margin: 10px 0;
border-radius: 4px;
padding: 10px;
}
.director-info-container {
display: flex;
}
.director-info-container > div {
width: 100%;
}
</style>

View File

@@ -0,0 +1,182 @@
<template>
<div class="production-section">
<h3>{{ $t('falukant.branch.production.title') }}</h3>
<div v-if="productions && productions.length > 0">
<h4>{{ $t('falukant.branch.production.current') }}</h4>
<table>
<thead>
<tr>
<th>{{ $t('falukant.branch.production.product') }}</th>
<th>{{ $t('falukant.branch.production.quantity') }}</th>
<th>{{ $t('falukant.branch.production.ending') }}</th>
<th>{{ $t('falukant.branch.production.remainingTime') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="production in productions" :key="production.id">
<td>{{ $t(`falukant.product.${production.productType.labelTr}`) }}</td>
<td>{{ production.quantity }}</td>
<td>{{ calculateEndDateTime(production.startTimestamp, production.productType.productionTime) }}</td>
<td>{{ calculateRemainingTime(production.startTimestamp, production.productType.productionTime) }} s</td>
</tr>
</tbody>
</table>
</div>
<div v-if="(!productions || productions.length < 2)">
<div>
<label for="product">{{ $t('falukant.branch.production.selectProduct') }}</label>
<select name="product" id="product" v-model="selectedProduct">
<option v-for="product in products" :key="product.id" :value="product.id">
{{ $t(`falukant.product.${product.labelTr}`) }}
</option>
</select>
</div>
<div>
<label for="quantity">{{ $t('falukant.branch.production.quantity') }}</label>
<input type="number" id="quantity" v-model.number="productionQuantity" min="1" max="200"/>
</div>
<div>
<p>{{ $t('falukant.branch.production.cost') }}:
<strong>{{ calculateProductionCost() }}</strong>
</p>
<p>{{ $t('falukant.branch.production.duration') }}:
<strong>{{ calculateProductionDuration(selectedProduct) }}</strong>
</p>
<p>{{ $t('falukant.branch.production.revenue') }}:
<strong>{{ calculateProductionRevenue() }}</strong>
</p>
</div>
<button @click="startProduction" :disabled="!selectedProduct || productionQuantity < 1">
{{ $t('falukant.branch.production.start') }}
</button>
</div>
</div>
</template>
<script>
import apiClient from '@/utils/axios.js';
export default {
name: "ProductionSection",
props: {
branchId: { type: Number, required: true },
products: { type: Array, required: true },
},
data() {
return {
productions: [],
selectedProduct: null,
productionQuantity: 1,
currentTime: Date.now(),
timer: null,
};
},
async mounted() {
await this.loadProductions();
this.timer = setInterval(() => {
this.currentTime = Date.now();
}, 1000);
},
beforeUnmount() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
},
methods: {
async loadProductions() {
try {
const response = await apiClient.get(`/api/falukant/branches/${this.branchId}`);
this.productions = response.data.productions.sort((a, b) => {
const endTimeA = new Date(a.startTimestamp).getTime() + a.productType.productionTime * 60 * 1000;
const endTimeB = new Date(b.startTimestamp).getTime() + b.productType.productionTime * 60 * 1000;
return endTimeA - endTimeB;
});
} catch (error) {
console.error('Error loading productions:', error);
}
},
calculateProductionCost() {
if (!this.products) return 0;
const product = this.products.find(p => p.id === this.selectedProduct);
return product ? 6 * product.category * this.productionQuantity : 0;
},
calculateProductionDuration(productId) {
if (!this.products || !productId) return 0;
const product = this.products.find(p => p.id === productId);
if (!product) return 0;
const totalMinutes = product.productionTime * 60;
return (totalMinutes / 60).toFixed(2).replace('.', ':');
},
calculateProductionRevenue() {
if (!this.selectedProduct || !this.products) return 0;
const product = this.products.find(p => p.id === this.selectedProduct);
if (!product) return 0;
const revenue = this.calculateProductRevenue(product).absolute * this.productionQuantity;
return revenue.toFixed(2).toLocaleString();
},
calculateEndDateTime(startTimestamp, productionTime) {
const start = new Date(startTimestamp);
const end = new Date(start.getTime() + productionTime * 60 * 1000);
return end.toLocaleString();
},
calculateRemainingTime(startTimestamp, productionTime) {
const start = new Date(startTimestamp).getTime();
const end = start + productionTime * 60 * 1000;
const secondsLeft = Math.max(Math.floor((end - this.currentTime) / 1000), 0);
return secondsLeft;
},
async startProduction() {
if (this.selectedProduct && this.productionQuantity > 0) {
this.productionQuantity = Math.min(this.productionQuantity, 200);
let productionQuantity = this.productionQuantity;
while (productionQuantity > 0) {
const sendQuantitiy = Math.min(productionQuantity, 100);
productionQuantity -= sendQuantitiy;
try {
await apiClient.post(`/api/falukant/production`, {
branchId: this.branchId,
productId: this.selectedProduct,
quantity: sendQuantitiy,
});
this.loadProductions();
} catch (error) {
alert(this.$t(`falukant.branch.production.error${error.response.data.error}`));
}
}
}
},
calculateProductRevenue(product) {
if (!product.knowledges || product.knowledges.length === 0) {
return { absolute: 0, perMinute: 0 };
}
const knowledgeFactor = product.knowledges[0].knowledge || 0;
const maxPrice = product.sellCost;
const minPrice = maxPrice * 0.6;
const revenuePerUnit = minPrice + (maxPrice - minPrice) * (knowledgeFactor / 100);
const perMinute = product.productionTime > 0 ? revenuePerUnit / product.productionTime : 0;
return {
absolute: revenuePerUnit.toFixed(2),
perMinute: perMinute.toFixed(2),
};
},
},
};
</script>
<style scoped>
.production-section {
border: 1px solid #ccc;
margin: 10px 0;
border-radius: 4px;
padding: 10px;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 8px;
border: 1px solid #ddd;
}
</style>

View File

@@ -0,0 +1,98 @@
<template>
<div class="revenue-section">
<h3>
<button @click="toggleRevenueTable">
{{ $t('falukant.branch.revenue.title') }}
{{ isRevenueTableOpen ? '' : '' }}
</button>
</h3>
<div v-if="isRevenueTableOpen" class="revenue-table">
<table>
<thead>
<tr>
<th>{{ $t('falukant.branch.revenue.product') }}</th>
<th>{{ $t('falukant.branch.revenue.knowledge') }}</th>
<th>{{ $t('falukant.branch.revenue.absolute') }}</th>
<th>{{ $t('falukant.branch.revenue.perMinute') }}</th>
<th>{{ $t('falukant.branch.revenue.profitAbsolute') }}</th>
<th>{{ $t('falukant.branch.revenue.profitPerMinute') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="product in products" :key="product.id" :class="{ highlight: product.id === productWithMaxRevenuePerMinute?.id }">
<td>{{ $t(`falukant.product.${product.labelTr}`) }}</td>
<td>{{ product.knowledges && product.knowledges[0] ? product.knowledges[0].knowledge : 0 }}</td>
<td>{{ calculateProductRevenue(product).absolute }}</td>
<td>{{ calculateProductRevenue(product).perMinute }}</td>
<td>{{ calculateProductProfit(product).absolute }}</td>
<td>{{ calculateProductProfit(product).perMinute }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script>
export default {
name: "RevenueSection",
props: {
products: { type: Array, required: true },
calculateProductRevenue: { type: Function, required: true },
calculateProductProfit: { type: Function, required: true },
},
data() {
return {
isRevenueTableOpen: false,
};
},
computed: {
productWithMaxRevenuePerMinute() {
if (!this.products || this.products.length === 0) return null;
return this.products.reduce((maxProduct, currentProduct) => {
const currentRevenue = parseFloat(this.calculateProductRevenue(currentProduct).perMinute);
const maxRevenue = maxProduct ? parseFloat(this.calculateProductRevenue(maxProduct).perMinute) : 0;
return currentRevenue > maxRevenue ? currentProduct : maxProduct;
}, null);
},
},
methods: {
toggleRevenueTable() {
this.isRevenueTableOpen = !this.isRevenueTableOpen;
},
},
};
</script>
<style scoped>
.revenue-section {
border: 1px solid #ccc;
margin: 10px 0;
border-radius: 4px;
padding: 10px;
}
.revenue-section button {
background: none;
border: none;
color: #007bff;
cursor: pointer;
font-size: 1em;
text-decoration: underline;
}
.revenue-table {
margin-top: 10px;
overflow-x: auto;
}
.revenue-table table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 8px;
border: 1px solid #ddd;
}
.highlight {
background-color: #dfffd6;
}
</style>

View File

@@ -0,0 +1,101 @@
<template>
<div class="sale-section">
<h3>{{ $t('falukant.branch.sale.title') }}</h3>
<!-- Beispielhafte Inventar-Tabelle -->
<div v-if="inventory.length > 0" class="inventory-table">
<table>
<thead>
<tr>
<th>{{ $t('falukant.branch.sale.region') }}</th>
<th>{{ $t('falukant.branch.sale.product') }}</th>
<th>{{ $t('falukant.branch.sale.quality') }}</th>
<th>{{ $t('falukant.branch.sale.quantity') }}</th>
<th>{{ $t('falukant.branch.sale.sell') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in inventory" :key="`${item.region.id}-${item.product.id}-${item.quality}`">
<td>{{ item.region.name }}</td>
<td>{{ $t(`falukant.product.${item.product.labelTr}`) }}</td>
<td>{{ item.quality }}</td>
<td>{{ item.totalQuantity }}</td>
<td>
<input type="number" v-model.number="item.sellQuantity" :min="1" :max="item.totalQuantity" />
<button @click="sellItem(index)">{{ $t('falukant.branch.sale.sellButton') }}</button>
</td>
</tr>
</tbody>
</table>
<button @click="sellAll">{{ $t('falukant.branch.sale.sellAllButton') }}</button>
</div>
<div v-else>
<p>{{ $t('falukant.branch.sale.noInventory') }}</p>
</div>
</div>
</template>
<script>
import apiClient from '@/utils/axios.js';
export default {
name: "SaleSection",
props: { branchId: { type: Number, required: true } },
data() {
return {
inventory: [],
};
},
async mounted() {
await this.loadInventory();
},
methods: {
async loadInventory() {
try {
const response = await apiClient.get(`/api/falukant/inventory/${this.branchId}`);
this.inventory = response.data.map(item => ({
...item,
sellQuantity: item.totalQuantity,
}));
} catch (error) {
console.error('Error loading inventory:', error);
}
},
sellItem(index) {
const item = this.inventory[index];
const quantityToSell = item.sellQuantity || item.totalQuantity;
apiClient.post(`/api/falukant/sell`, {
branchId: this.branchId,
productId: item.product.id,
quantity: quantityToSell,
quality: item.quality,
}).catch(() => {
alert(this.$t('falukant.branch.sale.sellError'));
});
},
sellAll() {
apiClient.post(`/api/falukant/sell/all`, { branchId: this.branchId })
.catch(() => {
alert(this.$t('falukant.branch.sale.sellAllError'));
});
},
},
};
</script>
<style scoped>
.sale-section {
border: 1px solid #ccc;
margin: 10px 0;
border-radius: 4px;
padding: 10px;
}
.inventory-table table {
width: 100%;
border-collapse: collapse;
}
.inventory-table th,
.inventory-table td {
padding: 2px 3px;
border: 1px solid #ddd;
}
</style>

View File

@@ -5,11 +5,16 @@
<span class="status-icon">{{ item.icon }}: {{ item.value }}</span>
</div>
</template>
<span v-if="statusItems.length > 0">
<template v-for="(menuItem, key) in menu.falukant.children" :key="menuItem.id" >
<img :src="'/images/icons/falukant/' + key + '.jpg'" class="menu-icon" @click="openPage(menuItem)" :title="$t(`navigation.m-falukant.${key}`)" />
</template>
</span>
</div>
</template>
<script>
import { mapState } from "vuex";
import { mapState, mapGetters } from "vuex";
import apiClient from "@/utils/axios.js";
export default {
@@ -25,18 +30,25 @@ export default {
};
},
computed: {
...mapState(["socket"]),
...mapState(["socket", "daemonSocket"]),
...mapGetters(['menu']),
},
async mounted() {
await this.fetchStatus();
if (this.socket) {
this.socket.on("falukantUpdateStatus", this.fetchStatus);
}
if (this.daemonSocket) {
this.daemonSocket.addEventListener("message", this.handleDaemonSocketMessage);
}
},
beforeUnmount() {
if (this.socket) {
this.socket.off("falukantUpdateStatus", this.fetchStatus);
}
if (this.daemonSocket) {
this.daemonSocket.removeEventListener("message", this.handleDaemonSocketMessage);
}
},
methods: {
async fetchStatus() {
@@ -66,6 +78,24 @@ export default {
console.error("Error fetching status:", error);
}
},
async handleDaemonSocketMessage(event) {
try {
const data = JSON.parse(event.data);
if (data.event === "falukantUpdateStatus") {
this.fetchStatus();
}
} catch (error) {
console.error("Error parsing daemonSocket message:", error);
}
},
openPage(url, hasSubmenu = false) {
if (hasSubmenu) {
return;
}
if (url) {
this.$router.push(url);
}
},
},
};
</script>
@@ -92,4 +122,11 @@ export default {
.status-icon {
font-size: 14px;
}
.menu-icon {
width: 30px;
height: 30px;
cursor: pointer;
padding: 4px 2px 0 0;
}
</style>

View File

@@ -0,0 +1,220 @@
<template>
<div class="storage-section">
<h3>{{ $t('falukant.branch.storage.title') }}</h3>
<div class="storage-info">
<p>
{{ $t('falukant.branch.storage.currentCapacity') }}:
<strong>{{ currentStorage }} / {{ maxStorage }}</strong>
</p>
<table>
<thead>
<tr>
<th>{{ $t('falukant.branch.storage.stockType') }}</th>
<th>{{ $t('falukant.branch.storage.totalCapacity') }}</th>
<th>{{ $t('falukant.branch.storage.used') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(usage, idx) in storageUsage" :key="idx">
<td>{{ $t(`falukant.branch.stocktype.${usage.stockTypeLabelTr}`) }}</td>
<td>{{ usage.totalCapacity }}</td>
<td>{{ usage.used }}</td>
</tr>
</tbody>
</table>
<h4>{{ $t('falukant.branch.storage.availableToBuy') }}</h4>
<table>
<thead>
<tr>
<th>{{ $t('falukant.branch.storage.stockType') }}</th>
<th>{{ $t('falukant.branch.storage.totalCapacity') }}</th>
</tr>
</thead>
<tbody>
<!-- Hier zeigen wir die gruppierten (nach Label) Einträge an -->
<tr v-for="(group, i) in buyableUsage" :key="i">
<td>{{ $t(`falukant.branch.stocktype.${group.stockTypeLabelTr}`) }}</td>
<td>{{ group.totalQuantity }}</td>
</tr>
</tbody>
</table>
</div>
<div class="storage-market">
<div class="buy-section">
<label>{{ $t('falukant.branch.storage.selectStockType') }}</label>
<!-- Auswahl basiert jetzt auf dem Label -->
<select v-model="selectedBuyStockTypeLabelTr">
<option v-for="group in buyableUsage" :key="group.stockTypeLabelTr" :value="group.stockTypeLabelTr">
{{ $t(`falukant.branch.stocktype.${group.stockTypeLabelTr}`) }} - {{ getCostOfType(group.stockTypeLabelTr) }}
</option>
</select>
<div>
<label>{{ $t('falukant.branch.storage.buyAmount') }}</label>
<input type="number" v-model.number="buyStorageAmount" :max="maxBuyableForSelectedBuy" min="1" />
<button @click="onBuyStorage">
{{ $t('falukant.branch.storage.buyStorageButton') }} ({{ buyCost }})
</button>
</div>
</div>
<div class="sell-section">
<label>{{ $t('falukant.branch.storage.selectStockType') }}</label>
<select v-model="selectedSellStockTypeId">
<option v-for="type in storageUsage" :key="type.stockTypeId" :value="type.stockTypeId">
{{ $t(`falukant.branch.stocktype.${type.stockTypeLabelTr}`) }} - {{ getCostOfTypeById(type.stockTypeId) }}
</option>
</select>
<div>
<label>{{ $t('falukant.branch.storage.sellAmount') }}</label>
<input type="number" v-model.number="sellStorageAmount" :max="maxSellableForSelectedSell" min="1" />
<button @click="onSellStorage">
{{ $t('falukant.branch.storage.sellStorageButton') }} ({{ sellIncome }})
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import apiClient from '@/utils/axios.js';
export default {
name: "StorageSection",
props: { branchId: { type: Number, required: true } },
data() {
return {
currentStorage: 0,
maxStorage: 0,
storageUsage: [],
buyableUsage: [],
buyStorageAmount: 0,
sellStorageAmount: 0,
stockTypes: [],
selectedBuyStockTypeLabelTr: null,
selectedSellStockTypeId: null,
};
},
computed: {
buyCost() {
const cost = this.getCostOfType(this.selectedBuyStockTypeLabelTr);
return this.buyStorageAmount * cost;
},
sellIncome() {
const cost = this.getCostOfTypeById(this.selectedSellStockTypeId);
return this.sellStorageAmount * cost;
},
maxBuyableForSelectedBuy() {
const group = this.buyableUsage.find(g => g.stockTypeLabelTr === this.selectedBuyStockTypeLabelTr);
return group ? group.totalQuantity : 0;
},
maxSellableForSelectedSell() {
const usage = this.storageUsage.find(u => u.stockTypeId === this.selectedSellStockTypeId);
return usage ? usage.totalCapacity : 0;
},
},
async mounted() {
await this.loadStorageData();
await this.loadStockTypes();
},
methods: {
async loadStorageData() {
try {
const { data } = await apiClient.get(`/api/falukant/storage/${this.branchId}`);
this.currentStorage = data.totalUsedCapacity;
this.maxStorage = data.maxCapacity;
this.storageUsage = data.usageByType;
const filteredBuyable = data.buyableByType.filter(item => item.quantity > 0);
const grouped = {};
filteredBuyable.forEach(item => {
const key = item.stockTypeLabelTr;
if (!grouped[key]) {
grouped[key] = { stockTypeLabelTr: key, totalQuantity: 0, items: [] };
}
grouped[key].totalQuantity += item.quantity;
grouped[key].items.push({ stockTypeId: item.stockTypeId, quantity: item.quantity });
});
this.buyableUsage = Object.values(grouped);
} catch (error) {
console.error('Error loading storage data:', error);
}
},
async loadStockTypes() {
try {
const response = await apiClient.get('/api/falukant/stocktypes');
this.stockTypes = response.data;
if (this.stockTypes.length) {
this.selectedBuyStockTypeLabelTr = this.stockTypes[0].labelTr;
this.selectedSellStockTypeId = this.stockTypes[0].id;
}
} catch (error) {
console.error('Error loading stock types:', error);
}
},
async onBuyStorage() {
if (!this.branchId || !this.buyStorageAmount || !this.selectedBuyStockTypeLabelTr) return;
const group = this.buyableUsage.find(g => g.stockTypeLabelTr === this.selectedBuyStockTypeLabelTr);
if (!group) return;
let remainingAmount = this.buyStorageAmount;
for (const item of group.items) {
if (remainingAmount <= 0) break;
const toBuy = Math.min(remainingAmount, item.quantity);
try {
await apiClient.post(`/api/falukant/storage`, {
branchId: this.branchId,
amount: toBuy,
stockTypeId: item.stockTypeId
});
} catch (err) {
console.error(err);
alert('Error buying storage for one part of the order');
}
remainingAmount -= toBuy;
}
if (remainingAmount > 0) {
alert(this.$t('falukant.branch.storage.notEnoughAvailable'));
}
this.loadStorageData();
},
onSellStorage() {
if (!this.branchId || !this.sellStorageAmount || !this.selectedSellStockTypeId) return;
apiClient.delete(`/api/falukant/storage`, {
data: {
branchId: this.branchId,
amount: this.sellStorageAmount,
stockTypeId: this.selectedSellStockTypeId
}
})
.then(() => this.loadStorageData())
.catch(err => {
console.error(err);
alert('Error selling storage');
});
},
getCostOfType(labelTr) {
const st = this.stockTypes.find(s => s.labelTr === labelTr);
return st ? st.cost : 0;
},
getCostOfTypeById(stockTypeId) {
const st = this.stockTypes.find(s => s.id === stockTypeId);
return st ? st.cost : 0;
},
},
};
</script>
<style scoped>
.storage-section {
border: 1px solid #ccc;
margin: 10px 0;
border-radius: 4px;
padding: 10px;
}
.storage-info table, .storage-market table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 8px;
border: 1px solid #ddd;
}
</style>