Füge neue Modelle für Produktion, Inventar und kaufbare Bestände hinzu; aktualisiere bestehende Modelle und Routen

This commit is contained in:
Torsten Schulz
2024-12-23 10:37:43 +01:00
parent 1bb2bd49d5
commit 6f7d97672e
15 changed files with 1143 additions and 44 deletions

View File

@@ -31,26 +31,150 @@
<div class="sale-section">
<h3>{{ $t('falukant.branch.sale.title') }}</h3>
<p>{{ $t('falukant.branch.sale.info') }}</p>
<div class="inventory-table" v-if="inventory.length > 0">
<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 }} ({{ $t(`falukant.regionType.${item.region.regionType.labelTr}`)
}})</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" :placeholder="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>
<!-- Production Section -->
<div class="production-section">
<div class="production-section" v-if="['fullstack', 'production'].includes(branchData?.branchType?.labelTr)">
<h3>{{ $t('falukant.branch.production.title') }}</h3>
<p>{{ $t('falukant.branch.production.info') }}</p>
<div v-if="this.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) }}</td>
</tr>
</tbody>
</table>
</div>
<template v-if="!this.productions || this.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" />
</div>
<div>
<p>
{{ $t('falukant.branch.production.cost') }}:
<strong>{{ calculateProductionCost() }}</strong>
</p>
<p>
{{ $t('falukant.branch.production.duration') }}:
<strong>{{ calculateProductionDuration(selectedProduct?.id) }}</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>
</template>
</div>
<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>
</tr>
</thead>
<tbody>
<tr v-for="product in products" :key="product.id">
<td>{{ $t(`falukant.product.${product.labelTr}`) }}</td>
<td>{{ product.knowledges[0]?.knowledge }} %</td>
<td>{{ calculateProductRevenue(product).absolute }}</td>
<td>{{ calculateProductRevenue(product).perMinute }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<MessageDialog ref="messageDialog" />
<ErrorDialog ref="errorDialog" />
</template>
<script>
import StatusBar from '@/components/falukant/StatusBar.vue';
import FormattedDropdown from '@/components/form/FormattedDropdown.vue';
import apiClient from '@/utils/axios.js';
import MessageDialog from '@/dialogues/standard/MessageDialog.vue';
import ErrorDialog from '@/dialogues/standard/ErrorDialog.vue';
import { mapState } from "vuex";
export default {
name: "BranchView",
components: {
StatusBar,
FormattedDropdown,
MessageDialog,
ErrorDialog,
},
computed: {
...mapState(["socket", "daemonSocket"]),
},
data() {
return {
@@ -60,19 +184,64 @@ export default {
{ field: "cityName", label: this.$t('falukant.branch.columns.city') },
{ field: "type", label: this.$t('falukant.branch.columns.type') },
],
products: [],
branchData: null,
selectedProduct: null,
productionQuantity: 1,
isRevenueTableOpen: false,
inventory: [],
currentTime: Date.now(),
timer: null,
};
},
async mounted() {
await this.loadBranches();
const branchId = this.$route.params.branchId;
console.log('route params:', this.$route.params, branchId);
const products = await apiClient.get('/api/falukant/products');
this.products = products.data;
console.log('products loaded');
if (branchId) {
console.log('branch selected');
this.selectedBranch = this.branches.find(branch => branch.id === parseInt(branchId)) || null;
} else {
console.log('main branch selected');
this.selectMainBranch();
}
if (this.socket) {
this.socket.on("falukantBranchUpdate", this.fetchBranch);
}
if (this.daemonSocket) {
this.daemonSocket.addEventListener('message', (event) => {
try {
if (event.data === "ping") {
return;
}
const message = JSON.parse(event.data);
console.log('Daemon WebSocket message received in BranchView:', message);
if (message.event === 'production_ready' && message.branch_id === this.selectedBranch?.id) {
this.handleProductionReadyEvent(message);
}
} catch (error) {
console.error('Error processing WebSocket message in BranchView:', error);
}
});
} else {
console.error('Daemon socket is not initialized.');
}
this.startTimer();
},
beforeUnmount() {
this.stopTimer();
if (this.socket) {
this.socket.off("falukantBranchUpdate", this.fetchBranch);
}
if (this.daemonSocket) {
this.daemonSocket.onmessage = null;
}
},
watch: {
async selectedBranch(newBranch) {
await this.loadBranchData(newBranch);
}
},
methods: {
async loadBranches() {
@@ -84,7 +253,6 @@ export default {
type: this.$t(`falukant.branch.types.${branch.branchType.labelTr}`),
isMainBranch: branch.isMainBranch,
}));
// Wenn keine selectedBranch gesetzt ist, versuche die Main Branch zu wählen
if (!this.selectedBranch) {
this.selectMainBranch();
}
@@ -92,11 +260,16 @@ export default {
console.error('Error loading branches:', error);
}
},
async loadBranchData(newBranch) {
if (newBranch) {
this.getBranchData(newBranch);
await this.loadInventory();
}
},
selectMainBranch() {
const main = this.branches.find(b => b.isMainBranch) || null;
if (main !== this.selectedBranch) {
this.selectedBranch = main;
console.log("Main branch selected:", this.selectedBranch);
}
},
createBranch() {
@@ -109,6 +282,163 @@ export default {
);
}
},
calculateProductionCost() {
if (!this.products) {
return 0;
}
const product = this.products.find(p => p.id === this.selectedProduct);
if (product) {
return 7 * product.category * this.productionQuantity;
}
return 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;
const decimalTime = (totalMinutes / 60).toFixed(2);
return decimalTime;
},
async startProduction() {
if (this.selectedBranch && this.selectedProduct && this.productionQuantity > 0) {
try {
await apiClient.post(`/api/falukant/production`, {
branchId: this.selectedBranch.id,
productId: this.selectedProduct,
quantity: this.productionQuantity,
});
this.$root.$refs.messageDialog.open(this.$t('falukant.branch.production.success'));
} catch (error) {
this.$root.$refs.errorDialog.open(this.$t(`falukant.branch.production.error${error.response.data.error}`));
}
}
},
calculateProductionRevenue() {
if (!this.selectedProduct) {
return 0;
}
if (!this.products) {
return 0;
}
const product = this.products.find(p => p.id === this.selectedProduct);
if (!product) {
return 0;
}
const productRevenue = this.calculateProductRevenue(product);
return productRevenue.absolute;
},
toggleRevenueTable() {
this.isRevenueTableOpen = !this.isRevenueTableOpen;
},
calculateProductRevenue(product) {
if (!product.knowledges || product.knowledges.length === 0) {
return { absolute: 0, perMinute: 0 };
}
const knowledgeFactor = product.knowledges[0]?.knowledge || 0;
const maxPrice = product.category * 9;
const minPrice = maxPrice * 0.6;
const revenuePerUnit = minPrice + (maxPrice - minPrice) * (knowledgeFactor / 100);
const productionTimeInMinutes = product.productionTime;
const perMinute = (revenuePerUnit / productionTimeInMinutes).toFixed(2);
return {
absolute: revenuePerUnit.toFixed(2),
perMinute: perMinute,
};
},
async fetchBranch(data) {
try {
if (data.branchId === this.selectedBranch.id) {
this.getBranchData();
}
} catch (error) {
console.error(error);
}
},
async getBranchData(newBranch = null) {
try {
const response = await apiClient.get(`/api/falukant/branches/${newBranch ? newBranch.id : this.selectedBranch.id}`);
this.branchData = response.data;
this.director = response.data.director;
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);
}
},
calculateEndDateTime(startTimestamp, productionTime) {
const startTime = new Date(startTimestamp);
const endTime = new Date(startTime.getTime() + productionTime * 60 * 1000);
return endTime.toLocaleString(); // Datum und Uhrzeit
},
calculateRemainingTime(startTimestamp, productionTime) {
const startTime = new Date(startTimestamp).getTime();
const endTime = startTime + productionTime * 60 * 1000;
const now = Date.now();
const remainingSeconds = Math.max(Math.floor((endTime - now) / 1000), 0);
return remainingSeconds; // Verbleibende Zeit in Sekunden
},
startTimer() {
this.timer = setInterval(() => {
this.$forceUpdate(); // Aktualisiert die verbleibende Zeit in der Tabelle
}, 1000);
},
stopTimer() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
},
sellItem(index) {
const item = this.inventory[index];
console.log(`Selling ${item.sellQuantity || item.totalQuantity} of ${item.product.labelTr}`);
const quantityToSell = item.sellQuantity || item.totalQuantity;
item.totalQuantity -= quantityToSell;
if (item.totalQuantity <= 0) {
this.inventory.splice(index, 1);
}
item.sellQuantity = null;
},
sellAll() {
console.log('Selling all items in inventory');
this.inventory.forEach(item => {
console.log(`Selling ${item.totalQuantity} of ${item.product.labelTr}`);
});
this.inventory = []; // Leert das Inventar
},
async loadInventory() {
try {
const response = await apiClient.get(`/api/falukant/inventory/${this.selectedBranch?.id}`, {});
this.inventory = response.data.map(item => ({
...item,
sellQuantity: item.totalQuantity, // Voreinstellung
}));
} catch (error) {
console.error('Error loading inventory:', error);
this.$refs.errorDialog.open(this.$t('falukant.branch.sale.loadError'));
}
},
async handleProductionReadyEvent(message) {
try {
console.log('Production ready event received:', message);
if (message.branch_id === this.selectedBranch?.id) {
await this.loadBranchData(this.selectedBranch);
await this.loadInventory();
}
} catch (error) {
console.error('Error processing production_ready event:', error);
console.error(message);
}
},
},
};
</script>
@@ -127,4 +457,110 @@ export default {
button {
margin: 5px;
}
.production-section {
width: auto;
}
.production-section label {
display: block;
margin-bottom: 5px;
}
.production-section input[type="number"] {
width: 100px;
}
.revenue-section {
border: 1px solid #ccc;
margin: 10px 0;
border-radius: 4px;
padding: 10px;
}
.revenue-section h3 {
margin: 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.revenue-section button {
background: none;
border: none;
color: #007bff;
cursor: pointer;
font-size: 1em;
padding: 0;
margin: 0;
text-decoration: underline;
}
.revenue-section button:hover {
color: #0056b3;
}
.revenue-table {
margin-top: 10px;
overflow-x: auto;
}
.revenue-table table {
width: 100%;
border-collapse: collapse;
}
.revenue-table th,
.revenue-table td {
text-align: left;
padding: 8px;
border: 1px solid #ddd;
}
.revenue-table th {
background-color: #f2f2f2;
}
.production-section table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
.production-section th,
.production-section td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
.production-section th {
background-color: #f2f2f2;
}
.inventory-table {
margin-top: 10px;
overflow-x: auto;
}
.inventory-table table {
width: 100%;
border-collapse: collapse;
}
.inventory-table th,
.inventory-table td {
text-align: left;
padding: 2px 3px;
border: 1px solid #ddd;
}
.inventory-table td button {
margin: 0 0 0 5px;
padding: 2px;
}
.inventory-table th {
background-color: #f2f2f2;
}
</style>