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

@@ -7,6 +7,8 @@ class FalukantController {
this.randomFirstName = this.randomFirstName.bind(this); this.randomFirstName = this.randomFirstName.bind(this);
this.randomLastName = this.randomLastName.bind(this); this.randomLastName = this.randomLastName.bind(this);
this.getInfo = this.getInfo.bind(this); this.getInfo = this.getInfo.bind(this);
this.getInventory = this.getInventory.bind(this);
this.sellProduct = this.sellProduct.bind(this);
} }
async getUser(req, res) { async getUser(req, res) {
@@ -69,6 +71,94 @@ class FalukantController {
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
} }
} }
async getBranch(req, res) {
try {
const { userid: hashedUserId } = req.headers;
const { branch: branchId } = req.params;
console.log(branchId, req.params);
const result = await FalukantService.getBranch(hashedUserId, branchId);
res.status(200).json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
async createProduction(req, res) {
try {
const { userid: hashedUserId } = req.headers;
const { branchId, productId, quantity } = req.body;
const result = await FalukantService.createProduction(hashedUserId, branchId, productId, quantity);
res.status(201).json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
async getProduction(req, res) {
try {
const { userid: hashedUserId } = req.headers;
const { branchId } = req.params;
const result = await FalukantService.getProduction(hashedUserId, branchId);
res.status(200).json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
async getStock(req, res) {
try {
const { userid: hashedUserId } = req.headers;
const { branchId } = req.params || null;
const result = await FalukantService.getStock(hashedUserId, branchId);
res.status(200).json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
async createStock(req, res) {
try {
const { userid: hashedUserId } = req.headers;
const { branchId, stockTypeId, stockSize } = req.body;
const result = await FalukantService.createStock(hashedUserId, branchId, stockTypeId, stockSize);
res.status(201).json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
async getProducts(req, res) {
try {
const { userid: hashedUserId } = req.headers;
const result = await FalukantService.getProducts(hashedUserId);
res.status(200).json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
async getInventory(req, res) {
try {
const { userid: hashedUserId } = req.headers;
const { branchId } = req.params;
const result = await FalukantService.getInventory(hashedUserId, branchId);
res.status(200).json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
async sellProduct(req, res) {
try {
const { userid: hashedUserId } = req.headers;
const { branchId, productId, quality, quantity } = req.body;
const result = await FalukantService.sellProduct(hashedUserId, branchId, productId, quality, quantity);
res.status(201).json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
} }
export default FalukantController; export default FalukantController;

View File

@@ -42,6 +42,9 @@ import TitleOfNobility from './falukant/type/title_of_nobility.js';
import TitleRequirement from './falukant/type/title_requirement.js'; import TitleRequirement from './falukant/type/title_requirement.js';
import Branch from './falukant/data/branch.js'; import Branch from './falukant/data/branch.js';
import BranchType from './falukant/type/branch.js'; import BranchType from './falukant/type/branch.js';
import Production from './falukant/data/production.js';
import Inventory from './falukant/data/inventory.js';
import BuyableStock from './falukant/data/buayble_stock.js';
export default function setupAssociations() { export default function setupAssociations() {
@@ -212,14 +215,8 @@ export default function setupAssociations() {
FalukantStock.belongsTo(FalukantStockType, { foreignKey: 'stockTypeId', as: 'stockType' }); FalukantStock.belongsTo(FalukantStockType, { foreignKey: 'stockTypeId', as: 'stockType' });
FalukantStockType.hasMany(FalukantStock, { foreignKey: 'stockTypeId', as: 'stocks' }); FalukantStockType.hasMany(FalukantStock, { foreignKey: 'stockTypeId', as: 'stocks' });
FalukantStock.belongsTo(FalukantUser, { foreignKey: 'userId', as: 'user' }); Knowledge.belongsTo(ProductType, { foreignKey: 'productId', as: 'productType' });
FalukantUser.hasMany(FalukantStock, { foreignKey: 'userId', as: 'stocks' }); ProductType.hasMany(Knowledge, { foreignKey: 'productId', as: 'knowledges' });
FalukantStock.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' });
RegionData.hasMany(FalukantStock, { foreignKey: 'regionId', as: 'stocksInRegion' });
Knowledge.belongsTo(ProductType, { foreignKey: 'productTypeId', as: 'productType' });
ProductType.hasMany(Knowledge, { foreignKey: 'productTypeId', as: 'knowledges' });
Knowledge.belongsTo(FalukantCharacter, { foreignKey: 'characterId', as: 'character' }); Knowledge.belongsTo(FalukantCharacter, { foreignKey: 'characterId', as: 'character' });
FalukantCharacter.hasMany(Knowledge, { foreignKey: 'characterId', as: 'knowledges' }); FalukantCharacter.hasMany(Knowledge, { foreignKey: 'characterId', as: 'knowledges' });
@@ -235,4 +232,23 @@ export default function setupAssociations() {
Branch.belongsTo(BranchType, { foreignKey: 'branchTypeId', as: 'branchType' }); Branch.belongsTo(BranchType, { foreignKey: 'branchTypeId', as: 'branchType' });
BranchType.hasMany(Branch, { foreignKey: 'branchTypeId', as: 'branches' }); BranchType.hasMany(Branch, { foreignKey: 'branchTypeId', as: 'branches' });
Production.belongsTo(Branch, { foreignKey: 'branchId', as: 'branch' });
Branch.hasMany(Production, { foreignKey: 'branchId', as: 'productions' });
Production.belongsTo(ProductType, { foreignKey: 'productId', as: 'productType' });
ProductType.hasMany(Production, { foreignKey: 'productId', as: 'productions' });
Inventory.belongsTo(FalukantStock, { foreignKey: 'stockId', as: 'stock' });
FalukantStock.hasMany(Inventory, { foreignKey: 'stockId', as: 'inventories' });
Inventory.belongsTo(ProductType, { foreignKey: 'productId', as: 'productType' });
ProductType.hasMany(Inventory, { foreignKey: 'productId', as: 'inventories' });
BuyableStock.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' });
RegionData.hasMany(BuyableStock, { foreignKey: 'regionId', as: 'buyableStocks' });
Branch.hasMany(FalukantStock, { foreignKey: 'branchId', as: 'stocks' });
FalukantStock.belongsTo(Branch, { foreignKey: 'branchId', as: 'branch' });
} }

View File

@@ -0,0 +1,28 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class BuyableStock extends Model { }
BuyableStock.init({
regionId: {
type: DataTypes.INTEGER,
allowNull: false,
},
stockTypeId: {
type: DataTypes.INTEGER,
allowNull: false,
},
quantity: {
type: DataTypes.INTEGER,
allowNull: false,
},
}, {
sequelize,
modelName: 'BuyableStock',
tableName: 'buyable_stock',
schema: 'falukant_data',
timestamps: false,
underscored: true,
});
export default BuyableStock;

View File

@@ -0,0 +1,37 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class Inventory extends Model { }
Inventory.init({
stockId: {
type: DataTypes.INTEGER,
allowNull: false,
},
productId: {
type: DataTypes.INTEGER,
allowNull: false,
},
quantity: {
type: DataTypes.INTEGER,
allowNull: false,
},
quality: {
type: DataTypes.INTEGER,
allowNull: false,
},
producedAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
}
}, {
sequelize,
modelName: 'Inventory',
tableName: 'inventory',
schema: 'falukant_data',
timestamps: false,
underscored: true,
});
export default Inventory;

View File

@@ -0,0 +1,33 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class Production extends Model { }
Production.init({
branchId: {
type: DataTypes.INTEGER,
allowNull: false,
},
productId: {
type: DataTypes.INTEGER,
allowNull: false,
},
quantity: {
type: DataTypes.INTEGER,
allowNull: false,
},
startTimestamp: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
}
}, {
sequelize,
modelName: 'Production',
tableName: 'production',
schema: 'falukant_data',
timestamps: false,
underscored: true,
});
export default Production;

View File

@@ -4,13 +4,10 @@ import { sequelize } from '../../../utils/sequelize.js';
class FalukantStock extends Model { } class FalukantStock extends Model { }
FalukantStock.init({ FalukantStock.init({
userId: { branchId: {
type: DataTypes.INTEGER,
allowNull: false,
},
regionId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false,
defaultValue: 0
}, },
stockTypeId: { stockTypeId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,

View File

@@ -37,6 +37,11 @@ FalukantUser.init({
allowNull: false, allowNull: false,
defaultValue: 0.00, defaultValue: 0.00,
}, },
certificate: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 1,
},
mainBranchRegionId: { mainBranchRegionId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: true, allowNull: true,

View File

@@ -46,6 +46,9 @@ import TitleRequirement from './falukant/type/title_requirement.js';
import TitleOfNobility from './falukant/type/title_of_nobility.js'; import TitleOfNobility from './falukant/type/title_of_nobility.js';
import BranchType from './falukant/type/branch.js'; import BranchType from './falukant/type/branch.js';
import Branch from './falukant/data/branch.js'; import Branch from './falukant/data/branch.js';
import Production from './falukant/data/production.js';
import Inventory from './falukant/data/inventory.js';
import BuyableStock from './falukant/data/buayble_stock.js';
const models = { const models = {
SettingsType, SettingsType,
@@ -96,6 +99,9 @@ const models = {
TitleRequirement, TitleRequirement,
BranchType, BranchType,
Branch, Branch,
Production,
Inventory,
BuyableStock,
}; };
export default models; export default models;

View File

@@ -9,5 +9,13 @@ router.post('/user', falukantController.createUser);
router.get('/name/randomfirstname/:gender', falukantController.randomFirstName); router.get('/name/randomfirstname/:gender', falukantController.randomFirstName);
router.get('/name/randomlastname', falukantController.randomLastName); router.get('/name/randomlastname', falukantController.randomLastName);
router.get('/info', falukantController.getInfo); router.get('/info', falukantController.getInfo);
router.get('/branches/:branch', falukantController.getBranch);
router.get('/branches', falukantController.getBranches); router.get('/branches', falukantController.getBranches);
router.post('/production', falukantController.createProduction);
router.get('/production/:branchId', falukantController.getProduction);
router.get('/stock/?:branchId', falukantController.getStock);
router.post('/stock', falukantController.createStock);
router.get('/products', falukantController.getProducts);
router.get('/inventory/?:branchId', falukantController.getInventory);
router.post('/sell', falukantController.sellProduct);
export default router; export default router;

View File

@@ -14,6 +14,12 @@ import { differenceInDays } from 'date-fns';
import TitleOfNobility from '../models/falukant/type/title_of_nobility.js'; import TitleOfNobility from '../models/falukant/type/title_of_nobility.js';
import Branch from '../models/falukant/data/branch.js'; import Branch from '../models/falukant/data/branch.js';
import BranchType from '../models/falukant/type/branch.js'; import BranchType from '../models/falukant/type/branch.js';
import Production from '../models/falukant/data/production.js';
import ProductType from '../models/falukant/type/product.js';
import { Op, fn, col } from 'sequelize';
import Knowledge from '../models/falukant/data/product_knowledge.js';
import Inventory from '../models/falukant/data/inventory.js';
import Stock from '../models/falukant/data/stock.js';
class FalukantService extends BaseService { class FalukantService extends BaseService {
async getFalukantUserByHashedId(hashedId) { async getFalukantUserByHashedId(hashedId) {
@@ -279,6 +285,292 @@ class FalukantService extends BaseService {
} }
} }
async getBranch(hashedUserId, branchId) {
try {
const falukantUser = await this.getFalukantUserByHashedId(hashedUserId);
if (!falukantUser) {
throw new Error('User not found');
}
const branch = await Branch.findOne({
where: { id: branchId, falukantUserId: falukantUser.id },
include: [
{
model: BranchType,
as: 'branchType',
attributes: ['labelTr'],
},
{
model: RegionData,
as: 'region',
attributes: ['name'],
},
{
model: Production,
as: 'productions',
attributes: ['quantity', 'startTimestamp'],
include: [
{
model: ProductType,
as: 'productType',
attributes: ['id', 'category', 'labelTr', 'sellCost', 'productionTime'],
}
]
}
],
attributes: ['id', 'regionId'],
});
if (!branch) {
throw new Error('Branch not found');
}
return branch;
} catch (error) {
console.error('Error in getBranch:', error);
throw new Error('Failed to retrieve branch');
}
}
async getStock(hashedUserId, branchId) {
try {
const falukantUser = await this.getFalukantUserByHashedId(hashedUserId);
if (!falukantUser) {
throw new Error('User not found');
}
const branch = await Branch.findOne({ where: { id: branchId, falukantUserId: falukantUser.id } });
if (!branch) {
throw new Error('Branch not found');
}
const stock = await FalukantStock.findOne({ where: { regionId: branch.regionId, userId: falukantUser.id } });
return stock;
} catch (error) {
console.error('Error in getStock:', error);
throw new Error('Failed to retrieve stock');
}
}
async createStock(hashedUserId, branchId, stockData) {
try {
const falukantUser = await this.getFalukantUserByHashedId(hashedUserId);
if (!falukantUser) {
throw new Error('User not found');
}
const branch = await Branch.findOne({ where: { id: branchId, falukantUserId: falukantUser.id } });
if (!branch) {
throw new Error('Branch not found');
}
const stock = await FalukantStock.create({
userId: falukantUser.id,
regionId: branch.regionId,
stockTypeId: stockData.stockTypeId,
quantity: stockData.quantity,
});
return stock;
} catch (error) {
console.error('Error in createStock:', error);
throw new Error('Failed to create stock');
}
}
async createProduction(hashedUserId, branchId, productId, quantity) {
const falukantUser = await this.getFalukantUserByHashedId(hashedUserId);
if (!falukantUser) {
throw new Error('User not found');
}
const branch = await Branch.findOne({ where: { id: branchId, falukantUserId: falukantUser.id } });
if (!branch) {
throw new Error('Branch not found');
}
const product = await ProductType.findOne({ where: { id: productId } });
if (falukantUser.money < quantity * product.category * 7) {
throw new Error('notenoughmoney');
}
const production = await Production.create({
branchId: branch.id,
productId: productId,
quantity: quantity,
});
falukantUser.update({ money: falukantUser.money - quantity * product.category * 7 });
notifyUser(falukantUser.user.hashedId, 'falukantUpdateStatus', {});
notifyUser(falukantUser.user.hashedId, 'falukantBranchUpdate', { branchId: branch.id });
return production;
}
async getProduction(hashedUserId, branchId) {
try {
const falukantUser = await this.getFalukantUserByHashedId(hashedUserId);
if (!falukantUser) {
throw new Error('User not found');
}
const branch = await Branch.findOne({ where: { id: branchId, falukantUserId: falukantUser.id } });
if (!branch) {
throw new Error('Branch not found');
}
const production = await FalukantProduction.findOne({ where: { regionId: branch.regionId } });
return production;
} catch (error) {
console.error('Error in getProduction:', error);
throw new Error('Failed to retrieve production');
}
}
async getProducts(hashedUserId) {
try {
const falukantUser = await this.getFalukantUserByHashedId(hashedUserId);
if (!falukantUser) {
throw new Error('User not found');
}
const products = await ProductType.findAll({
where: {
category: {
[Op.lte]: falukantUser.certificate
}
},
include: [
{
model: Knowledge,
as: 'knowledges',
attributes: ['knowledge'],
}
],
attributes: ['labelTr', 'id', 'sellCost', 'productionTime', 'category'],
});
return products;
} catch (error) {
console.error('Error in getProducts:', error);
throw new Error('Failed to retrieve products');
}
}
async getInventory(hashedUserId, branchId) {
try {
const falukantUser = await this.getFalukantUserByHashedId(hashedUserId);
if (!falukantUser) {
throw new Error('User not found');
}
const branchFilter = branchId
? { id: branchId, falukantUserId: falukantUser.id }
: { falukantUserId: falukantUser.id };
const branches = await Branch.findAll({
where: branchFilter,
include: [
{
model: FalukantStock,
as: 'stocks',
include: [
{
model: FalukantStockType,
as: 'stockType',
},
],
},
{
model: RegionData,
as: 'region',
include: [
{
model: RegionType,
as: 'regionType',
},
],
},
],
});
const stockIds = branches.flatMap(branch => branch.stocks.map(stock => stock.id));
const inventoryItems = await Inventory.findAll({
where: { stockId: stockIds },
include: [
{
model: FalukantStock,
as: 'stock',
include: [
{
model: Branch,
as: 'branch',
include: [
{
model: RegionData,
as: 'region',
include: [
{
model: RegionType,
as: 'regionType',
},
],
},
],
},
{
model: FalukantStockType,
as: 'stockType',
},
],
},
{
model: ProductType,
as: 'productType',
},
],
});
const groupedInventory = inventoryItems.reduce((acc, item) => {
const region = item.stock.branch.region;
const key = `${region.id}-${item.productType.id}-${item.quality}`;
if (!acc[key]) {
acc[key] = {
region,
product: item.productType,
quality: item.quality,
totalQuantity: 0,
};
}
acc[key].totalQuantity += item.quantity;
return acc;
}, {});
const sortedInventory = Object.values(groupedInventory).sort((a, b) => {
if (a.region.id !== b.region.id) {
return a.region.id - b.region.id;
}
if (a.product.id !== b.product.id) {
return a.product.id - b.product.id;
}
return a.quality - b.quality;
});
return sortedInventory;
} catch (error) {
console.error('Error in getInventory:', error);
throw new Error('Failed to retrieve inventory');
}
}
async sellProduct(hashedUserId, branchId, productId, quality, quantity) {
try {
const falukantUser = await this.getFalukantUserByHashedId(hashedUserId);
if (!falukantUser) {
throw new Error('User not found');
}
const branch = await Branch.findOne({ where: { id: branchId, falukantUserId: falukantUser.id } });
if (!branch) {
throw new Error('Branch not found');
}
const product = await ProductType.findOne({ where: { id: productId } });
if (!product) {
throw new Error('Product not found');
}
const stock = await Stock.findOne({ where: { branchId: branch.id, } });
if (!stock) {
throw new Error('Stock not found');
}
if (stock.quantity < quantity) {
throw new Error('Not enough stock');
}
await FalukantStock.decrement('quantity', { by: quantity, where: { regionId: branch.regionId, stockTypeId: product.id } });
const inventory = await Inventory.create({ stockId: stock.id, quality, quantity });
return inventory;
} catch (error) {
console.error('Error in sellProduct:', error);
throw new Error('Failed to sell product');
}
}
} }
export default new FalukantService(); export default new FalukantService();

View File

@@ -235,7 +235,7 @@ async function initializeFalukantProducts() {
{ labelTr: 'milk', category: 1, productionTime: 1, sellCost: 4 }, { labelTr: 'milk', category: 1, productionTime: 1, sellCost: 4 },
{ labelTr: 'cheese', category: 1, productionTime: 1, sellCost: 4 }, { labelTr: 'cheese', category: 1, productionTime: 1, sellCost: 4 },
{ labelTr: 'bread', category: 1, productionTime: 1, sellCost: 4 }, { labelTr: 'bread', category: 1, productionTime: 1, sellCost: 4 },
{ labelTr: 'beer', category: 1, productionTime: 1, sellCost: 4 }, { labelTr: 'beer', category: 2, productionTime: 3, sellCost: 4 },
{ labelTr: 'iron', category: 2, productionTime: 4, sellCost: 15 }, { labelTr: 'iron', category: 2, productionTime: 4, sellCost: 15 },
{ labelTr: 'copper', category: 2, productionTime: 4, sellCost: 15 }, { labelTr: 'copper', category: 2, productionTime: 4, sellCost: 15 },
{ labelTr: 'spices', category: 2, productionTime: 8, sellCost: 30 }, { labelTr: 'spices', category: 2, productionTime: 8, sellCost: 30 },

View File

@@ -59,7 +59,6 @@ export default {
watch: { watch: {
modelValue(newVal) { modelValue(newVal) {
this.selected = newVal; this.selected = newVal;
console.log("FormattedDropdown modelValue changed:", newVal);
}, },
}, },
methods: { methods: {

View File

@@ -109,12 +109,35 @@
"info": "Informationen über den Direktor der Niederlassung." "info": "Informationen über den Direktor der Niederlassung."
}, },
"sale": { "sale": {
"title": "Verkauf", "title": "Inventar",
"info": "Hier können Produkte verkauft werden." "info": "Hier finden Sie eine Übersicht über die vorhandenen Produkte in der Filiale.",
"region": "Region",
"product": "Produkt",
"quality": "Qualität",
"quantity": "Menge",
"noInventory": "Kein Inventar verfügbar.",
"loadError": "Fehler beim Laden des Inventars.",
"sell": "Verkauf",
"sellButton": "Verkaufen",
"sellAllButton": "Alles verkaufen"
}, },
"production": { "production": {
"title": "Produktion", "title": "Produktion",
"info": "Details zur Produktion in der Niederlassung." "info": "Details zur Produktion in der Niederlassung.",
"selectProduct": "Produkt auswählen",
"quantity": "Menge",
"cost": "Kosten",
"duration": "Dauer",
"revenue": "Erlös",
"start": "Produktion starten",
"success": "Produktion erfolgreich gestartet!",
"error": "Fehler beim Starten der Produktion.",
"minutes": "Minuten",
"ending": "Abgeschlossen:",
"time": "Uhr",
"current": "Laufende Produktionen",
"product": "Produkt",
"remainingTime": "Verbleibende Zeit (Sekunden)"
}, },
"columns": { "columns": {
"city": "Stadt", "city": "Stadt",
@@ -124,7 +147,53 @@
"production": "Produktion", "production": "Produktion",
"store": "Verkauf", "store": "Verkauf",
"fullstack": "Produktion mit Verkauf" "fullstack": "Produktion mit Verkauf"
},
"revenue": {
"title": "Produkt-Erträge",
"product": "Produkt",
"absolute": "Erlös (absolut)",
"perMinute": "Erlös pro Minute",
"expand": "Erträge anzeigen",
"collapse": "Erträge ausblenden",
"knowledge": "Produktwissen"
} }
},
"product": {
"wheat": "Weizen",
"grain": "Getreide",
"carrot": "Karotte",
"fish": "Fisch",
"meat": "Fleisch",
"leather": "Leder",
"wood": "Holz",
"stone": "Stein",
"milk": "Milch",
"cheese": "Käse",
"bread": "Brot",
"beer": "Bier",
"iron": "Eisen",
"copper": "Kupfer",
"spices": "Gewürze",
"salt": "Salz",
"sugar": "Zucker",
"vinegar": "Essig",
"cotton": "Baumwolle",
"wine": "Wein",
"gold": "Gold",
"diamond": "Diamant",
"furniture": "Möbel",
"clothing": "Kleidung",
"jewelry": "Schmuck",
"painting": "Gemälde",
"book": "Buch",
"weapon": "Waffe",
"armor": "Rüstung",
"shield": "Schild",
"horse": "Pferd",
"ox": "Ochse"
},
"regionType": {
"city": "Stadt"
} }
} }
} }

View File

@@ -12,6 +12,7 @@ const store = createStore({
language: navigator.language.startsWith('de') ? 'de' : 'en', language: navigator.language.startsWith('de') ? 'de' : 'en',
menu: JSON.parse(localStorage.getItem('menu')) || [], menu: JSON.parse(localStorage.getItem('menu')) || [],
socket: null, socket: null,
daemonSocket: null,
menuNeedsUpdate: false, menuNeedsUpdate: false,
}, },
mutations: { mutations: {
@@ -50,11 +51,21 @@ const store = createStore({
} }
state.socket = null; state.socket = null;
}, },
setDaemonSocket(state, daemonSocket) {
state.daemonSocket = daemonSocket;
},
clearDaemonSocket(state) {
if (state.daemonSocket) {
state.daemonSocket.disconnect();
}
state.daemonSocket = null;
},
}, },
actions: { actions: {
async login({ commit, dispatch }, user) { async login({ commit, dispatch }, user) {
await commit('dologin', user); await commit('dologin', user);
await dispatch('initializeSocket'); await dispatch('initializeSocket');
await dispatch('initializeDaemonSocket');
const socket = this.getters.socket; const socket = this.getters.socket;
if (socket) { if (socket) {
socket.emit('setUserId', user.id); socket.emit('setUserId', user.id);
@@ -63,19 +74,89 @@ const store = createStore({
}, },
logout({ commit }) { logout({ commit }) {
commit('clearSocket'); commit('clearSocket');
commit('clearDaemonSocket');
commit('dologout'); commit('dologout');
router.push('/'); router.push('/');
}, },
initializeSocket({ commit, state }) { initializeSocket({ commit, state }) {
if (state.isLoggedIn && state.user) { if (state.isLoggedIn && state.user) {
const connectSocket = () => {
const socket = io(import.meta.env.VITE_API_BASE_URL); const socket = io(import.meta.env.VITE_API_BASE_URL);
socket.on('connect', () => { socket.on('connect', () => {
console.log('Socket.io connected');
socket.emit('setUserId', state.user.id); socket.emit('setUserId', state.user.id);
}); });
socket.on('disconnect', (reason) => { socket.on('disconnect', (reason) => {
console.warn('WebSocket disconnected:', reason); console.warn('Socket.io disconnected:', reason);
retryConnection(connectSocket);
}); });
commit('setSocket', socket); commit('setSocket', socket);
};
const retryConnection = (reconnectFn) => {
setTimeout(() => {
console.log('Retrying Socket.io connection...');
reconnectFn();
}, 1000); // Retry every second
};
connectSocket();
}
},
initializeDaemonSocket({ commit, state }) {
if (state.isLoggedIn && state.user) {
const connectDaemonSocket = () => {
const daemonSocket = new WebSocket(import.meta.env.VITE_DAEMON_SOCKET);
daemonSocket.onopen = () => {
console.log('Daemon WebSocket connected');
const payload = JSON.stringify({
event: 'setUserId',
data: { userId: state.user.id }
});
daemonSocket.send(payload);
};
daemonSocket.onclose = (event) => {
console.warn('Daemon WebSocket disconnected:', event.reason);
retryConnection(connectDaemonSocket);
};
daemonSocket.onerror = (error) => {
console.error('Daemon WebSocket error:', error);
retryConnection(connectDaemonSocket);
};
daemonSocket.addEventListener('message', (event) => {
const message = event.data;
console.log(message);
if (message === "ping") {
console.log("Ping received, sending Pong...");
daemonSocket.send("pong");
} else {
try {
const data = JSON.parse(message);
console.log("Message received:", data);
} catch (error) {
console.error("Error parsing message:", error);
}
}
});
commit('setDaemonSocket', daemonSocket);
};
const retryConnection = (reconnectFn) => {
setTimeout(() => {
console.log('Retrying Daemon WebSocket connection...');
reconnectFn();
}, 1000); // Retry every second
};
connectDaemonSocket();
} }
}, },
setLanguage({ commit }, language) { setLanguage({ commit }, language) {
@@ -97,6 +178,7 @@ const store = createStore({
language: state => state.language, language: state => state.language,
menu: state => state.menu, menu: state => state.menu,
socket: state => state.socket, socket: state => state.socket,
daemonSocket: state => state.daemonSocket,
menuNeedsUpdate: state => state.menuNeedsUpdate, menuNeedsUpdate: state => state.menuNeedsUpdate,
}, },
modules: { modules: {
@@ -106,6 +188,7 @@ const store = createStore({
if (store.state.isLoggedIn && store.state.user) { if (store.state.isLoggedIn && store.state.user) {
store.dispatch('initializeSocket'); store.dispatch('initializeSocket');
store.dispatch('initializeDaemonSocket');
} }
export default store; export default store;

View File

@@ -31,26 +31,150 @@
<div class="sale-section"> <div class="sale-section">
<h3>{{ $t('falukant.branch.sale.title') }}</h3> <h3>{{ $t('falukant.branch.sale.title') }}</h3>
<p>{{ $t('falukant.branch.sale.info') }}</p> <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> </div>
<!-- Production Section --> <!-- 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> <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> </div>
</div>
<MessageDialog ref="messageDialog" />
<ErrorDialog ref="errorDialog" />
</template> </template>
<script> <script>
import StatusBar from '@/components/falukant/StatusBar.vue'; import StatusBar from '@/components/falukant/StatusBar.vue';
import FormattedDropdown from '@/components/form/FormattedDropdown.vue'; import FormattedDropdown from '@/components/form/FormattedDropdown.vue';
import apiClient from '@/utils/axios.js'; 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 { export default {
name: "BranchView", name: "BranchView",
components: { components: {
StatusBar, StatusBar,
FormattedDropdown, FormattedDropdown,
MessageDialog,
ErrorDialog,
},
computed: {
...mapState(["socket", "daemonSocket"]),
}, },
data() { data() {
return { return {
@@ -60,19 +184,64 @@ export default {
{ field: "cityName", label: this.$t('falukant.branch.columns.city') }, { field: "cityName", label: this.$t('falukant.branch.columns.city') },
{ field: "type", label: this.$t('falukant.branch.columns.type') }, { 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() { async mounted() {
await this.loadBranches(); await this.loadBranches();
const branchId = this.$route.params.branchId; 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) { if (branchId) {
console.log('branch selected');
this.selectedBranch = this.branches.find(branch => branch.id === parseInt(branchId)) || null; this.selectedBranch = this.branches.find(branch => branch.id === parseInt(branchId)) || null;
} else { } else {
console.log('main branch selected');
this.selectMainBranch(); 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: { methods: {
async loadBranches() { async loadBranches() {
@@ -84,7 +253,6 @@ export default {
type: this.$t(`falukant.branch.types.${branch.branchType.labelTr}`), type: this.$t(`falukant.branch.types.${branch.branchType.labelTr}`),
isMainBranch: branch.isMainBranch, isMainBranch: branch.isMainBranch,
})); }));
// Wenn keine selectedBranch gesetzt ist, versuche die Main Branch zu wählen
if (!this.selectedBranch) { if (!this.selectedBranch) {
this.selectMainBranch(); this.selectMainBranch();
} }
@@ -92,11 +260,16 @@ export default {
console.error('Error loading branches:', error); console.error('Error loading branches:', error);
} }
}, },
async loadBranchData(newBranch) {
if (newBranch) {
this.getBranchData(newBranch);
await this.loadInventory();
}
},
selectMainBranch() { selectMainBranch() {
const main = this.branches.find(b => b.isMainBranch) || null; const main = this.branches.find(b => b.isMainBranch) || null;
if (main !== this.selectedBranch) { if (main !== this.selectedBranch) {
this.selectedBranch = main; this.selectedBranch = main;
console.log("Main branch selected:", this.selectedBranch);
} }
}, },
createBranch() { 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> </script>
@@ -127,4 +457,110 @@ export default {
button { button {
margin: 5px; 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> </style>