import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm'; import { BaseEntity } from '@edr/api-common'; import { TrainCrewMember, TrainCrewRole } from './train-crew-member.entity'; /** * Legacy fixed corridor segments. * * Kept only so historic rows written before segments became yard-to-yard still * read back. New assignments carry `fromYardId`/`toYardId` instead: staff pick * any two yards on the corridor, so a leg is no longer limited to the three * spans the original design hard-coded. */ export enum CrewSegment { INDODE_DIRE_DAWA = 'INDODE_DIRE_DAWA', DIRE_DAWA_NAGAD = 'DIRE_DAWA_NAGAD', FULL_CORRIDOR = 'FULL_CORRIDOR', } /** Driver duty role for one run (§2). Null for non-driving crew. */ export enum CrewDutyRole { PRIMARY = 'PRIMARY', ASSISTANT = 'ASSISTANT', BENCH_RELIEF = 'BENCH_RELIEF', } export enum CrewAssignmentStatus { PLANNED = 'PLANNED', CONFIRMED = 'CONFIRMED', COMPLETED = 'COMPLETED', REMOVED = 'REMOVED', } /** * One roster member assigned to one train schedule. * * `role` is snapshotted from the roster at assignment time: a member who later * changes role must not silently rewrite the crew of a run that already * departed. `dutyRole` and `segment` live here rather than on the roster * because they are properties of THIS run — a driver who is Primary on one * trip is Assistant on the next. Crew sizes are free-form: operations size each * run to its own need rather than to a fixed pairing case. * * Duty stamps feed the §3 monthly overtime totals. Per the agreed scope the * platform reports OT hours only; no salary is stored anywhere, and the payroll * conversion stays with finance. */ @Entity({ schema: 'freight', name: 'train_crew_assignments' }) @Index(['trainScheduleId']) @Index(['crewMemberId']) @Index(['status']) export class TrainCrewAssignment extends BaseEntity { @Column({ name: 'train_schedule_id', type: 'uuid' }) trainScheduleId!: string; @Column({ name: 'crew_member_id', type: 'uuid' }) crewMemberId!: string; @ManyToOne(() => TrainCrewMember, { onDelete: 'RESTRICT' }) @JoinColumn({ name: 'crew_member_id' }) crewMember?: TrainCrewMember; @Column({ name: 'role', type: 'varchar', length: 32 }) role!: TrainCrewRole; @Column({ name: 'duty_role', type: 'varchar', length: 16, nullable: true }) dutyRole?: CrewDutyRole | null; /** Legacy fixed segment — null on every assignment written since yard legs. */ @Column({ name: 'segment', type: 'varchar', length: 24, nullable: true }) segment?: CrewSegment | null; /** * The leg this driver works, as two yards on the corridor. * * Free-form on purpose: operations pick any yard as a handover point, so a * crew change at Meiso or Feto is expressible without a code change. The * schedule's own origin and destination bound what staff may choose. */ @Column({ name: 'from_yard_id', type: 'uuid', nullable: true }) fromYardId?: string | null; @Column({ name: 'to_yard_id', type: 'uuid', nullable: true }) toYardId?: string | null; /** * Mandatory off-duty layover at Dire Dawa (§1.3). The document gives ~5 hours * as a typical duration, not a rule, so nothing here enforces a length — the * stamps are recorded and reported. */ @Column({ name: 'layover_start_at', type: 'timestamptz', nullable: true }) layoverStartAt?: Date | null; @Column({ name: 'layover_end_at', type: 'timestamptz', nullable: true }) layoverEndAt?: Date | null; /** Worked span for this run — accumulated monthly for the §3 OT calculation. */ @Column({ name: 'duty_start_at', type: 'timestamptz', nullable: true }) dutyStartAt?: Date | null; @Column({ name: 'duty_end_at', type: 'timestamptz', nullable: true }) dutyEndAt?: Date | null; @Column({ name: 'status', type: 'varchar', length: 16, default: CrewAssignmentStatus.PLANNED, }) status!: CrewAssignmentStatus; @Column({ name: 'notes', type: 'text', nullable: true }) notes?: string | null; }