mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
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:
@@ -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;
|
||||
|
||||
@@ -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 } });
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
<label className="label">Coaches (Optional)</label>
|
||||
<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>
|
||||
))
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user