Implement empty transport feature in DirectorInfo component
- Added functionality to allow directors to initiate empty transports without products, enhancing logistics management. - Introduced a new transport form in the DirectorInfo component, enabling selection of vehicle types and target branches. - Updated the i18n localization files to include new translations for the empty transport feature. - Enhanced the BranchView to pass vehicle and branch data to the DirectorInfo component, ensuring proper functionality. - This update aims to improve user experience and streamline transport operations within the application.
This commit is contained in:
7
backend/migrations/make_transport_product_nullable.sql
Normal file
7
backend/migrations/make_transport_product_nullable.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
-- Migration: Make productId and size nullable in transport table
|
||||
-- This allows empty transports (moving vehicles without products)
|
||||
|
||||
ALTER TABLE falukant_data.transport
|
||||
ALTER COLUMN product_id DROP NOT NULL,
|
||||
ALTER COLUMN size DROP NOT NULL;
|
||||
|
||||
@@ -15,11 +15,11 @@ Transport.init(
|
||||
},
|
||||
productId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
allowNull: true, // Nullable für leere Transporte (nur Fahrzeuge bewegen)
|
||||
},
|
||||
size: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
allowNull: true, // Nullable für leere Transporte (nur Fahrzeuge bewegen)
|
||||
},
|
||||
vehicleId: {
|
||||
type: DataTypes.INTEGER,
|
||||
|
||||
@@ -92,12 +92,14 @@ function calcSellPrice(product, knowledgeFactor = 0) {
|
||||
return min + (max - min) * (knowledgeFactor / 100);
|
||||
}
|
||||
|
||||
async function calcRegionalSellPrice(product, knowledgeFactor, regionId) {
|
||||
// Hole TownProductWorth für diese Region und dieses Produkt
|
||||
const townWorth = await TownProductWorth.findOne({
|
||||
where: { productId: product.id, regionId: regionId }
|
||||
});
|
||||
const worthPercent = townWorth?.worthPercent || 50; // Default 50% wenn nicht gefunden
|
||||
async function calcRegionalSellPrice(product, knowledgeFactor, regionId, worthPercent = null) {
|
||||
// Wenn worthPercent nicht übergeben wurde, hole es aus der Datenbank
|
||||
if (worthPercent === null) {
|
||||
const townWorth = await TownProductWorth.findOne({
|
||||
where: { productId: product.id, regionId: regionId }
|
||||
});
|
||||
worthPercent = townWorth?.worthPercent || 50; // Default 50% wenn nicht gefunden
|
||||
}
|
||||
|
||||
// Basispreis basierend auf regionalem worthPercent
|
||||
const basePrice = product.sellCost * (worthPercent / 100);
|
||||
@@ -795,42 +797,59 @@ class FalukantService extends BaseService {
|
||||
}
|
||||
const maxByVehicles = capacityPerVehicle * freeVehicles.length;
|
||||
|
||||
const stock = await FalukantStock.findOne({ where: { branchId: sourceBranch.id } });
|
||||
if (!stock) {
|
||||
throw new Error('Stock not found');
|
||||
// Produkt-Transport oder leerer Transport (nur Fahrzeuge bewegen)?
|
||||
const isEmptyTransport = !productId || !quantity || quantity <= 0;
|
||||
|
||||
let inventory = [];
|
||||
let available = 0;
|
||||
let maxByInventory = 0;
|
||||
let hardMax = 0;
|
||||
let requested = 0;
|
||||
let transportCost = 0.1; // Minimale Kosten für leeren Transport
|
||||
|
||||
if (!isEmptyTransport) {
|
||||
// Produkt-Transport: Inventar prüfen
|
||||
const stock = await FalukantStock.findOne({ where: { branchId: sourceBranch.id } });
|
||||
if (!stock) {
|
||||
throw new Error('Stock not found');
|
||||
}
|
||||
|
||||
inventory = await Inventory.findAll({
|
||||
where: { stockId: stock.id },
|
||||
include: [
|
||||
{
|
||||
model: ProductType,
|
||||
as: 'productType',
|
||||
required: true,
|
||||
where: { id: productId },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
available = inventory.reduce((sum, i) => sum + i.quantity, 0);
|
||||
if (available <= 0) {
|
||||
throw new PreconditionError('noInventory');
|
||||
}
|
||||
|
||||
maxByInventory = available;
|
||||
hardMax = Math.min(maxByVehicles, maxByInventory);
|
||||
|
||||
requested = Math.max(1, parseInt(quantity, 10) || 0);
|
||||
if (requested > hardMax) {
|
||||
throw new PreconditionError('quantityTooHigh');
|
||||
}
|
||||
|
||||
// Transportkosten: 1 % des Warenwerts, mindestens 0,1
|
||||
const productType = inventory[0]?.productType;
|
||||
const unitValue = productType?.sellCost || 0;
|
||||
const totalValue = unitValue * requested;
|
||||
transportCost = Math.max(0.1, totalValue * 0.01);
|
||||
} else {
|
||||
// Leerer Transport: Ein Fahrzeug wird bewegt
|
||||
requested = 1;
|
||||
hardMax = 1;
|
||||
}
|
||||
|
||||
const inventory = await Inventory.findAll({
|
||||
where: { stockId: stock.id },
|
||||
include: [
|
||||
{
|
||||
model: ProductType,
|
||||
as: 'productType',
|
||||
required: true,
|
||||
where: { id: productId },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const available = inventory.reduce((sum, i) => sum + i.quantity, 0);
|
||||
if (available <= 0) {
|
||||
throw new PreconditionError('noInventory');
|
||||
}
|
||||
|
||||
const maxByInventory = available;
|
||||
const hardMax = Math.min(maxByVehicles, maxByInventory);
|
||||
|
||||
const requested = Math.max(1, parseInt(quantity, 10) || 0);
|
||||
if (requested > hardMax) {
|
||||
throw new PreconditionError('quantityTooHigh');
|
||||
}
|
||||
|
||||
// Transportkosten: 1 % des Warenwerts, mindestens 0,1
|
||||
const productType = inventory[0]?.productType;
|
||||
const unitValue = productType?.sellCost || 0;
|
||||
const totalValue = unitValue * requested;
|
||||
const transportCost = Math.max(0.1, totalValue * 0.01);
|
||||
|
||||
if (user.money < transportCost) {
|
||||
throw new PreconditionError('insufficientFunds');
|
||||
}
|
||||
@@ -852,44 +871,52 @@ class FalukantService extends BaseService {
|
||||
|
||||
for (const v of freeVehicles) {
|
||||
if (remaining <= 0) break;
|
||||
const size = Math.min(remaining, capacityPerVehicle);
|
||||
const size = isEmptyTransport ? 0 : Math.min(remaining, capacityPerVehicle);
|
||||
const t = await Transport.create(
|
||||
{
|
||||
sourceRegionId,
|
||||
targetRegionId,
|
||||
productId,
|
||||
size,
|
||||
productId: isEmptyTransport ? null : productId,
|
||||
size: isEmptyTransport ? 0 : size,
|
||||
vehicleId: v.id,
|
||||
},
|
||||
{ transaction: tx }
|
||||
);
|
||||
transportsCreated.push(t);
|
||||
remaining -= size;
|
||||
}
|
||||
|
||||
if (remaining > 0) {
|
||||
throw new Error('Not enough vehicle capacity for requested quantity');
|
||||
}
|
||||
|
||||
// Inventar in der Quell-Niederlassung reduzieren
|
||||
let left = requested;
|
||||
for (const inv of inventory) {
|
||||
if (left <= 0) break;
|
||||
if (inv.quantity <= left) {
|
||||
left -= inv.quantity;
|
||||
await inv.destroy({ transaction: tx });
|
||||
if (!isEmptyTransport) {
|
||||
remaining -= size;
|
||||
} else {
|
||||
await inv.update({ quantity: inv.quantity - left }, { transaction: tx });
|
||||
left = 0;
|
||||
break;
|
||||
// Bei leerem Transport nur ein Fahrzeug bewegen
|
||||
remaining = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (left > 0) {
|
||||
throw new Error('Inventory changed during transport creation');
|
||||
if (remaining > 0 && !isEmptyTransport) {
|
||||
throw new Error('Not enough vehicle capacity for requested quantity');
|
||||
}
|
||||
|
||||
// Inventar in der Quell-Niederlassung reduzieren (nur bei Produkt-Transport)
|
||||
if (!isEmptyTransport && inventory.length > 0) {
|
||||
let left = requested;
|
||||
for (const inv of inventory) {
|
||||
if (left <= 0) break;
|
||||
if (inv.quantity <= left) {
|
||||
left -= inv.quantity;
|
||||
await inv.destroy({ transaction: tx });
|
||||
} else {
|
||||
await inv.update({ quantity: inv.quantity - left }, { transaction: tx });
|
||||
left = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (left > 0) {
|
||||
throw new Error('Inventory changed during transport creation');
|
||||
}
|
||||
|
||||
notifyUser(user.user.hashedId, 'stock_change', { branchId: sourceBranch.id });
|
||||
}
|
||||
|
||||
notifyUser(user.user.hashedId, 'stock_change', { branchId: sourceBranch.id });
|
||||
notifyUser(user.user.hashedId, 'falukantBranchUpdate', { branchId: sourceBranch.id });
|
||||
|
||||
return {
|
||||
@@ -3721,7 +3748,8 @@ class FalukantService extends BaseService {
|
||||
]
|
||||
});
|
||||
|
||||
// TownProductWorth für alle Städte und dieses Produkt abrufen
|
||||
// TownProductWorth für alle Städte und dieses Produkt einmalig abrufen
|
||||
// (vermeidet N+1 Query Problem)
|
||||
const townWorths = await TownProductWorth.findAll({
|
||||
where: { productId: productId },
|
||||
attributes: ['regionId', 'worthPercent']
|
||||
@@ -3729,13 +3757,14 @@ class FalukantService extends BaseService {
|
||||
const worthMap = new Map(townWorths.map(tw => [tw.regionId, tw.worthPercent]));
|
||||
|
||||
// Berechne den regionalen Preis für die aktuelle Region (falls angegeben)
|
||||
// WICHTIG: Ignoriere den übergebenen currentPrice, da er möglicherweise nicht
|
||||
// den regionalen Faktor berücksichtigt. Berechne stattdessen immer den korrekten
|
||||
// regionalen Preis basierend auf currentRegionId.
|
||||
let currentRegionalPrice = currentPrice; // Fallback auf übergebenen Preis
|
||||
if (currentRegionId) {
|
||||
const currentWorthPercent = worthMap.get(currentRegionId) || 50;
|
||||
const currentBasePrice = product.sellCost * (currentWorthPercent / 100);
|
||||
const currentMin = currentBasePrice * 0.6;
|
||||
const currentMax = currentBasePrice;
|
||||
currentRegionalPrice = currentMin + (currentMax - currentMin) * (knowledgeFactor / 100);
|
||||
// Verwende calcRegionalSellPrice mit bereits geladenem worthPercent (keine DB-Query)
|
||||
currentRegionalPrice = await calcRegionalSellPrice(product, knowledgeFactor, currentRegionId, currentWorthPercent);
|
||||
}
|
||||
|
||||
// Für jede Stadt den Preis berechnen und Branch-Typ bestimmen
|
||||
@@ -3746,16 +3775,9 @@ class FalukantService extends BaseService {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Regionaler Preis-Faktor (worthPercent zwischen 40-60)
|
||||
const worthPercent = worthMap.get(city.id) || 50; // Default 50% wenn nicht gefunden
|
||||
|
||||
// Basispreis basierend auf regionalem worthPercent
|
||||
const basePrice = product.sellCost * (worthPercent / 100);
|
||||
|
||||
// Dann Knowledge-Faktor anwenden (wie in calcSellPrice)
|
||||
const min = basePrice * 0.6;
|
||||
const max = basePrice;
|
||||
const priceInCity = min + (max - min) * (knowledgeFactor / 100);
|
||||
const worthPercent = worthMap.get(city.id) || 50;
|
||||
// Verwende calcRegionalSellPrice mit bereits geladenem worthPercent (keine DB-Query)
|
||||
const priceInCity = await calcRegionalSellPrice(product, knowledgeFactor, city.id, worthPercent);
|
||||
|
||||
// Nur Städte zurückgeben, wo der Preis höher ist
|
||||
// Kleine Toleranz (0.01) für Rundungsfehler bei Gleitkommaberechnungen
|
||||
|
||||
Reference in New Issue
Block a user