mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Enhance deletion logic in Fleet and Schedules services to prevent deletion of records with active dependencies; update Seats and Stations pages to improve UI and remove unused fields.
This commit is contained in:
@@ -134,9 +134,28 @@ export class FleetService {
|
||||
}
|
||||
|
||||
async deleteCoachType(id: string) {
|
||||
const coachType = await this.prisma.coachType.findUnique({ where: { id } });
|
||||
const coachType = await this.prisma.coachType.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
coaches: true,
|
||||
seatClasses: true,
|
||||
},
|
||||
});
|
||||
if (!coachType) throw new NotFoundException('Coach type not found');
|
||||
|
||||
// Check for related records
|
||||
if (coachType.coaches.length > 0) {
|
||||
throw new BadRequestException(
|
||||
`Cannot delete coach type. ${coachType.coaches.length} coach(es) are still using this coach type. Please reassign or delete the coaches first.`
|
||||
);
|
||||
}
|
||||
|
||||
if (coachType.seatClasses.length > 0) {
|
||||
throw new BadRequestException(
|
||||
`Cannot delete coach type. ${coachType.seatClasses.length} seat class(es) are still using this coach type. Please reassign or delete the seat classes first.`
|
||||
);
|
||||
}
|
||||
|
||||
return this.prisma.coachType.delete({ where: { id } });
|
||||
}
|
||||
|
||||
@@ -183,9 +202,29 @@ export class FleetService {
|
||||
}
|
||||
|
||||
async deleteClass(id: string) {
|
||||
const seatClass = await this.prisma.seatClass.findUnique({ where: { id } });
|
||||
const seatClass = await this.prisma.seatClass.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
fareRules: true,
|
||||
routeFareRules: true,
|
||||
segmentFares: true,
|
||||
},
|
||||
});
|
||||
if (!seatClass) throw new NotFoundException('Seat class not found');
|
||||
|
||||
// Check for related records
|
||||
const relatedRecords = [
|
||||
...seatClass.fareRules,
|
||||
...seatClass.routeFareRules,
|
||||
...seatClass.segmentFares,
|
||||
];
|
||||
|
||||
if (relatedRecords.length > 0) {
|
||||
throw new BadRequestException(
|
||||
`Cannot delete seat class. ${relatedRecords.length} fare rule(s) are still using this seat class. Please delete the fare rules first.`
|
||||
);
|
||||
}
|
||||
|
||||
return this.prisma.seatClass.delete({ where: { id } });
|
||||
}
|
||||
|
||||
@@ -220,8 +259,21 @@ export class FleetService {
|
||||
}
|
||||
|
||||
async deleteTrain(id: string) {
|
||||
const train = await this.prisma.train.findUnique({ where: { id } });
|
||||
const train = await this.prisma.train.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
schedules: true,
|
||||
},
|
||||
});
|
||||
if (!train) throw new NotFoundException('Train not found');
|
||||
|
||||
// Check for active schedules
|
||||
if (train.schedules.length > 0) {
|
||||
throw new BadRequestException(
|
||||
`Cannot delete train. This train has ${train.schedules.length} schedule(s). Please delete the schedules first.`
|
||||
);
|
||||
}
|
||||
|
||||
return this.prisma.train.delete({ where: { id } });
|
||||
}
|
||||
|
||||
@@ -305,10 +357,53 @@ export class FleetService {
|
||||
}
|
||||
|
||||
async deleteCoach(id: string) {
|
||||
const coach = await this.prisma.coach.findUnique({ where: { id } });
|
||||
const coach = await this.prisma.coach.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
assignments: true,
|
||||
seats: {
|
||||
include: {
|
||||
bookingSeats: true,
|
||||
blocks: true,
|
||||
ticketSeats: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!coach) throw new NotFoundException('Coach not found');
|
||||
|
||||
// Delete related seats first
|
||||
// Check for active assignments
|
||||
if (coach.assignments.length > 0) {
|
||||
throw new BadRequestException(
|
||||
`Cannot delete coach. This coach is assigned to ${coach.assignments.length} schedule(s). Please remove the assignments first.`
|
||||
);
|
||||
}
|
||||
|
||||
// Check for booked seats
|
||||
const bookedSeats = coach.seats.filter(seat => seat.bookingSeats.length > 0);
|
||||
if (bookedSeats.length > 0) {
|
||||
throw new BadRequestException(
|
||||
`Cannot delete coach. ${bookedSeats.length} seat(s) have active bookings. Please wait for bookings to complete or cancel them first.`
|
||||
);
|
||||
}
|
||||
|
||||
// Check for blocked seats
|
||||
const blockedSeats = coach.seats.filter(seat => seat.blocks.length > 0);
|
||||
if (blockedSeats.length > 0) {
|
||||
throw new BadRequestException(
|
||||
`Cannot delete coach. ${blockedSeats.length} seat(s) are blocked. Please unblock them first.`
|
||||
);
|
||||
}
|
||||
|
||||
// Check for tickets
|
||||
const seatsWithTickets = coach.seats.filter(seat => seat.ticketSeats.length > 0);
|
||||
if (seatsWithTickets.length > 0) {
|
||||
throw new BadRequestException(
|
||||
`Cannot delete coach. ${seatsWithTickets.length} seat(s) have issued tickets. Please wait for travel completion.`
|
||||
);
|
||||
}
|
||||
|
||||
// Delete related seats first (now safe to do)
|
||||
await this.prisma.seat.deleteMany({ where: { coachId: id } });
|
||||
|
||||
return this.prisma.coach.delete({ where: { id } });
|
||||
|
||||
@@ -545,8 +545,13 @@ export class SchedulesService {
|
||||
});
|
||||
}
|
||||
|
||||
if (dto.coaches && dto.coaches.length > 0) {
|
||||
await this.assignCoaches(id, dto.coaches);
|
||||
if (dto.coaches !== undefined) {
|
||||
if (dto.coaches.length > 0) {
|
||||
await this.assignCoaches(id, dto.coaches);
|
||||
} else {
|
||||
// Remove all coach assignments when empty array is sent
|
||||
await this.prisma.coachAssignment.deleteMany({ where: { scheduleId: id } });
|
||||
}
|
||||
}
|
||||
|
||||
return this.getSchedule(id);
|
||||
|
||||
@@ -204,14 +204,11 @@ export default function SchedulesPage() {
|
||||
departureAt: editForm.departureAt,
|
||||
arrivalAt: editForm.arrivalAt,
|
||||
status: editForm.status,
|
||||
};
|
||||
|
||||
if (editForm.coachIds.length > 0) {
|
||||
payload.coaches = editForm.coachIds.map((coachId: string, idx: number) => ({
|
||||
coaches: editForm.coachIds.map((coachId: string, idx: number) => ({
|
||||
coachId,
|
||||
positionNumber: idx + 1,
|
||||
}));
|
||||
}
|
||||
})),
|
||||
};
|
||||
|
||||
await updateScheduleMutation.mutateAsync({
|
||||
id: editingSchedule.id,
|
||||
@@ -327,14 +324,20 @@ export default function SchedulesPage() {
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'originStation.name',
|
||||
label: 'From',
|
||||
render: (schedule: Schedule) => <span>{schedule.originStation?.name}</span>,
|
||||
},
|
||||
{
|
||||
key: 'destinationStation.name',
|
||||
label: 'To',
|
||||
render: (schedule: Schedule) => <span>{schedule.destinationStation?.name}</span>,
|
||||
key: 'route',
|
||||
label: 'Route',
|
||||
sortable: true,
|
||||
render: (schedule: Schedule) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">
|
||||
{schedule.originStation?.name || 'Unknown'}
|
||||
</span>
|
||||
<span className="text-muted-foreground">→</span>
|
||||
<span className="text-sm font-medium">
|
||||
{schedule.destinationStation?.name || 'Unknown'}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'departureAt',
|
||||
|
||||
@@ -420,7 +420,12 @@ export default function SeatsPage() {
|
||||
const seats = (coach.seats || []).filter((s: any) => s.seatNumber);
|
||||
return seats.length > 0;
|
||||
})
|
||||
.sort((a: any, b: any) => (a.sequence || 0) - (b.sequence || 0));
|
||||
.sort((a: any, b: any) => {
|
||||
// Try multiple sequence field possibilities
|
||||
const seqA = a.positionNumber ?? a.sequence ?? a.coach?.sequence ?? 999;
|
||||
const seqB = b.positionNumber ?? b.sequence ?? b.coach?.sequence ?? 999;
|
||||
return seqA - seqB;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -460,8 +465,30 @@ export default function SeatsPage() {
|
||||
<p className="text-muted-foreground mt-3">Loading seats...</p>
|
||||
</div>
|
||||
) : coachesWithSeats.length === 0 ? (
|
||||
<div className="card text-center py-12 text-muted-foreground">
|
||||
<p>No coaches with seats found for this schedule</p>
|
||||
<div className="space-y-6">
|
||||
<div className="card">
|
||||
<label className="label">Select Schedule</label>
|
||||
<select
|
||||
value={selectedSchedule}
|
||||
onChange={(e) => setSelectedSchedule(e.target.value)}
|
||||
className="input"
|
||||
>
|
||||
<option value="">Select a schedule...</option>
|
||||
{schedules.map((schedule: any) => {
|
||||
const trainNumber = schedule.train?.trainNumber || schedule.train?.name || 'N/A';
|
||||
const routeName = schedule.route?.name || 'N/A';
|
||||
const date = schedule.departureAt ? new Date(schedule.departureAt).toLocaleDateString() : 'N/A';
|
||||
return (
|
||||
<option key={schedule.id} value={schedule.id}>
|
||||
{trainNumber} - {routeName} - {date}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
<div className="card text-center py-12 text-muted-foreground">
|
||||
<p>No coaches with seats found for this schedule</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
@@ -526,7 +553,7 @@ export default function SeatsPage() {
|
||||
const seats = (coach.seats || []).filter((s: any) => s.seatNumber);
|
||||
const isExpanded = expandedCoaches.has(coach.id);
|
||||
const seatOrBedLabel = isBedCoach ? 'beds' : 'seats';
|
||||
const sequence = coachData?.sequence ?? coach?.sequence ?? index + 1;
|
||||
const sequence = coach.positionNumber ?? coach.sequence ?? coachData?.sequence ?? index + 1;
|
||||
|
||||
return (
|
||||
<div key={coach.id} className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden bg-white dark:bg-gray-800/50 shadow-md hover:shadow-lg transition-shadow">
|
||||
|
||||
@@ -75,7 +75,6 @@ export default function StationsPage() {
|
||||
lat: parseFloat(formData.get('lat') as string) || null,
|
||||
lng: parseFloat(formData.get('lng') as string) || null,
|
||||
timezone: formData.get('timezone') as string,
|
||||
distance: parseFloat(formData.get('distance') as string) || 0,
|
||||
sequence,
|
||||
isOperational: formData.get('isOperational') === 'true',
|
||||
};
|
||||
@@ -136,13 +135,7 @@ export default function StationsPage() {
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'distance',
|
||||
label: 'Distance (km)',
|
||||
render: (station: any) => (
|
||||
<span className="font-mono text-sm">{station.distance ? `${station.distance}` : '0'}</span>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
key: 'isOperational',
|
||||
label: 'Status',
|
||||
@@ -346,18 +339,6 @@ export default function StationsPage() {
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">Distance from Previous (km)</label>
|
||||
<input
|
||||
type="number"
|
||||
name="distance"
|
||||
className="input"
|
||||
defaultValue={editingStation?.distance || 0}
|
||||
min="0"
|
||||
step="0.1"
|
||||
placeholder="e.g., 150.5"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">Sequence Number *</label>
|
||||
<input
|
||||
|
||||
Reference in New Issue
Block a user