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), (userId, req) => this.service.repairVehicle(userId, req.params.vehicleId),
{ successStatus: 200 } { 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.post('/vehicles', falukantController.buyVehicles);
router.get('/vehicles', falukantController.getVehicles); router.get('/vehicles', falukantController.getVehicles);
router.post('/vehicles/:vehicleId/repair', falukantController.repairVehicle); router.post('/vehicles/:vehicleId/repair', falukantController.repairVehicle);
router.post('/vehicles/repair-all', falukantController.repairAllVehicles);
router.post('/transports', falukantController.createTransport); router.post('/transports', falukantController.createTransport);
router.get('/transports/route', falukantController.getTransportRoute); router.get('/transports/route', falukantController.getTransportRoute);
router.get('/transports/branch/:branchId', falukantController.getBranchTransports); 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) { async createStock(hashedUserId, branchId, stockData) {
const u = await getFalukantUserOrFail(hashedUserId); const u = await getFalukantUserOrFail(hashedUserId);
const b = await getBranchOrFail(u.id, branchId); const b = await getBranchOrFail(u.id, branchId);

View File

@@ -28,6 +28,9 @@
"notify_election_created": "Es wurde eine neue Wahl ausgeschrieben.", "notify_election_created": "Es wurde eine neue Wahl ausgeschrieben.",
"production": { "production": {
"overproduction": "Überproduktion: Deine Produktion liegt {value}% über dem Bedarf." "overproduction": "Überproduktion: Deine Produktion liegt {value}% über dem Bedarf."
},
"transport": {
"waiting": "Transport wartet"
} }
}, },
"health": { "health": {
@@ -319,8 +322,16 @@
"cancel": "Abbrechen", "cancel": "Abbrechen",
"close": "Schließen", "close": "Schließen",
"repair": "Reparieren", "repair": "Reparieren",
"repairWithCost": "Reparieren (Kosten: {cost})",
"repairTooltip": "Fahrzeug reparieren", "repairTooltip": "Fahrzeug reparieren",
"repairVehicleTitle": "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", "repairVehicleType": "Fahrzeugtyp",
"repairCurrentCondition": "Aktueller Zustand", "repairCurrentCondition": "Aktueller Zustand",
"repairCost": "Reparaturkosten", "repairCost": "Reparaturkosten",

View File

@@ -68,9 +68,18 @@
<!-- Transportmittel --> <!-- Transportmittel -->
<div v-else-if="activeTab === 'transport'" class="branch-tab-pane"> <div v-else-if="activeTab === 'transport'" class="branch-tab-pane">
<p>{{ $t('falukant.branch.transport.placeholder') }}</p> <p>{{ $t('falukant.branch.transport.placeholder') }}</p>
<div class="vehicle-action-buttons">
<button @click="openBuyVehicleDialog"> <button @click="openBuyVehicleDialog">
{{ $t('falukant.branch.transport.buy') }} {{ $t('falukant.branch.transport.buy') }}
</button> </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"> <div class="vehicle-overview" v-if="vehicles && vehicles.length">
<table class="vehicle-table"> <table class="vehicle-table">
@@ -122,7 +131,7 @@
:title="$t('falukant.branch.transport.repairTooltip')" :title="$t('falukant.branch.transport.repairTooltip')"
class="repair-button" class="repair-button"
> >
{{ $t('falukant.branch.transport.repair') }} {{ $t('falukant.branch.transport.repairWithCost', { cost: formatMoney(calculateRepairCost(v)) }) }}
</button> </button>
<span v-if="v.status !== 'available'" class="no-action"></span> <span v-if="v.status !== 'available'" class="no-action"></span>
</div> </div>
@@ -194,6 +203,32 @@
</div> </div>
</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 --> <!-- Dialog zum Reparieren von Fahrzeugen -->
<div v-if="repairVehicleDialog.show" class="modal-overlay" @click.self="closeRepairVehicleDialog"> <div v-if="repairVehicleDialog.show" class="modal-overlay" @click.self="closeRepairVehicleDialog">
<div class="modal-content"> <div class="modal-content">
@@ -301,6 +336,11 @@ export default {
repairCost: null, repairCost: null,
buildTimeMinutes: null, buildTimeMinutes: null,
}, },
repairAllVehiclesDialog: {
show: false,
vehicleIds: [],
totalCost: null,
},
}; };
}, },
@@ -318,6 +358,21 @@ export default {
} }
return grouped; 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() { 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) { openRepairVehicleDialog(vehicle) {
if (!vehicle || vehicle.status !== 'available' || vehicle.condition >= 100) { if (!vehicle || vehicle.status !== 'available' || vehicle.condition >= 100) {
return; return;
} }
// Kosten berechnen: Baupreis * 0.8 * (100 - zustand) / 100 // Kosten berechnen: Baupreis * 0.8 * (100 - zustand) / 100
const baseCost = vehicle.type.cost || 0; const repairCost = this.calculateRepairCost(vehicle);
const repairCost = Math.round(baseCost * 0.8 * (100 - vehicle.condition) / 100);
const buildTimeMinutes = vehicle.type.buildTimeMinutes || 0; const buildTimeMinutes = vehicle.type.buildTimeMinutes || 0;
this.repairVehicleDialog = { 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() { closeRepairVehicleDialog() {
this.repairVehicleDialog = { this.repairVehicleDialog = {
show: false, show: false,
@@ -851,15 +952,17 @@ h2 {
.send-all-buttons button { .send-all-buttons button {
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
background-color: #007bff; background-color: #F9A22C;
color: white; color: #000000;
border: none; border: 1px solid #F9A22C;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
} }
.send-all-buttons button:hover { .send-all-buttons button:hover {
background-color: #0056b3; background-color: #fdf1db;
color: #7E471B;
border: 1px solid #7E471B;
} }
.no-action { .no-action {
@@ -998,4 +1101,36 @@ h2 {
font-size: 1.1em; font-size: 1.1em;
color: #28a745; 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> </style>