Add bulk vehicle repair functionality in Falukant module

- Implemented a new repairAllVehicles method in FalukantService to handle the repair of multiple vehicles at once, including cost calculation and precondition checks.
- Updated FalukantController to expose the repairAllVehicles endpoint, allowing users to initiate bulk repairs via the API.
- Enhanced FalukantRouter to include a new route for bulk vehicle repairs.
- Modified BranchView component to add UI elements for repairing all vehicles, including a dialog for confirmation and displaying repair details.
- Updated German localization files to include translations related to bulk vehicle repair actions, improving user experience for German-speaking users.
This commit is contained in:
Torsten Schulz (local)
2025-12-08 08:36:21 +01:00
parent b1d29f2083
commit fadc301d41
5 changed files with 263 additions and 10 deletions

View File

@@ -221,6 +221,10 @@ class FalukantController {
(userId, req) => this.service.repairVehicle(userId, req.params.vehicleId),
{ successStatus: 200 }
);
this.repairAllVehicles = this._wrapWithUser(
(userId, req) => this.service.repairAllVehicles(userId, req.body.vehicleIds),
{ successStatus: 200 }
);
}

View File

@@ -76,6 +76,7 @@ router.get('/vehicles/types', falukantController.getVehicleTypes);
router.post('/vehicles', falukantController.buyVehicles);
router.get('/vehicles', falukantController.getVehicles);
router.post('/vehicles/:vehicleId/repair', falukantController.repairVehicle);
router.post('/vehicles/repair-all', falukantController.repairAllVehicles);
router.post('/transports', falukantController.createTransport);
router.get('/transports/route', falukantController.getTransportRoute);
router.get('/transports/branch/:branchId', falukantController.getBranchTransports);

View File

