Initial commit: Aeroflot Flights Web Angular 12 application

This commit is contained in:
2026-04-03 10:10:52 +03:00
commit 2342f2e66e
1311 changed files with 128350 additions and 0 deletions
+86
View File
@@ -0,0 +1,86 @@
import { FlightModel } from './models-legacy';
export interface AirlinesReference<T> {
Aeroflot: T;
Aurora: T;
Rossiya: T;
Pobeda: T;
AirFrance: T;
}
export const AirlineCodes: AirlinesReference<string> = {
Aeroflot: 'SU',
Aurora: 'HZ',
Rossiya: 'FV',
Pobeda: 'DP',
AirFrance: 'AF',
};
export const airlineCodes = Object.values(AirlineCodes);
export interface Airline {
code: string;
url: string;
registrationUrl: string;
canViewStatus: boolean;
canViewNativeStatus: boolean;
canRedirect: boolean;
canRegister: boolean;
}
export const Airlines: AirlinesReference<Airline> = {
Aeroflot: {
code: AirlineCodes.Aeroflot,
url: 'https://www.aeroflot.ru/sb/ckin/app/ru-ru',
registrationUrl: 'https://www.aeroflot.ru/sb/ckin/app/ru-ru',
canViewStatus: true,
canViewNativeStatus: true,
canRedirect: false,
canRegister: true,
},
Aurora: {
code: AirlineCodes.Aurora,
url: 'https://www.flyaurora.ru',
registrationUrl: 'https://www.flyaurora.ru',
canViewStatus: true,
canViewNativeStatus: false,
canRedirect: true,
canRegister: true,
},
Rossiya: {
code: AirlineCodes.Rossiya,
url: 'https://www.rossiya-airlines.com',
registrationUrl: 'https://www.rossiya-airlines.com/flight-with-us/before_flight/the_ways_of_check-in/',
canViewStatus: true,
canViewNativeStatus: true,
canRedirect: false,
canRegister: true,
},
Pobeda: {
code: AirlineCodes.Pobeda,
url: 'https://www.pobeda.aero',
registrationUrl: 'https://www.pobeda.aero',
canViewStatus: true,
canViewNativeStatus: false,
canRedirect: true,
canRegister: true,
},
AirFrance: {
code: AirlineCodes.AirFrance,
url: '',
registrationUrl: '',
canViewStatus: false,
canViewNativeStatus: false,
canRedirect: false,
canRegister: false,
},
};
export const airlines: Airline[] = Object.values(Airlines);
export function getAirline(flight?: FlightModel): Airline | undefined {
if (!flight) return;
const { operatingBy } = flight;
const code = operatingBy?.actual || operatingBy?.scheduled;
return airlines.find((a) => a.code === code);
}
+11
View File
@@ -0,0 +1,11 @@
import { Directive } from '@angular/core';
import { Subject } from 'rxjs';
@Directive({})
export abstract class Destroyable {
protected destroy$ = new Subject();
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
@@ -0,0 +1,4 @@
export enum DispatchMode {
bus = 'Bus',
bridge = 'Bridge',
}
@@ -0,0 +1,7 @@
export enum FlightIcon {
routeChange = 'routeChange',
return = 'return',
intermediateLanding = 'intermediateLanding',
intermediateLandingMulti = 'intermediateLandingMulti',
transfer = 'transfer',
}
@@ -0,0 +1,6 @@
export enum FlightLoadedState {
board,
actual,
detailed,
scheduled,
}
@@ -0,0 +1,16 @@
export enum FlightRequestType {
Route = 'route',
Arrival = 'arrival',
Flight = 'flight',
Departure = 'departure',
Schedule = 'schedule',
Undefined = 'undefined',
Details = 'Details'
}
export enum ViewType {
Onlineboard = 'onlineboard',
Schedule = 'schedule',
FlightsMap = 'flights-map',
Undefined = 'undefined',
}
@@ -0,0 +1,10 @@
export enum FlightStatusLegacy {
scheduled = 'Scheduled',
sent = 'Sent',
inFlight = 'InFlight',
landed = 'Landed',
arrived = 'Arrived',
delayed = 'Delayed',
cancelled = 'Cancelled',
unknown = 'Unknown',
}
@@ -0,0 +1,8 @@
export * from './flight-icon.enum';
export * from './flight-status.enum';
export * from './language.enum';
export * from './dispatch-mode.enum';
export * from './operation-status.enum';
export * from './time-type.enum';
export * from './flight-loaded-state.enum';
export * from './route-type.enum';
@@ -0,0 +1,12 @@
export enum Language {
ru = 'ru',
en = 'en',
de = 'de',
fr = 'fr',
es = 'es',
it = 'it',
cn = 'cn',
ko = 'ko',
jp = 'jp',
zh = 'zh',
}
@@ -0,0 +1,5 @@
export enum OperationStatusLegacy {
scheduled = 'Scheduled',
inProgress = 'InProgress',
finished = 'Finished',
}
@@ -0,0 +1,5 @@
export enum RouteTypeLegacy {
Direct = 'Direct',
MultiLeg = 'MultiLeg',
Connecting = 'Connecting',
}
@@ -0,0 +1,5 @@
export enum TimeType {
scheduled = 'Scheduled',
estimated = 'Estimated',
actual = 'Actual',
}
@@ -0,0 +1,2 @@
export * from './translate-http-loader.factory';
export * from './schedule.factory';
@@ -0,0 +1,70 @@
import { ScheduleItemModel, FlightModel } from '@shared/models-legacy';
import { IRouteContract } from '@shared/models-legacy/contracts';
import { createFlights } from '@shared/models-legacy/populate.logic';
import * as moment from 'moment';
export function createScheduleFromRoutes(
contracts: IRouteContract[],
dateFrom: Date,
dateTo: Date,
) {
const flights = createFlights(contracts);
return createSchedule(flights, dateFrom, dateTo);
}
export function createSchedule(
flights: FlightModel[],
dateFrom: Date,
dateTo: Date,
): ScheduleItemModel[] {
function byDate(flight: FlightModel) {
return moment(flight.dateToSearchBy).startOf('day').toDate().getTime();
}
const result = flights.groupBy(byDate).map(({ key, items: flights }) => {
return new ScheduleItemModel({
date: moment(key).toDate(),
flights,
});
});
return addMissingDays(result, dateFrom, dateTo);
}
function addMissingDays(
items: ScheduleItemModel[],
dateFrom: Date,
dateTo: Date,
): ScheduleItemModel[] {
const map = new Map<number, ScheduleItemModel>();
for (const item of items) {
if (dateFrom <= item.date && item.date <= dateTo)
map.set(item.date.getTime(), item);
}
const daysBetween = moment(dateTo).diff(dateFrom, 'day');
for (let i = 0; i <= daysBetween; i++) {
const date = moment(dateFrom).add(i, 'day').toDate();
const key = date.getTime();
if (map.has(key)) continue;
map.set(key, new ScheduleItemModel({ date }));
}
items = [...map.values()];
function byDate(a: ScheduleItemModel, b: ScheduleItemModel) {
if (a.date > b.date) return 1;
if (a.date < b.date) return -1;
return 0;
}
return items.sort(byDate);
}
const ScheduleLegacyFactories = {
createScheduleFromRoutes,
createSchedule,
};
export default ScheduleLegacyFactories;
@@ -0,0 +1,6 @@
import { HttpClient } from '@angular/common/http';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
export function TranslateHttpLoaderFactory(httpClient: HttpClient) {
return new TranslateHttpLoader(httpClient);
}
@@ -0,0 +1 @@
export const DATE_FORMAT = 'DD.MM.YYYY';
@@ -0,0 +1 @@
export * from './dateHelper';
@@ -0,0 +1,33 @@
// https://github.com/ai/convert-layout/blob/main/convert.js
function convert(keys: string, values: string) {
const reverse = {};
const full = {};
for (let keyIndex = keys.length; keyIndex--; ) {
full[keys[keyIndex].toUpperCase()] = values[keyIndex].toUpperCase();
full[keys[keyIndex]] = values[keyIndex];
}
for (const key in full) {
reverse[full[key]] = key;
}
return {
fromEn: function (str) {
return str.replace(/./g, function (ch) {
return full[ch] || ch;
});
},
toEn: function (str) {
return str.replace(/./g, function (ch) {
return reverse[ch] || ch;
});
},
};
}
// copied from build of convert-layout
export const ruKeyboardLayout = convert(
'.exportsfunc"#$&\',/:;<>?@[]^`abdghijklmqvwyz{|}~',
'юучзщкеыагтсЭ№;?эб.ЖжБЮ,"хъ:ёфивпршолдьймцняХ/ЪЁ',
);
@@ -0,0 +1,11 @@
type IWithName = {
name: string;
};
export function sortByName<T extends IWithName>(a: T, b: T) {
return sortStrings(a.name, b.name);
}
export function sortStrings(a: string, b: string) {
return a.toLowerCase().localeCompare(b.toLowerCase());
}
+1
View File
@@ -0,0 +1 @@
export * from './shared.module';
@@ -0,0 +1,32 @@
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { Observable } from 'rxjs';
import { takeUntil, tap } from 'rxjs/operators';
import { HttpCancelService } from '../services/http-cancel.service';
@Injectable()
export class AppInterceptor implements HttpInterceptor {
constructor(private httpCancelService: HttpCancelService, private router: Router) {}
intercept<T>(req: HttpRequest<T>, next: HttpHandler): Observable<HttpEvent<T>> {
return next.handle(req).pipe(
takeUntil(this.httpCancelService.onCancelPendingRequests()),
tap({
next: () => {},
error: (err) => {
if (err instanceof HttpErrorResponse) {
switch (err.status) {
case 404:
this.router.navigateByUrl('/error/404');
break;
case 500:
this.router.navigateByUrl('/error');
break;
}
}
},
}),
);
}
}
@@ -0,0 +1,29 @@
import { TemplateRef, ViewContainerRef } from '@angular/core';
import { LocalizationService, } from '../services';
import { LocalisationReadyDirective } from './localisation-ready.directive';
import { DictionariesService } from './../../modules/components/page-filters/services/dictionaries-service';
describe('LocalisationReadyDirective', () => {
let locale: LocalizationService;
let dicts: DictionariesService;
let tempalte: TemplateRef<any>;
let view: ViewContainerRef;
let directive: LocalisationReadyDirective;
beforeEach(() => {
dicts = {
ready$: Promise.resolve()
} as any
locale = {
ready$: Promise.resolve()
} as any;
tempalte = {} as any;
view = {
createEmbeddedView: jasmine.createSpy().withArgs(tempalte).and.callThrough(),
} as any;
directive = new LocalisationReadyDirective(dicts, locale, tempalte, view);
});
it('should wait for localisation to be ready and the create view', async () => {
await directive.ngOnInit();
expect(view.createEmbeddedView).toHaveBeenCalled();
});
});
@@ -0,0 +1,20 @@
import { Directive, TemplateRef, ViewContainerRef } from '@angular/core';
import { DictionariesService } from '@app/modules/components/page-filters/services/dictionaries-service';
import { LocalizationService } from '../services';
@Directive({
selector: '[localisationReady]',
})
export class LocalisationReadyDirective {
constructor(
private dictionaries: DictionariesService,
private locale: LocalizationService,
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef,
) {}
async ngOnInit() {
await Promise.all([this.locale.ready$, this.dictionaries.ready$]);
this.viewContainer.createEmbeddedView(this.templateRef);
}
}
@@ -0,0 +1,12 @@
export const calendarDe = {
firstDayOfWeek: 1,
monthNames: ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'],
monthNamesShort: ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'],
dayNames: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'],
dayNamesShort: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'],
dayNamesMin: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'],
today: 'Heute',
clear: 'Leeren',
dateFormat: 'tt.mm.jj',
weekHeader: 'Wo',
};
@@ -0,0 +1,12 @@
export const calendarEn = {
firstDayOfWeek: 1,
monthNames: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
monthNamesShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
dayNames: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
dayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
dayNamesMin: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'],
today: 'Today',
clear: 'Clear',
dateFormat: 'dd.mm.yy',
weekHeader: 'Wk.',
};
@@ -0,0 +1,25 @@
export const calendarEs = {
firstDayOfWeek: 1,
monthNames: [
'Enero',
'Febrero',
'Marzo',
'Abril',
'Mayo',
'Junio',
'Julio',
'Agosto',
'Septiembre',
'Octubre',
'Noviembre',
'Diciembre',
],
monthNamesShort: ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic'],
dayNames: ['Domingo', 'Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado'],
dayNamesShort: ['Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb'],
dayNamesMin: ['D', 'L', 'M', 'X', 'J', 'V', 'S'],
today: 'Hoy',
clear: 'Claro',
dateFormat: 'dd/mm/yy',
weekHeader: 'Sm.',
};
@@ -0,0 +1,12 @@
export const calendarFr = {
firstDayOfWeek: 1,
monthNames: ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'],
monthNamesShort: ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun', 'Jul', 'Aoû', 'Sep', 'Oct', 'Nov', 'Déc'],
dayNames: ['Dimanche', 'Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi'],
dayNamesShort: ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'],
dayNamesMin: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
today: "Aujourd'hui",
clear: 'Clair',
dateFormat: 'dd.mm.yy',
weekHeader: 'Sem.',
};
@@ -0,0 +1,25 @@
export const calendarIt = {
firstDayOfWeek: 1,
monthNames: [
'Gennaio',
'Febbraio',
'Marzo',
'Aprile',
'Maggio',
'Giugno',
'Luglio',
'Agosto',
'Settembre',
'Ottobre',
'Novembre',
'Dicembre',
],
monthNamesShort: ['Gen', 'Feb', 'Mar', 'Apr', 'Maggio', 'Giu', 'Lug', 'Ago', 'Set', 'Ott', 'Nov', 'Dic'],
dayNames: ['Domenica', 'Lunedì', 'Martedì', 'Mercoledì', 'Giovedì', 'Venerdì', 'Sabato'],
dayNamesShort: ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab'],
dayNamesMin: ['Do', 'Lu', 'Ma', 'Me', 'Gi', 'Ve', 'Sa'],
today: 'Oggi',
clear: 'Cancella',
dateFormat: 'gg.mm.aa',
weekHeader: 'sett.',
};
@@ -0,0 +1,12 @@
export const calendarJp = {
firstDayOfWeek: 1,
monthNames: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
monthNamesShort: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
dayNames: ['日曜日', '月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日'],
dayNamesShort: ['日', '月', '火', '水', '木', '金', '土'],
dayNamesMin: ['日', '月', '火', '水', '木', '金', '土'],
today: '今日',
clear: 'クリア',
dateFormat: 'yy.mm.dd',
weekHeader: '週',
};
@@ -0,0 +1,12 @@
export const calendarKo = {
firstDayOfWeek: 1,
monthNames: ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'],
monthNamesShort: ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'],
dayNames: ['일요일', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일'],
dayNamesShort: ['일', '월', '화', '수', '목', '금', '토'],
dayNamesMin: ['일', '월', '화', '수', '목', '금', '토'],
today: '오늘',
clear: '지우기',
dateFormat: '일.월.년',
weekHeader: '주',
};
@@ -0,0 +1,12 @@
export const calendarRu = {
firstDayOfWeek: 1,
monthNames: ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'],
monthNamesShort: ['Янв', 'Фев', 'Мар', 'Апр', 'Май', 'Июн', 'Июл', 'Авг', 'Сен', 'Окт', 'Ноя', 'Дек'],
dayNames: ['Воскресенье', 'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота'],
dayNamesShort: ['Воск', 'Пон', 'Вт', 'Ср', 'Четв', 'Пят', 'Суб'],
dayNamesMin: ['Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб'],
today: 'Сегодня',
clear: 'Очистить',
dateFormat: 'dd.mm.yy',
weekHeader: 'Нд.',
};
@@ -0,0 +1,12 @@
export const calendarZh = {
firstDayOfWeek: 1,
monthNames: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'],
monthNamesShort: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'],
dayNames: ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'],
dayNamesShort: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'],
dayNamesMin: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'],
today: '今天',
clear: '清除',
dateFormat: 'yy.mm.dd.',
weekHeader: '周',
};
@@ -0,0 +1 @@
export * from './calendar-ru';
@@ -0,0 +1,12 @@
import { IFlightId } from './flight-id.model';
import { OnboardServiceModel } from './onboard-service.model';
export interface AircraftTypeModel {
title: string;
name: string;
registration: string;
type: string;
configuration: string;
previousFlightId: IFlightId;
onBoardServices: OnboardServiceModel[];
}
@@ -0,0 +1,18 @@
import { AircraftTypeModel } from './aircraft-type.model';
import { IFlightId } from './flight-id.model';
import { Seat } from './seat.model';
export interface IAircraft {
readonly scheduled: IAircraftScheduled;
actual: AircraftTypeModel;
configuration: IAircraftConfiguration;
previousFlight: IFlightId;
}
export interface IAircraftScheduled {
readonly title: string;
}
export interface IAircraftConfiguration {
seats: Seat[];
}
@@ -0,0 +1,5 @@
export interface AirportInfoModificationModel {
airport: boolean;
city: boolean;
country: boolean;
}
@@ -0,0 +1,118 @@
import * as moment from 'moment';
export interface AppSettings {
showDebugVersion: boolean;
buyPeriod: {
min: number;
max: number;
};
refreshInterval: number;
flightStatusAvailableFrom: number;
boardSearchFrom: number;
boardSearchTo: number;
scheduleSearchFrom: number;
scheduleSearchTo: number;
flightsMapSearchFrom: number;
flightsMapSearchTo: number;
boardMaxDate: Date;
boardMinDate: Date;
scheduleMaxDate: Date;
scheduleMinDate: Date;
//intervalId: number | null;
//startValidationDatesUpdatingInterval(): void;
//stopValidationDatesUpdatingInterval(): void;
}
export const defaultSettings: AppSettings = {
showDebugVersion: false,
buyPeriod: {
min: 2,
max: 330,
},
refreshInterval: 3 * 60 * 1000, // 3 min
// refreshInterval: 5 * 1000, // 5 sec
flightStatusAvailableFrom: 2,
boardSearchFrom: 1,
boardSearchTo: 7,
scheduleSearchFrom: 1,
scheduleSearchTo: 330,
flightsMapSearchFrom: 1,
flightsMapSearchTo: 186,
boardMaxDate: new Date(),
boardMinDate: new Date(),
scheduleMaxDate: new Date(),
scheduleMinDate: new Date(),
//intervalId: null,
//startValidationDatesUpdatingInterval() {
// always clear previous interval because appSettings instance can be
// shared between several components; So calling settings.observeDayChange
// from different parts will cause this.intervalId rewrite and other
// intervals won't be cleared.
// if (this.intervalId) {
// this.stopValidationDatesUpdatingInterval();
// }
// this.intervalId = setInterval(() => {
// this.boardMaxDate = moment().startOf('day').add(this.boardSearchTo, 'day').toDate();
// this.boardMinDate = moment().startOf('day').add(-this.boardSearchFrom, 'day').toDate();
// this.scheduleMaxDate = moment().startOf('day').add(this.scheduleSearchTo, 'day').toDate();
// this.scheduleMinDate = moment().startOf('day').add(-this.scheduleSearchFrom, 'day').toDate();
// this.flightsMapMaxDate = moment().startOf('day').add(this.flightsMapSearchTo, 'day').toDate();
// this.flightsMapMinDate = moment().startOf('day').add(-this.flightsMapSearchFrom, 'day').toDate();
// }, 60 * 1000);
// },
// stopValidationDatesUpdatingInterval() {
// if (!this.intervalId) {
// return; //already cleared
// }
// clearInterval(this.intervalId);
// this.intervalId = null;
// },
};
export interface AppSettingsContract {
showDebugVersion: string;
uiOptions: UIOptionsConfiguration;
}
export interface UIOptionsConfiguration {
isTestVersion: string;
filter: UIOptionsFilterConfiguration;
buttons: UIOptionsButtonsConfiguration;
}
export interface UIOptionsFilterConfiguration {
schedule: UIOptionsFilterScheduleConfiguration;
onlineboard: UIOptionsFilterOnlineboardConfiguration;
}
export interface UIOptionsFilterScheduleConfiguration {
searchFrom: string;
searchTo: string;
timeStep: string;
}
export interface UIOptionsFilterOnlineboardConfiguration {
searchFrom: string;
searchTo: string;
timeStep: string;
}
export interface UIOptionsButtonsConfiguration {
buyTicket: UIOptionsButtonsBuyTicketConfiguration;
flightStatus: UIOptionsButtonsFlightStatusConfiguration;
}
export interface UIOptionsButtonsBuyTicketConfiguration {
period: UIOptionsButtonsBuyTicketPeriodConfiguration;
}
export interface UIOptionsButtonsBuyTicketPeriodConfiguration {
max: string;
min: string;
}
export interface UIOptionsButtonsFlightStatusConfiguration {
visible: string;
availableFrom: string;
}
@@ -0,0 +1,17 @@
import { IFlightArrivalStationTimes } from '@typings/flight/flight-station';
import { DispatchMode, OperationStatusLegacy } from '../enumerators';
import { ArrivalTimesModel } from './arrival-times.model';
import { IAirportInfo } from '@typings/airport-info';
export interface IArrivalStation {
scheduled: IAirportInfo;
latest: IAirportInfo;
bagBelt: string;
terminal?: string;
deboardingStatus: OperationStatusLegacy;
deboardingStatusOriginal?: OperationStatusLegacy;
_times: IFlightArrivalStationTimes;
times: ArrivalTimesModel;
dispatch: DispatchMode;
gate: string;
}
@@ -0,0 +1,11 @@
import { PeriodFlightTimeModel } from './period-flight-time.model';
import { IFlightTime } from './flight-time.model';
import { ITimesModel } from './times.model';
export interface ArrivalTimesModel extends ITimesModel {
estimatedTouchDown: IFlightTime;
estimatedBlockOn: IFlightTime;
actualTouchDown: IFlightTime;
actualBlockOn: IFlightTime;
deboarding: PeriodFlightTimeModel;
}
@@ -0,0 +1,3 @@
export class IBookingClasses {
codes: string[];
}
@@ -0,0 +1,59 @@
import { IConnectingFlight } from './contracts';
import { ConnectingFlight } from './flight.model';
import { IFlightStrategy } from './flight.strategy';
import { RouteTypeLegacy } from '../enumerators';
import { createFlights } from './populate.logic';
export class ConnectingFlightStrategy implements IFlightStrategy {
constructor(private getStrategy: (type: RouteTypeLegacy) => IFlightStrategy) {}
getLegs(flight: ConnectingFlight) {
return flight.flights
.map((f) => this.getStrategy(f.routeType).getLegs(f))
.reduce((allLegs, flightLegs) => allLegs.concat(flightLegs), []);
}
getFlights(flight: ConnectingFlight) {
return [...flight.flights];
}
getFirstLeg(flight: ConnectingFlight) {
const first = this.getFirstFlight(flight);
return this.getStrategy(first.routeType).getFirstLeg(first);
}
getCurrentLeg(flight: ConnectingFlight) {
const legs = this.getLegs(flight);
return legs.find((l) => l.isCurrent) ?? legs[0];
}
getLastLeg(flight: ConnectingFlight) {
const last = this.getLastFlight(flight);
return this.getStrategy(last.routeType).getLastLeg(last);
}
getFirstFlight(flight: ConnectingFlight) {
return flight.flights[0];
}
getLastFlight(flight: ConnectingFlight) {
return flight.flights[flight.flights.length - 1];
}
getFlightId(flight: ConnectingFlight, isLast: boolean) {
const { flights } = flight;
const single = isLast ? flights[flights.length - 1] : flights[0];
return this.getStrategy(single.routeType).getFlightId(single);
}
fromContract(contract: IConnectingFlight) {
const flights = createFlights(contract.flights);
const flight: Partial<ConnectingFlight> = {
...contract,
isMultiFlight: true,
isMultiLeg: true,
flights,
};
return flight;
}
}
@@ -0,0 +1,8 @@
export interface IAirportInfoContract {
airport: string;
airportCode: string;
city: string;
cityCode: string;
country: string;
countryCode: string;
}
@@ -0,0 +1,20 @@
import { IFlightTimeContract, IPeriodFlightTimeContract } from './flight-time.contract';
import { OperationStatusLegacy } from '../../enumerators';
import { IStationContract } from './station.contract';
export interface IArrivalStationContract extends IStationContract {
bagBelt: string;
deboardingStatus: OperationStatusLegacy;
times: IArrivalTimesContract;
}
export interface IArrivalTimesContract {
scheduledArrival: IFlightTimeContract;
estimatedTouchDown: IFlightTimeContract;
estimatedBlockOn: IFlightTimeContract;
actualTouchDown: IFlightTimeContract;
actualBlockOn: IFlightTimeContract;
deboarding: IPeriodFlightTimeContract;
actualTime: IFlightTimeContract;
}
@@ -0,0 +1,21 @@
import { OperationStatusLegacy } from '../../enumerators';
import { IFlightTimeContract, IPeriodFlightTimeContract } from './flight-time.contract';
import { IStationContract } from './station.contract';
export interface IDepartureStationContract extends IStationContract {
checkinStatus: string;
boardingStatus: OperationStatusLegacy;
times: IDepartureTimesContract;
boardingStatusOriginal: string;
}
export interface IDepartureTimesContract {
scheduledDeparture: IFlightTimeContract;
estimatedBlockOff: IFlightTimeContract;
estimatedTakeOff: IFlightTimeContract;
actualBlockOff: IFlightTimeContract;
actualTakeOff: IFlightTimeContract;
checkIn: IPeriodFlightTimeContract;
boarding: IPeriodFlightTimeContract;
actualTime: IFlightTimeContract;
}
@@ -0,0 +1,20 @@
import { DayChangeModel } from '../day-change.model';
export interface IFlightTimeContract {
utc: Date;
local: Date;
localTime: string;
tzOffset: number;
dayChange: DayChangeModel;
unix: number;
}
export interface IPeriodFlightTimeContract {
start: IFlightTimeContract;
end: IFlightTimeContract;
}
export interface IUnixUtcDateContract {
utc: Date;
unix: number;
}
@@ -0,0 +1,15 @@
import { IOperatingByContract } from './operator.contract';
export interface IFlightContract {
operatingBy: IOperatingByContract;
flightId?: IFlightContractId;
pId: string;
}
export interface IFlightContractId {
carrier: string;
flightNumber: string;
suffix: string;
date: Date;
localDate?: Date
}
@@ -0,0 +1,9 @@
export * from './airport-info.contract';
export * from './arrival-station.contract';
export * from './departure-station.contract';
export * from './flight-time.contract';
export * from './flight.contract';
export * from './leg.contract';
export * from './operator.contract';
export * from './route.contract';
export * from './station.contract';
@@ -0,0 +1,26 @@
import { FlightStatusLegacy } from '../../enumerators';
import { IArrivalStationContract } from './arrival-station.contract';
import { IDepartureStationContract } from './departure-station.contract';
import { IDaysOfWeek } from '../days-of-week.model';
import { EquipmentModel } from '../equipment.model';
import { PassengersModel } from '../passengers.model';
import { TrafficRestrictionsModel } from '../traffic-restrictions.model';
type ILegContractFlags = {
checkinAvailable: boolean;
returnToAirport: boolean;
routeChanged: boolean;
};
export interface ILegContract {
updated: Date;
status: FlightStatusLegacy;
departure: IDepartureStationContract;
arrival: IArrivalStationContract;
daysOfWeek: IDaysOfWeek;
equipment: EquipmentModel;
passengers: PassengersModel;
trafficRestrictions: TrafficRestrictionsModel;
flags: ILegContractFlags;
}
@@ -0,0 +1,10 @@
export interface IOperatorContract {
carrier: string;
flightNumber: string;
}
export interface IOperatingByContract {
scheduled: string;
actual: string;
operators: IOperatorContract[];
}
@@ -0,0 +1,20 @@
import { IFlightContract } from './flight.contract';
import { ILegContract } from './leg.contract';
import { FlightStatusLegacy, RouteTypeLegacy } from '../../enumerators/';
export interface IRouteContract extends IFlightContract {
routeType: RouteTypeLegacy;
status: FlightStatusLegacy;
}
export interface IDirectFlight extends IRouteContract {
leg: ILegContract;
}
export interface IMultiFlight extends IRouteContract {
legs: ILegContract[];
}
export interface IConnectingFlight extends IRouteContract {
flights: (IDirectFlight | IMultiFlight)[];
}
@@ -0,0 +1,13 @@
import { DispatchMode } from '../../enumerators';
import { IAirportInfoContract } from './airport-info.contract';
import { ITransition } from '../transition.model';
export interface IStationContract {
scheduled: IAirportInfoContract;
latest: IAirportInfoContract;
terminal: string;
gate: string;
parkingStand: string;
dispatch: DispatchMode;
transition: ITransition;
}
@@ -0,0 +1,4 @@
export interface DayChangeModel {
value: number;
title: string;
}
@@ -0,0 +1,4 @@
export interface IDaysOfWeek {
current: string;
flight: string;
}
@@ -0,0 +1,19 @@
import { IAirportInfo } from '@typings/airport-info';
import { IFlightDepartureStationTimes } from '@typings/flight/flight-station';
import { DispatchMode, OperationStatusLegacy } from '../enumerators';
import { DepartureTimesModel } from './departure-times.model';
export interface IDepartureStation {
scheduled: IAirportInfo;
checkinStatus: string;
_times: IFlightDepartureStationTimes;
times: DepartureTimesModel;
latest?: IAirportInfo;
gate: string;
terminal?: string;
parkingStand: string;
dispatch: DispatchMode;
boardingStatus: OperationStatusLegacy;
boardingStatusOriginal: OperationStatusLegacy;
checkinStatusOriginal?: OperationStatusLegacy;
}
@@ -0,0 +1,12 @@
import { IFlightTime } from './flight-time.model';
import { PeriodFlightTimeModel } from './period-flight-time.model';
import { ITimesModel } from './times.model';
export interface DepartureTimesModel extends ITimesModel {
estimatedTakeOff: IFlightTime;
estimatedBlockOff: IFlightTime;
actualTakeOff: IFlightTime;
actualBlockOff: IFlightTime;
boarding: PeriodFlightTimeModel;
checkIn: PeriodFlightTimeModel;
}
@@ -0,0 +1,45 @@
import { IFlightFactory } from '.';
import { IDirectFlight } from './contracts';
import { DirectFlight } from './flight.model';
import { IFlightStrategy } from './flight.strategy';
export class DirectFlightStrategy implements IFlightStrategy {
getLegs(flight: DirectFlight) {
return [flight.leg];
}
getFlights(flight: DirectFlight) {
return [flight];
}
getFirstLeg(flight: DirectFlight) {
return flight.leg;
}
getCurrentLeg(flight: DirectFlight) {
return flight.leg;
}
getLastLeg(flight: DirectFlight) {
return flight.leg;
}
getFirstFlight(flight: DirectFlight) {
return flight;
}
getLastFlight(flight: DirectFlight) {
return flight;
}
getFlightId(flight: DirectFlight) {
return flight.flightId;
}
fromContract(contract: IDirectFlight, { createLeg }: IFlightFactory) {
const leg = createLeg(contract.leg);
const flight: Partial<DirectFlight> = {
...contract,
leg,
};
return flight;
}
}
@@ -0,0 +1,53 @@
import { Duration, getDuration, getDurationDiff } from './duration.model';
import * as moment from 'moment';
describe('DurationMode', () => {
it('should handle empty values', () => {
expect(getDuration(new Date(), undefined)).toBeUndefined();
expect(getDuration(undefined, new Date())).toBeUndefined();
expect(getDuration(undefined, undefined)).toBeUndefined();
});
it('should return duration from dates', () => {
const from = moment();
const to = from.clone();
to.add(5, 'hour').add(12, 'minute');
const actual = getDuration(from.toDate(), to.toDate());
const expected: Duration = { days: 0, hours: 5, minutes: 12 };
expect(actual).toEqual(expected);
});
it('should return duration from dates with more than 1 day', () => {
const from = moment();
const to = from.clone();
to.add(5, 'hour').add(12, 'minute').add(2, 'day');
const actual = getDuration(from.toDate(), to.toDate());
const expected: Duration = { days: 2, hours: 5, minutes: 12 };
expect(actual).toEqual(expected);
});
it('should return duration from dates in string', () => {
const from = moment();
const to = from.clone();
to.add(5, 'hour').add(12, 'minute');
const actual = getDuration(from.toDate().toISOString(), to.toDate().toISOString());
const expected: Duration = { days: 0, hours: 5, minutes: 12 };
expect(actual).toEqual(expected);
});
it('should calculate diff between two durations', () => {
const first = { days: 1, hours: 2, minutes: 4 } as Duration;
const next = { days: 2, hours: 5, minutes: 12 } as Duration;
const actual = getDurationDiff(first, next);
const expected: Duration = { days: 1, hours: 3, minutes: 8 };
expect(actual).toEqual(expected);
});
});
@@ -0,0 +1,37 @@
import { PeriodFlightTimeModel } from './period-flight-time.model';
export interface Duration {
days: number;
hours: number;
minutes: number;
}
export const zeroDuration: Duration = { days: 0, hours: 0, minutes: 0 };
export function getPeriodDuration(period?: PeriodFlightTimeModel) {
return getDuration(period?.start?.local, period?.end?.local) || zeroDuration;
}
export function getDuration(from?: Date | string, to?: Date | string): Duration | undefined {
if (!from || !to) return undefined;
return getDurationMs(new Date(to).getTime() - new Date(from).getTime());
}
export function getTotalMinutes({ days = 0, hours = 0, minutes = 0 }: Duration = {} as any) {
return minutes + hours * 60 + days * 24 * 60;
}
export function getDurationMs(ms: number): Duration {
ms = ms / 1000 / 60; // ms / milliseconds / seconds
const days = Math.floor(ms / (60 * 24));
const hours = Math.floor((ms / 60) % 24);
const minutes = Math.floor(ms % 60);
return { days, hours, minutes: minutes };
}
export function getDurationDiff(first: Duration, next: Duration): Duration {
if (!first || !next) return zeroDuration;
const totalMinutes = getTotalMinutes(next) - getTotalMinutes(first);
return getDurationMs(totalMinutes * 60 * 1000);
}
@@ -0,0 +1,13 @@
import { IAircraft } from './aircraft.model';
import { IServiceType } from './service-type.model';
import { ServiceClassModel } from './service-class.model';
import { Meal } from './meal.model';
import { IBookingClasses } from './booking-classes.model';
export class EquipmentModel {
serviceType: IServiceType;
aircraft: IAircraft;
serviceClasses: ServiceClassModel[];
bookingClasses: IBookingClasses;
meal: Meal[];
}
@@ -0,0 +1,37 @@
import { IRouteContract } from './contracts';
import { IFlightStrategy } from './flight.strategy';
import { RouteModel } from './route.model';
export class FallbackFlightStrategy implements IFlightStrategy {
getLegs(flight: RouteModel) {
return [];
}
getFlights(flight: RouteModel) {
return [];
}
getFirstLeg(flight: RouteModel) {
return {} as any;
}
getCurrentLeg(flight: RouteModel) {
return {} as any;
}
getLastLeg(flight: RouteModel) {
return {} as any;
}
getFirstFlight(flight: RouteModel) {
return {} as any;
}
getLastFlight(flight: RouteModel) {
return {} as any;
}
getFlightId(flight: RouteModel) {
return {} as any;
}
fromContract(contract: IRouteContract) {
return {} as any;
}
}
@@ -0,0 +1,10 @@
export interface IFlightId {
carrier: string;
flightNumber: string;
suffix: string;
date: Date;
}
export interface IPrevFlight extends IFlightId {
localDate?: Date;
}
@@ -0,0 +1,50 @@
import { getFirstLeg, getLastLeg, getTotalMinutes, IFlightTime, Leg } from '.';
import { FlightModel } from './flight.model';
type CompareDirection = 1 | -1;
// all covered with tests as a part of test for sort-flights-by.pipe.spec.ts
export function compareByArrivalDesc(a: FlightModel, b: FlightModel) {
return compareByArrival(a, b, -1);
}
export function compareByDepartureDesc(a: FlightModel, b: FlightModel) {
return compareByDeparture(a, b, -1);
}
export function compareByDurationDesc(a: FlightModel, b: FlightModel) {
return compareByDuration(a, b, -1);
}
export function compareByArrivalAsc(a: FlightModel, b: FlightModel) {
return compareByArrival(a, b, 1);
}
export function compareByDepartureAsc(a: FlightModel, b: FlightModel) {
return compareByDeparture(a, b, 1);
}
export function compareByDurationAsc(a: FlightModel, b: FlightModel) {
return compareByDuration(a, b, 1);
}
function compareByArrival(a: FlightModel, b: FlightModel, direction: CompareDirection) {
return compareByLeg(getLastLeg(a), getLastLeg(b), 'arrival', direction);
}
function compareByDeparture(a: FlightModel, b: FlightModel, direction: CompareDirection) {
return compareByLeg(getFirstLeg(a), getFirstLeg(b), 'departure', direction);
}
function compareByDuration(a: FlightModel, b: FlightModel, direction: CompareDirection) {
const amin = getTotalMinutes(a.duration);
const bmin = getTotalMinutes(b.duration);
if (amin > bmin) return direction;
if (amin < bmin) return direction * -1;
return 0;
}
function compareByLeg(a: Leg, b: Leg, property: keyof Pick<Leg, 'arrival' | 'departure'>, direction: CompareDirection) {
return compareByTime(a[property].times.scheduled, b[property].times.scheduled, direction);
}
function compareByTime(a: IFlightTime, b: IFlightTime, direction: CompareDirection) {
if (a.utc > b.utc) return direction;
if (a.utc < b.utc) return direction * -1;
return 0;
}
@@ -0,0 +1,10 @@
import { DayChangeModel } from './day-change.model';
export interface IFlightTime {
utc: Date;
local: Date;
localTime: string;
tzOffset: number;
dayChange: DayChangeModel;
unix: number;
}
@@ -0,0 +1,5 @@
export class FlightUpdateSignalModel {
flightNumber: string;
carrier: string;
date: Date;
}
@@ -0,0 +1,39 @@
import { FlightLoadedState } from '../enumerators';
import { IFlightId } from './flight-id.model';
import { Leg } from './leg.model';
import { IOperatingByLegacy } from './operator.model';
import { RouteModel } from './route.model';
export interface FlightModel extends RouteModel {
operatingBy: IOperatingByLegacy;
flightId: IFlightId;
dateToSearchBy: Date;
id: string;
boardings: number;
isMultiFlight: boolean; // aka Connected
isMultiLeg: boolean; // aka Multi-Segment, Multi-Leg
hash: string;
previous: FlightModel;
next: FlightModel;
loadedState: FlightLoadedState;
lastUpdate: Date;
flights?: FlightModel[];
leg: Leg;
legs: Leg[];
}
export interface DirectFlight extends FlightModel {
leg: Leg;
}
export interface MultiLegFlight extends FlightModel {
legs: Leg[];
}
export interface ConnectingFlight extends FlightModel {
flights: FlightModel[];
}
export type AnyOfFlightModel = DirectFlight | MultiLegFlight | ConnectingFlight;
export type ListItem<T> = T & { expanded?: boolean; visible?: boolean };
@@ -0,0 +1,21 @@
import { ILegContract, IRouteContract } from './contracts';
import { IFlightId } from './flight-id.model';
import { FlightModel } from './flight.model';
import { Leg } from './leg.model';
import { RouteModel } from './route.model';
export interface IFlightFactory {
createFlight: (contract: IRouteContract) => FlightModel;
createLeg: (contract: ILegContract) => Leg;
}
export interface IFlightStrategy {
getLegs(flight: RouteModel): Leg[];
getFlights(flight: RouteModel): FlightModel[];
getFirstLeg(flight: RouteModel): Leg;
getCurrentLeg(flight: RouteModel): Leg;
getLastLeg(flight: RouteModel): Leg;
getFirstFlight(flight: RouteModel): FlightModel;
getLastFlight(flight: RouteModel): FlightModel;
getFlightId(flight: RouteModel, isLast?: boolean): IFlightId;
fromContract: (contract: IRouteContract, factory: IFlightFactory) => Partial<FlightModel>;
}
@@ -0,0 +1,83 @@
import { ConnectingFlightStrategy } from './connected-flight.strategy';
import { DirectFlightStrategy } from './direct-flight.strategy';
import { MultiLegFlightStrategy } from './multileg-fligh.strategy';
import { IFlightStrategy } from './flight.strategy';
import { RouteTypeLegacy } from '../enumerators';
import { RouteModel } from './route.model';
import { FallbackFlightStrategy } from './fallback-flight.strategy';
import { FlightModel, IArrivalStation, IDepartureStation } from '.';
const direct = new DirectFlightStrategy();
const connecting = new ConnectingFlightStrategy(getStrategyByType);
const multiLeg = new MultiLegFlightStrategy();
const fallback = new FallbackFlightStrategy();
export function getStrategy(flight: RouteModel): IFlightStrategy {
return getStrategyByType(flight?.routeType);
}
export function getStrategyByType(type?: RouteTypeLegacy): IFlightStrategy {
switch (type) {
case RouteTypeLegacy.Connecting:
return connecting;
case RouteTypeLegacy.Direct:
return direct;
case RouteTypeLegacy.MultiLeg:
return multiLeg;
default:
return fallback;
}
}
export function getArrival(
flight: FlightModel,
property: keyof Pick<IArrivalStation, 'scheduled' | 'latest'> = 'scheduled',
) {
return getLastLeg(flight).arrival[property]?.cityCode;
}
export function getDeparture(
flight: FlightModel,
property: keyof Pick<
IDepartureStation,
'scheduled' | 'latest'
> = 'scheduled',
) {
return getFirstLeg(flight).departure[property]?.cityCode;
}
export function getAllFlights(flights: RouteModel[]) {
return (flights = flights
.map((f) => getFlights(f))
.reduce((a, c) => a.concat(c)));
}
export function getAllLegs(flights: RouteModel[]) {
return flights.reduce((a, c) => a.concat(getLegs(c)), []);
}
export function getLegs(flight: RouteModel) {
return getStrategy(flight).getLegs(flight);
}
export function getFlights(flight: RouteModel) {
return getStrategy(flight).getFlights(flight);
}
export function getFirstLeg(flight: RouteModel) {
return getStrategy(flight).getFirstLeg(flight);
}
export function getCurrentLeg(flight: RouteModel) {
return getStrategy(flight).getCurrentLeg(flight);
}
export function getLastLeg(flight: RouteModel) {
return getStrategy(flight).getLastLeg(flight);
}
export function getFirstFlight(flight: RouteModel) {
return getStrategy(flight).getFirstFlight(flight);
}
export function getLastFlight(flight: RouteModel) {
return getStrategy(flight).getLastFlight(flight);
}
export function getFlightId(flight: RouteModel) {
return getStrategy(flight).getFlightId(flight);
}
const DeprecatedFlightUtils = {
getFirstLeg,
};
export default DeprecatedFlightUtils;
@@ -0,0 +1,38 @@
export * from './times.model';
export * from './station.model';
export * from './route.model';
export * from './flight.model';
export * from './flight-id.model';
export * from './leg.model';
export * from './departure-station.model';
export * from './flight-time.model';
export * from './departure-times.model';
export * from './arrival-times.model';
export * from './period-flight-time.model';
export * from './arrival-station.model';
export * from './days-of-week.model';
export * from './passenger-info.model';
export * from './passengers.model';
export * from './equipment.model';
export * from './aircraft.model';
export * from './aircraft-type.model';
export * from './onboard-service.model';
export * from './traffic-restrictions.model';
export * from './service-type.model';
export * from './service-class.model';
export * from './booking-classes.model';
export * from './operator.model';
export * from './airport-info-modification.model';
export * from './duration.model';
export * from './schedule-item.model';
export * from './day-change.model';
export * from './flight-update-signal.model';
export * from './app-settings.model';
export * from './seat.model';
export * from './transition.model';
export * from './flight.strategy';
export * from './get-flight.strategy';
export * from './transfer.model';
export * from './transfer.logic';
export * from './sort-by.enum';
export * from './flight-sorting.logic';
@@ -0,0 +1,37 @@
import { FlightStatusLegacy } from '../enumerators';
import { PassengersModel } from './passengers.model';
import { EquipmentModel } from './equipment.model';
import { TrafficRestrictionsModel } from './traffic-restrictions.model';
import { Duration } from './duration.model';
import { IDepartureStation } from './departure-station.model';
import { IArrivalStation } from './arrival-station.model';
import { IDaysOfWeek } from './days-of-week.model';
import { IOperatingByLegacy, ITransition } from '.';
export interface Leg {
departure: IDepartureStation;
arrival: IArrivalStation;
daysOfWeek: IDaysOfWeek;
daysForTabs?: string[];
index: number;
status: FlightStatusLegacy;
passengers: PassengersModel;
equipment: EquipmentModel;
trafficRestrictions: TrafficRestrictionsModel;
reroute: boolean;
changeRoute: boolean;
updated: Date;
isCurrent: boolean; // TODO: KS: create task to add this on backend
previous: Leg;
next: Leg;
flightPercent: number;
remainingFlightDuration: Duration;
checkInPercent: number;
duration: Duration; // current flight time/duration
scheduledDuration: Duration;
estimatedDuration: Duration;
crossIndex: number;
operatingBy: IOperatingByLegacy;
hasCodesharing: boolean;
transition: ITransition;
}
@@ -0,0 +1,10 @@
export interface Meal {
type: MealType;
}
export enum MealType {
Economy = 'Economy',
Comfort = 'Comfort',
Business = 'Business',
Special = 'Special',
}
@@ -0,0 +1,44 @@
import { IMultiFlight } from './contracts';
import { MultiLegFlight } from './flight.model';
import { IFlightFactory, IFlightStrategy } from './flight.strategy';
export class MultiLegFlightStrategy implements IFlightStrategy {
getLegs(flight: MultiLegFlight) {
return flight.legs;
}
getFlights(flight: MultiLegFlight) {
return [flight];
}
getFirstLeg(flight: MultiLegFlight) {
return flight.legs[0];
}
getCurrentLeg(flight: MultiLegFlight) {
return flight.legs.find((l) => l.isCurrent) ?? flight.legs[0];
}
getLastLeg(flight: MultiLegFlight) {
return flight.legs[flight.legs.length - 1];
}
getFirstFlight(flight: MultiLegFlight) {
return flight;
}
getLastFlight(flight: MultiLegFlight) {
return flight;
}
getFlightId(flight: MultiLegFlight) {
return flight.flightId;
}
fromContract(contract: IMultiFlight, { createLeg }: IFlightFactory) {
const legs = contract.legs?.map((t) => createLeg(t)) ?? [];
const flight: Partial<MultiLegFlight> = {
...contract,
legs,
isMultiLeg: true,
};
return flight;
}
}
@@ -0,0 +1,6 @@
export class OnboardServiceModel {
id: number;
title: string;
description: string;
url: string;
}
@@ -0,0 +1,10 @@
export class OperatorModel {
carrier: string;
flightNumber: string;
}
export interface IOperatingByLegacy {
scheduled: string;
actual: string;
operators: OperatorModel[];
}
@@ -0,0 +1,4 @@
export class PassengerInfoModel {
code: string;
count: number;
}
@@ -0,0 +1,6 @@
import { PassengerInfoModel } from './passenger-info.model';
export class PassengersModel {
booked: PassengerInfoModel;
actual: PassengerInfoModel;
}
@@ -0,0 +1,6 @@
import { IFlightTime } from './flight-time.model';
export class PeriodFlightTimeModel {
start: IFlightTime;
end: IFlightTime;
}
@@ -0,0 +1,133 @@
import * as moment from 'moment';
import { ILegContract } from './contracts';
import { calculateLegState, calculateRemainingDuration, createLeg } from './populate.logic';
describe('PopulateLogic', () => {
beforeEach(() => {
jasmine.clock().install().mockDate(new Date(2021, 11, 26, 12, 35, 30));
});
afterEach(() => {
jasmine.clock().uninstall();
});
const _7h30minBack = () =>
moment()
.add(-(7 * 60 + 30), 'minutes')
.toDate();
const _8h40minBack = () =>
moment()
.add(-(8 * 60 + 40), 'minutes')
.toDate();
const _1hBack = () =>
moment()
.add(-(1 * 60), 'minutes')
.toDate();
const _5h10minForth = () =>
moment()
.add(5 * 60 + 10, 'minutes')
.toDate();
const _6h20minForth = () =>
moment()
.add(6 * 60 + 20, 'minutes')
.toDate();
const _1hForth = () =>
moment()
.add(1 * 60, 'minutes')
.toDate();
describe('calculateRemainingDuration', () => {
it('should calculate remaining duration using estimated arrival time when both scheduled and estimated given', () => {
const legContract = {
departure: {
times: {
actualBlockOff: { utc: _7h30minBack() },
scheduledDeparture: { utc: _8h40minBack() },
estimatedBlockOff: { utc: _8h40minBack() },
},
},
arrival: { times: { scheduledArrival: { utc: _6h20minForth() }, estimatedBlockOn: { utc: _5h10minForth() } } },
status: 'InFlight',
} as any as ILegContract;
const leg = createLeg(legContract);
expect(calculateRemainingDuration(leg)).toEqual({ days: 0, hours: 5, minutes: 10 });
});
it('should calculate remaining duration using scheduled arrival time when no estimated time given', () => {
const legContract = {
departure: {
times: {
actualBlockOff: { utc: _7h30minBack() },
scheduledDeparture: { utc: _8h40minBack() },
estimatedBlockOff: { utc: _8h40minBack() },
},
},
arrival: { times: { scheduledArrival: { utc: _6h20minForth() } } },
status: 'InFlight',
} as any as ILegContract;
const leg = createLeg(legContract);
expect(calculateRemainingDuration(leg)).toEqual({ days: 0, hours: 6, minutes: 20 });
});
it('should use zero as remaining for already passed', () => {
const leg = {
departure: { times: { actual: { utc: _5h10minForth() } } },
arrival: { times: { scheduled: { utc: _6h20minForth() } } },
} as any;
expect(calculateRemainingDuration(leg)).toEqual({ days: 0, hours: 0, minutes: 0 });
const leg2 = {
departure: { times: { actual: { utc: _8h40minBack() } } },
arrival: { times: { scheduled: { utc: _7h30minBack() } } },
} as any;
expect(calculateRemainingDuration(leg2)).toEqual({ days: 0, hours: 0, minutes: 0 });
});
});
describe('calculateFlightTimePercent', () => {
it('should calculate percent using estimated arrival time if both estimated and scheduled times are given', () => {
const legContract = {
departure: {
times: {
scheduledDeparture: { utc: _8h40minBack() },
actualBlockOff: { utc: _1hBack() },
},
},
arrival: { times: { scheduledArrival: { utc: _6h20minForth() }, estimatedBlockOn: { utc: _1hForth() } } },
status: 'InFlight',
} as any as ILegContract;
const leg = createLeg(legContract);
calculateLegState(leg);
expect(leg.flightPercent).toEqual(50);
});
it('should have flight percent eq 0 for future flights', () => {
const legContract = {
departure: { times: { scheduledDeparture: { utc: _5h10minForth() } } },
arrival: { times: { scheduledArrival: { utc: _6h20minForth() } } },
} as any as ILegContract;
const leg = createLeg(legContract);
calculateLegState(leg);
expect(leg.flightPercent).toEqual(0);
});
it('should have flight percent eq 100% for completed flights', () => {
const legContract = {
departure: { times: { scheduledDeparture: { utc: _8h40minBack() }, actualBlockOff: { utc: _8h40minBack() } } },
arrival: { times: { scheduledArrival: { utc: _7h30minBack() }, actualTouchDown: { utc: _7h30minBack() } } },
} as any as ILegContract;
const leg = createLeg(legContract);
calculateLegState(leg);
expect(leg.flightPercent).toEqual(100);
});
});
});
@@ -0,0 +1,313 @@
import { FlightLoadedState, FlightStatusLegacy } from '../enumerators';
import { IArrivalStation } from './arrival-station.model';
import { ILegContract, IMultiFlight, IRouteContract } from './contracts';
import { IDepartureStation } from './departure-station.model';
import { FlightModel, MultiLegFlight } from './flight.model';
import { getStrategy, getStrategyByType } from './get-flight.strategy';
import { Leg } from './leg.model';
import { Duration } from './duration.model';
import { getDuration, zeroDuration } from './duration.model';
import * as moment from 'moment';
import { getTotalMinutes } from '.';
import { MultiLegFlightStrategy } from './multileg-fligh.strategy';
export function createFlights(contracts: IRouteContract[]): FlightModel[] {
return contracts?.map((contract) => createFlight(contract)) ?? [];
}
const factories = { createFlight, createLeg };
export function createFlight(contract: IRouteContract): FlightModel {
const strategy = getStrategyByType(contract.routeType);
if (contract.routeType == 'MultiLeg') {
(contract as IMultiFlight).legs = (contract as IMultiFlight).legs.sort((l1, l2) => {
if (!l1.departure.times.actualBlockOff || !l2.departure.times.actualBlockOff) {
return 0;
}
if (l1.departure.times.actualBlockOff.utc > l2.departure.times.actualBlockOff.utc) {
return 1;
}
if (l1.departure.times.actualBlockOff.utc < l2.departure.times.actualBlockOff.utc) {
return -1;
}
return 0;
});
}
const flight = strategy.fromContract(contract, factories) as FlightModel;
return populate(flight);
}
function getSearchDate(leg: Leg) {
const departure = leg.departure.times.scheduled.local;
return moment.parseZone(departure).local(true).toDate();
}
export function populate(flight: FlightModel) {
const strategy = getStrategy(flight);
const flights = strategy.getFlights(flight).filter((f) => !!f.flightId);
const firstLeg = strategy.getFirstLeg(flight);
const lastLeg = strategy.getLastLeg(flight);
const legs = strategy.getLegs(flight);
flight.dateToSearchBy = getSearchDate(firstLeg);
flight.lastUpdate = new Date();
flight.loadedState = FlightLoadedState.scheduled;
flight.boardings = legs.length - 1;
flight.hash = flights
.map(
(f) =>
`${f.flightId.carrier}${f.flightId.flightNumber}${moment(
f.dateToSearchBy,
).format('DDMMYYYY')}`,
)
.join('');
flight.duration = getScheduledFlightTime(firstLeg, lastLeg);
link(legs);
link(flights);
legs.forEach((leg, index) => {
leg.index = index;
leg.crossIndex = index + 1;
});
// debug info, don't remove before 2.01.2022
// const operator = 'CI';
// flight.operatingBy = {
// scheduled: operator,
// actual: operator,
// operators: [{
// carrier: operator,
// flightNumber: 'SU-1234',
// }]
// };
return flight;
}
// TODO: refactor this method by moving calculations to server as much as possible
export function createLeg(legContract: ILegContract): Leg {
const leg: Leg = { ...legContract } as any as Leg;
leg.arrival = createArrival(legContract);
leg.departure = createDeparture(legContract);
// TODO: TASK 7352 - pending migration to server
leg.duration = getCurrentFlightTime(leg);
leg.scheduledDuration = getDuration(
leg.departure.times.scheduled?.utc,
leg.arrival.times.scheduled?.utc,
);
leg.estimatedDuration = getDuration(
leg.departure.times.latest?.utc,
leg.arrival.times.latest?.utc,
);
leg.reroute = legContract.flags?.returnToAirport;
leg.changeRoute = legContract.flags?.routeChanged;
// debug info. could be removed after 02.2022
// leg.operatingBy = {
// scheduled: 'KL',
// actual: 'SU',
// operators: [
// {
// carrier: 'KL',
// flightNumber: '1234',
// },
// {
// carrier: 'KL',
// flightNumber: '1234',
// },
// ],
// };
// ToDo: Refactor / move to server
leg.hasCodesharing = leg.operatingBy?.operators?.length > 0;
calculateLegState(leg);
// debug info. could be removed after 02.2022
// leg.reroute = true;
// leg.changeRoute = true;
return leg;
}
export function createDeparture(legContract: ILegContract): IDepartureStation {
const contract = legContract.departure;
const { times } = contract;
const departure: IDepartureStation = {
boardingStatus: contract.boardingStatus,
boardingStatusOriginal: contract.boardingStatus,
checkinStatus: contract.checkinStatus,
dispatch: contract.dispatch,
gate: contract.gate,
parkingStand: contract.parkingStand,
scheduled: { ...contract.scheduled },
terminal: contract.terminal,
_times: times as any,
times: {
...times,
scheduled: times.scheduledDeparture,
actual:
times.actualTime || times.actualBlockOff || times.actualTakeOff,
estimated: times.estimatedBlockOff || times.estimatedTakeOff,
},
latest: contract.latest
? { ...contract.latest }
: { ...contract.scheduled },
// debug info for registration button. could be removed after 02.2022
// transition: { registration: { status: OperationStatus.inProgress }} as any
};
departure.times.latest =
departure.times.actual || departure.times.estimated;
// dont remove, this is debug. could be removed after 02.2022
// departure.times.scheduled = { ...departure.times.scheduled, utc: new Date(), localTime: '10:35' };
// departure.times.scheduled.dayChange = { value: 2, title: '+2' };
return departure;
}
export function createArrival(legContract: ILegContract): IArrivalStation {
const contract = legContract.arrival;
const { times } = contract;
const arrival: IArrivalStation = {
bagBelt: contract.bagBelt,
deboardingStatus: contract.deboardingStatus,
latest: contract.latest
? { ...contract.latest }
: { ...contract.scheduled },
scheduled: { ...contract.scheduled },
terminal: contract.terminal,
_times: times as any,
times: {
...times,
scheduled: times.scheduledArrival,
estimated: times.estimatedBlockOn || times.estimatedTouchDown,
actual:
times.actualTime ||
times.actualBlockOn ||
times.actualTouchDown,
},
dispatch: contract.dispatch,
gate: contract.gate,
};
arrival.times.latest = arrival.times.actual || arrival.times.estimated;
// dont remove, this is debug. could be removed after 02.2022
// arrival.times.latest = { ...arrival.times.latest, utc: moment().add(1, 'hour').toDate(), localTime: '12:25' };
// arrival.times.scheduled = { ...arrival.times.scheduled, utc: new Date(), localTime: '10:35' };
// arrival.times.scheduled.dayChange = { value: 2, title: '+2' };
return arrival;
}
function link<T extends { next: T; previous: T }>(items: T[]) {
let previous: T;
items.forEach((item) => {
item.previous = previous;
if (previous) previous.next = item;
previous = item;
});
}
function getCurrentFlightTime(leg: Leg) {
const departureTime = leg.departure.times.actual;
const arrivalTime = leg.arrival.times.latest?.utc;
const now = moment().utc().toDate();
if (!departureTime) {
return zeroDuration;
}
const endTime = arrivalTime <= now ? arrivalTime : now;
return getDuration(departureTime.utc, endTime);
}
function getScheduledFlightTime(from: Leg, to?: Leg) {
to = to ?? from;
return getDuration(
from.departure.times.scheduled?.utc,
to.arrival.times.scheduled?.utc,
);
}
// TODO: TASK 7352 - pending migration to server
export function calculateLegState(leg: Leg) {
switch (leg.status) {
case FlightStatusLegacy.scheduled:
case FlightStatusLegacy.cancelled:
case FlightStatusLegacy.delayed:
case FlightStatusLegacy.unknown:
leg.flightPercent = 0;
break;
case FlightStatusLegacy.arrived:
case FlightStatusLegacy.landed:
leg.flightPercent = 100;
break;
case FlightStatusLegacy.sent:
leg.flightPercent = 10; // WI 7601
break;
default:
leg.flightPercent = calculateFlightTimePercent(leg);
break;
}
leg.remainingFlightDuration = calculateRemainingDuration(leg);
//debug info
// leg.status = FlightStatus.inFlight;
// leg.flightPercent = 30;
// leg.remainingFlightDuration = { hours: 5, days: 1, minutes: 24 };
// leg.checkInDuration = { hours: 4, days: 0, minutes: 12 };
}
export function calculateFlightTimePercent({
duration,
estimatedDuration,
scheduledDuration,
}: Leg): number {
const totalDuration = estimatedDuration ?? scheduledDuration;
if (!duration || !totalDuration) return 0;
const minutesInFlight = getTotalMinutes(duration);
const totalMinutes = getTotalMinutes(totalDuration);
const percentage = Math.round(100 * (minutesInFlight / totalMinutes));
return Math.min(100, percentage);
}
export function calculateRemainingDuration({
departure,
arrival,
}: Leg): Duration {
const start = departure?.times?.actual;
const end = arrival?.times?.latest || arrival?.times?.scheduled;
if (!start || !end) return zeroDuration;
const from = moment(start.utc);
const to = moment(end.utc);
const now = moment().utc();
if (from <= now && now <= to) return getDuration(now.toDate(), to.toDate());
return zeroDuration;
}
const PopulateLogicLegacy = {
createFlights,
};
export default PopulateLogicLegacy;
@@ -0,0 +1,9 @@
import { RouteTypeLegacy } from '../enumerators/route-type.enum';
import { Duration } from './duration.model';
import { FlightStatusLegacy } from '../enumerators';
export interface RouteModel {
routeType: RouteTypeLegacy;
status: FlightStatusLegacy;
duration: Duration;
}
@@ -0,0 +1,24 @@
import { FlightModel } from '.';
import { ListItem } from './flight.model';
export class ScheduleItemModel {
date: Date;
get month() {
return this.date.getMonth();
}
get year() {
return this.date.getFullYear();
}
get dayOfWeek() {
return this.date.getDay();
}
get day() {
return this.date.getDate();
}
expanded = false;
flights: ListItem<FlightModel>[] = [];
constructor(patch: Partial<ScheduleItemModel>) {
Object.assign(this, patch);
}
}
@@ -0,0 +1,10 @@
export interface Seat {
type: SeatType;
count: number;
}
export enum SeatType {
Economy = 'Economy',
Comfort = 'Comfort',
Business = 'Business',
}
@@ -0,0 +1,5 @@
export class ServiceClassModel {
code: string;
mealType: string;
canBookPremium: boolean;
}
@@ -0,0 +1,5 @@
export interface IServiceType {
code: string;
application: string;
operatonType: string;
}
@@ -0,0 +1,8 @@
export enum ScheduleSortMode {
departureUp = 'departureUp',
departureDown = 'departureDown',
timeUp = 'timeUp',
timeDown = 'timeDown',
arrivalUp = 'arrivalUp',
arrivalDown = 'arrivalDown',
}
@@ -0,0 +1,7 @@
import { IAirportInfo } from '@typings/airport-info';
export interface IStation {
scheduled: IAirportInfo;
latest?: IAirportInfo;
terminal?: string;
}
@@ -0,0 +1,8 @@
import { IFlightTime } from './flight-time.model';
export interface ITimesModel {
scheduled: IFlightTime;
estimated?: IFlightTime;
actual?: IFlightTime;
latest?: IFlightTime;
}
@@ -0,0 +1,3 @@
export class TrafficRestrictionsModel {
codes: string;
}
@@ -0,0 +1,248 @@
import { Injectable } from '@angular/core';
import { RouteTypeLegacy } from '@app/shared/enumerators';
import {
FlightModel,
getFirstLeg,
IArrivalStation,
IDepartureStation,
Leg,
StationChange,
Stations,
Transfer,
} from '@app/shared/models-legacy';
import { getLastLeg } from './get-flight.strategy';
@Injectable({ providedIn: 'root' })
export class TransferLogic {
getTransfer(
flight: FlightModel,
nextFlight: FlightModel,
leg: Leg,
): Transfer {
const type = this.getType(flight, nextFlight, leg);
if (!type) {
return {
type: undefined,
arrival: undefined,
departure: undefined,
};
}
const { arrival, departure } = this.getStations(
type,
flight,
nextFlight,
leg,
);
const stationChange = this.getStationChange({ arrival, departure });
return {
type,
arrival,
departure,
stationChange,
};
}
getType(
flight: FlightModel,
nextFlight: FlightModel,
leg: Leg,
): RouteTypeLegacy | null {
return flight && nextFlight
? RouteTypeLegacy.Connecting
: leg
? RouteTypeLegacy.MultiLeg
: null;
}
getStations(
type: RouteTypeLegacy,
flight: FlightModel,
nextFlight: FlightModel,
leg: Leg,
): Stations {
const arrival = this.getArrival(type, flight, leg);
const departure = this.getDeparture(type, nextFlight, leg);
return { arrival, departure };
}
cityChanged({ arrival, departure }: Stations) {
const arrivalInfo = arrival?.latest || arrival?.scheduled;
const departureInfo = departure?.latest || departure?.scheduled;
return arrivalInfo?.cityCode !== departureInfo?.cityCode;
}
airportChanged({ arrival, departure }: Stations) {
const arrivalInfo = arrival?.latest || arrival?.scheduled;
const departureInfo = departure?.latest || departure?.scheduled;
return arrivalInfo?.airportCode !== departureInfo?.airportCode;
}
terminalChanged({ arrival, departure }: Stations) {
if (!arrival?.terminal || !departure?.terminal) return false;
return arrival.terminal !== departure.terminal;
}
getStationChange(stations: Stations): StationChange {
if (this.cityChanged(stations)) {
return 'city';
}
if (this.airportChanged(stations)) {
return 'airport';
}
if (this.terminalChanged(stations)) {
return 'terminal';
}
return 'noChange';
}
private mapToTransfer(
arrival: IArrivalStation,
departure: IDepartureStation,
) {
if (this.cityChanged({ arrival, departure })) {
return this.constructTransfer(
RouteTypeLegacy.Connecting,
arrival,
departure,
'city',
);
}
if (this.airportChanged({ arrival, departure })) {
return this.constructTransfer(
RouteTypeLegacy.Connecting,
arrival,
departure,
'airport',
);
}
if (this.terminalChanged({ arrival, departure })) {
return this.constructTransfer(
RouteTypeLegacy.Connecting,
arrival,
departure,
'terminal',
);
}
return this.constructTransfer(
RouteTypeLegacy.Connecting,
arrival,
departure,
'noChange',
);
}
private getConnectingTransfers(flights: FlightModel[]): Transfer[] {
return flights
.reduce((acc, cv) => {
if (cv.leg) {
acc.push(cv.leg);
return acc;
}
if (cv.legs) {
return [...acc, ...cv.legs];
}
return acc;
}, [])
.filter((f) => f.next)
.map((leg) => {
const { arrival } = leg;
const { departure } = leg.next;
return this.mapToTransfer(arrival, departure);
});
}
private getMultiLegTransfers(flights: FlightModel[]) {
const connectedLegs = flights
.map((flight) => {
return (flight.legs || []).filter((l) => l.next);
})
.reduce((a, b) => a.concat(b), []);
return connectedLegs.map((leg) => {
const { arrival } = leg;
const { departure } = leg.next;
return this.mapToTransfer(arrival, departure);
});
}
getTransfers(
flights: FlightModel[],
type: RouteTypeLegacy.Connecting | RouteTypeLegacy.MultiLeg,
): Transfer[] {
return type === RouteTypeLegacy.Connecting
? this.getConnectingTransfers(flights)
: this.getMultiLegTransfers(flights);
}
getPrimaryTransfer(transfers: Transfer[]): Transfer | null {
if (!transfers) return null;
return this.sortTransfers(transfers)[0];
}
private getArrival(
type: RouteTypeLegacy,
flight: FlightModel,
leg: Leg,
): IArrivalStation | null {
if (!type) return null;
return type === RouteTypeLegacy.Connecting
? getLastLeg(flight).arrival
: leg.arrival;
}
private getDeparture(
type: RouteTypeLegacy,
nextFlight: FlightModel,
leg: Leg,
): IDepartureStation | null {
if (!type) return null;
return type === RouteTypeLegacy.Connecting
? getFirstLeg(nextFlight).departure
: leg.next.departure;
}
private constructTransfer(
type: RouteTypeLegacy,
arrival: IArrivalStation,
departure: IDepartureStation,
stationChange: StationChange,
): Transfer {
return {
type,
arrival,
departure,
stationChange,
};
}
private sortTransfers(transfers: Transfer[]): Transfer[] {
const importanceAsc = ['noChange', 'terminal', 'airport'];
return transfers.sort(
(t1, t2) =>
importanceAsc.indexOf(t1.stationChange) -
importanceAsc.indexOf(t2.stationChange),
);
}
}
@@ -0,0 +1,17 @@
import { RouteTypeLegacy } from '../enumerators';
import { IArrivalStation } from './arrival-station.model';
import { IDepartureStation } from './departure-station.model';
export type StationChange = 'city' | 'airport' | 'terminal' | 'noChange';
export interface Stations {
arrival: IArrivalStation;
departure: IDepartureStation;
}
export interface Transfer {
type: RouteTypeLegacy;
arrival: IArrivalStation;
departure: IDepartureStation;
stationChange?: StationChange;
}
@@ -0,0 +1,14 @@
import { IFlightTime } from '@shared/models-legacy/flight-time.model';
export interface ITransition {
registration: ITransitionItem;
boarding: ITransitionItem;
deboarding: ITransitionItem;
}
export interface ITransitionItem {
status: string;
start: IFlightTime;
end: IFlightTime;
isActual: boolean;
}
@@ -0,0 +1,95 @@
import { IArrivalStation } from '../models-legacy';
import { AirportTerminalPipe } from './airport-terminal.pipe';
describe('AirportTerminalPipe', () => {
let pipe: AirportTerminalPipe;
const arrival: IArrivalStation = {
latest: {
airport: 'Москва',
},
scheduled: {
airport: 'Санкт-Петербург',
},
terminal: '2C',
} as any;
beforeEach(() => {
pipe = new AirportTerminalPipe();
});
it('should return empty string on empty input or missing properties', () => {
expect(pipe.transform(null, null)).toBe('');
expect(pipe.transform(undefined, undefined)).toBe('');
expect(pipe.transform({} as any as IArrivalStation, null)).toBe('');
});
it('should return valid airport with proper formatting with dash', () => {
expect(pipe.transform(arrival, 'dash')).toBe('Москва - 2C');
});
it('should return valid airport with proper formatting with brackets', () => {
expect(pipe.transform(arrival, 'brackets')).toBe('Москва (2C)');
});
it('should return valid airport with proper formatting with dash when no latest airport given', () => {
const airportInfo = {
scheduled: {
airport: 'Санкт-Петербург',
},
terminal: '2C',
};
expect(pipe.transform(airportInfo as any as IArrivalStation, 'dash')).toBe('Санкт-Петербург - 2C');
});
it('should return valid airport with proper formatting with brackets when no latest airport given', () => {
const airportInfo = {
scheduled: {
airport: 'Санкт-Петербург',
},
terminal: '2C',
} as IArrivalStation;
expect(pipe.transform(airportInfo, 'brackets')).toBe('Санкт-Петербург (2C)');
});
it('should return only airport if no terminal given', () => {
const testCases = [
{
scheduled: {
airport: 'Санкт-Петербург',
},
} as IArrivalStation,
{
scheduled: {
airport: 'Санкт-Петербург',
},
terminal: null,
} as IArrivalStation,
{
scheduled: {
airport: 'Санкт-Петербург',
},
terminal: undefined,
} as IArrivalStation,
];
testCases.forEach((testCase) => {
expect(pipe.transform(testCase, 'dash')).toBe('Санкт-Петербург');
expect(pipe.transform(testCase, 'brackets')).toBe('Санкт-Петербург');
});
});
it('should return only terminal without formatting if onlyTerminal set to true', () => {
const station = {
scheduled: {
airport: 'Санкт-Петербург',
},
terminal: 'Пулково-1',
} as IArrivalStation;
expect(pipe.transform(station, 'dash', true)).toBe('Пулково-1');
expect(pipe.transform(station, 'brackets', true)).toBe('Пулково-1');
});
});
@@ -0,0 +1,34 @@
import { Pipe, PipeTransform } from '@angular/core';
import { IStation } from '../models-legacy';
@Pipe({
name: 'airportTerminal',
})
export class AirportTerminalPipe implements PipeTransform {
transform(
model: IStation,
separator: 'dash' | 'brackets' = 'dash',
onlyTerminal = false,
oldValue = false,
): string {
if (!model?.latest && !model?.scheduled) {
return '';
}
const targetAirport = oldValue
? model.scheduled
: model.latest ?? model.scheduled;
if (!model.terminal) {
return `${targetAirport.airport}`;
}
if (onlyTerminal) {
return `${model.terminal}`;
}
return separator === 'dash'
? `${targetAirport.airport} - ${model.terminal}`
: `${targetAirport.airport} (${model.terminal})`;
}
}
@@ -0,0 +1,70 @@
import { Leg, OperatorModel } from '../models-legacy';
import { CodesharingPipe } from './codesharing.pipe';
describe('CodesharingPipe', () => {
let pipe: CodesharingPipe;
beforeEach(() => {
pipe = new CodesharingPipe();
});
it('should return proper codeshare operators with correct formatting', () => {
const testCases = [
{
legs: undefined,
expected: '',
},
{
legs: null,
expected: '',
},
{
legs: [{ departure: {} } as Leg, { arrival: {} } as Leg] as Leg[],
expected: '',
},
{
legs: [{ departure: {}, operatingBy: {} } as Leg, { arrival: {} } as Leg] as Leg[],
expected: '',
},
{
legs: [
{ operatingBy: { operators: [{ carrier: 'AM', flightNumber: '1234' }] as OperatorModel[] } } as Leg,
{ arrival: {} } as Leg,
] as Leg[],
expected: 'AM 1234',
},
{
legs: [
{
operatingBy: {
operators: [
{ carrier: 'AM', flightNumber: '1234' },
{ carrier: 'FV', flightNumber: '5678' },
] as OperatorModel[],
},
} as Leg,
{ arrival: {} } as Leg,
] as Leg[],
expected: 'AM 1234, FV 5678',
},
{
legs: [
{
operatingBy: {
operators: [
{ carrier: 'AM', flightNumber: '1234' },
{ carrier: 'FV', flightNumber: '5678' },
] as OperatorModel[],
},
} as Leg,
{ operatingBy: { operators: [{ carrier: 'NV', flightNumber: '3456' }] as OperatorModel[] } } as Leg,
] as Leg[],
expected: 'AM 1234, FV 5678, NV 3456',
},
];
testCases.forEach((testCase) => {
expect(pipe.transform(testCase.legs)).toEqual(testCase.expected);
});
});
});
@@ -0,0 +1,22 @@
import { Pipe, PipeTransform } from '@angular/core';
import { Leg } from '../models-legacy';
@Pipe({
name: 'codesharing',
})
export class CodesharingPipe implements PipeTransform {
transform(legOrLegs: Leg | Leg[]): string {
if (!legOrLegs) return '';
if (!Array.isArray(legOrLegs)) legOrLegs = [legOrLegs];
const operators = legOrLegs
.filter((leg) => leg.operatingBy?.operators)
.map((leg) => leg.operatingBy?.operators)
.reduce((a, c) => a.concat(c), []);
return operators
.map((operator) => `${operator.carrier} ${operator.flightNumber}`)
.join(', ');
}
}
@@ -0,0 +1,38 @@
import { TranslatePipe } from '@ngx-translate/core';
import { DayChangePipe } from './day-change.pipe';
describe('DatePipe', () => {
const date = new Date(2021, 10, 22, 17, 34);
const translate: TranslatePipe = {
transform: jasmine.createSpy('transform').withArgs('SHARED.DAY').and.returnValue('day'),
} as any;
let pipe: DayChangePipe;
beforeEach(() => {
pipe = new DayChangePipe(translate);
});
it('should return empty on empty input', () => {
expect(pipe.transform(null)).toBe('');
expect(pipe.transform(undefined)).toBe('');
expect(pipe.transform('' as any)).toBe('');
expect(pipe.transform({ value: null } as any)).toBe('');
expect(pipe.transform({ value: 0 } as any)).toBe('');
expect(pipe.transform({ value: 9, title: '' }, null)).toBe('');
expect(pipe.transform({ value: 9, title: '' }, undefined)).toBe('');
});
it('should format (+-)1 day', () => {
expect(pipe.transform({ value: 1, title: '' })).toEqual('day');
expect(pipe.transform({ value: -1, title: '' })).toEqual('day');
});
it('should format more/less than 1 day with date as date', () => {
expect(pipe.transform({ value: 2, title: '' }, date)).toEqual('22.11.2021');
expect(pipe.transform({ value: -2, title: '' }, date)).toEqual('22.11.2021');
});
it('should format more/less than 1 day with date as string', () => {
expect(pipe.transform({ value: 2, title: '' }, date.toISOString())).toEqual('22.11.2021');
expect(pipe.transform({ value: -2, title: '' }, date.toISOString())).toEqual('22.11.2021');
});
});
@@ -0,0 +1,36 @@
import { Pipe, PipeTransform } from '@angular/core';
import { TranslatePipe } from '@ngx-translate/core';
import * as moment from 'moment';
import { DATE_FORMAT } from '../helpers';
import { DayChangeModel } from './../models-legacy';
@Pipe({
name: 'dayChange',
})
export class DayChangePipe implements PipeTransform {
constructor(private translate: TranslatePipe) {}
transform(model?: DayChangeModel, date?: string | Date): string {
const $day = this.translate.transform('SHARED.DAY');
switch (model?.value) {
case -1:
case 1: {
return $day;
}
case 0:
case undefined: {
return '';
}
}
return this.handleDate(date);
}
private handleDate(date?: string | Date) {
if (!date) {
return '';
}
return moment.parseZone(date).local(true).format(DATE_FORMAT)
}
}
@@ -0,0 +1,37 @@
import { TranslatePipe } from '@ngx-translate/core';
import { DurationPipe } from './duration.pipe';
describe('DurationPipe', () => {
let translate: TranslatePipe;
let pipe: DurationPipe;
beforeEach(() => {
translate = {
transform: jasmine
.createSpy('transform')
.withArgs('SHARED.SHORT-HOUR')
.and.returnValue('h')
.withArgs('SHARED.SHORT-MIN')
.and.returnValue('m')
.withArgs('SHARED.SHORT-DAY')
.and.returnValue('d'),
} as any;
pipe = new DurationPipe(translate);
});
it('should return empty on empty input', () => {
expect(pipe.transform(null)).toBe('');
expect(pipe.transform(undefined)).toBe('');
expect(pipe.transform('' as any)).toBe('');
});
it('should return 0h 0m on empty hour/minute input', () => {
expect(pipe.transform({} as any)).toBe('0h 0m');
});
it('should return time string on non empty hour/minute input', () => {
expect(pipe.transform({ days: 0, minutes: 10, hours: 3 })).toBe('3h 10m');
});
it('should return time string on non empty day/hour/minute input', () => {
expect(pipe.transform({ days: 4, minutes: 10, hours: 3 })).toBe('4d 3h 10m');
});
});
@@ -0,0 +1,24 @@
import { Pipe, PipeTransform } from '@angular/core';
import { TranslatePipe } from '@ngx-translate/core';
import { Duration } from '../models-legacy';
@Pipe({
name: 'duration',
})
export class DurationPipe implements PipeTransform {
constructor(private translate: TranslatePipe) {}
transform(value?: Duration): string {
if (!value) return '';
const { days, hours = 0, minutes = 0 } = value;
if (days < 0 || hours < 0 || minutes < 0) {
return this.translate.transform('FLIGHT-STATUSES.Unknown');
}
const $days = days ? `${days}${this.translate.transform('SHARED.SHORT-DAY')} ` : '';
return `${$days}${hours}${this.translate.transform('SHARED.SHORT-HOUR')} ${minutes}${this.translate.transform('SHARED.SHORT-MIN')}`;
}
}
@@ -0,0 +1,34 @@
import { RouteTypeLegacy } from '../enumerators';
import { FlightNumberPipe } from './flight-number.pipe';
describe('FlightNumberPipe', () => {
let pipe: FlightNumberPipe;
beforeEach(() => {
pipe = new FlightNumberPipe();
});
it('should return empty for invalid inputs', () => {
expect(pipe.transform(null)).toBe('');
expect(pipe.transform(undefined)).toBe('');
expect(pipe.transform({} as any)).toBe('');
expect(pipe.transform({ routeType: RouteTypeLegacy.Direct } as any)).toBe('');
});
it('should return "SU 1234" for direct flight', () => {
expect(
pipe.transform({
routeType: RouteTypeLegacy.Direct,
flightId: { carrier: 'SU', flightNumber: 1234 },
} as any),
).toBe('SU\u00a01234');
});
it('should return "SU 1234, SU 4321" for connected flight', () => {
expect(
pipe.transform({
routeType: RouteTypeLegacy.Connecting,
flights: [{ flightId: { carrier: 'SU', flightNumber: 1234 } }, { flightId: { carrier: 'SU', flightNumber: 4321 } }],
} as any),
).toBe('SU\u00a01234, SU\u00a04321');
});
});
@@ -0,0 +1,16 @@
import { Pipe, PipeTransform } from '@angular/core';
import { getFlights } from '../models-legacy';
import { FlightModel } from '../models-legacy/flight.model';
@Pipe({
name: 'flightNumber',
})
export class FlightNumberPipe implements PipeTransform {
transform(flight?: FlightModel): string {
if (!flight) return '';
return getFlights(flight)
.filter((f) => !!f.flightId)
.map((f) => `${f.flightId.carrier}\u00a0${f.flightId.flightNumber}${f.flightId.suffix}`)
.join(', ');
}
}

Some files were not shown because too many files have changed in this diff Show More