Initial commit: Aeroflot Flights Web Angular 12 application
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
<div class="app-show-debug p-print-none" *ngIf="settings.showDebugVersion">
|
||||
{{ 'SHARED.TEST-VERSION' | translate }}
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { AppSettings } from '@app/shared/models-legacy';
|
||||
import { APP_SETTINGS } from '@app/shared/services';
|
||||
@Component({
|
||||
selector: 'app-show-debug',
|
||||
templateUrl: './app-show-debug.component.html',
|
||||
})
|
||||
export class AppShowDebugComponent {
|
||||
constructor(@Inject(APP_SETTINGS) public settings: AppSettings) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<div class="app-version p-print-none" [style.visibility]="isVisible? 'visible': 'hidden'">
|
||||
<div>version - </div>
|
||||
{{ apiVersion }}
|
||||
</div>
|
||||
@@ -0,0 +1,26 @@
|
||||
.app-version {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
padding: 2px 16px;
|
||||
background-color: #f37b09;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
z-index: 100000;
|
||||
opacity: 0.6;
|
||||
transition-duration: 0.2s;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.app-version:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.app-version > div {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-version:hover > div {
|
||||
display: inline-block;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { NetworkService } from '@shared/services/network.service';
|
||||
import { AppSettingsService } from '@shared/services';
|
||||
|
||||
@Component({
|
||||
selector: 'app-version',
|
||||
templateUrl: './app-version.component.html',
|
||||
styleUrls: ['./app-version.component.scss'],
|
||||
})
|
||||
export class AppVersionComponent implements OnInit {
|
||||
apiVersion: string;
|
||||
isVisible: boolean;
|
||||
|
||||
constructor(private networkService: NetworkService, private settingsService: AppSettingsService) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.apiVersion = await this.networkService.getApiVersion();
|
||||
this.isVisible = (await this.settingsService.getSettings()).showDebugVersion;
|
||||
if (this.isVisible) {
|
||||
document.querySelector<HTMLElement>('.banner--top')!.style.display='block';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { BuyTicketLogic } from './buy-ticket.logic';
|
||||
import { FlightModel } from '../../../shared/models-legacy';
|
||||
import { RouteTypeLegacy } from '../../../shared/enumerators';
|
||||
import * as moment from 'moment';
|
||||
|
||||
describe('BuyLogic', () => {
|
||||
let logic: BuyTicketLogic;
|
||||
const in5Hours = moment().add(5, 'hour').toDate();
|
||||
const in7Hours = moment().add(7, 'hour').toDate();
|
||||
let window$: Window;
|
||||
const invalid: FlightModel = {
|
||||
routeType: RouteTypeLegacy.Direct,
|
||||
leg: {
|
||||
departure: { scheduled: { airportCode: 'RYZ' }, times: { scheduled: { utc: moment().add(100500, 'hour').toDate() } } },
|
||||
arrival: { scheduled: { airportCode: 'PRG' } },
|
||||
},
|
||||
} as any;
|
||||
const direct: FlightModel = {
|
||||
routeType: RouteTypeLegacy.Direct,
|
||||
leg: {
|
||||
departure: { scheduled: { airportCode: 'RYZ' }, times: { scheduled: { utc: in5Hours, local: in5Hours } } },
|
||||
arrival: { scheduled: { airportCode: 'PRG' } },
|
||||
},
|
||||
} as any;
|
||||
|
||||
const returning: FlightModel = {
|
||||
routeType: RouteTypeLegacy.Direct,
|
||||
leg: {
|
||||
departure: { scheduled: { airportCode: 'RYZ' }, times: { scheduled: { utc: in7Hours, local: in7Hours } } },
|
||||
arrival: { scheduled: { airportCode: 'PRG' } },
|
||||
},
|
||||
} as any;
|
||||
beforeEach(() => {
|
||||
window$ = {} as any;
|
||||
const settings = {
|
||||
buyPeriod: { min: -1, max: -10 },
|
||||
} as any;
|
||||
logic = new BuyTicketLogic(settings, window$);
|
||||
});
|
||||
it('should return isAnyAvailable for empty flights', () => {
|
||||
expect(logic.isAnyAvailable()).toBeFalse();
|
||||
expect(logic.isAnyAvailable(undefined as any, null as any)).toBeFalse();
|
||||
});
|
||||
|
||||
it('should return isAvailable for valid flight', () => {
|
||||
expect(logic.isAvailable(direct)).toBeTrue();
|
||||
});
|
||||
|
||||
it('should return isAvailable true for valid flight', () => {
|
||||
expect(logic.isAvailable(direct)).toBeTrue();
|
||||
});
|
||||
|
||||
it('should return isAvailable false for valid flight', () => {
|
||||
expect(logic.isAvailable(invalid)).toBeFalse();
|
||||
});
|
||||
|
||||
it('should return isAnyAvailable true for 1 valid of 2 flight', () => {
|
||||
expect(logic.isAnyAvailable(invalid, direct)).toBeTrue();
|
||||
expect(logic.isAnyAvailable(direct, invalid)).toBeTrue();
|
||||
});
|
||||
|
||||
it('should open single url', () => {
|
||||
const blank = { focus: jasmine.createSpy().and.callThrough() };
|
||||
window$.open = jasmine
|
||||
.createSpy('open')
|
||||
.withArgs(
|
||||
`https://www.aeroflot.ru/sb/app/ru-ru#/search?adults=1&cabin=economy&children=0&infants=0&routes=RYZ.${moment(
|
||||
in5Hours,
|
||||
).format('YYYYMMDD')}.PRG&autosearch=Y`,
|
||||
'_blank',
|
||||
)
|
||||
.and.returnValue(blank);
|
||||
|
||||
logic.buyAll(direct);
|
||||
|
||||
expect(window$.open).toHaveBeenCalled();
|
||||
expect(blank.focus).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should open double url', () => {
|
||||
const blank = { focus: jasmine.createSpy().and.callThrough() };
|
||||
|
||||
const expectedUrl = `https://www.aeroflot.ru/sb/app/ru-ru#/search?adults=1&cabin=economy&children=0&infants=0&routes=RYZ.${moment(
|
||||
in5Hours,
|
||||
).format('YYYYMMDD')}.PRG-RYZ.${moment(in7Hours).format('YYYYMMDD')}.PRG&autosearch=Y`;
|
||||
|
||||
window$.open = jasmine.createSpy('open').withArgs(expectedUrl, '_blank').and.returnValue(blank);
|
||||
|
||||
logic.buyAll(direct, returning);
|
||||
|
||||
expect(window$.open).toHaveBeenCalled();
|
||||
expect(blank.focus).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
+1
@@ -0,0 +1 @@
|
||||
<button class="color orange" pButton type="button" label="{{ 'SHARED.BUY-TICKET' | translate }}" (click)="buy(); $event.stopPropagation()"></button>
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
:host {
|
||||
display: flex;
|
||||
|
||||
button {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { BuyTicketLogic } from './buy-ticket.logic';
|
||||
import { BuyTicketButtonComponent } from './buy-ticket-button.component';
|
||||
import { FlightModel } from '@shared/models-legacy';
|
||||
import { getMockPipe } from '@shared/pipes/pipe.mock';
|
||||
describe('BuyTicketButtonComponent', () => {
|
||||
let component: BuyTicketButtonComponent;
|
||||
let fixture: ComponentFixture<BuyTicketButtonComponent>;
|
||||
let logic: BuyTicketLogic;
|
||||
const direct: FlightModel = {} as any;
|
||||
const returning: FlightModel = {} as any;
|
||||
beforeEach(async () => {
|
||||
logic = {} as any;
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [BuyTicketButtonComponent, getMockPipe('translate')],
|
||||
providers: [
|
||||
{
|
||||
provide: BuyTicketLogic,
|
||||
useFactory: () => logic,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BuyTicketButtonComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should buy', () => {
|
||||
logic.buyAll = jasmine
|
||||
.createSpy('buyAll')
|
||||
.withArgs(direct, returning)
|
||||
.and.callThrough();
|
||||
component.direct = direct;
|
||||
component.returning = returning;
|
||||
|
||||
component.buy();
|
||||
|
||||
expect(logic.buyAll).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { FlightModel } from '@app/shared/models-legacy';
|
||||
import { BuyTicketLogic } from './buy-ticket.logic';
|
||||
|
||||
@Component({
|
||||
selector: 'buy-ticket-button',
|
||||
templateUrl: './buy-ticket-button.component.html',
|
||||
styleUrls: ['./buy-ticket-button.component.scss']
|
||||
})
|
||||
export class BuyTicketButtonComponent {
|
||||
@Input() direct: FlightModel;
|
||||
@Input() returning: FlightModel;
|
||||
|
||||
constructor(private logic: BuyTicketLogic) {}
|
||||
|
||||
buy() {
|
||||
this.logic.buyAll(this.direct, this.returning);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { FlightStatusLegacy } from '@app/shared/enumerators';
|
||||
import { AppSettings, FlightModel, getFirstLeg, getLastLeg } from '@app/shared/models-legacy';
|
||||
import { APP_SETTINGS, WINDOW } from '@app/shared/services';
|
||||
import * as moment from 'moment';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class BuyTicketLogic {
|
||||
constructor(@Inject(APP_SETTINGS) public settings: AppSettings, @Inject(WINDOW) private window$: Window) {}
|
||||
isAnyAvailable(...flights: (FlightModel | undefined)[]) {
|
||||
flights = (flights || []).filter((f) => !!f);
|
||||
|
||||
if (!flights.length) return false;
|
||||
|
||||
for (const flight of flights) if (this.isAvailable(flight)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
isAvailable(flight?: FlightModel) {
|
||||
const { min, max } = this.settings.buyPeriod;
|
||||
|
||||
if (!flight) return false;
|
||||
if (flight.status === FlightStatusLegacy.cancelled) return false;
|
||||
if (flight.status == FlightStatusLegacy.inFlight) return false;
|
||||
|
||||
const date = getFirstLeg(flight).departure.times.scheduled.utc;
|
||||
|
||||
const now = moment();
|
||||
|
||||
const from = moment(date).add(max, 'hour'); // max before flight
|
||||
const to = moment(date).add(min, 'hour'); // min before flight
|
||||
|
||||
if (from < now && now < to) return true;
|
||||
|
||||
// console.log(`Buy ticket button is not available. departure utc: ${date}, from: ${from}, now: ${now}, to: ${to}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
private open(url: string) {
|
||||
this.window$.open(url, '_blank').focus();
|
||||
}
|
||||
|
||||
public buyAll(direct: FlightModel, returning?: FlightModel) {
|
||||
if (!direct) return;
|
||||
|
||||
const url = returning ? this.getLinkWithReturn(direct, returning) : this.getLink(direct);
|
||||
|
||||
this.open(url);
|
||||
}
|
||||
private getLink(flight: FlightModel) {
|
||||
const firstLeg = getFirstLeg(flight);
|
||||
const lastLeg = getLastLeg(flight);
|
||||
|
||||
const depAirport = firstLeg.departure.scheduled.airportCode;
|
||||
const arrAirport = lastLeg.arrival.scheduled.airportCode;
|
||||
const date = moment(firstLeg.departure.times.scheduled.local).format('YYYYMMDD');
|
||||
const params = `${depAirport}.${date}.${arrAirport}`;
|
||||
|
||||
return `https://www.aeroflot.ru/sb/app/ru-ru#/search?adults=1&cabin=economy&children=0&infants=0&routes=${params}&autosearch=Y`;
|
||||
}
|
||||
|
||||
private getLinkWithReturn(flight1: FlightModel, flight2: FlightModel) {
|
||||
const firstLeg1 = getFirstLeg(flight1);
|
||||
const lastLeg1 = getLastLeg(flight1);
|
||||
|
||||
const depAirport1 = firstLeg1.departure.scheduled.airportCode;
|
||||
const arrAirport1 = lastLeg1.arrival.scheduled.airportCode;
|
||||
const date1 = moment(firstLeg1.departure.times.scheduled.local).format('YYYYMMDD');
|
||||
|
||||
const firstLeg2 = getFirstLeg(flight2);
|
||||
const lastLeg2 = getLastLeg(flight2);
|
||||
|
||||
const depAirport2 = firstLeg2.departure.scheduled.airportCode;
|
||||
const arrAirport2 = lastLeg2.arrival.scheduled.airportCode;
|
||||
const date2 = moment(firstLeg2.departure.times.scheduled.local).format('YYYYMMDD');
|
||||
|
||||
const params = `${depAirport1}.${date1}.${arrAirport1}-${depAirport2}.${date2}.${arrAirport2}`;
|
||||
|
||||
return `https://www.aeroflot.ru/sb/app/ru-ru#/search?adults=1&cabin=economy&children=0&infants=0&routes=${params}&autosearch=Y`;
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
<div [ngClass]="timeGroupClasses">
|
||||
<text *ngIf="caption" color="light-gray" size="12" class="captioned-time-group__caption">
|
||||
{{ caption | translate }}
|
||||
</text>
|
||||
<time-group
|
||||
[bold]="bold"
|
||||
[dayChangePosition]="dayChangePosition"
|
||||
[dayChangeLayout]="dayChangeLayout"
|
||||
[size]="size"
|
||||
[scheduled]="times"
|
||||
[withUTC]="withUTC"
|
||||
[align]="align"
|
||||
>
|
||||
<text *ngIf="withDate" size="12" color="blue" class="captioned-time-group__date">
|
||||
{{ date }}
|
||||
</text>
|
||||
</time-group>
|
||||
</div>
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
@use 'src/styles/colors' as *;
|
||||
@use 'src/styles/fonts' as *;
|
||||
@use 'src/styles/screen' as *;
|
||||
|
||||
:host {
|
||||
// prevents component from growing in height due to font-size inheritance
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.captioned-time-group {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__caption {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.captioned-time-group--right {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.captioned-time-group--mobile-right {
|
||||
@include mobile {
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.captioned-time-group--mobile-left {
|
||||
@include gt-mobile {
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import { TimeGroupBaseComponent } from '@modules/components/time-group/time-group-base.component';
|
||||
import { DATE_FORMAT } from '@shared/helpers';
|
||||
import { ITimesSet } from '@typings/times';
|
||||
import * as moment from 'moment';
|
||||
|
||||
@Component({
|
||||
selector: 'captioned-time-group',
|
||||
templateUrl: './captioned-time-group.component.html',
|
||||
styleUrls: ['./captioned-time-group.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class CaptionedTimeGroupComponent extends TimeGroupBaseComponent {
|
||||
@Input() times: ITimesSet;
|
||||
@Input() withDate = false;
|
||||
@Input() caption: string;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.size = 'extra-small';
|
||||
}
|
||||
|
||||
get date() {
|
||||
const [date] = this.times.local.split('T');
|
||||
|
||||
return date
|
||||
? moment(date).format(DATE_FORMAT)
|
||||
: moment(this.times.local).format(DATE_FORMAT);
|
||||
}
|
||||
|
||||
get timeGroupClasses() {
|
||||
return {
|
||||
'captioned-time-group': true,
|
||||
[`captioned-time-group--${this.align}`]: this.align !== 'left',
|
||||
};
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<div
|
||||
class="day-change-square-legacy"
|
||||
pTooltip="{{ time?.dayChange | dayChange: time?.local }}"
|
||||
tooltipPosition="top"
|
||||
tooltipStyleClass="afl-tooltip"
|
||||
>
|
||||
{{ time?.dayChange?.title }}
|
||||
</div>
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
@use './src/styles/framework' as *;
|
||||
|
||||
.day-change-square-legacy {
|
||||
border: 1px solid $blue-icon;
|
||||
color: $blue-light;
|
||||
border-radius: $border-radius;
|
||||
font-size: 10px;
|
||||
font-weight: $font-bold;
|
||||
width: $label-shift-width;
|
||||
background-color: $white;
|
||||
text-align: center;
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, ViewEncapsulation } from '@angular/core';
|
||||
import { IFlightTime } from '@app/shared/models-legacy';
|
||||
|
||||
@Component({
|
||||
selector: 'day-change-square-legacy',
|
||||
templateUrl: './day-change-square-legacy.component.html',
|
||||
styleUrls: ['./day-change-square-legacy.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
})
|
||||
export class DayChangeSquareLegacy {
|
||||
@Input() time: IFlightTime;
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<div
|
||||
class="day-change-square"
|
||||
pTooltip="{{ times.dayChange | dayChange: times.local }}"
|
||||
tooltipPosition="top"
|
||||
tooltipStyleClass="afl-tooltip"
|
||||
>
|
||||
{{ times.dayChange.title }}
|
||||
</div>
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
@use 'src/styles/colors' as *;
|
||||
@use 'src/styles/fonts' as *;
|
||||
|
||||
.day-change-square {
|
||||
box-sizing: border-box;
|
||||
padding: 0 3px;
|
||||
|
||||
border: 1px solid $blue-icon;
|
||||
border-radius: 2px;
|
||||
|
||||
font-size: $font-size-xs;
|
||||
line-height: 11px;
|
||||
text-align: center;
|
||||
color: $blue-light;
|
||||
font-weight: $font-bold;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { ITimesSet } from '@typings/times';
|
||||
|
||||
@Component({
|
||||
selector: 'day-change-square',
|
||||
templateUrl: './day-change-square.component.html',
|
||||
styleUrls: ['./day-change-square.component.scss'],
|
||||
})
|
||||
export class DayChangeSquareComponent {
|
||||
@Input() times: ITimesSet;
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<div class="detail-header-badge-flight-number">
|
||||
<div>
|
||||
<div data-testid="flight-details-number">
|
||||
{{ flight | flightNumber }}
|
||||
</div>
|
||||
<div class="description" *ngIf="showCodeSharing">
|
||||
{{ legs | codesharing }}
|
||||
</div>
|
||||
</div>
|
||||
<operator-logo [caption]="!icons.round" [round]="icons.round" [large]="icons.large" [flight]="flight"></operator-logo>
|
||||
</div>
|
||||
<flight-status-button [small]="true" [flight]="flight" *ngIf="canShowStatus && shouldShowStatus"></flight-status-button>
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
@use './src/styles/layouts' as *;
|
||||
@use './src/styles/colors' as *;
|
||||
@use './src/styles/variables' as *;
|
||||
@use './src/styles/fonts' as *;
|
||||
@use './src/styles/screen' as *;
|
||||
|
||||
:host {
|
||||
@include v-spacing($space-m);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
.detail-header-badge-flight-number {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
font-weight: $font-medium;
|
||||
font-size: $font-size-xl;
|
||||
@include h-spacing($space-m);
|
||||
|
||||
& > div {
|
||||
@include v-spacing($space-s);
|
||||
|
||||
.description {
|
||||
@include mobile() {
|
||||
white-space: normal !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include mobile() {
|
||||
flex: 1;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { getMockPipe } from '@shared/pipes/pipe.mock';
|
||||
import { FlightStatusLogic } from '../flight-status-button/flight-status.logic';
|
||||
|
||||
import { DetailsHeaderBadgeComponent } from './details-header-badge.component';
|
||||
|
||||
describe('DetailsHeaderBadgeComponent', () => {
|
||||
let component: DetailsHeaderBadgeComponent;
|
||||
let fixture: ComponentFixture<DetailsHeaderBadgeComponent>;
|
||||
let logic: FlightStatusLogic;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
DetailsHeaderBadgeComponent,
|
||||
getMockPipe('flightNumber'),
|
||||
getMockPipe('codesharing'),
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: FlightStatusLogic,
|
||||
useFactory: () => logic,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DetailsHeaderBadgeComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Input,
|
||||
OnChanges,
|
||||
SimpleChanges,
|
||||
} from '@angular/core';
|
||||
import { RouteTypeLegacy } from '@app/shared/enumerators';
|
||||
import { FlightModel, getLegs, Leg } from '@app/shared/models-legacy';
|
||||
import { FlightStatusLogic } from '../flight-status-button/flight-status.logic';
|
||||
|
||||
@Component({
|
||||
selector: 'details-header-badge',
|
||||
templateUrl: './details-header-badge.component.html',
|
||||
styleUrls: ['./details-header-badge.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class DetailsHeaderBadgeComponent implements OnChanges {
|
||||
@Input() flight: FlightModel;
|
||||
@Input() canShowStatus = true;
|
||||
@Input() icons = { round: true, large: false };
|
||||
shouldShowStatus: boolean;
|
||||
RouteType = RouteTypeLegacy;
|
||||
legs: Leg[];
|
||||
showCodeSharing: boolean;
|
||||
|
||||
constructor(private statusLogic: FlightStatusLogic) {}
|
||||
|
||||
async ngOnChanges(changes: SimpleChanges) {
|
||||
if (this.flight && 'flight' in changes) {
|
||||
this.legs = getLegs(this.flight);
|
||||
this.shouldShowStatus = this.statusLogic.available(this.flight);
|
||||
this.showCodeSharing = this.hasCodeSharing();
|
||||
}
|
||||
}
|
||||
|
||||
private hasCodeSharing() {
|
||||
return this.legs.some((leg) => leg.hasCodesharing);
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
<details-header-badge [canShowStatus]="canShowStatus" [icons]="icons" *ngFor="let flight of flights" [flight]="flight"></details-header-badge>
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
@use './src/styles/layouts' as *;
|
||||
@use './src/styles/colors' as *;
|
||||
@use './src/styles/variables' as *;
|
||||
@use './src/styles/fonts' as *;
|
||||
@use './src/styles/screen' as *;
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
|
||||
@include gt-mobile() {
|
||||
& > *:not(:last-child) {
|
||||
padding-right: $space-xl;
|
||||
margin-right: $space-xl;
|
||||
border-right: 1px solid $border;
|
||||
}
|
||||
}
|
||||
|
||||
@include mobile() {
|
||||
flex: 1;
|
||||
flex-wrap: wrap;
|
||||
@include v-spacing($space-m);
|
||||
|
||||
details-header-badge {
|
||||
flex: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DetailsHeaderBadgesComponent } from './details-header-badges.component';
|
||||
|
||||
describe('DetailsHeaderBadgesComponent', () => {
|
||||
let component: DetailsHeaderBadgesComponent;
|
||||
let fixture: ComponentFixture<DetailsHeaderBadgesComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ DetailsHeaderBadgesComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DetailsHeaderBadgesComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import { FlightModel } from '@app/shared/models-legacy';
|
||||
|
||||
@Component({
|
||||
selector: 'details-header-badges',
|
||||
templateUrl: './details-header-badges.component.html',
|
||||
styleUrls: ['./details-header-badges.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class DetailsHeaderBadgesComponent {
|
||||
@Input() flights: FlightModel[];
|
||||
@Input() canShowStatus = true;
|
||||
@Input() icons = { round: true, large: false };
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<span *ngIf="!this.clarifying" class="duration">{{ duration | duration }}</span>
|
||||
<span *ngIf="this.clarifying" class="duration duration--clarifying">{{ 'FLIGHT-STATUSES.Unknown' | translate }}</span>
|
||||
@@ -0,0 +1,9 @@
|
||||
@use './src/styles/framework' as *;
|
||||
|
||||
.duration {
|
||||
@include font-overflow;
|
||||
|
||||
&--clarifying {
|
||||
color: $orange;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { getMockPipe } from '@shared/pipes/pipe.mock';
|
||||
import { DurationComponent } from './duration.component';
|
||||
|
||||
describe('DurationComponent', () => {
|
||||
let component: DurationComponent;
|
||||
let fixture: ComponentFixture<DurationComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [DurationComponent, getMockPipe('duration')],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DurationComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
|
||||
import { Duration, getTotalMinutes } from '@app/shared/models-legacy';
|
||||
|
||||
@Component({
|
||||
selector: 'duration',
|
||||
templateUrl: './duration.component.html',
|
||||
styleUrls: ['./duration.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class DurationComponent implements OnChanges {
|
||||
@Input() duration: Duration;
|
||||
clarifying = false;
|
||||
|
||||
ngOnChanges() {
|
||||
const minutes = getTotalMinutes(this.duration);
|
||||
|
||||
this.clarifying = minutes < 15;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<print-button
|
||||
class="flight-actions__action"
|
||||
*ngIf="print"
|
||||
[flight]="flight"
|
||||
></print-button>
|
||||
|
||||
<share-button
|
||||
class="flight-actions__action"
|
||||
*ngIf="share"
|
||||
[flight]="flight"
|
||||
[viewType]="viewType"
|
||||
></share-button>
|
||||
|
||||
<div class="k-space" *ngIf="wide"></div>
|
||||
|
||||
<buy-ticket-button
|
||||
class="flight-actions__action"
|
||||
*ngIf="buy && canBuy"
|
||||
[direct]="flight"
|
||||
></buy-ticket-button>
|
||||
|
||||
<registration-button
|
||||
class="flight-actions__action"
|
||||
*ngIf="register && canRegister"
|
||||
[flight]="flight"
|
||||
></registration-button>
|
||||
|
||||
<flight-status-button
|
||||
class="flight-actions__action"
|
||||
*ngIf="status && canViewStatus"
|
||||
[flight]="flight"
|
||||
></flight-status-button>
|
||||
|
||||
<flight-details-button
|
||||
class="flight-actions__action"
|
||||
*ngIf="details"
|
||||
[flight]="$any(flight)"
|
||||
(toDetails)="handleToDetailsEvent($event)"
|
||||
></flight-details-button>
|
||||
@@ -0,0 +1,58 @@
|
||||
@use './src/styles/layouts' as *;
|
||||
@use './src/styles/variables' as *;
|
||||
@use './src/styles/screen' as *;
|
||||
|
||||
@mixin main-buttons() {
|
||||
buy-ticket-button,
|
||||
registration-button,
|
||||
flight-status-button,
|
||||
flight-details-button {
|
||||
@content();
|
||||
}
|
||||
}
|
||||
|
||||
@mixin small-buttons() {
|
||||
print-button,
|
||||
share-button {
|
||||
@content();
|
||||
}
|
||||
}
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
& > .k-space {
|
||||
flex: 1;
|
||||
|
||||
@include mobile() {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@include gt-mobile() {
|
||||
@include h-spacing($space-m);
|
||||
|
||||
@include main-buttons() {
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
@include mobile() {
|
||||
flex-wrap: wrap;
|
||||
justify-content: start;
|
||||
@include v-spacing($space-l, true);
|
||||
|
||||
@include main-buttons() {
|
||||
order: 1;
|
||||
flex: 100%;
|
||||
}
|
||||
|
||||
@include small-buttons() {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
share-button {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { FlightModel } from '../../../shared/models-legacy';
|
||||
import { BuyTicketLogic } from '../buy-ticket-button/buy-ticket.logic';
|
||||
import { FlightStatusLogic } from '../flight-status-button/flight-status.logic';
|
||||
import { RegistrationLogic } from '../registration-button/registration.logic';
|
||||
|
||||
import { FlightActionsComponent } from './flight-actions.component';
|
||||
|
||||
describe('FlightActionsComponent', () => {
|
||||
let component: FlightActionsComponent;
|
||||
let fixture: ComponentFixture<FlightActionsComponent>;
|
||||
let registration: RegistrationLogic;
|
||||
let buying: BuyTicketLogic;
|
||||
let statuses: FlightStatusLogic;
|
||||
const flight: FlightModel = {} as any;
|
||||
|
||||
beforeEach(async () => {
|
||||
registration = { isAnyAvailable: jasmine.createSpy().withArgs(flight).and.returnValue(Promise.resolve(true))} as any;
|
||||
buying = {available: jasmine.createSpy().withArgs(flight).and.returnValue(true) } as any;
|
||||
statuses = {available: jasmine.createSpy().withArgs(flight).and.returnValue(Promise.resolve(true)) } as any;
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [FlightActionsComponent],
|
||||
providers: [
|
||||
{ provide: RegistrationLogic, useFactory: () => registration },
|
||||
{ provide: BuyTicketLogic, useFactory: () => buying },
|
||||
{ provide: FlightStatusLogic, useFactory: () => statuses },
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(FlightActionsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
} from '@angular/core';
|
||||
import { ViewType } from '@app/shared/enumerators/flight-request-type.enum';
|
||||
import { FlightModel } from '@app/shared/models-legacy';
|
||||
import { IFlight } from '@typings/flight/flight';
|
||||
import { BuyTicketLogic } from '../buy-ticket-button/buy-ticket.logic';
|
||||
import { RegistrationLogic } from '../registration-button/registration.logic';
|
||||
import { FlightStatusLogic } from '../flight-status-button/flight-status.logic';
|
||||
@Component({
|
||||
selector: 'flight-actions',
|
||||
templateUrl: './flight-actions.component.html',
|
||||
styleUrls: ['./flight-actions.component.scss'],
|
||||
host: {
|
||||
class: 'p-print-none',
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FlightActionsComponent implements OnChanges {
|
||||
@Input() print = false; // hidden in scope of 7541
|
||||
@Input() share = true;
|
||||
@Input() details = true;
|
||||
@Input() register = true;
|
||||
@Input() status = true;
|
||||
@Input() buy = true;
|
||||
|
||||
@Input() viewType: ViewType;
|
||||
@Input() flight: FlightModel;
|
||||
@Input() wide = false;
|
||||
|
||||
@Output() toDetails = new EventEmitter<IFlight>();
|
||||
|
||||
canBuy: boolean;
|
||||
canRegister: boolean;
|
||||
canViewStatus: boolean;
|
||||
|
||||
constructor(
|
||||
private logic: BuyTicketLogic,
|
||||
private registration: RegistrationLogic,
|
||||
private statuses: FlightStatusLogic,
|
||||
) {}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (this.flight && 'flight' in changes) {
|
||||
this.canBuy = this.logic.isAnyAvailable(this.flight);
|
||||
this.canRegister = this.registration.isAnyAvailable(this.flight);
|
||||
this.canViewStatus = this.statuses.available(this.flight);
|
||||
}
|
||||
}
|
||||
|
||||
handleToDetailsEvent(flight: IFlight) {
|
||||
this.toDetails.emit(flight);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<ng-container *ngFor="let leg of legs; let index = index; let last = last">
|
||||
<div class="leg-point-brief">
|
||||
<time-group-legacy
|
||||
[times]="getLatest(leg, 'departure')"
|
||||
[baseline]="getBaseline(leg, 'departure')"
|
||||
></time-group-legacy>
|
||||
|
||||
<station [station]="$any(leg.departure)"></station>
|
||||
</div>
|
||||
<div class="leg-point-brief">
|
||||
<time-group-legacy
|
||||
[times]="getLatest(leg, 'arrival')"
|
||||
[baseline]="getBaseline(leg, 'arrival')"
|
||||
></time-group-legacy>
|
||||
|
||||
<station [station]="$any(leg.arrival)"></station>
|
||||
</div>
|
||||
|
||||
<transfer [leg]="leg" [viewType]="viewType" *ngIf="!last && leg.next">
|
||||
</transfer>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,24 @@
|
||||
@use './src/styles/variables' as *;
|
||||
@use './src/styles/colors' as *;
|
||||
@use './src/styles/layouts' as *;
|
||||
@use './src/styles/fonts' as *;
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
.leg-point-brief {
|
||||
padding: $space-xl 0;
|
||||
margin: 0 $space-xl;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
time-group-legacy {
|
||||
font-size: $font-size-xl2;
|
||||
width: 90px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.leg-point-brief + .leg-point-brief {
|
||||
border-top: 1px dashed $border;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FlightBriefComponent } from './flight-brief.component';
|
||||
|
||||
describe('FlightBriefComponent', () => {
|
||||
let component: FlightBriefComponent;
|
||||
let fixture: ComponentFixture<FlightBriefComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ FlightBriefComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(FlightBriefComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { ViewType } from '@app/shared/enumerators/flight-request-type.enum';
|
||||
import { ArrivalTimesModel, DepartureTimesModel, Leg } from '@app/shared/models-legacy';
|
||||
import { StationProperty } from '../timeline/timeline.component';
|
||||
const defaults = {} as ArrivalTimesModel | DepartureTimesModel;
|
||||
@Component({
|
||||
selector: 'flight-brief',
|
||||
templateUrl: './flight-brief.component.html',
|
||||
styleUrls: ['./flight-brief.component.scss'],
|
||||
})
|
||||
export class FlightBriefComponent {
|
||||
@Input() legs: Leg[];
|
||||
@Input() canChange = false;
|
||||
viewType: ViewType;
|
||||
/// returns latest or scheduled in case the time can change
|
||||
/// returns scheduled in case the time can not change
|
||||
getLatest(leg: Leg, property: StationProperty) {
|
||||
const { latest, scheduled } = this.getTimes(leg, property);
|
||||
return this.canChange ? latest || scheduled : scheduled;
|
||||
}
|
||||
|
||||
/// returns scheduled in case the time can change
|
||||
/// returns undefined in case the time can not change
|
||||
getBaseline(leg: Leg, property: StationProperty) {
|
||||
if (!this.canChange) return;
|
||||
const { scheduled } = this.getTimes(leg, property);
|
||||
return scheduled;
|
||||
}
|
||||
|
||||
getType() {
|
||||
return this.canChange ? 'actual' : 'scheduled';
|
||||
}
|
||||
|
||||
private getTimes(leg: Leg, property: StationProperty) {
|
||||
if (!leg) return defaults;
|
||||
return leg[property]?.times || defaults;
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<button
|
||||
data-testid="flight-details-button"
|
||||
class="color blue"
|
||||
pButton
|
||||
type="button"
|
||||
label="{{ 'SHARED.FLIGHT-DETAILS' | translate }}"
|
||||
(click)="openDetails(); $event.stopPropagation()"
|
||||
></button>
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
:host {
|
||||
display: flex;
|
||||
|
||||
button {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { RouterHandlerService } from '@shared/services/router-handler.service';
|
||||
import { getMockPipe } from '@shared/pipes/pipe.mock';
|
||||
import { FlightDetailsButtonComponent } from './flight-details-button.component';
|
||||
|
||||
describe('FlightDetailsButtonComponent', () => {
|
||||
let component: FlightDetailsButtonComponent;
|
||||
let fixture: ComponentFixture<FlightDetailsButtonComponent>;
|
||||
let router: RouterHandlerService;
|
||||
|
||||
beforeEach(async () => {
|
||||
router = {} as any;
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
FlightDetailsButtonComponent,
|
||||
getMockPipe('translate'),
|
||||
],
|
||||
providers: [
|
||||
{ provide: RouterHandlerService, useFactory: () => router },
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(FlightDetailsButtonComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { IFlight } from '@typings/flight/flight';
|
||||
|
||||
@Component({
|
||||
selector: 'flight-details-button',
|
||||
templateUrl: './flight-details-button.component.html',
|
||||
styleUrls: ['./flight-details-button.component.scss'],
|
||||
})
|
||||
export class FlightDetailsButtonComponent {
|
||||
@Input() flight: IFlight;
|
||||
|
||||
@Output() toDetails = new EventEmitter<IFlight>();
|
||||
|
||||
openDetails() {
|
||||
this.toDetails.emit(this.flight);
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
<a class="flight-details-icon" target="_blank" [href]="href">
|
||||
<img class="flight-details-icon__image" [src]="src" />
|
||||
<div class="flight-details-icon__description">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</a>
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
@use "src/styles/framework" as *;
|
||||
|
||||
.flight-details-icon {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
padding: $space-xl;
|
||||
width: 120px;
|
||||
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
border-radius: $border-radius;
|
||||
transition-duration: 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: $blue-extra-light;
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
flex-direction: row;
|
||||
|
||||
padding: $space-m;
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
&__image {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-bottom: $space-s2;
|
||||
|
||||
@include smTablet {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
margin-bottom: 0;
|
||||
margin-right: $space-m;
|
||||
}
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-size: $font-size-s;
|
||||
line-height: 16px;
|
||||
font-weight: $font-medium;
|
||||
color: $blue-light;
|
||||
|
||||
white-space: normal !important;
|
||||
text-align: center;
|
||||
|
||||
@include mobile {
|
||||
font-size: $font-size-xs;
|
||||
line-height: 13px;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: 'flight-details-icon',
|
||||
templateUrl: './flight-details-icon.component.html',
|
||||
styleUrls: ['./flight-details-icon.component.scss']
|
||||
})
|
||||
export class FlightDetailsIconComponent {
|
||||
@Input() href;
|
||||
@Input() src;
|
||||
|
||||
constructor() { }
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<div class="flight-details-section">
|
||||
<div class="flight-details-section__caption">
|
||||
<ng-content select="[caption]"></ng-content>
|
||||
</div>
|
||||
<div class="flight-details-section__content">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
@use './src/styles/framework' as *;
|
||||
|
||||
.flight-details-section {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
@include mobile {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__caption {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
|
||||
margin-right: $space-xl;
|
||||
width: 210px;
|
||||
|
||||
font-size: $font-size-s;
|
||||
line-height: 16px;
|
||||
color: $gray;
|
||||
|
||||
@include mobile {
|
||||
margin-right: 0;
|
||||
margin-bottom: $space-m;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
|
||||
@include h-deep-spacing($space-xl);
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'flight-details-section',
|
||||
templateUrl: './flight-details-section.component.html',
|
||||
styleUrls: ['./flight-details-section.component.scss'],
|
||||
})
|
||||
export class FlightDetailsSectionComponent {
|
||||
constructor() {}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<div [ngClass]="flightEventsClasses">
|
||||
<div *ngIf="changeRoute" class="flight-events__event" role="changeRoute">
|
||||
<change-icon></change-icon>
|
||||
<div class="flight-events__event-description" *ngIf="showDescription">
|
||||
{{ 'SHARED.ROUTE-CHANGE' | translate }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--ВОЗВРАТ В АЭРОПОРТ ВЫЛЕТА-->
|
||||
<div *ngIf="reroute" class="flight-events__event" role="reroute">
|
||||
<return-icon></return-icon>
|
||||
<div class="flight-events__event-description" *ngIf="showDescription">
|
||||
{{ 'SHARED.RETURN' | translate }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,49 @@
|
||||
@use './src/styles/framework' as *;
|
||||
|
||||
@mixin column-layout() {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
@include h-spacing(0);
|
||||
@include v-spacing($space-s2);
|
||||
}
|
||||
|
||||
.flight-events {
|
||||
display: flex;
|
||||
@include h-spacing($space-m);
|
||||
|
||||
&__event {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
height: 28px;
|
||||
padding: 0 $space-m;
|
||||
|
||||
border-radius: $border-radius;
|
||||
border: 1px solid $border;
|
||||
|
||||
@include h-spacing($space-m);
|
||||
}
|
||||
|
||||
&__event-description {
|
||||
font-size: $font-size-s;
|
||||
line-height: 16px;
|
||||
color: $gray;
|
||||
}
|
||||
|
||||
&--column {
|
||||
@include column-layout;
|
||||
}
|
||||
|
||||
&--column-mobile {
|
||||
@include mobile {
|
||||
@include column-layout;
|
||||
}
|
||||
}
|
||||
|
||||
&--row-mobile {
|
||||
@include gt-mobile {
|
||||
@include column-layout;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Input,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
|
||||
export type IFlightEventsDirection =
|
||||
| 'row'
|
||||
| 'row-mobile'
|
||||
| 'column'
|
||||
| 'column-mobile';
|
||||
@Component({
|
||||
selector: 'flight-events',
|
||||
templateUrl: './flight-events.component.html',
|
||||
styleUrls: ['./flight-events.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FlightEventsComponent {
|
||||
@Input() reroute = false;
|
||||
@Input() changeRoute = false;
|
||||
@Input() showDescription = false;
|
||||
@Input() direction: IFlightEventsDirection = 'row';
|
||||
|
||||
get flightEventsClasses() {
|
||||
return {
|
||||
'flight-events': true,
|
||||
[`flight-events--${this.direction}`]: this.direction !== 'row',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<div class="flight-props-caption">
|
||||
<ng-content select="[caption]"></ng-content>
|
||||
</div>
|
||||
<div class="flight-props-body">
|
||||
<ng-content select="[body]"></ng-content>
|
||||
</div>
|
||||
<arrow-down-icon *ngIf="expandable"></arrow-down-icon>
|
||||
@@ -0,0 +1,80 @@
|
||||
@use './src/styles/variables' as *;
|
||||
@use './src/styles/screen' as *;
|
||||
@use './src/styles/colors' as *;
|
||||
@use './src/styles/fonts' as *;
|
||||
@use './src/styles/layouts' as *;
|
||||
|
||||
.flight-props {
|
||||
display: flex;
|
||||
color: $text-color;
|
||||
font-weight: $font-medium;
|
||||
position: relative;
|
||||
|
||||
&-caption {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
|
||||
@include h-spacing($space-xl);
|
||||
|
||||
@include mobile() {
|
||||
flex: 100%;
|
||||
align-items: center;
|
||||
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
&-body {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
|
||||
column-gap: $space-l;
|
||||
row-gap: $space-l;
|
||||
|
||||
@include mobile() {
|
||||
grid-template-columns: 3fr 2fr;
|
||||
|
||||
margin-top: $space-xl;
|
||||
|
||||
&:empty {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@include tablets() {
|
||||
grid-template-columns: repeat(4, var(--property-column-width, 1fr));
|
||||
}
|
||||
|
||||
@include desktop() {
|
||||
grid-template-columns: repeat(5, var(--property-column-width, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@include gt-mobile() {
|
||||
@include h-spacing($space-m);
|
||||
}
|
||||
|
||||
@include mobile() {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
time-note {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 3; // span for 2 columns
|
||||
}
|
||||
|
||||
&.equal-columns {
|
||||
.flight-props-body {
|
||||
@include mobile() {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
arrow-down-icon {
|
||||
@include mobile {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FlightPropsComponent } from './flight-props.component';
|
||||
|
||||
describe('FlightPropsComponent', () => {
|
||||
let component: FlightPropsComponent;
|
||||
let fixture: ComponentFixture<FlightPropsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ FlightPropsComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(FlightPropsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Component, Input, ViewEncapsulation } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'flight-props',
|
||||
templateUrl: './flight-props.component.html',
|
||||
styleUrls: ['./flight-props.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
host: {
|
||||
class: 'flight-props',
|
||||
},
|
||||
})
|
||||
export class FlightPropsComponent {
|
||||
@Input() columnWidth = '1fr';
|
||||
@Input() expandable = false;
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<button
|
||||
class="flight-status-button color"
|
||||
[ngClass]="{ 'blue-light': !small, 'blue-glow': small, small: small }"
|
||||
pButton
|
||||
type="button"
|
||||
label="{{ 'SHARED.DETAILS' | translate }}"
|
||||
pTooltip="{{ 'SHARED.DETAILS-TOOLTIP' | translate }}"
|
||||
tooltipPosition="top"
|
||||
tooltipStyleClass="afl-tooltip"
|
||||
(click)="open(); $event.stopPropagation()"
|
||||
></button>
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
@use './src/styles/colors' as *;
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
|
||||
button {
|
||||
&:not(.small) {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.flight-status-button {
|
||||
padding: 0 7px;
|
||||
}
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { getMockPipe } from '@shared/pipes/pipe.mock';
|
||||
import { FlightStatusButtonComponent } from './flight-status-button.component';
|
||||
import { RouteTypeLegacy } from '@shared/enumerators';
|
||||
import { AirlineCodes } from '@shared/airlines';
|
||||
import { FlightStatusLogic } from './flight-status.logic';
|
||||
import { RouterHandlerService } from '@shared/services/router-handler.service';
|
||||
import { FlightModel } from '@shared/models-legacy';
|
||||
|
||||
describe('FlightStatusButtonComponent', () => {
|
||||
let component: FlightStatusButtonComponent;
|
||||
let fixture: ComponentFixture<FlightStatusButtonComponent>;
|
||||
let navigate: RouterHandlerService;
|
||||
let logic: FlightStatusLogic;
|
||||
const aeroflotFlight: FlightModel = {
|
||||
routeType: RouteTypeLegacy.Direct,
|
||||
operatingBy: { scheduled: AirlineCodes.Aeroflot },
|
||||
} as any;
|
||||
const pobedayFlight: FlightModel = {
|
||||
routeType: RouteTypeLegacy.Direct,
|
||||
operatingBy: { scheduled: AirlineCodes.Pobeda },
|
||||
} as any;
|
||||
const rossiyaFlight: FlightModel = {
|
||||
routeType: RouteTypeLegacy.Direct,
|
||||
operatingBy: { scheduled: AirlineCodes.Rossiya },
|
||||
} as any;
|
||||
const auroraFlight: FlightModel = {
|
||||
routeType: RouteTypeLegacy.Direct,
|
||||
operatingBy: { scheduled: AirlineCodes.Aurora },
|
||||
} as any;
|
||||
const airfranceFlight: FlightModel = {
|
||||
routeType: RouteTypeLegacy.Direct,
|
||||
operatingBy: { scheduled: AirlineCodes.AirFrance },
|
||||
} as any;
|
||||
let window$: any;
|
||||
beforeEach(async () => {
|
||||
logic = {} as any;
|
||||
navigate = {} as any;
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
FlightStatusButtonComponent,
|
||||
getMockPipe('translate'),
|
||||
],
|
||||
providers: [
|
||||
{ provide: RouterHandlerService, useFactory: () => navigate },
|
||||
{ provide: FlightStatusLogic, useFactory: () => logic },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(FlightStatusButtonComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.window$ = window$ = {} as any;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should open aeroflot, but not open airfrance', () => {
|
||||
logic.available = jasmine
|
||||
.createSpy('available')
|
||||
.withArgs(airfranceFlight)
|
||||
.and.returnValue(false)
|
||||
.withArgs(aeroflotFlight)
|
||||
.and.returnValue(true);
|
||||
navigate.toBoardDetailsWithBlank = jasmine
|
||||
.createSpy('toBoardDetailsWithBlank')
|
||||
.withArgs(aeroflotFlight)
|
||||
.and.callThrough();
|
||||
|
||||
component.flight = {
|
||||
routeType: RouteTypeLegacy.Connecting,
|
||||
flights: [airfranceFlight, aeroflotFlight],
|
||||
} as any;
|
||||
component.open();
|
||||
expect(navigate.toBoardDetailsWithBlank).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should open pobeda and aurora on its own sites and open rossia on native details', () => {
|
||||
logic.available = jasmine.createSpy('available').and.returnValue(true);
|
||||
navigate.toBoardDetailsWithBlank = jasmine
|
||||
.createSpy('toBoardDetailsWithBlank')
|
||||
.withArgs(rossiyaFlight)
|
||||
.and.callThrough();
|
||||
|
||||
window$.open = jasmine
|
||||
.createSpy('open')
|
||||
.withArgs('https://www.pobeda.aero', '_blank')
|
||||
.and.callThrough()
|
||||
.withArgs('https://www.flyaurora.ru', '_blank')
|
||||
.and.callThrough();
|
||||
|
||||
component.flight = {
|
||||
routeType: RouteTypeLegacy.Connecting,
|
||||
flights: [pobedayFlight, auroraFlight, rossiyaFlight],
|
||||
} as any;
|
||||
component.open();
|
||||
expect(window$.open).toHaveBeenCalledTimes(2);
|
||||
expect(navigate.toBoardDetailsWithBlank).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { getAirline } from '@app/shared/airlines';
|
||||
import { FlightModel, getFlights } from '@app/shared/models-legacy';
|
||||
import { RouterHandlerService } from '@app/shared/services/router-handler.service';
|
||||
import { FlightStatusLogic } from './flight-status.logic';
|
||||
|
||||
@Component({
|
||||
selector: 'flight-status-button',
|
||||
templateUrl: './flight-status-button.component.html',
|
||||
styleUrls: ['./flight-status-button.component.scss'],
|
||||
})
|
||||
export class FlightStatusButtonComponent {
|
||||
@Input() flight: FlightModel;
|
||||
@Input() small = false;
|
||||
window$ = window;
|
||||
constructor(private navigate: RouterHandlerService, private logic: FlightStatusLogic) {}
|
||||
open() {
|
||||
getFlights(this.flight).forEach((flight) => this.openOne(flight));
|
||||
}
|
||||
|
||||
private openOne(flight: FlightModel) {
|
||||
if (!this.logic.available(flight)) return;
|
||||
|
||||
const { canViewStatus, canViewNativeStatus, url } = getAirline(flight) || {};
|
||||
|
||||
if (!canViewStatus) return;
|
||||
|
||||
if (canViewNativeStatus) return this.navigate.toBoardDetailsWithBlank(flight);
|
||||
|
||||
return this.window$.open(url, '_blank');
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
import { FlightStatusLogic } from './flight-status.logic';
|
||||
import { RouteTypeLegacy } from '../../../shared/enumerators';
|
||||
import * as moment from 'moment';
|
||||
import { AppSettings } from '../../../shared/models-legacy';
|
||||
|
||||
describe('FlightStatusLogic', () => {
|
||||
let logic: FlightStatusLogic;
|
||||
const settings: AppSettings = {
|
||||
flightStatusAvailableFrom: 24,
|
||||
} as any;
|
||||
beforeEach(() => {
|
||||
logic = new FlightStatusLogic(settings);
|
||||
});
|
||||
|
||||
it('should be not available for empty', () => {
|
||||
expect(logic.available(null as any)).toBeFalse();
|
||||
});
|
||||
|
||||
it('should be not available for far future', () => {
|
||||
expect(
|
||||
logic.available({
|
||||
operatingBy: {
|
||||
scheduled: 'SU',
|
||||
},
|
||||
routeType: RouteTypeLegacy.Direct,
|
||||
dateToSearchBy: moment().add(5, 'day').toDate(),
|
||||
leg: {
|
||||
departure: {
|
||||
times: {
|
||||
scheduled: {
|
||||
utc: moment().add(5, 'day').toDate(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any),
|
||||
).toBeFalse();
|
||||
});
|
||||
|
||||
it('should be available for less than 1 day', () => {
|
||||
expect(
|
||||
logic.available({
|
||||
operatingBy: {
|
||||
scheduled: 'SU',
|
||||
},
|
||||
routeType: RouteTypeLegacy.Direct,
|
||||
dateToSearchBy: moment().add(1, 'minute').toDate(),
|
||||
leg: {
|
||||
departure: {
|
||||
times: {
|
||||
scheduled: {
|
||||
utc: moment().add(1, 'minute').toDate(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any),
|
||||
).toBeTrue();
|
||||
});
|
||||
|
||||
it('should not be available for not SU', () => {
|
||||
expect(
|
||||
logic.available({
|
||||
operatingBy: {
|
||||
scheduled: 'AF',
|
||||
},
|
||||
routeType: RouteTypeLegacy.Direct,
|
||||
dateToSearchBy: moment().add(1, 'minute').toDate(),
|
||||
leg: {
|
||||
departure: {
|
||||
times: {
|
||||
scheduled: {
|
||||
utc: moment().add(1, 'minute').toDate(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any),
|
||||
).toBeFalse();
|
||||
});
|
||||
|
||||
['SU', 'HZ', 'FV', 'DP'].forEach((airline) => {
|
||||
it(`should be available for not ${airline}`, () => {
|
||||
expect(
|
||||
logic.available({
|
||||
operatingBy: {
|
||||
scheduled: airline,
|
||||
},
|
||||
routeType: RouteTypeLegacy.Direct,
|
||||
dateToSearchBy: moment().add(1, 'minute').toDate(),
|
||||
leg: {
|
||||
departure: {
|
||||
times: {
|
||||
scheduled: {
|
||||
utc: moment().add(1, 'minute').toDate(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any),
|
||||
).toBeTrue();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { AppSettings, FlightModel, getFirstFlight, getFirstLeg } from '@app/shared/models-legacy';
|
||||
import { APP_SETTINGS } from '@app/shared/services';
|
||||
import { airlines } from '@shared/airlines';
|
||||
import * as moment from 'moment';
|
||||
|
||||
const airlinesWithStatus = new Set(airlines.filter((a) => a.canViewStatus).map((a) => a.code));
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class FlightStatusLogic {
|
||||
constructor(@Inject(APP_SETTINGS) public settings: AppSettings) {}
|
||||
available(flight: FlightModel) {
|
||||
if (!flight) return false;
|
||||
|
||||
const leg = getFirstLeg(flight);
|
||||
flight = getFirstFlight(flight);
|
||||
|
||||
// 1. only for SU
|
||||
const airline = (flight.operatingBy || leg.operatingBy)?.scheduled;
|
||||
if (!airlinesWithStatus.has(airline)) {
|
||||
// console.log(`Кнопка "Стату рейса" недоступна для рейса ${flight.hash} потому, что компания не та`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const departure = leg.departure.times.scheduled.local;
|
||||
const flightDate = flight.dateToSearchBy;
|
||||
// 2. only today
|
||||
const now = moment(); // 25 Nov 2021 22:00 (17 hours in advance, should be fine)
|
||||
if (!now.isSame(moment(flightDate), 'day')) {
|
||||
// console.log(
|
||||
// `Кнопка "Стату рейса" недоступна для рейса ${
|
||||
// flight.hash
|
||||
// } потому, что другой день (Отправление: ${departure} (лок. время аэропорта), дата рейса: ${flightDate} (тоже локальное аэропорта), сейчас: ${now.toDate()} (время пользователя))`,
|
||||
// );
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. only 24h ahead
|
||||
// 24hours
|
||||
const from = this.settings.flightStatusAvailableFrom;
|
||||
|
||||
// 25 Nov 2021 15:00 // 26 Nov 2021 15:00
|
||||
const availableFrom = moment(departure).add(-from, 'hour');
|
||||
if (now > availableFrom) return true; // now already passed
|
||||
|
||||
// console.log(
|
||||
// `Кнопка "Стату рейса" недоступна для рейса ${
|
||||
// flight.hash
|
||||
// } потому, что еще вермя не пришло. сейсас - ${now.toDate()}, будет доступно с ${availableFrom.toDate()} (за ${
|
||||
// this.settings.flightStatusAvailableFrom
|
||||
// }ч)`,
|
||||
// );
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { FlightStatusLegacy } from '@app/shared/enumerators';
|
||||
|
||||
export const statusColors = {
|
||||
[FlightStatusLegacy.scheduled]: 'blue',
|
||||
[FlightStatusLegacy.delayed]: 'orange',
|
||||
[FlightStatusLegacy.cancelled]: 'red',
|
||||
[FlightStatusLegacy.arrived]: 'green',
|
||||
[FlightStatusLegacy.inFlight]: 'green',
|
||||
[FlightStatusLegacy.landed]: 'green',
|
||||
[FlightStatusLegacy.sent]: 'green',
|
||||
[FlightStatusLegacy.unknown]: 'green',
|
||||
};
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
<svg class="svg--plane" ngClass="{{ color }}">
|
||||
<path
|
||||
data-name="Path 143"
|
||||
d="M6.334 16.625h1.583l3.958-6.334h4.355a1.188 1.188 0 0 0 0-2.375h-4.355L7.917 1.583H6.334l1.983 6.333H3.963L2.771 6.333H1.583l.792 2.771-.792 2.771h1.188l1.187-1.583h4.359Z"
|
||||
/>
|
||||
<path data-name="Path 144" d="M19 0v19H0V0Z" fill="none" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 348 B |
+25
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FlightStatusIconComponent } from './flight-status-icon.component';
|
||||
|
||||
describe('FlightStatusIconComponent', () => {
|
||||
let component: FlightStatusIconComponent;
|
||||
let fixture: ComponentFixture<FlightStatusIconComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ FlightStatusIconComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(FlightStatusIconComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { FlightStatusLegacy } from '@app/shared/enumerators';
|
||||
import { statusColors } from './flight-status-colors';
|
||||
|
||||
@Component({
|
||||
selector: 'flight-status-icon',
|
||||
templateUrl: './flight-status-icon.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FlightStatusIconComponent implements OnChanges {
|
||||
@Input() status: FlightStatusLegacy;
|
||||
@Input() isSchedule: boolean;
|
||||
|
||||
color: string;
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (!changes.status) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.color = this.isSchedule ? statusColors[FlightStatusLegacy.scheduled] : statusColors[this.status];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<property class="flight-status" description="{{ 'BOARDING-STATUSES.STATUS' | translate }}">
|
||||
<div class="flight-status__content">
|
||||
<span [ngClass]="indicatorClasses"></span>
|
||||
<text weight="medium">
|
||||
{{ 'BOARDING-STATUSES.' + status | translate }}
|
||||
</text>
|
||||
</div>
|
||||
</property>
|
||||
@@ -0,0 +1,31 @@
|
||||
@use "src/styles/variables" as vars;
|
||||
@use "src/styles/colors";
|
||||
@use "src/styles/layouts";
|
||||
|
||||
.flight-status {
|
||||
&__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@include layouts.h-spacing(vars.$space-s2);
|
||||
}
|
||||
|
||||
&__indicator {
|
||||
width: vars.$status-indicator-size;
|
||||
height: vars.$status-indicator-size;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
background-color: colors.$light-gray;
|
||||
border: 1px solid colors.$light-gray;
|
||||
|
||||
&--Finished {
|
||||
background-color: colors.$red;
|
||||
border-color: colors.$red;
|
||||
}
|
||||
|
||||
&--InProgress {
|
||||
background-color: colors.$green;
|
||||
border-color: colors.$green;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { FlightTransitionStatus } from '@typings/flight/flight-transition';
|
||||
|
||||
@Component({
|
||||
selector: 'flight-status',
|
||||
templateUrl: './flight-status.component.html',
|
||||
styleUrls: ['./flight-status.component.scss'],
|
||||
})
|
||||
export class FlightStatusComponent {
|
||||
@Input() status: FlightTransitionStatus;
|
||||
|
||||
get indicatorClasses() {
|
||||
return {
|
||||
'flight-status__indicator': true,
|
||||
[`flight-status__indicator--${this.status}`]:
|
||||
this.status !== FlightTransitionStatus.EXPECTED,
|
||||
};
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
<div class="transition">
|
||||
<text weight="medium" color="light-gray" size="12" class="transition__title">
|
||||
{{ title | translate }}
|
||||
</text>
|
||||
|
||||
<flight-status class="transition__status" [status]="transition.status"></flight-status>
|
||||
|
||||
<captioned-time-group class="transition__beginning-time" [times]="transition.start" caption="SHARED.TIME-START"></captioned-time-group>
|
||||
|
||||
<captioned-time-group class="transition__finish-time" [times]="transition.end" caption="SHARED.TIME-END"></captioned-time-group>
|
||||
|
||||
<property [wrappable]="true" [ngClass]="gatePropertyClasses" description="{{ 'SHARED.NUMBER-EXIT' | translate }}" *ngIf="gate">
|
||||
<text weight="medium">{{ gate }}</text>
|
||||
</property>
|
||||
|
||||
<property
|
||||
[wrappable]="true"
|
||||
[ngClass]="dispatchPropertyClasses"
|
||||
description="{{ 'SHARED.LANDING-TRANSFER' | translate }}"
|
||||
*ngIf="dispatch"
|
||||
>
|
||||
<text weight="medium">{{ 'DISPATCH.' + dispatch | translate }}</text>
|
||||
</property>
|
||||
|
||||
<property [wrappable]="true" [ngClass]="bagBeltPropertyClasses" *ngIf="bagBelt" description="{{ 'SHARED.BAGBELT' | translate }}">
|
||||
<text weight="medium">{{ bagBelt }}</text>
|
||||
</property>
|
||||
</div>
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
@use 'src/styles/screen';
|
||||
@use 'src/styles/variables' as vars;
|
||||
@use '/src/styles/grid-sizes' as gridSizes;
|
||||
|
||||
@mixin intermediateScreens() {
|
||||
@media (min-width: vars.$media-breakpoint-desktop-min) and (max-width: 1180px) {
|
||||
@content;
|
||||
}
|
||||
|
||||
@media (min-width: vars.$media-breakpoint-small-tablet-min) and (max-width: 880px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@mixin templateColumns($title: gridSizes.$title-column-width) {
|
||||
grid-template-columns:
|
||||
[title] $title
|
||||
[status] gridSizes.$status-width
|
||||
[beginning-time] gridSizes.$time-width
|
||||
[finish-time] minmax(gridSizes.$time-width, 1fr)
|
||||
[first-property] minmax(min-content, gridSizes.$time-group-width)
|
||||
[second-property] minmax(min-content, 160px);
|
||||
}
|
||||
|
||||
.transition {
|
||||
display: grid;
|
||||
grid-gap: gridSizes.$gap-medium;
|
||||
@include templateColumns;
|
||||
|
||||
@include screen.tablets {
|
||||
grid-gap: gridSizes.$gap-small;
|
||||
@include templateColumns(gridSizes.$title-column-width-tablets);
|
||||
}
|
||||
|
||||
&__first-property {
|
||||
grid-area: 1 / 5 / 2 / 6; // fifth column of the first row
|
||||
|
||||
@include intermediateScreens {
|
||||
grid-area: 2 / 2 / 3 / 3; // second column of the second row
|
||||
}
|
||||
}
|
||||
|
||||
&__second-property {
|
||||
grid-area: 1 / 6 / 2 / 7; // sixth column of the first row
|
||||
|
||||
@include intermediateScreens {
|
||||
grid-area: 2 / 3 / 3 / 4; // third column of the second row
|
||||
}
|
||||
}
|
||||
|
||||
&__third-property {
|
||||
grid-area: 2 / 2 / 3 / 4; // second and third columns of the second row
|
||||
|
||||
@include intermediateScreens {
|
||||
grid-area: 2 / 4 / 3 / 6; // fourth and fifth columns of the second row
|
||||
}
|
||||
}
|
||||
|
||||
&__bag-belt.transition__first-property {
|
||||
grid-area: 1 / 5 / 2 / 7; // fifth and sixth columns of the first row
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
@use "src/styles/screen";
|
||||
@use "src/styles/grid-sizes" as gridSizes;
|
||||
|
||||
.transition {
|
||||
@include screen.mobile {
|
||||
grid-gap: gridSizes.$gap-small;
|
||||
grid-template-columns: repeat(2, minmax(gridSizes.$time-width, 1fr));
|
||||
|
||||
&__title {
|
||||
grid-area: 1 / 1 / 2 / 2; // first column of first row
|
||||
}
|
||||
|
||||
&__status {
|
||||
grid-area: 2 / 1 / 3 / 2; // first column of second row
|
||||
}
|
||||
|
||||
&__beginning-time {
|
||||
grid-area: 3 / 1 / 4 / 2; // first column of third row
|
||||
}
|
||||
|
||||
&__finish-time {
|
||||
grid-area: 3 / 2 / 4 / 3; // second column of third row
|
||||
}
|
||||
|
||||
&__first-property {
|
||||
grid-area: 4 / 1 / 5 / 2; // first column of fourth row
|
||||
}
|
||||
|
||||
&__second-property {
|
||||
grid-area: 4 / 2 / 5 / 3; // second column of fourth row
|
||||
}
|
||||
|
||||
&__third-property {
|
||||
grid-area: 5 / 1 / 6 / 3; // first and second columns of fifth row
|
||||
}
|
||||
|
||||
&__bag-belt.transition__first-property {
|
||||
grid-area: 4 / 1 / 5 / 3; // first and second columns of fourth row
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { DispatchType } from '@typings/enums';
|
||||
import { IFlightTransition } from '@typings/flight/flight-transition';
|
||||
|
||||
@Component({
|
||||
selector: 'flight-transition',
|
||||
templateUrl: './flight-transition.component.html',
|
||||
styleUrls: [
|
||||
'./flight-transition.component.layout.scss',
|
||||
'./flight-transition.component.mobile-layout.scss',
|
||||
],
|
||||
})
|
||||
export class FlightTransitionComponent {
|
||||
@Input() transition: IFlightTransition;
|
||||
@Input() title: string;
|
||||
@Input() dispatch: DispatchType;
|
||||
@Input() gate: string;
|
||||
@Input() bagBelt: string;
|
||||
|
||||
get gatePropertyClasses() {
|
||||
return {
|
||||
transition__gate: true,
|
||||
'transition__first-property': !!this.gate,
|
||||
};
|
||||
}
|
||||
|
||||
get dispatchPropertyClasses() {
|
||||
// if there is no gate we should show dispatch in the first property area
|
||||
const isFirstProperty = !this.gate;
|
||||
|
||||
// if there is a gate we should show dispatch in the second property area
|
||||
const isSecondProperty = !!this.gate;
|
||||
|
||||
return {
|
||||
transition__dispatch: true,
|
||||
'transition__first-property': isFirstProperty,
|
||||
'transition__second-property': isSecondProperty && !!this.dispatch,
|
||||
};
|
||||
}
|
||||
|
||||
get bagBeltPropertyClasses() {
|
||||
// if there is no dispatch and gate we should show bag belt in the first property area
|
||||
const isFirstProperty = !this.dispatch && !this.gate;
|
||||
|
||||
// There is dispatch and gate - we should show bag belt in the third property area
|
||||
const isThirdProperty = this.dispatch && this.gate;
|
||||
|
||||
// if there is dispatch or gate we should show bag belt in the second property area
|
||||
const isSecondProperty = !isFirstProperty && !isThirdProperty;
|
||||
|
||||
return {
|
||||
'transition__bag-belt': true,
|
||||
'transition__first-property': isFirstProperty,
|
||||
'transition__second-property': isSecondProperty,
|
||||
'transition__third-property': isThirdProperty && !!this.bagBelt,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { CaptionedTimeGroupComponent } from '@app/modules/components/captioned-time-group/captioned-time-group.component';
|
||||
import { FlightStatusComponent } from '@app/modules/components/flight-status/flight-status.component';
|
||||
import { PropertyComponent } from '@app/modules/components/property/property.component';
|
||||
import { TextComponent } from '@toolkit/text/text.component';
|
||||
import { Meta, moduleMetadata, Story } from '@storybook/angular';
|
||||
import { TooltipModule } from 'primeng/tooltip';
|
||||
import {
|
||||
FlightTransitionStatus,
|
||||
IFlightTransition,
|
||||
} from '../../../../typings/flight/flight-transition';
|
||||
import { TimeGroupComponent } from '../time-group/time-group.component';
|
||||
import { FlightTransitionComponent } from './flight-transition.component';
|
||||
|
||||
export default {
|
||||
title: 'Components/FlightTransition',
|
||||
component: FlightTransitionComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
declarations: [
|
||||
TextComponent,
|
||||
TimeGroupComponent,
|
||||
FlightStatusComponent,
|
||||
CaptionedTimeGroupComponent,
|
||||
PropertyComponent,
|
||||
],
|
||||
imports: [TooltipModule],
|
||||
}),
|
||||
],
|
||||
argTypes: {
|
||||
transition: {
|
||||
control: 'object',
|
||||
},
|
||||
title: {
|
||||
control: 'text',
|
||||
},
|
||||
dispatch: {
|
||||
options: ['Bus', 'Bridge', 'None'],
|
||||
mapping: {
|
||||
Bus: 'Bus',
|
||||
Bridge: 'Bridge',
|
||||
None: undefined,
|
||||
},
|
||||
control: {
|
||||
type: 'select',
|
||||
labels: {
|
||||
Bus: 'Bus',
|
||||
Bridge: 'Bridge',
|
||||
None: 'None',
|
||||
},
|
||||
},
|
||||
},
|
||||
gate: {
|
||||
control: 'text',
|
||||
},
|
||||
bagBelt: {
|
||||
control: 'text',
|
||||
},
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const deboarding: IFlightTransition = {
|
||||
status: FlightTransitionStatus.SPECIFIED,
|
||||
start: {
|
||||
utc: '2022-04-08T17:05:00Z',
|
||||
local: '2022-04-08T20:08:00+03:03',
|
||||
localTime: '20:08',
|
||||
tzOffset: 183,
|
||||
dayChange: {
|
||||
value: 0,
|
||||
title: '',
|
||||
},
|
||||
},
|
||||
end: {
|
||||
utc: '2022-04-08T17:35:00Z',
|
||||
local: '2022-04-08T20:38:00+03:03',
|
||||
localTime: '20:38',
|
||||
tzOffset: 183,
|
||||
dayChange: {
|
||||
value: 0,
|
||||
title: '',
|
||||
},
|
||||
},
|
||||
isActual: true,
|
||||
};
|
||||
|
||||
export const Default: Story<FlightTransitionComponent> = (args) => {
|
||||
return {
|
||||
props: args,
|
||||
};
|
||||
};
|
||||
Default.args = {
|
||||
transition: deboarding,
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
export * from './page-title/page-title.component';
|
||||
export * from './page-tabs/page-tabs.component';
|
||||
export * from '@toolkit/time-selector/time-selector.component';
|
||||
export * from '@components/search-history/search-history.component';
|
||||
export * from '@components/page/breadcrumds/page-breadcrumbs.component';
|
||||
export * from './flight-events/flight-events.component';
|
||||
export * from './share-panel/share-panel.component';
|
||||
export * from './app-version/app-version.component';
|
||||
export * from './page-loader/page-loader.component';
|
||||
export * from './time-note/time-note.component';
|
||||
export * from './app-show-debug/app-show-debug.component';
|
||||
export * from './page-footer-notes/page-footer-notes.component';
|
||||
export * from './day-change-square-legacy/day-change-square-legacy.component';
|
||||
export * from './transfer-inline-extended/transfer-inline-extended.component';
|
||||
@@ -0,0 +1,5 @@
|
||||
<share-button [flight]="flight" [viewType]="viewType"></share-button>
|
||||
<span class="description last-update__description">
|
||||
<span>{{ 'SHARED.LAST-UPDATE' | translate }}:</span>
|
||||
<span class="time"> {{ flight?.lastUpdate | aflDate: 'with-time' }}</span>
|
||||
</span>
|
||||
@@ -0,0 +1,28 @@
|
||||
@use './src/styles/screen';
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
@include screen.gt-mobile() {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@include screen.mobile() {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.last-update__description {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
share-button {
|
||||
@include screen.gt-mobile() {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { getMockPipe } from '@shared/pipes/pipe.mock';
|
||||
import { LastUpdateComponent } from './last-update.component';
|
||||
|
||||
describe('LastUpdateComponent', () => {
|
||||
let component: LastUpdateComponent;
|
||||
let fixture: ComponentFixture<LastUpdateComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
LastUpdateComponent,
|
||||
getMockPipe('translate'),
|
||||
getMockPipe('aflDate'),
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LastUpdateComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import { ViewType } from '@app/shared/enumerators/flight-request-type.enum';
|
||||
import { FlightModel } from '@app/shared/models-legacy';
|
||||
|
||||
@Component({
|
||||
selector: 'last-update',
|
||||
templateUrl: './last-update.component.html',
|
||||
styleUrls: ['./last-update.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class LastUpdateComponent {
|
||||
@Input() viewType: ViewType;
|
||||
@Input() flight: FlightModel;
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<a
|
||||
*ngIf="viewType === ViewType.Onlineboard"
|
||||
href="https://onlineboard.aeroflot.ru?utm_medium=site&utm_campaign=new_onlineboard_version&utm_source=new_onlineboard&utm_content=button"
|
||||
target="_blank"
|
||||
>
|
||||
{{ 'SHARED.GO-TO-PREVIOUS-VERSION' | translate }}
|
||||
</a>
|
||||
|
||||
<a
|
||||
*ngIf="viewType === ViewType.Schedule"
|
||||
href="https://www.aeroflot.ru/old_schedule/schedule?utm_medium=site&utm_campaign=new_onlineboard_version&utm_source=new_schedule&utm_content=button"
|
||||
target="_blank"
|
||||
>
|
||||
{{ 'SHARED.GO-TO-PREVIOUS-VERSION' | translate }}
|
||||
</a>
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
:host {
|
||||
a {
|
||||
display: inline-flex;
|
||||
text-align: center;
|
||||
border: none;
|
||||
background-color: #e2740b;
|
||||
padding: 3px 10px 4px 10px;
|
||||
color: #fff;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
margin-bottom: 10px;
|
||||
height: 25px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { LinkToOldVersionComponent } from './link-to-old-version.component';
|
||||
import { getMockPipe } from '@shared/pipes/pipe.mock';
|
||||
|
||||
describe('LinkToOldVersionComponent', () => {
|
||||
let component: LinkToOldVersionComponent;
|
||||
let fixture: ComponentFixture<LinkToOldVersionComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [LinkToOldVersionComponent, getMockPipe('translate')],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LinkToOldVersionComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { ViewType } from '@app/shared/enumerators/flight-request-type.enum';
|
||||
|
||||
@Component({
|
||||
selector: 'link-to-old-version',
|
||||
templateUrl: './link-to-old-version.component.html',
|
||||
styleUrls: ['./link-to-old-version.component.scss'],
|
||||
})
|
||||
export class LinkToOldVersionComponent {
|
||||
ViewType = ViewType;
|
||||
@Input() viewType: ViewType;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="note">
|
||||
<text size="12" mobileSize="10" color="light-gray" class="note__symbol">
|
||||
{{ 'SHARED.NOTE-SYMBOL' | translate }}
|
||||
</text>
|
||||
<text size="12" mobileSize="10" color="light-gray" class="note__text">
|
||||
<ng-content></ng-content>
|
||||
</text>
|
||||
</div>
|
||||
@@ -0,0 +1,19 @@
|
||||
@use "src/styles/variables" as vars;
|
||||
@use "src/styles/screen";
|
||||
|
||||
.note {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
::ng-deep .text {
|
||||
line-height: 17px !important;
|
||||
}
|
||||
|
||||
&__symbol {
|
||||
margin-right: vars.$space-s;
|
||||
}
|
||||
|
||||
&__text {
|
||||
white-space: pre-line;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'note',
|
||||
templateUrl: './note.component.html',
|
||||
styleUrls: ['./note.component.scss'],
|
||||
})
|
||||
export class NoteComponent {}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
<operator-logo [flight]="flight" [round]="round"></operator-logo>
|
||||
<text size="12" color="light-gray" *ngIf="showModel" [ngClass]="modelClasses">
|
||||
{{ leg?.equipment?.aircraft?.actual?.title || leg?.equipment?.aircraft?.scheduled?.title }}
|
||||
</text>
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
@use './src/styles/variables' as *;
|
||||
@use './src/styles/layouts' as *;
|
||||
@use './src/styles/screen' as *;
|
||||
|
||||
operator-logo-and-model {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@include gt-mobile() {
|
||||
@include v-spacing($space-s);
|
||||
}
|
||||
|
||||
.aircraft-name {
|
||||
&--right {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
&--mobile-right {
|
||||
@include mobile {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
&--mobile-left {
|
||||
@include mobile {
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { OperatorLogoAndModelComponent } from './operator-logo-and-model.component';
|
||||
|
||||
describe('OperatorLogoAndModelComponent', () => {
|
||||
let component: OperatorLogoAndModelComponent;
|
||||
let fixture: ComponentFixture<OperatorLogoAndModelComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ OperatorLogoAndModelComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(OperatorLogoAndModelComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges, ViewEncapsulation } from '@angular/core';
|
||||
import { FlightModel, getFirstLeg, Leg } from '@app/shared/models-legacy';
|
||||
import { IAlign } from '@typings/common/align';
|
||||
|
||||
@Component({
|
||||
selector: 'operator-logo-and-model',
|
||||
templateUrl: './operator-logo-and-model.component.html',
|
||||
styleUrls: ['./operator-logo-and-model.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
})
|
||||
export class OperatorLogoAndModelComponent implements OnChanges {
|
||||
@Input() flight: FlightModel;
|
||||
@Input() showModel = true;
|
||||
@Input() round = false;
|
||||
@Input() modelAlign: IAlign = 'left';
|
||||
leg: Leg;
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (this.flight && 'flight' in changes) {
|
||||
this.leg = getFirstLeg(this.flight);
|
||||
}
|
||||
}
|
||||
|
||||
get modelClasses() {
|
||||
return {
|
||||
'aircraft-name': true,
|
||||
[`aircraft-name--${this.modelAlign}`]: this.modelAlign !== 'left',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="description" *ngIf="caption">{{ 'SHARED.AVIACOMPANY' | translate }}</div>
|
||||
<div
|
||||
data-testid="flight-company-logo"
|
||||
[pTooltip]="operatingBy"
|
||||
tooltipPosition="top"
|
||||
tooltipStyleClass="afl-tooltip"
|
||||
class="company-logo"
|
||||
[ngClass]="classes"
|
||||
>
|
||||
<!--empty-->
|
||||
</div>
|
||||
@@ -0,0 +1,19 @@
|
||||
@use './src/styles/variables' as *;
|
||||
@use './src/styles/layouts' as *;
|
||||
@use './src/styles/screen' as *;
|
||||
|
||||
operator-logo {
|
||||
display: block;
|
||||
& > div.company-logo {
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
background-position: left top;
|
||||
}
|
||||
@include v-spacing($space-s);
|
||||
|
||||
@include mobile() {
|
||||
& > div.description {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { OperatorLogoComponent } from './operator-logo.component';
|
||||
import { LocalizationService } from '@shared/services';
|
||||
|
||||
describe('OperatorLogoComponent', () => {
|
||||
let component: OperatorLogoComponent;
|
||||
let fixture: ComponentFixture<OperatorLogoComponent>;
|
||||
let localization: LocalizationService;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [OperatorLogoComponent],
|
||||
providers: [{ provide: LocalizationService, useFactory: () => localization }]
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(OperatorLogoComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges, ViewEncapsulation } from '@angular/core';
|
||||
import { FlightModel } from '@app/shared/models-legacy';
|
||||
import { Language } from '@shared/enumerators';
|
||||
import { LocalizationService } from '@shared/services';
|
||||
|
||||
@Component({
|
||||
selector: 'operator-logo',
|
||||
templateUrl: './operator-logo.component.html',
|
||||
styleUrls: ['./operator-logo.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
})
|
||||
export class OperatorLogoComponent implements OnChanges {
|
||||
@Input() flight: FlightModel;
|
||||
@Input() round = false;
|
||||
@Input() large = false;
|
||||
@Input() caption = false;
|
||||
operatingBy: string;
|
||||
classes = {
|
||||
round: false,
|
||||
large: false,
|
||||
ru: false,
|
||||
};
|
||||
|
||||
constructor(private localization: LocalizationService) {}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (this.flight && 'flight' in changes) {
|
||||
const { actual, scheduled } = this.flight.operatingBy || {};
|
||||
this.operatingBy = (actual || scheduled)?.replace('/', '');
|
||||
}
|
||||
|
||||
this.classes = this.computeClasses();
|
||||
}
|
||||
|
||||
private computeClasses() {
|
||||
return {
|
||||
round: this.round,
|
||||
large: this.large,
|
||||
ru: this.localization.Language === Language.ru,
|
||||
...this.getLogoClass(this.operatingBy),
|
||||
};
|
||||
}
|
||||
|
||||
private getLogoClass(operatingBy?: string) {
|
||||
return operatingBy
|
||||
? {
|
||||
[`company-logo--${operatingBy}`]: true,
|
||||
}
|
||||
: {};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<section class="page-empty">
|
||||
<div class="page-empty__title">
|
||||
{{ 'SHARED.FLIGHTS-NOT-FOUND' | translate }}
|
||||
</div>
|
||||
<div class="page-empty__text">
|
||||
{{ 'SHARED.FLIGHTS-NOT-FOUND-TEXT' | translate }}
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,42 @@
|
||||
@use 'src/styles/framework' as *;
|
||||
|
||||
.page-empty {
|
||||
padding: 50px 40px 50px 124px;
|
||||
|
||||
background-color: $white;
|
||||
background-image: url('~src/assets/img/icon-not-found.svg');
|
||||
background-position: 40px center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 55px auto;
|
||||
|
||||
@include mobile {
|
||||
padding: 124px 40px 50px 40px;
|
||||
|
||||
background-position: center 40px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin-bottom: $space-s;
|
||||
|
||||
font-size: $font-size-xl;
|
||||
font-weight: $font-bold;
|
||||
color: $text-color;
|
||||
|
||||
@include mobile {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&__text {
|
||||
max-width: 80%;
|
||||
|
||||
font-size: $font-size-m;
|
||||
line-height: 22px;
|
||||
color: $gray;
|
||||
|
||||
@include mobile {
|
||||
text-align: center;
|
||||
max-width: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'page-empty-list',
|
||||
templateUrl: './page-empty-list.component.html',
|
||||
styleUrls: ['./page-empty-list.component.scss'],
|
||||
})
|
||||
export class PageEmptyListComponent {}
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface AirportModel {
|
||||
city_code: string;
|
||||
name: string;
|
||||
code: string;
|
||||
has_afl_flights: boolean;
|
||||
location: { lat: number; lon: number };
|
||||
title: any;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export interface CityModel {
|
||||
code: string;
|
||||
name: string;
|
||||
location: { lat: number; lon: number };
|
||||
title: any;
|
||||
airports: any[];
|
||||
has_afl_flights: boolean;
|
||||
country_code: string;
|
||||
countryName: string;
|
||||
isAirport: boolean;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user