@@ -1212,6 +1212,108 @@ class FalukantService extends BaseService {
};
}
async repairAllVehicles(hashedUserId, vehicleIds) {
const user = await getFalukantUserOrFail(hashedUserId);
const now = new Date();
if (!vehicleIds || !Array.isArray(vehicleIds) || vehicleIds.length === 0) {
throw new Error('Keine Fahrzeuge zum Reparieren angegeben');
}
// Alle Fahrzeuge laden
const vehicles = await Vehicle.findAll({
where: {
id: { [Op.in]: vehicleIds },
falukantUserId: user.id,
},
include: [
{
model: VehicleType,
as: 'type',
required: true,
},
{
model: Transport,
as: 'transports',
required: false,
attributes: ['id'],
},
],
});
if (vehicles.length !== vehicleIds.length) {
throw new Error('Nicht alle angegebenen Fahrzeuge gefunden oder gehören nicht dem Benutzer');
}
// Prüfe alle Fahrzeuge
const repairableVehicles = [];
let totalCost = 0;
for (const vehicle of vehicles) {
// Prüfen, ob Fahrzeug in Benutzung ist
const hasActiveTransport = Array.isArray(vehicle.transports) && vehicle.transports.length > 0;
const isBuilding = vehicle.availableFrom && new Date(vehicle.availableFrom).getTime() > now.getTime();
if (hasActiveTransport || isBuilding) {
continue; // Überspringe Fahrzeuge in Benutzung
}
// Prüfen, ob Reparatur nötig ist (Zustand < 100)
if (vehicle.condition >= 100) {
continue; // Überspringe bereits perfekte Fahrzeuge
}
// Kosten berechnen: Baupreis * 0.8 * (100 - zustand) / 100
const baseCost = vehicle.type.cost;
const repairCost = Math.round(baseCost * 0.8 * (100 - vehicle.condition) / 100);
totalCost += repairCost;
repairableVehicles.push({ vehicle, repairCost });
}
if (repairableVehicles.length === 0) {
throw new PreconditionError('noVehiclesToRepair');
}
// 10% Rabatt für Reparatur aller Fahrzeuge
const discountedCost = Math.round(totalCost * 0.9);
if (user.money < discountedCost) {
throw new PreconditionError('insufficientFunds');
}
// Alle Reparaturen in einer Transaktion durchführen
await sequelize.transaction(async (tx) => {
// Geld abziehen
const moneyResult = await updateFalukantUserMoney(
user.id,
-discountedCost,
'repair_all_vehicles',
user.id
);
if (!moneyResult.success) {
throw new Error('Failed to update money');
}
// Alle Fahrzeuge reparieren
for (const { vehicle, repairCost } of repairableVehicles) {
const buildTimeMinutes = vehicle.type.buildTimeMinutes || 0;
const buildMs = buildTimeMinutes * 60 * 1000;
const availableFrom = new Date(now.getTime() + buildMs);
await vehicle.update({
condition: 100,
availableFrom: availableFrom,
}, { transaction: tx });
}
});
return {
success: true,
repairedCount: repairableVehicles.length,
totalCost: discountedCost,
};
}
async createStock(hashedUserId, branchId, stockData) {
const u = await getFalukantUserOrFail(hashedUserId);
const b = await getBranchOrFail(u.id, branchId);

View File

@@ -28,6 +28,9 @@
"notify_election_created": "Es wurde eine neue Wahl ausgeschrieben.",
"production": {
"overproduction": "Überproduktion: Deine Produktion liegt {value}% über dem Bedarf."
},
"transport": {
"waiting": "Transport wartet"
}
},
"health": {
@@ -319,8 +322,16 @@
"cancel": "Abbrechen",
"close": "Schließen",
"repair": "Reparieren",
"repairWithCost": "Reparieren (Kosten: {cost})",
"repairTooltip": "Fahrzeug reparieren",
"repairVehicleTitle": "Fahrzeug reparieren",
"repairAll": "Alle reparieren ({count} Fahrzeuge, Kosten: {cost})",
"repairAllTitle": "Alle Fahrzeuge reparieren",
"repairAllDescription": "Möchtest du alle {count} Fahrzeuge reparieren? Gesamtkosten: {cost}",
"repairAllDiscount": "Hinweis: Bei Reparatur aller Fahrzeuge erhältst du 10% Rabatt.",
"repairAllConfirm": "Alle reparieren",
"repairAllSuccess": "Alle Fahrzeuge werden repariert!",
"repairAllError": "Fehler beim Reparieren der Fahrzeuge.",
"repairVehicleType": "Fahrzeugtyp",
"repairCurrentCondition": "Aktueller Zustand",
"repairCost": "Reparaturkosten",

View File

@@ -68,9 +68,18 @@
<!-- Transportmittel -->
<div v-else-if="activeTab === 'transport'" class="branch-tab-pane">
<p>{{ $t('falukant.branch.transport.placeholder') }}</p>
<div class="vehicle-action-buttons">
<button @click="openBuyVehicleDialog">
{{ $t('falukant.branch.transport.buy') }}
</button>
<button
v-if="repairableVehiclesCount > 0"
@click="openRepairAllVehiclesDialog"
class="repair-all-button"
>
{{ $t('falukant.branch.transport.repairAll', { count: repairableVehiclesCount, cost: formatMoney(repairAllCost) }) }}
</button>
</div>
<div class="vehicle-overview" v-if="vehicles && vehicles.length">
<table class="vehicle-table">
@@ -122,7 +131,7 @@
:title="$t('falukant.branch.transport.repairTooltip')"
class="repair-button"
>
{{ $t('falukant.branch.transport.repair') }}
{{ $t('falukant.branch.transport.repairWithCost', { cost: formatMoney(calculateRepairCost(v)) }) }}
</button>
<span v-if="v.status !== 'available'" class="no-action"></span>
</div>
@@ -194,6 +203,32 @@
</div>
</div>
<!-- Dialog zum Reparieren aller Fahrzeuge -->
<div v-if="repairAllVehiclesDialog.show" class="modal-overlay" @click.self="closeRepairAllVehiclesDialog">
<div class="modal-content">
<h3>{{ $t('falukant.branch.transport.repairAllTitle') }}</h3>
<div class="repair-all-form">
<p>
{{ $t('falukant.branch.transport.repairAllDescription', {
count: repairAllVehiclesDialog.vehicleIds.length,
cost: formatMoney(repairAllVehiclesDialog.totalCost)
}) }}
</p>
<p class="repair-all-discount">
{{ $t('falukant.branch.transport.repairAllDiscount') }}
</p>
<div class="modal-buttons">
<button @click="repairAllVehicles" class="repair-confirm-button">
{{ $t('falukant.branch.transport.repairAllConfirm') }}
</button>
<button @click="closeRepairAllVehiclesDialog">
{{ $t('falukant.branch.transport.cancel') }}
</button>
</div>
</div>
</div>
</div>
<!-- Dialog zum Reparieren von Fahrzeugen -->
<div v-if="repairVehicleDialog.show" class="modal-overlay" @click.self="closeRepairVehicleDialog">
<div class="modal-content">
@@ -301,6 +336,11 @@ export default {
repairCost: null,
buildTimeMinutes: null,
},
repairAllVehiclesDialog: {
show: false,
vehicleIds: [],
totalCost: null,
},
};
},
@@ -318,6 +358,21 @@ export default {
}
return grouped;
},
repairableVehicles() {
return (this.vehicles || []).filter(v =>
v.status === 'available' && v.condition < 100
);
},
repairableVehiclesCount() {
return this.repairableVehicles.length;
},
repairAllCost() {
const totalCost = this.repairableVehicles.reduce((sum, v) => {
return sum + this.calculateRepairCost(v);
}, 0);
// 10% Rabatt für Reparatur aller Fahrzeuge
return Math.round(totalCost * 0.9);
},
},
async mounted() {
@@ -755,14 +810,19 @@ export default {
}
},
calculateRepairCost(vehicle) {
if (!vehicle || !vehicle.type || vehicle.condition >= 100) return 0;
const baseCost = vehicle.type.cost || 0;
return Math.round(baseCost * 0.8 * (100 - vehicle.condition) / 100);
},
openRepairVehicleDialog(vehicle) {
if (!vehicle || vehicle.status !== 'available' || vehicle.condition >= 100) {
return;
}
// Kosten berechnen: Baupreis * 0.8 * (100 - zustand) / 100
const baseCost = vehicle.type.cost || 0;
const repairCost = Math.round(baseCost * 0.8 * (100 - vehicle.condition) / 100);
const repairCost = this.calculateRepairCost(vehicle);
const buildTimeMinutes = vehicle.type.buildTimeMinutes || 0;
this.repairVehicleDialog = {
@@ -773,6 +833,47 @@ export default {
};
},
openRepairAllVehiclesDialog() {
const repairableVehicles = this.repairableVehicles;
if (repairableVehicles.length === 0) {
return;
}
this.repairAllVehiclesDialog = {
show: true,
vehicleIds: repairableVehicles.map(v => v.id),
totalCost: this.repairAllCost,
};
},
closeRepairAllVehiclesDialog() {
this.repairAllVehiclesDialog = {
show: false,
vehicleIds: [],
totalCost: null,
};
},
async repairAllVehicles() {
if (!this.repairAllVehiclesDialog.vehicleIds || this.repairAllVehiclesDialog.vehicleIds.length === 0) {
return;
}
try {
await apiClient.post('/api/falukant/vehicles/repair-all', {
vehicleIds: this.repairAllVehiclesDialog.vehicleIds,
});
await this.loadVehicles();
this.closeRepairAllVehiclesDialog();
alert(this.$t('falukant.branch.transport.repairAllSuccess'));
this.$refs.statusBar?.fetchStatus();
} catch (error) {
console.error('Error repairing all vehicles:', error);
const errorMessage = error.response?.data?.message || this.$t('falukant.branch.transport.repairAllError');
alert(errorMessage);
}
},
closeRepairVehicleDialog() {
this.repairVehicleDialog = {
show: false,
@@ -851,15 +952,17 @@ h2 {
.send-all-buttons button {
padding: 0.5rem 1rem;
background-color: #007bff;
color: white;
border: none;
background-color: #F9A22C;
color: #000000;
border: 1px solid #F9A22C;
border-radius: 4px;
cursor: pointer;
}
.send-all-buttons button:hover {
background-color: #0056b3;
background-color: #fdf1db;
color: #7E471B;
border: 1px solid #7E471B;
}
.no-action {
@@ -998,4 +1101,36 @@ h2 {
font-size: 1.1em;
color: #28a745;
}
.vehicle-action-buttons {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.repair-all-button {
background-color: #28a745;
color: white;
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
}
.repair-all-button:hover {
background-color: #218838;
}
.repair-all-form {
display: flex;
flex-direction: column;
gap: 1rem;
margin-top: 1rem;
}
.repair-all-discount {
color: #28a745;
font-weight: bold;
}
</style>