Merge pull request #180 from Tria-plc/alpha

Enhance DTOs and services to support sequence and operational status for coaches and stations
This commit is contained in:
Eyob T.
2026-06-16 17:40:01 +03:00
committed by GitHub
7 changed files with 85 additions and 20 deletions

View File

@@ -17,7 +17,10 @@ export class CreateCoachDto {
@ApiPropertyOptional({ example: 'ACTIVE', description: 'Status: ACTIVE, INACTIVE' }) @IsOptional() @IsString() status?: string;
}
export class UpdateCoachDto extends PartialType(OmitType(CreateCoachDto, ['number'] as const)) {}
export class UpdateCoachDto extends PartialType(OmitType(CreateCoachDto, ['number'] as const)) {
@ApiPropertyOptional({ example: 1, description: 'Sequence number for ordering' })
@IsOptional() @IsInt() sequence?: number;
}
export class AssignCoachDto {
@ApiProperty({ example: 'schedule-uuid', description: 'TrainSchedule UUID' }) @IsString() scheduleId: string;

View File

@@ -298,6 +298,7 @@ export class FleetService {
arrangement: dto.arrangement,
capacity: dto.capacity,
status: dto.status,
sequence: dto.sequence,
},
include: { coachType: true },
});
@@ -306,6 +307,10 @@ export class FleetService {
async deleteCoach(id: string) {
const coach = await this.prisma.coach.findUnique({ where: { id } });
if (!coach) throw new NotFoundException('Coach not found');
// Delete related seats first
await this.prisma.seat.deleteMany({ where: { coachId: id } });
return this.prisma.coach.delete({ where: { id } });
}

View File

@@ -1,4 +1,4 @@
import { IsString, IsNumber, IsOptional } from 'class-validator';
import { IsString, IsNumber, IsOptional, IsInt, IsBoolean } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CreateStationDto {
@@ -6,6 +6,9 @@ export class CreateStationDto {
@ApiProperty({ example: 'Addis Ababa' }) @IsString() name: string;
@ApiProperty({ example: 'Addis Ababa' }) @IsString() city: string;
@ApiPropertyOptional() @IsOptional() @IsString() timezone?: string;
@ApiPropertyOptional() @IsOptional() @IsString() countryCode?: string;
@ApiProperty({ example: 9.0054 }) @IsNumber() lat: number;
@ApiProperty({ example: 38.7636 }) @IsNumber() lng: number;
@ApiPropertyOptional({ example: 1 }) @IsOptional() @IsInt() sequence?: number;
@ApiPropertyOptional({ example: true }) @IsOptional() @IsBoolean() isOperational?: boolean;
}

View File

@@ -1,4 +1,4 @@
import { Injectable, NotFoundException, Inject, Optional } from '@nestjs/common';
import { Injectable, NotFoundException, Inject, Optional, BadRequestException } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { PrismaService } from '../../common/prisma.service';
import { AuditService } from '../../common/audit.service';
@@ -65,9 +65,15 @@ export class StationsService {
async update(id: string, dto: Partial<CreateStationDto>) {
const oldStation = await this.findOne(id);
const { code, name, city, timezone, lat, lng } = dto;
const data: any = { code, name, city, timezone, lat, lng };
if ('countryCode' in dto) data.countryCode = (dto as any).countryCode;
if ('sequence' in dto) data.sequence = (dto as any).sequence;
if ('isOperational' in dto) data.isOperational = (dto as any).isOperational;
const updatedStation = await this.prisma.station.update({
where: { id },
data: dto,
data,
});
await this.auditService.log({

View File

@@ -338,6 +338,14 @@ export default function CoachesPage() {
// Coaches Columns
const coachColumns = [
{
key: 'sequence',
label: 'Sequence',
sortable: true,
render: (coach: any) => (
<span className="font-mono font-semibold text-sm">{coach.sequence}</span>
),
},
{
key: 'number',
label: 'Number',
@@ -358,15 +366,6 @@ export default function CoachesPage() {
<span className="text-sm">{coach.coachType?.name || 'N/A'}</span>
),
},
{
key: 'visualization',
label: 'Seats/Beds',
render: (coach: any) => (
<div className="bg-gray-50 dark:bg-gray-900/30 rounded p-2 max-w-xs overflow-x-auto">
{renderBedVisualization(coach)}
</div>
),
},
{
key: 'arrangement',
label: 'Arrangement',

View File

@@ -40,6 +40,7 @@ interface Coach {
number: string;
coachNumber?: string;
capacity: number;
sequence?: number;
coachType?: { name: string };
}
@@ -630,7 +631,22 @@ export default function SchedulesPage() {
</div>
<div>
<div className="flex items-center justify-between mb-2">
<label className="label">Coaches (Optional)</label>
<button
type="button"
onClick={() => {
if (bulkForm.coachIds.length === coaches.length) {
setBulkForm({ ...bulkForm, coachIds: [] });
} else {
setBulkForm({ ...bulkForm, coachIds: coaches.map((c: Coach) => c.id) });
}
}}
className="text-xs text-primary hover:underline"
>
{bulkForm.coachIds.length === coaches.length ? 'Deselect All' : 'Select All'}
</button>
</div>
<div className="border border-border rounded-lg p-3 max-h-64 overflow-y-auto space-y-2">
{coaches.length === 0 ? (
<p className="text-sm text-muted-foreground">No coaches available</p>
@@ -656,7 +672,7 @@ export default function SchedulesPage() {
className="rounded"
/>
<span className="text-sm">
{coach.number || coach.coachNumber} - {coach.coachType?.name} (Cap: {coach.capacity})
Seq {coach.sequence || 'N/A'} - {coach.number || coach.coachNumber} - {coach.coachType?.name} (Cap: {coach.capacity})
</span>
</label>
))
@@ -765,7 +781,22 @@ export default function SchedulesPage() {
</div>
<div>
<label className="label">Coaches</label>
<div className="flex items-center justify-between mb-2">
<label className="label">Coaches (Optional)</label>
<button
type="button"
onClick={() => {
if (editForm.coachIds.length === coaches.length) {
setEditForm({ ...editForm, coachIds: [] });
} else {
setEditForm({ ...editForm, coachIds: coaches.map((c: Coach) => c.id) });
}
}}
className="text-xs text-primary hover:underline"
>
{editForm.coachIds.length === coaches.length ? 'Deselect All' : 'Select All'}
</button>
</div>
<div className="border border-border rounded-lg p-3 max-h-64 overflow-y-auto space-y-2">
{coaches.length === 0 ? (
<p className="text-sm text-muted-foreground">No coaches available</p>
@@ -791,7 +822,7 @@ export default function SchedulesPage() {
className="rounded"
/>
<span className="text-sm">
{coach.number || coach.coachNumber} - {coach.coachType?.name} (Cap: {coach.capacity})
Seq {coach.sequence || 'N/A'} - {coach.number || coach.coachNumber} - {coach.coachType?.name} (Cap: {coach.capacity})
</span>
</label>
))

View File

@@ -152,7 +152,13 @@ export default function SeatsPage() {
};
const isCoachBlocked = (coach: any) => {
return coach.seats?.some((s: any) => s.status === 'BLOCKED' || s.isBlocked);
const seats = (coach.seats || []).filter((s: any) => s.seatNumber && !s.seatNumber.startsWith('-'));
return seats.length > 0 && seats.every((s: any) => s.status === 'BLOCKED' || s.isBlocked);
};
const isCoachUnblocked = (coach: any) => {
const seats = (coach.seats || []).filter((s: any) => s.seatNumber && !s.seatNumber.startsWith('-'));
return seats.length > 0 && seats.every((s: any) => s.status !== 'BLOCKED' && !s.isBlocked);
};
const submitBlockCoach = async () => {
@@ -520,6 +526,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;
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">
@@ -532,7 +539,7 @@ export default function SeatsPage() {
<ChevronDown className="w-5 h-5 text-[rgb(20,113,76)]" />
</div>
<div className="text-left">
<p className="font-semibold text-foreground">Coach {coach.coachNumber}</p>
<p className="font-semibold text-foreground">{sequence} - {coach.coachNumber}</p>
<p className="text-xs text-muted-foreground">{coachTypeName} {seats.length} {seatOrBedLabel}</p>
</div>
</button>
@@ -541,8 +548,19 @@ export default function SeatsPage() {
size="sm"
onClick={() => isCoachBlocked(coach) ? handleUnblockCoach(coach) : handleBlockCoach(coach)}
className="ml-2"
disabled={!isCoachBlocked(coach) && !isCoachUnblocked(coach)}
>
{isCoachBlocked(coach) ? 'Unblock' : 'Block'}
{isCoachBlocked(coach) ? (
<>
<Unlock className="w-4 h-4" /> Unblock
</>
) : isCoachUnblocked(coach) ? (
<>
<Lock className="w-4 h-4" /> Block
</>
) : (
'Mixed Status'
)}
</ActionButton>
</div>