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:
Stephanos A
2026-06-16 19:30:09 +03:00
parent eb60fc340d
commit ad83c87e2f
5 changed files with 156 additions and 45 deletions

View File

@@ -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 } });

View File

@@ -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);

View File

@@ -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',

View File

@@ -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">

View File

@@ -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