mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 05:25:41 +00:00
New Transit Agents admin table (name, valid-from/to, active/suspended, validity badge) — DJ assign step now picks from it, active+valid only.
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { FindOptionsOrder } from 'typeorm';
|
||||
|
||||
import { CreateTransitAgentDto } from './dto/create-transit-agent.dto';
|
||||
import { UpdateTransitAgentDto } from './dto/update-transit-agent.dto';
|
||||
import { TransitAgent } from './entities/transit-agent.entity';
|
||||
import { TransitAgentsRepository } from './transit-agents.repository';
|
||||
|
||||
export type TransitAgentValidityStatus = 'VALID' | 'NOT_STARTED' | 'EXPIRED';
|
||||
|
||||
export type TransitAgentView = TransitAgent & {
|
||||
validityStatus: TransitAgentValidityStatus;
|
||||
};
|
||||
|
||||
type TransitAgentListFilter = {
|
||||
isActive?: boolean;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
sortBy?: string;
|
||||
sortOrder?: string;
|
||||
};
|
||||
|
||||
/** Today as `yyyy-MM-dd`, matching the `date`-typed validity columns. */
|
||||
function todayISODate(): string {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function validityStatus(agent: Pick<TransitAgent, 'validFrom' | 'validTo'>): TransitAgentValidityStatus {
|
||||
const today = todayISODate();
|
||||
if (today < agent.validFrom) return 'NOT_STARTED';
|
||||
if (today > agent.validTo) return 'EXPIRED';
|
||||
return 'VALID';
|
||||
}
|
||||
|
||||
function withValidityStatus(agent: TransitAgent): TransitAgentView {
|
||||
return { ...agent, validityStatus: validityStatus(agent) };
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class TransitAgentsService {
|
||||
constructor(private readonly transitAgentsRepository: TransitAgentsRepository) {}
|
||||
|
||||
async findAll(filter: TransitAgentListFilter = {}): Promise<{
|
||||
data: TransitAgentView[];
|
||||
meta: { total: number; page: number; pageSize: number; totalPages: number };
|
||||
}> {
|
||||
const page = filter.page ?? 1;
|
||||
const pageSize = filter.pageSize ?? 500;
|
||||
const sortBy = ['name', 'validFrom', 'validTo', 'isActive'].includes(filter.sortBy ?? '')
|
||||
? (filter.sortBy as keyof TransitAgent)
|
||||
: 'name';
|
||||
const sortOrder = filter.sortOrder?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
|
||||
|
||||
const [data, total] = await this.transitAgentsRepository.findAndCount({
|
||||
where: filter.isActive === undefined ? {} : { isActive: filter.isActive },
|
||||
order: { [sortBy]: sortOrder } as FindOptionsOrder<TransitAgent>,
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
});
|
||||
|
||||
return {
|
||||
data: data.map(withValidityStatus),
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages: Math.max(1, Math.ceil(total / pageSize)),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Active and currently inside its validity window — the DJ assignment dropdown. */
|
||||
async findAssignable(): Promise<TransitAgent[]> {
|
||||
return this.transitAgentsRepository.findAssignable(todayISODate());
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<TransitAgentView> {
|
||||
const agent = await this.transitAgentsRepository.findById(id);
|
||||
if (!agent) {
|
||||
throw new NotFoundException(`Transit agent ${id} not found`);
|
||||
}
|
||||
return withValidityStatus(agent);
|
||||
}
|
||||
|
||||
/** Used by the assignment flow — rejects a suspended or out-of-window officer. */
|
||||
async getAssignable(id: string): Promise<TransitAgent> {
|
||||
const agent = await this.transitAgentsRepository.findById(id);
|
||||
if (!agent) {
|
||||
throw new BadRequestException('Selected transit officer was not found.');
|
||||
}
|
||||
if (!agent.isActive) {
|
||||
throw new BadRequestException(`${agent.name} is suspended — pick another transit officer.`);
|
||||
}
|
||||
if (validityStatus(agent) !== 'VALID') {
|
||||
throw new BadRequestException(
|
||||
`${agent.name}'s validity window has expired — pick another transit officer or extend their dates.`,
|
||||
);
|
||||
}
|
||||
return agent;
|
||||
}
|
||||
|
||||
async create(dto: CreateTransitAgentDto): Promise<TransitAgentView> {
|
||||
if (dto.validTo < dto.validFrom) {
|
||||
throw new BadRequestException('Valid-to date must be on or after valid-from date.');
|
||||
}
|
||||
const agent = await this.transitAgentsRepository.create({
|
||||
name: dto.name.trim(),
|
||||
validFrom: dto.validFrom,
|
||||
validTo: dto.validTo,
|
||||
isActive: dto.isActive ?? true,
|
||||
});
|
||||
return withValidityStatus(agent);
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateTransitAgentDto): Promise<TransitAgentView> {
|
||||
const current = await this.findById(id);
|
||||
const nextValidFrom = dto.validFrom ?? current.validFrom;
|
||||
const nextValidTo = dto.validTo ?? current.validTo;
|
||||
if (nextValidTo < nextValidFrom) {
|
||||
throw new BadRequestException('Valid-to date must be on or after valid-from date.');
|
||||
}
|
||||
|
||||
const updated = await this.transitAgentsRepository.update(id, {
|
||||
...dto,
|
||||
...(dto.name ? { name: dto.name.trim() } : {}),
|
||||
});
|
||||
|
||||
if (!updated) {
|
||||
throw new NotFoundException(`Transit agent ${id} not found`);
|
||||
}
|
||||
return withValidityStatus(updated);
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
await this.findById(id);
|
||||
await this.transitAgentsRepository.softDelete(id);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user